Опирается на правила:
R-OBS-SLO-1…R-OBS-SLO-4иR-OBS-SLO-X1…R-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.
| Endpoint | Availability SLO | Latency SLO |
|---|---|---|
POST /orders | 99.9% non-5xx | p95 < 500ms |
POST /payments | 99.95% non-5xx | p95 < 1s |
GET /orders/:id | 99.95% non-5xx | p95 < 200ms |
GET /products | 99.5% non-5xx | p95 < 800ms |
Target выбирается вместе с product owner: «сколько стоит дополнительная девятка?». 99.99% (52 мин/год) — другая архитектура, multi-region, immediate failover. 99.9% (8.7 ч/год) — намного дешевле.
RED-interceptor: база для SLI
SLI вычисляется из http_server_requests_seconds — Histogram, который заполняет 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 lag | nodejs_eventloop_lag_seconds > 0.1 | CPU-bound задача / блокировка event loop |
| Heap saturation | nodejs_heap_used_bytes / nodejs_heap_size_limit > 0.85 | утечка памяти, scale |
| pg pool | pg pool.waitingCount > 0 через Gauge | tune pool size, медленные запросы |
| BullMQ lag | bullmq_queue_waiting_total{queue="order-events"} > 500 | scale workers |
| Domain failures | order_failed_total rate > 50/min | регрессия в данных или в платёжном провайдере |
| Cache hit rate | cache_hits / (cache_hits + cache_misses) < 0.7 | tune 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-X1 | rate(app_errors_total[5m]) > 1, группировка по reason |
| SLO 100% target | R-OBS-SLO-X2 | 99.9% (43m/mo) или 99.95% — обсуждать с product owner |
Алерт без annotations.runbook | R-OBS-SLO-X3 | runbook URL обязателен в каждом правиле |
| Один широкий алерт «что-то сломалось» | R-OBS-SLO-4 | раздельные категории: SLO burn / infra / domain / queue |
| Только 30d window без short window | R-OBS-SLO-2 | multi-window: 1h fast burn + 6h slow burn |
Latency SLO по avg | R-OBS-SLO-1 | p95 или p99 через histogram_quantile |
for: не указан | R-OBS-SLO-2 | fast 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.