162 lines
5.9 KiB
Python
162 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
from typing import Any
|
|
|
|
from app.core.request_context import get_request_id
|
|
|
|
DEFAULT_SERVICE = "backend"
|
|
DEFAULT_EVENT = "app.log"
|
|
DEFAULT_LOG_LEVEL = os.getenv("PLANET_LOG_LEVEL", "INFO").upper()
|
|
REDACTED = "[REDACTED]"
|
|
SENSITIVE_FIELD_NAMES = {
|
|
"access_token",
|
|
"api_key",
|
|
"authorization",
|
|
"cookie",
|
|
"password",
|
|
"refresh_token",
|
|
"secret",
|
|
"token",
|
|
}
|
|
SENSITIVE_TEXT_PATTERNS = (
|
|
re.compile(r"(?i)(authorization\s*[:=]\s*)(.+)"),
|
|
re.compile(r"(?i)(bearer\s+)([A-Za-z0-9._\-]+)"),
|
|
re.compile(r"(?i)(token\s*[:=]\s*)(.+)"),
|
|
re.compile(r"(?i)(password\s*[:=]\s*)(.+)"),
|
|
re.compile(r"(?i)(cookie\s*[:=]\s*)(.+)"),
|
|
)
|
|
|
|
|
|
def sanitize_log_value(value: Any) -> Any:
|
|
if isinstance(value, Mapping):
|
|
return {
|
|
str(key): (REDACTED if str(key).lower() in SENSITIVE_FIELD_NAMES else sanitize_log_value(item))
|
|
for key, item in value.items()
|
|
}
|
|
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
return [sanitize_log_value(item) for item in value]
|
|
if isinstance(value, str):
|
|
sanitized = value
|
|
for pattern in SENSITIVE_TEXT_PATTERNS:
|
|
sanitized = pattern.sub(lambda match: f"{match.group(1)}{REDACTED}", sanitized)
|
|
return sanitized
|
|
return value
|
|
|
|
|
|
def _normalize_context(context: Any) -> dict[str, Any]:
|
|
if context is None:
|
|
return {}
|
|
if isinstance(context, Mapping):
|
|
sanitized = sanitize_log_value(context)
|
|
return {str(key): value for key, value in sanitized.items()}
|
|
return {"value": sanitize_log_value(context)}
|
|
|
|
|
|
class PlanetContextFilter(logging.Filter):
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
record.request_id = getattr(record, "request_id", None) or get_request_id() or "-"
|
|
record.service = getattr(record, "service", None) or DEFAULT_SERVICE
|
|
record.event = getattr(record, "event", None) or DEFAULT_EVENT
|
|
record.context = _normalize_context(getattr(record, "context", None))
|
|
record.message = sanitize_log_value(record.getMessage())
|
|
return True
|
|
|
|
|
|
class PlanetFormatter(logging.Formatter):
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
timestamp = self.formatTime(record, self.datefmt)
|
|
level = record.levelname
|
|
service = getattr(record, "service", DEFAULT_SERVICE)
|
|
module_name = record.name
|
|
event = getattr(record, "event", DEFAULT_EVENT)
|
|
request_id = getattr(record, "request_id", "-")
|
|
message = sanitize_log_value(record.getMessage())
|
|
context = _normalize_context(getattr(record, "context", None))
|
|
context_suffix = ""
|
|
if context:
|
|
context_suffix = f" context={json.dumps(context, ensure_ascii=False, sort_keys=True)}"
|
|
rendered = (
|
|
f"{timestamp} {level} service={service} module={module_name} "
|
|
f"event={event} request_id={request_id} message={message}{context_suffix}"
|
|
)
|
|
if record.exc_info:
|
|
rendered = f"{rendered}\n{self.formatException(record.exc_info)}"
|
|
return rendered
|
|
|
|
|
|
class PlanetLoggerAdapter(logging.LoggerAdapter):
|
|
def process(self, msg: Any, kwargs: dict[str, Any]) -> tuple[Any, dict[str, Any]]:
|
|
extra = dict(self.extra)
|
|
extra.update(kwargs.get("extra", {}))
|
|
if "context" in extra:
|
|
extra["context"] = _normalize_context(extra.get("context"))
|
|
kwargs["extra"] = extra
|
|
return sanitize_log_value(msg), kwargs
|
|
|
|
def log_event(
|
|
self,
|
|
level: int,
|
|
message: str,
|
|
*,
|
|
event: str,
|
|
context: Mapping[str, Any] | None = None,
|
|
**extra: Any,
|
|
) -> None:
|
|
self.log(level, message, extra={"event": event, "context": context or {}, **extra})
|
|
|
|
def debug_event(self, message: str, *, event: str, context: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
self.log_event(logging.DEBUG, message, event=event, context=context, **extra)
|
|
|
|
def info_event(self, message: str, *, event: str, context: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
self.log_event(logging.INFO, message, event=event, context=context, **extra)
|
|
|
|
def warning_event(self, message: str, *, event: str, context: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
self.log_event(logging.WARNING, message, event=event, context=context, **extra)
|
|
|
|
def error_event(self, message: str, *, event: str, context: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
self.log_event(logging.ERROR, message, event=event, context=context, **extra)
|
|
|
|
def exception_event(
|
|
self,
|
|
message: str,
|
|
*,
|
|
event: str,
|
|
context: Mapping[str, Any] | None = None,
|
|
**extra: Any,
|
|
) -> None:
|
|
self.error(message, exc_info=True, extra={"event": event, "context": context or {}, **extra})
|
|
|
|
|
|
def get_logger(name: str, *, service: str = DEFAULT_SERVICE) -> PlanetLoggerAdapter:
|
|
return PlanetLoggerAdapter(logging.getLogger(name), {"service": service})
|
|
|
|
|
|
def configure_logging(level: str | None = None) -> None:
|
|
root_logger = logging.getLogger()
|
|
if getattr(configure_logging, "_configured", False):
|
|
if level:
|
|
root_logger.setLevel(level.upper())
|
|
return
|
|
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(PlanetFormatter(datefmt="%Y-%m-%d %H:%M:%S"))
|
|
handler.addFilter(PlanetContextFilter())
|
|
|
|
root_logger.handlers.clear()
|
|
root_logger.addHandler(handler)
|
|
root_logger.setLevel((level or DEFAULT_LOG_LEVEL).upper())
|
|
|
|
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
|
|
target_logger = logging.getLogger(logger_name)
|
|
target_logger.handlers.clear()
|
|
target_logger.propagate = True
|
|
|
|
logging.captureWarnings(True)
|
|
configure_logging._configured = True
|