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

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

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

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

configure() выбрасывает исключение, если переданные аргументы невалидны. Python-биндинг выбрасывает ValueError; TypeScript-биндинг бросает RangeError (для уровня вне диапазона) или общий Error для неподдерживаемых типов; Go-биндинг завершается panic из конструкторов опций 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 изолируется — проблемный приёмник проглатывает исключение, а остальные продолжают доставку. Запись не повторяется на уровне логгера.
  • Отбрасывания при переполнении буфера (async-приёмники Phase 2) учитываются в счётчике dropped_total{sink_id}; приложение исключения не видит.

Python-биндинг ловит каждое исключение внутри веерной рассылки _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Полное квалифицированное имя типа исключения ("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 (без маскирования, без сэмплирования) ради минимальной гарантии доставки.

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

Ошибки shutdown

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

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

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

См. также