trace_db_call no longer takes an OpenTelemetry Context: it opens a db.<query_type>.<operation> span (same name via otel.name, same db.query_type / db.operation attrs) on the caller's ambient tracing span and runs the DAO body inside it. The cx / ctx / context parameter is gone from every DAO trait method, impl and call site, and from the helpers and background drains that only threaded it. The remaining request-handler spans (files, tags, faces, image, video, favorites, duplicates, memories) move to #[instrument]; the Context::current_with_span / with_remote_span_context plumbing is gone, as is extract_context_from_request (the tracing-actix-web root span covers it). otel::set_span_attributes replaces BoxedSpan::set_attributes. Background-job raw spans (thumbnails, video actors) are left for #142. tests/otel_db_spans.rs pins name, attributes, status and nesting (across an instrumented spawn); otel_traceparent now uses an #[instrument] handler. CLAUDE.md documents the tracing conventions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.3 KiB
Rust
106 lines
4.3 KiB
Rust
//! 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's `#[instrument]`ed operation
|
|
//! span nests *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::{App, HttpResponse, web};
|
|
use opentelemetry::trace::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: an `#[instrument]`ed operation span,
|
|
/// parented ambiently on the middleware's root span.
|
|
#[tracing::instrument(name = "test.operation", skip_all)]
|
|
async fn operation_handler() -> HttpResponse {
|
|
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"
|
|
);
|
|
}
|