feat(core): добавить структурированное логирование на structlog

Конфигурация structlog с JSON-выводом и цепочкой процессоров:
уровень, ISO-таймстамп в UTC, trace_id из contextvar для
сквозной трассировки запроса. Готовый логгер log для импорта
в сервисах. Добавлены поясняющие комментарии и docstring.
This commit is contained in:
Returner_org 2026-06-28 20:35:11 +03:00
parent 5761400b0d
commit 2d18ae7505
3 changed files with 66 additions and 0 deletions

View File

@ -7,6 +7,7 @@ requires-python = ">=3.14"
dependencies = [
"python-ulid>=3.1.0",
"starlette>=1.3.1",
"structlog>=26.1.0",
"typing-extensions>=4.15.0",
]

View File

@ -0,0 +1,54 @@
from typing import MutableMapping, Any
import structlog
from elexam_core.context import trace_id_context
def add_trace_id(event_dict: MutableMapping[str, Any]):
"""
Structlog-процессор: добавляет поле ``trace_id`` в каждую лог-запись.
``trace_id`` читается из ``trace_id_context`` это ``contextvars.ContextVar``,
которая устанавливается middleware при входе каждого HTTP-запроса. Таким образом
все лог-строки одного запроса объединяются одним сквозным идентификатором
это называется distributed tracing / сквозная трассировка.
.. note::
Стандартная сигнатура structlog-процессора три аргумента:
``(logger, method_name, event_dict)``. Здесь сигнатура упрощена до одного
параметра: structlog передаёт ``event_dict`` напрямую, потому что первые два
аргумента нигде в теле функции не нужны.
:param event_dict: ``MutableMapping[str, Any]`` (в терминах structlog ``EventDict``):
словарь с накопленными к данному моменту данными лог-записи (поля, добавленные
предыдущими процессорами и вызывающим кодом). Процессор мутирует его,
добавляя ключ ``"trace_id"``, и обязан вернуть его дальше по цепочке.
: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.JSONRenderer(),
]
)
# Готовый логгер для импорта в любом сервисе проекта.
# Использование: `from elexam_core.logging import log`, затем `log.info("msg", key=val)`.
log = structlog.get_logger()

11
uv.lock generated
View File

@ -21,6 +21,7 @@ source = { editable = "." }
dependencies = [
{ name = "python-ulid" },
{ name = "starlette" },
{ name = "structlog" },
{ name = "typing-extensions" },
]
@ -28,6 +29,7 @@ dependencies = [
requires-dist = [
{ name = "python-ulid", specifier = ">=3.1.0" },
{ name = "starlette", specifier = ">=1.3.1" },
{ name = "structlog", specifier = ">=26.1.0" },
{ name = "typing-extensions", specifier = ">=4.15.0" },
]
@ -61,6 +63,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
]
[[package]]
name = "structlog"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"