Содержание урока · 9 разделов
У каждого процесса на slog есть настройка, которую вы не писали: дефолтный логгер. Пока в main не появился slog.New, все вызовы slog.Info, slog.Warn и slog.Error уходят в него, и это не заглушка, а маршрут через старый пакет log. Отсюда два практических вопроса, которые обычно всплывают при первой стычке с реальным сервисом. Первый: в каком виде выглядит строка, если ничего не настраивать. Второй: как одним движением переключить на свой формат весь сервис, включая код третьих библиотек, который вы не контролируете. Оба ответа сводятся к паре slog.SetDefault и связи пакетов log и slog: новый структурный API пишет через дефолтный логгер, а после SetDefault и старый log пишет через ваш обработчик. В уроке разберём формат по умолчанию и его уровень, механику SetDefault с побочными эффектами, строки, которые успевают уйти до настройки, и мосты для логгеров из log.New.
Дефолтный логгер go: формат и уровень#
Разбор дефолтного логгер go стоит начать с того, что видит терминал при первом же запуске. Вызов slog.Info("кэш прогрет", "entries", 512) на чистом пакете печатает строку вида 2009/11/10 23:00:00 INFO кэш прогрет entries=512. Разбор по частям: дата и время — это флаги пакета log, через который дефолтный обработчик выводит запись; INFO — уровень; дальше сообщение, а за ним атрибуты в стиле text-формата из пятого урока, ключ равно значение. По умолчанию дефолтный обработчик пропускает записи уровня Info и выше: Debug на нём невидим. Это меняет функция slog.SetLogLoggerLevel, появившаяся в Go 1.22: передайте ей slog.LevelDebug, и отладочные записи начнут печататься без единой строки своего кода. Маршрут у записи простой: slog.Info собирает запись и отдаёт её обработчику по умолчанию, тот форматирует строку и отправляет в пакет log, а пакет пишет её в стандартный поток ошибок. Поэтому дежурному, который ищет строки сервиса до настройки, нужен именно stderr, а не stdout — собственный обработчик в main потом перенесёт вывод куда положено.
slog SetDefault: один вызов на весь сервис#
Функция slog SetDefault принимает логгер и делает его общим для пакета: после вызова все slog.Info, slog.Warn, slog.Error и slog.Debug без приёмника идут через обработчик этого логгера. Но у вызова есть побочный эффект, о котором в документации одна строка, а на практике — половина пользы: SetDefault переподключает и пакет log. Если обработчик нового логгера не внутренний дефолтный, функция ставит пакету log адаптер, который превращает обычные строки log.Printf в структурные записи на уровне Info, и сбрасывает флаги пакета в ноль, чтобы дата и имя файла не задваивались со служебными полями обработчика. Итог: легаси-код и библиотеки, которые пишут через log, попадают в ваш JSON или текстовый формат без правки их исходников. Это самый дешёвый способ собрать разнородные строки сервиса в один поток. Обратная сторона: уровень у таких строк один — Info, а текст сообщения становится полем msg целиком, вместе с уже раскрытыми Printf-ом глаголами. Разбирать такой JSON сложнее, чем записи с настоящими атрибутами, поэтому мост для log-строк — мера на время миграции, а не образ жизни.
SetDefault на старте main
GOpackage main
import (
"log"
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
slog.Info("порт открыт", "port", 8080)
log.Println("легаси-строка из старого кода")
}Две строки настройки — и обе записи приедут в stdout одним JSON-объектом: slog.Info с атрибутом port, log.Println текстом в поле msg на уровне INFO. Даты и префиксы пакета log исчезли: SetDefault обнулил флаги, чтобы служебные поля не дублировались с time и level. Замените NewJSONHandler на NewTextHandler — формат сменится без других правок.
Куда уходит строка при каждом вызове#
| Вызов | До SetDefault | После SetDefault с JSONHandler |
|---|---|---|
| slog.Info | stderr, текстовый формат дефолтного обработчика | ваш обработчик: JSON в stdout |
| slog.Debug | скрыт: минимум дефолтного обработчика — Info | решает уровень вашего обработчика |
| log.Println | stderr, формат пакета log с датой | ваш обработчик, уровень Info, текст в msg |
| свой логгер из log.New | его собственный writer | тот же writer: SetDefault его не трогает |
Что видят библиотеки и когда они это видят#
Главный вопрос интеграции: что видят библиотеки, чей код вы не правили. Ответ зависит от того, как они логируют. Библиотека, использующая slog.Info напрямую, при каждом вызове берёт текущий дефолтный логгер — значит, после SetDefault её строки окажутся в вашем формате с полной структурой. Библиотека на log.Printf после SetDefault тоже попадает в ваш обработчик, но текстом на уровне Info. А вот библиотека, которая при старте сохранила собственный логгер, созданный через log.New с отдельным writer, продолжает писать в свой поток: SetDefault меняет логгер пакета, а не чужие экземпляры. Второй важный нюанс — момент. SetDefault работает на будущее: всё, что успело напечататься до вызова, уже ушло в старый вывод и не перепрочитается. Импортируемые пакеты выполняют свои init-функции до main, и если библиотека логирует на инициализации, эти строки пройдут мимо настройки. Отсюда правило, которое стоит сделать рефлексом: SetDefault — первая содержательная строка main, до чтения конфига, до запуска фоновых горутин и до конструирования клиентов, которые могут захотеть залогировать подключение.
Кейс: в сервис добавили структурные логи, а вызов SetDefault поставили после инициализации клиента очереди. Клиент при конструировании проверяет соединение и пишет предупреждение через log.Printf. Ночью очередь мигнула, предупреждение ушло в stderr старым текстовым форматом, а дежурный разбирал JSON-поток: grep по level=warn ничего не дал, потому что у записи уровня не было вовсе. Разбор занял больше времени, чем сама миграция. Лечение простое: SetDefault в начале main, до конструирования любых клиентов и до go-рутин, а обёртки вида log.New с отдельным файлом выписать списком и переосмыслить при миграции.
Уровень дефолтного обработчика
GO// Go 1.22 и новее: поднять минимум у дефолтного обработчика
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Debug("соединение переподключено", "attempt", 3)До этой строки Debug на дефолтном логгере отбрасывался: минимум стоял на Info. SetLogLoggerLevel меняет и порог дефолтного обработчика, и уровень, на котором пакет log пишет после моста. Полезно на отладке локального стенда, когда свой логгер ещё не собран, а Debug-вывод чужой библиотеки нужен целиком.
log и slog после SetDefault: что теряется#
Мост из log в slog удобен, но у него есть цена, и лучше знать её заранее. Теряются: префиксы, которые задавал log.SetPrefix, флаги с датой, файлом и строкой вызова и уровень — его у текстовой строки нет, поэтому всё пишется как Info. Сохраняются: текст сообщения с раскрытыми глаголами форматирования и порядок строк — записи не перемешаются, потому что пакет log сериализует вывод мьютексом, как и до моста. Отдельно про обёртки: если в проекте есть хелпер, который зовёт log.Output(2, msg) ради правильного имени файла в выводе, при переходе на slog вычислять глубину вызова не нужно — Handler сам достанет место вызова через AddSource из двенадцатого урока. А если сервис держит отдельные log.Logger для разных назначений, их мостит slog.NewLogLogger — тема десятого урока; здесь достаточно помнить, что SetDefault перенаправляет только логгер пакета, то есть log.Default().
В браузерной песочнице доступны fmt, strings, strconv, math, sort, errors и time, пакета log/slog там нет — механику формата честно разминают на fmt.Fprintln(os.Stderr, ...) и time.Now().Format: соберите теми же средствами строку вида время LEVEL сообщение key=value. Полные slog.SetDefault и мост из log запускаются локально через go run; интерактивы ниже проверят порядок настройки, формат дефолтного вывода и маршрут log-строк.
Правила дефолтного логгера#
- SetDefault — первая содержательная строка main: всё, что напечатано до неё, уходит в старый вывод.
- После SetDefault строки пакета log идут через ваш обработчик на уровне Info с обнулёнными флагами.
- Свои логгеры из log.New SetDefault не трогает — для них есть slog.NewLogLogger из десятого урока.
- Минимальный уровень дефолтного обработчика меняет slog.SetLogLoggerLevel (Go 1.22).
- До настройки вывод идёт в стандартный поток ошибок: ищите строки сервиса в stderr, а не в stdout.
- Мост для log-строк — мера на время миграции: текст в msg проигрывает настоящим атрибутам.
Три вопроса, которые чаще всего всплывают на ревью такой настройки. Первый: «не потеряем ли строки, если SetDefault сработает неудачно?» — вызов не возвращает ошибку и не паникует: он назначает логгер, а сам логгер уже собран и проверен New-фабрикой. Второй: «что будет, если writer закрыт?» — обработчик пишет в переданный writer, и ошибка записи возвращается туда, куда её отдаёт Writer; это повод не строить молчаливые обёртки, а оставить обычный os.Stdout и собрать вывод на стороне окружения. Третий: «зачем мост, если легаси перепишем через месяц?» — потому что месяц переезда на живом сервисе выглядит как недели смешанного вывода, и без моста половина инцидентов разбиралась бы по двум файлам с разными форматами. На такие вопросы отвечает и таблица выше: маршрут каждой строки зависит от того, кем она написана и успел ли сработать SetDefault.
Связи с другими уроками курса: первый вывод slog.Info показывал формат по умолчанию вживую, пятый урок разбирает форматы, которые SetDefault подставляет всему сервису. Куда писать после настройки — вопрос шестнадцатого урока про stdout и файлы, а мосты для старого пакета log разворачивает десятый урок про миграцию. Если структурные логи понадобились, не дожидаясь конца миграции, посмотрите курс по zap — там та же схема полей и уровней с другим обработчиком. Карта курса — на странице курса.
Проверка: мост для пакета log
После slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) легаси-код вызывает log.Println("готово"). Что окажется в выводе?
Проверка: Debug на дефолтном логгере
Сервис ещё без своего обработчика, а Debug-записи нужны на локальном стенде. Что сделать?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
func main() {
slog.Info("кэш прогрет", "entries", 512)
}Соберите настройку на старте
Допишите: назначить логгер дефолтным, сделать структурную запись и печать из старого пакета.
Порядок настройки в main
Расставьте строки main по порядку выполнения: открыть функцию, создать логгер, назначить его дефолтным, структурная запись, легаси-запись, закрыть функцию.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: дефолтный логгер
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Без настройки slog пишет через дефолтный обработчик: текстовый формат с датой, минимум Info, вывод в stderr.
- slog.SetDefault делает логгер общим и перенаправляет пакет log в его обработчик, сбрасывая флаги.
- log-строки после моста пишутся на уровне Info текстом в msg — это мера на время миграции.
- Свои логгеры из log.New SetDefault не трогает — для них нужен slog.NewLogLogger.
- Минимальный уровень дефолтного обработчика поднимает slog.SetLogLoggerLevel (Go 1.22).
- SetDefault ставят первой содержательной строкой main: init-код и ранние горутины успевают написать до неё.
Домашнее задание
Запустите программу урока локально, затем поменяйте NewJSONHandler на NewTextHandler и убедитесь, что и структурная строка, и легаси-строка сменили формат без других правок. После этого вызовите SetLogLoggerLevel с LevelDebug, добавьте в main пару slog.Debug и проверьте, какие из записей видны, а какие остались скрытыми.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужно ли вызывать SetDefault, если slog используется только в своём коде?
Тогда логгер можно держать в main и передавать по зависимостям — это явнее глобального состояния. SetDefault нужен, когда хочется повлиять на библиотеки: и на те, что зовут slog.Info напрямую, и на те, что пишут через пакет log.
Что произойдёт, если вызвать SetDefault дважды?
Действовать будет последний вызов: и slog.Info без приёмника, и пакет log пойдут через обработчик нового логгера. В тестах этим пользуются — подменяют дефолтный логгер и восстанавливают его через defer, в проде настройку делают один раз в main.
Куда попадают записи после SetDefault с JSONHandler(os.Stdout)?
В стандартный поток вывода процесса, а дальше — куда он ведёт: в Docker-журнал, коллектор или терминал. Сам пакет log до моста писал в stderr, поэтому на переходном этапе строки могут оказаться в двух потоках — ещё один аргумент настроить всё сразу.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Группы: WithGroup и вложенные поля
- With: предзаданные поля и логгер на запрос
- LogValuer: отложенные и «опасные» значения
- Миграция с log на slog: мосты и совместимость
- TextHandler и JSONHandler: выбор формата вывода
- Ключи и атрибуты: Attr, Int, String и Any
- Уровни логов: Debug, Info, Warn и Error
- Первые строки: slog.Info и вывод по умолчанию
- Все курсы по Go
Следующая ступень учебного пути
PostgreSQL в Go: драйвер pgx с нуля до продакшена
Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.
- Подключаться к PostgreSQL через pgx и pgxpool
- Сканировать строки в структуры и справляться с NULL
- Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
- Проводить транзакции и массовые вставки через Batch и COPY