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

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

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

Урок 17 из 25 ~18 мин +20 XP

Логирование задач: zap и slog рядом

Логирование cron сводится к двум потокам: события самого планировщика — старт, стоп, добавление записи, тик — и записи, которые делают ваши задачи. У robfig/cron v3 есть для этого интерфейс cron.Logger и опция WithLogger, но по умолчанию планировщик пишет в io.Discard, а ни zap, ни slog его интерфейс из коробки не реализуют — нужен адаптер на десять строк. Разберём, какие события отдаёт библиотека, как подключить структурный логгер, какие поля делают разбор инцидента быстрым и где проходит граница между полезным логом и шумом.

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

Инцидент для старта: ночью пропали выгрузки, дежурный открыл логи и увидел строки вида func1 0x7f3a2c, повторяющиеся в случайные моменты. В них нет ни имени задачи, ни выражения, ни результата — только адрес анонимной функции из AddFunc. Понять, какая задача работала, оказалось возможно только по стеку внутри pprof-снимка, и это заняло дольше, чем сам сбой. Классическая ошибка — считать, что планировщик сам расскажет о себе: в robfig/cron v3 события логгера по умолчанию отбрасываются, а задачи пишут в общий лог без общих полей. Рабочая схема складывается из двух потоков: события библиотеки через cron.Logger и структурные записи самих задач с полем job — обе половины настраиваются один раз на старте и дальше работают на весь сервис. Этот урок закрывает первую половину, а про наблюдаемость в числах говорит следующий — про метрики задач.

Логирование cron: события планировщика и тишина по умолчанию#

Библиотека объявляет маленький интерфейс: type Logger interface { Info(msg string, keysAndValues ...interface{}); Error(err error, msg string, keysAndValues ...interface{}) } и опцию cron.WithLogger(l). Логирование cron начинается с честного факта: если WithLogger не передать, логгером будет PrintfLogger поверх log.New(io.Discard, ...), то есть все события уходят в никуда — ни паники Recover, ни тики не увидят свет. Второй факт: PrintfLogger печатает только ошибки, а для Info-событий нужен VerbosePrintfLogger — иначе вы подключили логгер, который молчит на тиках. Какие события библиотека отдаёт: start и stop при запуске и остановке, added и removed при регистрации и снятии записи, wake при пробуждении цикла и run — строка на каждое срабатывание с номером записи и моментом следующего тика. Это удобный каркас диагностики: вопрос «а задача вообще запланирована» закрывается строками added и run. Сами события несут только идентификаторы и время — бизнес-детали добавляете вы.

Планировщик с видимыми событиями

GO
func main() {
    c := cron.New(cron.WithChain(
        cron.Recover(cronLogger{log.New(os.Stdout, "", log.LstdFlags)}),
    ), cron.WithLogger(cronLogger{log.New(os.Stdout, "", log.LstdFlags)}))

    c.AddFunc("*/15 * * * *", pullOrders)
    c.Start()
    log.Println("планировщик запущен")
    select {}
}

Один и тот же логгер передаётся в WithLogger для событий библиотеки и в Recover для паник — оба потока идут в одно место и бьются в одном поиске. Без VerbosePrintfLogger событие run осталось бы невидимым даже при подключённом WithLogger. Здесь для наглядности стандартный log.Logger; дальше его место занимает структурный логгер.

Проверка: что печатает PrintfLogger

Планировщик создан с WithLogger(cron.PrintfLogger(l)), задача отпаниковала, Recover поймал. Что увидим в l?

zap и slog: адаптер на десять строк#

Ни zap, ни slog не реализуют cron.Logger из коробки, поэтому между ними и планировщиком ставится адаптер — структура с двумя методами. Для slog связка естественна: методы Info и Error принимают msg и пары ключ-значение, и slog.Info(msg, kv...) с ними совпадает почти дословно. Для zap удобнее SugaredLogger: Infow и Errorw принимают те же пары, а в Errorw ошибку передают первой парой err. Адаптер лучше писать один на сервис и держать рядом с конфигурацией логгера — место, где его найдут все новые задачи. Уровни переводятся честно: события библиотеки — это Info, ошибки Recover и ошибок парсинга — Error. Если сервис уже ведёт поля окружения через zap.With и Named, адаптеру можно присвоить имя cron, чтобы события планировщика отличались в поиске от HTTP-запросов. Базовую семантику уровней напомнит урок про уровни логов, а детали полей — урок про структурные поля zap.

Адаптеры для slog и zap

GO
type slogCronLogger struct{ l *slog.Logger }

func (s slogCronLogger) Info(msg string, kv ...interface{}) {
    s.l.Info(msg, kv...)
}

