Перейти к основному содержимому

Настройка логгера

Логгер не работает, пока не настроен. Вызов configure() (по спеке §9.2) поднимает root-логгер, прикрепляет приёмники, применяет переопределения уровня по логгерам и засевает OTel Resource атрибутами уровня процесса. Запускай его один раз при старте приложения, до того как бизнес-код вызовет Logger.get(name).

Шаг 1. Выбор источника конфигурации

В типичном dagstack-приложении логгер читает свою секцию из app-config.yaml, который парсится dagstack/config. Сам пакет logger-python не зависит от config-python — приложение извлекает секцию logging: из своего Config, дампит в dict и передаёт значения в configure(). Это сохраняет независимость библиотек и избегает циклических зависимостей.

Канонический YAML-блок выглядит так:

app-config.yaml
logging:
level: ${LOG_LEVEL:-INFO}

resource:
service.name: ${SERVICE_NAME:-order-service}
service.version: ${SERVICE_VERSION:-dev}
deployment.environment: ${DAGSTACK_ENV:-development}

loggers:
httpx: WARN
urllib3: WARN
order_service.checkout: DEBUG

sinks:
- type: console
mode: ${LOG_CONSOLE_MODE:-auto}
min_severity: ${LOG_LEVEL:-INFO}
- type: file
path: /var/log/order-service.jsonl
max_bytes: 100000000
keep: 10
min_severity: INFO

Поля выше совпадают с LoggerSchema из спеки §9.2; биндинги эмитят нативную схему (Pydantic, zod, Go-struct), валидирующую секцию до того, как она попадёт в логгер.

Шаг 2. Сборка приёмников из конфига

Преобразуй каждую sink-запись в нативный для биндинга экземпляр приёмника, потом передай список в configure():

from dagstack.logger import ConsoleSink, FileSink, configure


def build_sinks(sink_specs: list[dict]) -> list:
sinks = []
for spec in sink_specs:
kind = spec["type"]
if kind == "console":
sinks.append(ConsoleSink(
mode=spec.get("mode", "auto"),
min_severity=_resolve_severity(spec.get("min_severity", "INFO")),
))
elif kind == "file":
sinks.append(FileSink(
path=spec["path"],
max_bytes=spec.get("max_bytes", 0),
keep=spec.get("keep", 0),
min_severity=_resolve_severity(spec.get("min_severity", "INFO")),
))
else:
raise ValueError(f"unsupported sink type: {kind!r}")
return sinks


def _resolve_severity(value):
# configure() тоже принимает эти строки напрямую; этот хелпер —
# для приёмников, чей конструктор ждёт int.
return {"TRACE": 1, "DEBUG": 5, "INFO": 9, "WARN": 13, "ERROR": 17, "FATAL": 21}[value.upper()]

Шаг 3. Вызов configure() при старте

from dagstack.config import Config
from dagstack.logger import Logger, configure


def bootstrap():
config = Config.load("app-config.yaml")
log_section = config.get("logging", default={})

configure(
root_level=log_section.get("level", "INFO"),
sinks=build_sinks(log_section.get("sinks", [])),
per_logger_levels=log_section.get("loggers", {}),
resource_attributes=log_section.get("resource", {}),
)

# Теперь бизнес-код может разрешать логгеры по имени.
Logger.get("order_service.bootstrap").info("logger configured")


if __name__ == "__main__":
bootstrap()
run_application()

Что делает configure()

По спеке §9.2 вызов:

  1. Резолвит root_level (строка "INFO" или число 9) в severity_number и применяет к root-логгеру.
  2. Заменяет приёмники root-логгера переданным списком. Потомки root наследуют приёмники, если не переопределяют.
  3. Для каждой записи в per_logger_levels применяет переопределение уровня к названному логгеру. Переопределение остаётся в силе даже после создания потомков.
  4. Если resource_attributes непустой, собирает Resource и прикрепляет его к root-логгеру; каждая запись наследует его (если дочерний логгер не задаст собственный Resource).

Вызов идемпотентен — повторный configure() атомарно заменяет предыдущую конфигурацию. In-flight записи, отправляемые другими потоками, дописываются в старую конфигурацию до того, как вступят в силу новые приёмники.

Шаг 4. Переопределения по логгерам

Аргумент per_logger_levels гасит шумные сторонние логгеры и поднимает verbosity конкретного модуля:

configure(
root_level="INFO",
sinks=[ConsoleSink(mode="auto")],
per_logger_levels={
"httpx": "WARN",
"urllib3": "WARN",
"order_service.checkout": "DEBUG",
},
resource_attributes={"service.name": "order-service"},
)

Переопределение применяется, даже если Logger.get("order_service.checkout") вызвали уже после configure() — реестр сначала смотрит в карту переопределений, потом уже возвращает кешированный или свежесозданный логгер.

Распространённые ошибки

  • Вызов configure() после первой отправки записи. Записи, отправленные до вызова, уйдут в bootstrap-дефолт (обычный ConsoleSink в pretty-режиме, severity floor INFO). Вызывай configure() первой строкой стартовой функции.
  • Забыли service.name. OTel-бэкенды observability группируют все записи по Resource.service.name; без него твои записи попадут в безымянное ведро. Всегда задавай его через resource_attributes.
  • Разные service.version в двух репликах. Задавай service.version из git SHA сборки или релизного тега, а не из runtime-переменной, расходящейся между репликами.
  • Severity приёмника ниже severity логгера. Логгер применяет собственный фильтр min_severity до fan-out. Если root_level=INFO, а приёмник объявляет min_severity=DEBUG, приёмник всё равно получит только INFO+ записи — DEBUG логгер отбросит заранее.

Шаг 5. Graceful shutdown (рекомендуется)

Регистрируй обработчик atexit / сигнала, который сбрасывает буферы логгера:

import atexit
from dagstack.logger import Logger

@atexit.register
def shutdown_logger():
Logger.get("").flush(timeout=5.0)
Logger.get("").close()

Без graceful shutdown буферизованные записи (Phase 2-приёмники с фоновыми worker'ами) могут потеряться при выходе из процесса. Phase 1-приёмники (ConsoleSink, FileSink, InMemorySink) пишут синхронно, так что окно потерь маленькое, но ненулевое.

См. также