Содержание урока · 10 разделов
- LOG_LEVEL: строка из окружения в порог
- ParseLevel: словарь строк, который знает zerolog
- Dev против prod: writer и уровень меняются вместе
- Viper и файлы конфигурации: строка — и всё
- Один сервис, три окружения
- Чек-лист конфигурации логгера
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Порог, зашитый константой, работает ровно до первого неудобного дня: дебаг нужен на минуту, а путь к нему лежит через сборку, тесты и выкладку. Переменная окружения — самый дешёвый канал конфигурации: она есть в манифесте, в systemd-юните, в CI-джобе и в шелле разработчика, и читается стандартным os.Getenv без единой зависимости. Схема одна на все окружения: прочитали строку, подставили дефолт, если она пуста, распарсили, применили. Каждый шаг этого конвейера разбирается ниже по отдельности, потому что у каждого есть своё место, где схема ломается. Дефолт объявляется рядом с чтением, а не в глубине пакета: если значения конфигурации разъехались по трём файлам, ни один ревьюер не скажет, какое вступит в силу. Порог применяется через zerolog.SetGlobalLevel из урока про уровни — одной атомарной записью, безопасной на любом этапе старта. Отдельный вопрос — валидность: мусор в LOG_LEVEL должен валить старт, а не тихо игнорироваться, и это тема «живого места» в середине урока.
LOG_LEVEL: строка из окружения в порог#
Пустая переменная — не ошибка, а легитимное значение «не настроено», и дефолт для неё выбирается сознательно. Info — правильный ответ и для дева, и для прода: Debug-поток в деве удобен ровно до момента, когда лента перестаёт влезать в экран, а прод-объём считается не гигабайтами, а терабайтами. Дефолт должен стоять до парсинга: if level == "" { level = "info" } — две строки, после которых пустое чтение уже не сможет превратиться в сюрприз. os.Getenv возвращает пустую строку и для неустановленной переменной, и для пустой, поэтому один дефолт накрывает оба случая. Дальше строка идёт в zerolog.ParseLevel — словарь между именем уровня и числом, и на выходе получается zerolog.Level, который отдаётся zerolog.SetGlobalLevel. Подтверждающая Info-запись с принятым уровнем пишется сразу после настройки: три секунды работы, а утренние споры о том, какой порог реально вступил в силу, закрываются первой строкой ленты. И запомните порядок: чтение, дефолт, парсинг, применение, подтверждение — любое другое место для дефолта рано или поздно даст окружение, в котором конфиг есть, а порог другой.
Полная цепочка настройки при старте
GOpackage main
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
level := os.Getenv("LOG_LEVEL")
if level == "" {
level = "info"
}
lvl, err := zerolog.ParseLevel(level)
if err != nil {
log.Fatal().Err(err).Str("LOG_LEVEL", level).Msg("bad log level")
}
zerolog.SetGlobalLevel(lvl)
log.Info().Str("level", lvl.String()).Msg("logger configured")
log.Debug().Msg("виден только при LOG_LEVEL=debug")
}os.Getenv возвращает пустую строку и для неустановленной переменной, и для пустой — дефолт накрывает оба случая. ParseLevel возвращает уровень и ошибку; мусорная строка валит старт через Fatal, единственное место в сервисе, где Fatal уместен. Подтверждающая запись с lvl.String() показывает принятый порог в первой строке ленты, а финальный Debug демонстрирует работу порога на живом примере.
ParseLevel: словарь строк, который знает zerolog#
zerolog.ParseLevel(s string) (Level, error) — словарь между строкой конфига и числовым уровнем. Он знает все имена: trace, debug, info, warn, error, fatal, panic, disabled — и сравнивает их без учёта регистра, так что INFO, Info и info дают один и тот же уровень. Помимо имён функция принимает числа: строка 2 превращается в WarnLevel — удобно для конфигов, где уровни хранятся числами, и опасно для тех, где кто-то написал 0, имея в виду «выключить»: ноль — это DebugLevel. Ошибка возвращается только когда строка не совпала ни с одним именем и не распарсилась как число в границах int8; сообщение вида Unknown Level String несёт саму строку конфига внутри, так что опечатка видна без отладчика. Сюрприз, о котором стоит знать заранее: пустая строка ошибкой не считается — ParseLevel("") возвращает NoLevel без ошибки, потому что у NoLevel пустое строковое представление, и сравнение срабатывает. Для конфигов в yaml и json есть второй вход: тип zerolog.Level реализует encoding.TextUnmarshaler, поэтому парсер, умеющий разворачивать текстовые поля, вызовет UnmarshalText сам — уровень заполнится строкой из файла без ручного вызова, а невалидное значение вернётся ошибкой парсинга. Писать собственную таблицу if-ов над именами уровней не нужно никогда: словарь уже есть, он протестирован и совпадает с тем, что zerolog пишет в поле level.
lvl, _ := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")) — и опечатка ifno вместо info уезжает в прод. Функция вернёт NoLevel (6) и ошибку; второй результат проигнорирован, SetGlobalLevel(6) встаёт на место, и весь Info, Warn и Error замолкают: процесс жив, health-check зелёный, а лента пуста. Если ошибка парсинга проглатывается, то провал конфигурации выглядит не как падение, а как тишина, и искать её будут в сборщике логов, а не на старте. Лечение — не проходить мимо err: Fatal на старте или фолбэк на Info с Warn-записью о подмене. Диагностика уже работающего сервиса — zerolog.GlobalLevel() в подтверждении при старте: принятый порог виден в первой же строке, и опечатка обнаруживается до первого репорта от дежурного.
Dev против prod: writer и уровень меняются вместе#
Уровень и формат — не две независимые настройки, а одна: окружение выбирает пару целиком. Дев-терминал читает цветные строки — ConsoleWriter с Out в stdout и человекочитаемым TimeFormat из урока про консольный вывод; прод читает машиночитаемый JSON из stderr, куда смотрит сборщик. Если разнести их по разным флагам, рано или поздно появится комбинация «ConsoleWriter в проде»: цветовые коды в ленте, которую парсит Loki, и ни один тест этого не поймает, потому что оба режима по отдельности легальны. Соберите ветку в одну функцию: newLogger(env) возвращает готовый логгер, и внутри неё же ставится глобальный порог — Debug для разработки, Info для прода. По той же схеме живут семплеры: в проде прореживание, в деве полный поток, и это строка в той же ветке, а не отдельная настройка в другом модуле. Timestamp добавляется в контекст в обоих случаях: JSON-ленте нужен числовой time, консольному выводу — время, отформатированное под человека.
newLogger: ветка окружения в одном месте
GOfunc newLogger(env string) zerolog.Logger {
if env == "development" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
return zerolog.New(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "15:04:05",
}).With().Timestamp().Logger()
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
return zerolog.New(os.Stderr).With().Timestamp().Logger()
}Ветка по окружению — единственное место, где принимается решение о формате и пороге. ConsoleWriter с TimeFormat рисует читаемые строки в терминале разработчика; прод получает голый JSON в stderr — именно тот поток, которого ждёт сборщик. Timestamp в контексте одинаков для обеих веток, а глобальный порог ставится рядом с writer-ом: вместе настраиваются, вместе и проверяются на следующем релизе.
Viper и файлы конфигурации: строка — и всё#
Viper и соседи по конфиг-цеху работают так: прочитали файл, влили переменные окружения, отдали значение. Для zerolog важен только последний шаг — viper.GetString вернул строку, и дальше начинается территория zerolog: ParseLevel, SetGlobalLevel. Библиотека не знает про Viper и не должна: конфиг-менеджеры меняются, словарь уровней один. Если конфиг-парсер умеет encoding.TextUnmarshaler, путь короче: поле конфига типа zerolog.Level заполнится само — парсер вызовет UnmarshalText, внутри которого тот же ParseLevel, и невалидная строка всплывёт ошибкой парсинга файла там, где её чинить. Дефолты и тут решают: если ключ в файле отсутствует, GetString вернёт пустую строку — а пустая строка, как разобрано выше, это NoLevel, а не Info. Подставляйте дефолт до парсинга, а не после: ParseLevel над пустым чтением без дефолта — та самая тишина из «живого места», только через файл конфигурации вместо переменной. И держите в одном месте всю иерархию: окружение сильнее файла, файл сильнее дефолта — тогда спор «откуда взялся уровень» решается чтением одной функции, а не археологией по репозиторию.
Конфигурация — не только старт. Порог хранится в атомарной переменной, и SetGlobalLevel безопасен на живом процессе: сценарий с админ-эндпоинтом из урока про уровни — дежурный поднимает порог до Debug без деплоя, разбирается и опускает обратно. К этой схеме два правила. Первое: читать текущее значение нужно zerolog.GlobalLevel(), а не своей копией в конфиг-структуре — копия всегда врёт после ручного переключения. Второе: поднятый Debug надо опускать, и лучше не памятью дежурного, а таймаутом — обработчик эндпоинта возвращает порог через N минут, инцидент закрыт, объём вернулся в норму. Рядом с порогом на живом процессе уместны и семплеры из урока про sampling: всплеск Info затопил ленту — вкрученный на ходу семплер проредит поток без рестарта, потому что Sample и DisableSampling работают на копиях и атомарных флагах. Сигнал — тот же эндпоинт, только без сети: обработчик SIGHUP парсит строку и зовёт SetGlobalLevel, сам zerolog сигналов не слушает. И запишите в ранбук, какой эндпоинт меняет уровень: в три часа ночи это знание дороже любого бенчмарка.
Один сервис, три окружения#
| Окружение | Порог | Writer | Зачем так |
|---|---|---|---|
| development | Debug | ConsoleWriter в stdout | цветные строки, полный поток, время по часам |
| staging | Info | JSON в stderr | как прод, но шум виден раньше |
| production | Info или Warn | JSON в stderr и ротация файла | машину читает сборщик, диск живёт |
| инцидент | Debug временно | тот же, что в проде | SetGlobalLevel по эндпоинту, сброс по таймауту |
Чек-лист конфигурации логгера#
- уровень читается из окружения или конфига и применяется одним SetGlobalLevel при старте
- дефолт объявлен рядом с чтением: пустая строка — не Info, ParseLevel вернёт NoLevel
- ошибка ParseLevel не проглатывается: Fatal на старте или фолбэк с Warn-записью
- writer и уровень меняются вместе: ConsoleWriter — дев, JSON в stderr — прод
- принятый порог пишется в подтверждение при старте: zerolog.GlobalLevel() в первой строке
- ручка временного подъёма уровня есть в ранбуке: эндпоинт или сигнал со сбросом по таймауту
- семплеры настраиваются в той же ветке окружения, что и порог
- таблиц if-ов над именами уровней нет: словарь уже в ParseLevel
Программы этого урока исполняются локальным Go: go run main.go с вашей переменной окружения. Песочница сайта показывает программу на чистом Go, которая читает переменные окружения и печатает строки без zerolog — внешняя библиотека в песочнице не запускается. Разбирайте примеры построчно и соберите цепочку у себя: os.Getenv, ParseLevel, SetGlobalLevel — четыре строки, которые решают судьбу всех записей сервиса.
Проверка: словарь ParseLevel
Что вернёт zerolog.ParseLevel("INFO")?
Проверка: writer по окружению
Где должно жить решение «ConsoleWriter или JSON»?
Какие записи уйдут в ленту?
Прочитайте код и выберите, что он выведет:
lvl, _ := zerolog.ParseLevel("2")
zerolog.SetGlobalLevel(lvl)
log.Info().Msg("a")
log.Warn().Msg("b")Соберите цепочку настройки
Восстановите конвейер: чтение, парсинг, применение порога.
Программа настройки порога
Расставьте строки в правильном порядке: импорты, парсинг строки и применение порога.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: конфигурация
Что дальше по курсу#
Конфигурация собрана — дальше курс идёт в стыки. Следующий урок — мост с slog: стандартный структурный логгер появился в Go 1.21, часть библиотек принимает именно его хендлер, а записи должны уезжать в ту же JSON-ленту, что и zerolog. За ним — секреты: token, password и Authorization в полях, что скрывать всегда и как не потерять их в конфиге. Если конфигурация уже живёт в проде, сверьте её с чек-листом выше: порог, writer и ротация — три решения, которые принимаются один раз и правятся редко, и каждое из них обязано читаться из репозитория, а не из памяти администратора.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Конфигурация zerolog собирается один раз при старте: строка из окружения, дефолт, ParseLevel, SetGlobalLevel.
- ParseLevel знает имена уровней без учёта регистра и числа от -128 до 127; свои if-таблицы не нужны.
- ParseLevel на пустой строке возвращает NoLevel без ошибки: пустой конфиг — это не Info.
- Игнор ошибки ParseLevel поднимает порог до NoLevel: сервис жив, лента пуста.
- Writer и уровень меняются вместе: ConsoleWriter для дева, JSON в stderr для прода.
- zerolog.Level реализует encoding.TextUnmarshaler: yaml- и json-парсеры заполняют уровень сами.
- GlobalLevel() читает принятый порог, подтверждающая запись при старте закрывает споры.
- Живой уровень: эндпоинт или сигнал со сбросом по таймауту — из ранбука, не из памяти дежурного.
Домашнее задание
Соберите newLogger(env) из урока и запустите сервис при четырёх значениях LOG_LEVEL: unset, info, IFNO, warn — и запишите, сколько строк уходит в ленту в каждом. Добавьте подтверждение при старте с zerolog.GlobalLevel() и убедитесь, что опечатка видна в первой строке, а не на репорте от дежурного. Потом уберите проверку err и воспроизведите живое место: лента замолчала, процесс зелёный. Верните проверку. Затем добавьте обработчик, который поднимает порог до Debug по POST и опускает обратно по таймауту в минуту. Прогоните оба пути — поднятие без сброса и со сбросом — и приложите к PR заметку из трёх строк о том, какой уровень вступил в силу и почему.
Личные заметкиПусто
Вопросы и ответы по уроку
Как задать уровень логов через переменную окружения?
os.Getenv("LOG_LEVEL"), дефолт на пустую строку, zerolog.ParseLevel и zerolog.SetGlobalLevel — четыре строки при старте. ParseLevel понимает имена уровней без учёта регистра и числа, мусор возвращает ошибкой, и на старте её честно обрабатывают Fatal-ом. Подтверждающая запись с zerolog.GlobalLevel() в первой строке ленты покажет, какой порог реально вступил в силу.
Что возвращает zerolog.ParseLevel для пустой строки?
NoLevel (6) и nil — ошибки нет, потому что у NoLevel пустое строковое представление и сравнение со строкой-конфигом срабатывает. Это значит, что пустая переменная или отсутствующий ключ конфига без дефолта дадут порог, молча глушащий Info, Warn и Error. os.Getenv возвращает пустую строку и для неустановленной переменной, и для пустой, поэтому один дефолт накрывает оба случая: if level == "" { level = "info" } — до ParseLevel.
Как переключить уровень логов без перезапуска?
SetGlobalLevel атомарен и безопасен на живом процессе: админ-эндпоинт или сигнал парсит новую строку через ParseLevel и применяет порог, деплой не нужен. Текущее значение читается zerolog.GlobalLevel(), а поднятый Debug опускается таймаутом — иначе квартал спустя кто-то удивится объёму логов. Сценарий и правила гигиены разобраны в уроке про уровни.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила