Опирается на правила: R-OBS-SLO-1R-OBS-SLO-4 и R-OBS-SLO-X1R-OBS-SLO-X3раздел 7. SLO и алерты.

Важно знать

  • Каждый critical-endpoint имеет SLO: например, 99.9% non-5xx и p95 < 500ms на rolling 30-day window.
  • prom-client предоставляет http_server_requests_seconds (через RED-interceptor) — из неё считается SLI в Prometheus/Alertmanager.
  • Multi-window multi-burn-rate: fast burn (1h, rate > 14.4) — будит дежурного, slow burn (6h, rate > 6) — создаёт тикет.
  • Error budget: SLO 99.9% = 43 минуты downtime/month. 100% target невозможен и блокирует работу команды.
  • Alert на исчерпание бюджета (< 10%) — не «чинить ночью», а сигнал бизнесу сфокусироваться на устойчивости.
  • Алерты отдельные от SLO: event loop lag, heap, pg pool saturation, BullMQ lag, cache hit rate — это не SLO, но ранние признаки.
  • Alert на каждый ERROR в логах → fatigue → дежурный мьютит канал → пропускает реальный инцидент.
  • Каждый алерт имеет annotations.runbook. PagerDuty в 3 ночи без инструкции — эскалация без действия.

SLO (Service Level Objective) — обещание уровня обслуживания: «99.9% запросов к POST /orders завершатся успешно в течение 30-day window». Не «всё всегда работает». Разница принципиальная: появляется error budget, появляется возможность взвешивать reliability и скорость выкатки.

SLO для critical-endpoints

R-OBS-SLO-1: для каждого critical-endpoint — availability SLO и latency SLO.

EndpointAvailability SLOLatency SLO
POST /orders99.9% non-5xxp95 < 500ms
POST /payments99.95% non-5xxp95 < 1s
GET /orders/:id99.95% non-5xxp95 < 200ms
GET /products99.5% non-5xxp95 < 800ms

Target выбирается вместе с product owner: «сколько стоит дополнительная девятка?». 99.99% (52 мин/год) — другая архитектура, multi-region, immediate failover. 99.9% (8.7 ч/год) — намного дешевле.

RED-interceptor: база для SLI

SLI вычисляется из http_server_requests_secondsHistogram, который заполняет RED-interceptor:

// metrics/http-metrics.interceptor.ts
@Injectable()
export class HttpMetricsInterceptor implements NestInterceptor {
  private readonly histogram: Histogram<string>;

  constructor() {
    this.histogram = new Histogram({
      name: 'http_server_requests_seconds',
      help: 'HTTP request duration',
      labelNames: ['method', 'route', 'status_class'] as const,
      buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
    });
  }

  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const req = ctx.switchToHttp().getRequest<Request>();
    const route = req.route?.path ?? 'unknown';
    const method = req.method;
    const end = this.histogram.startTimer();

    return next.handle().pipe(
      tap({
        next: () => {
          const status = ctx.switchToHttp().getResponse<Response>().statusCode;
          end({ method, route, status_class: statusClass(status) });
        },
        error: (err: unknown) => {
          const code = err instanceof HttpException ? err.getStatus() : 500;
          end({ method, route, status_class: statusClass(code) });
        },
      }),
    );
  }
}

function statusClass(code: number): string {
  if (code < 400) return 'success';
  if (code < 500) return 'client_error';
  return 'server_error';
}

Label route — шаблон роута /orders/:id, не сырой URL (иначе high-cardinality по R-OBS-MTR-X1).

Расчёт SLI в Prometheus:

# Availability SLI: POST /orders за 30 дней
sum(rate(http_server_requests_seconds_count{route="/orders",method="POST",status_class="success"}[30d]))
  /
sum(rate(http_server_requests_seconds_count{route="/orders",method="POST"}[30d]))

# Latency SLI: p95
histogram_quantile(0.95,
  sum by (le) (rate(http_server_requests_seconds_bucket{route="/orders",method="POST"}[30d]))
)

Multi-window multi-burn-rate

R-OBS-SLO-2: подход из Google SRE Workbook, chapter 5.

Идея: SLO 99.9% означает error budget 0.1% за 30 дней. Если за 1 час расходуется 5% этого бюджета — темп сжигания катастрофический, бюджет закончится через 20 часов. Это fast burn — будит дежурного.

Формула burn rate:

burn_rate = error_rate_in_window / (1 - SLO_target)

Пороги для SLO 99.9%:

  • burn rate > 14.4 (1h window) → 5% бюджета за 1 час — немедленный алерт
  • burn rate > 6 (6h window) → 5% бюджета за 6 часов — тикет, разобраться утром
  • burn rate ≤ 1 — нормальный темп или лучше
# Alertmanager rules для сервиса orders
- alert: OrdersSloFastBurn
  expr: |
    (
      sum(rate(http_server_requests_seconds_count{route="/orders",method="POST",status_class="server_error"}[1h]))
      /
      sum(rate(http_server_requests_seconds_count{route="/orders",method="POST"}[1h]))
    ) > (14.4 * (1 - 0.999))
  for: 2m
  labels:
    severity: critical
  annotations:
    summary: "POST /orders fast burn — SLO под угрозой"
    runbook: "https://runbooks.internal/orders-slo-fast-burn"

- alert: OrdersSloSlowBurn
  expr: |
    (
      sum(rate(http_server_requests_seconds_count{route="/orders",method="POST",status_class="server_error"}[6h]))
      /
      sum(rate(http_server_requests_seconds_count{route="/orders",method="POST"}[6h]))
    ) > (6 * (1 - 0.999))
  for: 15m
  labels:
    severity: warning
  annotations:
    summary: "POST /orders slow burn — деградация"
    runbook: "https://runbooks.internal/orders-slo-slow-burn"

for: 2m на fast burn исключает кратковременные всплески. for: 15m на slow burn даёт время убедиться, что деградация устойчивая.

Аналогичные пары — для POST /payments (SLO 99.95%) с пересчитанными порогами:

  • burn rate > 14.4 × (1 - 0.9995) = burn rate > 0.00072 (порог по absolutes, для 99.95% чуть иначе)

Удобнее считать через Grafana SLO plugin или sloth для автогенерации правил из YAML-декларации SLO.

Error budget exhaustion

R-OBS-SLO-3: отдельный алерт на остаток бюджета.

# Остаток error budget [0..1], 0 = полностью исчерпан
1 - (
  (1 - sum(rate(http_server_requests_seconds_count{route="/orders",method="POST",status_class="success"}[30d]))
       / sum(rate(http_server_requests_seconds_count{route="/orders",method="POST"}[30d])))
  / (1 - 0.999)
)
- alert: OrdersErrorBudgetExhausted
  expr: <budget_remaining_expr> < 0.1
  for: 1h
  labels:
    severity: warning
  annotations:
    summary: "Осталось меньше 10% error budget на POST /orders"
    description: |
      Команда переключается с features на reliability.
      Рискованные релизы приостановлены до восстановления бюджета.
    runbook: "https://runbooks.internal/orders-error-budget"

Это не «будить дежурного ночью». Это сигнал product owner и тимлиду: следующий спринт — reliability, не новые фичи.

Доменные бизнес-метрики для SLO

Помимо HTTP-метрик, NestJS-сервис Order собирает доменные счётчики, которые идут в отдельные алерты (R-OBS-SLO-4):

// metrics/order.metrics.ts
export const orderCreatedTotal = new Counter({
  name: 'order_created_total',
  help: 'Orders successfully created',
  labelNames: ['type'] as const,
});

export const orderFailedTotal = new Counter({
  name: 'order_failed_total',
  help: 'Orders failed during processing',
  labelNames: ['reason'] as const,
});

export const paymentDurationSeconds = new Histogram({
  name: 'payment_processing_seconds',
  help: 'Payment processing latency',
  labelNames: ['payment_method'] as const,
  buckets: [0.1, 0.25, 0.5, 1, 2.5, 5],
});
// use-cases/create-order/create-order.handler.ts
@Injectable()
export class CreateOrderHandler {
  async handle(cmd: CreateOrderCommand): Promise<OrderId> {
    const end = paymentDurationSeconds.startTimer({ payment_method: cmd.paymentMethod });
    try {
      const order = await this.orderRepository.save(Order.create(cmd));
      orderCreatedTotal.inc({ type: cmd.type });
      end();
      return order.id;
    } catch (err) {
      orderFailedTotal.inc({ reason: classifyReason(err) });
      throw err;
    }
  }
}

Label reason — категория (validation_error, payment_declined, inventory_unavailable), не err.message. Низкая cardinality по R-OBS-MTR-7.

Алерты отдельные от SLO

R-OBS-SLO-4: SLO — про user-facing успешность. Инфраструктурные и доменные алерты — отдельная категория.

КатегорияМетрика Node/NestJSДействие
Event loop lagnodejs_eventloop_lag_seconds > 0.1CPU-bound задача / блокировка event loop
Heap saturationnodejs_heap_used_bytes / nodejs_heap_size_limit > 0.85утечка памяти, scale
pg poolpg pool.waitingCount > 0 через Gaugetune pool size, медленные запросы
BullMQ lagbullmq_queue_waiting_total{queue="order-events"} > 500scale workers
Domain failuresorder_failed_total rate > 50/minрегрессия в данных или в платёжном провайдере
Cache hit ratecache_hits / (cache_hits + cache_misses) < 0.7tune TTL / Redis eviction

Пример алерта на BullMQ lag:

- alert: OrderEventsQueueLag
  expr: bullmq_queue_waiting_total{queue="order-events",service="order-service"} > 500
  for: 5m
  annotations:
    summary: "BullMQ order-events queue lag > 500"
    runbook: "https://runbooks.internal/bullmq-order-events-lag"

Алерт на event loop lag — предшественник SLO-нарушения: latency вырастет через 1–2 минуты, если lag не упадёт.

pg pool как Gauge

// metrics/pg-pool.metrics.ts
@Injectable()
export class PgPoolMetricsService implements OnModuleInit {
  private readonly poolTotal: Gauge<string>;
  private readonly poolWaiting: Gauge<string>;

  constructor(private readonly pool: Pool) {
    this.poolTotal = new Gauge({ name: 'pg_pool_total_connections', help: 'pg pool total', labelNames: [] });
    this.poolWaiting = new Gauge({ name: 'pg_pool_waiting_count', help: 'pg pool waiting', labelNames: [] });
  }

  onModuleInit(): void {
    setInterval(() => {
      this.poolTotal.set(this.pool.totalCount);
      this.poolWaiting.set(this.pool.waitingCount);
    }, 5000);
  }
}

Что запрещено

АнтипаттернПравилоЧто взамен
Alert на каждый logger.error(...)R-OBS-SLO-X1rate(app_errors_total[5m]) > 1, группировка по reason
SLO 100% targetR-OBS-SLO-X299.9% (43m/mo) или 99.95% — обсуждать с product owner
Алерт без annotations.runbookR-OBS-SLO-X3runbook URL обязателен в каждом правиле
Один широкий алерт «что-то сломалось»R-OBS-SLO-4раздельные категории: SLO burn / infra / domain / queue
Только 30d window без short windowR-OBS-SLO-2multi-window: 1h fast burn + 6h slow burn
Latency SLO по avgR-OBS-SLO-1p95 или p99 через histogram_quantile
for: не указанR-OBS-SLO-2fast burn for: 2m, slow burn for: 15m
High-cardinality label в метрике (user_id, order_id)R-OBS-MTR-X1категория (type, status_class, reason)

Куда дальше

  • Конфигурация — отдельный management-порт для /metrics, explicit endpoint list.
  • Context propagation — AsyncLocalStorage, requestId и userId для корреляции при разборе алертов.
  • Health checks — почему liveness/readiness не заменяет SLO.
  • Logging — pino, nestjs-pino, structured fields при разборе инцидента.
  • Metrics — RED-метрики, prom-client, buckets для SLI.
  • Tracing — детальный разбор burn'а через startActiveSpan и OTel.