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

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

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

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

Метрики задач: длительность и ошибки

Метрики cron закрывают вопрос, который логи решают лишь наполовину: сколько тиков случилось, сколько из них провалилось и как долго задачи работали — в виде чисел, по которым строятся графики и срабатывают алерты. В robfig/cron v3 метрик из коробки нет — библиотека не тянет зависимостей, — но точка внедрения есть: обёртки из урока про wrappers. Обернув каждую задачу замером длительности и счётчиком исходов, вы получаете три стандартные метрики — счётчик тиков по статусам, гистограмму длительности и отметку последнего успеха — и алерты, которые замечают затихшую задачу раньше пользователей. Разберём, что измерять, какие значения лейблов допустимы и как выбирать бакеты под ночные выгрузки.

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

Сценарий для старта: сервис выгрузок работал месяц, никто не заглядывал в его логи — туда и не было повода, ошибок там не появлялось. Потом выяснилось, что третья неделя прошла без единого тика: задача была снята при рефакторинге конфига и обратно не вернулась. Логи молчали, потому что молчание — это состояние, которое никто не записывает. Метрику, напротив, читают постоянно: график тиков, линия которого обрывается две недели назад, виден с первого экрана дашборда. Второй сценарий зеркальный: тики идут, но каждая выгрузка падает с ошибкой аутентификации, и растущая линия счётчика по статусам пересекает порог алерта ночью, а не при разборе жалобы клиента. Дальше — три метрики, которые закрывают почти все вопросы к планировщику.

Метрики cron: три вопроса, три метрики#

Метрики cron сводятся к трём вопросам, которые дежурный задаёт планировщику. Первый: тики вообще происходят и с каким исходом — отвечает счётчик типа CounterVec с лейблами job и status, по одному инкременту на завершённый тик. Второй: как долго работает задача — отвечает гистограмма HistogramVec с лейблом job и замером через time.Since в секундах. Третий: когда задача последний раз отработала успешно — отвечает Gauge с отметкой времени в формате Unix, по которому алерт на тишину строится одним выражением. Все три регистрируются пакетом client_golang и отдаются тем же обработчиком /metrics — механика реестра и promhttp разобрана в уроке про первый endpoint. Планировщик — не веб-сервер: тиков мало, длительность измеряется от секунд до часов. Разницу Counter, Gauge и Histogram разбирает урок про типы метрик, а здесь — как они ложатся на расписание.

Три метрики задач в реестре

GO
var 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, либо ваш контракт с приведением типа в обёртке. Механика лейблов — одна метрика, много измерений из короткого списка — разобрана в уроке про лейблы.

Обёртка с замером длительности и статусом

GO
type 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_goroutinesGauge из 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 разбирает урок про логи и метрики.

Спорное: метрики вместо логов#

Спорная привычка, которую стоит оспорить: поставить счётчики и выкинуть логирование задач как избыточное. Метрика отвечает на вопросы «сколько» и «как долго», но не отвечает на вопрос «почему»: рост счётчика error на сорок за час не содержит ни одного кода ответа внешнего сервиса, ни одного стека, ни имени упавшего шага. Рабочая связка распределяет роли: метрика замечает, что деградация есть, и поднимает алерт с именем задачи и окном времени; лог объясняет, что именно случилось, и его записи содержат то, чего в числах нет — коды, стектрейсы, идентификаторы. Если по какой-то задаче достаточно одного счётчика без логов — это решение о конкретной задаче, а не правило для всех: у критичной ночной выгрузки и у безобидной чистки сессий разная цена разбора.

Чеклист метрик задач#

  • Счётчик тиков с лейблами job и status: значения статусов — закрытый список ok, error, panic, skip.
  • Гистограмма длительности с бакетами, подобранными под реальные длительности задач, а не под HTTP.
  • Gauge последнего успеха в Unix-времени: алерт на тишину строится одним выражением.
  • Имя задачи в лейбле совпадает с полем job в логе и берётся из конфига задач.
  • Бизнес-результат измеряется отдельной метрикой: счётчик ok не гарантирует содержательности.
  • Переименование задачи согласовано с дашбордами и алертами: старые серии не переносятся.

Соберите запись метрик в обёртке

Впишите метод выбора серии по лейблам, метод инкремента счётчика и функцию текущего времени.

taskDuration.(m.name).Observe(time.Since(start).Seconds()) taskTicks.WithLabelValues(m.name, "ok").() taskLastSuccess.WithLabelValues(m.name).Set(float64(time.().Unix()))

Расставьте сборку задачи с метриками

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

Строки

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

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

Карточки: метрики задач

Термин

CounterVec по статусам

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

Счётчик тиков с лейблами job и status из закрытого списка ok, error, panic, skip. Каждое новое значение лейбла — новая серия, поэтому текст ошибки в лейбл не попадает.

1 / 6

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

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

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

Почему нельзя класть текст ошибки в лейбл статуса?

Каждое уникальное значение лейбла создаёт новую серию. Тексты ошибок внешних API содержат коды, идентификаторы и подробности, поэтому их разнообразие не ограничено: кардинальность растёт, запросы к Prometheus замедляются, память под серии раздувается. В лейбле живут значения закрытого списка, а текст ошибки — в логе.

Как считать длительность, если задача может паниковать?

Замер и отправка в гистограмму выполняются в отложенной функции: defer срабатывает и при панике, поэтому аварийный тик попадает в гистограмму. Обёртка ставит статус panic в счётчике и перезапускает панику, чтобы стоящий снаружи Recover записал стек в лог.

Какие бакеты выбрать для задач по расписанию?

Границы подбирают под реальные длительности: например, 0.5, 1, 5, 15, 60, 300, 900, 3600 секунд. Дефолтные бакеты заканчиваются на 10 секундах: длинные задачи слипаются в последнем бакете, квантили теряют смысл. Посмотрите фактические длительности за месяц и расставьте границы между ними.

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

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

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

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

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

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

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