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:
Cameron CordesandClaude Opus 5.5 committed 2026-09-29 17:02:40 -04:00
1 parent 1c508cdabd
commit b6deca2a52
55 files changed
+1942 -3852

No files matched your search

+24 -56
View File
@@ -4,6 +4,7 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use actix_files::NamedFile;
use actix_web::{
@@ -12,7 +13,7 @@ use actix_web::{
};
use log::{debug, error, info, warn};
use opentelemetry::KeyValue;
use opentelemetry::trace::{Span, Status, Tracer};
use opentelemetry::trace::Status;
use serde::Serialize;
use crate::content_hash;
@@ -23,7 +24,6 @@ use crate::data::{
use crate::database::{ExifDao, PreviewDao};
use crate::files::is_valid_full_path;
use crate::libraries;
use crate::otel::{extract_context_from_request, global_tracer};
use crate::state::AppState;
use crate::video::actors::{
GeneratePreviewClipMessage, QueueVideosMessage, VideoToQueue, probe_video_stream_meta,
@@ -56,16 +56,14 @@ struct GenerateVideoResponse {
}
#[post("/video/generate")]
#[tracing::instrument(name = "generate_video", skip_all)]
pub async fn generate_video(
_claims: Claims,
request: HttpRequest,
app_state: Data<AppState>,
exif_dao: Data<std::sync::Mutex<Box<dyn ExifDao>>>,
body: web::Json<ThumbnailRequest>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("generate_video", &context);
let span = tracing::Span::current();
let preferred_library =
libraries::resolve_library_param_state(&app_state, body.library.as_deref())
@@ -106,11 +104,7 @@ pub async fn generate_video(
// already-ingested videos.
let hash_from_db: Option<String> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.get_exif_batch(
&context,
Some(resolved_library_id),
std::slice::from_ref(&rel_path),
) {
match dao.get_exif_batch(Some(resolved_library_id), std::slice::from_ref(&rel_path)) {
Ok(rows) => rows.into_iter().next().and_then(|r| r.content_hash),
Err(e) => {
warn!(
@@ -209,15 +203,14 @@ pub async fn generate_video(
/// shape is 400'd to defend against operators stashing other content in
/// the hash dir.
#[get("/video/hls/{hash}/{file}")]
#[tracing::instrument(name = "stream_hls_file", skip_all)]
pub async fn stream_hls_file(
request: HttpRequest,
_: Claims,
path: web::Path<(String, String)>,
app_state: Data<AppState>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("stream_hls_file", &context);
let span = tracing::Span::current();
let (hash, file) = path.into_inner();
if !is_valid_hash(&hash) {
@@ -319,6 +312,7 @@ fn is_allowed_hls_filename(name: &str) -> bool {
}
#[get("/video/preview")]
#[tracing::instrument(name = "get_video_preview", skip_all)]
pub async fn get_video_preview(
_claims: Claims,
request: HttpRequest,
@@ -326,9 +320,7 @@ pub async fn get_video_preview(
app_state: Data<AppState>,
preview_dao: Data<Mutex<Box<dyn PreviewDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_video_preview", &context);
let span = tracing::Span::current();
// Validate path
let full_path = match is_valid_full_path(&app_state.base_path, &req.path, true) {
@@ -351,7 +343,7 @@ pub async fn get_video_preview(
// Check preview status in DB
let preview = {
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
dao.get_preview(&context, &relative_path)
dao.get_preview(&relative_path)
};
match preview {
@@ -369,14 +361,7 @@ pub async fn get_video_preview(
Err(_) => {
// File missing on disk but DB says complete - reset and regenerate
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
let _ = dao.update_status(
&context,
&relative_path,
"pending",
None,
None,
None,
);
let _ = dao.update_status(&relative_path, "pending", None, None, None);
app_state
.preview_clip_generator
.do_send(GeneratePreviewClipMessage {
@@ -424,7 +409,7 @@ pub async fn get_video_preview(
// No record exists - insert as pending and trigger generation
{
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
let _ = dao.insert_preview(&context, &relative_path, "pending");
let _ = dao.insert_preview(&relative_path, "pending");
}
app_state
.preview_clip_generator
@@ -445,16 +430,14 @@ pub async fn get_video_preview(
}
#[post("/video/preview/status")]
#[tracing::instrument(name = "get_preview_status", skip_all)]
pub async fn get_preview_status(
_claims: Claims,
request: HttpRequest,
body: web::Json<PreviewStatusRequest>,
app_state: Data<AppState>,
preview_dao: Data<Mutex<Box<dyn PreviewDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_preview_status", &context);
let span = tracing::Span::current();
// Limit to 200 paths per request
if body.paths.len() > 200 {
@@ -465,7 +448,7 @@ pub async fn get_preview_status(
let previews = {
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
dao.get_previews_batch(&context, &body.paths)
dao.get_previews_batch(&body.paths)
};
match previews {
@@ -507,7 +490,7 @@ pub async fn get_preview_status(
// No record exists — insert as pending and trigger generation
{
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
let _ = dao.insert_preview(&context, path, "pending");
let _ = dao.insert_preview(path, "pending");
}
// Build full path for ffmpeg (actor needs the absolute path for input)
@@ -687,8 +670,7 @@ mod tests {
// Verify the DAO now has a pending record
let mut dao_lock = preview_dao.lock().unwrap();
let ctx = opentelemetry::Context::new();
let clip = dao_lock.get_preview(&ctx, "photos/new_video.mp4").unwrap();
let clip = dao_lock.get_preview("photos/new_video.mp4").unwrap();
assert!(clip.is_some());
assert_eq!(clip.unwrap().status, "pending");
}
@@ -696,18 +678,9 @@ mod tests {
#[actix_rt::test]
async fn test_get_preview_status_returns_complete_with_url() {
let mut dao = TestPreviewDao::new();
let ctx = opentelemetry::Context::new();
dao.insert_preview(&ctx, "photos/done.mp4", "pending")
dao.insert_preview("photos/done.mp4", "pending").unwrap();
dao.update_status("photos/done.mp4", "complete", Some(9.5), Some(500000), None)
.unwrap();
dao.update_status(
&ctx,
"photos/done.mp4",
"complete",
Some(9.5),
Some(500000),
None,
)
.unwrap();
let preview_dao = make_preview_dao(dao);
let app_state = Data::new(AppState::test_state());
@@ -771,10 +744,9 @@ mod tests {
#[actix_rt::test]
async fn test_get_preview_status_mixed_statuses() {
let mut dao = TestPreviewDao::new();
let ctx = opentelemetry::Context::new();
dao.insert_preview(&ctx, "a.mp4", "pending").unwrap();
dao.insert_preview(&ctx, "b.mp4", "pending").unwrap();
dao.update_status(&ctx, "b.mp4", "complete", Some(10.0), Some(100000), None)
dao.insert_preview("a.mp4", "pending").unwrap();
dao.insert_preview("b.mp4", "pending").unwrap();
dao.update_status("b.mp4", "complete", Some(10.0), Some(100000), None)
.unwrap();
let preview_dao = make_preview_dao(dao);
@@ -824,15 +796,11 @@ mod tests {
#[actix_rt::test]
async fn test_get_preview_status_requeues_pending_and_failed() {
let mut dao = TestPreviewDao::new();
let ctx = opentelemetry::Context::new();
// Simulate stale records left from a previous server run
dao.insert_preview(&ctx, "stale/pending.mp4", "pending")
.unwrap();
dao.insert_preview(&ctx, "stale/failed.mp4", "pending")
.unwrap();
dao.insert_preview("stale/pending.mp4", "pending").unwrap();
dao.insert_preview("stale/failed.mp4", "pending").unwrap();
dao.update_status(
&ctx,
"stale/failed.mp4",
"failed",
None,