Перейти к содержанию

Как читать трейсы в Tempo

Шпаргалка по distributed tracing: где смотреть, как искать trace, как коррелировать со слогами и метриками, как добавлять собственные spans. Полный reference по инструментации — в ../conventions/observability.md.

Содержание

Где трейсы

Tempo — backend для distributed traces. UI — Grafana → Explore → datasource Tempo.

Трейсы собираются OpenTelemetry SDK в каждом сервисе, экспортируются через OTLP в collector → Tempo. Настройка и context-propagation — в ../conventions/observability.md.

Найти trace по trace_id

Самый частый сценарий. trace_id уже есть:

  • В JSON-записи лога (trace_id — обязательное поле).
  • В error-ответе сервиса (если добавляется в header Trace-Id).
  • В report от пользователя / поддержки.

В Tempo → Search → вкладка TraceQL → введи trace_id:

{trace_id="4bf92f3577b34da6a3ce929d0e0e4736"}

Или прямой URL: /explore?left=...&query=...trace_id="...".

Поиск без trace_id

TraceQL позволяет искать по атрибутам spans:

По сервису

{resource.service.name="review"}

По длительности

{resource.service.name="review" && duration > 1s}

По тегам

{http.status_code=500}
{http.route="/v1/reviews" && duration > 500ms}
{db.system="postgresql" && duration > 200ms}
{messaging.system="kafka" && messaging.destination="kazmaps.review.review.created"}

По ошибке

{status=error}
{resource.service.name="review" && status=error}

По user_id

{user.id="42"}

Структура trace

  • Trace — полная операция, инициированная одним внешним запросом (HTTP call от клиента) или событием.
  • Span — один шаг операции: HTTP handler, DB-запрос, Kafka publish, внешний HTTP call.
  • Parent-child — spans образуют дерево. Корневой span — точка входа (обычно HTTP handler); дочерние — то, что происходит внутри.
  • Attributes — структурные поля span'а (http.method, db.statement, messaging.destination).
  • Events — точечные записи внутри span'а (error.message, cache.hit).

Анализ slow request

Стандартный workflow:

  1. Нашёл медленный trace — открой waterfall.
  2. Каждый span имеет duration. Ищи длинный span с малым self-time (время без дочерних) — это работа самого span'а (не ожидание дочерних).
  3. Critical path — последовательность самых долгих non-parallel span'ов от корня до листа. Обычно оптимизация здесь даёт максимум.
  4. Parallel spans с разной длительностью — slow-one тормозит весь родительский. Если parallel — это errgroup.Wait, медленный ветвь удерживает всех.

Типичные bottleneck'и:

  • DB span с duration > 100ms. Смотри db.statement в attributes — отсутствует индекс, or n+1.
  • External HTTP с duration > 500ms. Проверь timeout'ы, circuit breaker, кэш. См. caching.md.
  • Kafka publish с duration > 50ms. Проверь, публикуешь ли ты не через outbox — прямой publish из request-path это bug. См. ../patterns/outbox.md.
  • Gap между span'ами. Если visual gap в waterfall — код между span'ами делает работу без инструментации. Добавь span.

Correlation с logs

Каждый trace ↔ логи связаны через trace_id.

  • Из Tempo в Loki. В span → кнопка Logs for this span → автоматически открывается Loki с фильтром {service="<span.service>"} | json | trace_id="<trace>".
  • Из Loki в Tempo. В раскрытой JSON-записи кнопка View trace открывает trace в Tempo.

Rule of thumb: начинай с trace → спускайся в логи за деталями. Trace даёт временную структуру и где искать, логи — конкретные значения переменных в момент проблемы.

Correlation с metrics

Prometheus exemplars связывают график с конкретными trace'ами.

Если настроены exemplars (HistogramOpts с NativeHistogramBucketFactor и OTel exemplar hook в коде):

  1. Grafana dashboard → график histogram_quantile(0.99, http_request_duration_seconds) → spike.
  2. Клик по точке spike'а → появляется exemplar — конкретный request в этот момент.
  3. Кнопка рядом — прыжок в Tempo.

Так spike на графике превращается в кликабельный trace за два клика.

Типовые сценарии

Slow endpoint

  1. Grafana dashboard → latency panel → p99 вырос.
  2. Клик на exemplar → trace.
  3. Waterfall → самый длинный span.
  4. Если span — DB: смотри db.statement, добавь индекс / перепиши запрос.
  5. Если external HTTP: проверь downstream сервис, timeout, кэш.
  6. Прыжок в Loki по trace_id за контекстом (args, user_id).

Distributed flow (Kafka)

Publisher кладёт traceparent в Kafka-envelope (см. ../conventions/events.md), consumer'ы в других сервисах подхватывают контекст и продолжают trace.

Result: один trace покрывает путь HTTP → service A → outbox → forwarder → Kafka → service B → DB. Смотришь всю цепочку в одном окне.

Debug-применение: сообщение не дошло до DB в сервисе B.

  1. Найди trace по HTTP-request в сервисе A.
  2. Посмотри, дошёл ли span kafka.publish (forwarder).
  3. Есть ли span сервиса B? Если нет — сообщение ещё в Kafka, проблема в consumer.
  4. Если есть — смотри ошибку в span'е B, смотри status=error, прыжок в Loki сервиса B.

Race condition

Два trace с похожим shape за близкое время, но один проходит, другой падает. Ставь рядом в Grafana Compare traces (или открой в параллельных вкладках). Сравни timing span'ов — часто видно, что во втором trace какой-то span сдвинут на миллисекунды и попал в гонку.

Кастомные spans

Автоматическая инструментация (otelhttp, otelpgx, otelsarama через Watermill) покрывает большую часть. Добавляй собственные span'ы только для значимых бизнес-шагов, которых авто-инструментация не видит.

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

var tracer = otel.Tracer("kazmaps.review.service")

func (s *Service) ProcessReview(ctx context.Context, id int64) error {
    ctx, span := tracer.Start(ctx, "ProcessReview")
    defer span.End()
    span.SetAttributes(attribute.Int64("review.id", id))

    if err := s.validate(ctx, id); err != nil {
        span.RecordError(err)
        return err
    }
    return nil
}
  • Имя tracer'а — уникально на пакет: "kazmaps.<service>.<package>".
  • Имя span'а — CamelCase, короткое, без аргументов (ProcessReview, не ProcessReview(id=42)). Аргументы — в attributes.
  • span.RecordError(err) — стандартный способ пометить span failed.

Storage tradeoffs

Tempo оптимизирован под поиск по trace_id, не под полнотекстовый поиск:

  • Поиск по attributes работает, но тяжелее, чем в Loki.
  • Retention ограничена (обычно 7–14 дней). Для long-term — экспорт в отдельное хранилище.
  • Sampling в collector'е: не все trace'ы хранятся, часть сэмплируется. При расследовании редкого инцидента — учитывай, что конкретный trace мог быть отброшен до Tempo.

Anti-patterns

  • Span на каждую строчку кода. Тысячи span'ов на trace, Tempo режет, UI тормозит. Инструментируй только значимые границы (handler, service-method, external call).
  • PII в attributes. user.email, request.token, password — запрещено. Attributes тоже подчиняются правилам ../conventions/logging.md и ../conventions/security.md.
  • Trace для health-checks. /healthz, /readyz, /metrics — шум. Отключай в middleware (sampling 0 для этих path'ов).
  • Ручное управление trace_id. Генерировать trace_id самому и пробрасывать вручную — ломает W3C-propagation. Используй стандартные propagator'ы, они сами разберутся.
  • Span вместо лога. Не кладите в span.AddEvent то, что должно быть в slog.Info. Events в span — для точечных временных вех, не для подробных dump'ов.

Связанные разделы