Skip to main content

axon/
server_metrics.rs

1//! Server Metrics — Prometheus exposition format for live AxonServer metrics.
2//!
3//! Generates text/plain Prometheus metrics from the running server state:
4//!   - axon_server_uptime_seconds — server uptime
5//!   - axon_server_requests_total — total API requests
6//!   - axon_server_deployments_total — total deployments
7//!   - axon_server_errors_total — total errors
8//!   - axon_server_daemons_active — active daemons
9//!   - axon_server_daemons_by_state — daemons by lifecycle state
10//!   - axon_server_bus_events_published — events published on bus
11//!   - axon_server_bus_topics_seen — unique topics seen
12//!   - axon_server_versions_total — total flow versions tracked
13//!   - axon_server_flows_tracked — number of tracked flows
14//!   - axon_server_session_memory_count — ephemeral session entries
15//!   - axon_server_session_store_count — persistent session entries
16//!   - axon_server_rate_limiter_* — rate limiter state
17//!   - axon_server_request_log_* — request log buffer state
18//!   - axon_server_api_keys_* — API key counts
19//!   - axon_server_webhooks_* — webhook registry and delivery stats
20//!   - axon_server_audit_* — audit trail stats
21//!   - axon_server_middleware_* — request middleware stats
22//!   - axon_server_cors_permissive — CORS mode
23//!   - axon_server_shutdown_initiated — shutdown state
24
25use std::collections::HashMap;
26
27/// Per-daemon metric for labeled Prometheus exposition.
28#[derive(Debug, Clone)]
29pub struct DaemonMetric {
30    pub name: String,
31    pub state: String,
32    pub event_count: u64,
33    pub restart_count: u32,
34}
35
36/// Per-client rate limiter metric for labeled Prometheus exposition.
37#[derive(Debug, Clone)]
38pub struct ClientRateLimitMetric {
39    pub client_key: String,
40    pub total_requests: u64,
41    pub rejected: u64,
42}
43
44/// Per-topic metric for labeled Prometheus exposition.
45#[derive(Debug, Clone)]
46pub struct TopicMetric {
47    pub topic: String,
48    pub published: u64,
49}
50
51/// Per-flow execution metric for labeled Prometheus exposition.
52#[derive(Debug, Clone)]
53pub struct FlowMetric {
54    pub flow_name: String,
55    pub executions: u64,
56    pub errors: u64,
57    pub avg_latency_ms: u64,
58}
59
60/// Metrics snapshot from the running server.
61#[derive(Debug, Clone)]
62pub struct ServerSnapshot {
63    pub uptime_secs: u64,
64    pub server_start_timestamp: u64,
65    pub total_requests: u64,
66    pub total_deployments: u64,
67    pub total_errors: u64,
68    pub active_daemons: u32,
69    pub daemon_states: HashMap<String, u32>,
70    pub daemon_metrics: Vec<DaemonMetric>,
71    pub daemon_total_restarts: u64,
72    pub daemon_total_events: u64,
73    pub bus_events_published: u64,
74    pub bus_events_delivered: u64,
75    pub bus_events_dropped: u64,
76    pub bus_topics_seen: usize,
77    pub bus_active_subscribers: usize,
78    pub bus_topic_metrics: Vec<TopicMetric>,
79    pub flows_tracked: usize,
80    pub versions_total: usize,
81    pub session_memory_count: usize,
82    pub session_store_count: usize,
83    pub deploy_count: u64,
84    // ── Rate limiter ──
85    pub rate_limiter_enabled: bool,
86    pub rate_limiter_clients: usize,
87    pub rate_limiter_max_requests: u32,
88    pub rate_limiter_window_secs: u64,
89    pub rate_limiter_client_metrics: Vec<ClientRateLimitMetric>,
90    // ── Request log ──
91    pub request_log_enabled: bool,
92    pub request_log_buffered: usize,
93    pub request_log_capacity: usize,
94    pub request_log_total: u64,
95    pub request_log_errors: u64,
96    // ── API keys ──
97    pub api_keys_enabled: bool,
98    pub api_keys_active: usize,
99    pub api_keys_total: usize,
100    // ── Webhooks ──
101    pub webhooks_total: usize,
102    pub webhooks_active: usize,
103    pub webhooks_deliveries_total: u64,
104    pub webhooks_failures_total: u64,
105    // ── Audit trail ──
106    pub audit_buffered: usize,
107    pub audit_total_recorded: u64,
108    // ── Request middleware ──
109    pub middleware_enabled: bool,
110    pub middleware_requests_total: u64,
111    pub middleware_slow_threshold_ms: u64,
112    // ── CORS ──
113    pub cors_enabled: bool,
114    pub cors_permissive: bool,
115    // ── Trace store ──
116    pub trace_enabled: bool,
117    pub trace_buffered: usize,
118    pub trace_capacity: usize,
119    pub trace_total_recorded: u64,
120    pub trace_total_executions: u64,
121    pub trace_total_errors: u64,
122    pub flow_metrics: Vec<FlowMetric>,
123    // ── Schedules ──
124    pub schedules_total: usize,
125    pub schedules_enabled: usize,
126    pub schedules_total_runs: u64,
127    pub schedules_total_errors: u64,
128    pub schedules_avg_interval_secs: u64,
129    // ── Shutdown ──
130    pub shutdown_initiated: bool,
131}
132
133/// Generate Prometheus exposition format text from a server snapshot.
134pub fn to_prometheus(snap: &ServerSnapshot) -> String {
135    let mut out = String::new();
136
137    // Uptime
138    prom_gauge(&mut out, "axon_server_uptime_seconds", "Server uptime in seconds.", snap.uptime_secs);
139    prom_gauge(&mut out, "axon_server_start_timestamp", "Server start time (Unix seconds).", snap.server_start_timestamp);
140
141    // Requests
142    prom_counter(&mut out, "axon_server_requests_total", "Total API requests handled.", snap.total_requests);
143
144    // Deployments
145    prom_counter(&mut out, "axon_server_deployments_total", "Total flow deployments.", snap.total_deployments);
146    prom_counter(&mut out, "axon_server_deploy_count", "Total deploy operations.", snap.deploy_count);
147
148    // Errors
149    prom_counter(&mut out, "axon_server_errors_total", "Total errors encountered.", snap.total_errors);
150
151    // Daemons
152    prom_gauge(&mut out, "axon_server_daemons_active", "Number of active daemons.", snap.active_daemons as u64);
153
154    // Daemon states
155    if !snap.daemon_states.is_empty() {
156        out.push_str("# HELP axon_server_daemons_by_state Daemons by lifecycle state.\n");
157        out.push_str("# TYPE axon_server_daemons_by_state gauge\n");
158        let mut states: Vec<_> = snap.daemon_states.iter().collect();
159        states.sort_by_key(|(k, _)| (*k).clone());
160        for (state, count) in states {
161            out.push_str(&format!("axon_server_daemons_by_state{{state=\"{}\"}} {}\n", state, count));
162        }
163        out.push('\n');
164    }
165
166    // Daemon aggregate counters
167    prom_counter(&mut out, "axon_server_daemon_total_restarts", "Total daemon restarts across all daemons.", snap.daemon_total_restarts);
168    prom_counter(&mut out, "axon_server_daemon_total_events", "Total events processed across all daemons.", snap.daemon_total_events);
169
170    // Per-daemon metrics (labeled)
171    if !snap.daemon_metrics.is_empty() {
172        out.push_str("# HELP axon_server_daemon_event_count Events processed by daemon.\n");
173        out.push_str("# TYPE axon_server_daemon_event_count counter\n");
174        let mut sorted: Vec<_> = snap.daemon_metrics.iter().collect();
175        sorted.sort_by(|a, b| a.name.cmp(&b.name));
176        for dm in &sorted {
177            out.push_str(&format!(
178                "axon_server_daemon_event_count{{daemon=\"{}\",state=\"{}\"}} {}\n",
179                dm.name, dm.state, dm.event_count
180            ));
181        }
182        out.push('\n');
183
184        out.push_str("# HELP axon_server_daemon_restart_count Restart count by daemon.\n");
185        out.push_str("# TYPE axon_server_daemon_restart_count counter\n");
186        for dm in &sorted {
187            out.push_str(&format!(
188                "axon_server_daemon_restart_count{{daemon=\"{}\",state=\"{}\"}} {}\n",
189                dm.name, dm.state, dm.restart_count
190            ));
191        }
192        out.push('\n');
193    }
194
195    // Event bus
196    prom_counter(&mut out, "axon_server_bus_events_published", "Total events published on the bus.", snap.bus_events_published);
197    prom_counter(&mut out, "axon_server_bus_events_delivered", "Total events delivered to subscribers.", snap.bus_events_delivered);
198    prom_counter(&mut out, "axon_server_bus_events_dropped", "Total events dropped (no subscriber).", snap.bus_events_dropped);
199    prom_gauge(&mut out, "axon_server_bus_topics_seen", "Unique event topics seen.", snap.bus_topics_seen as u64);
200    prom_gauge(&mut out, "axon_server_bus_active_subscribers", "Active event bus subscribers.", snap.bus_active_subscribers as u64);
201
202    // Per-topic publish counts (labeled)
203    if !snap.bus_topic_metrics.is_empty() {
204        out.push_str("# HELP axon_server_bus_topic_published Events published per topic.\n");
205        out.push_str("# TYPE axon_server_bus_topic_published counter\n");
206        let mut sorted: Vec<_> = snap.bus_topic_metrics.iter().collect();
207        sorted.sort_by(|a, b| a.topic.cmp(&b.topic));
208        for tm in &sorted {
209            out.push_str(&format!(
210                "axon_server_bus_topic_published{{topic=\"{}\"}} {}\n",
211                tm.topic, tm.published
212            ));
213        }
214        out.push('\n');
215    }
216
217    // Versions
218    prom_gauge(&mut out, "axon_server_flows_tracked", "Number of tracked flows.", snap.flows_tracked as u64);
219    prom_gauge(&mut out, "axon_server_versions_total", "Total flow versions across all flows.", snap.versions_total as u64);
220
221    // Session
222    prom_gauge(&mut out, "axon_server_session_memory_count", "Ephemeral session memory entries.", snap.session_memory_count as u64);
223    prom_gauge(&mut out, "axon_server_session_store_count", "Persistent session store entries.", snap.session_store_count as u64);
224
225    // Rate limiter
226    prom_gauge(&mut out, "axon_server_rate_limiter_enabled", "Whether rate limiting is enabled.", snap.rate_limiter_enabled as u64);
227    prom_gauge(&mut out, "axon_server_rate_limiter_clients", "Number of tracked rate-limit clients.", snap.rate_limiter_clients as u64);
228    prom_gauge(&mut out, "axon_server_rate_limiter_max_requests", "Max requests per window.", snap.rate_limiter_max_requests as u64);
229    prom_gauge(&mut out, "axon_server_rate_limiter_window_secs", "Rate limit window in seconds.", snap.rate_limiter_window_secs);
230
231    // Per-client rate limiter metrics (labeled)
232    if !snap.rate_limiter_client_metrics.is_empty() {
233        out.push_str("# HELP axon_server_rate_limiter_client_requests Total requests per client.\n");
234        out.push_str("# TYPE axon_server_rate_limiter_client_requests counter\n");
235        let mut sorted: Vec<_> = snap.rate_limiter_client_metrics.iter().collect();
236        sorted.sort_by(|a, b| a.client_key.cmp(&b.client_key));
237        for cm in &sorted {
238            out.push_str(&format!(
239                "axon_server_rate_limiter_client_requests{{client=\"{}\"}} {}\n",
240                cm.client_key, cm.total_requests
241            ));
242        }
243        out.push('\n');
244
245        out.push_str("# HELP axon_server_rate_limiter_client_rejected Rejected requests per client.\n");
246        out.push_str("# TYPE axon_server_rate_limiter_client_rejected counter\n");
247        for cm in &sorted {
248            out.push_str(&format!(
249                "axon_server_rate_limiter_client_rejected{{client=\"{}\"}} {}\n",
250                cm.client_key, cm.rejected
251            ));
252        }
253        out.push('\n');
254    }
255
256    // Request log
257    prom_gauge(&mut out, "axon_server_request_log_enabled", "Whether request logging is enabled.", snap.request_log_enabled as u64);
258    prom_gauge(&mut out, "axon_server_request_log_buffered", "Entries currently in request log buffer.", snap.request_log_buffered as u64);
259    prom_gauge(&mut out, "axon_server_request_log_capacity", "Max capacity of request log buffer.", snap.request_log_capacity as u64);
260    prom_counter(&mut out, "axon_server_request_log_total", "Total requests recorded by request log.", snap.request_log_total);
261    prom_counter(&mut out, "axon_server_request_log_errors", "Total error responses recorded.", snap.request_log_errors);
262
263    // API keys
264    prom_gauge(&mut out, "axon_server_api_keys_enabled", "Whether API key auth is enabled.", snap.api_keys_enabled as u64);
265    prom_gauge(&mut out, "axon_server_api_keys_active", "Number of active (non-revoked) API keys.", snap.api_keys_active as u64);
266    prom_gauge(&mut out, "axon_server_api_keys_total", "Total API keys (including revoked).", snap.api_keys_total as u64);
267
268    // Webhooks
269    prom_gauge(&mut out, "axon_server_webhooks_total", "Total registered webhooks.", snap.webhooks_total as u64);
270    prom_gauge(&mut out, "axon_server_webhooks_active", "Active (enabled) webhooks.", snap.webhooks_active as u64);
271    prom_counter(&mut out, "axon_server_webhooks_deliveries_total", "Total webhook deliveries attempted.", snap.webhooks_deliveries_total);
272    prom_counter(&mut out, "axon_server_webhooks_failures_total", "Total webhook delivery failures.", snap.webhooks_failures_total);
273
274    // Audit trail
275    prom_gauge(&mut out, "axon_server_audit_buffered", "Entries currently in audit log buffer.", snap.audit_buffered as u64);
276    prom_counter(&mut out, "axon_server_audit_total_recorded", "Total audit entries recorded.", snap.audit_total_recorded);
277
278    // Request middleware
279    prom_gauge(&mut out, "axon_server_middleware_enabled", "Whether request middleware is enabled.", snap.middleware_enabled as u64);
280    prom_counter(&mut out, "axon_server_middleware_requests_total", "Total requests processed by middleware.", snap.middleware_requests_total);
281    prom_gauge(&mut out, "axon_server_middleware_slow_threshold_ms", "Slow request threshold in milliseconds.", snap.middleware_slow_threshold_ms);
282
283    // CORS
284    prom_gauge(&mut out, "axon_server_cors_enabled", "Whether CORS is enabled.", snap.cors_enabled as u64);
285    prom_gauge(&mut out, "axon_server_cors_permissive", "Whether CORS is in permissive (wildcard) mode.", snap.cors_permissive as u64);
286
287    // Trace store
288    prom_gauge(&mut out, "axon_server_trace_enabled", "Whether trace recording is enabled.", snap.trace_enabled as u64);
289    prom_gauge(&mut out, "axon_server_trace_buffered", "Number of traces currently in buffer.", snap.trace_buffered as u64);
290    prom_gauge(&mut out, "axon_server_trace_capacity", "Maximum trace buffer capacity.", snap.trace_capacity as u64);
291    prom_counter(&mut out, "axon_server_trace_total_recorded", "Total traces recorded (including evicted).", snap.trace_total_recorded);
292    prom_counter(&mut out, "axon_server_trace_total_executions", "Total flow executions via server.", snap.trace_total_executions);
293    prom_counter(&mut out, "axon_server_trace_total_errors", "Total execution errors recorded in traces.", snap.trace_total_errors);
294
295    // Per-flow execution metrics (labeled)
296    if !snap.flow_metrics.is_empty() {
297        out.push_str("# HELP axon_server_flow_executions Total executions per flow.\n");
298        out.push_str("# TYPE axon_server_flow_executions counter\n");
299        let mut sorted: Vec<_> = snap.flow_metrics.iter().collect();
300        sorted.sort_by(|a, b| a.flow_name.cmp(&b.flow_name));
301        for fm in &sorted {
302            out.push_str(&format!("axon_server_flow_executions{{flow=\"{}\"}} {}\n", fm.flow_name, fm.executions));
303        }
304        out.push('\n');
305
306        out.push_str("# HELP axon_server_flow_errors Total errors per flow.\n");
307        out.push_str("# TYPE axon_server_flow_errors counter\n");
308        for fm in &sorted {
309            out.push_str(&format!("axon_server_flow_errors{{flow=\"{}\"}} {}\n", fm.flow_name, fm.errors));
310        }
311        out.push('\n');
312
313        out.push_str("# HELP axon_server_flow_avg_latency_ms Average latency per flow in milliseconds.\n");
314        out.push_str("# TYPE axon_server_flow_avg_latency_ms gauge\n");
315        for fm in &sorted {
316            out.push_str(&format!("axon_server_flow_avg_latency_ms{{flow=\"{}\"}} {}\n", fm.flow_name, fm.avg_latency_ms));
317        }
318        out.push('\n');
319    }
320
321    // Schedules
322    prom_gauge(&mut out, "axon_server_schedules_total", "Total registered schedules.", snap.schedules_total as u64);
323    prom_gauge(&mut out, "axon_server_schedules_enabled", "Number of enabled schedules.", snap.schedules_enabled as u64);
324    prom_counter(&mut out, "axon_server_schedules_total_runs", "Total scheduled flow executions.", snap.schedules_total_runs);
325    prom_counter(&mut out, "axon_server_schedules_total_errors", "Total errors from scheduled executions.", snap.schedules_total_errors);
326    prom_gauge(&mut out, "axon_server_schedules_avg_interval_secs", "Average schedule interval in seconds.", snap.schedules_avg_interval_secs);
327
328    // Shutdown
329    prom_gauge(&mut out, "axon_server_shutdown_initiated", "Whether graceful shutdown has been initiated.", snap.shutdown_initiated as u64);
330
331    out
332}
333
334fn prom_gauge(out: &mut String, name: &str, help: &str, value: u64) {
335    out.push_str(&format!("# HELP {} {}\n", name, help));
336    out.push_str(&format!("# TYPE {} gauge\n", name));
337    out.push_str(&format!("{} {}\n\n", name, value));
338}
339
340fn prom_counter(out: &mut String, name: &str, help: &str, value: u64) {
341    out.push_str(&format!("# HELP {} {}\n", name, help));
342    out.push_str(&format!("# TYPE {} counter\n", name));
343    out.push_str(&format!("{} {}\n\n", name, value));
344}
345
346// ── Tests ────────────────────────────────────────────────────────────────
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    fn sample_snapshot() -> ServerSnapshot {
353        let mut daemon_states = HashMap::new();
354        daemon_states.insert("idle".to_string(), 2);
355        daemon_states.insert("running".to_string(), 1);
356
357        ServerSnapshot {
358            uptime_secs: 3600,
359            server_start_timestamp: 1700000000,
360            total_requests: 150,
361            total_deployments: 10,
362            total_errors: 3,
363            active_daemons: 3,
364            daemon_states,
365            daemon_metrics: vec![
366                DaemonMetric { name: "worker-1".into(), state: "running".into(), event_count: 42, restart_count: 1 },
367                DaemonMetric { name: "worker-2".into(), state: "idle".into(), event_count: 10, restart_count: 0 },
368            ],
369            daemon_total_restarts: 1,
370            daemon_total_events: 52,
371            bus_events_published: 50,
372            bus_events_delivered: 45,
373            bus_events_dropped: 5,
374            bus_topics_seen: 8,
375            bus_active_subscribers: 2,
376            bus_topic_metrics: vec![
377                TopicMetric { topic: "deploy".into(), published: 10 },
378                TopicMetric { topic: "daemon.started".into(), published: 5 },
379            ],
380            flows_tracked: 4,
381            versions_total: 12,
382            session_memory_count: 5,
383            session_store_count: 3,
384            deploy_count: 10,
385            rate_limiter_enabled: true,
386            rate_limiter_clients: 3,
387            rate_limiter_max_requests: 100,
388            rate_limiter_window_secs: 60,
389            rate_limiter_client_metrics: vec![
390                ClientRateLimitMetric { client_key: "user-1".into(), total_requests: 50, rejected: 2 },
391            ],
392            request_log_enabled: true,
393            request_log_buffered: 42,
394            request_log_capacity: 1000,
395            request_log_total: 150,
396            request_log_errors: 5,
397            api_keys_enabled: true,
398            api_keys_active: 3,
399            api_keys_total: 5,
400            webhooks_total: 4,
401            webhooks_active: 3,
402            webhooks_deliveries_total: 20,
403            webhooks_failures_total: 2,
404            audit_buffered: 100,
405            audit_total_recorded: 250,
406            middleware_enabled: true,
407            middleware_requests_total: 150,
408            middleware_slow_threshold_ms: 5000,
409            cors_enabled: true,
410            cors_permissive: true,
411            trace_enabled: true,
412            trace_buffered: 25,
413            trace_capacity: 500,
414            trace_total_recorded: 42,
415            trace_total_executions: 42,
416            trace_total_errors: 3,
417            flow_metrics: vec![
418                FlowMetric { flow_name: "Pipeline".into(), executions: 50, errors: 3, avg_latency_ms: 120 },
419            ],
420            schedules_total: 3,
421            schedules_enabled: 2,
422            schedules_total_runs: 15,
423            schedules_total_errors: 1,
424            schedules_avg_interval_secs: 120,
425            shutdown_initiated: false,
426        }
427    }
428
429    #[test]
430    fn prometheus_contains_uptime() {
431        let prom = to_prometheus(&sample_snapshot());
432        assert!(prom.contains("axon_server_uptime_seconds 3600"));
433        assert!(prom.contains("# TYPE axon_server_uptime_seconds gauge"));
434    }
435
436    #[test]
437    fn prometheus_contains_requests() {
438        let prom = to_prometheus(&sample_snapshot());
439        assert!(prom.contains("axon_server_requests_total 150"));
440        assert!(prom.contains("# TYPE axon_server_requests_total counter"));
441    }
442
443    #[test]
444    fn prometheus_contains_deployments() {
445        let prom = to_prometheus(&sample_snapshot());
446        assert!(prom.contains("axon_server_deployments_total 10"));
447    }
448
449    #[test]
450    fn prometheus_contains_errors() {
451        let prom = to_prometheus(&sample_snapshot());
452        assert!(prom.contains("axon_server_errors_total 3"));
453    }
454
455    #[test]
456    fn prometheus_contains_daemons() {
457        let prom = to_prometheus(&sample_snapshot());
458        assert!(prom.contains("axon_server_daemons_active 3"));
459        assert!(prom.contains("axon_server_daemons_by_state{state=\"idle\"} 2"));
460        assert!(prom.contains("axon_server_daemons_by_state{state=\"running\"} 1"));
461    }
462
463    #[test]
464    fn prometheus_contains_bus_metrics() {
465        let prom = to_prometheus(&sample_snapshot());
466        assert!(prom.contains("axon_server_bus_events_published 50"));
467        assert!(prom.contains("axon_server_bus_events_delivered 45"));
468        assert!(prom.contains("axon_server_bus_events_dropped 5"));
469        assert!(prom.contains("axon_server_bus_topics_seen 8"));
470        assert!(prom.contains("axon_server_bus_active_subscribers 2"));
471    }
472
473    #[test]
474    fn prometheus_contains_versions() {
475        let prom = to_prometheus(&sample_snapshot());
476        assert!(prom.contains("axon_server_flows_tracked 4"));
477        assert!(prom.contains("axon_server_versions_total 12"));
478    }
479
480    #[test]
481    fn prometheus_contains_session() {
482        let prom = to_prometheus(&sample_snapshot());
483        assert!(prom.contains("axon_server_session_memory_count 5"));
484        assert!(prom.contains("axon_server_session_store_count 3"));
485    }
486
487    #[test]
488    fn prometheus_has_help_and_type_for_all() {
489        let prom = to_prometheus(&sample_snapshot());
490        // Count HELP lines
491        let help_count = prom.lines().filter(|l| l.starts_with("# HELP")).count();
492        let type_count = prom.lines().filter(|l| l.starts_with("# TYPE")).count();
493        assert!(help_count >= 56);
494        assert_eq!(help_count, type_count);
495    }
496
497    #[test]
498    fn prometheus_empty_daemon_states() {
499        let mut snap = sample_snapshot();
500        snap.daemon_states.clear();
501        let prom = to_prometheus(&snap);
502        assert!(!prom.contains("axon_server_daemons_by_state"));
503    }
504
505    #[test]
506    fn prometheus_zero_snapshot() {
507        let snap = ServerSnapshot {
508            uptime_secs: 0,
509            server_start_timestamp: 0,
510            total_requests: 0,
511            total_deployments: 0,
512            total_errors: 0,
513            active_daemons: 0,
514            daemon_states: HashMap::new(),
515            daemon_metrics: Vec::new(),
516            daemon_total_restarts: 0,
517            daemon_total_events: 0,
518            bus_events_published: 0,
519            bus_events_delivered: 0,
520            bus_events_dropped: 0,
521            bus_topics_seen: 0,
522            bus_active_subscribers: 0,
523            bus_topic_metrics: Vec::new(),
524            flows_tracked: 0,
525            versions_total: 0,
526            session_memory_count: 0,
527            session_store_count: 0,
528            deploy_count: 0,
529            rate_limiter_enabled: false,
530            rate_limiter_clients: 0,
531            rate_limiter_max_requests: 0,
532            rate_limiter_window_secs: 0,
533            rate_limiter_client_metrics: Vec::new(),
534            request_log_enabled: false,
535            request_log_buffered: 0,
536            request_log_capacity: 0,
537            request_log_total: 0,
538            request_log_errors: 0,
539            api_keys_enabled: false,
540            api_keys_active: 0,
541            api_keys_total: 0,
542            webhooks_total: 0,
543            webhooks_active: 0,
544            webhooks_deliveries_total: 0,
545            webhooks_failures_total: 0,
546            audit_buffered: 0,
547            audit_total_recorded: 0,
548            middleware_enabled: false,
549            middleware_requests_total: 0,
550            middleware_slow_threshold_ms: 0,
551            cors_enabled: false,
552            cors_permissive: false,
553            trace_enabled: false,
554            trace_buffered: 0,
555            trace_capacity: 0,
556            trace_total_recorded: 0,
557            trace_total_executions: 0,
558            trace_total_errors: 0,
559            flow_metrics: Vec::new(),
560            schedules_total: 0,
561            schedules_enabled: 0,
562            schedules_total_runs: 0,
563            schedules_total_errors: 0,
564            schedules_avg_interval_secs: 0,
565            shutdown_initiated: false,
566        };
567        let prom = to_prometheus(&snap);
568        assert!(prom.contains("axon_server_uptime_seconds 0"));
569        assert!(prom.contains("axon_server_requests_total 0"));
570    }
571
572    #[test]
573    fn prometheus_contains_rate_limiter() {
574        let prom = to_prometheus(&sample_snapshot());
575        assert!(prom.contains("axon_server_rate_limiter_enabled 1"));
576        assert!(prom.contains("axon_server_rate_limiter_clients 3"));
577        assert!(prom.contains("axon_server_rate_limiter_max_requests 100"));
578        assert!(prom.contains("axon_server_rate_limiter_window_secs 60"));
579    }
580
581    #[test]
582    fn prometheus_contains_request_log() {
583        let prom = to_prometheus(&sample_snapshot());
584        assert!(prom.contains("axon_server_request_log_enabled 1"));
585        assert!(prom.contains("axon_server_request_log_buffered 42"));
586        assert!(prom.contains("axon_server_request_log_capacity 1000"));
587        assert!(prom.contains("axon_server_request_log_total 150"));
588        assert!(prom.contains("axon_server_request_log_errors 5"));
589    }
590
591    #[test]
592    fn prometheus_contains_api_keys() {
593        let prom = to_prometheus(&sample_snapshot());
594        assert!(prom.contains("axon_server_api_keys_enabled 1"));
595        assert!(prom.contains("axon_server_api_keys_active 3"));
596        assert!(prom.contains("axon_server_api_keys_total 5"));
597    }
598
599    #[test]
600    fn prometheus_contains_webhooks() {
601        let prom = to_prometheus(&sample_snapshot());
602        assert!(prom.contains("axon_server_webhooks_total 4"));
603        assert!(prom.contains("axon_server_webhooks_active 3"));
604        assert!(prom.contains("axon_server_webhooks_deliveries_total 20"));
605        assert!(prom.contains("axon_server_webhooks_failures_total 2"));
606    }
607
608    #[test]
609    fn prometheus_contains_audit() {
610        let prom = to_prometheus(&sample_snapshot());
611        assert!(prom.contains("axon_server_audit_buffered 100"));
612        assert!(prom.contains("axon_server_audit_total_recorded 250"));
613    }
614
615    #[test]
616    fn prometheus_contains_middleware() {
617        let prom = to_prometheus(&sample_snapshot());
618        assert!(prom.contains("axon_server_middleware_enabled 1"));
619        assert!(prom.contains("axon_server_middleware_requests_total 150"));
620        assert!(prom.contains("axon_server_middleware_slow_threshold_ms 5000"));
621    }
622
623    #[test]
624    fn prometheus_contains_cors() {
625        let prom = to_prometheus(&sample_snapshot());
626        assert!(prom.contains("axon_server_cors_enabled 1"));
627        assert!(prom.contains("axon_server_cors_permissive 1"));
628    }
629
630    #[test]
631    fn prometheus_contains_shutdown() {
632        let prom = to_prometheus(&sample_snapshot());
633        assert!(prom.contains("axon_server_shutdown_initiated 0"));
634    }
635
636    #[test]
637    fn prometheus_contains_trace_store() {
638        let prom = to_prometheus(&sample_snapshot());
639        assert!(prom.contains("axon_server_trace_enabled 1"));
640        assert!(prom.contains("axon_server_trace_buffered 25"));
641        assert!(prom.contains("axon_server_trace_capacity 500"));
642        assert!(prom.contains("axon_server_trace_total_recorded 42"));
643        assert!(prom.contains("axon_server_trace_total_executions 42"));
644        assert!(prom.contains("axon_server_trace_total_errors 3"));
645    }
646
647    #[test]
648    fn prometheus_valid_exposition_format() {
649        let prom = to_prometheus(&sample_snapshot());
650        // Every non-empty, non-comment line should be "metric_name{labels} value" or "metric_name value"
651        for line in prom.lines() {
652            if line.is_empty() || line.starts_with('#') {
653                continue;
654            }
655            // Should have at least one space separating metric from value
656            assert!(line.contains(' '), "Invalid line: {}", line);
657        }
658    }
659}