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

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

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

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

Logger.With: контекстные поля

Контекстные поля логов — поля, которые zerolog дописывает к каждому событию автоматически: имя сервиса, модуля, request_id. Метод Logger.With собирает их один раз в zerolog.Context и возвращает дочерний логгер, чтобы не повторять Str() в каждой строке. Разберём цепочки базовый — сублоггер — запрос, порядок полей в JSON и цену With.

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

Начнём с сервиса, в котором контекстных полей логов ещё нет. Каждый вызов пишет только то, что ему передали: log.Info().Str("user", id).Msg(...) — и чтобы идентификатор запроса оказался в записи, его надо не забыть дописать в каждом из двадцати мест, где обработчик логирует. Через месяц в коде тридцать вариантов одного и того же: где-то user_id, где-то uid, где-то поле пропущено вовсе, а на разборе инцидента половина записей блока обработки заказа осталась без order_id — и трек запроса не собирается даже руками. Вторая стадия этой болезни — копипаст-константы: строка "service":"billing" повторяется в каждом вызове, и при переименовании сервиса кто-то обязательно обновит половину вхождений, а вторая половина будет врать в логах молча. Корень проблемы в том, что общие для всего потока поля живут на уровне события, а задаются на уровне вызова. Логичнее объявить их один раз на логгере: вот этот логгер пишет от имени модуля auth, этот — от имени обработки запроса r-1024, и каждое событие уходит уже с нужными полями. Именно это делает With: общие поля переезжают из вызова в конфигурацию логгера, а в коде события остаётся только то, что уникально для конкретной записи.

Logger.With: контекстные поля логов#

В zerolog With() — метод логгера, который возвращает zerolog.Context, накопитель предзаданных полей. Вы цепочкой добавляете в него Str, Int, Bool, Timestamp — что угодно из того же набора, что и у события, — и финализируете вызовом Logger(): на выходе новый логгер, у которого в контексте лежат все выбранные поля. Ключевая деталь механики: контекст хранится уже сериализованным — готовыми JSON-фрагментами вроде "service":"billing" — поэтому на каждое событие zerolog просто дописывает этот готовый кусок в буфер записи: без перечитывания полей, без рефлексии и без аллокаций. Отсюда и порядок в итоговом JSON: уровень, ключ time, если он в контексте, затем поля контекста в порядке их добавления, затем поля самого события, и последним — message: он дописывается при финализации, после всех полей. Контекстное поле легко узнать в записи по позиции — оно стоит раньше всего, что добавлено в цепочке события. Метод работает и от глобального логгера пакета log — log.With().Str("service", "api").Logger() — хотя в приложениях чаще собирают собственный базовый логгер при старте и раздают его по зависимостям, как мы разбирали в уроке про первый лог.

Первый With: service и env на логгере

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

func main() {
        logger := zerolog.New(os.Stdout).With().
                Str("service", "billing").
                Str("env", "prod").
                Logger()

        logger.Info().Str("user", "ann").Msg("signed in")
        logger.Error().Str("op", "charge").Msg("charge failed")
}

// Output:
// {"level":"info","service":"billing","env":"prod","user":"ann","message":"signed in"}
// {"level":"error","service":"billing","env":"prod","op":"charge","message":"charge failed"}

With() собирает zerolog.Context: два строковых поля, затем Logger() возвращает логгер, у которого эти байты уже лежат в контексте. Оба события выводят service и env автоматически, в JSON они стоят после уровня и раньше полей события — ровно в порядке добавления. Ни один вызов Info или Error не повторяет общие поля: они заданы один раз на логгере и попадают в каждую запись, пока живёт эта копия.

zerolog.Context: методы и порядок в JSON#

Набор методов у zerolog.Context почти тот же, что у события: Str и Strs, Int и все целочисленные вплоть до Uint64, Float32 и Float64, Bool, Bytes, Hex, Dur, Time, Err — он, как и у события, молча пропускает nil, — AnErr, Errs, Interface и Any, RawJSON, Dict. Плюс служебные методы, которым в контексте самое место. Timestamp() добавляет ко всем событиям ключ time — так собирают базовый логгер с самого первого урока. Caller() добавляет файл и строку вызова, Stack() — стек при ошибках. Fields принимает map или слайс пар ключ-значение и добавляет их все разом; деталь, о которой мало кто читает в документации: ключи карты zerolog сортирует по алфавиту, чтобы вывод не зависел от случайного порядка обхода в Go, — так что для критичных к порядку полей пишите отдельные Str. Reset() очищает контекст. Всё это задаётся один раз на логгер, поэтому сюда переезжают и Timestamp, и общие Err — то, что раньше повторялось в каждом вызове из урока про поля события. Отдельно упомяну экзотику, которая прячется в конце списка методов: IPAddr и MACAddr для сетевых адресов, Stringer для любого значения с методом String() — удобно для типизированных ID из enum-подобных типов, Type для имени типа значения. Всё это — одни и те же байты контекста, и все они доступны ровно там, где есть zerolog.Context: и в With, и в цепочке события.

Правила контекстных полей одним списком#

  • общие для потока поля — на логгер через With; уникальные для записи — в цепочку события
  • ключи контекста и события не дедуплицируются: одноимённое поле события даст в JSON второй ключ — следите за уникальностью имён
  • Timestamp и Caller — тоже контекстные поля: добавляйте их в базовый логгер, а не в каждый вызов
  • имена ключей выносите в константы одного пакета: контекст обрастает дашбордами и менять его потом больно
  • Context.Fields(map) сортирует ключи по алфавиту — порядок предсказуем, но не ваш
  • логгер на запрос создавайте после чтения заголовков, когда request_id уже известен
  • не складывайте в контекст большие дампы: они поедут в каждое событие этого логгера

Логгер на модуль и на запрос#

Из базового логгера вырастает целое дерево. Первый этаж — логгеры на модуль: base.With().Str("module", "auth").Logger() и рядом billing, payments, worker — каждая подсистема получает свою копию со своим module. Второй этаж — логгер на запрос: в начале обработки reqLog := base.With().Str("request_id", id).Logger(), и все записи обработчика, включая вложенные вызовы, куда reqLog передали аргументом, уже размечены идентификатором. Дочерние логгеры расширяются так же: auth.With().Str("component", "oauth").Logger() даст события с module=auth и component=oauth сразу. Такая разметка — это не украшение, а формат данных для системы сбора: графики по модулям и треки запросов строятся фильтром по одному ключу. Имена модулей держите в константах и придумывайте один раз — они станут координатами в каждой записи, и переименование после того, как на module настроены дашборды и алерты, обойдётся дороже, чем кажется. Полный разбор дерева сублоггеров с реестром и тестами — в отдельном уроке про сублоггеры, а middleware, который размечает запросы request_id автоматически, ждёт в уроке про HTTP-логи.

Логгер на запрос: request_id в каждой записи

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

func handle(base zerolog.Logger, requestID string) {
        reqLog := base.With().Str("request_id", requestID).Logger()

        reqLog.Info().Msg("request received")
        reqLog.Info().Str("route", "/orders").Msg("request handled")
}

func main() {
        base := zerolog.New(os.Stdout).With().
                Str("service", "api").
                Logger()

        handle(base, "r-1024")
}

// Output:
// {"level":"info","service":"api","request_id":"r-1024","message":"request received"}
// {"level":"info","service":"api","request_id":"r-1024","route":"/orders","message":"request handled"}

reqLog — копия base с готовым request_id в контексте; в handle он передаётся обычным значением, без указателей и ссылок. Обе записи внутри обработчика размечены идентификатором автоматически: ни один вызов Info не упоминает request_id. Событие со Str("route") показывает порядок: контекст base, затем контекст reqLog, затем поля события и последним message.

Value-семантика: копия логгера — снимок#

zerolog.Logger — тип-значение: структура с writer-ом, уровнем и байтовым контекстом. Все методы модификации — With, Level, Output, Sample, Hook — возвращают копию, а не меняют receiver. Из этого следуют три практических правила. Первое: дочерний логгер — это снимок родителя на момент создания; если позже пересобрать base с новым полем, уже созданные дети этого поля не увидят. Второе: копию можно свободно передавать в функции и горутины — никакой общей мутируемой нагрузки, кроме writer-а, у копии нет, а writer в zerolog рассчитан на параллельные вызовы. Третье: раз логгеры — значения, поля контекста лежат в каждой копии своими байтами, и на длинных цепочках With контекст растёт для всех потомков — ещё одна причина не складывать туда лишнее. Экономнее всего разметка живёт на длинных связках: одна и та же копия логгера прокидывается через конструкторы репозиториев и сервисов, и каждая запись на выходе самодостаточна. Глобальный порог zerolog.SetGlobalLevel при этом общий для всех копий — он проверяется на каждом событии поверх локального уровня. Есть и обратная операция: GetLevel() читает порог копии, что удобно в тестах конфигурации — собрали логгер для шума на Warn, проверили GetLevel, не разбирая JSON. Отличать поля контекста от полей события и выбирать, что куда, помогает навык из урока про поля события: там же и таблица типизированных методов.

Цена контекста: один append и один memcpy#

Стоимость With раскладывается на две части. При создании логгера каждое поле контекста один раз сериализуется в байты и дописывается в буфер контекста — это единственная работа на всю жизнь копии. При каждом событии zerolog копирует готовые байты контекста в буфер события — по сути memcpy куска на десятки-сотни байт; аллокаций нет, рефлексии нет. Отключённое событие не копирует ничего: nil-событие получает не буфер, а отказ, и контекст остаётся не у дел. Сравните с альтернативой без With: те же поля дописываются в каждом вызове — больше кода, шанс пропустить и та же цена сериализации, только на каждом событии вместо одного раза. Так что With выгоднее и короче одновременно — редкий случай, когда лучший по читаемости вариант и самый быстрый совпадают. Замеры логгера в бенчмарках с контекстными полями и честные оговорки — в уроке про производительность. Проверить всё руками проще локально: песочница сайта — тематическая программа на чистом Go, а настоящий With с живым JSON запускается через go run за минуту.

Контекстные поля против полей события#

АспектКонтекст (With)Событие (цепочка вызова)
Задаётсяодин раз при создании логгерав каждом вызове логирования
Известнызаранее: сервис, модуль, request_idв момент записи: op, code, err
Позиция в JSONпосле уровня и timeпосле контекста, перед message
Ценасериализация один раз, далее memcpyсериализация на каждом событии
Методытот же набор + Timestamp, Caller, Stackтот же набор полей
Типичный источникmain и конструкторыаргументы конкретной функции

Практический порядок внедрения простой. Пройдитесь по коду и выпишите поля, которые повторяются в разных вызовах: обычно это service, env, version, module и идентификаторы потоков. Соберите базовый логгер при старте и раздайте модулям их копии через With — конструкторы хендлеров и воркеров принимают zerolog.Logger параметром. Оставьте в вызовах только уникальные поля. Через неделю посмотрите в системе сбора, как изменился поиск: записи одного запроса собираются фильтром по request_id, графики по модулям — фильтром по module. Это и есть главный дивиденд контекстных полей: не скорость записи, а предсказуемость каждой строки, которую вы будете читать глазами в три часа ночи. Перед закрытием темы проверьте два места, где With ошибается незаметно. Первое — порядок сборки: поле, добавленное в base позже, чем создана копия, в этой копии не появится, и это не баг, а снимок. Второе — имена ключей: два разных модуля, размечающие одно и то же поле с разными именами вроде user_id и userId, с гарантией дадут два графика вместо одного. Сведите имена в константы одного пакета ещё на этапе внедрения — дешевле, чем миграция после того, как на ключи опираются алерты.

Проверка: что возвращает With

Что возвращает вызов logger.With()?

Проверка: порядок в JSON

Где в JSON-записи окажется поле service, добавленное через With()?

Какой JSON выйдет из дочернего логгера?

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

base := zerolog.New(os.Stdout).With().Str("service", "api").Logger()

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

auth.Info().Str("user", "ann").Msg("ready")

Увидит ли дочерний логгер новое поле родителя?

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

base := zerolog.New(os.Stdout).With().Str("service", "api").Logger()

reqLog := base.With().Str("request_id", "r-1").Logger()

base = base.With().Str("extra", "late").Logger()

reqLog.Info().Msg("work")

Соберите цепочку With

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

package import ( "os" "github.com/rs/zerolog" ) func main() { base := zerolog.New(os.Stdout).With(). ("service", "billing"). (). Logger() auth := base.With().("module", "auth").Logger() auth.().Str("user", "ann").Msg("started") }

Программа с базовым и дочерним логгером

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

Строки

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

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

Карточки: With и контекст

Термин

Logger.With()

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

Метод zerolog, возвращающий zerolog.Context — накопитель предзаданных полей. Финализируется вызовом Logger() и создаёт копию логгера с этой разметкой.

1 / 6

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

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

Вывод

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

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

  • With() возвращает zerolog.Context; Logger() финализирует цепочку и создаёт логгер с контекстными полями.
  • Контекст хранится готовыми JSON-байтами: на событие — один memcpy, без сериализации и аллокаций.
  • Порядок в JSON: уровень, time, поля контекста, поля события, последним — message.
  • Цепочка base — сублоггер на модуль — логгер на запрос размечает записи без повторов в коде.
  • Logger — значение: дочерний логгер — снимок родителя, позже добавленные поля в него не приезжают.
  • Context.Fields(map) сортирует ключи по алфавиту — порядок внутри контекста предсказуем.
  • Timestamp и Caller — тоже контекстные поля: им место в базовом логгере, а не в каждом вызове.

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

Соберите дерево логгеров на локальном Go: base с service и Timestamp, два сублоггера на вымышленные модули и логгер на запрос с request_id. Запустите и проверьте по выводу позицию каждого поля в JSON. Затем пересоберите base с дополнительным полем после создания сублоггеров и убедитесь, что записи сублоггеров не изменились. Наконец, передайте reqLog в функцию с горутиной и проверьте, что все её записи размечены.

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

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

Чем Logger.With отличается от event-полей?

With задаёт поля один раз на логгере — они попадают в каждое событие этой копии. Поля события добавляются в конкретную запись и живут один вызов. Правило: общее для потока — в контекст, уникальное для записи — в цепочку вызова.

Что произойдёт, если событие добавит поле с тем же ключом, что в контексте?

Zerolog не дедуплицирует ключи: в JSON окажутся два поля с одним именем, а парсеры возьмут последнее. Не рассчитывайте на перезапись — следите за уникальностью имён ключей контекста и события на уровне констант в коде.

Сколько стоит With на горячем пути?

Почти ничего: поля сериализуются один раз при создании логгера, а на каждом событии готовые байты контекста копируются в буфер записи. Отключённое событие контекст не копирует вовсе. Замеры с бенчмарками — в уроке про производительность.

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

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

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

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

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

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

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