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

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

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

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

Динамические уровни: AtomicLevel

zap AtomicLevel решает проблему, знакомую каждому, кто ловил баг на проде ночью: уровень логов нужно поднять до Debug прямо сейчас, а рестарт процесса разорвёт соединения и потеряет состояние. Разберём привязку уровня к конфигу, смену на живом сервисе, защищённый HTTP-endpoint /loglevel и цену такой ручки в чужих руках.

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

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

Логгер, привязанный к атомарному уровню

GO
level := 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, список адресов), ограничение размера тела и запись в лог самого факта смены с инициатором. Ниже — обработчик целиком; он маленький, но каждая его строчка отвечает за реальный инцидент, который где-то уже случался.

Код

GO
func 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 ставят в каждый сервис либо выравнивают уровень через конфиг-сервис.

Смена уровня и конфиг: что переживает рестарт#

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.InfoLevel0
"warn"zapcore.WarnLevel1
"error"zapcore.ErrorLevel2
"dpanic"zapcore.DPanicLevel3
"panic"zapcore.PanicLevel4
"fatal"zapcore.FatalLevel5

Что выведет логгер?

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

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?

Соберите привязку уровня

Восстановите три строки: конструктор, привязка к конфигу, смена на живом сервисе.

level := zap.() cfg := zap.NewProductionConfig() cfg.Level = // включить подробности без рестарта: level.(zapcore.DebugLevel)

Соберите поток: от уровня до Debug-записи

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

Строки

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

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

Карточки: динамические уровни

Термин

zap.AtomicLevel

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

Обёртка над атомарным уровнем: реализует LevelEnabler, копии делят одну переменную — SetLevel виден всем логгерам на ней.

1 / 6

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

Ручка уровня живёт рядом с логирующим 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 через десять минут после последней смены.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Динамические уровни: AtomicLevel)

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

Можно ли менять уровень у логгера из пресета NewProduction без своего конфига?

Да: поле Level в пресетах — настоящий AtomicLevel. Возьмите cfg := zap.NewProductionConfig(), сохраните level := cfg.Level, соберите логгер через cfg.Build() — и вызывайте level.SetLevel в любой момент.

Какой уровень ставить по умолчанию?

Info для прода, Debug для дев-окружения — выбор живёт в конфиге окружения. Ручка /loglevel — временное средство: окно Debug закрывается возвратом к Info вручную или таймером, а постоянная потребность в подробностях решается правкой конфига.

Почему после SetLevel(Debug) записи всё равно частично пропадают?

Если у ядра стоит сэмплирование, оно продолжает прореживать повторяющиеся сообщения и на Debug. Настройки сэмплера задаются при сборке логгера: для полной выкладки соберите второй логгер без сэмплера в отдельный файл.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси