Observability — метрики, логи, трейсы
Перед началом: Модули 1-10. Инструменты: Yandex Monitoring, Cloud Logging, tracing. Время: ~5 ч.
В этом модуле добавляем наблюдаемость в бэкенд чата: метрики (Prometheus/OpenTelemetry),
структурированные логи (JSON с trace_id) и трейсы. Цель: видеть, где «тормозит» и что упало — до
того, как об этом напишет пользователь.
🔥 Зачем это вообще нужно
В 3 часа ночи — алерт: «Error Rate 15%». Вы открете логи — 10 000 строк в минуту. Где проблема? YDB? Function? Gateway? WebSocket? Без метрик и трейсов — поиск иголки в стоге сена. Нужна Observability: метрики (Prometheus), логи (Loki), трейсы (Jaeger) — всё с trace_id.
📖 Теория
Три стула наблюдаемости
| Столп | Инструмент | Что даёт |
|---|---|---|
| Метрики | OpenTelemetry → Prometheus endpoint | агрегаты: RPS, latency p95/p99, error rate |
| Логи | JSON в stdout → Yandex Cloud Logging | детальный контекст: что произошло, trace_id |
| Трейсы | OpenTelemetry → Yandex Tracing | сквозной путь запроса: handler → UseCase → YDB |
Структура пакета observability
internal/observability/
├── metrics/
│ ├── http.go # latency, коды ответов
│ ├── usecase.go # длительность сценариев
│ ├── ydb.go # latency, throttle, ошибки
│ └── ws.go # активные соединения, ping
├── tracing/
│ └── provider.go # init OTel tracer
└── logging/
└── structured.go # JSON-логи с trace_id
Шаг 1. Инициализация OpenTelemetry
go// internal/observability/metrics/init.go func Init(ctx context.Context) error { exporter, err := prometheus.New() if err != nil { return err } provider := metric.NewMeterProvider(metric.WithReader(exporter)) otel.SetMeterProvider(provider) // Prometheus endpoint запускается автоматически return nil }
В main.go: observability.Init(ctx) до запуска сервера. Это выполняется один раз на «холодный старт»
и переиспользуется для всех запросов.
Шаг 2. Метрики HTTP
govar httpDuration = otel.Meter("chat").MustFloat64Histogram( "http.request.duration", metric.WithUnit("ms"), ) func RecordHTTP(ctx context.Context, method, path string, status int, dur time.Duration) { httpDuration.Record(ctx, float64(dur.Milliseconds()), attribute.String("method", method), attribute.String("path", path), attribute.Int("status", status), ) }
Оборачиваем mux middleware:
gofunc MetricsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: 200} next.ServeHTTP(rec, r) RecordHTTP(r.Context(), r.Method, r.URL.Path, rec.status, time.Since(start)) }) }
Шаг 3. Метрики UseCase
govar usecaseDuration = otel.Meter("chat").MustFloat64Histogram( "usecase.duration", metric.WithUnit("ms"), ) func UseCaseTimer(ctx context.Context, name string) func(err error) { start := time.Now() return func(err error) { usecaseDuration.Record(ctx, float64(time.Since(start).Milliseconds()), attribute.String("usecase", name), attribute.Bool("error", err != nil), ) } }
Использование в любом UseCase:
gofunc (uc *SendMessageUseCase) Execute(ctx context.Context, msg domain.Message) error { defer observability.UseCaseTimer(ctx, "send_message")(nil) // err обновится return uc.service.ProcessMessage(ctx, msg) }
Шаг 4. Метрики YDB
govar ( ydbDuration = otel.Meter("ydb").MustFloat64Histogram("ydb.query.duration", metric.WithUnit("ms")) ydbThrottled = otel.Meter("ydb").MustInt64Counter("ydb.throttled") ) func RecordYDB(ctx context.Context, query string, dur time.Duration, err error) { ydbDuration.Record(ctx, float64(dur.Milliseconds()), attribute.String("query", query)) if err != nil && isThrottlingError(err) { ydbThrottled.Add(ctx, 1, attribute.String("query", query)) } }
Рост ydb.throttled — первый сигнал, что нужны индексы или оптимизация запросов (см. модуль 6).
Шаг 5. Метрики WebSocket
govar wsActive = otel.Meter("ws").MustInt64ObservableGauge("ws.connections.active") type WSMonitor struct{ count int64 } func (m *WSMonitor) Inc() { atomic.AddInt64(&m.count, 1) } func (m *WSMonitor) Dec() { atomic.AddInt64(&m.count, -1) } // Регистрация gauge → экспортирует count при scrape
Шаг 6. Структурированные логи (JSON)
gotype LogEntry struct { Time time.Time `json:"time"` Level string `json:"level"` Msg string `json:"msg"` TraceID string `json:"trace_id,omitempty"` UserID string `json:"user_id,omitempty"` } func Log(ctx context.Context, level, msg string, fields map[string]any) { entry := LogEntry{Time: time.Now(), Level: level, Msg: msg} if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() { entry.TraceID = span.SpanContext().TraceID().String() } // merge fields → JSON → stdout json.NewEncoder(os.Stdout).Encode(entry) }
Yandex Cloud Logging автоматически парсит JSON и строит индексы по trace_id, level, user_id.
Интеграция с Yandex Cloud
| Сервис | Что забирает | Как |
|---|---|---|
| Yandex Monitoring | метрики из Prometheus endpoint | автоматический scrape (или ручная настройка) |
| Yandex Logging | JSON-логи из stdout | ингестия из Container/Function |
| Yandex Tracing | трейсы OTel | OTLP exporter → tracing.cloud.yandex.net |
Все три — serverless, без управления инфраструктурой.
SLO и алерты
Минимальный набор алертов для чата:
| Алерт | Условие | Действие |
|---|---|---|
| HTTP 5xx всплеск | rate(http_errors[5m]) > 1% | дежурный → расследование |
| Latency деградация | histogram_quantile(0.99, http_duration) > 500ms | проверка YDB/функций |
| YDB throttling | rate(ydb_throttled[5m]) > 0 | нужны индексы или RU |
| WS reconnects всплеск | rate(ws_reconnects[5m]) > N | проверка WS-контейнера |
SLO для MVP: p99 latency отправки < 200 мс; p99 доставки (через WS) < 3 с; uptime > 99.5%.
💱 Компромиссы
- Метрики vs логи vs трейсы: разная степень агрегации и стоимость; метрики — дёшево и агрегированно, трейсы — детально, но дорого.
- Trace sample rate: 100% → полная картина, но дорого; низкий rate → слепые пятна при редких багах.
- Жёсткий vs мягкий SLO / Error Budget: жёсткий → много алертов, отвлекают; мягкий → пропускаем деградацию.
- Структурированные JSON-логи: индексируемы и парсятся — но больше объёма и затрат на хранение.
Практика
- Добавить
http.request.durationиusecase.durationв существующий код; открыть/metricsлокально. - Настроить Yandex Monitoring дашборд: RPS, p95/p99 latency, error rate, WS connections.
- Сэмулировать рост
ydb.throttledи проверить, что алерт срабатывает. - Отправить сообщение с
trace_idи найти его путь: HTTP → UseCase → YDB — в Yandex Tracing.
🎓 Финал курса: Вы построили бэкенд чата: Go + YDB + Serverless + WebSocket + YMQ + Terraform. Система работает, масштабируется и наблюдаема. Теперь — фронтенд.
🧩 Проверь себя
🎓 Финал курса
Вы построили бэкенд чата: Go + YDB + Serverless + WebSocket + YMQ + Terraform. Система работает, масштабируется и наблюдаема. Теперь — фронтенд.