Опирается на правила:
R-OBS-SLO-1,R-OBS-SLO-2,R-OBS-SLO-3,R-OBS-SLO-4,R-OBS-SLO-X1,R-OBS-SLO-X2,R-OBS-SLO-X3из Observability Style Guide → раздел 7. SLO и алерты.
Важно знать
- Каждый critical-endpoint имеет SLO: availability (99.9% non-5xx) и latency (p99 < 500ms) на rolling 30-day window.
- Multi-window multi-burn-rate alerts по Google SRE Workbook: fast burn (1h, rate > 14.4) и slow burn (6h, rate > 6).
- Error budget: 99.9% target = 43 минуты downtime/month как бюджет для рискованных релизов.
- Алерт на исчерпание бюджета (< 10%) — сигнал команде переключиться с features на reliability.
- Алерты отдельные от SLO: infra (горутины, pgxpool), domain (бизнес-фейлы), resilience, consumer lag.
- Alert на каждый ERROR → alert fatigue → команда мьютит канал → пропускает реальный инцидент.
- Каждый alert содержит
runbook_url. PagerDuty в 3 ночи без инструкции — эскалация без действия.- Алерты реализуются в Prometheus/Alertmanager rules (ops-репо), Go-код поставляет метрики.
SLO — это обещание пользователям уровня обслуживания: не «всё всегда работает», а «99.9% запросов успешны в течение 30-day window». Разница принципиальная: появляется error budget, появляется возможность осознанно торговать надёжностью на скорость релизов.
Метрики для SLO из chi-middleware
R-OBS-SLO-1: SLO считается из RED-метрик, которые chi-middleware пишет при каждом запросе (см. статью Metrics). Ключевые метрики для SLI:
// internal/platform/middleware/metrics.go
var (
httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
}, []string{"method", "path", "status_class"})
httpRequestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency",
// Для платёжных эндпоинтов с SLO p99 < 500ms — кастомные бакеты
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0},
}, []string{"method", "path", "status_class"})
)
func Metrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
start := time.Now()
next.ServeHTTP(ww, r)
status := statusClass(ww.Status())
path := chi.RouteContext(r.Context()).RoutePattern()
httpRequestsTotal.WithLabelValues(r.Method, path, status).Inc()
httpRequestDurationSeconds.WithLabelValues(r.Method, path, status).Observe(time.Since(start).Seconds())
})
}
func statusClass(code int) string {
switch {
case code < 400:
return "success"
case code < 500:
return "client_error"
default:
return "server_error"
}
}
path берётся из chi.RouteContext(...).RoutePattern() — шаблон /orders/{orderID}, не raw URL /orders/ord-123. Это критично: без паттерна каждый order_id создаёт отдельный time series, OOM в Prometheus.
SLO targets по эндпоинтам
| Endpoint | Availability SLO | Latency SLO |
|---|---|---|
POST /orders | 99.9% non-5xx | p99 < 500ms |
POST /payments | 99.95% non-5xx | p99 < 1s |
GET /orders/{orderID} | 99.95% non-5xx | p99 < 200ms |
GET /products | 99.5% non-5xx | p99 < 800ms |
Бизнес и product owner выбирают target совместно: 99.99% (52 мин/год) означает multi-region active-active и другой уровень затрат. 99.9% (8.7 ч/год) достижимо на одном регионе с нормальным on-call.
PromQL для SLI
# Availability SLI для POST /orders (rolling 30d window)
sum(rate(http_requests_total{path="/orders",method="POST",status_class="success"}[30d]))
/
sum(rate(http_requests_total{path="/orders",method="POST"}[30d]))
# Latency SLI — p99 за 30d
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{path="/orders",method="POST"}[30d]))
)
Multi-window multi-burn-rate
R-OBS-SLO-2: подход из Google SRE Workbook ch.5.
Error budget 99.9% = 0.1% за 30 дней ≈ 43.2 минуты. Если за 1 час расходуется больше 1/720 бюджета (≈ 14.4× нормальной скорости) — это аварийный темп: при нём бюджет иссякнет за 20 часов, нужно будить ops.
burn_rate = error_rate_in_window / (1 - SLO_target)
| Window | Burn rate threshold | Что значит | Severity |
|---|---|---|---|
| 1h | > 14.4 | 5% бюджета сгорело за час | page (немедленно) |
| 6h | > 6 | 5% бюджета сгорело за 6 часов | ticket (в рабочее время) |
| 24h | > 3 | 10% бюджета сгорело за сутки | warning (мониторинг) |
# ops-репо/alerts/order-slo.yaml
groups:
- name: order-slo
rules:
- alert: OrdersAvailabilityFastBurn
expr: |
(
sum(rate(http_requests_total{path="/orders",method="POST",status_class="server_error"}[1h]))
/
sum(rate(http_requests_total{path="/orders",method="POST"}[1h]))
) > (14.4 * (1 - 0.999))
for: 2m
labels:
severity: critical
annotations:
summary: "Orders SLO: быстрое сжигание error budget (1h window)"
runbook_url: "https://runbooks.internal/orders-slo-fast-burn"
- alert: OrdersAvailabilitySlowBurn
expr: |
(
sum(rate(http_requests_total{path="/orders",method="POST",status_class="server_error"}[6h]))
/
sum(rate(http_requests_total{path="/orders",method="POST"}[6h]))
) > (6 * (1 - 0.999))
for: 15m
labels:
severity: warning
annotations:
summary: "Orders SLO: медленное сжигание error budget (6h window)"
runbook_url: "https://runbooks.internal/orders-slo-slow-burn"
for: 2m (fast burn) — не будить по кратковременному пику. for: 15m (slow burn) — устойчивая деградация, не шум.
Latency SLO alert
- alert: OrdersLatencyP99High
expr: |
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{path="/orders",method="POST"}[5m]))
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "Orders p99 latency > 500ms"
runbook_url: "https://runbooks.internal/orders-latency-high"
Error budget exhaustion
R-OBS-SLO-3: отдельный алерт когда бюджет остался < 10% за 30-day window. Recording rule считает остаток:
groups:
- name: order-slo-recording
rules:
- record: job:orders_availability_sli:rate30d
expr: |
sum(rate(http_requests_total{path="/orders",method="POST",status_class="success"}[30d]))
/
sum(rate(http_requests_total{path="/orders",method="POST"}[30d]))
- record: job:orders_error_budget_remaining:ratio
expr: |
1 - ((1 - job:orders_availability_sli:rate30d) / (1 - 0.999))
- name: order-slo-alerts
rules:
- alert: OrdersErrorBudgetExhausted
expr: job:orders_error_budget_remaining:ratio < 0.1
for: 1h
labels:
severity: warning
annotations:
summary: "Orders: осталось менее 10% error budget"
description: |
Команда переключается с features на reliability.
Рискованные релизы приостановлены до восстановления бюджета.
runbook_url: "https://runbooks.internal/orders-error-budget-exhausted"
Это не «будить ночью» — это сигнал на следующем planning: «следующий спринт = reliability work».
Алерты отдельные от SLO
R-OBS-SLO-4: SLO — про user-facing успешность запросов. Есть отдельные категории сигналов с другими actionable шагами.
Бизнес-метрики домена
Go-сервис пишет бизнес-счётчики через promauto прямо в UseCase Handler:
// internal/order/metrics.go
var (
ordersFailedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "orders_failed_total",
Help: "Orders that failed business validation or processing",
}, []string{"reason"})
paymentDeclinedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "payment_declined_total",
Help: "Payments declined by payment provider",
}, []string{"decline_code"})
)
// internal/order/usecase/create_order.go
func (h *CreateOrderHandler) Handle(ctx context.Context, cmd CreateOrderCommand) (*Order, error) {
order, err := h.orders.Create(ctx, cmd)
if err != nil {
var domainErr *DomainError
if errors.As(err, &domainErr) {
ordersFailedTotal.WithLabelValues(domainErr.Reason).Inc()
}
return nil, fmt.Errorf("create order: %w", err)
}
ordersCreatedTotal.WithLabelValues(string(cmd.PaymentMethod)).Inc()
return order, nil
}
decline_code — не customer_id или order_id. Только низко-кардинальные значения: insufficient_funds, card_expired, fraud_suspected.
Ресурсы процесса (USE-method)
// cmd/server/main.go — регистрируем при старте
prometheus.MustRegister(collectors.NewGoCollector())
prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
Алерты на pgxpool — pool stats через кастомный Collector:
// internal/platform/metrics/pgpool.go
type pgPoolCollector struct {
pool *pgxpool.Pool
waiting *prometheus.Desc
idle *prometheus.Desc
}
func NewPgPoolCollector(pool *pgxpool.Pool) prometheus.Collector {
return &pgPoolCollector{
pool: pool,
waiting: prometheus.NewDesc("pgxpool_waiting_connections",
"Connections waiting for an available connection from the pool", nil, nil),
idle: prometheus.NewDesc("pgxpool_idle_connections",
"Idle connections in the pool", nil, nil),
}
}
func (c *pgPoolCollector) Collect(ch chan<- prometheus.Metric) {
stat := c.pool.Stat()
ch <- prometheus.MustNewConstMetric(c.waiting, prometheus.GaugeValue,
float64(stat.EmptyAcquireCount()))
ch <- prometheus.MustNewConstMetric(c.idle, prometheus.GaugeValue,
float64(stat.IdleConns()))
}
func (c *pgPoolCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.waiting
ch <- c.idle
}
# alert на истощение pgxpool
- alert: PgPoolConnectionsWaiting
expr: pgxpool_waiting_connections > 5
for: 1m
labels:
severity: warning
annotations:
runbook_url: "https://runbooks.internal/pgpool-saturation"
Сводная таблица категорий алертов
| Категория | Метрика (Go) | Порог | Действие |
|---|---|---|---|
| Infra / горутины | go_goroutines > 10000 | steady > 10k | goroutine leak |
| Infra / pgxpool | pgxpool_waiting_connections > 5 | 1m | tune pool size |
| Domain / заказы | rate(orders_failed_total[5m]) > 100 | 5m | product: данные |
| Domain / платежи | rate(payment_declined_total{decline_code="fraud"}[5m]) > 10 | 2m | security team |
| Resilience | circuit_breaker_state{state="open"} == 1 | немедленно | внешний сервис |
| Kafka lag | kafka_consumer_group_lag > 10000 | 5m | scale consumers |
Runbook как часть алерта
R-OBS-SLO-X3: каждый alert содержит annotations.runbook_url. Структура runbook для OrdersAvailabilityFastBurn:
# Orders SLO Fast Burn — Runbook
## Симптомы
Alert `OrdersAvailabilityFastBurn` сработал: burn rate > 14.4× за последний час.
## Диагностика (в порядке)
1. `rate(http_requests_total{path="/orders",status_class="server_error"}[5m])` —
смотрим абсолютный rate 5xx.
2. `pgxpool_waiting_connections` — если > 0, DB-соединений не хватает: scale app или tune pool.
3. `circuit_breaker_state{service="payment-provider"}` — если OPEN, внешний сервис упал.
4. Grafana → трейсы с `status=ERROR` по `/orders` за последние 30 мин — смотрим span attributes.
## Действия
- Если #2: `kubectl scale deployment order-service --replicas=N+2`
- Если #3: ack alert, ждём восстановления payment-provider; включить fallback если есть.
- Иначе: эскалировать в `#order-service-oncall`.
Go-код не содержит runbook — он только поставляет метрики. Runbook — в ops-репо, ссылка — в YAML-правиле алерта.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Alert на каждый slog.Error(...) | R-OBS-SLO-X1 | rate(app_errors_total[5m]) > 0.1 — агрегат по типу |
| SLO target 100% | R-OBS-SLO-X2 | 99.9% (43 мин/мес) или 99.95% / 99.99% |
Alert без annotations.runbook_url | R-OBS-SLO-X3 | обязательная ссылка на ops-docs |
order_id / customer_id как label | R-OBS-MTR-X1 | только низко-кардинальные метки |
| Один burn-rate window 30d без fast/slow | R-OBS-SLO-2 | multi-window: 1h fast + 6h slow |
| Latency SLO по avg вместо percentile | R-OBS-SLO-1 | p99 (или p95) — avg скрывает хвост |
Alert без for: — срабатывает на любой пик | R-OBS-SLO-2 | for: 2m fast / for: 15m slow |
Куда дальше
- Metrics — chi-middleware RED,
promauto, pgxpool Collector — откуда берутся метрики для SLI. - Tracing — детальный разбор burn через OTel traces с ошибками.
- Health checks — почему liveness/readiness не для бизнес-SLO.
- Logging — slog + OTel bridge, связка лог-запись → trace.
- Context propagation — ctx в горутинах, разрыв trace.
- Конфигурация — management-сервер, отдельный порт для
/metrics. - Google SRE Workbook ch.5 — Alerting on SLOs — первоисточник multi-window burn rate.