0Pricing
Spring Boot 4 Complete Guide · 강의

컨텍스트 전파 및 스팬 계측

Micrometer Observation과 OpenTelemetry를 사용해 스레드와 서비스 간에 추적 컨텍스트를 전달합니다.

컨텍스트 전파 및 스팬 계측은(는) CoddyKit의 무료 Spring Boot 4 Complete Guide 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Spring Boot 4 Complete Guide 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Context Propagation Matters

In a distributed system, a single user request hops across threads, queues, and services. Distributed tracing stitches these hops together using a shared trace context (a trace ID plus the current span ID).

  • If the context is lost, your trace breaks into disconnected fragments.
  • Spring Boot 4 uses Micrometer Observation as the unified API and OpenTelemetry (or Brave) as the tracing bridge.
  • The challenge: keep the active span attached as work crosses thread and process boundaries.

This lesson shows how to instrument spans and propagate context both in-process (across threads) and across services (over HTTP/messaging headers).

The Observation API

Micrometer's ObservationRegistry is the entry point. An Observation abstracts both metrics and a tracing span, so one instrumentation point feeds both pillars.

  • Observation.createNotStarted(name, registry) builds it.
  • observe(Runnable) opens a scope, runs the code, and closes it — making the span the active one for the current thread.

While the scope is open, any nested instrumentation (HTTP client, JDBC, logging MDC) automatically becomes a child span.

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;

@Service
public class OrderService {

    private final ObservationRegistry registry;

    public OrderService(ObservationRegistry registry) {
        this.registry = registry;
    }

    public Order place(Long id) {
        return Observation.createNotStarted("order.place", registry)
            .lowCardinalityKeyValue("order.type", "standard")
            .observe(() -> doPlace(id));
    }

    private Order doPlace(Long id) {
        // child spans created here join the same trace
        return new Order(id);
    }
}

High vs Low Cardinality Tags

Observation key-values become both metric tags and span attributes. Choosing the right cardinality is a key decision:

  • lowCardinalityKeyValue: bounded values (HTTP method, status, region). Safe for metric dimensions.
  • highCardinalityKeyValue: unbounded values (user ID, order ID). Attached only to the span, never to metrics, to avoid a tag explosion.

Putting a user ID into a low-cardinality tag would create one time series per user and overwhelm your metrics backend.

Observation.createNotStarted("order.place", registry)
    .lowCardinalityKeyValue("order.type", order.getType())   // bounded -> metric + span
    .highCardinalityKeyValue("order.id", order.getId().toString()) // span only
    .observe(() -> process(order));

The Active Span and Thread-Locals

OpenTelemetry stores the current Context in a thread-local. Micrometer mirrors this with an Observation scope that is also thread-bound.

  • Inside observe(...), Span.current() returns the live span.
  • The moment you hand work to another thread (executor, CompletableFuture, reactive scheduler), the thread-local is empty there — the span does not follow automatically.

This is the root cause of broken traces in async code. The next scenes fix it.

import io.opentelemetry.api.trace.Span;

public void log() {
    Span span = Span.current();
    System.out.println("traceId=" + span.getSpanContext().getTraceId());
}

Propagating Across Threads with ContextSnapshot

Micrometer's context-propagation library captures all registered thread-local values into a ContextSnapshot and restores them in another thread.

  • Capture on the producing thread with ContextSnapshotFactory.captureAll().
  • Restore inside the worker by wrapping the task with snapshot.wrap(runnable).

This carries the OpenTelemetry context and Observation scope so the worker's spans join the original trace.

import io.micrometer.context.ContextSnapshot;
import io.micrometer.context.ContextSnapshotFactory;
import java.util.concurrent.ExecutorService;

public void runAsync(ExecutorService pool) {
    ContextSnapshot snapshot = ContextSnapshotFactory.builder().build().captureAll();
    pool.submit(snapshot.wrap(() -> {
        // active span here is the same as on the calling thread
        doWork();
    }));
}

Auto-Propagating Executors

Wrapping every task by hand is error-prone. Instead, decorate the executor once so every submitted task captures and restores context automatically.

  • ContextExecutorService.wrap(delegate, () -> ContextSnapshot.captureAll()) from context-propagation, or
  • Spring's ContextPropagatingTaskDecorator on a ThreadPoolTaskExecutor.

With Spring Boot 4, registering the task decorator means @Async methods keep the trace context with no per-call code.

import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Bean
public ThreadPoolTaskExecutor applicationTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
    executor.initialize();
    return executor;
}

Creating Custom Spans Manually

Sometimes you need a span around a specific block without a full Observation. Inject the Micrometer Tracer and manage the span explicitly.

  • tracer.nextSpan().name("...").start() creates a child of the current span.
  • Open a SpanInScope with tracer.withSpan(span) so nested code sees it as current.
  • Always end() in a finally block, and record errors with span.error(ex).
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;

