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

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

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

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

Первый лог в zerolog: New и JSON-событие

Первый лог в zerolog собирается из трёх вызовов: zerolog.New(os.Stdout), цепочка .With().Timestamp() и Info().Msg() — и сразу печатает однострочный JSON. В этом уроке напишем первую программу, разберём каждый ключ записи, сравним глобальный логгер с собственным и поймём, почему у zerolog нет метода Sync, который вы привыкли искать после zap.

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

Установка: go get github.com/rs/zerolog#

Начните с пустого модуля: mkdir demo, cd demo, go mod init demo. Дальше — go get github.com/rs/zerolog/log: команда подтянет последнюю версию ветки v1 и пропишет её в go.mod. В коде вы встретите два пакета. Ядро — github.com/rs/zerolog: типы Logger, Event, уровни, ConsoleWriter, конструктор zerolog.New. Над ним — github.com/rs/zerolog/log: пакет с готовым глобальным логгером для коротких программ и утилит. Один модуль, два входа; импортируете тот, что нужен. Оговорка о формате курса: библиотечный код здесь не исполняется — песочница сайта показывает тематическую программу на чистом Go, а примеры урока вы запускаете локально, где zerolog честно печатает свои строки. Разобраны они построчно, так что вывод вы предскажете до запуска. Пакет годами держит одну мажорную версию: код из туториалов 2018 года собирается сегодняшним компилятором без правок, а go.mod после установки остаётся почти пустым — цепочек транзитивных зависимостей, как у веб-фреймворков, здесь нет. Если проект уже живёт на другом логгере, ставьте zerolog рядом и переводите на него модуль за модулем: конфликтов имён у двух логгеров не бывает.

Первая программа: zerolog.New(os.Stdout)

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

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

        logger.Info().Msg("service started")
        logger.Error().Str("reason", "port busy").Msg("shutdown")
}

// Output:
// {"level":"info","time":"2024-04-06T12:00:00+03:00","message":"service started"}
// {"level":"error","time":"2024-04-06T12:00:00+03:00","reason":"port busy","message":"shutdown"}

zerolog.New принимает io.Writer — сюда уедет каждая запись; здесь это os.Stdout. Метод New возвращает логгер без времени, поэтому .With().Timestamp().Logger() — каноническая достройка: контекстное поле time добавляется к каждому событию, а .Logger() собирает итоговый тип. Info() и Error() начинают события разных уровней, Msg дописывает message и печатает строку. Поле reason стоит перед message — сообщение всегда приходит последним. Уберите .With().Timestamp() — и ключ time пропадёт из обеих строк: predict ниже закрепит это наблюдение.

Читаем JSON: level, time, message#

Каждая запись zerolog — одна строка JSON, и ключи в ней стоит назвать по именам. level — уровень строчными: info, error, debug, warn. time — время в формате RFC3339 вроде 2024-04-06T12:00:00+03:00: zerolog по умолчанию пишет человекочитаемую строку, а не секунды от эпохи, как zap с его ts; если предпочитаете unix-time, выставьте zerolog.TimeFieldFormat = zerolog.TimeFormatUnix. message — сообщение; обратите внимание на полное слово: в zap ключ называется msg, и пальцы первое время путаются. Ваши поля идут между контекстом и сообщением, в том порядке, в котором вы их дописали. Все эти имена — не приговор, а константы пакета: TimestampFieldName, MessageFieldName и прочие можно переопределить и подстроить схему под систему сбора — этому посвящён урок про структуру JSON. Запись при этом всегда ровно одна строка: zerolog не переносит JSON и разделяет события переводом строки — формат NDJSON. Из этого следуют приятные вещи: tail -f показывает по записи на строку, поток безопасно резать по переводу строки, а системы сбора читают такие ленты стримом, не буферизуя весь файл. Внутри записи переносов не бывает: переводы строк внутри значений zerolog экранирует, поэтому правило «одна строка — одно событие» держится без исключений. Формат ключа time управляется глобальной переменной zerolog.TimeFieldFormat. По умолчанию она равна time.RFC3339, и запись выглядит как time: «2024-04-06T12:00:00+03:00» — человекочитаемо, но строка на два десятка символов в каждой записи. Если система сбора предпочитает unix-time, выставьте zerolog.TimeFieldFormat = zerolog.TimeFormatUnix при старте: время станет числом секунд от эпохи, коротким и дешёвым в парсинге; рядом есть TimeFormatUnixMs с миллисекундами. Выбор — не вкусовщина: Loki и ClickHouse переваривают обе формы, но числовое значение индексируется дешевле, а лента в целом становится короче. Меняйте константу один раз при инициализации, до первых записей: переменная читается на каждом событии, и перезапись её на живом процессе из другой горутины — та гонка, которую не стоит устраивать даже ради эксперимента. Если оставляете RFC3339, помните про зону: zerolog пишет локальную зону процесса, и в контейнере с UTC она не совпадёт с вашей машиной — первое, что стоит проверить, сравнивая логи двух сред.