func (s slogCronLogger) Error(err error, msg string, kv ...interface{}) {
    s.l.Error(msg, append([]interface{}{"err", err}, kv...)...)
}

type zapCronLogger struct{ l *zap.SugaredLogger }

func (z zapCronLogger) Info(msg string, kv ...interface{}) {
    z.l.Infow(msg, kv...)
}

func (z zapCronLogger) Error(err error, msg string, kv ...interface{}) {
    z.l.Errorw(msg, append([]interface{}{"err", err}, kv...)...)
}

Два адаптера — весь мост между интерфейсом библиотеки и структурными логгерами. Разница только в способе передать ошибку: slog ждёт её парой ключ-значение, zap — первой парой в Errorw. Оба адаптера потокобезопасны, поэтому один экземпляр разделяется планировщиком, задачами и Recover без оговорок.

Проверка: имя задачи в логе

Десять задач пишут в общий slog-логгер. Как в записях различать задачи?

Поля лога задач: job, длительность, исход#

Лог задачи становится полезным, когда поля повторяются от задачи к задаче и совпадают с метриками. Минимальный набор: job — имя из конфига, длительность тика, исход — ok или описание ошибки, при ретраях — номер попытки. Старт тика логировать целиком нет смысла: событие run от планировщика уже есть, а повторная строка только удваивает объём; информативнее запись после завершения. Уровни распределяются так: успешный тик — Info, пропуск и задержка от обёрток — Warn, ошибка и паника — Error. Выигрывает от того же правила, что и HTTP: одна запись на операцию со всеми полями, а не россыпь Println по ходу. Ошибки из глубины задачи оборачивают через fmt.Errorf с контекстом — механику %w и errors.Is разбирал урок про оборачивание ошибок. Согласованность с метриками проверяется просто: те же значения поля job и те же исходы, что и в CounterVec, — тогда связка «алерт поднял метрику, лог нашёл причину» работает без ручного сопоставления.

Что напечатает slog-запись?

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

logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
log := logger.With("job", "cleanup")
log.Info("тик завершён", "dur", "1.2s")

Что логировать на каком уровне#

СобытиеУровеньПоляКто пишет
старт и стоп планировщикаInfoсобытие от библиотекиcron.Logger через WithLogger
регистрация и снятие записиInfojob, идентификатор EntryIDcron.Logger через WithLogger
завершение тикаInfojob, длительность, исходкод задачи
пропуск или задержка тикаWarnjob, причинаобёртки Skip и Delay
ошибка и паника задачиErrorjob, err, попыткакод задачи и Recover

Логи рядом с метриками: одна история инцидента#

Логи задач go-сервиса живут в паре с метриками: одна сторона показывает, что деградация есть, вторая — почему. Рабочий сценарий: алерт поднялся по росту ошибок выгрузки, дежурный фильтрует логи по полю job=pull-orders и окну времени алерта, находит строку ошибки с кодом внешнего сервиса. Эта связка работает, только если поля совпадают: значение job в логе равно значению лейбла job в CounterVec, а исход в логе совпадает с лейблом status. Приём корреляции логов и метрик на примере веб-сервиса разобран в уроке про логи и метрики; для cron он ложится один в один. Второй уровень связи — трейс: если сервис раздаёт request id на HTTP-запросы из урока про request id, задачам, которые дёргают API, полезно передавать свой id вызова — тогда лог задачи и лог внешнего запроса сшиваются. Ошибки и стектрейсы в zap — отдельная настройка, которую подробно делал урок про ошибки и стеки.

Шум против пользы: спорное про лог на каждый тик#

Спорная привычка, которую стоит оспорить: логировать завершение каждого тика каждой задачи на Info. Двадцать задач с пятиминутным интервалом дают почти шесть тысяч записей в сутки только о благополучных тиках, и в них тонут записи о том, что действительно случилось. Мера разумности простая: лог должен уменьшать время разбора, а не увеличивать объём поиска. Для стабильных задач достаточно событий библиотеки — run в verbose-логгере показывает, что тик состоялся, без вашей записи; свою строку пишут при отклонении от нормы: длительность выше порога, ошибка, повтор. Второй инструмент против шума — сэмплирование: если запись нужна на каждый тик, например для аудита, их гасят сэмплером из урока про сэмплирование zap. Граница, которую нельзя переступить: Warn и Error сэмплировать не стоит — это те записи, ради которых лог существует. Баланс пересматривают после инцидентов: если дежурный листает девять экранов ради одной строки ошибки, настройки шумят.

