1. Correlation ID
Сквозная связность логов, метрик и трейсов
«Главное правило отладки: обнаружив ошибку, исправьте ее, а затем найдите и уничтожьте все остальные ошибки того же рода».
— Брайан Керниган
В предыдущих разделах мы подробно изучили все три столпа Observability по отдельности: метрики (сигнал тревоги), логи (бортовой журнал событий) и распределенную трассировку (виртуальный стек вызовов). Но если эти три инструмента существуют в изоляции друг от друга, они напоминают трех свидетелей одного инцидента, говорящих на трех совершенно разных языках.
Инженер видит всплеск 500-х ошибок в Prometheus, переключается в Kibana или Loki, пытается найти нужные записи среди сотен тысяч строк логов, а затем идет в Jaeger и пытается наугад угадать проблемный трейс. Это мучительно медленно и превращает дежурство в кошмар.
Настоящая наблюдаемость начинается тогда, когда все три столпа намертво сшиты воедино. Главным связующим звеном и универсальным первичным ключом этой интеграции выступает Correlation ID (в современном облачном мире — Trace ID).
Связующий элемент Observability
В распределенных системах, где один запрос порождает каскад вызовов между микросервисами, базами данных и очередями, Correlation ID (часто совпадающий с Trace ID) выступает в роли первичного ключа, позволяющего собрать пазл из разрозненных данных.
Без него распределенная система подобна городу без названий улиц и номеров домов: найти конкретную посылку среди миллионов отправлений становится статистически невозможно.
Trace ID как новый стандарт Correlation ID
Исторически Correlation ID и Trace ID были разными понятиями:
- Correlation ID: Идентификатор бизнес-операции, передаваемый через HTTP-заголовки (например,
X-Request-ID), чтобы связать логи. - Trace ID: Идентификатор технического трейса, понятный системам APM (Jaeger, Zipkin).
С появлением стандарта OpenTelemetry и W3C Trace Context, эти понятия слились. Теперь Trace ID является основным Correlation ID. Это уникальный 128-битный идентификатор, который генерируется в самом начале пути запроса и путешествует с ним до самого конца.
Архитектура связывания данных
В зрелой системе Observability поток телеметрии образует замкнутый треугольник взаимных ссылок:
flowchart TD
A["Client Request"] -->|"1 - Generate Trace ID"| B["API Gateway"]
B -->|"2 - Propagate ID"| C["Service A"]
C -->|"3 - Propagate ID"| D["Service B"]
subgraph "Data Flow"
C -->|"Log"| E["Log Aggregator<br>Loki"]
C -->|"Metric"| F["Prometheus<br>Exemplar"]
C -->|"Trace"| G["Tracing Backend<br>Tempo"]
end
E -->|"Trace ID Link"| H["Grafana Dashboard"]
F -->|"Trace ID Link"| H
G -->|"Trace ID Link"| H
style A fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#fff
style B fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#fff
style H fill:#065f46,stroke:#10b981,stroke-width:2px,color:#fff
style Data Flow fill:#0f172a,stroke:#475569,stroke-width:1px,color:#fff
Связь компонентов:
- Logs: Каждая запись лога содержит поле
trace_id. - Metrics: Метрики могут содержать Exemplars (ссылки на конкретные трейсы).
- Traces: Сам Trace ID является первичным ключом.
Это позволяет в Grafana совершать бесшовные "прыжки" (drill-down):
- Увидели всплеск ошибок на графике (Metrics)?
- Кликнули на точку -> перешли к конкретному трейсу (Tracing).
- В трейсе увидели span с ошибкой -> перешли к логам по этому Trace ID (Logs).
Реализация в Go (OpenTelemetry + slog)
В современных версиях Go (1.21+) связка context и логгера позволяет автоматизировать этот процесс.
Шаг 1: Middleware для извлечения/генерации ID
Мы используем промежуточное ПО, чтобы убедиться, что контекст трассировки всегда доступен.
package main
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Извлекаем контекст из заголовков (если пришел от другого сервиса)
// или создаем новый, если это входной запрос.
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// 2. Запускаем корневой спан (это автоматически создаст Trace ID, если его нет)
tr := otel.Tracer("my-service")
ctx, span := tr.Start(ctx, r.Method+" "+r.URL.Path)
defer span.End()
// 3. Пробрасываем контекст дальше
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Шаг 2: Автоматическая запись Trace ID в логи
Нам нужен хендлер для slog, который умеет извлекать Trace ID из контекста. Это делается через кастомный обработчик или middleware для логгера.
package main
import (
"context"
"log/slog"
"go.opentelemetry.io/otel/trace"
)
// TraceLoggerMiddleware оборачивает slog, добавляя trace_id из ctx
func TraceLoggerMiddleware(logger *slog.Logger) *slog.Logger {
return slog.New(slog.HandlerFunc(func(ctx context.Context, r slog.Record) error {
// Извлекаем SpanContext из контекста OpenTelemetry
spanCtx := trace.SpanContextFromContext(ctx)
// Если Trace ID валиден, добавляем его в лог
if spanCtx.IsValid() {
r.AddAttrs(slog.String("trace_id", spanCtx.TraceID().String()))
}
return logger.Handler().Handle(ctx, r)
}))
}
Теперь любой вызов logger.InfoContext(ctx, ...) автоматически добавит trace_id в вывод, если в переданном ctx есть активная трассировка.
Связь с метриками: Exemplars
Exemplars — это "изюминка" Prometheus, о которой часто забывают. Это возможность прикрепить Trace ID к метрике в момент её записи.
В стандарте OpenMetrics замер гистограммы может сопровождаться комментарием:
http_request_duration_seconds_bucket{le="0.5"} 1245 # {trace_id="4bf92f3577b34da6a3ce929d0e0e4736"} 0.421 1695040000.123
В Go клиенте Prometheus это делается автоматически, если вы передаете контекст с трассировкой при записи метрики.
// При использовании Histogram
httpDuration.WithLabelValues("/api").ObserveWithExemplar(duration, map[string]string{
"traceID": trace.SpanContextFromContext(ctx).TraceID().String(),
})
В интерфейсе Prometheus/Grafana вы увидите, что рядом с точкой на графике есть ссылка на Trace ID. Это превращает график метрик в "кликабельную карту" инцидентов.
Mechanical Sympathy: Стоимость связывания
- Извлечение ID:
trace.SpanContextFromContext(ctx)— это очень дешевая операция (несколько наносекунд), так как это просто приведение типа интерфейса. - Логирование: Добавление атрибута в
slogстоит недорого (аллокация строки). Критически важно использовать JSON Handler, чтобы полеtrace_idбыло отдельным ключом, а не частью сообщения. Это позволяет Loki проиндексировать его и искать мгновенно.
trace.Link позволяет создать новый корневой трейс для асинхронного фонового процесса, сохранив слабую связь со спаном-инициатором, не ломая родительский жизненный цикл.
Итог
- Correlation ID = Trace ID в современном мире OpenTelemetry.
- Автоматизируйте инъекцию Trace ID в логи через Middleware. Не передавайте его вручную в параметрах функций.
- Используйте Exemplars в Prometheus, чтобы связать метрики и трейсы.
- Эта связка превращает Observability из набора разрозненных инструментов в единый механизм расследования инцидентов.
В следующей статье мы поговорим о том, как использовать все эти инструменты для реальной отладки проблем: 2. Debugging через observability.