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

+12 -22
View File
@@ -529,26 +529,24 @@ impl PersonaChatSession {
if persona_id.is_empty() {
return Err(PersonaChatError::UnknownPersona(String::new()));
}
let cx = opentelemetry::Context::current();
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
// Validate up front rather than letting the first turn 404: a
// conversation row pointing at a persona that doesn't exist would be
// a permanently dead entry in the list.
let persona = dao
.get_persona(&cx, user_id, persona_id)
.get_persona(user_id, persona_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?;
if persona.is_none() {
return Err(PersonaChatError::UnknownPersona(persona_id.to_string()));
}
dao.create_persona_chat(&cx, user_id, persona_id, Utc::now().timestamp_millis())
dao.create_persona_chat(user_id, persona_id, Utc::now().timestamp_millis())
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))
}
/// Delete a conversation and its transcript outright.
pub fn delete_conversation(&self, user_id: i32, conversation_id: &str) -> Result<()> {
let cx = opentelemetry::Context::current();
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.delete_persona_chat(&cx, user_id, conversation_id)
dao.delete_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to delete persona chat: {e:?}"))?;
Ok(())
}
@@ -565,10 +563,9 @@ impl PersonaChatSession {
conversation_id: &str,
branch_id: Option<u64>,
) -> Result<PersonaChatHistoryView> {
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let Some(row) = row else {
@@ -628,18 +625,17 @@ impl PersonaChatSession {
return Err(PersonaChatError::UnknownConversation(conversation_id));
}
let cx = opentelemetry::Context::current();
// The persona comes from the stored conversation, not the request:
// a transcript's voice is fixed when it is created, so a stale
// client can't quietly swap it half-way through a thread.
let (persona, persona_id) = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let row = dao
.get_persona_chat(&cx, user_id, &conversation_id)
.get_persona_chat(user_id, &conversation_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
.ok_or_else(|| PersonaChatError::UnknownConversation(conversation_id.clone()))?;
let persona = dao
.get_persona(&cx, user_id, &row.persona_id)
.get_persona(user_id, &row.persona_id)
.map_err(|e| PersonaChatError::Db(anyhow!("{e:?}")))?
.ok_or_else(|| PersonaChatError::UnknownPersona(row.persona_id.clone()))?;
(persona, row.persona_id)
@@ -742,10 +738,9 @@ impl PersonaChatSession {
let _guard = lock.lock().await;
// Replay the active branch of the conversation tree.
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, &conversation_id)
dao.get_persona_chat(user_id, &conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
@@ -935,7 +930,6 @@ impl PersonaChatSession {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.update_persona_chat(
&cx,
user_id,
&conversation_id,
&json,
@@ -958,7 +952,7 @@ impl PersonaChatSession {
.unwrap_or_else(|| fallback_title(&messages));
if !title.is_empty() {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
if let Err(e) = dao.set_persona_chat_title(&cx, user_id, &conversation_id, &title) {
if let Err(e) = dao.set_persona_chat_title(user_id, &conversation_id, &title) {
log::warn!("failed to store persona chat title: {e:?}");
}
}
@@ -989,14 +983,13 @@ impl PersonaChatSession {
/// Conversations that hold nothing but the seed are omitted too — the
/// user has never actually said anything in them.
pub fn list_conversations(&self, user_id: i32) -> Result<Vec<PersonaConversationSummary>> {
let cx = opentelemetry::Context::current();
let (rows, personas) = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.list_persona_chats(&cx, user_id)
.list_persona_chats(user_id)
.map_err(|e| anyhow!("failed to list persona chats: {e:?}"))?;
let personas = dao
.list_personas(&cx, user_id)
.list_personas(user_id)
.map_err(|e| anyhow!("failed to list personas: {e:?}"))?;
(rows, personas)
};
@@ -1099,10 +1092,9 @@ impl PersonaChatSession {
};
let _guard = lock.lock().await;
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;
@@ -1121,7 +1113,6 @@ impl PersonaChatSession {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
let rows = dao
.update_persona_chat(
&cx,
user_id,
conversation_id,
&json,
@@ -1195,10 +1186,9 @@ impl PersonaChatSession {
node_id: Option<u64>,
viewing_leaf: Option<u64>,
) -> Result<(Vec<crate::ai::llm_client::BranchLeafInfo>, u64)> {
let cx = opentelemetry::Context::current();
let row = {
let mut dao = self.persona_dao.lock().expect("persona_dao poisoned");
dao.get_persona_chat(&cx, user_id, conversation_id)
dao.get_persona_chat(user_id, conversation_id)
.map_err(|e| anyhow!("failed to load persona chat: {e:?}"))?
};
let row = row.ok_or_else(|| anyhow!("conversation not found"))?;