//! Verifies the per-request root-span middleware (#139 / map #135). //! //! Two things are pinned here: //! //! 1. **Traceparent extraction.** An incoming W3C `traceparent` header is //! picked up by the `tracing-actix-web` middleware (via the global text-map //! propagator installed in `otel::init_tracing`), so the request's root span //! is parented onto the caller's span — the whole request shares the //! caller's `trace_id`. //! //! 2. **No duplicate request span.** A handler that parents its operation span //! through `otel::extract_context_from_request` nests it *under* the //! middleware's root span (one tree) rather than re-parsing the header into //! a second, sibling request span. //! //! One process-global subscriber + propagator per test binary (one-shot), so //! this file holds a single test. use actix_web::{web, App, HttpRequest, HttpResponse}; use opentelemetry::trace::{Tracer, TracerProvider}; use opentelemetry_sdk::propagation::TraceContextPropagator; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SpanData}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; const TRACE_ID: &str = "0af7651916cd43dd8448eb211c80319c"; const REMOTE_PARENT: &str = "b7ad6b7169203331"; /// Mirrors the app's handler pattern: parent the operation span via /// `extract_context_from_request` (which sources the middleware's root span). async fn operation_handler(req: HttpRequest) -> HttpResponse { let cx = image_api::otel::extract_context_from_request(&req); let tracer = image_api::otel::global_tracer(); let _span = tracer.start_with_context("test.operation", &cx); HttpResponse::Ok().finish() } fn find<'a>(spans: &'a [SpanData], name: &str) -> Option<&'a SpanData> { spans.iter().find(|s| s.name.as_ref() == name) } #[actix_web::test] async fn traceparent_header_nests_request_spans() { // In-memory exporter + provider; install as the global provider so the // handler's `global_tracer()` and the middleware's layer share it. let exporter = InMemorySpanExporter::default(); let provider = SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); opentelemetry::global::set_tracer_provider(provider.clone()); // The middleware's `set_otel_parent` extracts via the global propagator. opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); // Subscriber with the OTel layer (mirrors `otel::init_tracing`). let tracer = provider.tracer("traceparent-test"); Registry::default() .with(tracing_opentelemetry::OpenTelemetryLayer::new(tracer)) .try_init() .expect("global subscriber not yet set"); let app = App::new() .wrap(tracing_actix_web::TracingLogger::default()) .service(web::resource("/op").route(web::get().to(operation_handler))); let srv = actix_web::test::init_service(app).await; let req = actix_web::test::TestRequest::get() .uri("/op") .insert_header(( "traceparent", format!("00-{TRACE_ID}-{REMOTE_PARENT}-01"), )) .to_request(); let _res = actix_web::test::call_and_read_body(&srv, req).await; let spans = exporter.get_finished_spans().expect("in-memory exporter"); // The middleware's root span is the one whose parent is remote (the // caller's span from the traceparent header). let root = spans .iter() .find(|s| s.parent_span_is_remote) .expect("a root span parented on the remote traceparent span"); // The handler's operation span. let op = find(&spans, "test.operation").expect("the handler's operation span"); // (1) The whole request shares the caller's trace_id. assert_eq!( root.span_context.trace_id().to_string(), TRACE_ID, "root span must carry the incoming traceparent's trace_id" ); assert_eq!( op.span_context.trace_id().to_string(), TRACE_ID, "operation span must be in the same trace" ); // (1) The root span is parented on the caller's (remote) span. assert_eq!( root.parent_span_id.to_string(), REMOTE_PARENT, "root span parent must be the traceparent's parent span id" ); // (2) The operation span nests under the root span (child, not sibling). assert_eq!( op.parent_span_id.to_string(), root.span_context.span_id().to_string(), "operation span must be a child of the middleware root span, not a sibling" ); }