Настройка логгера
Логгер не работает, пока не настроен. Вызов 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-блок выглядит так:
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():
- Python
- TypeScript
- Go
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()]
import { ConsoleSink, FileSink, type Sink } from "@dagstack/logger";
interface SinkSpec {
type: string;
mode?: "auto" | "json" | "pretty";
path?: string;
max_bytes?: number;
keep?: number;
min_severity?: number | string;
}
const SEVERITY: Record<string, number> = {
TRACE: 1, DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17, FATAL: 21,
};
function resolveSeverity(value: number | string | undefined): number {
if (value === undefined) return 9;
if (typeof value === "number") return value;
return SEVERITY[value.toUpperCase()] ?? 9;
}
export function buildSinks(specs: SinkSpec[]): Sink[] {
return specs.map((spec) => {
if (spec.type === "console") {
return new ConsoleSink({
mode: spec.mode ?? "auto",
minSeverity: resolveSeverity(spec.min_severity),
});
}
if (spec.type === "file") {
if (!spec.path) throw new Error("file sink requires path");
return new FileSink(spec.path, {
maxBytes: spec.max_bytes ?? 0,
keep: spec.keep ?? 0,
minSeverity: resolveSeverity(spec.min_severity),
});
}
throw new Error(`unsupported sink type: ${spec.type}`);
});
}
package bootstrap
import (
"fmt"
"strings"
"go.dagstack.dev/logger"
)
type SinkSpec struct {
Type string
Mode string
Path string
MaxBytes int64
Keep int
MinSeverity string
}
var severityNames = map[string]int{
"TRACE": int(logger.SeverityTrace),
"DEBUG": int(logger.SeverityDebug),
"INFO": int(logger.SeverityInfo),
"WARN": int(logger.SeverityWarn),
"ERROR": int(logger.SeverityError),
"FATAL": int(logger.SeverityFatal),
}
func resolveSeverity(name string) int {
if name == "" {
return int(logger.SeverityInfo)
}
if n, ok := severityNames[strings.ToUpper(name)]; ok {
return n
}
return int(logger.SeverityInfo)
}
func BuildSinks(specs []SinkSpec) ([]logger.Sink, error) {
out := make([]logger.Sink, 0, len(specs))
for _, s := range specs {
switch s.Type {
case "console":
mode := logger.ConsoleAuto
switch s.Mode {
case "json":
mode = logger.ConsoleJSON
case "pretty":
mode = logger.ConsolePretty
}
out = append(out, logger.NewConsoleSink(mode, nil, resolveSeverity(s.MinSeverity)))
case "file":
fs, err := logger.NewFileSink(s.Path, s.MaxBytes, s.Keep, resolveSeverity(s.MinSeverity))
if err != nil {
return nil, fmt.Errorf("file sink %q: %w", s.Path, err)
}
out = append(out, fs)
default:
return nil, fmt.Errorf("unsupported sink type: %q", s.Type)
}
}
return out, nil
}
Шаг 3. Вызов configure() при старте
- Python
- TypeScript
- Go
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()
import { Logger, configure } from "@dagstack/logger";
import { buildSinks } from "./bootstrap";
import { loadConfig } from "./config"; // загрузчик конфига приложения
export async function bootstrap() {
const config = await loadConfig("app-config.yaml");
const log = config.logging ?? {};
configure({
rootLevel: log.level ?? "INFO",
sinks: buildSinks(log.sinks ?? []),
perLoggerLevels: log.loggers ?? {},
resourceAttributes: log.resource ?? {},
});
// Теперь бизнес-код может разрешать логгеры по имени.
Logger.get("order_service.bootstrap").info("logger configured");
}
if (require.main === module) {
bootstrap().then(runApplication);
}
package main
import (
"go.dagstack.dev/logger"
)
func bootstrap() error {
cfg, err := loadConfig("app-config.yaml")
if err != nil {
return err
}
sinks, err := BuildSinks(cfg.Logging.Sinks)
if err != nil {
return err
}
// Собрать карту переопределений по логгерам из конфига.
perLogger := make(map[string]any, len(cfg.Logging.Loggers))
for name, level := range cfg.Logging.Loggers {
perLogger[name] = level
}
logger.Configure(
logger.WithRootLevel(cfg.Logging.Level),
logger.WithSinks(sinks...),
logger.WithPerLoggerLevels(perLogger),
logger.WithResourceAttributes(cfg.Logging.Resource),
)
logger.Get("order_service.bootstrap").Info("logger configured", nil)
return nil
}
Что делает configure()
По спеке §9.2 вызов:
- Резолвит
root_level(строка"INFO"или число9) в severity_number и применяет к root-логгеру. - Заменяет приёмники root-логгера переданным списком. Потомки root наследуют приёмники, если не переопределяют.
- Для каждой записи в
per_logger_levelsприменяет переопределение уровня к названному логгеру. Переопределение остаётся в силе даже после создания потомков. - Если
resource_attributesнепустой, собираетResourceи прикрепляет его к root-логгеру; каждая запись наследует его (если дочерний логгер не задаст собственныйResource).
Вызов идемпотентен — повторный configure() атомарно заменяет предыдущую конфигурацию. In-flight записи, отправляемые другими потоками, дописываются в старую конфигурацию до того, как вступят в силу новые приёмники.
Шаг 4. Переопределения по логгерам
Аргумент per_logger_levels гасит шумные сторонние логгеры и поднимает verbosity конкретного модуля:
- Python
- TypeScript
- Go
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"},
)
configure({
rootLevel: "INFO",
sinks: [new ConsoleSink({ mode: "auto" })],
perLoggerLevels: {
axios: "WARN",
undici: "WARN",
"order_service.checkout": "DEBUG",
},
resourceAttributes: { "service.name": "order-service" },
});
logger.Configure(
logger.WithRootLevel("INFO"),
logger.WithSinks(logger.NewConsoleSink(logger.ConsoleAuto, nil, 1)),
logger.WithPerLoggerLevels(map[string]any{
"net/http": "WARN",
"order_service.checkout": "DEBUG",
}),
logger.WithResourceAttributes(logger.Attrs{"service.name": "order-service"}),
)
Переопределение применяется, даже если Logger.get("order_service.checkout") вызвали уже после configure() — реестр сначала смотрит в карту переопределений, потом уже возвращает кешированный или свежесозданный логгер.
Распространённые ошибки
- Вызов
configure()после первой отправки записи. Записи, отправленные до вызова, уйдут в bootstrap-дефолт (обычныйConsoleSinkв pretty-режиме, severity floorINFO). Вызывай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 / сигнала, который сбрасывает буферы логгера:
- Python
- TypeScript
- Go
import atexit
from dagstack.logger import Logger
@atexit.register
def shutdown_logger():
Logger.get("").flush(timeout=5.0)
Logger.get("").close()
import { Logger } from "@dagstack/logger";
async function shutdownLogger(): Promise<void> {
const root = Logger.get("");
await root.flush(5000);
await root.close();
}
process.on("SIGTERM", async () => {
await shutdownLogger();
process.exit(0);
});
process.on("SIGINT", async () => {
await shutdownLogger();
process.exit(0);
});
package main
import (
"os"
"os/signal"
"syscall"
"go.dagstack.dev/logger"
)
func waitForShutdown() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
<-c
root := logger.Get("")
_, _ = root.Flush(5.0)
_ = root.Close()
}
Без graceful shutdown буферизованные записи (Phase 2-приёмники с фоновыми worker'ами) могут потеряться при выходе из процесса. Phase 1-приёмники (ConsoleSink, FileSink, InMemorySink) пишут синхронно, так что окно потерь маленькое, но ненулевое.
См. также
- Приёмники — что настраивается на каждом приёмнике.
- Уровни — таблица bucket'ов и константы.
- Поля LogRecord — что отправляет настроенный логгер.
- ADR-0001 §9 (полный нормативный текст).