Чеклист лога задач#

  • WithLogger подключён, события start, run, added видны в общем поиске.
  • Адаптер cron.Logger один на сервис: для slog или zap, рядом с конфигом логгера.
  • Поле job у каждой задачи из конфига, значение совпадает с лейблом метрики.
  • Поля записи: длительность, исход, попытка — без персональных данных и токенов.
  • Уровни по правилу: Info — завершение и отклонения, Warn — пропуски, Error — ошибки и паники.
  • Записи о благополучных тиках стабильных задач не пишутся или сэмплируются.

Соберите связку slog и планировщика

Впишите создание текстового хендлера, опцию логгера и метод для ошибок в адаптере.

logger := slog.New(slog.(os.Stdout, nil)) c := cron.New(cron.(slogCronLogger{logger})) c.AddFunc("0 2 * * *", digest) func (s slogCronLogger) Error(err error, msg string, kv ...interface{}) { s.l.(msg, append([]interface{}{"err", err}, kv...)...) }

Расставьте сборку логирующего планировщика

Расставьте строки в порядке файла: логгер, адаптер, планировщик с двумя подключениями, регистрация, старт.

Строки

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

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

Карточки: логи задач

Термин

cron.Logger

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

Интерфейс с Info и Error, который библиотека зовёт на событиях. Подключается опцией WithLogger; по умолчанию запись идёт в io.Discard.

1 / 6

Логирование cron — настройка на старте, которая потом работает на весь сервис: WithLogger открывает события планировщика, адаптер переносит их в zap или slog, а поле job у каждой задачи сшивает записи в единый поток, совместимый с метриками. Правила полей повторяют HTTP-мир: идентификаторы вместо содержимого, одна запись на тик, уровни по факту события, Warn и Error без сэмплирования. Дальше по маршруту: метрики задач — вторая половина наблюдаемости, к которой лог теперь готов, и graceful shutdown — как остановка сервиса отражается в логе. Если возвращались с пропуска — обёртки wrappers объясняют, откуда берутся записи о пропусках и паниках, а конфиг и база — где живут имена задач для поля job. Полный маршрут — на странице курса. В домашнем задании — адаптер, поле job и связка алерта с логом.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • По умолчанию cron пишет в io.Discard: без WithLogger события планировщика не видны нигде.
  • PrintfLogger печатает только ошибки; для Info-событий start, run, added нужен VerbosePrintfLogger.
  • Адаптер cron.Logger — структура с Info и Error: для slog почти дословно, для zap — через SugaredLogger.
  • Поле job из конфига в дочернем логгере: совпадает с лейблом метрики, по нему ищут записи.
  • Поля записи: длительность, исход, попытка; без персональных данных, токенов и текстов сообщений.
  • Уровни: Info — завершение тика, Warn — пропуски, Error — ошибки и паники; шум стабильных тиков не пишут.

Домашнее задание

Подключите WithLogger через адаптер: напишите slogCronLogger с Info и Error, передайте его и в WithLogger, и в Recover. Добавьте каждой задаче поле job из конфига расписания и логируйте завершение тика с длительностью и исходом. Проверьте в выводе все события: start, run, added, запись задачи и панику из тестовой задачи. Затем заведите сэмплер для Info-записей шумной задачи и убедитесь, что Warn и Error при этом остаются без потерь. Финал — свяжите значение job с лейблом метрики из следующего урока: алерт по ошибкам должен находить лог за один фильтр.

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · Логирование задач: zap и slog рядом)

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

Почему события планировщика не видны без настройки?

Логгером по умолчанию служит PrintfLogger поверх log.Logger, пишущего в io.Discard: все Info и Error отбрасываются. Опция WithLogger подключает свой логгер, а PrintfLogger и VerbosePrintfLogger различаются тем, что первый печатает только ошибки.

Как логировать имя задачи, если AddFunc принимает анонимную функцию?

Имя задаёт ваш код: дочерний логгер с полем job через slog.With или zap.Named, создаваемый рядом с регистрацией задачи. Библиотека в событиях даёт только EntryID, поэтому осмысленное имя держит конфиг задач и структура из урока про AddJob.

Что делать с задачами, которые тикают каждые 30 секунд и шумят лог?

Записи о благополучных тиках стабильных задач не пишите или сэмплируйте: событие run от verbose-логгера уже показывает факт запуска. Warn и Error сэмплировать не стоит — они и есть материал разбора инцидентов.

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

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

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

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

Логирование в Go: структурные логи на slog

Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.

  • Логировать структурированно: уровни, ключи-атрибуты, группы
  • Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
  • Мигрировать со старого пакета log без переписывания всей кодовой базы
  • Держать производительность: Enabled, предвычисление, горячий путь