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

+5 -7
View File
@@ -3,27 +3,25 @@
//! under lib2 if the same rel_path resolves there too.
use std::sync::Mutex;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use actix_web::{
HttpRequest, HttpResponse, Responder, delete, get, put,
HttpResponse, Responder, delete, get, put,
web::{self, Data},
};
use log::{error, info, warn};
use opentelemetry::trace::{Span, Status, Tracer};
use opentelemetry::trace::Status;
use crate::data::{AddFavoriteRequest, Claims, PhotosResponse};
use crate::database::{DbError, DbErrorKind, FavoriteDao};
use crate::otel::{extract_context_from_request, global_tracer};
#[get("image/favorites")]
#[tracing::instrument(name = "get favorites", skip_all)]
pub async fn favorites(
claims: Claims,
request: HttpRequest,
favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
) -> impl Responder {
let tracer = global_tracer();
let context = extract_context_from_request(&request);
let mut span = tracer.start_with_context("get favorites", &context);
let span = tracing::Span::current();
match web::block(move || {
favorites_dao
+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);
+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,