170 lines
5.5 KiB
Rust
170 lines
5.5 KiB
Rust
use super::traits::{Observer, ObserverEvent, ObserverMetric};
|
|
use tracing::info;
|
|
|
|
/// Log-based observer — uses tracing, zero external deps
|
|
pub struct LogObserver;
|
|
|
|
impl LogObserver {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl Observer for LogObserver {
|
|
fn record_event(&self, event: &ObserverEvent) {
|
|
match event {
|
|
ObserverEvent::AgentStart { provider, model } => {
|
|
info!(provider = %provider, model = %model, "agent.start");
|
|
}
|
|
ObserverEvent::LlmRequest {
|
|
provider,
|
|
model,
|
|
messages_count,
|
|
} => {
|
|
info!(
|
|
provider = %provider,
|
|
model = %model,
|
|
messages_count = messages_count,
|
|
"llm.request"
|
|
);
|
|
}
|
|
ObserverEvent::LlmResponse {
|
|
provider,
|
|
model,
|
|
duration,
|
|
success,
|
|
error_message,
|
|
} => {
|
|
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
|
|
info!(
|
|
provider = %provider,
|
|
model = %model,
|
|
duration_ms = ms,
|
|
success = success,
|
|
error = ?error_message,
|
|
"llm.response"
|
|
);
|
|
}
|
|
ObserverEvent::AgentEnd {
|
|
duration,
|
|
tokens_used,
|
|
} => {
|
|
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
|
|
info!(duration_ms = ms, tokens = ?tokens_used, "agent.end");
|
|
}
|
|
ObserverEvent::ToolCallStart { tool } => {
|
|
info!(tool = %tool, "tool.start");
|
|
}
|
|
ObserverEvent::ToolCall {
|
|
tool,
|
|
duration,
|
|
success,
|
|
} => {
|
|
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
|
|
info!(tool = %tool, duration_ms = ms, success = success, "tool.call");
|
|
}
|
|
ObserverEvent::TurnComplete => {
|
|
info!("turn.complete");
|
|
}
|
|
ObserverEvent::ChannelMessage { channel, direction } => {
|
|
info!(channel = %channel, direction = %direction, "channel.message");
|
|
}
|
|
ObserverEvent::HeartbeatTick => {
|
|
info!("heartbeat.tick");
|
|
}
|
|
ObserverEvent::Error { component, message } => {
|
|
info!(component = %component, error = %message, "error");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn record_metric(&self, metric: &ObserverMetric) {
|
|
match metric {
|
|
ObserverMetric::RequestLatency(d) => {
|
|
let ms = u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
|
|
info!(latency_ms = ms, "metric.request_latency");
|
|
}
|
|
ObserverMetric::TokensUsed(t) => {
|
|
info!(tokens = t, "metric.tokens_used");
|
|
}
|
|
ObserverMetric::ActiveSessions(s) => {
|
|
info!(sessions = s, "metric.active_sessions");
|
|
}
|
|
ObserverMetric::QueueDepth(d) => {
|
|
info!(depth = d, "metric.queue_depth");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
"log"
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::time::Duration;
|
|
|
|
#[test]
|
|
fn log_observer_name() {
|
|
assert_eq!(LogObserver::new().name(), "log");
|
|
}
|
|
|
|
#[test]
|
|
fn log_observer_all_events_no_panic() {
|
|
let obs = LogObserver::new();
|
|
obs.record_event(&ObserverEvent::AgentStart {
|
|
provider: "openrouter".into(),
|
|
model: "claude-sonnet".into(),
|
|
});
|
|
obs.record_event(&ObserverEvent::LlmRequest {
|
|
provider: "openrouter".into(),
|
|
model: "claude-sonnet".into(),
|
|
messages_count: 2,
|
|
});
|
|
obs.record_event(&ObserverEvent::LlmResponse {
|
|
provider: "openrouter".into(),
|
|
model: "claude-sonnet".into(),
|
|
duration: Duration::from_millis(250),
|
|
success: true,
|
|
error_message: None,
|
|
});
|
|
obs.record_event(&ObserverEvent::AgentEnd {
|
|
duration: Duration::from_millis(500),
|
|
tokens_used: Some(100),
|
|
});
|
|
obs.record_event(&ObserverEvent::AgentEnd {
|
|
duration: Duration::ZERO,
|
|
tokens_used: None,
|
|
});
|
|
obs.record_event(&ObserverEvent::ToolCallStart {
|
|
tool: "shell".into(),
|
|
});
|
|
obs.record_event(&ObserverEvent::ToolCall {
|
|
tool: "shell".into(),
|
|
duration: Duration::from_millis(10),
|
|
success: false,
|
|
});
|
|
obs.record_event(&ObserverEvent::TurnComplete);
|
|
obs.record_event(&ObserverEvent::ChannelMessage {
|
|
channel: "telegram".into(),
|
|
direction: "outbound".into(),
|
|
});
|
|
obs.record_event(&ObserverEvent::HeartbeatTick);
|
|
obs.record_event(&ObserverEvent::Error {
|
|
component: "provider".into(),
|
|
message: "timeout".into(),
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn log_observer_all_metrics_no_panic() {
|
|
let obs = LogObserver::new();
|
|
obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_secs(2)));
|
|
obs.record_metric(&ObserverMetric::TokensUsed(0));
|
|
obs.record_metric(&ObserverMetric::TokensUsed(u64::MAX));
|
|
obs.record_metric(&ObserverMetric::ActiveSessions(1));
|
|
obs.record_metric(&ObserverMetric::QueueDepth(999));
|
|
}
|
|
}
|