//! Verifies the DAO-layer tracing migration (#141 / map #135). //! //! Every DAO method funnels through `otel::trace_db_call`, which no longer //! takes an OpenTelemetry `Context`: the `db..` span //! parents on the caller's ambient `tracing` span. Pinned here: //! //! 1. **Name + attributes.** The exported span keeps the pre-migration name //! (`db.query.get_tags`) and `db.query_type` / `db.operation` attributes, //! plus anything the closure records on the span it's handed. //! 2. **Nesting.** It is a child of the ambient span — including inside a //! `tokio::spawn` that carries the parent via `.instrument`. //! 3. **Status.** `Ok` on success, `Error` with the message on failure. //! //! One process-global subscriber per test binary, so this file holds a //! single test. use opentelemetry::trace::{Status, TracerProvider}; use opentelemetry::Value; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SpanData}; use tracing::Instrument; use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; fn attr(span: &SpanData, key: &str) -> Option { span.attributes .iter() .find(|kv| kv.key.as_str() == key) .map(|kv| kv.value.clone()) } fn find<'a>(spans: &'a [SpanData], name: &str) -> &'a SpanData { spans .iter() .find(|s| s.name == name) .unwrap_or_else(|| panic!("span {name} not exported")) } #[test] fn db_spans_nest_under_ambient_span() { let exporter = InMemorySpanExporter::default(); let provider = SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); Registry::default() .with(tracing_opentelemetry::OpenTelemetryLayer::new( provider.tracer("db-span-test"), )) .try_init() .expect("global subscriber not yet set"); let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build() .unwrap(); rt.block_on(async { let handler = tracing::info_span!("test.handler"); tokio::spawn( async { let ok = image_api::otel::trace_db_call("query", "get_tags", |span| { span.set_attribute("path", "a/b.jpg"); Ok(3) }); assert_eq!(ok.unwrap(), 3); let err: anyhow::Result<()> = image_api::otel::trace_db_call("delete", "remove_tag", |_span| { anyhow::bail!("no such tag") }); assert!(err.is_err()); } .instrument(handler), ) .await .expect("spawned handler"); }); let _ = provider.force_flush(); let spans = exporter.get_finished_spans().unwrap(); let handler = find(&spans, "test.handler"); let get = find(&spans, "db.query.get_tags"); let del = find(&spans, "db.delete.remove_tag"); for (span, qt, op) in [(get, "query", "get_tags"), (del, "delete", "remove_tag")] { assert_eq!(span.span_context.trace_id(), handler.span_context.trace_id()); assert_eq!( span.parent_span_id, handler.span_context.span_id(), "{op}: db span must be a child of the ambient span" ); assert_eq!(attr(span, "db.query_type"), Some(qt.into())); assert_eq!(attr(span, "db.operation"), Some(op.into())); } assert_eq!(attr(get, "path"), Some("a/b.jpg".into())); assert_eq!(get.status, Status::Ok); assert_eq!(del.status, Status::error("no such tag")); }