Stdout или stderr: куда уедет запись#

Куда именно уедет запись — решает writer, переданный в New, и это второй важный выбор после уровня. os.Stdout — самый частый вариант для сервисов: в контейнере stdout собирает рантайм Docker, дальше логи едут в Loki или ELK. Глобальный логгер из пакета zerolog/log настроен иначе: он пишет в os.Stderr и добавляет время из коробки — вы могли заметить это по выводу первого урока. Разделение потоков — конвенция Unix: данные программы идут в stdout, диагностика — в stderr, и CLI-инструменты этим живут, что хорошо видно в курсе по Cobra, где вывод команды и логи разделены с первого дня. Для веб-сервиса выбор stdout безобиден: сборщикам всё равно, для CLI — stdout держите чистым под данные. Проверить разделение можно без инструментов: запустите программу с 2>/dev/null — и увидите, что именно ушло в stdout, отдельно от диагностики. Docker и journald собирают оба потока, но помечают по-разному, а часть сборщиков теряет порядок между ними: если порядок записей в доли секунды важен, пишите весь лог одним потоком.

Глобальный логгер: log.Info().Msg()

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
        "github.com/rs/zerolog/log"
)

func main() {
        log.Info().Str("user", "ann").Msg("signed in")

        log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
        log.Info().Str("user", "ann").Msg("signed in again")
}

// Первый вызов уходит в stderr, второй — в stdout.

Пакет zerolog/log хранит переменную log.Logger, созданную как zerolog.New(os.Stderr).With().Timestamp().Logger() — именно поэтому время в записях есть сразу, без With-достроек. До первого присваивания записи идут в stderr; перезаписали log.Logger — все новые вызовы уехали в stdout. Для коротких программ и экспериментов глобальный логгер быстрее на старте, для сервиса со структурой зависимостей — см. tip ниже. После присваивания новые вызовы log уехали в stdout — существующий код подхватил новую поверхность вывода без единой правки.

Перезапись log.Logger из примера выше безопасна в одном месте — при старте, до первых записей. Глобальная переменная не защищена мьютексом: если одна горутина присвоит log.Logger новое значение, пока другая печатает, компилятор промолчит, а go run -race покажет гонку. Поэтому развилка такая: утилита на один файл — живите с глобальным log, он настроен из коробки и пишет в stderr с временем; сервис с горутинами — соберите логгер в main и передайте зависимостью, тогда гонка не станет возможной в принципе. В тестах глобальный логгер удобно подменять: сохранить old := log.Logger, подставить логгер поверх буфера, вернуть old в конце — но и это лучше делать один раз в TestMain, а не в каждом тесте, где про возврат забудут.

Почему в zerolog нет Sync#

Если вы пришли из zap, рука тянется дописать defer logger.Sync() — в zerolog компилятор её остановит: такого метода нет, и это не упущение, а следствие архитектуры. Zap собирает запись во внутренние буферы и сбрасывает их по расписанию — без Sync хвост логов теряется. Zerolog при каждом Msg дописывает message в буфер события и тут же вызывает Write у вашего writer: строка уходит наружу сразу, буферизовать нечего, терять нечего. Памятью вы расплачиваетесь один раз — при записи в файл системный вызов происходит на каждый Msg, и если это станет узким местом, оберните writer в буфер сами (bufio.Writer, Flush в конце — тема урока про файлы) или возьмите асинхронный diode.Writer из поставки zerolog. Правило простое: синхронность — дефолт, буферизация — ваше осознанное решение, а не спрятанное в библиотеке. Заметьте симметрию: у zap буферизация спрятана внутри, и Sync обязателен; у zerolog всё синхронно, и буфер — ваша забота. Первая модель удобнее на старте, вторая честнее: увидев в профиле подозрительный Write, вы точно знаете, кто замедлился, и чем это лечить, а про файлы и Flush есть отдельный урок про запись в файл.

Ключи записи: zerolog против zap#

Что в записиzerologzap (production)
Уровеньlevel: "info"level: "info"
Времяtime: RFC3339-строкаts: секунды от эпохи
Сообщениеmessage (полное слово)msg (короткое)
Ваши поляпосле контекста, в порядке вызововпосле msg
Ошибкаerrorerror
Место вызоваcaller (если включён)caller (включён по умолчанию)

Чек-лист первой программы#

  • writer: os.Stdout — для сервиса под сборщиком логов, os.Stderr — для диагностики и утилит
  • время: With().Timestamp() почти всегда; формат — zerolog.TimeFieldFormat при старте, если система сбора хочет unix
  • глобальный log — для утилит и экспериментов; собственный zerolog.New — для сервиса с зависимостями
  • финализатор: Msg по умолчанию, Send — события без сообщения, Msgf — с осторожностью, данные лучше в поля
  • Sync в API нет: вместо него вы сами решаете, буферизовать writer или писать синхронно
  • уровень по умолчанию Debug: перед деплоем поднимите порог SetGlobalLevel — тема следующего урока

Три способа завершить событие — Msg, Msgf и Send — закрывают разные задачи. Msg принимает готовую строку: Msg("order created") — случай по умолчанию, факт без данных. Msgf тянет за собой fmt.Sprintf: Msgf("retry %d for order %s", attempt, id) — удобно для быстрых записей, но данные внутри сообщения недоступны фильтрам, так что в боевом коде данные лучше уводить в поля, а Msgf оставить редким гостем. Send пишет событие без сообщения вовсе: log.Info().Int("count", batch).Send() — запись из одних полей, полезная для событий-метрик, которые читает не человек, а система. Выбор поля под задачу — тема следующего-следующего урока, а пока запомните: Msg — по умолчанию, Send — для безмолвных событий, Msgf — с осторожностью. Ориентир для ревью: Msgf с двумя плейсхолдерами и больше — почти всегда сигнал, что данные просятся в поля. Одиночная деталь в сообщении терпима, но если завтра по этому месту понадобится график, Msgf придётся переписывать, а поля — просто добавить.

Проверка: глобальный логгер

Куда пишет глобальный логгер из пакета github.com/rs/zerolog/log?

Проверка: ключ сообщения

Какой ключ в JSON zerolog содержит сообщение?

Проверка: With().Timestamp()

Что добавляет достройка zerolog.New(os.Stdout).With().Timestamp().Logger()?

Что выведет логгер без Timestamp?

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

logger := zerolog.New(os.Stdout)

logger.Info().Msg("hi")

Соберите конструктор логгера

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

package main import ( "os" "github.com/rs/zerolog" ) func main() { logger := zerolog.(os.Stdout).With().().Logger() logger.().Str("service", "billing").("service started") }

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

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

Строки

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

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

Карточки: New и первое событие

Термин

zerolog.New(w io.Writer)

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

Конструктор логгера поверх любого io.Writer. Записи уходят в writer при каждом Msg — синхронно, без внутренней буферизации.

1 / 6

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

Следующий урок — уровни логов: восемь констант от Trace до Disabled, SetGlobalLevel и то, почему отключённый Debug почти бесплатен. Потом — поля события: Str, Int, Err и долг жизни Any. Если хочется сразу увидеть записи глазами, а не JSON — загляните в урок про ConsoleWriter, он переделывает вывод этого урока в цветные колонки. Карта всех двадцати пяти тем — на странице курса zerolog.

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

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

Вывод

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

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

  • go get github.com/rs/zerolog/log; ядро — пакет zerolog, глобальный логгер — пакет zerolog/log.
  • Канонический конструктор: zerolog.New(os.Stdout).With().Timestamp().Logger().
  • Запись: {"level":"info","time":"...","message":"..."} — message пишется последним, после ваших полей.
  • Глобальный логгер пишет в os.Stderr и даёт time из коробки; собственный из New — только с With().Timestamp().
  • Sync в zerolog нет: каждый Msg синхронно пишет в writer; буферизацию и Flush добавляете сами.
  • Цепочка завершается Msg, Msgf или Send — без финализатора запись не уедет.
  • Время задаётся zerolog.TimeFieldFormat: RFC3339-строка по умолчанию, TimeFormatUnix — число секунд для систем сбора.

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

Соберите программу урока и запустите её дважды: с глобальным логгером из zerolog/log и с собственным zerolog.New(os.Stdout). Сравните потоки: у какого варианта вывод ушёл в stderr — проверьте перенаправлением 2>/dev/null. Потом удалите With().Timestamp() и убедитесь, что ключ time пропал из JSON. Наконец, напишите событие с Send() вместо Msg и посмотрите, как изменилась запись. Потом выставьте zerolog.TimeFieldFormat = zerolog.TimeFormatUnix и сравните два вида ключа time.

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

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

Как сделать первый лог в zerolog?

Установите пакет (go get github.com/rs/zerolog/log), создайте логгер zerolog.New(os.Stdout).With().Timestamp().Logger() и вызовите logger.Info().Msg("...") — в stdout уйдёт JSON-строка с level, time и message. Ещё быстрее: импортируйте zerolog/log и вызовите log.Info().Msg() — глобальный логгер настроен за вас. Формат записи — NDJSON: одна строка на событие, разделитель — перевод строки.

Нужен ли в zerolog logger.Sync()?

Нет, такого метода нет и он не нужен: zerolog пишет запись в writer синхронно при каждом Msg, ничего не буферизуя. Буферизацию вы добавляете сами (bufio.Writer, diode.Writer) — тогда и сброс делаете сами. Если задержки Write станут видны в профиле — вот сигнал оборачивать writer в буфер.

Почему в JSON zerolog нет поля time?

Собственный логгер из zerolog.New не добавляет время автоматически. Добавьте .With().Timestamp().Logger() — или пользуйтесь глобальным логгером из zerolog/log, у которого время настроено с самого начала. Для unix-time выставьте TimeFieldFormat = TimeFormatUnix — ключ time станет числом.

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

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

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

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

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

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

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