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
  

Связь компонентов:

  1. Logs: Каждая запись лога содержит поле trace_id.
  2. Metrics: Метрики могут содержать Exemplars (ссылки на конкретные трейсы).
  3. Traces: Сам Trace ID является первичным ключом.

Это позволяет в Grafana совершать бесшовные "прыжки" (drill-down):

  • Увидели всплеск ошибок на графике (Metrics)?
  • Кликнули на точку -> перешли к конкретному трейсу (Tracing).
  • В трейсе увидели span с ошибкой -> перешли к логам по этому Trace ID (Logs).

Реализация в Go (OpenTelemetry + slog)

В современных версиях Go (1.21+) связка context и логгера позволяет автоматизировать этот процесс.

Шаг 1: Middleware для извлечения/генерации ID

Мы используем промежуточное ПО, чтобы убедиться, что контекст трассировки всегда доступен.

Go
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 для логгера.

Go
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 замер гистограммы может сопровождаться комментарием:

Text
http_request_duration_seconds_bucket{le="0.5"} 1245 # {trace_id="4bf92f3577b34da6a3ce929d0e0e4736"} 0.421 1695040000.123

В Go клиенте Prometheus это делается автоматически, если вы передаете контекст с трассировкой при записи метрики.

Go
// При использовании Histogram
httpDuration.WithLabelValues("/api").ObserveWithExemplar(duration, map[string]string{
    "traceID": trace.SpanContextFromContext(ctx).TraceID().String(),
})

В интерфейсе Prometheus/Grafana вы увидите, что рядом с точкой на графике есть ссылка на Trace ID. Это превращает график метрик в "кликабельную карту" инцидентов.


Mechanical Sympathy: Стоимость связывания

  1. Извлечение ID: trace.SpanContextFromContext(ctx) — это очень дешевая операция (несколько наносекунд), так как это просто приведение типа интерфейса.
  2. Логирование: Добавление атрибута в slog стоит недорого (аллокация строки). Критически важно использовать JSON Handler, чтобы поле trace_id было отдельным ключом, а не частью сообщения. Это позволяет Loki проиндексировать его и искать мгновенно.

trace.Link позволяет создать новый корневой трейс для асинхронного фонового процесса, сохранив слабую связь со спаном-инициатором, не ломая родительский жизненный цикл.


Итог

  1. Correlation ID = Trace ID в современном мире OpenTelemetry.
  2. Автоматизируйте инъекцию Trace ID в логи через Middleware. Не передавайте его вручную в параметрах функций.
  3. Используйте Exemplars в Prometheus, чтобы связать метрики и трейсы.
  4. Эта связка превращает Observability из набора разрозненных инструментов в единый механизм расследования инцидентов.

В следующей статье мы поговорим о том, как использовать все эти инструменты для реальной отладки проблем: 2. Debugging через observability.