Опирается на правила: R-OBS-TRC-1R-OBS-TRC-6 и R-OBS-TRC-X1R-OBS-TRC-X4 из Observability Style Guide → раздел 3. Tracing.

Важно знать

  • OpenTelemetry автоинструментация через opentelemetry-spring-boot-starter — auto-spans для HTTP server/client, JDBC, Kafka, Spring Cache.
  • traceparent W3C Trace Context propagation между сервисами — автоматически (incoming HTTP headers → outgoing).
  • Manual spans для значимых use case handlers через Tracer и try-with-resources / try-finally.
  • Span attributes — business context (orderId, paymentMethod, circuit_breaker.state), не PII.
  • Sampling 1-10% в проде + 100% для error-traces через tail-based sampling.
  • traceId/spanId в MDC автоматически — связка лог-запись ↔ distributed trace.
  • Trace разрывается на @Async без TaskDecorator — пропагация через runnable wrapper обязательна.

Tracing — третья нога observability рядом с logs и metrics. Когда метрики говорят «p95 вырос», а логи — «много ошибок в payment-service», trace показывает конкретный запрос от POST /orders через 5 сервисов: где провели 200ms в DB, где 800ms в HTTP-вызове, где упало. UCP опирается на OpenTelemetry — он отраслевой стандарт, заменивший Zipkin/Jaeger client'ов.

Автоинструментация через стартер

R-OBS-TRC-1: подключение через opentelemetry-spring-boot-starter (Spring Boot 3+).

implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")
implementation("io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0")

Auto-spans создаются для:

  • HTTP server requests (Spring MVC, WebFlux) — incoming span с атрибутами http.method, http.url, http.status_code.
  • HTTP client (RestClient, RestTemplate, WebClient) — outgoing span с propagation traceparent.
  • JDBC — span на каждый SQL-запрос с db.statement (без plain SQL значений PII).
  • Kafka producer/consumer — span на send/receive с messaging.destination, messaging.kafka.partition.
  • Spring Cache — span на cache hit/miss.

Без custom-кода уже получаем полную картину «request → JDBC → Kafka → HTTP-выход → DB».

Traceparent propagation

R-OBS-TRC-2: W3C Trace Context (R-HDR-4) — стандартный формат заголовка.

traceparent: 00-5e92c8a3b1f4d2e6a7c8e9f0a1b2c3d4-1f2e3d4c5b6a7980-01
              │     │                                  │                 │
              │     trace-id (16 bytes)                span-id (8 bytes) flags
              version

OTel автоматически:

  • Извлекает traceparent из incoming HTTP headers, создаёт child span.
  • Пропагирует в outgoing HTTP / Kafka headers исходящих запросов.
  • Связывает spans в единый distributed trace в collector-е.

Это работает между сервисами без явного кода — каждый сервис на OTel автоматически становится участником trace-цепочки.

Manual spans для use case

R-OBS-TRC-3: для значимых business-операций добавляем явный span с business-атрибутами.

@UseCase
@RequiredArgsConstructor
public class ConfirmOrderHandler implements UseCaseHandler<ConfirmOrderCommand, Order> {

    private final OrderRepository orderRepository;
    private final Tracer tracer;

    @Override
    @Transactional
    public Order handle(ConfirmOrderCommand command) {
        var span = tracer.spanBuilder("confirmOrder")
            .setAttribute("order.id", command.orderId())
            .setAttribute("user.id", command.userId())
            .startSpan();
        try (var scope = span.makeCurrent()) {
            var order = orderRepository.findById(command.orderId(), SelectMode.FOR_UPDATE)
                .orElseThrow();
            order.confirm();
            span.setAttribute("order.status", order.status().name());
            return orderRepository.save(order);
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, e.getMessage());
            throw e;
        } finally {
            span.end();
        }
    }
}

Альтернатива — аннотация @WithSpan("confirmOrder") из OTel (требует javaagent или extension). Менее гибко, но короче.

