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

+34 -69
View File
@@ -93,14 +93,12 @@ pub async fn generation_status_handler(
query: web::Query<GenerationStatusQuery>,
app_state: web::Data<AppState>,
) -> impl Responder {
let ctx = opentelemetry::Context::new();
if let Some(jid) = query.job_id {
let mut dao = app_state
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.get_job_by_id(&ctx, jid) {
match dao.get_job_by_id(jid) {
Ok(Some(job)) => {
return HttpResponse::Ok().json(GenerationStatusResponse {
job_id: job.id,
@@ -136,7 +134,7 @@ pub async fn generation_status_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.get_active_job(&ctx, library.id, &normalized) {
match dao.get_active_job(library.id, &normalized) {
Ok(Some(job)) => {
return HttpResponse::Ok().json(GenerationStatusResponse {
job_id: job.id,
@@ -187,14 +185,12 @@ pub async fn cancel_generation_handler(
request: web::Json<CancelGenerationRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
let ctx = opentelemetry::Context::new();
if let Some(jid) = request.job_id {
let mut dao = app_state
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.cancel_job(&ctx, jid) {
match dao.cancel_job(jid) {
Ok(true) => {
let mut handles = app_state
.insight_job_handles
@@ -238,12 +234,12 @@ pub async fn cancel_generation_handler(
.lock()
.expect("Unable to lock InsightJobDao");
let ids = dao
.get_active_job(&ctx, library.id, &normalized)
.get_active_job(library.id, &normalized)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized);
let _ = dao.cancel_active_jobs(library.id, &normalized);
ids
};
@@ -400,7 +396,7 @@ fn apply_owner_defaults(
let Some(mut dao) = app_state.user_ai_prefs_dao.lock().ok() else {
return;
};
let Ok(Some(prefs)) = dao.get_prefs(&opentelemetry::Context::new()) else {
let Ok(Some(prefs)) = dao.get_prefs() else {
return;
};
if model.is_none() {
@@ -455,14 +451,13 @@ pub async fn generate_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
let ctx = opentelemetry::Context::new();
let ids = dao
.get_active_job(&ctx, library.id, &normalized_path)
.get_active_job(library.id, &normalized_path)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
ids
};
for jid in &old_job_ids {
@@ -481,12 +476,7 @@ pub async fn generate_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.create_job(
&opentelemetry::Context::new(),
library.id,
&normalized_path,
gen_type,
) {
match dao.create_job(library.id, &normalized_path, gen_type) {
Ok(id) => id,
Err(e) => {
log::error!("Failed to create generation job: {:?}", e);
@@ -538,7 +528,6 @@ pub async fn generate_insight_handler(
})
.await;
let ctx = opentelemetry::Context::new();
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
match result {
@@ -547,18 +536,12 @@ pub async fn generate_insight_handler(
.insight_dao()
.lock()
.expect("Unable to lock InsightDao");
let insight_id = insight_dao
.get_insight(&ctx, &path)
.ok()
.flatten()
.map(|i| i.id);
let insight_id = insight_dao.get_insight(&path).ok().flatten().map(|i| i.id);
if let Some(id) = insight_id {
if let Err(e) = dao.complete_job(&ctx, job_id, id) {
if let Err(e) = dao.complete_job(job_id, id) {
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
}
} else if let Err(e) =
dao.fail_job(&ctx, job_id, "generation returned no insight")
{
} else if let Err(e) = dao.fail_job(job_id, "generation returned no insight") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
}
}
@@ -566,7 +549,7 @@ pub async fn generate_insight_handler(
log::error!("Insight generation failed for {}: {:?}", path, e);
// `{:#}` = one-line context chain; the job's error_message is
// returned to the client verbatim, so no Debug/backtrace here.
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -577,14 +560,14 @@ pub async fn generate_insight_handler(
timeout_secs
);
if let Err(err) =
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
{
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
Err(_) => {
log::error!("Insight generation task panicked for {}", path);
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -627,8 +610,6 @@ pub async fn get_insight_handler(
let normalized_path = normalize_path(&query.path);
log::debug!("Fetching insight for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
// Expand to rel_paths sharing content so an insight generated under
// library 1 still shows when the same photo is viewed from library 2.
let library = libraries::resolve_library_param_state(&app_state, query.library.as_deref())
@@ -637,13 +618,13 @@ pub async fn get_insight_handler(
.unwrap_or_else(|| app_state.primary_library());
let sibling_paths = {
let mut exif = exif_dao.lock().expect("Unable to lock ExifDao");
exif.get_rel_paths_sharing_content(&otel_context, library.id, &normalized_path)
exif.get_rel_paths_sharing_content(library.id, &normalized_path)
.unwrap_or_else(|_| vec![normalized_path.clone()])
};
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_insight_for_paths(&otel_context, &sibling_paths) {
match dao.get_insight_for_paths(&sibling_paths) {
Ok(Some(insight)) => HttpResponse::Ok().json(PhotoInsightResponse::from(insight)),
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
"error": "Insight not found"
@@ -667,10 +648,9 @@ pub async fn delete_insight_handler(
let normalized_path = normalize_path(&query.path);
log::info!("Deleting insight for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.delete_insight(&otel_context, &normalized_path) {
match dao.delete_insight(&normalized_path) {
Ok(()) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Insight deleted successfully"
@@ -692,10 +672,9 @@ pub async fn get_all_insights_handler(
) -> impl Responder {
log::debug!("Fetching all insights");
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_all_insights(&otel_context) {
match dao.get_all_insights() {
Ok(insights) => {
let responses: Vec<PhotoInsightResponse> = insights
.into_iter()
@@ -725,10 +704,9 @@ pub async fn get_insight_history_handler(
let normalized_path = normalize_path(&query.path);
log::debug!("Fetching insight history for {}", normalized_path);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
match dao.get_insight_history(&otel_context, &normalized_path) {
match dao.get_insight_history(&normalized_path) {
Ok(insights) => {
let responses: Vec<PhotoInsightResponse> = insights
.into_iter()
@@ -790,14 +768,13 @@ pub async fn generate_agentic_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
let ctx = opentelemetry::Context::new();
let ids = dao
.get_active_job(&ctx, library.id, &normalized_path)
.get_active_job(library.id, &normalized_path)
.ok()
.flatten()
.map(|j| vec![j.id])
.unwrap_or_default();
let _ = dao.cancel_active_jobs(&ctx, library.id, &normalized_path);
let _ = dao.cancel_active_jobs(library.id, &normalized_path);
ids
};
for jid in &old_job_ids {
@@ -816,12 +793,7 @@ pub async fn generate_agentic_insight_handler(
.insight_job_dao
.lock()
.expect("Unable to lock InsightJobDao");
match dao.create_job(
&opentelemetry::Context::new(),
library.id,
&normalized_path,
gen_type,
) {
match dao.create_job(library.id, &normalized_path, gen_type) {
Ok(id) => id,
Err(e) => {
log::error!("Failed to create agentic generation job: {:?}", e);
@@ -844,7 +816,6 @@ pub async fn generate_agentic_insight_handler(
};
let fewshot_examples: Vec<Vec<ChatMessage>> = {
let otel_context = opentelemetry::Context::new();
let mut dao = app_state
.insight_chat
.insight_dao()
@@ -853,7 +824,7 @@ pub async fn generate_agentic_insight_handler(
fewshot_ids
.iter()
.filter_map(|id| {
let insight = dao.get_insight_by_id(&otel_context, *id).ok().flatten()?;
let insight = dao.get_insight_by_id(*id).ok().flatten()?;
let json = insight.training_messages?;
match serde_json::from_str::<Vec<ChatMessage>>(&json) {
Ok(msgs) => Some(msgs),
@@ -940,19 +911,16 @@ pub async fn generate_agentic_insight_handler(
})
.await;
let ctx = opentelemetry::Context::new();
let mut dao = job_dao.lock().expect("Unable to lock InsightJobDao");
match result {
Ok(Ok(Ok((Some(insight_id), _, _)))) => {
if let Err(e) = dao.complete_job(&ctx, job_id, insight_id) {
if let Err(e) = dao.complete_job(job_id, insight_id) {
log::error!("Failed to mark job {} as completed: {:?}", job_id, e);
}
}
Ok(Ok(Ok((None, _, _)))) => {
if let Err(e) =
dao.fail_job(&ctx, job_id, "agentic generation returned no insight")
{
if let Err(e) = dao.fail_job(job_id, "agentic generation returned no insight") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, e);
}
}
@@ -960,7 +928,7 @@ pub async fn generate_agentic_insight_handler(
log::error!("Agentic insight generation failed for {}: {:?}", path, e);
// `{:#}` = one-line context chain; the job's error_message is
// returned to the client verbatim, so no Debug/backtrace here.
if let Err(err) = dao.fail_job(&ctx, job_id, &format!("{:#}", e)) {
if let Err(err) = dao.fail_job(job_id, &format!("{:#}", e)) {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -971,14 +939,14 @@ pub async fn generate_agentic_insight_handler(
timeout_secs
);
if let Err(err) =
dao.fail_job(&ctx, job_id, &format!("timeout after {}s", timeout_secs))
dao.fail_job(job_id, &format!("timeout after {}s", timeout_secs))
{
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
Err(_) => {
log::error!("Agentic insight generation task panicked for {}", path);
if let Err(err) = dao.fail_job(&ctx, job_id, "generation task panicked") {
if let Err(err) = dao.fail_job(job_id, "generation task panicked") {
log::error!("Failed to mark job {} as failed: {:?}", job_id, err);
}
}
@@ -1135,14 +1103,13 @@ pub async fn rate_insight_handler(
request.approved
);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
// Rate a specific version by id when provided (history view), otherwise
// rate the current insight for the path.
let result = match request.insight_id {
Some(id) => dao.rate_insight_by_id(&otel_context, id, request.approved),
None => dao.rate_insight(&otel_context, &normalized_path, request.approved),
Some(id) => dao.rate_insight_by_id(id, request.approved),
None => dao.rate_insight(&normalized_path, request.approved),
};
match result {
@@ -1169,13 +1136,12 @@ pub async fn export_training_data_handler(
let approved_only = query.approved_only.unwrap_or(true);
log::info!("Exporting training data (approved_only={})", approved_only);
let otel_context = opentelemetry::Context::new();
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
let insights = if approved_only {
dao.get_approved_insights(&otel_context)
dao.get_approved_insights()
} else {
dao.get_all_insights(&otel_context)
dao.get_all_insights()
};
match insights {
@@ -2369,8 +2335,7 @@ mod owner_defaults_tests {
)));
if let Some(p) = prefs {
let mut dao = SqliteUserAiPrefsDao::from_connection(conn);
dao.upsert_prefs(&opentelemetry::Context::new(), &p)
.expect("seed prefs");
dao.upsert_prefs(&p).expect("seed prefs");
}
state
}