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:
1 parent
1c508cdabd
commit
b6deca2a52
55 files changed
+1942
-3852
No files matched your search
+58
-83
@@ -34,7 +34,6 @@ use crate::data::Claims;
|
||||
use crate::database::{ExifDao, InsightDao};
|
||||
use crate::libraries::{Library, resolve_library_param};
|
||||
use crate::memories::MemoriesSpan;
|
||||
use crate::otel::extract_context_from_request;
|
||||
use crate::state::AppState;
|
||||
use selector::ReelSelector;
|
||||
|
||||
@@ -118,7 +117,6 @@ fn capture_prefs(
|
||||
_ => "all".to_string(),
|
||||
};
|
||||
let mut dao = app_state.user_ai_prefs_dao.lock().expect("lock");
|
||||
let ctx = opentelemetry::Context::new();
|
||||
// The row passively mirrors the latest request's EFFECTIVE reel params
|
||||
// (after the request → pref → default resolution done by the caller), so
|
||||
// the stored reel_span / reel_max_segments track what was last used —
|
||||
@@ -130,28 +128,25 @@ fn capture_prefs(
|
||||
// silently wipe the owner's stored defaults on every reel creation. If the
|
||||
// read fails we bail without writing: this is best-effort, and clobbering
|
||||
// owner defaults on a transient error is worse than skipping a mirror.
|
||||
let existing = match dao.get_prefs(&ctx) {
|
||||
let existing = match dao.get_prefs() {
|
||||
Ok(row) => row,
|
||||
Err(e) => return Err(anyhow::anyhow!("failed to read prefs before mirror: {e}")),
|
||||
};
|
||||
dao.upsert_prefs(
|
||||
&ctx,
|
||||
&UpsertUserAiPrefs {
|
||||
voice: req.voice.clone().filter(|s| !s.is_empty()),
|
||||
tz_offset_minutes: Some(
|
||||
req.timezone_offset_minutes
|
||||
.unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc()),
|
||||
),
|
||||
library: Some(library),
|
||||
updated_at: now as i64,
|
||||
display_tz: req.display_tz.clone().filter(|s| !s.is_empty()),
|
||||
default_model: existing.as_ref().and_then(|e| e.default_model.clone()),
|
||||
default_backend: existing.as_ref().and_then(|e| e.default_backend.clone()),
|
||||
default_persona_id: existing.as_ref().and_then(|e| e.default_persona_id.clone()),
|
||||
reel_span: Some(effective_span.to_string()),
|
||||
reel_max_segments: Some(effective_max_segments as i32),
|
||||
},
|
||||
)
|
||||
dao.upsert_prefs(&UpsertUserAiPrefs {
|
||||
voice: req.voice.clone().filter(|s| !s.is_empty()),
|
||||
tz_offset_minutes: Some(
|
||||
req.timezone_offset_minutes
|
||||
.unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc()),
|
||||
),
|
||||
library: Some(library),
|
||||
updated_at: now as i64,
|
||||
display_tz: req.display_tz.clone().filter(|s| !s.is_empty()),
|
||||
default_model: existing.as_ref().and_then(|e| e.default_model.clone()),
|
||||
default_backend: existing.as_ref().and_then(|e| e.default_backend.clone()),
|
||||
default_persona_id: existing.as_ref().and_then(|e| e.default_persona_id.clone()),
|
||||
reel_span: Some(effective_span.to_string()),
|
||||
reel_max_segments: Some(effective_max_segments as i32),
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("failed to upsert user_ai_prefs: {e}"))
|
||||
}
|
||||
|
||||
@@ -446,15 +441,12 @@ pub struct PrecomputedReelResponse {
|
||||
/// requested span. Returns 202 + a job id; the client polls GET /reels/{id}.
|
||||
#[post("/reels")]
|
||||
pub async fn create_reel_handler(
|
||||
http_request: HttpRequest,
|
||||
_claims: Claims,
|
||||
req: web::Json<CreateReelRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
|
||||
insight_dao: web::Data<Mutex<Box<dyn InsightDao>>>,
|
||||
) -> impl Responder {
|
||||
let span_context = extract_context_from_request(&http_request);
|
||||
|
||||
if app_state.llamacpp.is_none() {
|
||||
return HttpResponse::ServiceUnavailable().json(json!({
|
||||
"error": "Reel narration needs the LLM/TTS backend (set LLAMA_SWAP_URL)"
|
||||
@@ -470,7 +462,7 @@ pub async fn create_reel_handler(
|
||||
.user_ai_prefs_dao
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut dao| dao.get_prefs(&opentelemetry::Context::new()).ok().flatten());
|
||||
.and_then(|mut dao| dao.get_prefs().ok().flatten());
|
||||
let (span, max_segments) =
|
||||
resolve_reel_params(req.span, req.max_segments, stored_prefs.as_ref());
|
||||
let display_zone_name = {
|
||||
@@ -487,7 +479,7 @@ pub async fn create_reel_handler(
|
||||
|
||||
// Cheap pass: resolve the media set for the cache key and the emptiness
|
||||
// check. Insight enrichment + scripting happen in the background job.
|
||||
let (planned, meta) = match selector::resolve(&app_state, &exif_dao, &span_context, &selector) {
|
||||
let (planned, meta) = match selector::resolve(&app_state, &exif_dao, &selector) {
|
||||
Ok(r) => r,
|
||||
Err(msg) => return HttpResponse::BadRequest().body(msg),
|
||||
};
|
||||
@@ -684,7 +676,6 @@ pub async fn precomputed_reel_handler(
|
||||
};
|
||||
let min_generated_at = now - (max_age_hours * 3600);
|
||||
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let mut dao = app_state
|
||||
.precomputed_reel_dao
|
||||
.lock()
|
||||
@@ -692,20 +683,14 @@ pub async fn precomputed_reel_handler(
|
||||
|
||||
// Fast existence gate: is there a fresh row at all?
|
||||
if !dao
|
||||
.exists_fresh(
|
||||
&ctx,
|
||||
span,
|
||||
&library_key,
|
||||
RENDER_VERSION as i32,
|
||||
min_generated_at,
|
||||
)
|
||||
.exists_fresh(span, &library_key, RENDER_VERSION as i32, min_generated_at)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return HttpResponse::NotFound().json(json!({ "error": "no precomputed reel found" }));
|
||||
}
|
||||
|
||||
// Fetch the latest row for full validity checks.
|
||||
let reel = match dao.latest_for(&ctx, span, &library_key) {
|
||||
let reel = match dao.latest_for(span, &library_key) {
|
||||
Ok(Some(r)) => r,
|
||||
_ => {
|
||||
return HttpResponse::NotFound().json(json!({ "error": "no precomputed reel found" }));
|
||||
@@ -794,8 +779,7 @@ pub(crate) async fn produce_reel(
|
||||
// (one LLM call → one narration line per beat).
|
||||
emit_progress(progress, "scripting");
|
||||
log::info!("reel produce_reel: scripting narration via LLM…");
|
||||
let span_context = opentelemetry::Context::new();
|
||||
selector::enrich(insight_dao, exif_dao, &span_context, &mut planned);
|
||||
selector::enrich(insight_dao, exif_dao, &mut planned);
|
||||
let script = match scripter {
|
||||
ScripterMode::Fast => script::generate_script(&client, &meta, &planned).await?,
|
||||
ScripterMode::Agentic => {
|
||||
@@ -1032,14 +1016,13 @@ pub(crate) fn secs_until_next_run_hour(now: chrono::DateTime<chrono::Local>, run
|
||||
/// DST-proof, unlike the old fixed-offset env vars.
|
||||
fn load_pregen_params(app_state: &AppState) -> (String, Option<String>, String) {
|
||||
// Try DB row first
|
||||
if let Ok(mut dao) = app_state.user_ai_prefs_dao.lock() {
|
||||
let ctx = opentelemetry::Context::new();
|
||||
if let Ok(Some(prefs)) = dao.get_prefs(&ctx) {
|
||||
let tz = crate::camera_tz::display_zone_for(prefs.display_tz.as_deref());
|
||||
let voice = prefs.voice;
|
||||
let library = prefs.library.unwrap_or_else(|| "all".to_string());
|
||||
return (tz, voice, library);
|
||||
}
|
||||
if let Ok(mut dao) = app_state.user_ai_prefs_dao.lock()
|
||||
&& let Ok(Some(prefs)) = dao.get_prefs()
|
||||
{
|
||||
let tz = crate::camera_tz::display_zone_for(prefs.display_tz.as_deref());
|
||||
let voice = prefs.voice;
|
||||
let library = prefs.library.unwrap_or_else(|| "all".to_string());
|
||||
return (tz, voice, library);
|
||||
}
|
||||
// Fall back to env (explicit zone overrides auto-detect)
|
||||
let tz = std::env::var("REEL_PREGEN_DISPLAY_TZ")
|
||||
@@ -1137,8 +1120,7 @@ async fn pregen_one(
|
||||
|
||||
let exif_dao = app_state.insight_generator.exif_dao();
|
||||
let insight_dao = app_state.insight_generator.insight_dao();
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let (planned, reel_meta) = match selector::resolve(app_state, exif_dao, &ctx, &selector) {
|
||||
let (planned, reel_meta) = match selector::resolve(app_state, exif_dao, &selector) {
|
||||
Ok((p, m)) => (p, m),
|
||||
Err(e) => {
|
||||
log::warn!("Pre-gen resolve failed for span={}: {}", span, e);
|
||||
@@ -1179,7 +1161,7 @@ async fn pregen_one(
|
||||
let already_current = {
|
||||
let mut dao = app_state.precomputed_reel_dao.lock().expect("lock");
|
||||
matches!(
|
||||
dao.latest_for(&ctx, span, library),
|
||||
dao.latest_for(span, library),
|
||||
Ok(Some(row))
|
||||
if row.cache_key == key
|
||||
&& row.render_version == RENDER_VERSION as i32
|
||||
@@ -1222,24 +1204,21 @@ async fn pregen_one(
|
||||
// grace window. Done under one lock so the prune sees the row we just wrote.
|
||||
let superseded = {
|
||||
let mut reel_dao = app_state.precomputed_reel_dao.lock().expect("lock");
|
||||
reel_dao.record_reel(
|
||||
&ctx,
|
||||
&crate::database::models::InsertablePrecomputedReel {
|
||||
span: span.to_string(),
|
||||
library_key: library.to_string(),
|
||||
cache_key: key.clone(),
|
||||
output_path: mp4.to_string_lossy().to_string(),
|
||||
title,
|
||||
media_count,
|
||||
render_version: RENDER_VERSION as i32,
|
||||
tz_offset_minutes: crate::camera_tz::display_zone_offset_minutes(display_tz, now)
|
||||
.unwrap_or(0) as i32,
|
||||
voice: voice.clone(),
|
||||
generated_at: now,
|
||||
},
|
||||
)?;
|
||||
reel_dao.record_reel(&crate::database::models::InsertablePrecomputedReel {
|
||||
span: span.to_string(),
|
||||
library_key: library.to_string(),
|
||||
cache_key: key.clone(),
|
||||
output_path: mp4.to_string_lossy().to_string(),
|
||||
title,
|
||||
media_count,
|
||||
render_version: RENDER_VERSION as i32,
|
||||
tz_offset_minutes: crate::camera_tz::display_zone_offset_minutes(display_tz, now)
|
||||
.unwrap_or(0) as i32,
|
||||
voice: voice.clone(),
|
||||
generated_at: now,
|
||||
})?;
|
||||
reel_dao
|
||||
.prune_superseded(&ctx, span, library, PREGEN_KEEP_PER_SCOPE)
|
||||
.prune_superseded(span, library, PREGEN_KEEP_PER_SCOPE)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for row in &superseded {
|
||||
@@ -1311,9 +1290,8 @@ fn sweep_reel_cache(app_state: &AppState) -> usize {
|
||||
|
||||
// Files a ledger row still points at (current pre-gen reels).
|
||||
let protected: std::collections::HashSet<String> = {
|
||||
let ctx = opentelemetry::Context::new();
|
||||
let mut dao = app_state.precomputed_reel_dao.lock().expect("lock");
|
||||
dao.all_cache_keys(&ctx)
|
||||
dao.all_cache_keys()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect()
|
||||
@@ -1451,21 +1429,18 @@ mod tests {
|
||||
)));
|
||||
{
|
||||
let mut dao = SqliteUserAiPrefsDao::from_connection(conn.clone());
|
||||
dao.upsert_prefs(
|
||||
&opentelemetry::Context::new(),
|
||||
&UpsertUserAiPrefs {
|
||||
voice: None,
|
||||
tz_offset_minutes: None,
|
||||
library: None,
|
||||
updated_at: 0,
|
||||
display_tz: None,
|
||||
default_model: Some("llama3.2:3b".into()),
|
||||
default_backend: Some("hybrid".into()),
|
||||
default_persona_id: Some("cabin".into()),
|
||||
reel_span: None,
|
||||
reel_max_segments: None,
|
||||
},
|
||||
)
|
||||
dao.upsert_prefs(&UpsertUserAiPrefs {
|
||||
voice: None,
|
||||
tz_offset_minutes: None,
|
||||
library: None,
|
||||
updated_at: 0,
|
||||
display_tz: None,
|
||||
default_model: Some("llama3.2:3b".into()),
|
||||
default_backend: Some("hybrid".into()),
|
||||
default_persona_id: Some("cabin".into()),
|
||||
reel_span: None,
|
||||
reel_max_segments: None,
|
||||
})
|
||||
.expect("seed owner defaults");
|
||||
}
|
||||
|
||||
@@ -1483,7 +1458,7 @@ mod tests {
|
||||
.user_ai_prefs_dao
|
||||
.lock()
|
||||
.expect("lock")
|
||||
.get_prefs(&opentelemetry::Context::new())
|
||||
.get_prefs()
|
||||
.expect("read")
|
||||
.expect("row");
|
||||
// The mirrored reel params landed...
|
||||
|
||||
Reference in new issue
Block a user