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

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

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

Урок 17 из 25 ~18 мин +50 XP

Мост с slog: Adapter

Go 1.21 принёс в стандартную библиотеку slog — структурный логгер с интерфейсом slog.Handler, и часть зависимостей теперь принимает именно его. Ваше приложение при этом живёт на zerolog, а две системы логирования в одном процессе — это две ленты, два формата и два переключателя. В этом уроке строится мост: slog.Handler, который пишет через zerolog.Event, со словарём уровней в обе стороны, разбором attrs и групп и честной ценой слоя. Про официальный адаптер скажем сразу и прямо: в версии v1.34.0 его ещё нет, а с релиза v1.35.0 (март 2026) конструктор NewSlogHandler уже в пакете. Собственный мост из сорока строк работает на любой версии и заодно показывает, как slog устроен внутри.

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

До Go 1.21 вопрос «чем логировать» решался выбором библиотеки, и zerolog был одним из ответов. С появлением slog у экосистемы появился общий язык: библиотека больше не обязана выбирать логгер за вас — она принимает slog.Handler или slog.Logger и пишет через него. Ваше приложение от этого не перестало быть на zerolog, и здесь два пути: выбросить быстрый JSON-логгер и переписать всё под slog либо построить мост. Мост — это реализация slog.Handler, которая собирает записи в zerolog.Event и уезжает в ту же ленту, тот же writer и тот же порог, что и остальной сервис. Урок идёт по порядку: что такое slog.Handler и зачем он нужен, что с адаптером в поставке zerolog, словарь уровней между системами, собственный мост на сорок строк и правила, при которых мост помогает, а не множит сущности.

Slog в Go 1.21: зачем мост#

Slog спроектирован как фасад: slog.Logger владеет контрактом, а байты не пишет — за вывод отвечает slog.Handler, интерфейс из четырёх методов. Стандартная поставка даёт текстовый и JSON-хендлеры, и на них мир не кончается: любая библиотека, принимающая slog.Handler, подставит ваш бэкенд. Мост нужен в двух пересечениях. Первое: приложение на zerolog, а зависимость требует slog.Logger — без моста зависимость заведёт собственную ленту со своим порогом и своим форматом, и вы получите два диска, два сборщика и две истории одного инцидента. Второе: команда мигрирует — новые пакеты пишут через slog, старые через zerolog.Logger, и мост держит единый вывод на время переезда. Курс по slog разбирает фасад подробно; здесь — только сторона zerolog. Обратная ситуация, когда код ждёт zerolog.Logger, а бэкенд хочется подменить, мостом из коробки не решается: zerolog.Logger — это структура, а не интерфейс, и гибкость там достигается писателем, а не адаптером. Поэтому направление у моста одно: записи slog уезжают в zerolog.

Официальный адаптер: с какой версии#

Короткий ответ по исходникам: в zerolog v1.34.0 адаптера нет — файла slog.go в архиве релиза не существует. Мост принят в репозиторий после v1.34.0 (PR 755, закрывает issue 571) и вышел в релизе v1.35.0 27 марта 2026: тип SlogHandler и конструктор NewSlogHandler(logger zerolog.Logger) *SlogHandler. Как он работает по исходникам: Enabled зеркалит проверки порогов should — глобальный и порог копии — но без семплера; Handle переводит уровень по словарю, прикладывает контекст через event.Ctx, добавляет attrs из WithAttrs, потом attrs записи; timestamp берётся из record.Time, но только если у логгера нет своего timestampHook — иначе ключ time задвоился бы; WithAttrs возвращает копию хендлера; WithGroup собирает префикс через точки. Словарь типов при этом бережный: ошибка в attr уходит через AnErr, время и длительность — родными методами, []byte — через Bytes, всё прочее — Interface. Если ваша версия zerolog от v1.35.0, живите на NewSlogHandler; если старее — мост из следующего раздела, и по интерфейсу они interchangeable: замена конструктора — единственная правка.

Мост из пакета: zerolog от v1.35.0

GO
// zerolog v1.35.0 и новее: адаптер уже в пакете
zl := zerolog.New(os.Stdout).With().Timestamp().Logger()

slog.SetDefault(slog.New(zerolog.NewSlogHandler(zl)))

slog.Info("service started", "port", 8080)
slog.Warn("disk almost full", "percent", 93)

req := slog.Default().WithGroup("http")
req.Info("request handled", "method", "GET", "status", 200)

SetDefault делает мост дефолтным логгером: и ваши вызовы, и зависимости, зовущие пакетные slog.Info, уезжают в zerolog. Группа http превращает attrs в ключи http.method и http.status — плоские имена с точками, а не вложенный объект. Порог держит zerolog: SetGlobalLevel действует и на мостовые записи, потому что Enabled сверяется с его порогами.

Свой мост: slog.Handler на zerolog.Event#

Интерфейс slog.Handler — четыре метода: Enabled(ctx, slog.Level) bool, Handle(ctx, slog.Record) error, WithAttrs([]slog.Attr) Handler и WithGroup(name) Handler. Record — уже собранная запись: поля Time, Level, Message, PC и метод Attrs(func(slog.Attr) bool) — attrs читаются колбэком, потому что slog хранит их с ленивым разворачиванием. Attr — пара Key string и Value; у Value есть Kind() и константы KindString, KindInt64, KindBool, KindFloat64, KindDuration, KindTime, KindGroup, KindAny, а Resolve() разворачивает LogValuer — значение, вычисляемое в момент записи. Уровни slog — числа с шагом четыре: Debug это -4, Info 0, Warn 4, Error 8, а между ними разрешены кастомные вроде LevelInfo+2. Словарь в zerolog собирается порогами из урока про уровни: всё ниже Debug — в Trace, ниже Info — Debug, ниже Warn — Info, ниже Error — Warn, остальное — Error. Enabled обязан повторить проверки порогов zerolog, иначе slog будет собирать записи, которые тут же умрут; Handle при отключённом событии получает nil от WithLevel и молча выходит. Полная реализация ниже и она честно скучная: словарь типов плюс словарь уровней.

ZBridge: мост на любой версии zerolog

GO
import (
        "context"
        "log/slog"

        "github.com/rs/zerolog"
)

type ZBridge struct {
        l     zerolog.Logger
        attrs []slog.Attr
}

func NewZBridge(l zerolog.Logger) *ZBridge { return &ZBridge{l: l} }

func zLevel(lvl slog.Level) zerolog.Level {
        switch {
        case lvl < slog.LevelDebug:
                return zerolog.TraceLevel
        case lvl < slog.LevelInfo:
                return zerolog.DebugLevel
        case lvl < slog.LevelWarn:
                return zerolog.InfoLevel
        case lvl < slog.LevelError:
                return zerolog.WarnLevel
        default:
                return zerolog.ErrorLevel
        }
}

func put(e *zerolog.Event, a slog.Attr) *zerolog.Event {
        v := a.Value.Resolve()
        switch v.Kind() {
        case slog.KindString:
                return e.Str(a.Key, v.String())
        case slog.KindInt64:
                return e.Int64(a.Key, v.Int64())
        case slog.KindBool:
                return e.Bool(a.Key, v.Bool())
        case slog.KindFloat64:
                return e.Float64(a.Key, v.Float64())
        case slog.KindDuration:
                return e.Dur(a.Key, v.Duration())
        case slog.KindTime:
                return e.Time(a.Key, v.Time())
        default:
                return e.Interface(a.Key, v.Any())
        }
}

func (b *ZBridge) Enabled(_ context.Context, lvl slog.Level) bool {
        z := zLevel(lvl)
        return z >= zerolog.GlobalLevel() && z >= b.l.GetLevel()
}

func (b *ZBridge) Handle(_ context.Context, r slog.Record) error {
        e := b.l.WithLevel(zLevel(r.Level))
        if e == nil {
                return nil
        }
        for _, a := range b.attrs {
                e = put(e, a)
        }
        r.Attrs(func(a slog.Attr) bool {
                e = put(e, a)
                return true
        })
        e.Msg(r.Message)
        return nil
}

func (b *ZBridge) WithAttrs(attrs []slog.Attr) slog.Handler {
        nb := &ZBridge{l: b.l}
        nb.attrs = append(append(make([]slog.Attr, 0, len(b.attrs)+len(attrs)), b.attrs...), attrs...)
        return nb
}

func (b *ZBridge) WithGroup(string) slog.Handler { return b }

Enabled повторяет проверку should: глобальный порог и порог копии, без семплера — иначе slog соберёт запись, которую zerolog потом выбросит. WithLevel возвращает nil-событие для отключённого уровня, и Handle тихо выходит: контракт slog требует ошибку только при реальном сбое. Resolve разворачивает LogValuer до обычного Value. KindAny с error здесь уходит в Interface — в прод-версии добавьте case для error и зовите AnErr, чтобы сохранить Err-обработку. WithAttrs копирует слайс: производные хендлеры не портят родителя. WithGroup без префикса — осознанное упрощение: группы остаются плоскими ключами.

Мост — слой конвертации, и его цена складывается из трёх частей. Первая — формат: записи уезжают в JSON zerolog, поэтому ключи и время совпадают с остальной лентой; в ZBridge время даёт With().Timestamp() логгера, а record.Time игнорируется. Вторая — вычисления: attrs хранятся ленивыми, и разворачивать LogValuer обязан хендлер, поэтому Resolve срабатывает в Handle после Enabled. Дорогой attr, обёрнутый в LogValuer, посчитается на каждом прошедшем порог вызове — и семплер zerolog на этом пути не спасает, см. живое место ниже. Третья — уровни: из slog невозможно достать Fatal или Panic, у фасада таких уровней нет, и словарь сводит всё выше Error в ErrorLevel; Fatal-записи остаются эксклюзивом zerolog-кода. Отсюда мост односторонний и по семантике: пропускная способность есть, а часть событий zerolog через slog не выразить. Обратное направление — zerolog-код под slog-бэкендом — пакетом не поставляется и руками не окупается: если нужен единый вывод, проще оставить zerolog бэкендом и кормить его через мост.

Словарь уровней в обе стороны#

slogЧислоzerologКомментарий
ниже LevelDebug, например LevelDebug-4-8TraceLevelкастомная сверх-отладка оседает в trace
LevelDebug-4DebugLevelштатная отладка
LevelInfo0InfoLevelхроника сервиса
LevelInfo+1 до LevelWarn-11-3InfoLevelкастомные уровни теряют гранулярность
LevelWarn4WarnLevelдеградации и ретраи
LevelError и выше8ErrorLevelвсё, что slog зовёт ошибкой
Fatal и Panic zerologнет мест—обратный словарь: LevelError+4 и +8 как кастомные

Когда мост нужен, а когда мешает#

Мост — инструмент стыка, а не самоцель. Он нужен, если зависимость требует slog.Logger, а вывод должен остаться одним, или если команда переезжает между логгерами — на время переезда он дешевле двух лент. Он мешает, когда живёт постоянно на горячем пути: каждый вызов платит за словарь уровней, разбор kinds и копирование attrs, и это заметно на частотных Debug-потоках — цена слоя против наносекунд прямого zerolog измерялась в уроке про производительность. Правило выбора: новый код в проекте на zerolog зовёт zerolog.Logger напрямую, мост включается там, где интерфейс навязан снаружи — зависимостью, фреймворком, стандартной библиотекой. Если в проекте всё на slog и нужен быстрый JSON-бэкенд, мост подключает zerolog как writer, и здесь же вспоминается ConsoleWriter для дев-терминала. Отдельный случай — тесты: мосту всё равно, куда писать, и bytes.Buffer через SyncWriter даёт детерминированный вывод для проверок полей и уровней.

Правила моста одним списком#

  • slog.Handler — четыре метода: Enabled, Handle, WithAttrs, WithGroup; Record приходит собранным
  • словарь уровней: ниже Debug — Trace, ниже Info — Debug, ниже Warn — Info, ниже Error — Warn, остальное — Error
  • Fatal и Panic через slog не проходят: у фасада нет таких уровней
  • Resolve() обязателен: LogValuer разворачивается в Handle, а не при создании записи
  • ошибку в attr ведите через AnErr: Interface потеряет Err-обработку zerolog
  • WithAttrs возвращает копию хендлера: производные не портят родителя
  • WithGroup даёт плоские ключи с точками: request.method, а не вложенный объект
  • Enabled повторяет пороги zerolog без семплера: экономика семплера через мост не переносится
  • в v1.34.0 адаптера нет: ZBridge на любой версии или NewSlogHandler на zerolog v1.35.0+

Проверка: контракт Handle

Что должен вернуть Handle, когда событие ниже порога?

Проверка: словарь уровней

Какой zerolog-уровень получит запись slog с LevelInfo+2?

Какой JSON напечатает мост?

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

zl := zerolog.New(buf)

slog.SetDefault(slog.New(zerolog.NewSlogHandler(zl)))

