fix(logging): correct structlog processor signature and handle exc_info

This commit is contained in:
Returner_org 2026-07-01 03:23:44 +03:00
parent ebdfa07c1f
commit f833a25f90

View File

@ -1,55 +1,34 @@
from typing import MutableMapping, Any
import structlog
from structlog.typing import EventDict
from elexam_core.context import trace_id_context
def add_trace_id(event_dict: EventDict):
def add_trace_id(logger, method_name, event_dict: EventDict) -> EventDict:
"""
Structlog-процессор: добавляет поле ``trace_id`` в каждую лог-запись.
``trace_id`` читается из ``trace_id_context`` это ``contextvars.ContextVar``,
которая устанавливается middleware при входе каждого HTTP-запроса. Таким образом
все лог-строки одного запроса объединяются одним сквозным идентификатором
это называется distributed tracing / сквозная трассировка.
Сигнатура обязана быть ``(logger, method_name, event_dict)`` structlog
вызывает каждый процессор именно с этими тремя позиционными аргументами.
Первые два не используем, но принять их обязаны. ``trace_id`` читается из
``trace_id_context`` (``contextvars.ContextVar``, ставится middleware на входе
запроса), что связывает все строки лога одного запроса единым идентификатором.
.. note::
Стандартная сигнатура structlog-процессора три аргумента:
``(logger, method_name, event_dict)``. Здесь сигнатура упрощена до одного
параметра: structlog передаёт ``event_dict`` напрямую, потому что первые два
аргумента нигде в теле функции не нужны.
:param event_dict: ``EventDict`` (structlog-тип из ``structlog.typing``, эквивалентный ``MutableMapping[str, Any]``):
словарь с накопленными к данному моменту данными лог-записи (поля, добавленные
предыдущими процессорами и вызывающим кодом). Процессор мутирует его,
добавляя ключ ``"trace_id"``, и обязан вернуть его дальше по цепочке.
:return: Тот же объект ``event_dict`` с добавленным полем ``"trace_id"``.
Возврат обязателен structlog передаёт его следующему процессору в цепочке.
:return: тот же ``event_dict`` с добавленным ``trace_id`` возврат обязателен,
structlog передаёт результат следующему процессору в цепочке.
"""
event_dict["trace_id"] = trace_id_context.get()
return event_dict
# Глобальная конфигурация structlog.
# `processors` — это цепочка (pipeline): каждая лог-запись проходит через все
# процессоры строго по порядку. Каждый процессор получает `event_dict` от предыдущего,
# дополняет или преобразует его и передаёт дальше. Последний процессор должен
# вернуть финальную строку или байты — это и есть то, что уйдёт в stdout/файл.
structlog.configure(
processors=[
# 1. Добавляет поле "level" (например, "info", "error") в event_dict.
structlog.processors.add_log_level,
# 2. Добавляет поле "timestamp" в формате ISO 8601, время в UTC.
structlog.processors.TimeStamper(fmt="iso", utc=True),
# 3. Наш процессор: добавляет поле "trace_id" для сквозной трассировки запроса.
add_trace_id,
# 4. Финальный рендер: сериализует весь event_dict в JSON-строку.
# Стоит последним — к этому моменту все поля уже добавлены.
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
]
)
# Готовый логгер для импорта в любом сервисе проекта.
# Использование: `from elexam_core.logging import log`, затем `log.info("msg", key=val)`.
log = structlog.get_logger()