Tracing: LLM clients + streaming spans on tracing, drop cx threading (#140)
LlmClient methods no longer take an OpenTelemetry Context. llm.request is now a tracing span (otel.kind=client, llm.provider/operation/model) that parents on the caller's ambient span; traceparent injection reads the span's OTel context via OpenTelemetrySpanExt. The src/ai feature spans (handlers, insight generation, chat, persona chat, daily summaries, TTS) move to #[instrument] / info_span!, and every LLM-bearing tokio::spawn carries its parent span with .instrument(): the streaming chat turn (chat_turn_stream_inner), async chat / persona turns, background insight jobs and TTS jobs. Sub-spans in the agentic loops (ai.chat.loop, ai.agentic.loop) instrument each LLM / tool await. DAO calls keep their explicit Context (derived from the tracing span) until the DAO migration (#141). tests/otel_llm_spans.rs pins nesting + traceparent for the blocking and streaming paths across a spawn on a multi-thread runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a59afc62b
commit
1c508cdabd
20 files changed
+973
-1030
No files matched your search
+29
-72
@@ -1,14 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
|
||||
use opentelemetry::trace::Status;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::time::sleep;
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
use crate::ai::{EMBEDDING_MODEL, OllamaClient, SmsApiClient, SmsMessage, user_display_name};
|
||||
use crate::database::{DailySummaryDao, InsertDailySummary};
|
||||
use crate::otel::global_tracer;
|
||||
|
||||
/// Strip boilerplate prefixes and common phrases from summaries before embedding.
|
||||
/// This improves embedding diversity by removing structural similarity.
|
||||
@@ -155,6 +154,7 @@ pub fn strip_summary_boilerplate(summary: &str) -> String {
|
||||
|
||||
/// Generate and embed daily conversation summaries for a date range
|
||||
/// Default: August 2024 ±30 days (July 1 - September 30, 2024)
|
||||
#[tracing::instrument(name = "ai.daily_summary.generate_batch", skip_all)]
|
||||
pub async fn generate_daily_summaries(
|
||||
contact: &str,
|
||||
start_date: Option<NaiveDate>,
|
||||
@@ -163,30 +163,16 @@ pub async fn generate_daily_summaries(
|
||||
sms_client: &SmsApiClient,
|
||||
summary_dao: Arc<Mutex<Box<dyn DailySummaryDao>>>,
|
||||
) -> Result<()> {
|
||||
let tracer = global_tracer();
|
||||
|
||||
// Get current context (empty in background task) and start span with it
|
||||
let current_cx = opentelemetry::Context::current();
|
||||
let mut span = tracer.start_with_context("ai.daily_summary.generate_batch", ¤t_cx);
|
||||
span.set_attribute(KeyValue::new("contact", contact.to_string()));
|
||||
|
||||
// Create context with this span for child operations
|
||||
let parent_cx = current_cx.with_span(span);
|
||||
let span = tracing::Span::current();
|
||||
span.set_attribute("contact", contact.to_string());
|
||||
|
||||
// Default to August 2024 ±30 days
|
||||
let start = start_date.unwrap_or_else(|| NaiveDate::from_ymd_opt(2024, 7, 1).unwrap());
|
||||
let end = end_date.unwrap_or_else(|| NaiveDate::from_ymd_opt(2024, 9, 30).unwrap());
|
||||
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("start_date", start.to_string()));
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("end_date", end.to_string()));
|
||||
parent_cx.span().set_attribute(KeyValue::new(
|
||||
"date_range_days",
|
||||
(end - start).num_days() + 1,
|
||||
));
|
||||
span.set_attribute("start_date", start.to_string());
|
||||
span.set_attribute("end_date", end.to_string());
|
||||
span.set_attribute("date_range_days", (end - start).num_days() + 1);
|
||||
|
||||
log::info!("========================================");
|
||||
log::info!("Starting daily summary generation for {}", contact);
|
||||
@@ -267,15 +253,8 @@ pub async fn generate_daily_summaries(
|
||||
}
|
||||
|
||||
// Generate summary for this day
|
||||
match generate_and_store_daily_summary(
|
||||
&parent_cx,
|
||||
date,
|
||||
contact,
|
||||
messages,
|
||||
ollama,
|
||||
summary_dao.clone(),
|
||||
)
|
||||
.await
|
||||
match generate_and_store_daily_summary(date, contact, messages, ollama, summary_dao.clone())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
processed += 1;
|
||||
@@ -322,55 +301,38 @@ pub async fn generate_daily_summaries(
|
||||
log::info!("========================================");
|
||||
|
||||
// Record final metrics in span
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("days_processed", processed as i64));
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("days_skipped", skipped as i64));
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("days_failed", failed as i64));
|
||||
parent_cx
|
||||
.span()
|
||||
.set_attribute(KeyValue::new("total_days", total_days as i64));
|
||||
span.set_attribute("days_processed", processed as i64);
|
||||
span.set_attribute("days_skipped", skipped as i64);
|
||||
span.set_attribute("days_failed", failed as i64);
|
||||
span.set_attribute("total_days", total_days as i64);
|
||||
|
||||
if failed > 0 {
|
||||
parent_cx
|
||||
.span()
|
||||
.set_status(Status::error(format!("{} days failed to process", failed)));
|
||||
span.set_status(Status::error(format!("{} days failed to process", failed)));
|
||||
} else {
|
||||
parent_cx.span().set_status(Status::Ok);
|
||||
span.set_status(Status::Ok);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate and store a single day's summary
|
||||
#[tracing::instrument(name = "ai.daily_summary.generate_single", skip_all)]
|
||||
async fn generate_and_store_daily_summary(
|
||||
parent_cx: &opentelemetry::Context,
|
||||
date: &NaiveDate,
|
||||
contact: &str,
|
||||
messages: &[SmsMessage],
|
||||
ollama: &OllamaClient,
|
||||
summary_dao: Arc<Mutex<Box<dyn DailySummaryDao>>>,
|
||||
) -> Result<()> {
|
||||
let tracer = global_tracer();
|
||||
let mut span = tracer.start_with_context("ai.daily_summary.generate_single", parent_cx);
|
||||
span.set_attribute(KeyValue::new("date", date.to_string()));
|
||||
span.set_attribute(KeyValue::new("contact", contact.to_string()));
|
||||
span.set_attribute(KeyValue::new("message_count", messages.len() as i64));
|
||||
// Parent context for the child llm.request spans; the span itself stays
|
||||
// borrowed here for the attribute updates below.
|
||||
let llm_cx =
|
||||
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
||||
let span = tracing::Span::current();
|
||||
span.set_attribute("date", date.to_string());
|
||||
span.set_attribute("contact", contact.to_string());
|
||||
span.set_attribute("message_count", messages.len() as i64);
|
||||
|
||||
let (prompt, system_prompt) = build_daily_summary_prompt(contact, date, messages);
|
||||
|
||||
// Generate summary with LLM
|
||||
let summary = ollama
|
||||
.generate(&llm_cx, &prompt, Some(system_prompt))
|
||||
.await?;
|
||||
let summary = ollama.generate(&prompt, Some(system_prompt)).await?;
|
||||
|
||||
log::debug!(
|
||||
"Generated summary for {}: {}",
|
||||
@@ -378,7 +340,7 @@ async fn generate_and_store_daily_summary(
|
||||
summary.chars().take(100).collect::<String>()
|
||||
);
|
||||
|
||||
span.set_attribute(KeyValue::new("summary_length", summary.len() as i64));
|
||||
span.set_attribute("summary_length", summary.len() as i64);
|
||||
|
||||
// Strip boilerplate before embedding to improve vector diversity
|
||||
let stripped_summary = strip_summary_boilerplate(&summary);
|
||||
@@ -388,14 +350,9 @@ async fn generate_and_store_daily_summary(
|
||||
);
|
||||
|
||||
// Embed the stripped summary (store original summary in DB)
|
||||
let embedding = ollama
|
||||
.generate_embedding(&llm_cx, &stripped_summary)
|
||||
.await?;
|
||||
let embedding = ollama.generate_embedding(&stripped_summary).await?;
|
||||
|
||||
span.set_attribute(KeyValue::new(
|
||||
"embedding_dimensions",
|
||||
embedding.len() as i64,
|
||||
));
|
||||
span.set_attribute("embedding_dimensions", embedding.len() as i64);
|
||||
|
||||
// Store in database
|
||||
let insert = InsertDailySummary {
|
||||
@@ -408,8 +365,8 @@ async fn generate_and_store_daily_summary(
|
||||
model_version: EMBEDDING_MODEL.to_string(),
|
||||
};
|
||||
|
||||
// Create context from current span for DB operation
|
||||
let child_cx = opentelemetry::Context::current_with_span(span);
|
||||
// DAO calls still take an explicit OTel context (#141).
|
||||
let child_cx = span.context();
|
||||
|
||||
let mut dao = summary_dao.lock().expect("Unable to lock DailySummaryDao");
|
||||
let result = dao
|
||||
@@ -417,8 +374,8 @@ async fn generate_and_store_daily_summary(
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store summary: {:?}", e));
|
||||
|
||||
match &result {
|
||||
Ok(_) => child_cx.span().set_status(Status::Ok),
|
||||
Err(e) => child_cx.span().set_status(Status::error(e.to_string())),
|
||||
Ok(_) => span.set_status(Status::Ok),
|
||||
Err(e) => span.set_status(Status::error(e.to_string())),
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
Reference in new issue
Block a user