Цели модуля
После модуля студент сможет:
- Проектировать метрики по методологиям RED, USE, Four Golden Signals
- Избегать проблем high-cardinality labels
- Настраивать distributed tracing через OpenTelemetry для микросервисов
- Коррелировать логи, метрики и трейсы между сервисами
- Проектировать стратегию алертинга без alert fatigue
- Определять SLI, SLO, SLA и управлять error budget
- Выбирать observability стек под масштаб системы
5.1 Observability на масштабе: отличия от базового
Схема задаёт маршрут данных: сервисы отдают метрики, логи и трейсы через
OTel SDK/агент (семплинг и батчинг — чтобы не задушить себя и хранилище);
каждый столп едет в свой бэкенд, а Grafana сводит их в дашборды и алерты,
привязанные к SLO/error budget. Отдельная ветка — high-cardinality
значения (user_id, request_id): их место в логах и трейсах, а не в метках
метрик, иначе кардинальность взрывает хранилище.
Базовый vs продвинутый observability
| Аспект | Базовый (Middle) | Продвинутый (Senior) |
|---|---|---|
| Метрики | HTTP requests, errors | RED/USE, бизнес-метрики, SLO |
| Логи | Structured logging | Correlation между сервисами |
| Трейсы | Один сервис | Distributed tracing |
| Алерты | На ошибки | На SLO burn rate |
| Дашборды | По сервису | По бизнес-процессу |
Три столпа observability на масштабе
Logs:
- Детали конкретных событий
- Обязательна корреляция (trace ID, request ID)
- Централизованное хранилище (Loki, Elasticsearch)
Metrics:
- Агрегированные показатели
- Дёшевы в хранении
- Основа для алертов и SLO
Traces:
- Путь запроса через сервисы
- Поиск узких мест
- Анализ задержек
Связь трёх столпов
Правильный observability связывает всё:
Метрика: error rate вырос
-> Лог: конкретная ошибка с trace_id
-> Трейс: путь запроса через сервисы, где упал
Это называется Observability Pipeline: Metrics (что?) -> Logs (почему?) -> Traces (где?)
Требования к observability на масштабе
- Централизация: все данные в одном месте
- Корреляция: единый trace ID через все сервисы
- Retention: политики хранения по типам данных
- Sampling: для трейсов при большом объёме
- Cost control: observability сам по себе дорог
5.2 Metrics design
Four Golden Signals (Google SRE)
Четыре ключевых сигнала для любого сервиса:
| Сигнал | Что измеряет | Пример метрики |
|---|---|---|
| Latency | Задержка запросов | http_request_duration_seconds |
| Traffic | Нагрузка на сервис | http_requests_total (rate) |
| Errors | Процент ошибок | http_requests_total{status=~"5.."} |
| Saturation | Загрузка ресурсов | cpu_usage, memory_usage, pool_usage |
RED метод (для сервисов)
RED — Rate, Errors, Duration. Применяется к каждому сервису:
# Rate: запросов в секунду
rate(http_requests_total[5m])
# Errors: процент ошибок
rate(http_requests_total{status=~"5.."}[5m])
/ rate(http_requests_total[5m])
# Duration: распределение задержек
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m]))
USE метод (для ресурсов)
USE — Utilization, Saturation, Errors. Применяется к ресурсам:
# Utilization: использование ресурса
# CPU
rate(container_cpu_usage_seconds_total[5m])
# Memory
container_memory_usage_bytes / container_spec_memory_limit_bytes
# Saturation: очередь/перегрузка
# Connection pool
db_pool_waiting_connections
# Errors: ошибки ресурса
db_connection_errors_total
Бизнес-метрики
Кроме технических, собирайте бизнес-метрики:
govar ( reposCreatedTotal = promauto.NewCounter( prometheus.CounterOpts{ Name: "repos_created_total", Help: "Total repositories created", }, ) ordersProcessedTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "orders_processed_total", Help: "Total orders processed", }, []string{"status", "payment_method"}, ) outboxLagSeconds = promauto.NewGauge( prometheus.GaugeOpts{ Name: "outbox_lag_seconds", Help: "Age of oldest pending outbox event", }, ) )
Бизнес-метрики помогают отвечать на вопросы:
- Сколько заказов обработано?
- Какой процент успешных платежей?
- Есть ли задержка в outbox?
Типы метрик: когда что использовать
| Тип | Когда | Пример |
|---|---|---|
| Counter | Монотонно растёт | requests_total |
| Gauge | Растёт и падает | pool_size, temperature |
| Histogram | Распределение с бакетами | duration_seconds |
| Summary | Квантили на клиенте | duration_seconds (p99) |
Histogram vs Summary:
- Histogram: бакеты, агрегация на сервере, можно считать любые квантили
- Summary: квантили на клиенте, нельзя агрегировать между инстансами
Рекомендация: используйте Histogram. Summary не агрегируется.
Правила именования метрик
# Формат: <namespace>_<subsystem>_<name>_<unit>
http_server_request_duration_seconds
db_pool_acquired_connections
outbox_events_pending_total
Правила:
- Суффикс единицы измерения: _seconds, _bytes, _total
- Counter обязательно с _total
- Не используйте точки и дефисы (только подчёркивания)
- Единообразие во всех сервисах
5.3 High-cardinality labels
Что такое кардинальность
Кардинальность метки — количество уникальных значений.
Низкая кардинальность (хорошо):
- method: GET, POST, PUT, DELETE (4-9 значений)
- status: 200, 400, 404, 500 (~20 значений)
- region: us-east, eu-west (~10 значений)
Высокая кардинальность (плохо):
- user_id: миллионы значений
- request_id: уникален для каждого запроса
- email: уникален для каждого пользователя
- url_path: /users/123, /users/456 (тысячи значений)
Почему high-cardinality — проблема
Каждая уникальная комбинация меток — отдельная временная серия.
Пример: http_requests_total{method="GET", path="/users/{id}"}
Если path не нормализован: /users/1, /users/2, ..., /users/1000000
Это 1 миллион временных серий на одну метрику!
Последствия:
- Взрыв памяти Prometheus
- Медленные запросы
- Высокая стоимость хранения
- Невозможность агрегации
Решение 1: нормализация path
go// ПЛОХО: реальный path func normalizePath(path string) string { return path // /users/123 } // ПРАВИЛЬНО: шаблон path func normalizePath(r *http.Request) string { // Используем шаблон роута из ServeMux if pattern := r.Pattern; pattern != "" { return pattern // "GET /users/{id}" } return "unknown" }
В Go 1.22+ http.Request.Pattern содержит шаблон роута:
gomux.HandleFunc("GET /users/{id}", handler)
go// В middleware pattern := r.Pattern // "GET /users/{id}"
Решение 2: ограничение значений
go// ПЛОХО: status как есть httpRequestsTotal.WithLabelValues(method, path, strconv.Itoa(status)) // ПРАВИЛЬНО: группируем статусы func normalizeStatus(status int) string { switch { case status >= 200 && status < 300: return "2xx" case status >= 400 && status < 500: return "4xx" case status >= 500: return "5xx" default: return "other" } }
Решение 3: вынос в логи
Детали с высокой кардинальностью — в логи, не в метки:
go// ПЛОХО: user_id в метке apiRequestsTotal.WithLabelValues(userID, endpoint).Inc() // ПРАВИЛЬНО: user_id в логе apiRequestsTotal.WithLabelValues(endpoint).Inc() logger.Info("api request", "user_id", userID, // в логе, не в метке "endpoint", endpoint, )
Правило
Метки с кардинальностью > 100 — красный флаг. Метки с кардинальностью > 1000 — запретить.
Мониторинг кардинальности
# Топ метрик по количеству серий
topk(10, count by (__name__)({__name__=~".+"}))
# Количество серий на метрику
count by (__name__)({__name__=~".+"})
5.4 Tracing в распределённых системах
Проблема монолитного tracing
В монолите один запрос — один стек вызовов. В микросервисах запрос проходит через несколько сервисов:
Client -> API Gateway -> Order Service -> Payment Service -> DB
Нужно видеть весь путь, а не только один сервис.
OpenTelemetry: ключевые концепции
Trace — полный путь запроса через систему. Span — одна операция в рамках trace. Span Context — метаданные для передачи между сервисами.
Trace abc-123:
├── Span: HTTP GET /orders (API Gateway, 150ms)
│ ├── Span: OrderService.GetOrder (80ms)
│ │ ├── Span: DB SELECT (50ms)
│ │ └── Span: Cache GET (5ms)
│ └── Span: PaymentService.CheckStatus (60ms)
│ └── Span: gRPC call (55ms)
Propagation через W3C Trace Context
Стандартный заголовок для передачи trace context:
traceparent: 00-<trace-id>-<parent-id>-<trace-flags>
Пример: traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Каждый сервис:
- Извлекает traceparent из входящего запроса
- Создаёт новый span с тем же trace-id
- Передаёт traceparent в исходящие запросы
Настройка OpenTelemetry в Go
Инициализация TracerProvider:
gopackage tracing import ( "context" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) func InitTracer(ctx context.Context, cfg TracingConfig) (*sdktrace.TracerProvider, error) { // Exporter (OTLP gRPC) exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.Endpoint), otlptracegrpc.WithInsecure(), ) if err != nil { return nil, fmt.Errorf("create exporter: %w", err) } // Resource: информация о сервисе res, err := resource.Merge( resource.Default(), resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceName(cfg.ServiceName), semconv.ServiceVersion(cfg.Version), semconv.DeploymentEnvironment(cfg.Environment), ), ) if err != nil { return nil, fmt.Errorf("create resource: %w", err) } // TracerProvider с sampling tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.SampleRate)), ) // Глобальные настройки otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) return tp, nil }
Middleware для HTTP
Используем otelhttp из OpenTelemetry contrib:
goimport "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" // Оборачиваем роутер handler := otelhttp.NewHandler(mux, "git-server") // Для клиентских запросов client := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }
Создание spans в бизнес-логике
goimport ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" ) var tracer = otel.Tracer("git-server") func (s *RepoService) GetRepo(ctx context.Context, id string) (*models.Repo, error) { ctx, span := tracer.Start(ctx, "RepoService.GetRepo") defer span.End() // Атрибуты для поиска span.SetAttributes( attribute.String("repo.id", id), ) repo, err := s.repo.GetByID(ctx, id) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "repo not found") return nil, fmt.Errorf("get repo: %w", err) } return repo, nil }
Sampling стратегии
При большом объёме трейсы дорого хранить. Используем sampling:
| Стратегия | Описание | Когда |
|---|---|---|
| Always on | Все трейсы | Разработка, staging |
| Always off | Ничего | Не использовать |
| Ratio | Процент трейсов | Production (0.1 = 10%) |
| Tail-based | По результату | Ошибки, медленные запросы |
Ratio sampling в Go:
gotp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // 10% )
Tail-based sampling (сложнее, через OpenTelemetry Collector):
- Все ошибки — сохраняем
- Запросы > 1s — сохраняем
- Остальные — 1%
Tracing для баз данных и внешних сервисов
Для pgx есть обёртки:
goimport "github.com/exaring/otelpgx" pool, err := pgxpool.NewWithConfig(ctx, config) // config.ConnConfig.Tracer = otelpgx.NewTracer()
Для HTTP-клиентов:
goclient := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }
Tracing для фоновых jobs
Для cron jobs создаём корневой span:
gofunc (j *OutboxRelayJob) Run(ctx context.Context) error { ctx, span := tracer.Start(ctx, "OutboxRelayJob.Run", trace.WithSpanKind(trace.SpanKindConsumer), ) defer span.End() // Логика job }
5.5 Log correlation
Зачем нужна корреляция
Один запрос проходит через:
- Несколько middleware
- Несколько сервисов
- Несколько внешних вызовов
Нужно связать все логи одного запроса.
Уровни корреляции
- Request ID: в рамках одного сервиса
- Trace ID: через все сервисы
- Span ID: в рамках одной операции
Реализация через slog
Кастомный handler который добавляет trace_id из context:
gotype TracingHandler struct { next slog.Handler } func (h *TracingHandler) Handle(ctx context.Context, r slog.Record) error { // Извлекаем trace ID из context spanCtx := trace.SpanContextFromContext(ctx) if spanCtx.IsValid() { r.AddAttrs( slog.String("trace_id", spanCtx.TraceID().String()), slog.String("span_id", spanCtx.SpanID().String()), ) } return h.next.Handle(ctx, r) }
Использование:
gologger := slog.New(&TracingHandler{ next: slog.NewJSONHandler(os.Stdout, nil), }) // В коде: logger получает ctx автоматически logger.InfoContext(ctx, "repo created", "repo_id", repo.ID)
Результат в JSON:
json{ "time": "2026-01-15T10:30:00Z", "level": "INFO", "msg": "repo created", "repo_id": "repo-123", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7" }
Поиск логов по trace ID
В Loki:
text{service="git-server"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
В Grafana можно кликнуть на trace ID и перейти к трейсу.
Передача trace context через очереди
Для асинхронных операций (очереди, outbox) trace context нужно сохранять в метаданных сообщения:
go// Публикация event.Metadata["traceparent"] = formatTraceparent(span.SpanContext()) // Потребление func (c *Consumer) Process(ctx context.Context, msg Message) { // Извлекаем traceparent из метаданных propagator := otel.GetTextMapPropagator() ctx = propagator.Extract(ctx, propagation.MapCarrier(msg.Metadata)) // Создаём span для обработки ctx, span := tracer.Start(ctx, "Consumer.Process") defer span.End() // Обработка }
5.6 Alerting strategy
Проблема alert fatigue
Если алертов слишком много:
- Разработчики игнорируют их
- Критичные алерты теряются
- Выгорание команды
Правило: алерт должен требовать действия. Если алерт не требует действия — это не алерт, а метрика для дашборда.
Типы алертов
| Тип | Пример | Действие |
|---|---|---|
| Symptom-based | Error rate > 5% | Инвестигация, fix |
| Cause-based | DB connection failed | Инвестигация, fix |
| Predictive | Disk 80% full | Планирование |
Рекомендация: больше symptom-based, меньше cause-based.
Symptom: "пользователи видят ошибки" — важно. Cause: "один pod перезапустился" — может быть не важно.
Алерты на SLO burn rate
Вместо простых алертов на error rate — алерты на SLO burn rate.
Burn rate: как быстро расходуется error budget.
# SLO: 99.9% успеха за 30 дней
# Error budget: 0.1% = 43.2 минуты простоя за 30 дней
# Burn rate 1.0: расходуем budget ровно за 30 дней
# Burn rate 14.4: расходуем budget за 2 дня
# Burn rate 100: расходуем budget за 7 часов
Пример алерта:
yamlgroups: - name: slo-alerts rules: # Критичный: budget кончится за 1 час - alert: HighErrorBurnRate expr: | ( rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) ) > (14.4 * 0.001) for: 5m labels: severity: critical slo: api-availability annotations: summary: "Error budget burning too fast" description: "At this rate, error budget will be exhausted in 1 hour" # Предупреждение: budget кончится за 6 часов - alert: ElevatedErrorBurnRate expr: | ( rate(http_requests_total{status=~"5.."}[30m]) / rate(http_requests_total[30m]) ) > (6 * 0.001) for: 15m labels: severity: warning slo: api-availability
Правила хорошего алертинга
-
Алерт должен быть actionable Плохо: "CPU high" (и что делать?) Хорошо: "Error rate > 5% for 5 min" (нужно инвестигировать)
-
Severity имеет значение
- critical: будит on-call, требует немедленного действия
- warning: в рабочее время, планировать fix
- info: в дашборд, не алерт
-
Группируйте и дедуплицируйте Один алерт на инцидент, не 100 алертов на 100 pod'ов.
-
Тестируйте алерты Регулярно проверяйте что алерты срабатывают. Проводите game days.
-
Документируйте runbook Каждый алерт должен ссылаться на runbook: "что делать когда этот алерт сработал".
Alert routing
Разные алерты — разные каналы:
yaml# Alertmanager config route: receiver: default routes: - match: severity: critical receiver: pagerduty repeat_interval: 1h - match: severity: warning receiver: slack repeat_interval: 4h
5.7 SLI/SLO/SLA и error budgets
Определения
SLI (Service Level Indicator): Измеряемый показатель качества сервиса. Примеры: latency, availability, error rate.
SLO (Service Level Objective): Целевое значение SLI. Внутренняя цель. Пример: "p99 latency < 200ms", "availability > 99.9%".
SLA (Service Level Agreement): Контракт с клиентом с последствиями за нарушение. Обычно слабее SLO. Если SLA нарушено — компенсации.
Примеры SLI
| Сервис | SLI | Как измерить |
|---|---|---|
| API | Availability | Успешные запросы / все запросы |
| API | Latency | p99 < 200ms |
| Batch job | Freshness | Данные не старше 5 минут |
| Storage | Durability | Отсутствие потерь данных |
Определение SLO
Шаг 1: Выберите SLI
# Availability SLI
ratio = успешные запросы / все запросы
Шаг 2: Определите целевое значение
# SLO: 99.9% запросов успешны за 30 дней
Шаг 3: Определите окно
# Rolling 30 days
Error budget
Error budget = 100% - SLO
Для SLO 99.9%: Error budget = 0.1% = 43.2 минуты за 30 дней
Что можно делать с error budget:
- Если budget есть: можно рисковать, деплоить чаще
- Если budget кончился: freeze деплоев, focus на reliability
Расчёт error budget в Prometheus
# SLO availability: 99.9% за 30 дней
# Error ratio за 30 дней
1 - (
sum(rate(http_requests_total{status!~"5.."}[30d]))
/ sum(rate(http_requests_total[30d]))
)
# Оставшийся error budget
(1 - 0.999) - (
1 - (
sum(rate(http_requests_total{status!~"5.."}[30d]))
/ sum(rate(http_requests_total[30d]))
)
)
Таблица популярных SLO
| SLO | Downtime в месяц | Downtime в год |
|---|---|---|
| 99% | 7.3 часа | 3.65 дня |
| 99.9% | 43.2 мин | 8.76 часов |
| 99.99% | 4.32 мин | 52.6 мин |
| 99.999% | 26.3 сек | 5.26 мин |
Каждая девятка дороже предыдущей в 10 раз. Не ставьте 99.999% без реальной необходимости.
SLO для разных компонентов
Не только для API:
# SLO для outbox relay
# Events должны публиковаться в течение 60 секунд
outbox_event_age_seconds{quantile="0.99"} < 60
# SLO для базы данных
# Query latency p99 < 50ms
db_query_duration_seconds{quantile="0.99"} < 0.05
Отображение SLO в Grafana
Создайте SLO дашборд:
- Текущий SLI vs SLO цель
- Error budget remaining (в процентах и минутах)
- Burn rate за разные окна (1h, 6h, 24h, 30d)
- История нарушений SLO
5.8 Observability architecture: стек и компоненты
Типичный стек для production
Метрики:
- Prometheus (сбор и хранение)
- Grafana (визуализация)
- Alertmanager (алерты)
Логи:
- Fluent Bit / Promtail (сбор)
- Loki (хранение)
- Grafana (визуализация)
Трейсы:
- OpenTelemetry Collector (приём и обработка)
- Jaeger / Tempo (хранение)
- Grafana (визуализация)
Unified observability через Grafana
Grafana объединяет все три столпа:
- Metrics datasource: Prometheus
- Logs datasource: Loki
- Traces datasource: Tempo/Jaeger
Связь через trace ID:
- В логе есть trace_id
- Из лога можно перейти к трейсу
- Из трейса можно перейти к метрикам сервиса
OpenTelemetry Collector
Для production рекомендуется использовать OTel Collector:
Сервисы -> OTel Collector -> Backends (Jaeger, Prometheus, etc.)
Преимущества:
- Единая точка приёма телеметрии
- Sampling, фильтрация, обогащение
- Независимость от backend
- Retry и буферизация
Пример конфига Collector:
yamlreceivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 10s memory_limiter: limit_mib: 512 tail_sampling: policies: - name: errors-policy type: status_code status_code: {status_codes: [ERROR]} - name: slow-traces-policy type: latency latency: {threshold_ms: 1000} - name: base-rate type: probabilistic probabilistic: {sampling_percentage: 10} exporters: otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true prometheus: endpoint: 0.0.0.0:8889 service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, tail_sampling, batch] exporters: [otlp/jaeger] metrics: receivers: [otlp] processors: [memory_limiter, batch] exporters: [prometheus]
Retention политики
Разные данные хранятся разное время:
| Данные | Retention | Почему |
|---|---|---|
| Metrics (raw) | 15 дней | Дорого хранить |
| Metrics (downsampled) | 1 год | Агрегаты дешевле |
| Logs | 30 дней | Для debugging |
| Traces | 7 дней | Очень объёмные |
Стоимость observability
Observability — значительная статья расходов.
Факторы стоимости:
- Объём метрик (количество серий)
- Объём логов (GB/день)
- Объём трейсов (GB/день)
- Retention период
Оптимизация:
- Sampling для трейсов
- Нормализация меток
- Relabeling для удаления ненужных меток
- Агрегация вместо raw данных
Привязка к шаблону проекта
Где observability в шаблоне
internal/observability/
├── observability.go # Инициализация стека
├── logging/
│ ├── logger.go # slog setup
│ ├── pii.go # PII masking
│ └── fields.go # Standard fields
├── tracing/
│ ├── tracer.go # OTel TracerProvider
│ ├── propagation.go # W3C Trace Context
│ └── middleware.go # Tracing middleware
├── metrics/
│ ├── metrics.go # Определение метрик
│ ├── prometheus.go # /metrics endpoint
│ └── labels.go # Cardinality rules
└── contextx/
└── context.go # Context helpers
Что изучаем на примере шаблона
- RED метрики для HTTP
- High-cardinality защита через нормализацию path
- OpenTelemetry для tracing
- Correlation через trace_id в логах
- Health checks
Задание: изучите internal/observability/
- Какие метрики собираются?
- Есть ли защита от high-cardinality?
- Как настроен tracing?
- Как коррелируются логи и трейсы?
💱 Компромиссы
- Метки vs логи: агрегированные метки дёшевы и питают SLO-алерты,
но
user_idилиrequest_idв label взрывают число временных серий; высококардинальные детали уходят в логи сtrace_id, но поиск по ним медленнее и дороже в хранении. - Head-based ratio vs tail-based sampling:
TraceIDRatioBased(0.1)решает на старте трейса и почти бесплатен, но может выкинуть ошибку, ради которой трейс и нужен; tail-based сохраняет все ошибки и запросы дольше 1s, но требует OTel Collector и буферизации целого трейса. - Symptom-based vs cause-based алерты: симптомы вроде error rate и burn rate будят только по делу, но указывают на боль, а не на виновника — нужна инвестигация; cause-based сразу называет причину, но шумит: перезапуск одного pod'а часто не влияет на пользователей.
- SLO 99.9% vs 99.999%: каждая следующая девятка примерно в 10 раз дороже и сжимает error budget до 26.3 секунды простоя в месяц; мягкий SLO оставляет бюджет для смелых релизов, но пользователи чаще видят ошибки.
- Histogram vs Summary: бакеты histogram агрегируются между инстансами и позволяют считать любые квантили на сервере, но квантили приближённые; Summary считает p99 точно на клиенте, но его серии нельзя агрегировать между инстансами.
Практическое задание
Задание 1: RED метрики
Реализуйте полный набор RED метрик для вашего сервиса:
http_requests_total{method, path, status}
http_request_duration_seconds_bucket{method, path}
http_requests_in_flight
Используйте нормализацию path через r.Pattern.
Задание 2: High-cardinality защита
Напишите middleware который:
- Нормализует path (шаблон вместо реального пути)
- Группирует status коды (2xx, 4xx, 5xx)
- Не добавляет user_id, request_id в метки
Проверьте что кардинальность каждой метки < 100.
Задание 3: Distributed tracing
Настройте OpenTelemetry:
- TracerProvider с OTLP exporter
- otelhttp middleware для сервера
- otelhttp transport для клиента
- Span в RepoService.GetRepo с атрибутами
- Sampling 10% для production
Задание 4: Log correlation
Реализуйте TracingHandler для slog:
- Извлекает trace_id и span_id из context
- Добавляет в каждый лог
Проверьте что по trace_id можно найти все логи запроса.
Задание 5: SLO алерты
Определите SLO для вашего сервиса:
- Availability: 99.9% за 30 дней
- Latency: p99 < 200ms
Напишите Prometheus rules для burn rate алертов:
- Critical: budget кончится за 1 час
- Warning: budget кончится за 6 часов
Задание 6: SLO дашборд
Создайте Grafana дашборд с:
- Текущий SLI vs SLO цель
- Error budget remaining
- Burn rate за 1h, 6h, 24h, 30d
- RED метрики
Задание 7: Alert routing
Настройте Alertmanager:
- critical -> PagerDuty / Telegram (немедленно)
- warning -> Slack (рабочее время)
- Группировка по service и severity
- Repeat interval: 1h для critical, 4h для warning
Задание 8: OTel Collector
Разверните OpenTelemetry Collector с:
- OTLP receiver (gRPC + HTTP)
- Tail-based sampling (errors + slow + 10% base)
- Экспорт в Jaeger и Prometheus
Quiz "Проверь себя"
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > (14.4 * 0.001)user_id нельзя использовать как label в Prometheus-метрике?✅ Итоги модуля
Студент должен:
- Проектировать метрики по RED, USE, Four Golden Signals
- Избегать high-cardinality labels
- Настраивать OpenTelemetry для distributed tracing
- Коррелировать логи, метрики, трейсы через trace ID
- Проектировать алерты без alert fatigue
- Определять SLI/SLO/SLA и error budget
- Понимать observability стек и retention политики
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 6
Следующий модуль: Модуль 6. Security and Secrets - HMAC signing and verification (углублённо), secret rotation, least privilege, secure configuration, audit-friendly logging, secure external communication, mTLS, Zero Trust. Привязка к internal/security/ шаблона.