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 -28
View File
@@ -132,9 +132,8 @@ pub fn cleanup_orphaned_playlists(
// lives in any library is alive, even if the library a given
// download attributed it to has since been disabled.
let alive_hashes: HashSet<String> = {
let context = opentelemetry::Context::new();
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.list_distinct_content_hashes(&context) {
match dao.list_distinct_content_hashes() {
Ok(hashes) => hashes.into_iter().collect(),
Err(e) => {
error!(
@@ -399,11 +398,10 @@ pub fn watch_files(
if !lib.enabled {
continue;
}
let context = opentelemetry::Context::new();
let had_data = exif_dao
.lock()
.expect("exif_dao poisoned")
.count_for_library(&context, lib.id)
.count_for_library(lib.id)
.map(|n| n > 0)
.unwrap_or(false);
libraries::refresh_health(&library_health, lib, had_data);
@@ -448,10 +446,9 @@ pub fn watch_files(
// probe gate to cover handoff and orphan GC. See
// CLAUDE.md "Library availability and safety".
let had_data = {
let context = opentelemetry::Context::new();
let mut guard = exif_dao.lock().expect("exif_dao poisoned");
guard
.count_for_library(&context, lib.id)
.count_for_library(lib.id)
.map(|n| n > 0)
.unwrap_or(false)
};
@@ -477,10 +474,8 @@ pub fn watch_files(
let effective_excludes = lib.effective_excluded_dirs(&excluded_dirs);
if face_client.is_enabled() {
let context = opentelemetry::Context::new();
backfill::backfill_unhashed_backlog(&context, lib, &exif_dao);
backfill::backfill_unhashed_backlog(lib, &exif_dao);
backfill::process_face_backlog(
&context,
lib,
&face_client,
&face_dao,
@@ -494,9 +489,7 @@ pub fn watch_files(
// that don't have the face engine wired up. Mirrors the
// face drain shape (capped per tick, no-op when disabled).
if clip_client.is_enabled() {
let context = opentelemetry::Context::new();
backfill::process_clip_backlog(
&context,
lib,
&clip_client,
&exif_dao,
@@ -509,8 +502,7 @@ pub fn watch_files(
// of face detection — runs even on deploys that don't
// configure Apollo, since `/memories` depends on it.
{
let context = opentelemetry::Context::new();
backfill::backfill_missing_date_taken(&context, lib, &exif_dao);
backfill::backfill_missing_date_taken(lib, &exif_dao);
// True-instant drain (ADR-0001, #118): convert
// legacy mixed-convention rows, invalidating day
@@ -520,7 +512,7 @@ pub fn watch_files(
// prefs change applies without restart.
let display_tz = {
let mut prefs = user_ai_prefs_dao.lock().expect("prefs dao poisoned");
match prefs.get_prefs(&context) {
match prefs.get_prefs() {
Ok(Some(p)) => {
crate::camera_tz::display_zone_for(p.display_tz.as_deref())
}
@@ -528,7 +520,6 @@ pub fn watch_files(
}
};
backfill::backfill_utc_dates(
&context,
lib,
&exif_dao,
&precomputed_reel_dao,
@@ -536,7 +527,6 @@ pub fn watch_files(
utc_backfill_cursors.entry(lib.id).or_insert(0),
);
backfill::refresh_utc_backfill_progress(
&context,
lib,
&exif_dao,
&mut utc_backfill_remaining,
@@ -595,11 +585,9 @@ pub fn watch_files(
// visit this tick get visited next tick. See
// library_maintenance::detect_missing_files_for_library.
{
let context = opentelemetry::Context::new();
let offset = missing_file_offsets.get(&lib.id).copied().unwrap_or(0);
let (deleted, next_offset) =
library_maintenance::detect_missing_files_for_library(
&context,
lib,
&exif_dao,
offset,
@@ -708,7 +696,6 @@ pub fn process_new_files(
playlist_manager: Addr<VideoPlaylistManager>,
preview_generator: Addr<video::actors::PreviewClipGenerator>,
) {
let context = opentelemetry::Context::new();
let thumbs = dotenv::var("THUMBNAILS").expect("THUMBNAILS not defined");
let thumbnail_directory = Path::new(&thumbs);
let base_path = Path::new(&library.root_path);
@@ -737,7 +724,7 @@ pub fn process_new_files(
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
// Walk is per-library, so scope the lookup so a same-named file
// in another library doesn't make this one look already-indexed.
match dao.get_exif_batch(&context, Some(library.id), &file_paths) {
match dao.get_exif_batch(Some(library.id), &file_paths) {
Ok(exif_records) => exif_records
.into_iter()
.map(|record| (record.file_path, record.content_hash))
@@ -888,7 +875,7 @@ pub fn process_new_files(
};
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
if let Err(e) = dao.store_exif(&context, insert_exif) {
if let Err(e) = dao.store_exif(insert_exif) {
error!(
"Failed to register {} in image_exif: {:?}",
relative_path, e
@@ -915,9 +902,8 @@ pub fn process_new_files(
// right tool for very large legacy libraries; this branch
// ensures small/medium deploys self-heal without operator
// action.
backfill::backfill_missing_content_hashes(&context, &files, library, &exif_dao);
let candidates =
backfill::build_face_candidates(&context, library, &files, &exif_dao, &face_dao);
backfill::backfill_missing_content_hashes(&files, library, &exif_dao);
let candidates = backfill::build_face_candidates(library, &files, &exif_dao, &face_dao);
debug!(
"face_watch: scan tick — {} image file(s) walked, {} candidate(s) (library '{}', modified_since={})",
files
@@ -996,7 +982,7 @@ pub fn process_new_files(
let existing_previews: HashMap<String, String> = {
let mut dao = preview_dao.lock().expect("Unable to lock PreviewDao");
match dao.get_previews_batch(&context, &video_rel_paths) {
match dao.get_previews_batch(&video_rel_paths) {
Ok(clips) => clips
.into_iter()
.map(|clip| (clip.file_path, clip.status))
@@ -1021,7 +1007,7 @@ pub fn process_new_files(
// Insert pending record using relative path
if status.is_none() {
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");
}
// Send full path in the message — the actor will derive relative path from it
@@ -1047,7 +1033,7 @@ pub fn process_new_files(
let disk_paths: HashSet<String> = files.iter().map(|(_, rel)| rel.clone()).collect();
let db_paths: Vec<String> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
dao.get_rel_paths_for_library(&context, library.id)
dao.get_rel_paths_for_library(library.id)
.unwrap_or_else(|e| {
error!(
"Reconciliation: failed to load image_exif rel_paths for lib {}: {:?}",
@@ -1070,7 +1056,7 @@ pub fn process_new_files(
);
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
for rel in &stale {
if let Err(e) = dao.delete_exif_by_library(&context, library.id, rel) {
if let Err(e) = dao.delete_exif_by_library(library.id, rel) {
warn!(
"Reconciliation: failed to delete {} (lib {}): {:?}",
rel, library.id, e