Содержание урока · 10 разделов
- Логирование cron: события планировщика и тишина по умолчанию
- zap и slog: адаптер на десять строк
- Поля лога задач: job, длительность, исход
- Что логировать на каком уровне
- Логи рядом с метриками: одна история инцидента
- Шум против пользы: спорное про лог на каждый тик
- Чеклист лога задач
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Инцидент для старта: ночью пропали выгрузки, дежурный открыл логи и увидел строки вида 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. Сами события несут только идентификаторы и время — бизнес-детали добавляете вы.
Планировщик с видимыми событиями
GOfunc 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
GOtype 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, — тогда связка «алерт поднял метрику, лог нашёл причину» работает без ручного сопоставления.
Кейс: сервис напоминаний логировал текст сообщения и адрес получателя на каждом тике — через месяц в логе лежали персональные данные пользователей, а лог хранился год. Правило для задач то же, что и для HTTP: логируйте идентификаторы, а не содержимое. ID напоминания, число получателей, код ошибки внешнего сервиса — достаточно для разбора; текст сообщения, токен и адрес — не логировать вовсе. Отдельный случай — ошибки внешних API: текст ошибки может содержать данные запроса, поэтому перед записью ошибку приводят к типу и берут из него код. Политика хранения логов — тема соседнего курса про эксплуатацию, но её минимум на стороне задач: поля без персональных данных и единый формат.
Что напечатает slog-запись?
Прочитайте код и выберите, что он выведет:
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
log := logger.With("job", "cleanup")
log.Info("тик завершён", "dur", "1.2s")Что логировать на каком уровне#
| Событие | Уровень | Поля | Кто пишет |
|---|---|---|---|
| старт и стоп планировщика | Info | событие от библиотеки | cron.Logger через WithLogger |
| регистрация и снятие записи | Info | job, идентификатор EntryID | cron.Logger через WithLogger |
| завершение тика | Info | job, длительность, исход | код задачи |
| пропуск или задержка тика | Warn | job, причина | обёртки Skip и Delay |
| ошибка и паника задачи | Error | job, 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 и планировщика
Впишите создание текстового хендлера, опцию логгера и метод для ошибок в адаптере.
Расставьте сборку логирующего планировщика
Расставьте строки в порядке файла: логгер, адаптер, планировщик с двумя подключениями, регистрация, старт.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: логи задач
Пакета cron и модулей slog и zap в песочнице нет, но механику адаптера и полей записи она закрывает: предикт о формате вывода переписывается на fmt.Println с теми же парами ключ-значение, а обёртка-логгер — обычная структура с двумя методами, которую песочница умеет исполнять через fmt.Println. Пакет log и log.New для PrintfLogger работают локально из модуля второго урока, там же ставятся zap и slog через go get. Упражнение для песочницы: функция, превращающая структурную запись в строку пар ключ-значение в формате текстового хендлера.
Логирование 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 с лейблом метрики из следующего урока: алерт по ошибкам должен находить лог за один фильтр.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему события планировщика не видны без настройки?
Логгером по умолчанию служит PrintfLogger поверх log.Logger, пишущего в io.Discard: все Info и Error отбрасываются. Опция WithLogger подключает свой логгер, а PrintfLogger и VerbosePrintfLogger различаются тем, что первый печатает только ошибки.
Как логировать имя задачи, если AddFunc принимает анонимную функцию?
Имя задаёт ваш код: дочерний логгер с полем job через slog.With или zap.Named, создаваемый рядом с регистрацией задачи. Библиотека в событиях даёт только EntryID, поэтому осмысленное имя держит конфиг задач и структура из урока про AddJob.
Что делать с задачами, которые тикают каждые 30 секунд и шумят лог?
Записи о благополучных тиках стабильных задач не пишите или сэмплируйте: событие run от verbose-логгера уже показывает факт запуска. Warn и Error сэмплировать не стоит — они и есть материал разбора инцидентов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Метрики задач: длительность и ошибки
- Graceful shutdown планировщика
- Утечки горутин в задачах
- Несколько инстансов: блокировки задач
- Wrappers: Recover, DelayIfStillRunning, Skip
- Динамическое расписание: добавление на лету
- Хранение расписания: конфиг и база
- Инспекция расписания: Entries и Remove
- Все курсы по Go
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь