use std::collections::HashMap;
use std::fmt;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Info,
Warning,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Warning => write!(f, "warning"),
Self::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone)]
pub enum DiagnosticEvent {
AgentStarted {
agent_id: String,
timestamp: SystemTime,
},
AgentCompleted {
agent_id: String,
duration: Duration,
success: bool,
timestamp: SystemTime,
},
ToolCalled {
agent_id: String,
tool_name: String,
duration: Duration,
success: bool,
timestamp: SystemTime,
},
ErrorOccurred {
agent_id: String,
message: String,
severity: Severity,
timestamp: SystemTime,
},
ThresholdExceeded {
metric_name: String,
threshold: f64,
actual: f64,
timestamp: SystemTime,
},
MemoryPressure {
agent_id: String,
usage_bytes: u64,
limit_bytes: u64,
timestamp: SystemTime,
},
MiddlewareExecuted {
agent_id: String,
middleware_name: String,
duration: Duration,
timestamp: SystemTime,
},
LlmCallCompleted {
agent_id: String,
model: String,
duration: Duration,
timestamp: SystemTime,
},
}
impl DiagnosticEvent {
pub fn agent_id(&self) -> Option<&str> {
match self {
Self::AgentStarted { agent_id, .. }
| Self::AgentCompleted { agent_id, .. }
| Self::ToolCalled { agent_id, .. }
| Self::ErrorOccurred { agent_id, .. }
| Self::MemoryPressure { agent_id, .. }
| Self::MiddlewareExecuted { agent_id, .. }
| Self::LlmCallCompleted { agent_id, .. } => Some(agent_id),
Self::ThresholdExceeded { .. } => None,
}
}
pub fn timestamp(&self) -> SystemTime {
match self {
Self::AgentStarted { timestamp, .. }
| Self::AgentCompleted { timestamp, .. }
| Self::ToolCalled { timestamp, .. }
| Self::ErrorOccurred { timestamp, .. }
| Self::ThresholdExceeded { timestamp, .. }
| Self::MemoryPressure { timestamp, .. }
| Self::MiddlewareExecuted { timestamp, .. }
| Self::LlmCallCompleted { timestamp, .. } => *timestamp,
}
}
pub fn kind(&self) -> &'static str {
match self {
Self::AgentStarted { .. } => "agent_started",
Self::AgentCompleted { .. } => "agent_completed",
Self::ToolCalled { .. } => "tool_called",
Self::ErrorOccurred { .. } => "error_occurred",
Self::ThresholdExceeded { .. } => "threshold_exceeded",
Self::MemoryPressure { .. } => "memory_pressure",
Self::MiddlewareExecuted { .. } => "middleware_executed",
Self::LlmCallCompleted { .. } => "llm_call_completed",
}
}
}
pub trait DiagnosticSink {
fn receive(&mut self, event: DiagnosticEvent);
fn flush(&mut self) {}
}
#[derive(Debug, Default)]
pub struct InMemoryDiagnosticSink {
events: Vec<DiagnosticEvent>,
capacity: Option<usize>,
}
impl InMemoryDiagnosticSink {
pub fn new() -> Self {
Self {
events: Vec::new(),
capacity: None,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
events: Vec::with_capacity(capacity),
capacity: Some(capacity),
}
}
pub fn events(&self) -> &[DiagnosticEvent] {
&self.events
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn filter_by_kind(&self, kind: &str) -> Vec<&DiagnosticEvent> {
self.events.iter().filter(|e| e.kind() == kind).collect()
}
pub fn filter_by_agent(&self, agent_id: &str) -> Vec<&DiagnosticEvent> {
self.events
.iter()
.filter(|e| e.agent_id() == Some(agent_id))
.collect()
}
pub fn count_by_kind(&self, kind: &str) -> usize {
self.events.iter().filter(|e| e.kind() == kind).count()
}
pub fn errors(&self) -> Vec<&DiagnosticEvent> {
self.filter_by_kind("error_occurred")
}
pub fn clear(&mut self) {
self.events.clear();
}
}
impl DiagnosticSink for InMemoryDiagnosticSink {
fn receive(&mut self, event: DiagnosticEvent) {
if let Some(cap) = self.capacity {
if self.events.len() >= cap && cap > 0 {
self.events.remove(0);
}
}
self.events.push(event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProfileCategory {
Llm,
Tool,
Middleware,
Other,
}
impl fmt::Display for ProfileCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Llm => write!(f, "llm"),
Self::Tool => write!(f, "tool"),
Self::Middleware => write!(f, "middleware"),
Self::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct ProfileEntry {
category: ProfileCategory,
duration: Duration,
label: String,
}
#[derive(Debug)]
pub struct AgentProfiler {
agent_id: String,
entries: Vec<ProfileEntry>,
wall_start: Option<Instant>,
wall_end: Option<Instant>,
active_timers: HashMap<String, (ProfileCategory, Instant)>,
}
impl AgentProfiler {
pub fn new(agent_id: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
entries: Vec::new(),
wall_start: None,
wall_end: None,
active_timers: HashMap::new(),
}
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn start(&mut self) {
self.wall_start = Some(Instant::now());
}
pub fn stop(&mut self) {
self.wall_end = Some(Instant::now());
}
pub fn record(
&mut self,
category: ProfileCategory,
label: impl Into<String>,
duration: Duration,
) {
self.entries.push(ProfileEntry {
category,
duration,
label: label.into(),
});
}
pub fn start_timer(&mut self, category: ProfileCategory, label: impl Into<String>) -> String {
let label = label.into();
let key = format!("{}:{}", category, label);
self.active_timers
.insert(key.clone(), (category, Instant::now()));
key
}
pub fn stop_timer(&mut self, key: &str) -> Option<Duration> {
if let Some((category, start)) = self.active_timers.remove(key) {
let duration = start.elapsed();
let label = key.split_once(':').map(|x| x.1).unwrap_or(key).to_string();
self.entries.push(ProfileEntry {
category,
duration,
label,
});
Some(duration)
} else {
None
}
}
pub fn wall_duration(&self) -> Option<Duration> {
match (self.wall_start, self.wall_end) {
(Some(start), Some(end)) => Some(end.duration_since(start)),
(Some(start), None) => Some(start.elapsed()),
_ => None,
}
}
pub fn total_for_category(&self, category: ProfileCategory) -> Duration {
self.entries
.iter()
.filter(|e| e.category == category)
.map(|e| e.duration)
.sum()
}
pub fn count_for_category(&self, category: ProfileCategory) -> usize {
self.entries
.iter()
.filter(|e| e.category == category)
.count()
}
pub fn report(&self) -> ProfileReport {
let wall = self.wall_duration().unwrap_or(Duration::ZERO);
let wall_secs = wall.as_secs_f64();
let mut categories = HashMap::new();
for cat in &[
ProfileCategory::Llm,
ProfileCategory::Tool,
ProfileCategory::Middleware,
ProfileCategory::Other,
] {
let total = self.total_for_category(*cat);
let count = self.count_for_category(*cat);
let pct = if wall_secs > 0.0 {
(total.as_secs_f64() / wall_secs) * 100.0
} else {
0.0
};
categories.insert(
*cat,
CategoryBreakdown {
total_duration: total,
call_count: count,
percentage: pct,
},
);
}
ProfileReport {
agent_id: self.agent_id.clone(),
wall_duration: wall,
categories,
entry_count: self.entries.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct CategoryBreakdown {
pub total_duration: Duration,
pub call_count: usize,
pub percentage: f64,
}
#[derive(Debug)]
pub struct ProfileReport {
pub agent_id: String,
pub wall_duration: Duration,
pub categories: HashMap<ProfileCategory, CategoryBreakdown>,
pub entry_count: usize,
}
impl ProfileReport {
pub fn get(&self, category: ProfileCategory) -> Option<&CategoryBreakdown> {
self.categories.get(&category)
}
pub fn to_summary(&self) -> String {
let mut out = String::new();
out.push_str(&format!("Profile Report for agent '{}'\n", self.agent_id));
out.push_str(&format!(
"Wall duration: {:.3}ms\n",
self.wall_duration.as_secs_f64() * 1000.0
));
out.push_str(&format!("Total entries: {}\n", self.entry_count));
for cat in &[
ProfileCategory::Llm,
ProfileCategory::Tool,
ProfileCategory::Middleware,
ProfileCategory::Other,
] {
if let Some(bd) = self.categories.get(cat) {
out.push_str(&format!(
" {}: {:.3}ms ({:.1}%), {} calls\n",
cat,
bd.total_duration.as_secs_f64() * 1000.0,
bd.percentage,
bd.call_count,
));
}
}
out
}
}
#[derive(Debug, Clone)]
pub struct Alert {
pub rule_name: String,
pub severity: Severity,
pub message: String,
pub timestamp: SystemTime,
}
impl Alert {
pub fn new(
rule_name: impl Into<String>,
severity: Severity,
message: impl Into<String>,
) -> Self {
Self {
rule_name: rule_name.into(),
severity,
message: message.into(),
timestamp: SystemTime::now(),
}
}
}
impl fmt::Display for Alert {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}: {}",
self.severity, self.rule_name, self.message
)
}
}
#[derive(Debug, Clone)]
pub enum AlertCondition {
GreaterThan(f64),
LessThan(f64),
Equals(f64),
}
impl AlertCondition {
pub fn evaluate(&self, actual: f64) -> bool {
match self {
Self::GreaterThan(threshold) => actual > *threshold,
Self::LessThan(threshold) => actual < *threshold,
Self::Equals(expected) => (actual - expected).abs() < f64::EPSILON,
}
}
pub fn threshold(&self) -> f64 {
match self {
Self::GreaterThan(v) | Self::LessThan(v) | Self::Equals(v) => *v,
}
}
}
#[derive(Debug, Clone)]
pub struct AlertRule {
pub name: String,
pub metric_name: String,
pub condition: AlertCondition,
pub severity: Severity,
pub message_template: String,
pub enabled: bool,
}
impl AlertRule {
pub fn new(
name: impl Into<String>,
metric_name: impl Into<String>,
condition: AlertCondition,
severity: Severity,
message_template: impl Into<String>,
) -> Self {
Self {
name: name.into(),
metric_name: metric_name.into(),
condition,
severity,
message_template: message_template.into(),
enabled: true,
}
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn evaluate(&self, value: f64) -> Option<Alert> {
if !self.enabled {
return None;
}
if self.condition.evaluate(value) {
let message = self
.message_template
.replace("{value}", &format!("{:.4}", value))
.replace("{threshold}", &format!("{:.4}", self.condition.threshold()));
Some(Alert::new(&self.name, self.severity, message))
} else {
None
}
}
}
#[derive(Debug, Default)]
pub struct AlertManager {
rules: Vec<AlertRule>,
fired_alerts: Vec<Alert>,
}
impl AlertManager {
pub fn new() -> Self {
Self {
rules: Vec::new(),
fired_alerts: Vec::new(),
}
}
pub fn add_rule(&mut self, rule: AlertRule) {
self.rules.push(rule);
}
pub fn remove_rule(&mut self, name: &str) -> bool {
let before = self.rules.len();
self.rules.retain(|r| r.name != name);
self.rules.len() < before
}
pub fn rules(&self) -> &[AlertRule] {
&self.rules
}
pub fn evaluate(&mut self, metrics: &HashMap<String, f64>) -> Vec<Alert> {
let mut new_alerts = Vec::new();
for rule in &self.rules {
if let Some(value) = metrics.get(&rule.metric_name) {
if let Some(alert) = rule.evaluate(*value) {
new_alerts.push(alert);
}
}
}
self.fired_alerts.extend(new_alerts.clone());
new_alerts
}
pub fn evaluate_metric(&mut self, metric_name: &str, value: f64) -> Vec<Alert> {
let mut metrics = HashMap::new();
metrics.insert(metric_name.to_string(), value);
self.evaluate(&metrics)
}
pub fn fired_alerts(&self) -> &[Alert] {
&self.fired_alerts
}
pub fn alerts_by_severity(&self, severity: Severity) -> Vec<&Alert> {
self.fired_alerts
.iter()
.filter(|a| a.severity == severity)
.collect()
}
pub fn clear_alerts(&mut self) {
self.fired_alerts.clear();
}
pub fn alert_count(&self) -> usize {
self.fired_alerts.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Informational,
Degraded,
Unhealthy,
}
impl fmt::Display for HealthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Healthy => write!(f, "healthy"),
Self::Informational => write!(f, "informational"),
Self::Degraded => write!(f, "degraded"),
Self::Unhealthy => write!(f, "unhealthy"),
}
}
}
#[derive(Debug)]
pub struct DiagnosticDashboard {
sink: InMemoryDiagnosticSink,
profilers: HashMap<String, AgentProfiler>,
alert_manager: AlertManager,
}
impl DiagnosticDashboard {
pub fn new() -> Self {
Self {
sink: InMemoryDiagnosticSink::new(),
profilers: HashMap::new(),
alert_manager: AlertManager::new(),
}
}
pub fn sink(&self) -> &InMemoryDiagnosticSink {
&self.sink
}
pub fn sink_mut(&mut self) -> &mut InMemoryDiagnosticSink {
&mut self.sink
}
pub fn emit(&mut self, event: DiagnosticEvent) {
self.sink.receive(event);
}
pub fn profiler(&mut self, agent_id: &str) -> &mut AgentProfiler {
self.profilers
.entry(agent_id.to_string())
.or_insert_with(|| AgentProfiler::new(agent_id))
}
pub fn get_profiler(&self, agent_id: &str) -> Option<&AgentProfiler> {
self.profilers.get(agent_id)
}
pub fn alert_manager_mut(&mut self) -> &mut AlertManager {
&mut self.alert_manager
}
pub fn alert_manager(&self) -> &AlertManager {
&self.alert_manager
}
pub fn add_alert_rule(&mut self, rule: AlertRule) {
self.alert_manager.add_rule(rule);
}
pub fn evaluate_alerts(&mut self, metrics: &HashMap<String, f64>) -> Vec<Alert> {
self.alert_manager.evaluate(metrics)
}
pub fn health_status(&self) -> HealthStatus {
let alerts = self.alert_manager.fired_alerts();
if alerts.is_empty() {
return HealthStatus::Healthy;
}
let max_severity = alerts.iter().map(|a| a.severity).max().unwrap();
match max_severity {
Severity::Info => HealthStatus::Informational,
Severity::Warning => HealthStatus::Degraded,
Severity::Critical => HealthStatus::Unhealthy,
}
}
pub fn all_reports(&self) -> Vec<ProfileReport> {
self.profilers.values().map(|p| p.report()).collect()
}
pub fn summary(&self) -> String {
let mut out = String::new();
out.push_str("=== Diagnostic Dashboard ===\n\n");
out.push_str(&format!("Health: {}\n", self.health_status()));
out.push_str(&format!("Events: {}\n", self.sink.len()));
out.push_str(&format!("Agents profiled: {}\n", self.profilers.len()));
out.push_str(&format!(
"Alerts fired: {}\n",
self.alert_manager.alert_count()
));
out.push_str(&format!(
"Alert rules: {}\n",
self.alert_manager.rules().len()
));
if !self.alert_manager.fired_alerts().is_empty() {
out.push_str("\nActive Alerts:\n");
for alert in self.alert_manager.fired_alerts() {
out.push_str(&format!(" {}\n", alert));
}
}
for report in self.all_reports() {
out.push_str(&format!("\n{}", report.to_summary()));
}
out
}
pub fn event_count(&self) -> usize {
self.sink.len()
}
pub fn error_count(&self) -> usize {
self.sink.count_by_kind("error_occurred")
}
}
impl Default for DiagnosticDashboard {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_display() {
assert_eq!(Severity::Info.to_string(), "info");
assert_eq!(Severity::Warning.to_string(), "warning");
assert_eq!(Severity::Critical.to_string(), "critical");
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Critical);
assert!(Severity::Info < Severity::Critical);
}
#[test]
fn test_severity_equality() {
assert_eq!(Severity::Info, Severity::Info);
assert_ne!(Severity::Info, Severity::Warning);
}
#[test]
fn test_event_agent_started() {
let event = DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
};
assert_eq!(event.agent_id(), Some("a1"));
assert_eq!(event.kind(), "agent_started");
}
#[test]
fn test_event_agent_completed() {
let event = DiagnosticEvent::AgentCompleted {
agent_id: "a2".into(),
duration: Duration::from_secs(5),
success: true,
timestamp: SystemTime::now(),
};
assert_eq!(event.agent_id(), Some("a2"));
assert_eq!(event.kind(), "agent_completed");
}
#[test]
fn test_event_tool_called() {
let event = DiagnosticEvent::ToolCalled {
agent_id: "a1".into(),
tool_name: "search".into(),
duration: Duration::from_millis(100),
success: true,
timestamp: SystemTime::now(),
};
assert_eq!(event.kind(), "tool_called");
assert_eq!(event.agent_id(), Some("a1"));
}
#[test]
fn test_event_error_occurred() {
let event = DiagnosticEvent::ErrorOccurred {
agent_id: "a1".into(),
message: "timeout".into(),
severity: Severity::Critical,
timestamp: SystemTime::now(),
};
assert_eq!(event.kind(), "error_occurred");
}
#[test]
fn test_event_threshold_exceeded_no_agent() {
let event = DiagnosticEvent::ThresholdExceeded {
metric_name: "latency".into(),
threshold: 5.0,
actual: 7.0,
timestamp: SystemTime::now(),
};
assert_eq!(event.agent_id(), None);
assert_eq!(event.kind(), "threshold_exceeded");
}
#[test]
fn test_event_memory_pressure() {
let event = DiagnosticEvent::MemoryPressure {
agent_id: "a1".into(),
usage_bytes: 900,
limit_bytes: 1000,
timestamp: SystemTime::now(),
};
assert_eq!(event.kind(), "memory_pressure");
assert_eq!(event.agent_id(), Some("a1"));
}
#[test]
fn test_event_middleware_executed() {
let event = DiagnosticEvent::MiddlewareExecuted {
agent_id: "a1".into(),
middleware_name: "rate_limiter".into(),
duration: Duration::from_millis(2),
timestamp: SystemTime::now(),
};
assert_eq!(event.kind(), "middleware_executed");
}
#[test]
fn test_event_llm_call_completed() {
let event = DiagnosticEvent::LlmCallCompleted {
agent_id: "a1".into(),
model: "gpt-4".into(),
duration: Duration::from_millis(500),
timestamp: SystemTime::now(),
};
assert_eq!(event.kind(), "llm_call_completed");
}
#[test]
fn test_event_timestamp() {
let now = SystemTime::now();
let event = DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: now,
};
assert_eq!(event.timestamp(), now);
}
#[test]
fn test_sink_new_empty() {
let sink = InMemoryDiagnosticSink::new();
assert!(sink.is_empty());
assert_eq!(sink.len(), 0);
}
#[test]
fn test_sink_receive_event() {
let mut sink = InMemoryDiagnosticSink::new();
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
assert_eq!(sink.len(), 1);
assert!(!sink.is_empty());
}
#[test]
fn test_sink_capacity_eviction() {
let mut sink = InMemoryDiagnosticSink::with_capacity(2);
for i in 0..3 {
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: format!("a{}", i),
timestamp: SystemTime::now(),
});
}
assert_eq!(sink.len(), 2);
assert_eq!(sink.events()[0].agent_id(), Some("a1"));
assert_eq!(sink.events()[1].agent_id(), Some("a2"));
}
#[test]
fn test_sink_filter_by_kind() {
let mut sink = InMemoryDiagnosticSink::new();
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
sink.receive(DiagnosticEvent::ErrorOccurred {
agent_id: "a1".into(),
message: "fail".into(),
severity: Severity::Warning,
timestamp: SystemTime::now(),
});
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a2".into(),
timestamp: SystemTime::now(),
});
let started = sink.filter_by_kind("agent_started");
assert_eq!(started.len(), 2);
let errors = sink.filter_by_kind("error_occurred");
assert_eq!(errors.len(), 1);
}
#[test]
fn test_sink_filter_by_agent() {
let mut sink = InMemoryDiagnosticSink::new();
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a2".into(),
timestamp: SystemTime::now(),
});
sink.receive(DiagnosticEvent::ToolCalled {
agent_id: "a1".into(),
tool_name: "search".into(),
duration: Duration::from_millis(10),
success: true,
timestamp: SystemTime::now(),
});
let a1_events = sink.filter_by_agent("a1");
assert_eq!(a1_events.len(), 2);
let a2_events = sink.filter_by_agent("a2");
assert_eq!(a2_events.len(), 1);
}
#[test]
fn test_sink_count_by_kind() {
let mut sink = InMemoryDiagnosticSink::new();
for _ in 0..5 {
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
}
assert_eq!(sink.count_by_kind("agent_started"), 5);
assert_eq!(sink.count_by_kind("error_occurred"), 0);
}
#[test]
fn test_sink_errors() {
let mut sink = InMemoryDiagnosticSink::new();
sink.receive(DiagnosticEvent::ErrorOccurred {
agent_id: "a1".into(),
message: "bad".into(),
severity: Severity::Critical,
timestamp: SystemTime::now(),
});
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
assert_eq!(sink.errors().len(), 1);
}
#[test]
fn test_sink_clear() {
let mut sink = InMemoryDiagnosticSink::new();
sink.receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
assert_eq!(sink.len(), 1);
sink.clear();
assert!(sink.is_empty());
}
#[test]
fn test_sink_flush_is_noop() {
let mut sink = InMemoryDiagnosticSink::new();
sink.flush(); }
#[test]
fn test_profile_category_display() {
assert_eq!(ProfileCategory::Llm.to_string(), "llm");
assert_eq!(ProfileCategory::Tool.to_string(), "tool");
assert_eq!(ProfileCategory::Middleware.to_string(), "middleware");
assert_eq!(ProfileCategory::Other.to_string(), "other");
}
#[test]
fn test_profiler_new() {
let p = AgentProfiler::new("agent-1");
assert_eq!(p.agent_id(), "agent-1");
assert!(p.wall_duration().is_none());
}
#[test]
fn test_profiler_start_stop() {
let mut p = AgentProfiler::new("a1");
p.start();
std::thread::sleep(Duration::from_millis(5));
p.stop();
let dur = p.wall_duration().unwrap();
assert!(dur >= Duration::from_millis(5));
}
#[test]
fn test_profiler_record() {
let mut p = AgentProfiler::new("a1");
p.record(ProfileCategory::Llm, "call-1", Duration::from_millis(100));
p.record(ProfileCategory::Llm, "call-2", Duration::from_millis(200));
p.record(ProfileCategory::Tool, "search", Duration::from_millis(50));
assert_eq!(
p.total_for_category(ProfileCategory::Llm),
Duration::from_millis(300)
);
assert_eq!(p.count_for_category(ProfileCategory::Llm), 2);
assert_eq!(p.count_for_category(ProfileCategory::Tool), 1);
assert_eq!(p.count_for_category(ProfileCategory::Middleware), 0);
}
#[test]
fn test_profiler_timer() {
let mut p = AgentProfiler::new("a1");
let key = p.start_timer(ProfileCategory::Tool, "search");
std::thread::sleep(Duration::from_millis(5));
let dur = p.stop_timer(&key);
assert!(dur.is_some());
assert!(dur.unwrap() >= Duration::from_millis(5));
assert_eq!(p.count_for_category(ProfileCategory::Tool), 1);
}
#[test]
fn test_profiler_stop_timer_unknown_key() {
let mut p = AgentProfiler::new("a1");
assert!(p.stop_timer("nonexistent").is_none());
}
#[test]
fn test_profiler_wall_duration_while_running() {
let mut p = AgentProfiler::new("a1");
p.start();
let dur = p.wall_duration();
assert!(dur.is_some());
}
#[test]
fn test_profiler_report() {
let mut p = AgentProfiler::new("a1");
p.start();
p.record(ProfileCategory::Llm, "call", Duration::from_millis(100));
p.record(ProfileCategory::Tool, "tool", Duration::from_millis(50));
p.record(ProfileCategory::Middleware, "mw", Duration::from_millis(10));
p.stop();
let report = p.report();
assert_eq!(report.agent_id, "a1");
assert_eq!(report.entry_count, 3);
assert!(report.wall_duration >= Duration::from_millis(0));
let llm = report.get(ProfileCategory::Llm).unwrap();
assert_eq!(llm.call_count, 1);
assert_eq!(llm.total_duration, Duration::from_millis(100));
}
#[test]
fn test_profile_report_summary_format() {
let mut p = AgentProfiler::new("test-agent");
p.start();
p.record(ProfileCategory::Llm, "c", Duration::from_millis(50));
p.stop();
let report = p.report();
let summary = report.to_summary();
assert!(summary.contains("test-agent"));
assert!(summary.contains("llm"));
}
#[test]
fn test_profiler_report_zero_wall() {
let p = AgentProfiler::new("a1");
let report = p.report();
for bd in report.categories.values() {
assert_eq!(bd.percentage, 0.0);
}
}
#[test]
fn test_condition_greater_than() {
let cond = AlertCondition::GreaterThan(5.0);
assert!(cond.evaluate(6.0));
assert!(!cond.evaluate(5.0));
assert!(!cond.evaluate(4.0));
assert_eq!(cond.threshold(), 5.0);
}
#[test]
fn test_condition_less_than() {
let cond = AlertCondition::LessThan(3.0);
assert!(cond.evaluate(2.0));
assert!(!cond.evaluate(3.0));
assert!(!cond.evaluate(4.0));
}
#[test]
fn test_condition_equals() {
let cond = AlertCondition::Equals(10.0);
assert!(cond.evaluate(10.0));
assert!(!cond.evaluate(10.1));
}
#[test]
fn test_alert_rule_fires() {
let rule = AlertRule::new(
"high_latency",
"latency_ms",
AlertCondition::GreaterThan(5000.0),
Severity::Warning,
"Latency {value}ms exceeds {threshold}ms",
);
let alert = rule.evaluate(6000.0);
assert!(alert.is_some());
let alert = alert.unwrap();
assert_eq!(alert.rule_name, "high_latency");
assert_eq!(alert.severity, Severity::Warning);
assert!(alert.message.contains("6000"));
}
#[test]
fn test_alert_rule_does_not_fire() {
let rule = AlertRule::new(
"high_latency",
"latency_ms",
AlertCondition::GreaterThan(5000.0),
Severity::Warning,
"too slow",
);
assert!(rule.evaluate(3000.0).is_none());
}
#[test]
fn test_alert_rule_disabled() {
let mut rule = AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Critical,
"msg",
);
rule.disable();
assert!(rule.evaluate(100.0).is_none());
}
#[test]
fn test_alert_rule_enable_disable() {
let mut rule = AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Info,
"msg",
);
assert!(rule.enabled);
rule.disable();
assert!(!rule.enabled);
rule.enable();
assert!(rule.enabled);
}
#[test]
fn test_alert_display() {
let alert = Alert::new("rule1", Severity::Critical, "system overloaded");
let s = format!("{}", alert);
assert!(s.contains("critical"));
assert!(s.contains("rule1"));
assert!(s.contains("system overloaded"));
}
#[test]
fn test_alert_new() {
let alert = Alert::new("test", Severity::Info, "all good");
assert_eq!(alert.rule_name, "test");
assert_eq!(alert.severity, Severity::Info);
assert_eq!(alert.message, "all good");
}
#[test]
fn test_alert_manager_new() {
let am = AlertManager::new();
assert!(am.rules().is_empty());
assert!(am.fired_alerts().is_empty());
assert_eq!(am.alert_count(), 0);
}
#[test]
fn test_alert_manager_add_rule() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(10.0),
Severity::Warning,
"exceeded",
));
assert_eq!(am.rules().len(), 1);
}
#[test]
fn test_alert_manager_remove_rule() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(10.0),
Severity::Warning,
"exceeded",
));
assert!(am.remove_rule("r1"));
assert!(am.rules().is_empty());
assert!(!am.remove_rule("nonexistent"));
}
#[test]
fn test_alert_manager_evaluate() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"high_err",
"error_rate",
AlertCondition::GreaterThan(0.1),
Severity::Critical,
"Error rate {value} > {threshold}",
));
am.add_rule(AlertRule::new(
"low_throughput",
"throughput",
AlertCondition::LessThan(100.0),
Severity::Warning,
"Low throughput",
));
let mut metrics = HashMap::new();
metrics.insert("error_rate".to_string(), 0.15);
metrics.insert("throughput".to_string(), 200.0);
let alerts = am.evaluate(&metrics);
assert_eq!(alerts.len(), 1);
assert_eq!(alerts[0].rule_name, "high_err");
assert_eq!(am.alert_count(), 1);
}
#[test]
fn test_alert_manager_evaluate_metric() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"latency",
AlertCondition::GreaterThan(1000.0),
Severity::Warning,
"slow",
));
let alerts = am.evaluate_metric("latency", 2000.0);
assert_eq!(alerts.len(), 1);
}
#[test]
fn test_alert_manager_no_match() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"latency",
AlertCondition::GreaterThan(1000.0),
Severity::Warning,
"slow",
));
let alerts = am.evaluate_metric("latency", 500.0);
assert!(alerts.is_empty());
}
#[test]
fn test_alert_manager_alerts_by_severity() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Warning,
"w",
));
am.add_rule(AlertRule::new(
"r2",
"m2",
AlertCondition::GreaterThan(0.0),
Severity::Critical,
"c",
));
let mut metrics = HashMap::new();
metrics.insert("m1".to_string(), 1.0);
metrics.insert("m2".to_string(), 1.0);
am.evaluate(&metrics);
assert_eq!(am.alerts_by_severity(Severity::Warning).len(), 1);
assert_eq!(am.alerts_by_severity(Severity::Critical).len(), 1);
assert_eq!(am.alerts_by_severity(Severity::Info).len(), 0);
}
#[test]
fn test_alert_manager_clear() {
let mut am = AlertManager::new();
am.add_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Info,
"msg",
));
am.evaluate_metric("m1", 1.0);
assert_eq!(am.alert_count(), 1);
am.clear_alerts();
assert_eq!(am.alert_count(), 0);
}
#[test]
fn test_health_status_display() {
assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
assert_eq!(HealthStatus::Informational.to_string(), "informational");
assert_eq!(HealthStatus::Degraded.to_string(), "degraded");
assert_eq!(HealthStatus::Unhealthy.to_string(), "unhealthy");
}
#[test]
fn test_health_status_equality() {
assert_eq!(HealthStatus::Healthy, HealthStatus::Healthy);
assert_ne!(HealthStatus::Healthy, HealthStatus::Unhealthy);
}
#[test]
fn test_dashboard_new() {
let d = DiagnosticDashboard::new();
assert_eq!(d.event_count(), 0);
assert_eq!(d.error_count(), 0);
assert_eq!(d.health_status(), HealthStatus::Healthy);
}
#[test]
fn test_dashboard_default() {
let d = DiagnosticDashboard::default();
assert_eq!(d.event_count(), 0);
}
#[test]
fn test_dashboard_emit_events() {
let mut d = DiagnosticDashboard::new();
d.emit(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
d.emit(DiagnosticEvent::ErrorOccurred {
agent_id: "a1".into(),
message: "oops".into(),
severity: Severity::Warning,
timestamp: SystemTime::now(),
});
assert_eq!(d.event_count(), 2);
assert_eq!(d.error_count(), 1);
}
#[test]
fn test_dashboard_profiler() {
let mut d = DiagnosticDashboard::new();
{
let p = d.profiler("a1");
p.start();
p.record(ProfileCategory::Llm, "call", Duration::from_millis(100));
p.stop();
}
assert!(d.get_profiler("a1").is_some());
assert!(d.get_profiler("a2").is_none());
}
#[test]
fn test_dashboard_all_reports() {
let mut d = DiagnosticDashboard::new();
{
let p = d.profiler("a1");
p.start();
p.stop();
}
{
let p = d.profiler("a2");
p.start();
p.stop();
}
let reports = d.all_reports();
assert_eq!(reports.len(), 2);
}
#[test]
fn test_dashboard_health_healthy() {
let d = DiagnosticDashboard::new();
assert_eq!(d.health_status(), HealthStatus::Healthy);
}
#[test]
fn test_dashboard_health_info() {
let mut d = DiagnosticDashboard::new();
d.add_alert_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Info,
"info",
));
d.evaluate_alerts(&{
let mut m = HashMap::new();
m.insert("m1".to_string(), 1.0);
m
});
assert_eq!(d.health_status(), HealthStatus::Informational);
}
#[test]
fn test_dashboard_health_degraded() {
let mut d = DiagnosticDashboard::new();
d.add_alert_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Warning,
"warn",
));
d.evaluate_alerts(&{
let mut m = HashMap::new();
m.insert("m1".to_string(), 1.0);
m
});
assert_eq!(d.health_status(), HealthStatus::Degraded);
}
#[test]
fn test_dashboard_health_unhealthy() {
let mut d = DiagnosticDashboard::new();
d.add_alert_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Critical,
"crit",
));
d.evaluate_alerts(&{
let mut m = HashMap::new();
m.insert("m1".to_string(), 1.0);
m
});
assert_eq!(d.health_status(), HealthStatus::Unhealthy);
}
#[test]
fn test_dashboard_summary() {
let mut d = DiagnosticDashboard::new();
d.emit(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
let s = d.summary();
assert!(s.contains("Diagnostic Dashboard"));
assert!(s.contains("healthy"));
assert!(s.contains("Events: 1"));
}
#[test]
fn test_dashboard_summary_with_alerts() {
let mut d = DiagnosticDashboard::new();
d.add_alert_rule(AlertRule::new(
"test_rule",
"metric",
AlertCondition::GreaterThan(0.0),
Severity::Warning,
"alert fired",
));
d.evaluate_alerts(&{
let mut m = HashMap::new();
m.insert("metric".to_string(), 1.0);
m
});
let s = d.summary();
assert!(s.contains("Active Alerts"));
assert!(s.contains("test_rule"));
}
#[test]
fn test_dashboard_sink_access() {
let mut d = DiagnosticDashboard::new();
d.sink_mut().receive(DiagnosticEvent::AgentStarted {
agent_id: "a1".into(),
timestamp: SystemTime::now(),
});
assert_eq!(d.sink().len(), 1);
}
#[test]
fn test_dashboard_alert_manager_access() {
let mut d = DiagnosticDashboard::new();
d.alert_manager_mut().add_rule(AlertRule::new(
"r1",
"m1",
AlertCondition::GreaterThan(0.0),
Severity::Info,
"msg",
));
assert_eq!(d.alert_manager().rules().len(), 1);
}
}