Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 10 из 25 ~18 мин +20 XP

Миграция с log на slog: мосты и совместимость

Переход на slog редко умещается в один релиз, и план migrate log slog держится на мостах: стандартная библиотека позволяет старому пакету log писать через новый обработчик и наоборот. Разберём slog.NewLogLogger для API, требующих log.Logger, что даёт SetDefault легаси-строкам, как меняется Fatalf и почему переписывание по слоям надёжнее разового рывка.

Содержание урока · 9 разделов

В большом проекте на пакет log лежат сотни вызовов: обёртки, утилиты, код третьих библиотек, который вы не контролируете. Разовый рывок «переписать всё за выходные» проваливается по одному сценарию — в середине ветки обнаруживается зависимость, которая требует log.Logger, и ветка замирает на месяцы. Стандартная библиотека предлагает другой путь: оба пакета умеют работать друг через друга, поэтому миграция становится переездом по слоям, где на каждом шаге система собирается, запускается и читаема. Мостов три: SetDefault перенаправляет строки пакета log в ваш обработчик, NewLogLogger выдаёт настоящую log.Logger для чужих API, а аккуратная замена вызовов в собственном коде движется слоями от транспорта к утилитам. В этом уроке — все три моста, таблица соответствия вызовов, ловушки Fatalf и calldepth и чек-лист, по которому миграцию можно признать завершённой.

Переход на slog: мосты между пакетами log и slog#

Первый мост уже знаком по шестому уроку: slog.SetDefault перенаправляет логгер пакета log в ваш обработчик, и все log.Print, log.Printf и log.Println после этого попадают в общий поток на уровне Info, текстом в поле msg. Это мост нулевого дня: один вызов в main — и разнородные строки сервиса уже собираются в одном JSON, ещё до первой правки чужого кода. Второй мост работает в обратную сторону: slog.NewLogLogger(handler, level) возвращает настоящий log.Logger, который пишет записи через заданный обработчик на заданном уровне. Он нужен там, где API требует именно log.Logger: поле ErrorLog у http.Server, конструкторы шаблонов, внешние библиотеки с параметром старого типа. Вы передаёте им мост, и их строки становятся структурными записями уровня Error — без правки самой библиотеки. Третий мост — собственный код: вызовы log в вашем слое заменяются на slog слой за слоем, и это уже не мост, а переезд, у которого ниже будет свой план. Разбор перехода на slog стоит начать с первых двух: они дают результат в первый же день.

ErrorLog у http.Server через NewLogLogger

GO
package main

import (
        "log/slog"
        "net/http"
        "os"
)

func main() {
        h := slog.NewJSONHandler(os.Stdout, nil)
        slog.SetDefault(slog.New(h))

        srv := &http.Server{
                Addr:     ":8080",
                ErrorLog: slog.NewLogLogger(h, slog.LevelError),
        }

        slog.Info("сервер стартует", "addr", srv.Addr)
        log.Fatal(srv.ListenAndServe())

Ошибки HTTP-сервера — осыпавшиеся соединения, проблемы с TLS — пойдут через тот же JSON-обработчик на уровне ERROR, а не текстом в stderr. Строки вашего кода после SetDefault тоже в общем потоке; log.Fatal в конце стоит здесь намеренно — о нём отдельный разговор ниже.

Таблица соответствия вызовов#

Замена вызовов механична, если под рукой есть таблица соответствия. Прямые переезды очевидны: log.Print и log.Println превращаются в slog.Info с одним сообщением, log.Printf — в slog.Info с fmt.Sprint-склейкой или, лучше, с настоящими атрибутами: раз уж переписывается вызов, время разложить fmt.Sprintf("подключение к %s", host) на два поля. Сложнее с парой Fatal: log.Fatalf печатает запись и завершает процесс кодом один, а slog.Error только печатает — выход нужно дописать руками. Обёртки log.New со своим writer переезжают на slog.New с обработчиком, а для чужого API, требующего log.Logger, остаётся NewLogLogger. Отдельно про место вызова: если в проекте есть хелпер с log.Output(2, msg) ради правильного имени файла в выводе, при переезде на slog вычислять глубину не нужно — место вызова достаёт Handler через AddSource. Таблица ниже закрывает большинство кодовых баз; остальное решается по месту с той же логикой.

Вызовы пакета log и их преемники#

Было на logСтало на slogЗамечание
log.Println("старт")slog.Info("старт")прямой переезд без изменений смысла
log.Printf("подключение к %s", host)slog.Info("подключение", "host", host)строку формата раскладывают на атрибуты
log.Fatalf("конфиг: %v", err)slog.Error("конфиг", "err", err); os.Exit(1)выход дописывают руками: Error не завершает процесс
log.New(f, "", 0)slog.New(slog.NewJSONHandler(f, nil))свой writer становится обработчиком
log.Logger в чужом APIslog.NewLogLogger(h, slog.LevelError)мост для http.Server.ErrorLog и подобных

Замена Fatalf на две строки

GO
// было
log.Fatalf("не удалось открыть конфиг: %v", err)

// стало
slog.Error("не удалось открыть конфиг", "err", err)
os.Exit(1)

Главная ловушка миграции: slog.Error только пишет запись, завершения процесса у него нет. Забытый os.Exit оставляет сервис жить с пустым конфигом — кейс ниже ровно про это. Заодно атрибут err стал структурным полем, а не частью строки.

План migrate log slog: переписывание по слоям#

Собственный код переезжает не по файлам, а по слоям, и порядок здесь такой же, как у зависимостей. Сначала транспорт: HTTP-обработчики и middleware — они чаще всего пишут, и именно здесь логгер на запрос из восьмого урока даёт мгновенный прирост. Затем слой логики: бизнес-операции с настоящими атрибутами вместо Printf-склеек. Утилиты и обёртки переезжают последними, потому что на них держится меньше всего контекста. После каждого слоя проект собирается, запускается и логируется целиком: старые строки через мост, новые через обработчик, общий формат один. Готовность проверяется поиском: в собственном коде не остаётся вызовов log, кроме мостов для чужих API, а grep по log.Print по пакетам проекта возвращает пустоту. Про инверсию — когда свою библиотеку требуется отдать наружу с API на log.Logger — снова выручает NewLogLogger: внутри библиотека живёт на slog, наружу отдаёт мост. Такой план не требует заморозки релизов: слои мигрируют между обычными задачами, а продуктовый поток не замечает переезда.

Проверки после миграции#

Финал миграции — не последний правленный файл, а список проверок. Первая: формат — все строки сервиса в одном выводе, и текстовые строки с датой в стиле пакета log больше не появляются ни в stdout, ни в stderr; их ловит простой скрипт по суффиксу формата. Вторая: уровни — записи, переехавшие как Info, размечены заново там, где на самом деле были ошибки; мост пишёт всё на Info, и часть Error-событий легаси-кода первое время живёт на уровне Info — это приемлемо на переходном этапе, но размечается в бэклоге. Третья: Fatal-пути — каждое место с выходом из процесса проверяется битым конфигом и недоступной зависимостью. Четвёртая: тесты из двадцатого урока — захват вывода в память и утверждения по полям работают и для мостов, потому что мост пишет через тот же Handler. Пятая: дашборды — фильтры по полям не опустели после смены путей, как это случалось с группами в седьмом уроке. Этот же чек-лист пригодится позже, когда сервис переедет на прод-настройки из восемнадцатого урока.

Отдельного разговора заслуживают обёртки-фасады, которые встречаются в больших проектах: файл logger.go с функциями LogInfo, LogError и глобальной переменной внутри. При миграции у фасада два пути. Первый — оставить фасад и поменять его внутренности на slog: вызовы по коду не трогаются, но и польза от структурных полей приходит лишь там, где фасад принимал атрибуты; как правило, фасад на log принимает строку, и это тупик переезда. Второй — расформировать фасад и звать slog в точках записи: правок больше, зато атрибуты появляются сразу. Практика показывает: фасад, написанный ради fmt-подобного API, живёт недолго — его раскрывают, как только дело доходит до полей; фасад ради централизованной настройки заменяется конструированием логгера в main и передачей потомков. В обоих случаях помогает правило из восьмого урока: логгер — значение, которое передают, а не функция, которую ищут.

Чек-лист миграции#

  • Day one: SetDefault в начале main — легаси-строки уже в общем формате.
  • Мосты NewLogLogger для API, требующих log.Logger: http.Server.ErrorLog и подобные.
  • Собственный код переезжает слоями: транспорт, логика, утилиты — с работающей сборкой после каждого.
  • Каждый Fatal превращается в slog.Error плюс os.Exit(1): выход дописывают руками.
  • Printf-строки раскладывают на атрибуты, а не переносят склейкой в msg.
  • Финал: grep по log.Print в своём коде пуст, чек-лист проверок пройден.

Связи с другими уроками: шестнадцатый урок про дефолтный логгер разбирает первый мост — SetDefault — со всеми побочными эффектами, второй урок показывает исходный формат, из которого всё переезжает. Формат вывода настраивает пятый урок про TextHandler и JSONHandler, тесты мостов пишет двадцатый урок, а прод-окружение для переезжающего сервиса готовит курс по Viper. Карта курса — на странице курса.

Проверка: NewLogLogger

Зачем нужен slog.NewLogLogger(h, slog.LevelError), если весь код сервиса уже на slog?

Проверка: замена Fatalf

log.Fatalf("конфиг: %v", err) переписали на slog.Error("конфиг", "err", err) и забыли дописать выход. Что случится при битом конфиге?

Что окажется в выводе?

Прочитайте код и выберите, что он выведет:

h := slog.NewJSONHandler(os.Stdout, nil)
slog.SetDefault(slog.New(h))

legacy := slog.NewLogLogger(h, slog.LevelError)
legacy.Print("соединение оборвано")

log.Println("легаси-строка")

Соберите мосты

Допишите: мост для http.Server, вывод через старый пакет и назначение дефолтного логгера.

srv := &http.Server{ Addr: ":8080", ErrorLog: slog.(h, slog.LevelError), } log.(0, "легаси-хелпер пишет строку") slog.(slog.New(h))

Мосты в одной программе

Расставьте строки по порядку: обработчик, логгер, назначение дефолтным, мост для легаси, печать через мост, структурная запись.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: миграция с log

Термин

slog.NewLogLogger

нажмите, чтобы перевернуть
Определение

Возвращает log.Logger поверх обработчика на заданном уровне: мост для API, требующих старый тип.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Три моста миграции: SetDefault для строк пакета log, NewLogLogger для API на log.Logger, замена вызовов слоями.
  • NewLogLogger(handler, level) отдаёт чужому коду настоящий log.Logger, пишущий структурными записями.
  • log.Fatalf превращается в slog.Error плюс os.Exit(1): без выхода сервис продолжит работу с пустым конфигом.
  • Printf-строки раскладывают на атрибуты: склейка через fmt.Sprintf в msg — полшага, а не переезд.
  • Хелперам с log.Output глубина вызова больше не нужна: место вызова достаёт Handler через AddSource.
  • Слои мигрируют от транспорта к утилитам, после каждого проект собирается и логируется целиком.

Домашнее задание

Возьмите небольшой проект на пакете log или скопируйте пример из уроков локально: поставьте SetDefault в начало main, переведите транспортный слой на slog и проверьте, что старые и новые строки пишутся одним форматом. Затем найдите все Fatalf, замените их парой Error плюс os.Exit(1) и устроите тест: битый конфиг должен завершать процесс с кодом 1.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Миграция с log на slog: мосты и совместимость)

Вопросы и ответы по уроку

С чего начать миграцию: с чужих библиотек или со своего кода?

Со своего, а чужие подключаются мостами: SetDefault забирает строки библиотек на пакете log уже в первый день, а для API с параметром log.Logger ставится NewLogLogger. Переписывать чужой код не получится, и не нужно: мост закрывает вопрос вывода, а вы занимаетесь своими слоями.

Что делать со строками, которые после моста пишутся на Info, а по смыслу — ошибки?

На переходном этапе это приемлемо: мост не знает уровня, и всё идёт как Info. Разметку выносят в бэклог и по ходу миграции слоёв переводят значимые строки на slog.Error с настоящими атрибутами; полностью убрать мосты можно, когда свой код и ключевые зависимости переехали.

Стоит ли мигрировать на slog, если в проекте уже zap?

Сравнение логгеров — отдельный разговор, но мосты и там и там похожи: для zap есть zapstd.LogLogger и обёртки, для slog — NewLogLogger. Миграция имеет смысл, когда хочется уйти от зависимости к стандартной библиотеке; если zap уже приносит пользу, переезд не срочный.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

pgx25 уроков~12 чпрактика25 уроков уже доступны

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY