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

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

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

Урок 10 из 25 ~16 мин +50 XP

Сублоггеры: логгер на модуль

Zerolog сублоггеры — это копии базового логгера со своей разметкой: With().Str("module", "auth") даёт логгер модуля, чьи записи подписаны автоматически. Разберём именование модулей, отдельный уровень и writer на сублоггер, передачу логгера в горутины и тест, который не даст конфигурации сломаться молча. Сразу договоримся о слове: сублоггер — не отдельный тип из пакета zerolog, а приём — тот же zerolog.Logger, собранный через With с подписью модуля.

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

Сервис вырос, и теперь в нём десяток подсистем: аутентификация, биллинг, воркеры очередей, интеграции. Пишут они в один лог одним логгером, и каждая запись — вопрос «а кто это сказал?». По тексту сообщения отвечать не выходит: три модуля логируют ret, два — done, а сообщение об ошибке вообще скопировано из соседнего пакета. Разбор инцидента превращается в археологию: дежурный собирает вокруг записи соседние строки, угадывает границы запроса, и если два модуля писали вперемешку — восстанавливает последовательность по времени с точностью до недоразумения. Grep по строке не масштабируется ещё и потому, что сообщения меняются при каждом рефакторинге, а потребители логов — дашборды и алерты — должны работать годами. Решение известно из урока про контекстные поля: общая разметка живёт на логгере, а не в вызовах. Доведём идею до конца: каждой подсистеме — собственный логгер с подписью module, собранный от одного базового, и сервис в логе обретает структуру, которую читают фильтрами, а не глазами. Обещание то же, что давала механика With из прошлого урока: записью управляет конфигурация, а не память разработчика.

Zerolog сублоггеры: With().Str("module")#

Сублоггер в zerolog — это логгер модуля go, собранный от базового одной строкой: auth := base.With().Str("module", "auth").Logger(). Механика та же, что у логгера на запрос из урока про Logger.With: With возвращает накопитель контекста, Str дописывает подпись, Logger финализирует — и на выходе тип-значение zerolog.Logger со всеми свойствами родителя: тем же writer-ом, уровнем и временем, плюс собственный байтовый кусок контекста с module. Каждый вызов auth.Info() подписывает запись модулем автоматически, и код событий не упоминает module ни разу. Дерево продолжается вниз: auth.With().Str("component", "oauth").Logger() даст события с двумя полями сразу — модуль и компонент, — и на любом этаже дерево можно разветвить ещё раз. Важно, что base никуда не девается: он остаётся точкой сборки, от которой отрастают новые ветки, — конструкторы подсистем принимают base параметром и строят себе сублоггер на входе. Логгер модуля — не отдельная сущность, а та же копия логгера с готовой разметкой, и вся сила подхода в том, что разметка задаётся один раз в месте, которое легко найти и проверить.

Два сублоггера от одного base

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

const (
        moduleAuth    = "auth"
        moduleBilling = "billing"
)

func main() {
        base := zerolog.New(os.Stdout).With().Timestamp().Logger()

        auth := base.With().Str("module", moduleAuth).Logger()
        billing := base.With().Str("module", moduleBilling).Logger()

        auth.Info().Str("user", "ann").Msg("signed in")
        billing.Warn().Int("retries", 3).Msg("charge retry")
}

// Output:
// {"level":"info","time":"...","module":"auth","user":"ann","message":"signed in"}
// {"level":"warn","time":"...","module":"billing","retries":3,"message":"charge retry"}

Имена модулей вынесены в константы — литералов в коде логирования нет. Оба сублоггера собраны от одного base: writer и Timestamp унаследованы, а module у каждого свой. В JSON подпись стоит после time и раньше полей события — контекст копии дописывается раньше полей вызова.

Именование: module, константы и реестр#

Ключ module станет координатой в каждой записи сервиса, поэтому конвенцию фиксируют письменно. Имя — существительное из словаря домена: auth, billing, worker, mailer; без версий и имён файлов, потому что переименованный модуль сломает дашборды, настроенные на старое значение. Вложенность раскладывают на соседние ключи — module и component, — а не в один составной auth.oauth: по каждому ключу фильтруется независимо, и графики строятся на любом уровне. Литералы в вызовах запрещают правилом линтера и ревью: имя живёт в константе пакета, а лучше — в одном месте. Когда модулей больше пяти, вместо набора констант заводят реестр: пакет logger с функцией Get(name), которая при первом обращении собирает и кеширует сублоггер от base — подсистемы просят логгер по имени и не знают, как он собран. base в реестре прячут за атомарной ссылкой или sync.Once, а до инициализации Get возвращает запасной логгер с полем module=uninitialized — такие записи в системе сбора видны сразу, и пропущенную инициализацию ловят на первом же запуске, а не по пустому каталогу через неделю. Смысловая нагрузка одна: zerolog сублоггеры работают только при дисциплине имён, потому что их продуктом является не запись, а предсказуемый ключ, по которому записи одного модуля собираются в систему сбора. Сломанный ключ — это тишина на дашборде, и её заметят позже, чем падение теста, поэтому конфигурацию сублоггеров проверяют тестом, как ниже в этом уроке.

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

  • имя модуля — существительное из словаря домена: auth, billing, worker; без версий и имён файлов
  • один ключ на уровень разметки: module для подсистемы, component для вложенности — фильтры по ним независимы
  • литералы в вызовах запрещены: имя живёт константой, а лучше — в реестре логгеров
  • sublogger собирается от base в конструкторе подсистемы: подсистема получает логгер параметром или просит из реестра
  • переименование модуля — миграция: старое значение остаётся в дашбордах и алертах
  • не подписывайте модуль строкой в message — строка не фильтруется, ключ фильтруется
  • новый ключ разметки — это новая схема данных: обсудите его так же серьёзно, как поле API

Уровень и writer на сублоггер#

Раз сублоггер — полноценный логгер, у него можно переопределить и уровень, и приёмник. Метод Level(lvl) возвращает копию со своим порогом: шумный модуль отладочных трасс переводится на Warn одной строкой — auth := base.With().Str("module", moduleAuth).Logger().Level(zerolog.WarnLevel), — и это не глушит соседей: базовый уровень и глобальный SetGlobalLevel из урока про уровни логов работают как раньше, порог копии проверяется своим полем. Метод Output(w) аналогично меняет writer копии: модуль аудита получает отдельный журнал, модуль отладки — файл с темами. Оба метода возвращают новые копии и не трогают родителя — value-семантика, разобранная в уроке про контекст, — поэтому эксперименты с уровнем безопасны: не понравилось, пересобрали. Порог копии без записи проверяет GetLevel(): собрали ветку с Warn — GetLevel вернёт WarnLevel, что дешевле разбора JSON в тестах конфигурации. Практический порядок такой: общий base задаёт writer и время, сублоггеры добавляют разметку, а точечные отклонения уровня или приёмника — уже поверх, на конкретных ветках. Глобальный SetGlobalLevel остаётся общим фильтром поверх всех локальных порогов, и это стоит держать в голове, когда ищете, почему модуль молчит. Если отклонений становится много, это сигнал не разветвлять дерево дальше, а возвращаться к единому приёмнику и фильтрации на стороне системы сбора: чем больше индивидуальных конфигураций, тем труднее ответить, куда вообще попадёт запись.

Горутины и передача по значению#

Сублоггер — тип-значение, и в горутины он уезжает копией: go func(l zerolog.Logger) {...}(authLog). Копия безопасна: writer общий, но писатели zerolog — os.File и stdout — рассчитаны на параллельные записи, а буфер события у каждой записи свой из sync.Pool. Копия логгера дешёвая — структура примерно на сотню байт плюс ссылка на общий writer и уже собранные байты контекста, — так что передача по значению в воркеры и горутины норма, а не расточительство. Никаких мьютексов на логгере держать не нужно, и это осознанный контракт zerolog: один writer на сервис, логгеров сколько угодно. Гонка возможна там, где её создаёт автор кода: перезапись глобальной переменной-логгера во время работы, о чём честно предупреждал урок про первый лог, — сублоггеры эту проблему не создают, если собираются один раз и дальше только читаются. Передавайте в горутину самый размеченный логгер из доступных: воркер очереди получает логгер модуля плюс идентификатор задачи в контексте, и все его записи самодостаточны, сколько бы задач ни крутилось параллельно. Глубокая проверка потокобезопасности — pprof и гонки — отдельная тема, ей посвящён урок про логи в горутинах, а здесь достаточно правила: собрал — раздал — не меняй.

Тест конфигурации сублоггера

GO
func TestAuthSublogger(t *testing.T) {
        var buf bytes.Buffer

        base := zerolog.New(&buf).With().Timestamp().Logger()
        auth := base.With().Str("module", "auth").Logger()

        auth.Info().Msg("signed in")

        var rec map[string]any
        if err := json.Unmarshal(buf.Bytes(), &rec); err != nil {
                t.Fatalf("bad json: %v", err)
        }
        if rec["module"] != "auth" {
                t.Fatalf("module = %v, want auth", rec["module"])
        }
}

Тест собирает тот же сублоггер, но поверх буфера: одна запись, один JSON, разбор в карту и проверка ключа module. Контракт фиксирован: подпись модуля обязана быть в каждой записи, и сломанная сборка контекста падает в тестах, а не тишиной на дашборде. Буфер вместо writer-а — приём из урока про тесты логов.

Тест выше — минимальная дисциплина, которая окупается в первый же рефакторинг: кто-то перепутает порядок With и Level, вынесет Str в переменную с другим значением или случайно соберёт сублоггер от глобального логгера — и тест поймает разницу в JSON раньше прода. Расширяйте его по мере дерева: проверяйте, что поля base на месте, что Level копии не затронула соседей, что реестр возвращает один и тот же сублоггер на повторный Get. Добавьте и обратный тест: собрать соседний сублоггер billing и убедиться, что в его записях module=billing даже когда auth живёт рядом — так ловится класс ошибок, где одна копия логгера переиспользована между двумя модулями. Здесь же уместен вопрос вкуса: не плодите сублоггер на каждый файл. Модуль — единица домена, а не единица компиляции: логгер на пакет ради пакета раздробит разметку, и фильтр по module превратится в угадывание. Мне достаточно трёх-семи веток на сервис; когда счёт идёт на десятки, чаще всего это признак, что разметка замещает нормальную структуру полей события. Различие простое: module отвечает на вопрос «кто пишет», а подробности движения — на вопрос «что происходит» — и это поля события, а не ключи разметки.

Четыре этажа дерева логгеров#

ЛоггерЧто добавляетКому живётТипичный контекст
Глобальный zerolog/logничего: stderr и времяскрипты и первые дниlevel, time
basewriter, уровень, service, envточка сборки сервисаservice, env, version
Сублоггер модуляподпись подсистемыконструкторам подсистемmodule
Логгер на запрос/задачуидентификатор потокаобработчикам и горутинамrequest_id, task_id

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

Сублоггер — прикладной итог темы контекста: в уроке про Logger.With лежит механика With и порядок полей, здесь — дисциплина именования и конфигурация. Дальше по курсу: логирование HTTP покажет сублоггер-промежуточник, который размечает запросы request_id автоматически, а урок про тесты логов развернёт буферный приёмник из этого урока в целую методику проверки записей. Справочная задача на закрепление: соберите сервис с тремя модулями, один из них переведите на Warn и напишите тест, который ловит поломку подписи module. Если тест проходит на правильной конфигурации и падает на сломанной — вы освоили главный навык урока: разметка модулей проверяется машиной, а не глазами дежурного.

Проверка: сборка сублоггера

Как создать сублоггер на модуль auth от базового логгера base?

Проверка: Level на копии

Что сделает auth.Level(zerolog.WarnLevel), где auth — сублоггер от base?

Какой JSON выйдет из сублоггера?

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

base := zerolog.New(os.Stdout).With().Timestamp().Logger()

auth := base.With().Str("module", "auth").Logger()

auth.Info().Str("user", "ann").Msg("signed in")

Что напечатает Debug у сублоггера на Warn?

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

base := zerolog.New(os.Stdout).With().Timestamp().Logger()

auth := base.With().Str("module", "auth").Logger().Level(zerolog.WarnLevel)

auth.Debug().Str("trace", "step-1").Msg("details")

Соберите сублоггер

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

package import ( "os" "github.com/rs/zerolog" ) const moduleAuth = "auth" func main() { base := zerolog.New(os.).With().Timestamp().Logger() auth := base.().Str("module", moduleAuth).() auth.().Str("user", "ann").Msg("signed in") }

Программа с логгером модуля

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

Строки

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

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

Карточки: сублоггеры

Термин

Сублоггер

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

Копия базового логгера с готовой разметкой: base.With().Str("module", "auth").Logger(). Подписывает каждую запись модулем без повторов в коде.

1 / 6

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

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

Вывод

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

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

  • Сублоггер — копия base с подписью: base.With().Str("module", "auth").Logger().
  • Разметка модуля живёт в контексте: в JSON она стоит после time и раньше полей события.
  • Имена модулей — константы или реестр с Get(name); литералы в вызовах запрещены правилом ревью.
  • Level() и Output() на сублоггере возвращают копию: свой порог и writer не трогают base.
  • Сублоггеры собирают после base: пакетная переменная до инициализации уедет с дефолтами в stderr.
  • В горутины логгер передают по значению: writer потокобезопасен, буфер события у записи свой.
  • Тест с bytes.Buffer фиксирует контракт: подпись module есть в каждой записи модуля.
  • Логгер — на модуль домена, а не на каждый файл: разметка «кто пишет», поля события — «что происходит».

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

Соберите сервис с тремя модулями — auth, billing, worker — от одного base с Timestamp. Выведите по событию из каждого модуля и убедитесь по JSON, что подписи разные, а writer общий. Переведите billing на Warn и проверьте, что его debug исчез, а у auth остались. Напишите тест из урока: буфер, одна запись, проверка module — и временно сломайте подпись, чтобы тест упал. Наконец, соберите сублоггер пакетной переменной до main и запишите своими словами, куда уехали его записи.

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

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

Что такое сублоггер в zerolog?

Копия базового логгера с предзаданными полями контекста, чаще всего с подписью модуля: base.With().Str("module", "auth").Logger(). Каждая запись сублоггера подписана автоматически, а уровень и writer можно переопределить на копии методами Level и Output, не трогая base.

Сколько сублоггеров заводить?

По числу подсистем домена: обычно три-семь веток — auth, billing, worker, интеграции. Логгер на каждый файл раздробит разметку, и фильтры станут угадыванием. Если веток оказывается больше десяти, сначала проверьте, не дублирует ли module обычные поля события: чаще всего половина веток нужна лишь для фильтрации по значению, которое логгер мог бы класть в событие. Вложенность раскладывайте соседними ключами — module и component — и не создавайте ключи, по которым никто не ищет.

Как убедиться, что конфигурация сублоггеров не сломается?

Тестом: соберите сублоггер поверх bytes.Buffer, сделайте одну запись, разберите JSON в карту и проверьте ключ module и прочие обязательные поля. Такой тест фиксирует контракт разметки: рефакторинг, который уронит подпись, падает в CI, а не тишины на дашборде через неделю.

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

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

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

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

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

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

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