Background work has no request parent, so each job now opens a named tracing root and carries it into its fan-out: - watcher: `watcher.tick` (scan=quick|full) entered per tick on the watcher's std thread, `watcher.library` per library, and `watcher.playlist_cleanup` per orphaned-HLS pass. face_watch / clip_watch carry the ambient span into every per-candidate tokio::spawn and give each call its own `face_watch.detect` / `clip_watch.encode` span. - thumbnails: `creating thumbnails` via #[instrument]; per-video spans on rayon workers name their parent explicitly. - video actors: `playlistgenerator.generate_playlist` and `preview_clip.generate` are tracing roots that instrument their futures. - reels: `reel.job` (created before the spawn, so on-demand jobs stay in the request trace), `reel.pregen_batch`, `reel.cache_sweep`. - RefreshThumbnailsMessage and the dead-code GIF job move to tracing. No raw OpenTelemetry span creation is left outside otel.rs; `global_tracer` is removed. tests/otel_background.rs pins the std-thread tick -> block_on -> spawn shape and the rayon explicit-parent shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
3.7 KiB
Rust
104 lines
3.7 KiB
Rust
//! 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::Value;
|
|
use opentelemetry::trace::{Status, TracerProvider};
|
|
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"));
|
|
}
|