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

+29 -65
View File
@@ -8,6 +8,7 @@ use std::io::ErrorKind;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use actix_files::NamedFile;
use actix_multipart as mp;
@@ -19,7 +20,7 @@ use chrono::Utc;
use futures::stream::StreamExt;
use log::{debug, error, info, trace, warn};
use opentelemetry::KeyValue;
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
use opentelemetry::trace::Status;
use urlencoding::decode;
use crate::content_hash;
@@ -34,11 +35,11 @@ use crate::file_types;
use crate::files::{RefreshThumbnailsMessage, is_image_or_video, is_valid_full_path};
use crate::libraries;
use crate::memories;
use crate::otel::{extract_context_from_request, global_tracer};
use crate::perceptual_hash;
use crate::state::AppState;
#[get("/image")]
#[tracing::instrument(name = "get_image", skip_all)]
pub async fn get_image(
_claims: Claims,
request: HttpRequest,
@@ -46,10 +47,7 @@ pub async fn get_image(
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_image", &context);
let span = tracing::Span::current();
// Resolve library from query param; default to primary so clients that
// don't yet send `library=` continue to work.
@@ -110,7 +108,7 @@ pub async fn get_image(
// rows that aren't hashed yet.
let hash_large_path: Option<PathBuf> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.get_exif(&context, &relative_path_str) {
match dao.get_exif(&relative_path_str) {
Ok(Some(row)) => row
.content_hash
.as_deref()
@@ -208,7 +206,7 @@ pub async fn get_image(
let hash_xlarge_path: Option<PathBuf> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.get_exif(&context, &relative_path_str) {
match dao.get_exif(&relative_path_str) {
Ok(Some(row)) => row
.content_hash
.as_deref()
@@ -327,7 +325,7 @@ pub async fn get_image(
// prevents needless 404s for already-warmed deployments.
let hash_thumb_path: Option<PathBuf> = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
match dao.get_exif(&context, &relative_path_str) {
match dao.get_exif(&relative_path_str) {
Ok(Some(row)) => row
.content_hash
.as_deref()
@@ -479,18 +477,14 @@ async fn create_circular_thumbnail(
}
#[get("/image/metadata")]
#[tracing::instrument(name = "get_file_metadata", skip_all)]
pub async fn get_file_metadata(
_: Claims,
request: HttpRequest,
path: web::Query<ThumbnailRequest>,
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_file_metadata", &context);
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let span = tracing::Span::current();
let library = libraries::resolve_library_param_state(&app_state, path.library.as_deref())
.ok()
@@ -531,7 +525,7 @@ pub async fn get_file_metadata(
// Query EXIF data if available
if let Ok(mut dao) = exif_dao.lock()
&& let Ok(Some(exif)) = dao.get_exif(&span_context, &path.path)
&& let Ok(Some(exif)) = dao.get_exif(&path.path)
{
response.exif = Some(exif.into());
}
@@ -567,18 +561,14 @@ struct SetGpsRequest {
}
#[post("/image/exif/gps")]
#[tracing::instrument(name = "set_image_gps", skip_all)]
pub async fn set_image_gps(
_: Claims,
request: HttpRequest,
body: web::Json<SetGpsRequest>,
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("set_image_gps", &context);
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let span = tracing::Span::current();
let library = libraries::resolve_library_param_state(&app_state, body.library.as_deref())
.ok()
@@ -693,10 +683,10 @@ pub async fn set_image_gps(
// If the row doesn't exist yet (file isn't indexed for some reason),
// insert instead so the GPS write is at least visible the moment
// the watcher catches up.
match dao.get_exif(&span_context, &normalized_path) {
Ok(Some(_)) => dao.update_exif(&span_context, insert_exif),
Ok(None) => dao.store_exif(&span_context, insert_exif),
Err(_) => dao.update_exif(&span_context, insert_exif),
match dao.get_exif(&normalized_path) {
Ok(Some(_)) => dao.update_exif(insert_exif),
Ok(None) => dao.store_exif(insert_exif),
Err(_) => dao.update_exif(insert_exif),
}
};
@@ -744,15 +734,13 @@ pub async fn set_image_gps(
/// to 503 when exiftool isn't installed (deployer guidance is the same
/// as for the RAW preview pipeline: install exiftool for full coverage).
#[get("/image/exif/full")]
#[tracing::instrument(name = "get_full_exif", skip_all)]
pub async fn get_full_exif(
_: Claims,
request: HttpRequest,
path: web::Query<ThumbnailRequest>,
app_state: Data<AppState>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get_full_exif", &context);
let span = tracing::Span::current();
let library = libraries::resolve_library_param_state(&app_state, path.library.as_deref())
.ok()
@@ -887,18 +875,14 @@ fn build_metadata_response_for_date_mutation(
}
#[post("/image/exif/date")]
#[tracing::instrument(name = "set_image_date", skip_all)]
pub async fn set_image_date(
_: Claims,
request: HttpRequest,
body: web::Json<SetDateRequest>,
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("set_image_date", &context);
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let span = tracing::Span::current();
let library = match libraries::resolve_library_param_state(&app_state, body.library.as_deref())
{
@@ -922,7 +906,7 @@ pub async fn set_image_date(
.user_ai_prefs_dao
.lock()
.expect("Unable to lock UserAiPrefsDao");
match prefs_dao.get_prefs(&span_context) {
match prefs_dao.get_prefs() {
Ok(Some(prefs)) => crate::camera_tz::display_zone_for(prefs.display_tz.as_deref()),
_ => crate::camera_tz::display_zone_for(None),
}
@@ -930,13 +914,7 @@ pub async fn set_image_date(
let updated = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
dao.set_manual_date_taken(
&span_context,
library.id,
&normalized_path,
body.date_taken,
&display_tz,
)
dao.set_manual_date_taken(library.id, &normalized_path, body.date_taken, &display_tz)
};
match updated {
@@ -961,18 +939,14 @@ pub async fn set_image_date(
}
#[post("/image/exif/date/clear")]
#[tracing::instrument(name = "clear_image_date", skip_all)]
pub async fn clear_image_date(
_: Claims,
request: HttpRequest,
body: web::Json<ClearDateRequest>,
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("clear_image_date", &context);
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let span = tracing::Span::current();
let library = match libraries::resolve_library_param_state(&app_state, body.library.as_deref())
{
@@ -988,7 +962,7 @@ pub async fn clear_image_date(
let updated = {
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
dao.clear_manual_date_taken(&span_context, library.id, &normalized_path)
dao.clear_manual_date_taken(library.id, &normalized_path)
};
match updated {
@@ -1018,19 +992,15 @@ struct UploadQuery {
}
#[post("/image")]
#[tracing::instrument(name = "upload_image", skip_all)]
pub async fn upload_image(
_: Claims,
request: HttpRequest,
query: web::Query<UploadQuery>,
mut payload: mp::Multipart,
app_state: Data<AppState>,
exif_dao: Data<Mutex<Box<dyn ExifDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("upload_image", &context);
let span_context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
let span = tracing::Span::current();
// Resolve the optional library selector. Absent → primary library
// (backwards-compatible with clients that don't yet send `library=`).
@@ -1097,7 +1067,7 @@ pub async fn upload_image(
.to_string();
{
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
if let Ok(Some(existing)) = dao.find_by_content_hash(&span_context, &upload_hash)
if let Ok(Some(existing)) = dao.find_by_content_hash(&upload_hash)
&& existing.duplicate_of_hash.is_none()
{
let library_name = libraries::load_all(&mut crate::database::connect())
@@ -1116,12 +1086,6 @@ pub async fn upload_image(
}
}
let context =
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
tracer
.span_builder("file write")
.start_with_context(&tracer, &context);
let uploaded_path = if !full_path.is_file() && is_image_or_video(&full_path) {
let mut file = File::create(&full_path).unwrap();
file.write_all(&file_content).unwrap();
@@ -1212,7 +1176,7 @@ pub async fn upload_image(
};
if let Ok(mut dao) = exif_dao.lock() {
if let Err(e) = dao.store_exif(&span_context, insert_exif) {
if let Err(e) = dao.store_exif(insert_exif) {
error!("Failed to store EXIF data for {}: {:?}", relative_path, e);
} else {
debug!("EXIF data stored for {}", relative_path);