Цели модуля

После модуля студент сможет:

  • Проектировать метрики по методологиям RED, USE, Four Golden Signals
  • Избегать проблем high-cardinality labels
  • Настраивать distributed tracing через OpenTelemetry для микросервисов
  • Коррелировать логи, метрики и трейсы между сервисами
  • Проектировать стратегию алертинга без alert fatigue
  • Определять SLI, SLO, SLA и управлять error budget
  • Выбирать observability стек под масштаб системы

5.1 Observability на масштабе: отличия от базового

Стек observability: три столпа → Grafana → SLO

Схема задаёт маршрут данных: сервисы отдают метрики, логи и трейсы через OTel SDK/агент (семплинг и батчинг — чтобы не задушить себя и хранилище); каждый столп едет в свой бэкенд, а Grafana сводит их в дашборды и алерты, привязанные к SLO/error budget. Отдельная ветка — high-cardinality значения (user_id, request_id): их место в логах и трейсах, а не в метках метрик, иначе кардинальность взрывает хранилище.

Базовый vs продвинутый observability

АспектБазовый (Middle)Продвинутый (Senior)
МетрикиHTTP requests, errorsRED/USE, бизнес-метрики, SLO
ЛогиStructured loggingCorrelation между сервисами
ТрейсыОдин сервис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 на масштабе

  1. Централизация: все данные в одном месте
  2. Корреляция: единый trace ID через все сервисы
  3. Retention: политики хранения по типам данных
  4. Sampling: для трейсов при большом объёме
  5. 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

Бизнес-метрики

Кроме технических, собирайте бизнес-метрики:

go
var (
    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 содержит шаблон роута:

go
mux.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

Каждый сервис:

  1. Извлекает traceparent из входящего запроса
  2. Создаёт новый span с тем же trace-id
  3. Передаёт traceparent в исходящие запросы

Настройка OpenTelemetry в Go

Инициализация TracerProvider:

go
package 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:

go
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

// Оборачиваем роутер
handler := otelhttp.NewHandler(mux, "git-server")

// Для клиентских запросов
client := &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

Создание spans в бизнес-логике

go
import (
    "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:

go
tp := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),  // 10%
)

Tail-based sampling (сложнее, через OpenTelemetry Collector):

  • Все ошибки — сохраняем
  • Запросы > 1s — сохраняем
  • Остальные — 1%

Tracing для баз данных и внешних сервисов

Для pgx есть обёртки:

go
import "github.com/exaring/otelpgx"

pool, err := pgxpool.NewWithConfig(ctx, config)
// config.ConnConfig.Tracer = otelpgx.NewTracer()

Для HTTP-клиентов:

go
client := &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

Tracing для фоновых jobs

Для cron jobs создаём корневой span:

go
func (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
  • Несколько сервисов
  • Несколько внешних вызовов

Нужно связать все логи одного запроса.

Уровни корреляции

  1. Request ID: в рамках одного сервиса
  2. Trace ID: через все сервисы
  3. Span ID: в рамках одной операции

Реализация через slog

Кастомный handler который добавляет trace_id из context:

go
type 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)
}

Использование:

go
logger := 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-basedError rate > 5%Инвестигация, fix
Cause-basedDB connection failedИнвестигация, fix
PredictiveDisk 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 часов

Пример алерта:

yaml
groups:
  - 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

Правила хорошего алертинга

  1. Алерт должен быть actionable Плохо: "CPU high" (и что делать?) Хорошо: "Error rate > 5% for 5 min" (нужно инвестигировать)

  2. Severity имеет значение

    • critical: будит on-call, требует немедленного действия
    • warning: в рабочее время, планировать fix
    • info: в дашборд, не алерт
  3. Группируйте и дедуплицируйте Один алерт на инцидент, не 100 алертов на 100 pod'ов.

  4. Тестируйте алерты Регулярно проверяйте что алерты срабатывают. Проводите game days.

  5. Документируйте 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Как измерить
APIAvailabilityУспешные запросы / все запросы
APILatencyp99 < 200ms
Batch jobFreshnessДанные не старше 5 минут
StorageDurabilityОтсутствие потерь данных

Определение 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

SLODowntime в месяц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:

yaml
receivers:
  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 годАгрегаты дешевле
Logs30 днейДля debugging
Traces7 днейОчень объёмные

Стоимость 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

Что изучаем на примере шаблона

  1. RED метрики для HTTP
  2. High-cardinality защита через нормализацию path
  3. OpenTelemetry для tracing
  4. Correlation через trace_id в логах
  5. 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 "Проверь себя"

1. Для чего нужен заголовок W3C Trace Context?
text
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
2. Чем Histogram отличается от Summary в метриках?
3. Как избежать alert fatigue? — выберите все подходящие
4. Чему равен error budget при SLO 99.9% availability за 30 дней?
5. Чем SLI отличается от SLO и SLA?
6. Что означает burn rate 14.4 в алерте?
yaml
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > (14.4 * 0.001)
7. Что измеряет RED-метод? — выберите все подходящие
8. Что такое tail-based sampling?
9. Почему user_id нельзя использовать как label в Prometheus-метрике?
10. Какие сигналы входят в Four Golden Signals? — выберите все подходящие

✅ Итоги модуля

Студент должен:

  • Проектировать метрики по 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/ шаблона.