Как добавить метрику и алерт¶
Пошаговый рецепт: объявить Prometheus-метрику в сервисе, инкрементировать
её в коде, проверить что она экспонирована, и подключить alert на её
поведение. Полный reference — в
../conventions/observability.md.
Здесь — сжатый практический сценарий для PR-автора.
Содержание¶
- 1. Где живут метрики
- 2. Выбор типа
- 3. Объявление метрики
- 4. Naming
- 5. Labels
- 6. Инкремент из кода
- 7.
/metricsendpoint - 8. Тест, что метрика exposed
- 9. Добавление alert rule
- 10. SLO-based alerts
- 11. Dashboards
- 12. Helper для бизнес-событий
- Anti-patterns
- Чеклист перед merge
- Связанные разделы
1. Где живут метрики¶
В каждом сервис-репо метрики объявляются в одном пакете
internal/metrics/ — metrics.go (определения) + register.go
(MustRegister). Это owned-пакет сервиса, другие сервисы не лезут.
Один registry на сервис. Регистрация — один раз на старте в
cmd/server/main.go через metrics.MustRegister().
2. Выбор типа¶
| Тип | Когда использовать | Пример |
|---|---|---|
Counter |
Монотонно растущий счётчик фактов | review_created_total |
Gauge |
Текущее значение, может расти и падать | outbox_unpublished_rows |
Histogram |
Распределение длительностей / размеров | http_request_duration_seconds |
Summary |
Почти не используем | — |
Summary считает quantile'ы в клиенте и нельзя агрегировать между
pod'ами. Histogram + histogram_quantile(...) в PromQL гибче — для
новых метрик выбирай histogram.
3. Объявление метрики¶
// internal/metrics/metrics.go
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var ReviewCreatedTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "review_created_total",
Help: "Total reviews created, partitioned by outcome.",
},
[]string{"result"},
)
var ReviewCreateDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "review_create_duration_seconds",
Help: "Latency of CreateReview service method.",
Buckets: prometheus.DefBuckets,
},
[]string{"result"},
)
promauto.New* регистрирует метрику в глобальном registry
автоматически — отдельного MustRegister не нужно. Если используешь
кастомный registry (для теста) — бери prometheus.NewCounterVec +
reg.MustRegister.
4. Naming¶
Reference — в
../conventions/observability.md
§Naming. Кратко:
snake_case, всё в нижнем регистре.- Unit-суффикс:
_seconds,_bytes,_total,_ratio. - Counter заканчивается на
_total, кроме_duration_seconds. - Формат:
<domain>_<action>_<unit>(например,user_registrations_total,kafka_publish_duration_seconds).
Плохо: numReviews, reviewTime, errors_count.
5. Labels¶
Labels — низкой cardinality. Каждая уникальная комбинация label'ов создаёт отдельную серию в TSDB; высокая cardinality = OOMKill у Prometheus.
- Хорошо:
result,endpoint,status_code,topic,event_type. - Плохо:
user_id,request_id,correlation_id,email,error_message(произвольная строка).
Для каждой label документируй допустимые значения:
Правило большого пальца: если label может иметь > 100 уникальных значений — он не в метрику, он в лог или span.
6. Инкремент из кода¶
Типовой паттерн: метрика увеличивается в defer после успешной/сбойной
операции, с маппингом ошибки в label.
// internal/service/review_service.go
func (s *ReviewService) Create(ctx context.Context, cmd CreateReviewCommand) (r *domain.Review, err error) {
start := time.Now()
defer func() {
result := resultFromError(err)
metrics.ReviewCreatedTotal.WithLabelValues(result).Inc()
metrics.ReviewCreateDuration.WithLabelValues(result).Observe(time.Since(start).Seconds())
}()
// ... бизнес-логика
return r, err
}
resultFromError — один helper на сервис:
// internal/metrics/result.go
func resultFromError(err error) string {
switch {
case err == nil:
return "ok"
case errors.Is(err, service.ErrValidation):
return "validation"
case errors.Is(err, service.ErrConflict):
return "conflict"
case errors.Is(err, pkgdb.ErrNotFound):
return "not_found"
default:
return "internal"
}
}
Такой helper гарантирует согласованный набор значений label'а result
по всему сервису. Ревьюер может быстро проверить: «сколько значений
я ожидаю у result? Столько в switch».
7. /metrics endpoint¶
Endpoint монтируется один раз в роутере:
Правила (см.
../conventions/observability.md
§Prometheus endpoint):
- Не за auth-middleware — scrape идёт из Prometheus внутри кластера.
- Не роутится Gateway'ем наружу.
- Не вешай access-log middleware — забьёт логи scrape-запросами.
8. Тест, что метрика exposed¶
// internal/handler/metrics_test.go
func TestMetricsEndpoint_ExposesReviewCreated(t *testing.T) {
handler := promhttp.Handler()
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
body := rec.Body.String()
require.Contains(t, body, "review_created_total")
require.Contains(t, body, "review_create_duration_seconds")
}
Тест ловит регрессии вида «переименовал метрику, забыл обновить
dashboards». Запуск — в обычном make test.
Полезный tip: testutil.ToFloat64(metric) (из
prometheus/client_golang/prometheus/testutil) — прочитать текущее
значение counter'а/gauge'а в тесте.
9. Добавление alert rule¶
Alert-правила живут в infra-репо (не в сервисе, не в handbook). Handbook описывает формат. Типовое правило:
# infra-repo: alerts/review.yml
groups:
- name: review
rules:
- alert: ReviewCreateErrorRateHigh
expr: |
sum(rate(review_created_total{result!="ok"}[5m]))
/
sum(rate(review_created_total[5m]))
> 0.05
for: 10m
labels:
severity: warn
service: review
annotations:
summary: "review: create error rate > 5% for 10m"
description: |
Over the last 5 minutes, {{ $value | humanizePercentage }}
of review.create calls failed.
runbook: "<runbook-url-placeholder>"
Обязательные поля alert'а:
expr— PromQL, без magic numbers в правой части (всегда human-readable:0.05для 5%, не.05).for— минимум 5 минут для warn-alert'ов, 10 минут для page-alert'ов. Безforлюбой transient spike даст шум.labels.severity—page/warn/info. Определяет, куда идёт нотификация.annotations.summary— одна строка, видна в pager.annotations.runbook— ссылка на runbook-страницу, которую on-call откроет в 3 часа ночи. Если runbook'а ещё нет — placeholder плюс TODO на PR в infra-репозиторий.
10. SLO-based alerts¶
Если метрика описывает SLI (например, доля успешных HTTP-запросов на endpoint), правильный подход — multi-burn-rate alert от SLO, а не сырой порог:
- Fast burn (5 минут на 1 час сжечь): немедленный page.
- Slow burn (30 минут на 6 часов сжечь): тикет на утро.
SLO-определения per-сервис живут в infra-репо, не в handbook. Здесь — ссылка: когда метрика — SLI, заводи SLO рядом с alert'ом, не только threshold.
11. Dashboards¶
Grafana dashboards — JSON в infra-репо. Минимум для новой метрики:
- Timeseries panel с rate/avg новой метрики.
- Разбивка по основным labels (обычно
result). - Range последние 24 часа, step 1m.
Коллаба с infra-командой через PR в infra-репо. Не блокируй merge кода из-за отсутствия dashboard — создай follow-up task.
12. Helper для бизнес-событий¶
Часто нужно: «каждый факт = метрика + info-лог» (регистрация
пользователя, upload файла). Для этого helper (см.
../conventions/observability.md):
metrics.BusinessEvent(ctx, "user.registered",
slog.Int64("user_id", user.ID),
slog.String("source", "email"))
Helper инкрементирует counter business_event_total{name="user.registered"}
и пишет INFO-лог с теми же attrs. Используй для операций, которые
хочется и в логах видеть, и в дашборде считать.
Anti-patterns¶
- Метрика без
Help-текста. В Grafana вместо описания — пустая строка, никто не помнит, что это за число. - High cardinality labels. Один
user_idкак label = OOMKill Prometheus'у за пару часов под нагрузкой. - Summary вместо Histogram. Потом в Grafana нельзя построить «p99 по всему кластеру» — summary не агрегируется.
- Alert без
for. Transient spike (один 500-й ответ в минуту) будит on-call. - Регистрация метрики на каждый запрос. Если
promauto.New*в handler'е — при втором запросе panicduplicate metrics collector. Регистрируй один раз в init/main/package-level var. - Runbook не указан. Без runbook alert бесполезен — on-call не
знает, что делать. Ссылка на
troubleshooting/*.mdдостаточна. - Label
error_message= текст ошибки. Любая вариация строки — новая серия. Используйerror_classс коротким enum'ом.
Чеклист перед merge¶
- Имя метрики соответствует convention (snake_case, unit-суффикс).
-
Helpзаполнен, на английском, короче 70 символов. - Labels документированы (enum возможных значений в комменте у определения метрики).
- Cardinality каждого label — проверена (< 100 уникальных значений).
- Есть test, что метрика exposed на
/metrics. - Метрика инкрементируется из нужного места (handler / service / middleware — в зависимости от того, что она измеряет).
- Если метрика требует alert — open PR в infra-репо с alert-rule.
- Runbook-ссылка в alert'е указывает на существующую страницу (или placeholder + follow-up task).
Связанные разделы¶
../conventions/observability.md— три сигнала, naming, labels, correlation, OTel, autoinstrumentation.../conventions/logging.md— когда лог, когда метрика, когда span.../conventions/testing.md— как тестировать handler без глобального Prometheus registry (testutil).debug-outbox-lag.md— runbook пример того, какие метрики и alert'ы используются на практике.