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

+14 -46
View File
@@ -20,16 +20,14 @@ use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use actix_web::{App, HttpRequest, HttpResponse, Responder, dev::ServiceFactory, web};
use actix_web::{App, HttpResponse, Responder, dev::ServiceFactory, web};
use bk_tree::{BKTree, Metric};
use lazy_static::lazy_static;
use opentelemetry::trace::{TraceContextExt, Tracer};
use serde::{Deserialize, Serialize};
use crate::data::Claims;
use crate::database::{DuplicateRow, ExifDao};
use crate::libraries;
use crate::otel::{extract_context_from_request, global_tracer};
use crate::state::AppState;
// ── Cache ────────────────────────────────────────────────────────────────
@@ -223,17 +221,13 @@ pub struct FolderPair {
// ── Handlers ─────────────────────────────────────────────────────────────
#[tracing::instrument(name = "duplicates.list_exact", skip_all)]
async fn list_exact_handler(
_: Claims,
request: HttpRequest,
app_state: web::Data<AppState>,
query: web::Query<ListDuplicatesQuery>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("duplicates.list_exact", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
@@ -242,7 +236,7 @@ async fn list_exact_handler(
let rows = {
let mut dao = exif_dao.lock().expect("exif dao lock");
match dao.list_duplicates_exact(&span_context, library_id, include_resolved) {
match dao.list_duplicates_exact(library_id, include_resolved) {
Ok(rows) => rows,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
@@ -254,17 +248,13 @@ async fn list_exact_handler(
HttpResponse::Ok().json(GroupsResponse { groups })
}
#[tracing::instrument(name = "duplicates.list_perceptual", skip_all)]
async fn list_perceptual_handler(
_: Claims,
request: HttpRequest,
app_state: web::Data<AppState>,
query: web::Query<ListDuplicatesQuery>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("duplicates.list_perceptual", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
@@ -287,7 +277,7 @@ async fn list_perceptual_handler(
let rows = {
let mut dao = exif_dao.lock().expect("exif dao lock");
match dao.list_perceptual_candidates(&span_context, library_id, include_resolved) {
match dao.list_perceptual_candidates(library_id, include_resolved) {
Ok(rows) => rows,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
@@ -310,16 +300,12 @@ async fn list_perceptual_handler(
HttpResponse::Ok().json(GroupsResponse { groups })
}
#[tracing::instrument(name = "duplicates.resolve", skip_all)]
async fn resolve_handler(
_: Claims,
request: HttpRequest,
body: web::Json<ResolveDuplicatesReq>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("duplicates.resolve", &context);
let span_context = opentelemetry::Context::current_with_span(span);
if body.demoted.is_empty() {
return HttpResponse::BadRequest().body("demoted list is empty");
}
@@ -328,11 +314,8 @@ async fn resolve_handler(
// Resolve survivor → its content_hash, plus the canonical rel_path
// we'll use as the destination for any tag-union INSERTs.
let survivor = match dao.lookup_duplicate_row(
&span_context,
body.survivor.library_id,
&body.survivor.rel_path,
) {
let survivor = match dao.lookup_duplicate_row(body.survivor.library_id, &body.survivor.rel_path)
{
Ok(Some(row)) => row,
Ok(None) => return HttpResponse::NotFound().body("survivor not found"),
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
@@ -348,11 +331,7 @@ async fn resolve_handler(
let mut resolved_count = 0usize;
for member_ref in &body.demoted {
let demoted = match dao.lookup_duplicate_row(
&span_context,
member_ref.library_id,
&member_ref.rel_path,
) {
let demoted = match dao.lookup_duplicate_row(member_ref.library_id, &member_ref.rel_path) {
Ok(Some(row)) => row,
Ok(None) => {
log::warn!(
@@ -379,7 +358,6 @@ async fn resolve_handler(
// already shared at the bytes layer — the union is a no-op.
if demoted.content_hash != survivor.content_hash
&& let Err(e) = dao.union_perceptual_tags(
&span_context,
&survivor.content_hash,
&demoted.content_hash,
&survivor.rel_path,
@@ -397,7 +375,6 @@ async fn resolve_handler(
}
if let Err(e) = dao.set_duplicate_of(
&span_context,
demoted.library_id,
&demoted.rel_path,
&survivor.content_hash,
@@ -416,18 +393,14 @@ async fn resolve_handler(
HttpResponse::Ok().json(ResolveResponse { resolved_count })
}
#[tracing::instrument(name = "duplicates.unresolve", skip_all)]
async fn unresolve_handler(
_: Claims,
request: HttpRequest,
body: web::Json<UnresolveDuplicateReq>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("duplicates.unresolve", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let mut dao = exif_dao.lock().expect("exif dao lock");
if let Err(e) = dao.clear_duplicate_of(&span_context, body.library_id, &body.rel_path) {
if let Err(e) = dao.clear_duplicate_of(body.library_id, &body.rel_path) {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
@@ -438,17 +411,13 @@ async fn unresolve_handler(
HttpResponse::Ok().finish()
}
#[tracing::instrument(name = "duplicates.list_folder_pairs", skip_all)]
async fn list_folder_pairs_handler(
_: Claims,
request: HttpRequest,
app_state: web::Data<AppState>,
query: web::Query<ListFolderPairsQuery>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let context = extract_context_from_request(&request);
let span = global_tracer().start_with_context("duplicates.list_folder_pairs", &context);
let span_context = opentelemetry::Context::current_with_span(span);
let library_id = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
.ok()
.flatten()
@@ -472,12 +441,11 @@ async fn list_folder_pairs_handler(
let (dup_rows, all_paths) = {
let mut dao = exif_dao.lock().expect("exif dao lock");
let dup_rows = match dao.list_duplicates_exact(&span_context, library_id, include_resolved)
{
let dup_rows = match dao.list_duplicates_exact(library_id, include_resolved) {
Ok(rows) => rows,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
let all_paths = match dao.list_image_paths(&span_context, library_id, include_resolved) {
let all_paths = match dao.list_image_paths(library_id, include_resolved) {
Ok(rows) => rows,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};