First product
This commit is contained in:
parent
778da9e2fc
commit
4433d7d2c4
157 changed files with 23975 additions and 0 deletions
183
app/logging_config.py
Normal file
183
app/logging_config.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from app.logging_context import get_request_id
|
||||
|
||||
DEFAULT_REDACT_FIELDS = {
|
||||
"authorization",
|
||||
"cookie",
|
||||
"csrf_token",
|
||||
"new_password",
|
||||
"password",
|
||||
"password_hash",
|
||||
"session",
|
||||
"session_secret_key",
|
||||
}
|
||||
|
||||
_BASE_RECORD = logging.makeLogRecord({})
|
||||
_STANDARD_RECORD_FIELDS = set(_BASE_RECORD.__dict__.keys()) | {"message", "asctime"}
|
||||
_NOISY_RECORD_FIELDS = {"color_message"}
|
||||
|
||||
|
||||
def parse_redact_fields(raw: str | None) -> set[str]:
|
||||
fields = {field.strip().lower() for field in (raw or "").split(",") if field.strip()}
|
||||
return DEFAULT_REDACT_FIELDS | fields
|
||||
|
||||
|
||||
def configure_logging(
|
||||
*,
|
||||
level: str,
|
||||
log_format: str,
|
||||
redact_fields: set[str],
|
||||
include_uvicorn_access: bool,
|
||||
) -> None:
|
||||
normalized_level = _normalize_level(level)
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.handlers.clear()
|
||||
root_logger.setLevel(normalized_level)
|
||||
|
||||
handler = logging.StreamHandler(stream=sys.stdout)
|
||||
handler.setLevel(normalized_level)
|
||||
handler.addFilter(RequestContextFilter())
|
||||
|
||||
normalized_format = log_format.strip().lower()
|
||||
if normalized_format == "json":
|
||||
handler.setFormatter(JsonLogFormatter(redact_fields=redact_fields))
|
||||
else:
|
||||
handler.setFormatter(ConsoleLogFormatter(redact_fields=redact_fields))
|
||||
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# Route server/framework logs through our single root handler.
|
||||
for logger_name in ("uvicorn", "uvicorn.error"):
|
||||
framework_logger = logging.getLogger(logger_name)
|
||||
framework_logger.handlers.clear()
|
||||
framework_logger.propagate = True
|
||||
framework_logger.setLevel(normalized_level)
|
||||
|
||||
access_logger = logging.getLogger("uvicorn.access")
|
||||
access_logger.handlers.clear()
|
||||
access_logger.propagate = True
|
||||
access_logger.setLevel(normalized_level if include_uvicorn_access else logging.WARNING)
|
||||
|
||||
|
||||
class RequestContextFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if not getattr(record, "request_id", None):
|
||||
request_id = get_request_id()
|
||||
if request_id:
|
||||
record.request_id = request_id
|
||||
return True
|
||||
|
||||
|
||||
class JsonLogFormatter(logging.Formatter):
|
||||
def __init__(self, *, redact_fields: set[str]) -> None:
|
||||
super().__init__()
|
||||
self._redact_fields = {field.lower() for field in redact_fields}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload: dict[str, Any] = {
|
||||
"timestamp": _format_timestamp(record.created),
|
||||
"level": record.levelname.lower(),
|
||||
"logger": record.name,
|
||||
"event": getattr(record, "event", record.getMessage()),
|
||||
}
|
||||
request_id = getattr(record, "request_id", None)
|
||||
if request_id:
|
||||
payload["request_id"] = request_id
|
||||
payload.update(self._redact_mapping(_extra_fields(record)))
|
||||
if record.exc_info:
|
||||
payload["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(payload, ensure_ascii=True, default=str)
|
||||
|
||||
def _redact_mapping(self, value: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: self._redact_value(key, item) for key, item in value.items()}
|
||||
|
||||
def _redact_value(self, key: str, value: Any) -> Any:
|
||||
if key.lower() in self._redact_fields:
|
||||
return "[REDACTED]"
|
||||
if isinstance(value, dict):
|
||||
return {nested_key: self._redact_value(nested_key, nested_value) for nested_key, nested_value in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [self._redact_value(key, item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
class ConsoleLogFormatter(logging.Formatter):
|
||||
def __init__(self, *, redact_fields: set[str]) -> None:
|
||||
super().__init__()
|
||||
self._json_formatter = JsonLogFormatter(redact_fields=redact_fields)
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload = json.loads(self._json_formatter.format(record))
|
||||
timestamp = payload.get("timestamp", "")
|
||||
level = _short_level(payload.get("level", "info"))
|
||||
logger_name = str(payload.get("logger", "app"))
|
||||
event = str(payload.get("event", ""))
|
||||
|
||||
parts = [timestamp, level, logger_name, event]
|
||||
|
||||
request_id = payload.pop("request_id", None)
|
||||
method = payload.pop("method", None)
|
||||
path = payload.pop("path", None)
|
||||
status_code = payload.pop("status_code", None)
|
||||
duration_ms = payload.pop("duration_ms", None)
|
||||
|
||||
if request_id:
|
||||
parts.append(f"rid={request_id}")
|
||||
if method and path:
|
||||
parts.append(f"{method} {path}")
|
||||
if status_code is not None:
|
||||
parts.append(str(status_code))
|
||||
if duration_ms is not None:
|
||||
parts.append(f"{duration_ms}ms")
|
||||
|
||||
for key in sorted(payload.keys()):
|
||||
if key in {"timestamp", "level", "logger", "event", "exception"}:
|
||||
continue
|
||||
parts.append(f"{key}={payload[key]}")
|
||||
|
||||
if "exception" in payload:
|
||||
parts.append(f"exception={payload['exception']}")
|
||||
|
||||
return " | ".join(str(part) for part in parts if part)
|
||||
|
||||
|
||||
def _extra_fields(record: logging.LogRecord) -> dict[str, Any]:
|
||||
extras: dict[str, Any] = {}
|
||||
for key, value in record.__dict__.items():
|
||||
if key in _STANDARD_RECORD_FIELDS or key.startswith("_"):
|
||||
continue
|
||||
if key in _NOISY_RECORD_FIELDS:
|
||||
continue
|
||||
extras[key] = value
|
||||
return extras
|
||||
|
||||
|
||||
def _normalize_level(level: str) -> int:
|
||||
normalized = level.strip().upper()
|
||||
mapping = logging.getLevelNamesMapping()
|
||||
if normalized not in mapping:
|
||||
return logging.INFO
|
||||
return mapping[normalized]
|
||||
|
||||
|
||||
def _format_timestamp(created_ts: float) -> str:
|
||||
dt = datetime.fromtimestamp(created_ts, tz=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%SZ")
|
||||
|
||||
|
||||
def _short_level(level: str) -> str:
|
||||
mapping = {
|
||||
"debug": "DBG",
|
||||
"info": "INF",
|
||||
"warning": "WRN",
|
||||
"error": "ERR",
|
||||
"critical": "CRT",
|
||||
}
|
||||
return mapping.get(level.lower(), level[:3].upper())
|
||||
Loading…
Add table
Add a link
Reference in a new issue