Observability¶
«Сервис тормозит» — это не диагноз. Observability — это способность ответить
на вопрос «почему» без редеплоя и без printf. Три кита: метрики, логи,
трейсы. Над ними — SLI/SLO/SLA и error budget.
Три столпа¶
🧩 Простыми словами.
- Метрики — числа во времени. Сколько RPS, сколько ошибок, какая p99. Дёшевы по объёму, дороги по детализации.
- Логи — текстовые события. Точные подробности конкретного запроса. Дёшевы по детализации, дороги по объёму.
- Трейсы — путь одного запроса через несколько сервисов. Видишь, где потерялись 500ms.
graph LR
Metrics[Метрики<br/>что-то не так,<br/>где-то тормозит] --> Traces
Traces[Трейсы<br/>в каком сервисе<br/>и какой шаг] --> Logs
Logs[Логи<br/>детали конкретного<br/>сбоя]
⚙️ Под капотом. Каждый из них даёт свой угол, и они дополняют друг друга. Метрики говорят «есть проблема», трейсы — «вот где», логи — «вот в чём».
RED-метрики (для сервисов)¶
Минимум, который должен быть на каждом API-сервисе.
🧩 R-E-D.
- Rate — RPS, разбитые по эндпоинтам.
- Errors — процент 5xx (и 4xx, если важно).
- Duration — latency: p50 / p95 / p99.
graph TB
R[Rate: req/s] --> SLO[Health dashboard]
E[Errors: %] --> SLO
D[Duration: p99 latency] --> SLO
🛠 В Go. Стандартный стек — Prometheus + prometheus/client_golang.
import "github.com/prometheus/client_golang/prometheus"
var (
httpRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total"},
[]string{"method", "path", "status"})
httpDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{Name: "http_request_duration_seconds",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5}},
[]string{"method", "path"})
)
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
path := normalizePath(r.URL.Path) // /users/123 → /users/:id
httpRequests.WithLabelValues(r.Method, path,
strconv.Itoa(rw.status)).Inc()
httpDuration.WithLabelValues(r.Method, path).Observe(
time.Since(start).Seconds())
})
}
📌 Histogram, не gauge. Latency — это распределение. Хочешь p99 — нужен
histogram. Один mean врёт.
📌 Карты лейблов взрывают cardinality. НЕ кладите user_id в labels —
миллионы серий убьют Prometheus. Только нормализованные пути, методы, статусы.
USE-метрики (для инфраструктуры)¶
🧩 U-S-E.
- Utilization — % используемого ресурса (CPU, memory, disk).
- Saturation — насколько ресурс перегружен (queue length, run queue).
- Errors — fail-каунт ресурса (disk errors, IO errors).
Применяется к серверам, дискам, БД-инстансам, очередям. Дополняет RED.
Структурированные логи¶
🧩 Простыми словами. Не «error: что-то пошло не так», а JSON с полями.
Тогда можешь грепать request_id по всему кластеру и увидеть полную картину.
{
"ts":"2026-04-30T10:23:01Z",
"level":"error",
"msg":"payment failed",
"request_id":"abcd-1234",
"user_id":42,
"amount":1500,
"downstream_err":"timeout"
}
🛠 В Go. Используй slog (стандартная с Go 1.21+) или zap. Никогда —
fmt.Println.
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.InfoContext(ctx, "payment processed",
slog.String("user_id", userID),
slog.Int64("amount", amount),
slog.Duration("took", time.Since(start)),
)
Request-ID propagation¶
🧩 Простыми словами. На границе сервиса присваиваешь UUID запросу. Носишь его в context и пишешь в каждый лог. Передаёшь дальше в headers downstream.
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Request-ID")
if rid == "" {
rid = uuid.NewString()
}
ctx := context.WithValue(r.Context(), "rid", rid)
w.Header().Set("X-Request-ID", rid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
→ когда в логах увидишь request_id=abcd-1234, ты можешь найти все записи
во всех сервисах за этот запрос.
Куда складывать логи¶
- Loki (Grafana) — дёшево, индекс по labels, full-text по содержимому.
- Elasticsearch (ELK) — мощно, дорого.
- CloudWatch / GCP Logging — managed.
- Stdout → kubectl logs — для разработки. В проде гонять через агента (Vector, Promtail, Fluent Bit).
Distributed tracing¶
🧩 Простыми словами. Запрос идёт через 5 сервисов, в каждом по 2–3 шага. Trace — это «дерево» этих шагов с длительностями, нарисованное на одной картинке.
gantt
title Trace: POST /order
dateFormat X
axisFormat %s ms
section api-gateway
auth check :0, 5
forward to orders :5, 10
section orders
validate :10, 15
save tx :15, 80
section payment
charge call :30, 75
section emails
queue notify :80, 85
⚙️ Под капотом. Открытый стандарт — W3C Trace Context (header
traceparent). Trace = uuid; spans внутри со своими uuid и parent_id.
Реализации: OpenTelemetry (стандарт де-факто), Jaeger / Zipkin / Tempo / Datadog.
🛠 В Go. OpenTelemetry SDK.
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
tracer := otel.Tracer("orders-service")
ctx, span := tracer.Start(ctx, "Place order")
defer span.End()
span.SetAttributes(
attribute.Int64("user.id", userID),
attribute.Int("items.count", len(items)),
)
Главное: tracer носит trace-id через context.Context. Все downstream-вызовы
автоматически (через otel-instrumentation для http/grpc) пробросят header.
📌 Sampling. Не логируй 100% trace'ов в проде — слишком много данных. Sample 1–10%, плюс 100% для error spans.
SLI / SLO / SLA / Error budget¶
🧩 Простыми словами.
- SLI (Service Level Indicator) — что меряем. «99% запросов GET /feed отвечают за < 500ms».
- SLO (Objective) — какую планку ставим себе. «99.9% за 30 дней».
- SLA (Agreement) — что обещаем клиенту в контракте. Обычно SLA < SLO, чтобы был запас. «99.5%».
- Error budget —
1 - SLO. За месяц 30 × 24 × 60 × (1 - 0.999) = 43 минуты. Это ваш бюджет на сбои.
graph LR
SLI[SLI: что меряем<br/>p99 latency, error rate] --> SLO[SLO: цель<br/>99.9%]
SLO --> Budget[Error budget:<br/>сколько можно лежать]
Budget --> Decision{Бюджет потрачен?}
Decision -->|Да| Stop[Стоп фичам, работа над reliability]
Decision -->|Нет| Ship[Релизим дальше, рискуем]
SLO --> SLA[SLA: контракт<br/>99.5% — обещаем меньше, чем целимся]
⚙️ Зачем нужен error budget. Это инструмент решений: если бюджет потрачен — команда фокусируется на надёжности, не на новых фичах. Если бюджет не выходит — можно пушить релизы быстрее, рисковать с экспериментами.
📌 «100% — не SLO». Он недостижим и убьёт скорость разработки.
Мониторинг и алертинг¶
graph LR
Apps -->|metrics| Prom[Prometheus]
Apps -->|logs| Loki[Loki]
Apps -->|traces| Tempo[Tempo / Jaeger]
Prom --> Graf[Grafana<br/>dashboard]
Loki --> Graf
Tempo --> Graf
Prom --> AM[Alertmanager]
AM --> PD[PagerDuty / Slack / Telegram]
📌 Алёрты на симптомы, не на причины.
❌ Плохой алёрт: «CPU > 80%». Часто — нормальная нагрузка. ✅ Хороший алёрт: «Error rate /api > 1% over 5 min» или «p99 latency > 1s».
📌 SLO-burn rate alert. Если за час сжечь 5% месячного бюджета — это скорость, при которой бюджет кончится за 20 часов. Алёрт.
🛠 Применение в Go-проекте: эталонный стек¶
# docker-compose / k8s
- prometheus: /metrics → scrape
- grafana: dashboards
- loki: structured JSON logs
- promtail: собирает логи из контейнеров
- tempo: трейсы (OTel)
- otel-collector: принимает OTel и роутит в backends
- alertmanager: alert routing
// main.go
import (
"go.opentelemetry.io/otel"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
setupTracing(ctx) // OTel exporter → tempo
setupLogging() // slog JSON → stdout
prometheus.MustRegister(httpRequests, httpDuration)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.Handle("/api/", instrumentHandler(apiHandler))
http.ListenAndServe(":8080", mux)
}
🔥 Пример: дашборд «здоровья» сервиса¶
Top row (RED):
- RPS (last 5 min)
- Error rate (5xx %)
- p50 / p95 / p99 latency
Middle (USE):
- CPU per pod
- Memory per pod
- DB connection pool: in_use / idle / wait
Bottom (бизнес):
- Orders/min
- Payment success rate
- Conversion funnel
При инциденте смотришь сверху вниз: RED — нашёл аномалию → USE — нашёл ресурс → trace → log по request_id → готово.
❌ Типичные ошибки¶
- Логи в plaintext, без request_id → debug по нескольким сервисам — пытка.
- High-cardinality labels (
user_id,email) → Prometheus падает. - Метрика, на которую никто не смотрит — мёртвая метрика. Удали.
- Алёрт на CPU 80% → алёрт-усталость, реальные проблемы пропускаются.
- 100% trace sampling в проде → дорого и не нужно.
- Одно целевое значение SLO для всего сервиса → разные эндпоинты имеют разную природу.
🤖 Что спрашивает AI-ментор¶
- Что такое RED-метрики и почему именно эти три?
- Чем отличаются metrics, logs и traces? Когда какой инструмент использовать?
- Что такое distributed tracing и зачем нужен trace_id в headers?
- Объясни SLO и error budget на примере.
- Зачем нужны structured logs?
📊 Уровни глубины¶
L1. Знаешь Prometheus. Используешь slog. Видел Grafana dashboard.
L2. Имплементировал RED-метрики, request_id propagation, structured logs.
Понимаешь cardinality и почему user_id в labels — плохо.
L3. Настраивал OpenTelemetry в проде. Считал error budget, делал SLO-based alerting. Дебажил инциденты по traces. Знаешь tradeoff sampling rate vs cost. Видел chaos engineering и game day.
📝 Подумай¶
- Сервис тормозит, p99 latency скакнул с 100ms до 2s. Какой следующий шаг диагностики?
- У тебя SLO 99.9% доступности. За первые 10 дней месяца сервис лежал 30 минут. Сколько у тебя осталось error budget на оставшиеся 20 дней?
- Зачем нужен
traceparentheader при вызове downstream-сервиса?
Ответ
- Дашборд RED → видишь что упало (errors? latency?). Если latency — смотришь по эндпоинтам, потом по downstream. USE → видишь ли saturation (DB connections wait, CPU 100%). Trace последнего медленного запроса → где конкретный шаг затормозил. Логи по этому request_id → детали.
- Error budget 99.9% за месяц = 0.1% × 30 × 24 × 60 = 43 минуты. Потратил 30, осталось 13 минут на 20 дней. Бюджет почти кончился — стоп фичам, фокус на reliability.
- Это W3C Trace Context: содержит trace_id текущего запроса. Downstream сервис прочитает его и привяжет свои spans к тому же trace, чтобы в Jaeger/Tempo была единая картинка end-to-end.
Дальше: case-studies.md — собираем всё в готовые архитектуры.