slog.Info("cache miss", "key", "user:42")

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

Восстановите два вызова: сборка slog-логгера из хендлера и первое событие.

zl := zerolog.New(os.Stdout).With().Timestamp().Logger() slog.SetDefault(slog.(&ZBridge{l: zl})) slog.("service started", "port", 8080) slog.Default().Warn("disk almost full")

Программа с мостом

Расставьте строки в правильном порядке: импорты, zerolog-логгер, мост и запись.

Строки

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

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

Карточки: мост с slog

Термин

slog.Handler

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

Интерфейс из четырёх методов: Enabled, Handle, WithAttrs, WithGroup. Подменяет бэкенд у slog.Logger — основа моста.

1 / 6

Что дальше по курсу#

Мост закрыл стык со стандартной библиотекой — дальше курс идёт в данные записей. Следующий урок — секреты: token, password и Authorization в полях, маскирование и ПДн, что скрывать всегда и как не потерять debugging при этом. Потом — структура JSON: имена ключей, вложенность и сообщения для Loki, где плоские ключи моста пригодятся снова. А если после моста захотелось измерить цену каждого слоя — вернитесь к бенчмаркам и прогоните их с мостом и без него: цифра разницы — аргумент в споре о том, где мосту жить постоянно, а где он костыль на время переезда.

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

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

Вывод

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

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

  • slog.Handler — интерфейс из четырёх методов; мост — хендлер, пишущий через zerolog.Event.
  • В релизе v1.34.0 адаптера нет; официальный NewSlogHandler принят в репозиторий после v1.34.0 и войдёт в ближайший релиз.
  • Словарь уровней: ниже Debug — Trace, ниже Info — Debug, ниже Warn — Info, ниже Error — Warn, остальное — Error.
  • Fatal и Panic через slog недостижимы: у фасада нет таких уровней.
  • WithAttrs возвращает копию хендлера; WithGroup даёт плоские ключи с точками.
  • Resolve() разворачивает LogValuer в Handle — обязанность хендлера, а не slog.
  • Семплер zerolog через Enabled моста не виден: дорогие attrs считаются, запись дропается молча.
  • Мост — инструмент стыка: новый код зовёт zerolog напрямую, мост живёт там, где интерфейс навязан.

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

Соберите ZBridge и прогоните через него пять событий: slog.Info с двумя attrs, slog.Warn с LogValuer-полем, slog.Debug при пороге Info, ошибку через slog.Error и запись с WithGroup. Сравните JSON с ожиданиями словаря уровней и найдите расхождение — группу, которая станет точками. Затем уберите из Enabled проверку GlobalLevel и посмотрите, как slog начинает собирать записи, которые zerolog роняет: верните проверку. Возьмите BurstSampler на zerolog-копию и воспроизведите живое место: Enabled говорит да, лента молчит. Когда адаптер появится в вашей версии zerolog, замените ZBridge на NewSlogHandler и убедитесь, что ни один вызов slog менять не пришлось — ради этого контракт и делали.

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

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

Есть ли в zerolog встроенный slog-адаптер?

В выпущенной версии v1.34.0 нет: файла slog.go в архиве релиза не существует. В репозитории zerolog адаптер принят после v1.34.0 — тип SlogHandler и конструктор NewSlogHandler из PR 755, и он войдёт в ближайший релиз. Проверка проста: если NewSlogHandler компилируется, вы на мосту из пакета; если нет — мост из сорока строк урока работает на любой версии.

Как направить slog-зависимость в zerolog?

Соберите zerolog.Logger как обычно, оберните его в хендлер — zerolog.NewSlogHandler(zl) на свежем zerolog или ZBridge{l: zl} на v1.34.0 — и передайте в slog.New. Если зависимости зовут пакетные slog.Info, поставьте slog.SetDefault: они тоже уедут в zerolog. Порог остаётся на стороне zerolog: SetGlobalLevel действует и на мостовые записи.

Чем мост платит за себя?

Слоем конвертации: словарь уровней, разбор Kind по каждому attr, копирование слайса в WithAttrs. Кастомные уровни slog теряют гранулярность, группы становятся плоскими ключами с точками, а семплер zerolog не участвует в Enabled — дорогие attrs считаются до дропа. Если интерфейс никем не навязан, зовите zerolog.Logger напрямую: быстрее и без сюрпризов.

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

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

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

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

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила