Содержание урока · 12 разделов
- Zap AtomicLevel: обёртка над уровнем
- Динамическая смена уровня логов: SetLevel
- HTTP-endpoint /loglevel: POST для админа
- Зачем это в проде: дебаг без рестарта
- Смена уровня и конфиг: что переживает рестарт
- Debug-окно и сэмплирование
- Чек-лист ручки /loglevel
- Строки, которые понимает ParseLevel
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сбой в 23:47: платёжный сервис отвечает 500 на каждый пятый запрос, но в логах только Info — «request accepted», «request served». Подозрение падает на новую ветку валидации купонов, и чтобы её увидеть, нужен Debug. Классический путь — включить уровень в конфиге, пересобрать, выкатить: минус тёплые соединения к базе, минус состояние в памяти, плюс десять минут раскатки, пока инцидент тлеет. Сервисы на zap умеют короче: уровень логов может быть атомарной переменной, которую ядро читает на каждой записи, — и её можно менять изнутри живого процесса, хоть по HTTP. Подняли до Debug, воспроизвели, посмотрели, вернули Info — процесс ни разу не перезапускался, трафик не дрогнул. В этом уроке разберём тип zap.AtomicLevel, его место в конфиге, смену уровня на лету и риски, из-за которых в половине команд эту ручку запрещают. Пока процесс перезапускается, теряется и ещё одна вещь, о которой часто забывают: пул соединений прогревается заново, и первые секунды после подъёма сервис отвечает медленнее — для инцидента это ещё одна порция пятисотых.
Zap AtomicLevel: обёртка над уровнем#
zap.NewAtomicLevel() возвращает значение типа zap.AtomicLevel — тонкую обёртку над атомарным целым. Контракт у неё тот же, что у любого фильтра ядра: интерфейс zapcore.LevelEnabler с методом Enabled(zapcore.Level) bool — именно его ядро вызывает на каждой записи, решая, пропускать ли событие дальше. Внутри обёртки — указатель на atomic int32, поэтому все копии значения делят одну переменную: SetLevel с любой копии мгновенно виден всем логгерам, собранным на этом уровне. Это не случайность дизайна: поле cfg.Level в zap.Config имеет тип zap.AtomicLevel, а не числовой уровень, — конфиг задуман так, чтобы динамическая смена уровня логов была штатной опцией, а не хаком. По умолчанию NewAtomicLevel даёт Info; нужен другой старт — NewAtomicLevelAt(zap.DebugLevel). Пара zap AtomicLevel встречается в проекте дважды: в поле конфига при старте и в переменной, которую держат для ручки смены, — и оба раза это одно значение: логгер собирается на нём, а ссылка живёт рядом с конфигурацией.
Логгер, привязанный к атомарному уровню
GOlevel := zap.NewAtomicLevel() // Info по умолчанию
cfg := zap.NewProductionConfig()
cfg.Level = level
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
// позже, из любого места процесса:
level.SetLevel(zapcore.DebugLevel)
logger.Debug("теперь видно и такие записи")Build захватил ту же переменную, что лежит в level: ядро спрашивает Enabled у неё на каждой записи, поэтому SetLevel после Build действует на уже собранный логгер — пересоздавать ничего не нужно. В JSON Debug-записи пойдут в те же приёмники, куда писался Info: меняется фильтр, а не вывод. Понадобится импорт zapcore рядом с zap. Пример целиком запускается только на локальном Go — песочница курса zap не исполняет.
Динамическая смена уровня логов: SetLevel#
Динамическая смена уровня логов — это один вызов level.SetLevel(zapcore.WarnLevel). Аргумент — обычный zapcore.Level: от Debug(-1) до Fatal(5). Текущее значение читается методом Level(), строковое имя для админки — методом String(). Момент применения стоит понимать точно: решение «пропускать ли запись» принимается в момент вызова Info или Debug, а не заранее, поэтому никаких перезагрузок и прогрева не существует — следующая же запись оценивается по новому порогу. При этом сам факт смены никуда не пишется: логгер не знает, что его фильтр изменили. Приучите себя логировать смену вручную: кто, когда и с какого уровня на какой перешёл. Без такой записи ночной Debug, забытый включённым до утра, превращается в загадку «почему диск кончился в четыре часа». Уровень при этом — операционный параметр, а не константа кода: он меняет поведение наблюдения, не трогая данные, поэтому и обращаться с ним стоит как с изменением конфигурации — с логом, автором и правом откатить.
Проверка: что меняет SetLevel
logger собран через cfg.Build() с атомарным уровнем. Что попадёт в вывод после level.SetLevel(zapcore.DebugLevel)?
HTTP-endpoint /loglevel: POST для админа#
Смена уровня по HTTP — самый практичный способ достать до ручки из админки или curl-а. Endpoint принимают методом POST: запрос меняет состояние сервиса, а GET по семантике обязан быть безопасным и его кешируют прокси. Тело — короткая строка уровня, которую разбирает zapcore.ParseLevel. Обязательные атрибуты: проверка прав (токен, mTLS, список адресов), ограничение размера тела и запись в лог самого факта смены с инициатором. Ниже — обработчик целиком; он маленький, но каждая его строчка отвечает за реальный инцидент, который где-то уже случался.
Код
GOfunc loglevelHandler(level zap.AtomicLevel, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || !isAdmin(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, 32))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
lvl, err := zapcore.ParseLevel(string(body))
if err != nil {
http.Error(w, "unknown level", http.StatusBadRequest)
return
}
level.SetLevel(lvl)
logger.Warn("log level changed",
zap.String("level", lvl.String()),
zap.String("remote", r.RemoteAddr),
)
}
}
// mux.Handle("/loglevel", loglevelHandler(level, logger))ParseLevel принимает строки debug, info, warn, error, dpanic, panic, fatal — на неизвестную отвечает ошибкой, которую мы превращаем в 400. LimitReader режет тело: даже закрытый endpoint не обязан читать мегабайты. Смена пишется на Warn с инициатором — это изменение поведения сервиса, оно должно оставлять след. Функцию isAdmin подставьте свою: токен, mTLS, внутренняя сеть.
Зачем это в проде: дебаг без рестарта#
Сценарии, где ручка окупается сразу. Первый — локализация бага на живом трафике: гипотеза «падает валидация купонов» проверяется пятиминутным Debug-окном, а не полчаса выката. Второй — переходные периоды: миграция базы, смена балансера, свежий релиз — на час поднимается Debug, потом снимается. Третий, обратный: шумный модуль заваливает поток записями — на живом сервисе уровень поднимается до Warn без пересборки, пока команда готовит патч. Моё мнение, спорное для части команд: /loglevel должен быть в каждом сервисе по умолчанию — но закрытый авторизацией, с логированием смены и с таймером, который сам возвращает Info через десять минут, если оператор забыл. Без таймера ручка превращается в мину: Debug, включённый в марте, находят в декабре, когда кончается диск. Отдельный случай — многослойные сервисы: ручка меняет уровень только вашего процесса и на соседние не распространяется. Если трассировать нужно всю цепочку, endpoint ставят в каждый сервис либо выравнивают уровень через конфиг-сервис.
Неавторизованный /loglevel даёт два рычага. Поставить Fatal — и сервис перестаёт писать ошибки: инцидент становится невидимым для алертов, построенных на уровнях. Поставить Debug на гигабайтном трафике — и утопить диски, ротацию и систему сбора: lumberjack прореживание не спасёт, а сэмплирование продолжит ронять часть Debug. Минимум защиты: авторизация, POST вместо GET, логирование смены.
Смена уровня и конфиг: что переживает рестарт#
SetLevel меняет память процесса: рестарт вернёт уровень из YAML, переменных окружения или дефолта пресета. Это осознанный дизайн, и он удобнее, чем кажется: временный debug не прилипает к окружению навсегда, а постоянное желание «всегда видеть Debug у billing» — повод править конфиг, а не дёргать ручку. Если устойчивость нужна, уровень хранят снаружи: переменная окружения на старте или ключ в config-сервисе, который приложение читает при подъёме. Второй нюанс — несколько ядер: если логгеры собраны через tee, у каждого ядра свой LevelEnabler, и ваша переменная меняет только то ядро, которому её отдали. Держите один AtomicLevel как источник истины, если явно не хотите раздельных фильтров. Наследники тут без сюрпризов: потомки от With и Named делят ядро с родителем и собственных фильтров не имеют, поэтому одна смена уровня охватывает и их — модульных настроек по умолчанию не существует.
Debug-окно и сэмплирование#
Отдельного разговора стоит связь динамического уровня с сэмплированием из урока про NewSamplerWithOptions. Сэмплер сидит над ядром и прореживает повторяющиеся сообщения по счётчику уровень-плюс-текст; включение Debug ручкой не трогает его правила. Итог честный: после подъёма уровня вы увидите первые записи каждого вида, а хвост — с пропусками. Для короткого окна это нормально: цель окна — подтвердить гипотезу, а не собрать полную выкладку. Если нужна полнота, соберите на время диагностики второй логгер без сэмплера в отдельный файл — и уберите, когда закончите. Обратная связка тоже полезна: ручка уровня — безопасный способ снизить шум, когда сэмплер настроить некогда. Практическое правило: Debug-окно оценивают по первым записям каждого вида, а не по полноте потока. Если гипотезе нужен полный массив — это работа для воспроизведения на стенде, а не для прод-ручки.
Чек-лист ручки /loglevel#
- POST и авторизация: токен админа, mTLS или список адресов — анонимная смена уровня запрещена.
- Ограничение тела: io.LimitReader — принимаем короткую строку уровня, а не что попало.
- zapcore.ParseLevel с ответом 400 на неизвестную строку: без самодельных парсеров.
- Логирование смены на Warn: кто, когда, с какого уровня на какой.
- Таймер возврата к Info: забытый Debug закрывается сам.
- Понимание сэмплирования: Debug будет неполным, и это ожидаемо.
Строки, которые понимает ParseLevel#
| Строка | Уровень | Число |
|---|---|---|
| "debug" | zapcore.DebugLevel | -1 |
| "info" | zapcore.InfoLevel | 0 |
| "warn" | zapcore.WarnLevel | 1 |
| "error" | zapcore.ErrorLevel | 2 |
| "dpanic" | zapcore.DPanicLevel | 3 |
| "panic" | zapcore.PanicLevel | 4 |
| "fatal" | zapcore.FatalLevel | 5 |
Что выведет логгер?
Прочитайте код и выберите, что он выведет:
level := zap.NewAtomicLevelAt(zap.WarnLevel)
cfg := zap.NewProductionConfig()
cfg.Level = level
logger, _ := cfg.Build()
level.SetLevel(zapcore.InfoLevel)
logger.Info("deploy finished", zap.String("version", "1.7.2"))Проверка: почему POST
Почему для endpoint смены уровня обычно выбирают POST, а не GET?
Соберите привязку уровня
Восстановите три строки: конструктор, привязка к конфигу, смена на живом сервисе.
Соберите поток: от уровня до Debug-записи
Расставьте строки в порядке выполнения программы.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: динамические уровни
NewDevelopmentConfig и NewProductionConfig кладут в поле Level настоящие AtomicLevel: захватите cfg.Level до Build в переменную и меняйте потом. При чтении конфига из YAML тоже всё честно — у AtomicLevel есть UnmarshalText, строка info разложится в уровень сама.
Что дальше по курсу#
Ручка уровня живёт рядом с логирующим middleware: в уроке про логирование HTTP-запросов /loglevel встраивается в тот же роутер, что и админские маршруты. Цену лишнего Debug в цифрах — аллокации и пропускную способность — меряют и обсуждают в уроке про производительность zap, а про связь уровней с алертами и метриками честно сказано в материале про логи и наблюдаемость. Карта остальных тем — на странице курса. Ручка уровня — последний кирпич операционной зрелости логов: дальше идут перехватчики gRPC и эксплуатация, но с ней ночная смена уже проходит без рестартов.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- cfg.Level — это zap.AtomicLevel: атомарная обёртка, которую ядро опрашивает на каждой записи.
- SetLevel действует мгновенно на уже собранный логгер: пересборка и рестарт не нужны.
- /loglevel — POST-эндпоинт с авторизацией, ParseLevel, лимитом тела и логированием смены.
- Смена уровня не переживает рестарт: временный Debug не прилипает, устойчивость — через конфиг.
- Сэмплирование продолжает прореживать Debug: полноты ручка не даёт.
- Открытая ручка — риск: Fatal прячет ошибки, Debug топит диски и систему сбора.
- Сам факт смены пишите в лог: кто, когда, с какого уровня на какой.
Домашнее задание
Соберите локально сервис с атомарным уровнем: NewAtomicLevel, привязка к cfg.Level, обработчик POST /loglevel с проверкой токена и логированием смены на Warn. Поднимите уровень до Debug curl-запросом, убедитесь по JSON-выводу, что Debug-записи пошли, верните Info и проверьте, что они пропали. Добавьте таймер, который сам возвращает Info через десять минут после последней смены.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли менять уровень у логгера из пресета NewProduction без своего конфига?
Да: поле Level в пресетах — настоящий AtomicLevel. Возьмите cfg := zap.NewProductionConfig(), сохраните level := cfg.Level, соберите логгер через cfg.Build() — и вызывайте level.SetLevel в любой момент.
Какой уровень ставить по умолчанию?
Info для прода, Debug для дев-окружения — выбор живёт в конфиге окружения. Ручка /loglevel — временное средство: окно Debug закрывается возвратом к Info вручную или таймером, а постоянная потребность в подробностях решается правкой конфига.
Почему после SetLevel(Debug) записи всё равно частично пропадают?
Если у ядра стоит сэмплирование, оно продолжает прореживать повторяющиеся сообщения и на Debug. Настройки сэмплера задаются при сборке логгера: для полной выкладки соберите второй логгер без сэмплера в отдельный файл.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси