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

Локальные переопределения

Локальное переопределение временно подменяет (или расширяет) приёмники логгера для ограниченной области выполнения. Оно существует ради трёх основных сценариев: захват логов в тестах, запись audit trail на отдельный запуск и включение debug-захвата для одного эндпоинта без правки глобальной конфигурации.

Три операции

Спека (§6.1) определяет три операции над существующим логгером:

ОперацияЭффект
with_sinks([...])Возвращает дочерний логгер, чьи приёмники заменяют родительские.
append_sinks([...])Возвращает дочерний логгер, чьи приёмники — это родительские плюс переданные дополнительные.
without_sinks()Возвращает дочерний логгер без приёмников — отправки отбрасываются.

Каждая операция возвращает дочерний логгер; конфигурация родителя не трогается. Возвращённый логгер не кешируется — вызов Logger.get(name) извне продолжает возвращать исходный экземпляр с его глобальными приёмниками.

from dagstack.logger import Logger, InMemorySink, FileSink

logger = Logger.get("order_service")

# Заменить приёмники — отправки получает только InMemorySink.
test_logger = logger.with_sinks([InMemorySink(capacity=100)])
test_logger.info("captured here")

# Добавить приёмник — отправки получают и родительские, и дополнительный.
audit_logger = logger.append_sinks([FileSink("/var/log/audit.jsonl")])
audit_logger.info("audit event")

# Отбрасывание — отправки уходят в /dev/null.
silent_logger = logger.without_sinks()
silent_logger.info("never seen")

Лексически ограниченная область

Для блочно-ограниченного захвата (тест-кейс, обработчик запроса, паттерн defer) спека определяет scope_sinks — контекст-менеджер, который подменяет приёмники на самом исходном логгере на время блока, а на выходе восстанавливает прежние.

Ключевое отличие от with_sinks: scope_sinks модифицирует self, поэтому любой код, дотягивающийся до того же логгера через Logger.get(name) во время области действия, пишет в подменённые приёмники. Полезно, когда бизнес-логика разрешает логгеры по имени внутри себя и подсунуть альтернативный логгер сверху не получится.

from dagstack.logger import Logger, InMemorySink

logger = Logger.get("order_service")
sink = InMemorySink(capacity=100)

with logger.scope_sinks([sink]):
run_business_logic() # отправки через Logger.get("order_service") попадут в sink
# другие модули, вызывающие Logger.get("order_service") внутри блока,
# тоже отправляют в sink

# Вне блока отправки снова идут в глобальные приёмники.
assert len(sink.records()) > 0

Сценарии

Тесты — InMemorySink для assertion'ов

Самый частый сценарий локального переопределения: захватить записи во время теста и потом проверить их. Полный пример смотри в guide по тестированию.

Audit на отдельный запуск

Оркестратор рабочих процессов может создавать scoped-логгер на каждый запуск, который пишет одновременно в глобальный OTLP-экспортёр (для агрегации между запусками) и в выделенный файловый приёмник (для audit конкретного запуска). На финализации запуска файл закрывается и архивируется вместе с метаданными запуска.

Маскирование на отдельный hook

Plugin-hook обрабатывает чувствительный payload — скажем, webhook от платёжного провайдера. Scoped-логгер оборачивает hook расширенным RedactionProcessor (Phase 2), маскирующим дополнительные поля схемы провайдера, не затрагивая остальное приложение.

Debug-сессия

capture_bodies: true можно включить для одного запроса через debug-заголовок плюс ACL-проверку. Middleware конструирует scoped-логгер с debug-флагом на длительность запроса; другие конкурентные запросы остаются в режиме приватности.

Антипаттерны

  • Долгоживущие scoped-логгеры. Локальное переопределение, переживающее границы операции, — признак того, что на самом деле нужен отдельный именованный логгер (Logger.get(name)) с собственной конфигурацией. Scope должен быть эфемерным.
  • Утечки scope через async-границы. Биндинг гарантирует изоляцию scope только внутри лексического блока. Если ты запускаешь async-задачу внутри блока scope_sinks и даёшь ей завершиться после выхода из блока, её отправки попадут в неправильные приёмники (или упадут на закрытом приёмнике). Используй await / defer / scheduling, который понимает контекст.
  • Мутация приёмников внутри scope. Scope захватывает ссылки на объекты приёмников, а не глубокие копии. Вызов sink.close() на захваченном приёмнике делает его невалидным для любого последующего кода, держащего ту же ссылку.

См. также