Tracing: DAO layer + handler spans on tracing, drop all Context params (#141)
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>
This commit is contained in:
1 parent
1c508cdabd
commit
b6deca2a52
55 files changed
+1942
-3852
No files matched your search
@@ -0,0 +1,100 @@
|
||||
//! 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.<query_type>.<operation>` 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<Value> {
|
||||
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"));
|
||||
}
|
||||
+10
-16
@@ -8,16 +8,15 @@
|
||||
//! 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.
|
||||
//! 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::{web, App, HttpRequest, HttpResponse};
|
||||
use opentelemetry::trace::{Tracer, TracerProvider};
|
||||
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;
|
||||
@@ -27,12 +26,10 @@ 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);
|
||||
/// 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()
|
||||
}
|
||||
|
||||
@@ -65,10 +62,7 @@ async fn traceparent_header_nests_request_spans() {
|
||||
|
||||
let req = actix_web::test::TestRequest::get()
|
||||
.uri("/op")
|
||||
.insert_header((
|
||||
"traceparent",
|
||||
format!("00-{TRACE_ID}-{REMOTE_PARENT}-01"),
|
||||
))
|
||||
.insert_header(("traceparent", format!("00-{TRACE_ID}-{REMOTE_PARENT}-01")))
|
||||
.to_request();
|
||||
let _res = actix_web::test::call_and_read_body(&srv, req).await;
|
||||
|
||||
|
||||
Reference in new issue
Block a user