Содержание урока · 10 разделов
- Восемь констант zerolog: от Trace до Disabled
- Семантика уровней: что писать на каждом
- Nil-событие: отключённый уровень почти бесплатен
- Trace, Debug и Info: три скорости детализации
- Warn против Error: спорная граница
- Конвенция уровней одним списком
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Восемь констант zerolog: от Trace до Disabled#
Уровень в zerolog — это число типа int8. Снизу вверх: TraceLevel равно -1, DebugLevel — 0, InfoLevel — 1, WarnLevel — 2, ErrorLevel — 3, FatalLevel — 4, PanicLevel — 5, NoLevel — 6 и Disabled — 7; чем больше число, тем серьёзнее событие. Логгер пишет запись, только если её уровень не ниже двух порогов сразу: порога самого логгера и глобального уровня, который задаёт zerolog.SetGlobalLevel для всего процесса. Пороги складываются по максимуму: глобальный Info отсечёт Debug, даже если конкретный логгер настроен мягче. Из коробки пороги стоят так: zerolog.New создаёт логгер с уровнем Trace, но глобальный уровень по умолчанию — Debug, поэтому единственный уровень, который вы не увидите без настройки, — Trace; чтобы включить его, вызовите zerolog.SetGlobalLevel(zerolog.TraceLevel). Константа Disabled — самое жёсткое значение: SetGlobalLevel(zerolog.Disabled) глушит все записи процесса, что удобно как аварийный рубильник или заглушка в тестах. Особняком стоит NoLevel (6): это не про серьёзность, а про события без уровня — их создаёт метод log.Log(), и ключа level в JSON у таких записей нет вовсе. Встречаются они редко, но знать про них стоит, чтобы запись без level не выглядела поломкой. Примеры ниже исполняются локально — SetGlobalLevel требует библиотеки, а песочница сайта показывает тематическую программу на чистом Go. Проверка происходит дважды: когда вы вызываете метод уровня и когда событие уходит в writer. Из-за этого комбинация «мягкий логгер при жёстком глобальном» безопасна, а обратная — нет: событие, прошедшее локальный порог, всё равно упрётся в глобальный.
Семантика уровней: что писать на каждом#
| Уровень | Число | Что логировать | Пример записи |
|---|---|---|---|
| Trace | -1 | вход и выход функций, шаги циклов | entering handler /users |
| Debug | 0 | параметры, состояния, попадания в кэш | cache miss key=user:42 |
| Info | 1 | штатные события — хроника сервиса | service started port=8080 |
| Warn | 2 | работает, но с потерями или странно | retry 2/5 attempt delayed |
| Error | 3 | операция провалилась, есть ошибка | dial failed err=timeout |
| Fatal | 4 | процесс дальше жить не может | config invalid missing=tls_cert |
| Panic | 5 | записать событие и запаниковать | invariant broken state=empty |
zerolog.SetGlobalLevel и фильтрация
GOpackage main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Debug().Str("query", "SELECT ...").Msg("db query") // молчит
log.Info().Msg("service started")
log.Warn().Int("percent", 93).Msg("disk almost full")
log.Error().Str("op", "dial").Msg("connection failed")
}
// Вывод: три строки — info, warn, error. Запись Debug отсечена.SetGlobalLevel атомарно поднимает глобальный порог для всех логгеров процесса. Вызов Debug создаёт проверку: 0 (debug) меньше 1 (info) — событие не проходит, и цепочка превращается в no-op ещё до Msg. Остальные три вызова проходят порог и пишутся. Заметьте: проверка уровня случается в начале цепочки, поэтому поля отключённого события даже не собираются. SetGlobalLevel меняет одну атомарную переменную — переключение читается целиком из любой горутины, без мьютексов и гонок.
Nil-событие: отключённый уровень почти бесплатен#
Что именно происходит с отключённой записью — лучшая деталь zerolog. Когда уровень события ниже порога, Info-подобный метод возвращает nil вместо *Event, а каждый метод цепочки — Str, Int, Err, Msg — начинается с проверки receiver на nil и просто возвращает событие дальше. Ни паники, ни записи, ни аллокаций: в собственных бенчмарках README отключённое событие стоит около четырёх наносекунд. Отсюда и спокойная практика для библиотек: библиотечный код логирует Debug сколько угодно — включать его детали решает приложение, а не автор библиотеки. Из этого следует практика, которую в других логгерах приходится строить руками: отладочные вызовы не надо оборачивать в if и не надо удалять из горячего пути — Debug-цепочка в HTTP-обработчике стоит ноль, когда уровень Info. Это же делает честной связку «в dev включаем Debug, в prod выключаем»: код одинаковый, меняется одна константа при старте. Практический вывод для ревью: обёртки if вокруг Debug-вызовов — мусор, принесённый из других логгеров. Здесь условие не нужно никогда: дешевле вызвать и отсечься порогом, чем плодить ветвления, которые кто-то потом будет поддерживать. Подробный разбор цены записей с бенчмарками — в уроке про производительность. Именно поэтому в zerolog не нужен отдельный заглушечный логгер для библиотек: роль заглушки играет сам порог, а для случаев, когда логировать нельзя вовсе, есть zerolog.Nop() — логгер, который глотает всё.
Глобальный уровень — не единственная ручка. Метод Level возвращает копию логгера с собственным порогом, не трогая исходник: quiet := logger.Level(zerolog.WarnLevel). Иммутабельность здесь та же, что и у всего zerolog: дочерние логгеры создаются, а не перенастраиваются. Пороги глобальный и локальный работают как максимум из двух, поэтому заглушить шумный модуль через локальный уровень не выйдет, если глобальный стоит выше, — сначала опустите глобальный. Типичное применение: сторонний клиент в вашем процессе пишет слишком много Info — оберните его логгер копией с Warn. Систему сублоггеров на модули мы соберём в уроке про сублоггеры, здесь важно само свойство: уровень — часть конфигурации логгера, а не свойство вызова. Спорное место — локальный Disabled: логгер с ним молчит на всё, включая Fatal. Для временной заглушки шума это удобно, но если под заглушкой случится настоящий фатал — вы узнаете о нём по падению процесса без единой строки в журнале. Я не ставлю Disabled локально никогда: глобальный рубильник видно снаружи и его можно поднять, локальный — невидимка.
Свой порог у отдельного логгера
GOpackage main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
quiet := logger.Level(zerolog.WarnLevel)
quiet.Info().Msg("не напечатается: порог Warn")
quiet.Error().Msg("connection lost")
logger.Info().Msg("исходный логгер не изменился")
}
// Вывод: две записи — error от quiet и info от logger.logger.Level(zerolog.WarnLevel) возвращает копию с новым порогом: quiet молчит на Info, но пишет Error. Исходный logger продолжает писать Info — копирование не задевает оригинал. Именно поэтому 3 (Error) не ниже 2 (Warn) — запись проходит. Если бы где-то выше стоял SetGlobalLevel(zerolog.DebugLevel), оба логгера подчинились бы и ему: глобальный порог всегда проверяется дополнительно.
SetGlobalLevel — не только настройка при старте: порог хранится в атомарной переменной, и менять его безопасно из любой горутины на живом процессе. Прочитать текущее значение можно зеркальной функцией zerolog.GlobalLevel() — она избавляет от копий порога в собственных структурах, которые всегда врут. Классический сценарий: в проде случилась странность, деплоить нельзя — дежурный дёргает админ-эндпоинт или сигнал, обработчик парсит строку через zerolog.ParseLevel, поднимает порог до Debug, разбирается и опускает обратно. Отсюда два правила гигиены. Первое: строку конфига парсит ParseLevel, а не таблица if-ов из вашего кода — функция знает все имена уровней и возвращает ошибку на мусор. Второе: поднятый Debug забудут опустить, поэтому ставьте сброс по таймауту или держите возврат уровня в чек-листе инцидента — иначе через квартал кто-то удивится объёму логов. Механика чтения уровня из окружения с дефолтами и валидацией — тема урока про конфигурацию.
Trace, Debug и Info: три скорости детализации#
Уровни — это скорость ленты. Trace — самая быстрая: входы и выходы функций, итерации, шаги конвейера; включается на минуты точечной отладки и выключается, иначе лента станет бесконечной. Debug — подробности штатной работы: параметры запросов, состояния кэша, решения маршрутизатора; в dev живёт всегда, в prod — по требованию, потому что именно Debug-записи вы будете читать, разбирая, почему запрос пошёл не туда. Info — хроника сервиса: старт и стоп, принятая конфигурация, приём и завершение значимых операций. Правило качества: Info-лента должна читаться как дневник — глаз дежурного пробегает её и видит события, а не шум. Если Info-записи можно удалять пачками без потери смысла, они лежат на неправильном уровне: это Debug. И наоборот, если без Debug-вызовов не собрать картину инцидента, что-то важное спряталось не на своём уровне. Ориентир по объёму: на один HTTP-запрос у взрослого сервиса выходит одна-две Info-записи, десяток Debug и сотня Trace. Если Info выходит по десять на запрос — хроника утонула в шуме ещё до системы сбора, и фильтр по уровню перестаёт быть инструментом дежурного.
Warn против Error: спорная граница#
Границу Warn/Error каждая команда проводит сама, и это честно спорное место. Базовое правило: Warn — операция выжила с потерями (ретрай удался, включился fallback, данные деградировали), Error — операция провалилась, и у записи есть ошибка. Спор начинается на HTTP-статусах. Моё правило: 4xx от клиента — Warn, а не Error, потому что сервер свою работу сделал, а некорректный запрос — не дефект сервера. Условие, при котором я готов уступить: если у вас нет алертов на Error-ленту, то Error безопаснее — вы ничего не пропустите; если алерты есть, Error на каждый 404 разбудит дежурного из-за бота-сканера. Что сломается при путанице уровней в обратную сторону: Warn на всё — и горящий узел чувствует себя нормально, потому что алерты настроены на Error, а дежурный видит лишь спокойную жёлтую ленту. Уровни — это язык алертов: договоритесь в команде один раз и запишите конвенцию в репозиторий. И ошибка — не единственный признак Error: операция без объекта ошибки, которая всё-таки провалилась (нет свободного места, исчерпан лимит соединений), тоже Error — с полем-причиной, даже если err формально нет. Уровень описывает судьбу операции, а не наличие объекта error в коде.
log.Fatal().Msg("...") пишет запись и немедленно завершает процесс с кодом 1 — os.Exit не даёт выполниться ни одному defer, поэтому незакрытые файлы и транзакции останутся как есть. Zerolog сам закроет writer, если тот реализует io.Closer, но ваш код не запустится. Нюанс с буфером: запись до exit гарантированно уходит в writer, но если writer буферизован и без Flush — редкий, но реальный случай — хвост может не долететь: ещё одна причина не буферизовать без нужды. Место Fatal — провал на старте: битая конфигурация, недоступная база при поднятии. В библиотеках Fatal запрещён: библиотека не имеет права убивать процесс вызывающего.
Сервис, где и успешный запрос, и падение оплаты пишутся Info, лишает уровни смысла: фильтр по уровню ничего не находит, алерты строятся по сообщениям — то есть по grep. Если монолит уже так живёт, начните с Error и Warn: перенесите сначала провалы, потом деградации, и лента обретёт слои. Быстрый диагностический вопрос команде: какие события у нас будят дежурного? Если ответ «ничто» — Error-лента пуста или алертов нет, и уровни пока дают только слова в JSON.
Конвенция уровней одним списком#
- Trace — отладка шагов: вход/выход функций, итерации; в prod выключен, включается точечно
- Debug — подробности штатной работы: параметры, состояния, кэш; включается по требованию
- Info — хроника сервиса: старт, конфигурация, значимые операции; читается как дневник
- Warn — операция выжила с потерями: ретраи, fallback, деградация, некорректные запросы клиента
- Error — операция провалилась, запись всегда с ошибкой в поле error; здесь живут алерты
- Fatal — только на старте процесса, когда продолжать нельзя; в библиотечном коде запрещён
- Panic — редкий гость: записать инвариант и запаниковать, если выше есть recover
- SetGlobalLevel — при старте из конфига окружения, а не константой в коде
Проверка: дефолты
Какой уровень zerolog действует по умолчанию, если SetGlobalLevel не вызывали?
Проверка: Fatal
Что делает log.Fatal().Msg("config invalid") после записи события?
Проверка: рубильник
Как полностью заглушить все записи zerolog в процессе?
Сколько строк напечатает программа?
Прочитайте код и выберите, что он выведет:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Trace().Msg("a")
log.Info().Msg("b")
log.Warn().Msg("c")
log.Error().Msg("d")Управление уровнями
Восстановите вызовы: глобальный порог Info, тихий отладочный вызов и запись об ошибке.
Программа с уровнями
Расставьте строки в правильном порядке: импорты, глобальный порог и две записи.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: уровни zerolog
Что дальше по курсу#
Следующий урок — поля события: Str, Int, Err и Dur, ошибка как данные, а не строка в сообщении. Потом — ConsoleWriter: как уровни превращаются в цветные INF и ERR на терминале. Если главный вопрос «как уровни помогают продам» — пригодятся уроки про sampling, где вместо глушения поток прореживают, и про логи на проде, где фильтр по уровню становится первым инструментом дежурного.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Уровни zerolog — числа: Trace -1, Debug 0, Info 1, Warn 2, Error 3, Fatal 4, Panic 5, Disabled 7.
- Событие пишется, если проходит и порог логгера, и глобальный SetGlobalLevel — работает максимум из двух.
- По умолчанию глобальный уровень Debug: из коробки пишется всё, кроме Trace.
- Отключённый уровень возвращает nil-событие: цепочка no-op за единицы наносекунд, Debug можно не вычищать из кода.
- Warn — выжили с потерями, Error — операция провалилась; конвенцию уровней записывают один раз на команду.
- Fatal пишет запись и вызывает os.Exit(1): defer не выполняется, в библиотеках Fatal запрещён.
- Порог хранится атомарно: SetGlobalLevel безопасен на живом процессе, zerolog.GlobalLevel() читает значение, ParseLevel парсит строку конфига.
Домашнее задание
Соберите программу урока и прогоните её при четырёх глобальных уровнях: Trace, Debug, Info, Warn — посчитайте строки вывода для каждого. Потом замените SetGlobalLevel на zerolog.Disabled и убедитесь, что вывод пуст. Наконец, напишите функцию, которая принимает уровень строкой ("info", "warn") из os.Args, конвертирует через zerolog.ParseLevel и выставляет глобальный порог — это заготовка урока про конфигурацию по окружению. Проверьте под go run -race, что смена уровня из отдельной горутины не даёт гонки.
Личные заметкиПусто
Вопросы и ответы по уроку
Какие уровни логов есть в zerolog?
Trace (-1), Debug (0), Info (1), Warn (2), Error (3), Fatal (4), Panic (5), NoLevel (6) и Disabled (7). Для каждой константы есть метод события: Trace(), Debug(), Info() и так далее, а глобальный порог задаёт zerolog.SetGlobalLevel. Порядок стоит помнить именно числами: сравнение с порогом идёт по ним.
Какой уровень выбрать для Debug-записей в проде?
Держите Debug в коде и выключайте уровнем: отключённый Debug в zerolog почти бесплатен — nil-событие и no-op цепочка. Включайте порог Debug точечно: по окружению, по модулю или при разборе инцидента — для последнего уровень можно поднять на живом процессе через админ-эндпоинт, деплой не нужен.
Чем Fatal отличается от Error?
Error фиксирует провал операции, процесс продолжает работать. Fatal пишет запись и завершает процесс через os.Exit(1) — defer не выполняется. Fatal уместен только при старте, когда сервис не может работать без упавшей зависимости. Для сценария «записать и упасть с восстановлением через recover» есть Panic: он паникует после записи, а не выходит из процесса.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила