Содержание урока · 10 разделов
- Метрики cron: три вопроса, три метрики
- Лейбл job и статусы: контракт с ограниченным набором
- Гистограмма длительности: бакеты под расписание
- Метрики планировщика и их вопросы
- Мониторинг задач go: алерты на ошибки и тишину
- Спорное: метрики вместо логов
- Чеклист метрик задач
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сценарий для старта: сервис выгрузок работал месяц, никто не заглядывал в его логи — туда и не было повода, ошибок там не появлялось. Потом выяснилось, что третья неделя прошла без единого тика: задача была снята при рефакторинге конфига и обратно не вернулась. Логи молчали, потому что молчание — это состояние, которое никто не записывает. Метрику, напротив, читают постоянно: график тиков, линия которого обрывается две недели назад, виден с первого экрана дашборда. Второй сценарий зеркальный: тики идут, но каждая выгрузка падает с ошибкой аутентификации, и растущая линия счётчика по статусам пересекает порог алерта ночью, а не при разборе жалобы клиента. Дальше — три метрики, которые закрывают почти все вопросы к планировщику.
Метрики cron: три вопроса, три метрики#
Метрики cron сводятся к трём вопросам, которые дежурный задаёт планировщику. Первый: тики вообще происходят и с каким исходом — отвечает счётчик типа CounterVec с лейблами job и status, по одному инкременту на завершённый тик. Второй: как долго работает задача — отвечает гистограмма HistogramVec с лейблом job и замером через time.Since в секундах. Третий: когда задача последний раз отработала успешно — отвечает Gauge с отметкой времени в формате Unix, по которому алерт на тишину строится одним выражением. Все три регистрируются пакетом client_golang и отдаются тем же обработчиком /metrics — механика реестра и promhttp разобрана в уроке про первый endpoint. Планировщик — не веб-сервер: тиков мало, длительность измеряется от секунд до часов. Разницу Counter, Gauge и Histogram разбирает урок про типы метрик, а здесь — как они ложатся на расписание.
Три метрики задач в реестре
GOvar taskTicks = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cron_task_ticks_total",
Help: "Тики задач по имени и исходу",
},
[]string{"job", "status"},
)
var taskDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "cron_task_duration_seconds",
Help: "Длительность тика задачи в секундах",
Buckets: []float64{0.5, 1, 5, 15, 60, 300, 900, 3600},
},
[]string{"job"},
)
var taskLastSuccess = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cron_task_last_success_unixtime",
Help: "Момент последнего успешного тика",
},
[]string{"job"},
)
func init() {
prometheus.MustRegister(taskTicks, taskDuration, taskLastSuccess)
}Имена начинаются с префикса cron_task_, чтобы в общем списке метрик планировщик отделялся от HTTP. Лейблы заданы при создании: job — имя из конфига задач, status — исход тика. Бакеты гистограммы объявлены вручную — от полсекунды до часа. Второй MustRegister той же метрики приводит к панике на старте: регистрация выполняется один раз рядом с конфигурацией.
Проверка: что попадает в лейбл status
В обёртке метрик решено записывать в лейбл status текст ошибки из внешнего API. К чему это приведёт?
Лейбл job и статусы: контракт с ограниченным набором#
Лейбл job берётся из того же источника, что и поле в логе, — из конфига задач, о котором писал урок про расписание из конфига. Совпадение значений не украшение: связка с логом задач сшивается без ручного сопоставления. Значения лейбла status держат закрытым списком: ok — тик завершился, error — задача сообщила о неудаче, panic — обёртка поймала панику, skip — тик пропущен обёрткой SkipIfStillRunning из урока про wrappers. Здесь стоит произнести честное ограничение: интерфейс Job в библиотеке объявляет метод Run без возврата ошибки, поэтому обёртка видит панику — и только панику. Бизнес-ошибку фиксирует либо сама задача, инкрементируя счётчик с лейблом error, либо ваш контракт с приведением типа в обёртке. Механика лейблов — одна метрика, много измерений из короткого списка — разобрана в уроке про лейблы.
Обёртка с замером длительности и статусом
GOtype meteredJob struct {
name string
job cron.Job
}
func (m meteredJob) Run() {
start := time.Now()
defer func() {
taskDuration.WithLabelValues(m.name).Observe(time.Since(start).Seconds())
if r := recover(); r != nil {
taskTicks.WithLabelValues(m.name, "panic").Inc()
panic(r) // отдаём панику дальше — в Recover из цепочки
}
taskTicks.WithLabelValues(m.name, "ok").Inc()
taskLastSuccess.WithLabelValues(m.name).Set(float64(time.Now().Unix()))
}()
m.job.Run()
}
c.AddJob("0 2 * * *", meteredJob{name: "nightly-digest", job: cron.FuncJob(nightlyDigest)})Замер и запись живут в отложенной функции: она выполняется и при панике, поэтому гистограмма получает длительность даже аварийного тика. Recover в обёртке помечает статус panic, инкрементирует счётчик и перезапускает панику — дальше по цепочке её ловит cron.Recover и пишет стек в лог. Порядок в WithChain прежний: Recover первым, то есть снаружи, обёртка метрик — сразу за ним. Статус ok здесь означает лишь отсутствие паники; бизнес-ошибку задача считает собственным инкрементом с лейблом error.
Проверка: бакеты против длинной задачи
Гистограмма создана с бакетами по умолчанию — от 0.005 до 10 секунд. Ночная выгрузка работает 20 минут. Что покажет квантиль p95?
Гистограмма длительности: бакеты под расписание#
Дефолтные бакеты client_golang подобраны под HTTP-запросы: от 5 миллисекунд до 10 секунд. Задача по расписанию живёт в другом диапазоне: инкрементальная синхронизация — единицы секунд, ночная выгрузка — десятки минут, отчёт за квартал — часы. Поэтому в HistogramOpts список Buckets задают руками: например, 0.5, 1, 5, 15, 60, 300, 900, 3600 — границы, на которых ваши задачи меняют поведение. Устройство бакетов и квантили разобрано в уроке про гистограммы; для задач важны два следствия. Первое: бакеты кумулятивны — каждый считает всё, что меньше или равно его границе, включая плюс бесконечность; поэтому наблюдение в 20 минут подняло бы не один бакет, а все, чьи границы больше значения. Второе: квантиль считается на стороне Prometheus выражением histogram_quantile по бакетам, и точность зависит от границ, а не от числа тиков. Замер ведут в секундах через time.Since; работу с длительностями разбирает урок про пакет time.
Что покажут бакеты?
Прочитайте код и выберите, что он выведет:
h := prometheus.NewHistogram(prometheus.HistogramOpts{
Buckets: []float64{1, 10, 60},
})
h.Observe(7.5)Метрики планировщика и их вопросы#
| Метрика | Тип | На что отвечает | Типичный алерт |
|---|---|---|---|
| cron_task_ticks_total{job, status} | CounterVec | сколько тиков и с каким исходом | рост error и panic за окно |
| cron_task_duration_seconds{job} | HistogramVec | как долго работает тик | p95 выше порога grace |
| cron_task_last_success_unixtime{job} | GaugeVec | когда последний раз получилось | теперь минус значение больше интервала |
| go_goroutines | Gauge из client_golang | фоновая активность процесса | аномальный рост после релиза |
Мониторинг задач go: алерты на ошибки и тишину#
Мониторинг задач go держится на двух семействах алертов, и оба строятся короткими выражениями поверх этих метрик. Первое семейство — рост ошибок: rate(cron_task_ticks_total{status="error"}[1h]) выше нуля или доля error среди тиков выше порога. Для редких задач окно берут длиннее — сутки, иначе rate между тиками показывает ноль. Второе семейство — тишина: time() минус max(cron_task_last_success_unixtime) больше разрешённого интервала. Порог привязывают к расписанию с запасом: пятиминутной задаче дают час, ночной — сутки плюс несколько часов, иначе алерт срабатывает при безобидном опоздании. Третий случай — исчезнувшая задача: absent(cron_task_ticks_total{job="nightly-digest"}) возвращает серию, только когда исходной нет, и ловит снятую при рефакторинге задачу из первого сценария. Связку алерта с логом по тем же значениям job разбирает урок про логи и метрики.
Кейс: ночная сводка каждую ночь отрабатывала с лейблом ok, счётчик ошибок молчал, алертов не было. Через неделю бухгалтер заметил, что сводка пустая: внешняя система отдавала пустой массив, а задача считала отсутствие данных успехом. Обёртка измеряет факт завершения тика, а не качество результата — это разные вещи, и смешивать их в одном лейбле не стоит. Для качества вводится отдельная бизнес-метрика: количество обработанных записей, число отправленных отчётов, размер выгрузки — по ней ставится алерт на аномально малое значение. Вторая ловушка того же рода — переименование задачи в конфиге: новое значение job создаёт новые серии, старые остаются пустыми, графики рвутся пополам, поэтому переименование согласуют с дашбордами и алертами.
Спорное: метрики вместо логов#
Спорная привычка, которую стоит оспорить: поставить счётчики и выкинуть логирование задач как избыточное. Метрика отвечает на вопросы «сколько» и «как долго», но не отвечает на вопрос «почему»: рост счётчика error на сорок за час не содержит ни одного кода ответа внешнего сервиса, ни одного стека, ни имени упавшего шага. Рабочая связка распределяет роли: метрика замечает, что деградация есть, и поднимает алерт с именем задачи и окном времени; лог объясняет, что именно случилось, и его записи содержат то, чего в числах нет — коды, стектрейсы, идентификаторы. Если по какой-то задаче достаточно одного счётчика без логов — это решение о конкретной задаче, а не правило для всех: у критичной ночной выгрузки и у безобидной чистки сессий разная цена разбора.
Чеклист метрик задач#
- Счётчик тиков с лейблами job и status: значения статусов — закрытый список ok, error, panic, skip.
- Гистограмма длительности с бакетами, подобранными под реальные длительности задач, а не под HTTP.
- Gauge последнего успеха в Unix-времени: алерт на тишину строится одним выражением.
- Имя задачи в лейбле совпадает с полем job в логе и берётся из конфига задач.
- Бизнес-результат измеряется отдельной метрикой: счётчик ok не гарантирует содержательности.
- Переименование задачи согласовано с дашбордами и алертами: старые серии не переносятся.
Соберите запись метрик в обёртке
Впишите метод выбора серии по лейблам, метод инкремента счётчика и функцию текущего времени.
Расставьте сборку задачи с метриками
Расставьте строки в порядке файла: создание счётчика, регистрация, обёртка задачи, регистрация в планировщике, старт и удержание процесса.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: метрики задач
Пакет client_golang в песочнице недоступен, но логика обёртки проверяется без него: замер через time.Now и time.Since, выбор статуса через recover и целочисленный счётчик в map со строковыми ключами — всё это песочница исполняет, потому что пакет time в ней есть. Упражнение: перепишите замер так, чтобы вместо Observe печаталась длительность через fmt.Println. Полный же набор — CounterVec, гистограмма, /metrics — поднимается локально из модуля второго урока: добавьте client_golang и promhttp и посмотрите сырые серии в браузере.
Метрики задач — второй поток наблюдаемости планировщика, дополняющий логи числами: счётчик тиков с закрытым набором статусов отвечает, сколько и как удачно, гистограмма с бакетами под расписание — как долго, gauge последнего успеха — когда в последний раз получилось. Точка внедрения — та же обёртка из урока про wrappers: одна структура с замером в defer и recover-статусом обслуживает все задачи сервиса. Дальше по маршруту: graceful shutdown — как остановка сервиса отражается в тех же метриках, и ретраи задач — что делать, когда счётчик error действительно вырос. Если возвращались с пропуска — логирование задач даёт вторую половину истории. Более глубокое погружение в саму систему мониторинга — курс про Prometheus, полный маршрут — на странице курса. В домашнем задании — три метрики, обёртка и алерт на тишину.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Три метрики закрывают вопросы планировщика: счётчик тиков по статусам, гистограмма длительности, отметка последнего успеха.
- Лейбл status — закрытый список ok, error, panic, skip; текст ошибки в лейбле порождает неконтролируемый рост кардинальности.
- Интерфейс Job не возвращает ошибку: бизнес-неудачу фиксирует сама задача или ваш контракт поверх Run.
- Бакеты гистограммы задаются под реальные длительности задач; кумулятивность и граница плюс бесконечность объясняют прилипание квантилей.
- Алерты: рост error за окно, тишина по last_success, исчезновение серии через absent.
- Метрика замечает деградацию, лог объясняет причину: значения job совпадают, связка сшивается одним фильтром.
Домашнее задание
Добавьте в проект три метрики из урока: счётчик тиков с лейблами job и status, гистограмму длительности с бакетами под свои задачи и gauge последнего успеха. Соберите обёртку meteredJob с замером в defer и recover-статусом, поставьте её в цепочку после Recover и убедитесь тестом, что паника доходит до Recover со стеком. Затем постройте два алерта — долю error за окно и тишину по last_success — и проверьте их на тестовой задаче, которая падает. Финал — заведите задачу, считающую успехом пустой результат, и добавьте бизнес-метрику, которая ловит это поведение.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему нельзя класть текст ошибки в лейбл статуса?
Каждое уникальное значение лейбла создаёт новую серию. Тексты ошибок внешних API содержат коды, идентификаторы и подробности, поэтому их разнообразие не ограничено: кардинальность растёт, запросы к Prometheus замедляются, память под серии раздувается. В лейбле живут значения закрытого списка, а текст ошибки — в логе.
Как считать длительность, если задача может паниковать?
Замер и отправка в гистограмму выполняются в отложенной функции: defer срабатывает и при панике, поэтому аварийный тик попадает в гистограмму. Обёртка ставит статус panic в счётчике и перезапускает панику, чтобы стоящий снаружи Recover записал стек в лог.
Какие бакеты выбрать для задач по расписанию?
Границы подбирают под реальные длительности: например, 0.5, 1, 5, 15, 60, 300, 900, 3600 секунд. Дефолтные бакеты заканчиваются на 10 секундах: длинные задачи слипаются в последнем бакете, квантили теряют смысл. Посмотрите фактические длительности за месяц и расставьте границы между ними.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь