""" Structured logging primitives. Every log record is emitted as a single JSON object so Logstash can ingest it without grok patterns, and every record carries the request id of the HTTP request that produced it, which makes a single user action traceable end to end in Kibana. """ from __future__ import annotations import datetime as dt import json import logging import traceback from contextvars import ContextVar from typing import Any # Propagates the current request id to any code running in the same context, # including background threads started per request. _request_id: ContextVar[str] = ContextVar("request_id", default="-") # Attributes that ``logging`` puts on every record; anything else the caller # passed through ``extra=`` is treated as structured context and serialised. _STANDARD_ATTRS = frozenset( { "args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", "threadName", "taskName", "request_id", } ) # fmt: skip def set_request_id(value: str) -> None: _request_id.set(value) def get_request_id() -> str: return _request_id.get() class RequestIDFilter(logging.Filter): """Attaches the ambient request id to every record.""" def filter(self, record: logging.LogRecord) -> bool: record.request_id = getattr(record, "request_id", None) or get_request_id() return True class JSONFormatter(logging.Formatter): """Renders a ``LogRecord`` as one line of JSON.""" def __init__(self, service: str = "app", environment: str = "development") -> None: super().__init__() self.service = service self.environment = environment def format(self, record: logging.LogRecord) -> str: payload: dict[str, Any] = { "@timestamp": dt.datetime.fromtimestamp(record.created, tz=dt.UTC).isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "service": self.service, "environment": self.environment, "request_id": getattr(record, "request_id", "-"), } if record.exc_info: exc_type, exc_value, exc_tb = record.exc_info payload["error"] = { "type": getattr(exc_type, "__name__", str(exc_type)), "message": str(exc_value), "stack": "".join(traceback.format_exception(exc_type, exc_value, exc_tb))[:8000], } for key, value in record.__dict__.items(): if key not in _STANDARD_ATTRS and not key.startswith("_"): payload[key] = _coerce(value) return json.dumps(payload, ensure_ascii=False, default=str) def _coerce(value: Any) -> Any: """Keep the payload JSON serialisable without blowing up on odd types.""" if isinstance(value, str | int | float | bool | type(None) | list | dict): return value return str(value)