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

Модель ошибок

Логгер выводит ошибки на двух границах: на этапе конфигурации (выкидывает) и на этапе emit (проглатывает + диагностика). Ошибки жизненного цикла приёмников возвращаются через результаты flush() / close().

Ошибки конфигурации

configure() выкидывает исключение, если переданные аргументы невалидны. Биндинг Python выкидывает ValueError; биндинг TypeScript бросает RangeError (для уровня вне диапазона) или общий Error для неподдерживаемых типов; биндинг Go панически завершается из конструкторов опций WithRootLevel / WithPerLoggerLevels, когда имя уровня не разрешается (вызывающие обычно делают recover() на старте).

ТриггерИсключение PythonПочему
severity_number вне [1, 24]ValueError("severity_number {N} not in [1, 24]")Инвариант диапазона OTel
Неизвестное имя уровня (например, "VERBOSE")ValueError("unknown severity name 'VERBOSE'; expected one of [...]")Строки уровней — фиксированный набор
Тип приёмника не поддерживается build_sinksValueError("unsupported sink type: 'foo'")Фабрика — это шлюз приложения

Ошибки на этапе emit

По спеке §3.6 и §13 логгер не выкидывает на emit. Контракт:

  • Невалидный атрибут (тип не из Value) отбрасывается или приводится; остальная запись проходит дальше.
  • Ошибка приёмника во время emit изолируется — проблемный приёмник проглатывает исключение, а остальные продолжают доставку. Запись не повторяется на уровне логгера.
  • Drop'ы при переполнении буфера (Phase 2 async-приёмники) учитываются в счётчике dropped_total{sink_id}; приложение исключения не видит.

Биндинг Python ловит каждое исключение внутри fan-out _emit:

# dagstack/logger-python: src/dagstack/logger/logger.py — набросок production-поведения.
for sink in self.effective_sinks():
try:
sink.emit(record)
except Exception:
# Сбой приёмника изолирован — остальные приёмники продолжают emit.
# Phase 2+: репорт через канал dagstack.logger.internal.
continue

Это гарантирует, что один сломанный приёмник (закрытый файл, неправильно настроенный удалённый эндпоинт) не глушит остальные и не валит вызывающий код.

Логирование с учётом исключения

Используй logger.exception(err, attributes=...), чтобы залогировать ошибку с заполненными OTel-атрибутами exception.*:

try:
process_order(order_id)
except OrderValidationError as err:
logger.exception(err, attributes={"order.id": order_id})

Запись отправляется на уровне ERROR (severity_number = 17) с тремя дополнительными атрибутами:

АтрибутЗначение
exception.typeПолное qualified-имя типа исключения ("OrderValidationError").
exception.messagestr(err).
exception.stacktraceОтформатированный traceback как UTF-8-строка.

Формат совместим с OTel-semconv — бэкенды, понимающие exception.* (Datadog, Honeycomb, Sentry), парсят атрибуты нативно.

Диагностический канал — dagstack.logger.internal

Логгер резервирует именованный логгер dagstack.logger.internal под собственную диагностику: сбои приёмников, переполнение буфера, отброшенные записи, ошибки валидации схемы, ошибки async-callback'ов. Контракт изоляции (спека §7.4):

  • dagstack.logger.internal не наследует приёмники от root по умолчанию — это предотвращает бесконечный цикл, если приёмники root сами сломаны.
  • Биндинг настраивает для него минимальный выделенный приёмник (stderr JSON-lines, прямая запись, без фонового worker'а), чтобы диагностика дошла до оператора, даже когда основной конвейер сломан.
  • Записи в dagstack.logger.internal обходят цепочку LogProcessor (без маскирования, без сэмплирования) ради минимальной гарантии доставки.

Phase 1-имплементация Python-биндинга пока не направляет диагностику через dagstack.logger.internal; паттерн с проглатыванием в _emit просто роняет ошибку. Подключение внутреннего канала — в roadmap v0.2.

Ошибки shutdown

flush(timeout) и close() — best-effort:

  • flush(timeout) обходит каждый эффективный приёмник и вызывает его flush(timeout). Сбой внутри одного flush проглатывается (спека это разрешает); вызов идёт к следующему приёмнику.
  • close() обходит каждый эффективный приёмник и вызывает его close(). Идемпотентен — повторный close() no-op.

Спека определяет более богатую форму FlushResult { success, partial, failed_sinks: [{sink_id, error}] }; Phase 1 Python-биндинга возвращает None из обоих методов. Phase 2 примет структурированный результат, чтобы shutdown-обработчики приложения могли реагировать на per-sink сбои.

См. также