Опирается на правила: 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 по эндпоинтам

EndpointAvailability SLOLatency SLO
POST /orders99.9% non-5xxp99 < 500ms
POST /payments99.95% non-5xxp99 < 1s
GET /orders/{orderID}99.95% non-5xxp99 < 200ms
GET /products99.5% non-5xxp99 < 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)
WindowBurn rate thresholdЧто значитSeverity
1h> 14.45% бюджета сгорело за часpage (немедленно)
6h> 65% бюджета сгорело за 6 часовticket (в рабочее время)
24h> 310% бюджета сгорело за сутки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 > 10000steady > 10kgoroutine leak
Infra / pgxpoolpgxpool_waiting_connections > 51mtune pool size
Domain / заказыrate(orders_failed_total[5m]) > 1005mproduct: данные
Domain / платежиrate(payment_declined_total{decline_code="fraud"}[5m]) > 102msecurity team
Resiliencecircuit_breaker_state{state="open"} == 1немедленновнешний сервис
Kafka lagkafka_consumer_group_lag > 100005mscale 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-X1rate(app_errors_total[5m]) > 0.1 — агрегат по типу
SLO target 100%R-OBS-SLO-X299.9% (43 мин/мес) или 99.95% / 99.99%
Alert без annotations.runbook_urlR-OBS-SLO-X3обязательная ссылка на ops-docs
order_id / customer_id как labelR-OBS-MTR-X1только низко-кардинальные метки
Один burn-rate window 30d без fast/slowR-OBS-SLO-2multi-window: 1h fast + 6h slow
Latency SLO по avg вместо percentileR-OBS-SLO-1p99 (или p95) — avg скрывает хвост
Alert без for: — срабатывает на любой пикR-OBS-SLO-2for: 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.