Tracing: per-request root span via tracing-actix-web middleware (#139)
Add a conventional per-request OTel server span using the standard tracing-actix-web middleware (TracingLogger) as the outermost middleware, so the whole request lives under one root span and an incoming W3C traceparent is extracted onto it via the global text-map propagator (installed in #138). extract_context_from_request now sources the operation span's parent from that root span (tracing::Span::current().context()) instead of re-parsing the header, so per-handler operation spans nest under the root span as one trace tree rather than duplicating it as a sibling. Header parsing remains as a fallback for handlers invoked without the middleware (tests). The literal per-handler span boilerplate is intentionally NOT removed here; it becomes #[instrument] in the LLM (#140) and DAO (#141) migrations, where the &Context threading disappears entirely. tests/otel_traceparent.rs pins the behavior end-to-end: an incoming traceparent's trace_id propagates to the request, the root span is parented on the caller's (remote) span, and the operation span nests under the root span rather than alongside it.
This commit is contained in:
1 parent
e93f6b6d69
commit
7a59afc62b
5 files changed
+148
No files matched your search
@@ -0,0 +1,111 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user