From 2d18ae750535155225e089f3c5aa76c84442c5a8 Mon Sep 17 00:00:00 2001 From: Returner_org Date: Sun, 28 Jun 2026 20:35:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(core):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B0=20structlog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конфигурация structlog с JSON-выводом и цепочкой процессоров: уровень, ISO-таймстамп в UTC, trace_id из contextvar для сквозной трассировки запроса. Готовый логгер log для импорта в сервисах. Добавлены поясняющие комментарии и docstring. --- pyproject.toml | 1 + src/elexam_core/logging.py | 54 ++++++++++++++++++++++++++++++++++++++ uv.lock | 11 ++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/elexam_core/logging.py diff --git a/pyproject.toml b/pyproject.toml index 7cb5f2e..854f8bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/src/elexam_core/logging.py b/src/elexam_core/logging.py new file mode 100644 index 0000000..e3ddec2 --- /dev/null +++ b/src/elexam_core/logging.py @@ -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() \ No newline at end of file diff --git a/uv.lock b/uv.lock index 82549fe..8ab049f 100644 --- a/uv.lock +++ b/uv.lock @@ -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"