Модель ошибок
Логгер выводит ошибки на двух границах: на этапе конфигурации (выкидывает) и на этапе 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_sinks | ValueError("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.message | str(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 сбои.
См. также
- Приёмники — что делают
flushиcloseна каждом приёмнике. - Настройка логгера — паттерн graceful shutdown.
- Реализация собственного приёмника — ожидания по обработке ошибок для авторов приёмников.
- ADR-0001 §3.6 / §13 (полный нормативный текст).