Menu

slog и log в Golang: структурированное логирование в Go

Как логировать в Go: классический пакет log с его флагами и log.Fatal и log/slog (Go 1.21) для структурированных логов с уровнями, атрибутами ключ-значение, текстовыми и JSON-обработчиками и логгерами, которые несут контекст через With.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

slog в одном примере

log/slog (Go 1.21) пишет структурированные записи: сообщение, уровень и атрибуты ключ-значение.

Каждая строка выходит в виде time=... level=INFO msg="user logged in" user=ada attempts=1. Поскольку каждое значение это отдельное поле, сборщик логов (Loki, Elasticsearch, CloudWatch, Datadog) может фильтровать по user=ada или level=ERROR без регулярных выражений по свободному тексту.

Примеры на этой странице пишут в os.Stdout, чтобы вывод шёл по порядку. В реальном сервисе логи обычно идут в os.Stderr, куда пишет и логгер по умолчанию.

Уровни

УровеньЗначениеДля чего
slog.LevelDebug-4подробности для разработчиков, в продакшене выключены
slog.LevelInfo0обычные события: запуск, обслуженный запрос, завершённое задание
slog.LevelWarn4что-то неожиданное, с чем программа справилась
slog.LevelError8операция завершилась ошибкой

Обработчик отбрасывает записи ниже своего минимального уровня, а минимум по умолчанию Info. Поэтому slog.Debug(...) ничего не печатает, пока вы не настроите обработчик с Level: slog.LevelDebug. Промежутки между значениями оставляют место для своих уровней вроде slog.Level(2).

Чтобы менять уровень во время работы (по флагу, через админский эндпоинт или по сигналу), положите в опции slog.LevelVar и позже вызывайте у него Set:

var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written

Текст или JSON

slog.NewTextHandler пишет пары key=value, которые легко читать в терминале. slog.NewJSONHandler пишет по одному JSON-объекту на строку, и именно такой формат ожидает большинство конвейеров логов. Вызовы логирования остаются теми же, меняется только обработчик.

Значения сохраняют свои типы: status в JSON-выводе это число, а retry булево значение, time.Duration печатается как 42ms в тексте и в наносекундах в JSON, а error печатает своё сообщение. ReplaceAttr это хук для переписывания или удаления атрибутов; здесь он убирает время, а на практике им переименовывают ключи (msg в message) или маскируют значения.

Атрибуты

Слаботипизированная форма чередует ключи и значения: "user", "ada", "attempts", 3. Она короткая, и у неё один вид сбоя: нечётное число аргументов. Лишнее значение логируется под ключом !BADKEY. go vet это ловит:

./main.go:14:2: call to slog.Info missing a final value

Для типобезопасности и чуть меньшего числа аллокаций используйте конструкторы атрибутов, а в горячем коде LogAttrs:

logger.Info("order placed",
	slog.Int("order_id", 1017),
	slog.String("currency", "EUR"),
	slog.Float64("total", 59.90),
	slog.Duration("took", elapsed),
)

logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))

Держите одно соглашение об именах ключей во всём коде (user_id везде, а не userID в одном пакете и uid в другом). От этого зависят запросы в вашей системе логов.

With: логгеры, которые несут контекст

logger.With(attrs...) возвращает новый логгер, который добавляет эти атрибуты к каждой записи. Создавайте такой на каждый запрос или задание, и все строки, которые он пишет, можно будет связать между собой:

Каждая строка несёт service, version, request_id и user, и повторять их в каждом вызове не нужно. slog.Group вкладывает атрибуты: JSON-обработчик пишет их вложенным объектом ("payment":{"amount":25,"currency":"USD"}), а текстовый ключами через точку (payment.amount=25). logger.WithGroup("db") помещает все последующие атрибуты этого логгера в группу.

Передавайте логгер уровня запроса вниз параметром или полем структуры. Хранить его в context.Context можно, но это прячет зависимость; методы slog вида InfoContext(ctx, ...) передают контекст обработчику, и собственный обработчик может достать из него trace ID.

Скрытие секретов через LogValuer

Тип может сам определять, как его логируют, реализовав slog.LogValuer. Так пароли и токены не попадают в логи, кто бы ни логировал значение:

User логирует только свой ID и email, а Token, залогированный отдельно, печатает REDACTED. Обработчик вызывает LogValue, только когда запись действительно пишется, так что это подходит и для значений, которые дорого вычислять.

Классический пакет log

log появился раньше slog и по-прежнему хорош для маленьких программ и скриптов. Он пишет строки в стандартный поток ошибок с префиксом из даты и времени:

ФлагДобавляет
log.LstdFlags (по умолчанию)дату и время 2009/11/10 23:00:00
log.Lmicrosecondsмикросекунды ко времени
log.LUTCвремя в UTC
log.Lshortfile / log.Llongfilemain.go:14 / полный путь
log.Lmsgprefixставит префикс перед сообщением, а не в начало строки

Три функции завершают программу или паникуют, и разница важна:

  • log.Fatal, log.Fatalf, log.Fatalln печатают, а затем вызывают os.Exit(1). Отложенные вызовы не выполняются. Используйте их в main для сбоев при запуске и никогда в коде библиотек или обработчиках запросов.
  • log.Panic и родственные печатают, а затем паникуют, так что отложенные вызовы выполняются, и панику можно перехватить.
  • Все остальные функции просто пишут строку.

Чтобы логировать в файл, откройте его и передайте в log.New или log.SetOutput; io.MultiWriter(os.Stderr, f) пишет в оба места.

log и slog вместе

slog.SetDefault(logger) делает logger логгером по умолчанию для функций верхнего уровня вроде slog.Info, а заодно направляет через него вывод пакета log. Существующие вызовы log.Printf в вашем коде или зависимостях тогда выходят структурированными записями уровня Info:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}

До SetDefault логгер slog по умолчанию пишет через пакет log, поэтому голый slog.Info("hi") печатает 2026/09/23 14:30:00 INFO hi.

Практические правила

  • Логируйте ошибку или возвращайте её, но не то и другое. Если функция логирует ошибку и возвращает её, один и тот же сбой логируется на каждом уровне стека вызовов. Возвращайте ошибки наверх с контекстом и логируйте один раз там, где их обрабатывают.
  • Изменяемые данные кладите в атрибуты, а не в сообщение. logger.Info("user created", "user_id", id) хорошо группируется в системе логов; logger.Info(fmt.Sprintf("user %d created", id)) создаёт отдельное сообщение для каждого пользователя.
  • Никогда не логируйте секреты и полные тела запросов. Маскируйте через LogValuer или ReplaceAttr.
  • JSON в продакшене, текст при разработке. Выбирайте обработчик при запуске по флагу или переменной окружения.
  • Выбирайте уровни осознанно. Если всё логируется уровнем Error, оповещения об ошибках превращаются в шум.

Часто задаваемые вопросы

Что такое slog в Go?

log/slog это пакет структурированного логирования, добавленный в стандартную библиотеку в Go 1.21. Вместо форматированных строк у каждой записи есть сообщение, уровень (Debug, Info, Warn, Error) и атрибуты ключ-значение, а обработчик пишет её как текст key=value или как JSON: slog.Info("login", "user", "ada", "attempts", 3).

Как включить debug-логи в slog?

Минимальный уровень по умолчанию Info, поэтому slog.Debug ничего не печатает. Создайте обработчик с уровнем ниже и сделайте его логгером по умолчанию: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Если уровень нужно менять во время работы программы, используйте slog.LevelVar вместо константы.

Чем log отличается от slog в Go?

log пишет строки в свободной форме с необязательным префиксом времени, и уровней у него нет. slog пишет записи с уровнями и типизированными атрибутами ключ-значение, которые сборщики логов умеют разбирать и фильтровать. Оба пакета в стандартной библиотеке; slog.SetDefault ещё и перенаправляет вывод пакета log через обработчик slog.

Выполняет ли log.Fatal отложенные функции?

Нет. log.Fatal и log.Fatalf печатают сообщение и вызывают os.Exit(1), который пропускает все отложенные вызовы. Используйте их только в main или коде настройки, где нечего очищать. log.Panic вместо этого паникует, так что отложенные вызовы выполняются.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