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

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

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

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

Конфигурация по окружению

Пороги из урока про уровни задавались константой в коде, а это значит: чтобы сделать сервис тише, надо пересобрать бинарник. Взрослый сервис читает уровень из окружения — переменная LOG_LEVEL, флаг старта или строка в конфиге — и настраивает zerolog ровно один раз при старте процесса. В этом уроке собирается вся цепочка: строка из окружения превращается в zerolog.Level через zerolog.ParseLevel, глобальный порог встаёт на место одним вызовом, writer выбирается по окружению — цветной ConsoleWriter в деве, JSON в stderr в проде. Плюс грабли, на которых ломают прод: проглоченная ошибка парсинга молчит громче паники, а пустая строка конфига — это не Info, а совсем другой уровень. Отдельно — writer по окружению, файлы конфигурации и живая смена порога без деплоя.

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

Порог, зашитый константой, работает ровно до первого неудобного дня: дебаг нужен на минуту, а путь к нему лежит через сборку, тесты и выкладку. Переменная окружения — самый дешёвый канал конфигурации: она есть в манифесте, в 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-запись с принятым уровнем пишется сразу после настройки: три секунды работы, а утренние споры о том, какой порог реально вступил в силу, закрываются первой строкой ленты. И запомните порядок: чтение, дефолт, парсинг, применение, подтверждение — любое другое место для дефолта рано или поздно даст окружение, в котором конфиг есть, а порог другой.

Полная цепочка настройки при старте

GO
package 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.

Dev против prod: writer и уровень меняются вместе#

Уровень и формат — не две независимые настройки, а одна: окружение выбирает пару целиком. Дев-терминал читает цветные строки — ConsoleWriter с Out в stdout и человекочитаемым TimeFormat из урока про консольный вывод; прод читает машиночитаемый JSON из stderr, куда смотрит сборщик. Если разнести их по разным флагам, рано или поздно появится комбинация «ConsoleWriter в проде»: цветовые коды в ленте, которую парсит Loki, и ни один тест этого не поймает, потому что оба режима по отдельности легальны. Соберите ветку в одну функцию: newLogger(env) возвращает готовый логгер, и внутри неё же ставится глобальный порог — Debug для разработки, Info для прода. По той же схеме живут семплеры: в проде прореживание, в деве полный поток, и это строка в той же ветке, а не отдельная настройка в другом модуле. Timestamp добавляется в контекст в обоих случаях: JSON-ленте нужен числовой time, консольному выводу — время, отформатированное под человека.

newLogger: ветка окружения в одном месте

GO
func 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Зачем так
developmentDebugConsoleWriter в stdoutцветные строки, полный поток, время по часам
stagingInfoJSON в stderrкак прод, но шум виден раньше
productionInfo или WarnJSON в stderr и ротация файламашину читает сборщик, диск живёт
инцидентDebug временнотот же, что в продеSetGlobalLevel по эндпоинту, сброс по таймауту

Чек-лист конфигурации логгера#

  • уровень читается из окружения или конфига и применяется одним SetGlobalLevel при старте
  • дефолт объявлен рядом с чтением: пустая строка — не Info, ParseLevel вернёт NoLevel
  • ошибка ParseLevel не проглатывается: Fatal на старте или фолбэк с Warn-записью
  • writer и уровень меняются вместе: ConsoleWriter — дев, JSON в stderr — прод
  • принятый порог пишется в подтверждение при старте: zerolog.GlobalLevel() в первой строке
  • ручка временного подъёма уровня есть в ранбуке: эндпоинт или сигнал со сбросом по таймауту
  • семплеры настраиваются в той же ветке окружения, что и порог
  • таблиц if-ов над именами уровней нет: словарь уже в ParseLevel

Проверка: словарь ParseLevel

Что вернёт zerolog.ParseLevel("INFO")?

Проверка: writer по окружению

Где должно жить решение «ConsoleWriter или JSON»?

Какие записи уйдут в ленту?

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

lvl, _ := zerolog.ParseLevel("2")

zerolog.SetGlobalLevel(lvl)

log.Info().Msg("a")
log.Warn().Msg("b")

Соберите цепочку настройки

Восстановите конвейер: чтение, парсинг, применение порога.

level := os.("LOG_LEVEL") if level == "" { level = "info" } lvl, err := zerolog.(level) if err != nil { log.Fatal().Err(err).Msg("bad LOG_LEVEL") } zerolog.(lvl)

Программа настройки порога

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

Строки

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

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

Карточки: конфигурация

Термин

LOG_LEVEL

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

Строка окружения для порога: os.Getenv, пустое значение накрывается дефолтом до парсинга.

1 / 6

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

Конфигурация собрана — дальше курс идёт в стыки. Следующий урок — мост с 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 заметку из трёх строк о том, какой уровень вступил в силу и почему.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Конфигурация по окружению)

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

Как задать уровень логов через переменную окружения?

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 опускается таймаутом — иначе квартал спустя кто-то удивится объёму логов. Сценарий и правила гигиены разобраны в уроке про уровни.

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

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

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

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

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

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

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