try-with-resources на Scope + try-finally на span.end() — обязательны. Если span не закроется, collector не получит данные, trace висит «в процессе».

Span attributes — business, не PII

R-OBS-TRC-4: что класть в attributes.

ОКНе ОК
order.id, customer.id (internal IDs)customer.email, customer.phone
order.status, payment.method (enum)card.number, iban
external.system="sber"external.api_key
circuit_breaker.state="open"request.body целиком

Tracing-данные часто хранятся в отдельном storage (Tempo, Jaeger) с другим режимом доступа и retention. PII там — нарушение compliance. Внутренние ID — норма.

Sampling

R-OBS-TRC-5: 1-10% в проде по умолчанию.

otel:
  traces:
    sampler: parentbased_traceidratio
    sampler.arg: 0.1   # 10%
  exporter:
    otlp:
      endpoint: http://otel-collector:4317

parentbased_traceidratio — если incoming request уже имеет traceparent с флагом sampled, мы тоже sampling-ом включаемся (head-based). Иначе — 10% случайных.

Tail-based sampling (на стороне collector-а) добавляет «100% если в trace есть error». Это даёт полный набор error-traces без раздувания storage normal-trace-ами.

Для low-traffic сервисов (<10 req/s) sampling можно держать 100% — storage не перегружается.

TraceId в логах через MDC

R-OBS-TRC-6: opentelemetry-logback-mdc-1.0 автоматически кладёт traceId, spanId в MDC. Encoder включает их в JSON.

{
  "@timestamp": "2026-05-25T22:30:00Z",
  "level": "ERROR",
  "message": "Failed to charge payment: orderId=12345",
  "mdc": {
    "traceId": "5e92c8a3b1f4d2e6a7c8e9f0a1b2c3d4",
    "spanId": "1f2e3d4c5b6a7980"
  },
  "stack_trace": "..."
}

В Loki / ELK кликаем на traceId → переходим в Tempo / Jaeger → видим весь distributed trace с этим span-ом. Связка «лог-запись ↔ trace» — главный профит OTel + structured logs.

Что запрещено

Sampling 100% в проде

R-OBS-TRC-X1: для среднего/высоконагруженного сервиса 100% sampling → terabytes traces / день, переполненный Tempo, счёт за storage растёт линейно. 1-10% даёт достаточный набор «нормальных» traces + tail-based ловит errors.

PII в span attributes

R-OBS-TRC-X2: customer.email, card.number, passport — никогда. См. Logging для PII-гигиены, тот же принцип для tracing.

Manual span без try-finally

R-OBS-TRC-X3: если span создан, но span.end() не вызван — span утекает в collector в IN_PROGRESS, искажает aggregations.

// ПЛОХО — span не закроется при exception
var span = tracer.spanBuilder("foo").startSpan();
doWork();
span.end();

// ХОРОШО — try-finally
var span = tracer.spanBuilder("foo").startSpan();
try (var scope = span.makeCurrent()) {
    doWork();
} finally {
    span.end();
}

Trace разрывается на @Async

R-OBS-TRC-X4: CompletableFuture.runAsync(...) / @Async отрабатывают на отдельном thread без OTel context. Trace ломается на границе.

Решение — TaskDecorator, прокидывающий context. Подробнее — Context propagation.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
Sampling 100% в high-traffic prodR-OBS-TRC-X11-10% + tail-based для errors
PII в span attributesR-OBS-TRC-X2только internal IDs
Manual span без try-finallyR-OBS-TRC-X3try-with-resources + finally end()
@Async без TaskDecoratorR-OBS-TRC-X4TaskDecorator передаёт OTel context
Plain Zipkin / Brave вместо OTelR-OBS-TRC-1OpenTelemetry стандарт
Trace без traceparent propagationR-OBS-TRC-2OTel auto-instruments HTTP client
traceId руками в MDCR-OBS-TRC-6opentelemetry-logback-mdc-1.0

Куда дальше