use super::events::{
ChatEvent, ChatExchange, LlmEvent, LlmInteractionType, StreamEvent, StreamEventType,
TokenUsage, ToolEvent, TracingEvent,
};
use super::{SpanId, TraceId};
use crate::types::{ChatMessage, ChatResponse, Tool, ToolCall};
use serde_json::Value;
use std::collections::HashMap;
use std::time::{Duration, Instant, SystemTime};
use tracing::{debug, info, warn};
pub struct ExchangeParams {
pub input: Vec<ChatMessage>,
pub output: Option<ChatResponse>,
pub duration: Duration,
pub streaming: bool,
pub tools_used: Vec<String>,
pub token_usage: Option<TokenUsage>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct LlmTracer {
provider: String,
model: Option<String>,
include_content: bool,
max_content_length: usize,
}
impl LlmTracer {
pub fn new(provider: String, model: Option<String>) -> Self {
Self {
provider,
model,
include_content: true,
max_content_length: 1000,
}
}
pub fn with_content_settings(mut self, include: bool, max_length: usize) -> Self {
self.include_content = include;
self.max_content_length = max_length;
self
}
pub fn start_chat(
&self,
trace_id: TraceId,
messages: &[ChatMessage],
tools: Option<&[Tool]>,
parameters: HashMap<String, Value>,
) -> ChatContext {
let span_id = SpanId::new();
let start_time = Instant::now();
info!(
trace_id = %trace_id,
span_id = %span_id,
provider = %self.provider,
model = ?self.model,
message_count = messages.len(),
tool_count = tools.map(|t| t.len()).unwrap_or(0),
"LLM chat interaction started"
);
if self.include_content {
debug!(
trace_id = %trace_id,
span_id = %span_id,
messages = ?self.truncate_messages(messages),
tools = ?tools,
parameters = ?parameters,
"LLM chat details"
);
}
ChatContext {
trace_id,
span_id,
start_time,
provider: self.provider.clone(),
model: self.model.clone(),
input_messages: messages.to_vec(),
tools: tools.map(|t| t.to_vec()),
parameters,
response: None,
error: None,
}
}
pub fn end_chat(
&self,
mut context: ChatContext,
result: Result<ChatResponse, crate::error::LlmError>,
) -> ChatContext {
let duration = context.start_time.elapsed();
match result {
Ok(response) => {
context.response = Some(response.clone());
info!(
trace_id = %context.trace_id,
span_id = %context.span_id,
duration_ms = duration.as_millis(),
"LLM chat interaction completed successfully"
);
if self.include_content {
debug!(
trace_id = %context.trace_id,
span_id = %context.span_id,
response = ?self.truncate_response(&response),
"LLM chat response"
);
}
}
Err(error) => {
context.error = Some(error.to_string());
warn!(
trace_id = %context.trace_id,
span_id = %context.span_id,
error = %error,
duration_ms = duration.as_millis(),
"LLM chat interaction failed"
);
}
}
context
}
pub fn create_chat_event(&self, context: &ChatContext) -> TracingEvent {
let duration = context.start_time.elapsed();
TracingEvent::Llm(LlmEvent {
timestamp: SystemTime::now(),
provider: context.provider.clone(),
model: context.model.clone().unwrap_or_default(),
interaction_type: LlmInteractionType::Chat,
input_messages: if self.include_content {
self.truncate_messages(&context.input_messages)
} else {
vec![]
},
tools: context.tools.clone(),
response: if self.include_content {
context
.response
.as_ref()
.map(|r| self.truncate_response(r).clone())
} else {
None
},
duration: Some(duration),
token_usage: context
.response
.as_ref()
.and_then(|r| self.extract_token_usage(r)),
parameters: context.parameters.clone(),
error: context.error.clone(),
})
}
fn truncate_messages(&self, messages: &[ChatMessage]) -> Vec<ChatMessage> {
messages
.iter()
.map(|msg| self.truncate_message(msg))
.collect()
}
fn truncate_message(&self, message: &ChatMessage) -> ChatMessage {
message.clone()
}
fn truncate_response(&self, response: &ChatResponse) -> ChatResponse {
response.clone()
}
fn extract_token_usage(&self, _response: &ChatResponse) -> Option<TokenUsage> {
None
}
}
#[derive(Debug, Clone)]
pub struct ChatContext {
pub trace_id: TraceId,
pub span_id: SpanId,
pub start_time: Instant,
pub provider: String,
pub model: Option<String>,
pub input_messages: Vec<ChatMessage>,
pub tools: Option<Vec<Tool>>,
pub parameters: HashMap<String, Value>,
pub response: Option<ChatResponse>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StreamTracer {
trace_id: TraceId,
span_id: SpanId,
cumulative_size: usize,
position: u64,
}
impl StreamTracer {
pub fn new(trace_id: TraceId) -> Self {
Self {
trace_id,
span_id: SpanId::new(),
cumulative_size: 0,
position: 0,
}
}
pub fn record_event(
&mut self,
event_type: StreamEventType,
data: String,
is_final: bool,
) -> TracingEvent {
let chunk_size = data.len();
self.cumulative_size += chunk_size;
self.position += 1;
debug!(
trace_id = %self.trace_id,
span_id = %self.span_id,
event_type = ?event_type,
chunk_size = chunk_size,
cumulative_size = self.cumulative_size,
position = self.position,
is_final = is_final,
"Stream event recorded"
);
TracingEvent::Stream(StreamEvent {
timestamp: SystemTime::now(),
event_type,
data,
chunk_size,
cumulative_size: self.cumulative_size,
position: self.position,
is_final,
})
}
}
#[derive(Debug, Clone)]
pub struct ToolTracer {
trace_id: TraceId,
include_details: bool,
}
impl ToolTracer {
pub fn new(trace_id: TraceId, include_details: bool) -> Self {
Self {
trace_id,
include_details,
}
}
pub fn record_tool_call(
&self,
tool_call: &ToolCall,
result: Option<String>,
duration: Option<Duration>,
error: Option<String>,
) -> TracingEvent {
let span_id = SpanId::new();
info!(
trace_id = %self.trace_id,
span_id = %span_id,
tool_name = ?tool_call.function.as_ref().map(|f| &f.name),
duration_ms = duration.map(|d| d.as_millis()),
success = error.is_none(),
"Tool call recorded"
);
let parameters = if self.include_details {
tool_call
.function
.as_ref()
.and_then(|f| serde_json::from_str(&f.arguments).ok())
.unwrap_or_default()
} else {
HashMap::new()
};
TracingEvent::Tool(ToolEvent {
timestamp: SystemTime::now(),
tool_call: tool_call.clone(),
result: if self.include_details { result } else { None },
duration,
error,
parameters,
})
}
}
#[derive(Debug, Clone)]
pub struct ChatTracer {
session_id: String,
include_details: bool,
}
impl ChatTracer {
pub fn new(session_id: String, include_details: bool) -> Self {
Self {
session_id,
include_details,
}
}
pub fn record_exchange(&self, params: ExchangeParams) -> TracingEvent {
info!(
session_id = %self.session_id,
duration_ms = params.duration.as_millis(),
streaming = params.streaming,
tools_count = params.tools_used.len(),
success = params.error.is_none(),
"Chat exchange recorded"
);
TracingEvent::Chat(ChatEvent {
timestamp: SystemTime::now(),
session_id: self.session_id.clone(),
exchange: ChatExchange {
input: if self.include_details {
params.input
} else {
vec![]
},
output: if self.include_details {
params.output
} else {
None
},
streaming: params.streaming,
tool_calls_count: params.tools_used.len() as u32,
},
duration: params.duration,
token_usage: params.token_usage,
tools_used: params.tools_used,
error: params.error,
})
}
}