Опирается на правила:
R-OBS-TRC-1…R-OBS-TRC-6иR-OBS-TRC-X1…R-OBS-TRC-X4из Observability Style Guide → раздел 3. Tracing.
Важно знать
- OpenTelemetry автоинструментация через
opentelemetry-spring-boot-starter— auto-spans для HTTP server/client, JDBC, Kafka, Spring Cache.traceparentW3C 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 с propagationtraceparent. - 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 prod | R-OBS-TRC-X1 | 1-10% + tail-based для errors |
| PII в span attributes | R-OBS-TRC-X2 | только internal IDs |
Manual span без try-finally | R-OBS-TRC-X3 | try-with-resources + finally end() |
@Async без TaskDecorator | R-OBS-TRC-X4 | TaskDecorator передаёт OTel context |
| Plain Zipkin / Brave вместо OTel | R-OBS-TRC-1 | OpenTelemetry стандарт |
Trace без traceparent propagation | R-OBS-TRC-2 | OTel auto-instruments HTTP client |
traceId руками в MDC | R-OBS-TRC-6 | opentelemetry-logback-mdc-1.0 |
Куда дальше
- Observability → раздел 3. Tracing — нормативные формулировки.
- Logging —
traceIdв MDC, связка с trace. - Metrics — почему high-cardinality не для метрик.
- Context propagation (MDC) —
TaskDecoratorдля @Async. - Configuration —
otel.*settings вapplication.yml. - Kafka → traceparent в headers — OTel propagation для Kafka.
- REST API → headers — W3C Trace Context на границе.