public void doImport(Tracer tracer) {
    Span span = tracer.nextSpan().name("file.import").start();
    try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
        parseAndStore();
    } catch (RuntimeException ex) {
        span.error(ex);
        throw ex;
    } finally {
        span.end();
    }
}

Cross-Service Propagation over HTTP

Across processes, context travels in HTTP headers. The default format is W3C Trace Context: the traceparent header carries trace ID, parent span ID, and flags.

  • Auto-instrumented RestClient/WebClient beans inject the header outbound.
  • The receiving Spring Boot app extracts it and continues the same trace.

Use the framework-provided builders (not new RestClient.Builder()) so the tracing interceptor is attached.

@Service
public class InventoryClient {

    private final RestClient restClient;

    // inject the auto-configured, instrumented builder
    public InventoryClient(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("http://inventory").build();
    }

    public Stock check(String sku) {
        // traceparent header is injected automatically
        return restClient.get().uri("/stock/{sku}", sku)
            .retrieve().body(Stock.class);
    }
}

The W3C traceparent Header

Understanding the wire format helps when debugging broken traces. A traceparent looks like:

  • 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
  • 00 = version, then 16-byte trace-id, 8-byte parent-id, and 01 = sampled flag.

If a downstream span shows a new trace ID, the header was dropped — often because a non-instrumented client or a manual header copy stripped it. A companion tracestate header carries vendor-specific data.

Propagating Through Messaging

For Kafka or RabbitMQ, context rides in message headers. With instrumented Spring Messaging, the producer injects traceparent into the record headers and the consumer extracts it, creating a span linked to the producer.

  • Producer span kind = PRODUCER; consumer span kind = CONSUMER.
  • Across a queue the relationship is often modeled as a span link rather than a strict parent/child, since many consumers may process batches.

Keep using the auto-configured KafkaTemplate/listeners so propagation works without manual header handling.

@Component
public class OrderEvents {

    private final KafkaTemplate<String, OrderEvent> template;

    public OrderEvents(KafkaTemplate<String, OrderEvent> template) {
        this.template = template;
    }

    public void publish(OrderEvent event) {
        // traceparent header added to the Kafka record automatically
        template.send("orders", event.id(), event);
    }

    @KafkaListener(topics = "orders")
    public void consume(OrderEvent event) {
        // this span links back to the producing trace
        process(event);
    }
}

Correlating Logs and Baggage

Two finishing touches make traces usable:

  • Log correlation: Micrometer pushes traceId and spanId into the SLF4J MDC, so each log line carries the IDs. Spring Boot's default log pattern prints them.
  • Baggage: arbitrary key-values that propagate across services alongside trace context. Configure management.tracing.baggage.correlation.fields and remote-fields to forward and expose them in MDC.

Use baggage sparingly (e.g. a userId or tenantId) — every value is copied onto every downstream hop.

management:
  tracing:
    sampling:
      probability: 1.0
    baggage:
      remote-fields: tenantId
      correlation:
        fields: tenantId

Quick Check

Test your understanding of in-process context propagation.

Recap

You learned how trace context flows through a Spring Boot 4 system:

  • Micrometer Observation unifies metrics and spans; observe(...) opens a thread-bound scope.
  • Choose low-cardinality tags for metrics and high-cardinality attributes for spans only.
  • Context lives in a thread-local, so async handoffs need ContextSnapshot or a ContextPropagatingTaskDecorator to keep the trace intact.
  • Create explicit spans with the Tracer when an Observation is overkill, always ending them in a finally block.
  • Across services, the W3C traceparent header (HTTP) or message headers (Kafka/Rabbit) carry the trace; use auto-instrumented clients.
  • MDC correlation ties logs to traces, and baggage forwards small key-values downstream.

자주 묻는 질문

“컨텍스트 전파 및 스팬 계측” 강의는 무료인가요?

네 — “컨텍스트 전파 및 스팬 계측” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Spring Boot 4 Complete Guide 강의 전체를 잠금 해제할 수 있습니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.

“컨텍스트 전파 및 스팬 계측”에서 뭘 배우나요?

Micrometer Observation과 OpenTelemetry를 사용해 스레드와 서비스 간에 추적 컨텍스트를 전달합니다. 브라우저에서 직접 실행하는 실습 코드로 Spring Boot 4 Complete Guide을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Spring Boot 4 Complete Guide을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Spring Boot 4 Complete Guide은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“컨텍스트 전파 및 스팬 계측” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Spring Boot 4 Complete Guide 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Spring Boot 4 Complete Guide 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 컨텍스트 전파 및 스팬 계측
  2. 회로 차단기 및 격벽 격리
  3. 호출률 제한, 재시도 및 시간 제한기
  4. 로그, 메트릭 및 추적 상관관계 분석
← Spring Boot 4 Complete Guide(으)로 돌아가기