1use once_cell::sync::Lazy;
4use prometheus::{
5 Gauge, GaugeVec, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec,
6 Opts, Registry,
7};
8use std::sync::Arc;
9use tracing::debug;
10
11#[derive(Debug, Clone, Copy)]
19pub struct DriftEvaluationSample<'a> {
20 pub workspace_id: &'a str,
21 pub endpoint: &'a str,
22 pub method: &'a str,
23 pub total: u32,
24 pub breaking: u32,
25 pub potentially_breaking: u32,
26 pub budget_exceeded: bool,
27}
28
29#[derive(Clone)]
31pub struct MetricsRegistry {
32 registry: Arc<Registry>,
33
34 pub requests_total: IntCounterVec,
36 pub requests_duration_seconds: HistogramVec,
37 pub requests_in_flight: IntGaugeVec,
38
39 pub requests_by_path_total: IntCounterVec,
41 pub request_duration_by_path_seconds: HistogramVec,
42 pub average_latency_by_path_seconds: GaugeVec,
43
44 pub workspace_requests_total: IntCounterVec,
46 pub workspace_requests_duration_seconds: HistogramVec,
47 pub workspace_active_routes: IntGaugeVec,
48 pub workspace_errors_total: IntCounterVec,
49
50 pub errors_total: IntCounterVec,
52 pub error_rate: GaugeVec,
53
54 pub plugin_executions_total: IntCounterVec,
56 pub plugin_execution_duration_seconds: HistogramVec,
57 pub plugin_errors_total: IntCounterVec,
58
59 pub ws_connections_active: IntGauge,
61 pub ws_connections_total: IntCounter,
62 pub ws_connection_duration_seconds: HistogramVec,
63 pub ws_messages_sent: IntCounter,
64 pub ws_messages_received: IntCounter,
65 pub ws_errors_total: IntCounter,
66
67 pub smtp_connections_active: IntGauge,
69 pub smtp_connections_total: IntCounter,
70 pub smtp_messages_received_total: IntCounter,
71 pub smtp_messages_stored_total: IntCounter,
72 pub smtp_errors_total: IntCounterVec,
73
74 pub mqtt_connections_active: IntGauge,
76 pub mqtt_connections_total: IntCounter,
77 pub mqtt_messages_published_total: IntCounter,
78 pub mqtt_messages_received_total: IntCounter,
79 pub mqtt_topics_active: IntGauge,
80 pub mqtt_subscriptions_active: IntGauge,
81 pub mqtt_retained_messages: IntGauge,
82 pub mqtt_errors_total: IntCounterVec,
83
84 pub memory_usage_bytes: Gauge,
86 pub cpu_usage_percent: Gauge,
87 pub thread_count: Gauge,
88 pub uptime_seconds: Gauge,
89
90 pub active_scenario_mode: IntGauge,
92 pub chaos_triggers_total: IntCounter,
93
94 pub service_availability: GaugeVec,
96 pub slo_compliance: GaugeVec,
97 pub successful_request_rate: GaugeVec,
98 pub p95_latency_slo_compliance: GaugeVec,
99 pub error_budget_remaining: GaugeVec,
100
101 pub marketplace_publish_total: IntCounterVec,
103 pub marketplace_publish_duration_seconds: HistogramVec,
104 pub marketplace_download_total: IntCounterVec,
105 pub marketplace_download_duration_seconds: HistogramVec,
106 pub marketplace_search_total: IntCounterVec,
107 pub marketplace_search_duration_seconds: HistogramVec,
108 pub marketplace_errors_total: IntCounterVec,
109 pub marketplace_items_total: IntGaugeVec,
110
111 pub drift_percentage: GaugeVec,
118 pub drift_total_changes: IntGaugeVec,
120 pub drift_breaking_changes: IntGaugeVec,
123 pub drift_budget_exceeded: IntGaugeVec,
125}
126
127impl MetricsRegistry {
128 pub fn new() -> Self {
130 let registry = Registry::new();
131
132 let requests_total = IntCounterVec::new(
134 Opts::new(
135 "mockforge_requests_total",
136 "Total number of requests by protocol, method, status, and pillar",
137 ),
138 &["protocol", "method", "status", "pillar"],
139 )
140 .expect("Failed to create requests_total metric");
141
142 let requests_duration_seconds = HistogramVec::new(
143 HistogramOpts::new(
144 "mockforge_request_duration_seconds",
145 "Request duration in seconds by protocol, method, and pillar",
146 )
147 .buckets(vec![
148 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
149 ]),
150 &["protocol", "method", "pillar"],
151 )
152 .expect("Failed to create requests_duration_seconds metric");
153
154 let requests_in_flight = IntGaugeVec::new(
155 Opts::new(
156 "mockforge_requests_in_flight",
157 "Number of requests currently being processed",
158 ),
159 &["protocol"],
160 )
161 .expect("Failed to create requests_in_flight metric");
162
163 let errors_total = IntCounterVec::new(
165 Opts::new(
166 "mockforge_errors_total",
167 "Total number of errors by protocol, error type, and pillar",
168 ),
169 &["protocol", "error_type", "pillar"],
170 )
171 .expect("Failed to create errors_total metric");
172
173 let error_rate = GaugeVec::new(
174 Opts::new("mockforge_error_rate", "Error rate by protocol (0.0 to 1.0)"),
175 &["protocol"],
176 )
177 .expect("Failed to create error_rate metric");
178
179 let plugin_executions_total = IntCounterVec::new(
181 Opts::new("mockforge_plugin_executions_total", "Total number of plugin executions"),
182 &["plugin_name", "status"],
183 )
184 .expect("Failed to create plugin_executions_total metric");
185
186 let plugin_execution_duration_seconds = HistogramVec::new(
187 HistogramOpts::new(
188 "mockforge_plugin_execution_duration_seconds",
189 "Plugin execution duration in seconds",
190 )
191 .buckets(vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]),
192 &["plugin_name"],
193 )
194 .expect("Failed to create plugin_execution_duration_seconds metric");
195
196 let plugin_errors_total = IntCounterVec::new(
197 Opts::new("mockforge_plugin_errors_total", "Total number of plugin errors"),
198 &["plugin_name", "error_type"],
199 )
200 .expect("Failed to create plugin_errors_total metric");
201
202 let requests_by_path_total = IntCounterVec::new(
205 Opts::new(
206 "mockforge_requests_by_path_total",
207 "Total number of requests by path, method, and status",
208 ),
209 &["path", "method", "status"],
210 )
211 .expect("Failed to create requests_by_path_total metric");
212
213 let request_duration_by_path_seconds = HistogramVec::new(
214 HistogramOpts::new(
215 "mockforge_request_duration_by_path_seconds",
216 "Request duration by path in seconds",
217 )
218 .buckets(vec![
219 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
220 ]),
221 &["path", "method"],
222 )
223 .expect("Failed to create request_duration_by_path_seconds metric");
224
225 let average_latency_by_path_seconds = GaugeVec::new(
226 Opts::new(
227 "mockforge_average_latency_by_path_seconds",
228 "Average request latency by path in seconds",
229 ),
230 &["path", "method"],
231 )
232 .expect("Failed to create average_latency_by_path_seconds metric");
233
234 let workspace_requests_total = IntCounterVec::new(
236 Opts::new(
237 "mockforge_workspace_requests_total",
238 "Total number of requests by workspace, method, and status",
239 ),
240 &["workspace_id", "method", "status"],
241 )
242 .expect("Failed to create workspace_requests_total metric");
243
244 let workspace_requests_duration_seconds = HistogramVec::new(
245 HistogramOpts::new(
246 "mockforge_workspace_request_duration_seconds",
247 "Request duration by workspace in seconds",
248 )
249 .buckets(vec![
250 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
251 ]),
252 &["workspace_id", "method"],
253 )
254 .expect("Failed to create workspace_requests_duration_seconds metric");
255
256 let workspace_active_routes = IntGaugeVec::new(
257 Opts::new(
258 "mockforge_workspace_active_routes",
259 "Number of active routes in each workspace",
260 ),
261 &["workspace_id"],
262 )
263 .expect("Failed to create workspace_active_routes metric");
264
265 let workspace_errors_total = IntCounterVec::new(
266 Opts::new("mockforge_workspace_errors_total", "Total number of errors by workspace"),
267 &["workspace_id", "error_type"],
268 )
269 .expect("Failed to create workspace_errors_total metric");
270
271 let ws_connections_active = IntGauge::new(
273 "mockforge_ws_connections_active",
274 "Number of active WebSocket connections",
275 )
276 .expect("Failed to create ws_connections_active metric");
277
278 let ws_connections_total = IntCounter::new(
279 "mockforge_ws_connections_total",
280 "Total number of WebSocket connections established",
281 )
282 .expect("Failed to create ws_connections_total metric");
283
284 let ws_connection_duration_seconds = HistogramVec::new(
285 HistogramOpts::new(
286 "mockforge_ws_connection_duration_seconds",
287 "WebSocket connection duration in seconds",
288 )
289 .buckets(vec![1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0, 1800.0, 3600.0]),
290 &["status"],
291 )
292 .expect("Failed to create ws_connection_duration_seconds metric");
293
294 let ws_messages_sent = IntCounter::new(
295 "mockforge_ws_messages_sent_total",
296 "Total number of WebSocket messages sent",
297 )
298 .expect("Failed to create ws_messages_sent metric");
299
300 let ws_messages_received = IntCounter::new(
301 "mockforge_ws_messages_received_total",
302 "Total number of WebSocket messages received",
303 )
304 .expect("Failed to create ws_messages_received metric");
305
306 let ws_errors_total =
307 IntCounter::new("mockforge_ws_errors_total", "Total number of WebSocket errors")
308 .expect("Failed to create ws_errors_total metric");
309
310 let smtp_connections_active =
312 IntGauge::new("mockforge_smtp_connections_active", "Number of active SMTP connections")
313 .expect("Failed to create smtp_connections_active metric");
314
315 let smtp_connections_total =
316 IntCounter::new("mockforge_smtp_connections_total", "Total number of SMTP connections")
317 .expect("Failed to create smtp_connections_total metric");
318
319 let smtp_messages_received_total = IntCounter::new(
320 "mockforge_smtp_messages_received_total",
321 "Total number of SMTP messages received",
322 )
323 .expect("Failed to create smtp_messages_received_total metric");
324
325 let smtp_messages_stored_total = IntCounter::new(
326 "mockforge_smtp_messages_stored_total",
327 "Total number of SMTP messages stored in mailbox",
328 )
329 .expect("Failed to create smtp_messages_stored_total metric");
330
331 let smtp_errors_total = IntCounterVec::new(
332 Opts::new("mockforge_smtp_errors_total", "Total number of SMTP errors by type"),
333 &["error_type"],
334 )
335 .expect("Failed to create smtp_errors_total metric");
336
337 let mqtt_connections_active = IntGauge::new(
339 "mockforge_mqtt_connections_active",
340 "Number of active MQTT client connections",
341 )
342 .expect("Failed to create mqtt_connections_active metric");
343
344 let mqtt_connections_total = IntCounter::new(
345 "mockforge_mqtt_connections_total",
346 "Total number of MQTT client connections established",
347 )
348 .expect("Failed to create mqtt_connections_total metric");
349
350 let mqtt_messages_published_total = IntCounter::new(
351 "mockforge_mqtt_messages_published_total",
352 "Total number of MQTT messages published",
353 )
354 .expect("Failed to create mqtt_messages_published_total metric");
355
356 let mqtt_messages_received_total = IntCounter::new(
357 "mockforge_mqtt_messages_received_total",
358 "Total number of MQTT messages received",
359 )
360 .expect("Failed to create mqtt_messages_received_total metric");
361
362 let mqtt_topics_active =
363 IntGauge::new("mockforge_mqtt_topics_active", "Number of active MQTT topics")
364 .expect("Failed to create mqtt_topics_active metric");
365
366 let mqtt_subscriptions_active = IntGauge::new(
367 "mockforge_mqtt_subscriptions_active",
368 "Number of active MQTT subscriptions",
369 )
370 .expect("Failed to create mqtt_subscriptions_active metric");
371
372 let mqtt_retained_messages =
373 IntGauge::new("mockforge_mqtt_retained_messages", "Number of retained MQTT messages")
374 .expect("Failed to create mqtt_retained_messages metric");
375
376 let mqtt_errors_total = IntCounterVec::new(
377 Opts::new("mockforge_mqtt_errors_total", "Total number of MQTT errors by type"),
378 &["error_type"],
379 )
380 .expect("Failed to create mqtt_errors_total metric");
381
382 let memory_usage_bytes =
384 Gauge::new("mockforge_memory_usage_bytes", "Memory usage in bytes")
385 .expect("Failed to create memory_usage_bytes metric");
386
387 let cpu_usage_percent = Gauge::new("mockforge_cpu_usage_percent", "CPU usage percentage")
388 .expect("Failed to create cpu_usage_percent metric");
389
390 let thread_count = Gauge::new("mockforge_thread_count", "Number of active threads")
391 .expect("Failed to create thread_count metric");
392
393 let uptime_seconds = Gauge::new("mockforge_uptime_seconds", "Server uptime in seconds")
394 .expect("Failed to create uptime_seconds metric");
395
396 let active_scenario_mode = IntGauge::new(
398 "mockforge_active_scenario_mode",
399 "Active scenario mode (0=healthy, 1=degraded, 2=error, 3=chaos)",
400 )
401 .expect("Failed to create active_scenario_mode metric");
402
403 let chaos_triggers_total = IntCounter::new(
404 "mockforge_chaos_triggers_total",
405 "Total number of chaos mode triggers",
406 )
407 .expect("Failed to create chaos_triggers_total metric");
408
409 let service_availability = GaugeVec::new(
411 Opts::new(
412 "mockforge_service_availability",
413 "Service availability percentage (0.0 to 1.0) by protocol",
414 ),
415 &["protocol"],
416 )
417 .expect("Failed to create service_availability metric");
418
419 let slo_compliance = GaugeVec::new(
420 Opts::new(
421 "mockforge_slo_compliance",
422 "SLO compliance percentage (0.0 to 1.0) by protocol and slo_type",
423 ),
424 &["protocol", "slo_type"],
425 )
426 .expect("Failed to create slo_compliance metric");
427
428 let successful_request_rate = GaugeVec::new(
429 Opts::new(
430 "mockforge_successful_request_rate",
431 "Successful request rate (0.0 to 1.0) by protocol",
432 ),
433 &["protocol"],
434 )
435 .expect("Failed to create successful_request_rate metric");
436
437 let p95_latency_slo_compliance = GaugeVec::new(
438 Opts::new(
439 "mockforge_p95_latency_slo_compliance",
440 "P95 latency SLO compliance (1.0 = compliant, 0.0 = non-compliant) by protocol",
441 ),
442 &["protocol"],
443 )
444 .expect("Failed to create p95_latency_slo_compliance metric");
445
446 let error_budget_remaining = GaugeVec::new(
447 Opts::new(
448 "mockforge_error_budget_remaining",
449 "Remaining error budget percentage (0.0 to 1.0) by protocol",
450 ),
451 &["protocol"],
452 )
453 .expect("Failed to create error_budget_remaining metric");
454
455 let marketplace_publish_total = IntCounterVec::new(
457 Opts::new(
458 "mockforge_marketplace_publish_total",
459 "Total number of marketplace items published by type and status",
460 ),
461 &["type", "status"], )
463 .expect("Failed to create marketplace_publish_total metric");
464
465 let marketplace_publish_duration_seconds = HistogramVec::new(
466 HistogramOpts::new(
467 "mockforge_marketplace_publish_duration_seconds",
468 "Marketplace publish operation duration in seconds",
469 )
470 .buckets(vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0]),
471 &["type"], )
473 .expect("Failed to create marketplace_publish_duration_seconds metric");
474
475 let marketplace_download_total = IntCounterVec::new(
476 Opts::new(
477 "mockforge_marketplace_download_total",
478 "Total number of marketplace items downloaded by type and status",
479 ),
480 &["type", "status"], )
482 .expect("Failed to create marketplace_download_total metric");
483
484 let marketplace_download_duration_seconds = HistogramVec::new(
485 HistogramOpts::new(
486 "mockforge_marketplace_download_duration_seconds",
487 "Marketplace download operation duration in seconds",
488 )
489 .buckets(vec![0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]),
490 &["type"], )
492 .expect("Failed to create marketplace_download_duration_seconds metric");
493
494 let marketplace_search_total = IntCounterVec::new(
495 Opts::new(
496 "mockforge_marketplace_search_total",
497 "Total number of marketplace searches by type and status",
498 ),
499 &["type", "status"], )
501 .expect("Failed to create marketplace_search_total metric");
502
503 let marketplace_search_duration_seconds = HistogramVec::new(
504 HistogramOpts::new(
505 "mockforge_marketplace_search_duration_seconds",
506 "Marketplace search operation duration in seconds",
507 )
508 .buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0]),
509 &["type"], )
511 .expect("Failed to create marketplace_search_duration_seconds metric");
512
513 let marketplace_errors_total = IntCounterVec::new(
514 Opts::new(
515 "mockforge_marketplace_errors_total",
516 "Total number of marketplace errors by type and error_code",
517 ),
518 &["type", "error_code"], )
520 .expect("Failed to create marketplace_errors_total metric");
521
522 let marketplace_items_total = IntGaugeVec::new(
523 Opts::new(
524 "mockforge_marketplace_items_total",
525 "Total number of marketplace items by type",
526 ),
527 &["type"], )
529 .expect("Failed to create marketplace_items_total metric");
530
531 let drift_percentage = GaugeVec::new(
534 Opts::new(
535 "mockforge_drift_percentage",
536 "Contract drift severity as a 0.0–100.0 percentage (breaking + potentially-breaking ÷ total observed × 100)",
537 ),
538 &["workspace_id", "endpoint", "method"],
539 )
540 .expect("Failed to create drift_percentage metric");
541
542 let drift_total_changes = IntGaugeVec::new(
543 Opts::new(
544 "mockforge_drift_total_changes",
545 "Total mismatches observed in the most recent drift evaluation",
546 ),
547 &["workspace_id", "endpoint", "method"],
548 )
549 .expect("Failed to create drift_total_changes metric");
550
551 let drift_breaking_changes = IntGaugeVec::new(
552 Opts::new(
553 "mockforge_drift_breaking_changes",
554 "Breaking changes observed in the most recent drift evaluation",
555 ),
556 &["workspace_id", "endpoint", "method"],
557 )
558 .expect("Failed to create drift_breaking_changes metric");
559
560 let drift_budget_exceeded = IntGaugeVec::new(
561 Opts::new(
562 "mockforge_drift_budget_exceeded",
563 "1 when the configured drift budget was exceeded by the most recent evaluation, 0 otherwise",
564 ),
565 &["workspace_id", "endpoint", "method"],
566 )
567 .expect("Failed to create drift_budget_exceeded metric");
568
569 registry
571 .register(Box::new(requests_total.clone()))
572 .expect("Failed to register requests_total");
573 registry
574 .register(Box::new(requests_duration_seconds.clone()))
575 .expect("Failed to register requests_duration_seconds");
576 registry
577 .register(Box::new(requests_in_flight.clone()))
578 .expect("Failed to register requests_in_flight");
579 registry
580 .register(Box::new(requests_by_path_total.clone()))
581 .expect("Failed to register requests_by_path_total");
582 registry
583 .register(Box::new(request_duration_by_path_seconds.clone()))
584 .expect("Failed to register request_duration_by_path_seconds");
585 registry
586 .register(Box::new(average_latency_by_path_seconds.clone()))
587 .expect("Failed to register average_latency_by_path_seconds");
588 registry
589 .register(Box::new(workspace_requests_total.clone()))
590 .expect("Failed to register workspace_requests_total");
591 registry
592 .register(Box::new(workspace_requests_duration_seconds.clone()))
593 .expect("Failed to register workspace_requests_duration_seconds");
594 registry
595 .register(Box::new(workspace_active_routes.clone()))
596 .expect("Failed to register workspace_active_routes");
597 registry
598 .register(Box::new(workspace_errors_total.clone()))
599 .expect("Failed to register workspace_errors_total");
600 registry
601 .register(Box::new(errors_total.clone()))
602 .expect("Failed to register errors_total");
603 registry
604 .register(Box::new(error_rate.clone()))
605 .expect("Failed to register error_rate");
606 registry
607 .register(Box::new(plugin_executions_total.clone()))
608 .expect("Failed to register plugin_executions_total");
609 registry
610 .register(Box::new(plugin_execution_duration_seconds.clone()))
611 .expect("Failed to register plugin_execution_duration_seconds");
612 registry
613 .register(Box::new(plugin_errors_total.clone()))
614 .expect("Failed to register plugin_errors_total");
615 registry
616 .register(Box::new(ws_connections_active.clone()))
617 .expect("Failed to register ws_connections_active");
618 registry
619 .register(Box::new(ws_connections_total.clone()))
620 .expect("Failed to register ws_connections_total");
621 registry
622 .register(Box::new(ws_connection_duration_seconds.clone()))
623 .expect("Failed to register ws_connection_duration_seconds");
624 registry
625 .register(Box::new(ws_messages_sent.clone()))
626 .expect("Failed to register ws_messages_sent");
627 registry
628 .register(Box::new(ws_messages_received.clone()))
629 .expect("Failed to register ws_messages_received");
630 registry
631 .register(Box::new(ws_errors_total.clone()))
632 .expect("Failed to register ws_errors_total");
633 registry
634 .register(Box::new(smtp_connections_active.clone()))
635 .expect("Failed to register smtp_connections_active");
636 registry
637 .register(Box::new(smtp_connections_total.clone()))
638 .expect("Failed to register smtp_connections_total");
639 registry
640 .register(Box::new(smtp_messages_received_total.clone()))
641 .expect("Failed to register smtp_messages_received_total");
642 registry
643 .register(Box::new(smtp_messages_stored_total.clone()))
644 .expect("Failed to register smtp_messages_stored_total");
645 registry
646 .register(Box::new(smtp_errors_total.clone()))
647 .expect("Failed to register smtp_errors_total");
648 registry
649 .register(Box::new(mqtt_connections_active.clone()))
650 .expect("Failed to register mqtt_connections_active");
651 registry
652 .register(Box::new(mqtt_connections_total.clone()))
653 .expect("Failed to register mqtt_connections_total");
654 registry
655 .register(Box::new(mqtt_messages_published_total.clone()))
656 .expect("Failed to register mqtt_messages_published_total");
657 registry
658 .register(Box::new(mqtt_messages_received_total.clone()))
659 .expect("Failed to register mqtt_messages_received_total");
660 registry
661 .register(Box::new(mqtt_topics_active.clone()))
662 .expect("Failed to register mqtt_topics_active");
663 registry
664 .register(Box::new(mqtt_subscriptions_active.clone()))
665 .expect("Failed to register mqtt_subscriptions_active");
666 registry
667 .register(Box::new(mqtt_retained_messages.clone()))
668 .expect("Failed to register mqtt_retained_messages");
669 registry
670 .register(Box::new(mqtt_errors_total.clone()))
671 .expect("Failed to register mqtt_errors_total");
672 registry
673 .register(Box::new(memory_usage_bytes.clone()))
674 .expect("Failed to register memory_usage_bytes");
675 registry
676 .register(Box::new(cpu_usage_percent.clone()))
677 .expect("Failed to register cpu_usage_percent");
678 registry
679 .register(Box::new(thread_count.clone()))
680 .expect("Failed to register thread_count");
681 registry
682 .register(Box::new(uptime_seconds.clone()))
683 .expect("Failed to register uptime_seconds");
684 registry
685 .register(Box::new(active_scenario_mode.clone()))
686 .expect("Failed to register active_scenario_mode");
687 registry
688 .register(Box::new(chaos_triggers_total.clone()))
689 .expect("Failed to register chaos_triggers_total");
690 registry
691 .register(Box::new(service_availability.clone()))
692 .expect("Failed to register service_availability");
693 registry
694 .register(Box::new(slo_compliance.clone()))
695 .expect("Failed to register slo_compliance");
696 registry
697 .register(Box::new(successful_request_rate.clone()))
698 .expect("Failed to register successful_request_rate");
699 registry
700 .register(Box::new(p95_latency_slo_compliance.clone()))
701 .expect("Failed to register p95_latency_slo_compliance");
702 registry
703 .register(Box::new(error_budget_remaining.clone()))
704 .expect("Failed to register error_budget_remaining");
705 registry
706 .register(Box::new(marketplace_publish_total.clone()))
707 .expect("Failed to register marketplace_publish_total");
708 registry
709 .register(Box::new(marketplace_publish_duration_seconds.clone()))
710 .expect("Failed to register marketplace_publish_duration_seconds");
711 registry
712 .register(Box::new(marketplace_download_total.clone()))
713 .expect("Failed to register marketplace_download_total");
714 registry
715 .register(Box::new(marketplace_download_duration_seconds.clone()))
716 .expect("Failed to register marketplace_download_duration_seconds");
717 registry
718 .register(Box::new(marketplace_search_total.clone()))
719 .expect("Failed to register marketplace_search_total");
720 registry
721 .register(Box::new(marketplace_search_duration_seconds.clone()))
722 .expect("Failed to register marketplace_search_duration_seconds");
723 registry
724 .register(Box::new(marketplace_errors_total.clone()))
725 .expect("Failed to register marketplace_errors_total");
726 registry
727 .register(Box::new(marketplace_items_total.clone()))
728 .expect("Failed to register marketplace_items_total");
729 registry
730 .register(Box::new(drift_percentage.clone()))
731 .expect("Failed to register drift_percentage");
732 registry
733 .register(Box::new(drift_total_changes.clone()))
734 .expect("Failed to register drift_total_changes");
735 registry
736 .register(Box::new(drift_breaking_changes.clone()))
737 .expect("Failed to register drift_breaking_changes");
738 registry
739 .register(Box::new(drift_budget_exceeded.clone()))
740 .expect("Failed to register drift_budget_exceeded");
741
742 debug!("Initialized Prometheus metrics registry");
743
744 Self {
745 registry: Arc::new(registry),
746 requests_total,
747 requests_duration_seconds,
748 requests_in_flight,
749 requests_by_path_total,
750 request_duration_by_path_seconds,
751 average_latency_by_path_seconds,
752 workspace_requests_total,
753 workspace_requests_duration_seconds,
754 workspace_active_routes,
755 workspace_errors_total,
756 errors_total,
757 error_rate,
758 plugin_executions_total,
759 plugin_execution_duration_seconds,
760 plugin_errors_total,
761 ws_connections_active,
762 ws_connections_total,
763 ws_connection_duration_seconds,
764 ws_messages_sent,
765 ws_messages_received,
766 ws_errors_total,
767 smtp_connections_active,
768 smtp_connections_total,
769 smtp_messages_received_total,
770 smtp_messages_stored_total,
771 smtp_errors_total,
772 mqtt_connections_active,
773 mqtt_connections_total,
774 mqtt_messages_published_total,
775 mqtt_messages_received_total,
776 mqtt_topics_active,
777 mqtt_subscriptions_active,
778 mqtt_retained_messages,
779 mqtt_errors_total,
780 memory_usage_bytes,
781 cpu_usage_percent,
782 thread_count,
783 uptime_seconds,
784 active_scenario_mode,
785 chaos_triggers_total,
786 service_availability,
787 slo_compliance,
788 successful_request_rate,
789 p95_latency_slo_compliance,
790 error_budget_remaining,
791 marketplace_publish_total,
792 marketplace_publish_duration_seconds,
793 marketplace_download_total,
794 marketplace_download_duration_seconds,
795 marketplace_search_total,
796 marketplace_search_duration_seconds,
797 marketplace_errors_total,
798 marketplace_items_total,
799 drift_percentage,
800 drift_total_changes,
801 drift_breaking_changes,
802 drift_budget_exceeded,
803 }
804 }
805
806 pub fn record_drift_evaluation(&self, sample: DriftEvaluationSample<'_>) {
821 let pct = if sample.total == 0 {
822 0.0
823 } else {
824 (sample.breaking + sample.potentially_breaking) as f64 / sample.total as f64 * 100.0
825 };
826 let labels = [sample.workspace_id, sample.endpoint, sample.method];
827 self.drift_percentage.with_label_values(&labels).set(pct);
828 self.drift_total_changes.with_label_values(&labels).set(sample.total as i64);
829 self.drift_breaking_changes
830 .with_label_values(&labels)
831 .set(sample.breaking as i64);
832 self.drift_budget_exceeded
833 .with_label_values(&labels)
834 .set(if sample.budget_exceeded { 1 } else { 0 });
835 }
836
837 pub fn registry(&self) -> &Registry {
839 &self.registry
840 }
841
842 pub fn is_initialized(&self) -> bool {
844 true
845 }
846
847 pub fn record_http_request(&self, method: &str, status: u16, duration_seconds: f64) {
849 self.record_http_request_with_pillar(method, status, duration_seconds, "");
850 }
851
852 pub fn record_http_request_with_pillar(
854 &self,
855 method: &str,
856 status: u16,
857 duration_seconds: f64,
858 pillar: &str,
859 ) {
860 let status_str = status.to_string();
861 let pillar_label = if pillar.is_empty() { "unknown" } else { pillar };
862 self.requests_total
863 .with_label_values(&["http", method, &status_str, pillar_label])
864 .inc();
865 self.requests_duration_seconds
866 .with_label_values(&["http", method, pillar_label])
867 .observe(duration_seconds);
868 }
869
870 pub fn record_grpc_request(&self, method: &str, status: &str, duration_seconds: f64) {
872 self.record_grpc_request_with_pillar(method, status, duration_seconds, "");
873 }
874
875 pub fn record_grpc_request_with_pillar(
877 &self,
878 method: &str,
879 status: &str,
880 duration_seconds: f64,
881 pillar: &str,
882 ) {
883 let pillar_label = if pillar.is_empty() { "unknown" } else { pillar };
884 self.requests_total
885 .with_label_values(&["grpc", method, status, pillar_label])
886 .inc();
887 self.requests_duration_seconds
888 .with_label_values(&["grpc", method, pillar_label])
889 .observe(duration_seconds);
890 }
891
892 pub fn record_ws_message_sent(&self) {
894 self.ws_messages_sent.inc();
895 }
896
897 pub fn record_ws_message_received(&self) {
899 self.ws_messages_received.inc();
900 }
901
902 pub fn record_graphql_request(&self, operation: &str, status: u16, duration_seconds: f64) {
904 let status_str = status.to_string();
905 self.requests_total
907 .with_label_values(&["graphql", operation, &status_str, "contracts"])
908 .inc();
909 self.requests_duration_seconds
910 .with_label_values(&["graphql", operation, "contracts"])
911 .observe(duration_seconds);
912 }
913
914 pub fn record_plugin_execution(&self, plugin_name: &str, success: bool, duration_seconds: f64) {
916 let status = if success { "success" } else { "failure" };
917 self.plugin_executions_total.with_label_values(&[plugin_name, status]).inc();
918 self.plugin_execution_duration_seconds
919 .with_label_values(&[plugin_name])
920 .observe(duration_seconds);
921 }
922
923 pub fn increment_in_flight(&self, protocol: &str) {
925 self.requests_in_flight.with_label_values(&[protocol]).inc();
926 }
927
928 pub fn decrement_in_flight(&self, protocol: &str) {
930 self.requests_in_flight.with_label_values(&[protocol]).dec();
931 }
932
933 pub fn record_error(&self, protocol: &str, error_type: &str) {
935 self.record_error_with_pillar(protocol, error_type, "");
936 }
937
938 pub fn record_error_with_pillar(&self, protocol: &str, error_type: &str, pillar: &str) {
940 let pillar_label = if pillar.is_empty() { "unknown" } else { pillar };
941 self.errors_total.with_label_values(&[protocol, error_type, pillar_label]).inc();
942 }
943
944 pub fn update_memory_usage(&self, bytes: f64) {
946 self.memory_usage_bytes.set(bytes);
947 }
948
949 pub fn update_cpu_usage(&self, percent: f64) {
951 self.cpu_usage_percent.set(percent);
952 }
953
954 pub fn set_scenario_mode(&self, mode: i64) {
956 self.active_scenario_mode.set(mode);
957 }
958
959 pub fn record_chaos_trigger(&self) {
961 self.chaos_triggers_total.inc();
962 }
963
964 pub fn record_http_request_with_path(
966 &self,
967 path: &str,
968 method: &str,
969 status: u16,
970 duration_seconds: f64,
971 ) {
972 self.record_http_request_with_path_and_pillar(path, method, status, duration_seconds, "");
973 }
974
975 pub fn record_http_request_with_path_and_pillar(
977 &self,
978 path: &str,
979 method: &str,
980 status: u16,
981 duration_seconds: f64,
982 pillar: &str,
983 ) {
984 let normalized_path = normalize_path(path);
986 let status_str = status.to_string();
987
988 self.requests_by_path_total
990 .with_label_values(&[normalized_path.as_str(), method, status_str.as_str()])
991 .inc();
992 self.request_duration_by_path_seconds
993 .with_label_values(&[normalized_path.as_str(), method])
994 .observe(duration_seconds);
995
996 let current = self
999 .average_latency_by_path_seconds
1000 .with_label_values(&[normalized_path.as_str(), method])
1001 .get();
1002 let new_avg = if current == 0.0 {
1003 duration_seconds
1004 } else {
1005 (current * 0.95) + (duration_seconds * 0.05)
1006 };
1007 self.average_latency_by_path_seconds
1008 .with_label_values(&[normalized_path.as_str(), method])
1009 .set(new_avg);
1010
1011 self.record_http_request_with_pillar(method, status, duration_seconds, pillar);
1013 }
1014
1015 pub fn record_ws_connection_established(&self) {
1017 self.ws_connections_total.inc();
1018 self.ws_connections_active.inc();
1019 }
1020
1021 pub fn record_ws_connection_closed(&self, duration_seconds: f64, status: &str) {
1023 self.ws_connections_active.dec();
1024 self.ws_connection_duration_seconds
1025 .with_label_values(&[status])
1026 .observe(duration_seconds);
1027 }
1028
1029 pub fn record_ws_error(&self) {
1031 self.ws_errors_total.inc();
1032 }
1033
1034 pub fn record_smtp_connection_established(&self) {
1036 self.smtp_connections_total.inc();
1037 self.smtp_connections_active.inc();
1038 }
1039
1040 pub fn record_smtp_connection_closed(&self) {
1042 self.smtp_connections_active.dec();
1043 }
1044
1045 pub fn record_smtp_message_received(&self) {
1047 self.smtp_messages_received_total.inc();
1048 }
1049
1050 pub fn record_smtp_message_stored(&self) {
1052 self.smtp_messages_stored_total.inc();
1053 }
1054
1055 pub fn record_smtp_error(&self, error_type: &str) {
1057 self.smtp_errors_total.with_label_values(&[error_type]).inc();
1058 }
1059
1060 pub fn update_thread_count(&self, count: f64) {
1062 self.thread_count.set(count);
1063 }
1064
1065 pub fn update_uptime(&self, seconds: f64) {
1067 self.uptime_seconds.set(seconds);
1068 }
1069
1070 pub fn record_workspace_request(
1074 &self,
1075 workspace_id: &str,
1076 method: &str,
1077 status: u16,
1078 duration_seconds: f64,
1079 ) {
1080 let status_str = status.to_string();
1081 self.workspace_requests_total
1082 .with_label_values(&[workspace_id, method, &status_str])
1083 .inc();
1084 self.workspace_requests_duration_seconds
1085 .with_label_values(&[workspace_id, method])
1086 .observe(duration_seconds);
1087 }
1088
1089 pub fn update_workspace_active_routes(&self, workspace_id: &str, count: i64) {
1091 self.workspace_active_routes.with_label_values(&[workspace_id]).set(count);
1092 }
1093
1094 pub fn record_workspace_error(&self, workspace_id: &str, error_type: &str) {
1096 self.workspace_errors_total.with_label_values(&[workspace_id, error_type]).inc();
1097 }
1098
1099 pub fn increment_workspace_routes(&self, workspace_id: &str) {
1101 self.workspace_active_routes.with_label_values(&[workspace_id]).inc();
1102 }
1103
1104 pub fn decrement_workspace_routes(&self, workspace_id: &str) {
1106 self.workspace_active_routes.with_label_values(&[workspace_id]).dec();
1107 }
1108
1109 pub fn record_marketplace_publish(
1113 &self,
1114 item_type: &str,
1115 success: bool,
1116 duration_seconds: f64,
1117 ) {
1118 let status = if success { "success" } else { "error" };
1119 self.marketplace_publish_total.with_label_values(&[item_type, status]).inc();
1120 self.marketplace_publish_duration_seconds
1121 .with_label_values(&[item_type])
1122 .observe(duration_seconds);
1123 }
1124
1125 pub fn record_marketplace_download(
1127 &self,
1128 item_type: &str,
1129 success: bool,
1130 duration_seconds: f64,
1131 ) {
1132 let status = if success { "success" } else { "error" };
1133 self.marketplace_download_total.with_label_values(&[item_type, status]).inc();
1134 self.marketplace_download_duration_seconds
1135 .with_label_values(&[item_type])
1136 .observe(duration_seconds);
1137 }
1138
1139 pub fn record_marketplace_search(&self, item_type: &str, success: bool, duration_seconds: f64) {
1141 let status = if success { "success" } else { "error" };
1142 self.marketplace_search_total.with_label_values(&[item_type, status]).inc();
1143 self.marketplace_search_duration_seconds
1144 .with_label_values(&[item_type])
1145 .observe(duration_seconds);
1146 }
1147
1148 pub fn record_marketplace_error(&self, item_type: &str, error_code: &str) {
1150 self.marketplace_errors_total.with_label_values(&[item_type, error_code]).inc();
1151 }
1152
1153 pub fn update_marketplace_items_total(&self, item_type: &str, count: i64) {
1155 self.marketplace_items_total.with_label_values(&[item_type]).set(count);
1156 }
1157}
1158
1159fn normalize_path(path: &str) -> String {
1164 let mut segments: Vec<&str> = path.split('/').collect();
1165
1166 for segment in &mut segments {
1167 if is_uuid(segment)
1169 || segment.parse::<i64>().is_ok()
1170 || (segment.len() > 8 && segment.chars().all(|c| c.is_ascii_hexdigit()))
1171 {
1172 *segment = ":id";
1173 }
1174 }
1175
1176 segments.join("/")
1177}
1178
1179fn is_uuid(s: &str) -> bool {
1181 s.len() == 36 && s.chars().filter(|&c| c == '-').count() == 4
1182}
1183
1184impl Default for MetricsRegistry {
1185 fn default() -> Self {
1186 Self::new()
1187 }
1188}
1189
1190static GLOBAL_REGISTRY: Lazy<MetricsRegistry> = Lazy::new(MetricsRegistry::new);
1192
1193pub fn get_global_registry() -> &'static MetricsRegistry {
1195 &GLOBAL_REGISTRY
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201
1202 #[test]
1203 fn test_metrics_registry_creation() {
1204 let registry = MetricsRegistry::new();
1205 assert!(registry.is_initialized());
1206 }
1207
1208 #[test]
1209 fn test_record_http_request() {
1210 let registry = MetricsRegistry::new();
1211 registry.record_http_request("GET", 200, 0.045);
1212 registry.record_http_request("POST", 201, 0.123);
1213
1214 assert!(registry.is_initialized());
1216 }
1217
1218 #[test]
1219 fn test_record_http_request_with_pillar() {
1220 let registry = MetricsRegistry::new();
1221 registry.record_http_request_with_pillar("GET", 200, 0.045, "reality");
1222 registry.record_http_request_with_pillar("POST", 201, 0.123, "contracts");
1223
1224 assert!(registry.is_initialized());
1226 }
1227
1228 #[test]
1229 fn test_global_registry() {
1230 let registry = get_global_registry();
1231 assert!(registry.is_initialized());
1232 }
1233
1234 #[test]
1235 fn test_record_drift_evaluation_basic_percentage() {
1236 let registry = MetricsRegistry::new();
1237 registry.record_drift_evaluation(DriftEvaluationSample {
1239 workspace_id: "ws-a",
1240 endpoint: "/users",
1241 method: "GET",
1242 total: 10,
1243 breaking: 1,
1244 potentially_breaking: 2,
1245 budget_exceeded: false,
1246 });
1247
1248 let pct = registry.drift_percentage.with_label_values(&["ws-a", "/users", "GET"]).get();
1249 assert!((pct - 30.0).abs() < f64::EPSILON);
1250 assert_eq!(
1251 registry.drift_total_changes.with_label_values(&["ws-a", "/users", "GET"]).get(),
1252 10
1253 );
1254 assert_eq!(
1255 registry
1256 .drift_breaking_changes
1257 .with_label_values(&["ws-a", "/users", "GET"])
1258 .get(),
1259 1
1260 );
1261 assert_eq!(
1262 registry
1263 .drift_budget_exceeded
1264 .with_label_values(&["ws-a", "/users", "GET"])
1265 .get(),
1266 0
1267 );
1268 }
1269
1270 #[test]
1271 fn test_record_drift_evaluation_zero_total_is_zero_percent() {
1272 let registry = MetricsRegistry::new();
1273 registry.record_drift_evaluation(DriftEvaluationSample {
1275 workspace_id: "",
1276 endpoint: "/health",
1277 method: "GET",
1278 total: 0,
1279 breaking: 0,
1280 potentially_breaking: 0,
1281 budget_exceeded: false,
1282 });
1283 let pct = registry.drift_percentage.with_label_values(&["", "/health", "GET"]).get();
1284 assert_eq!(pct, 0.0);
1285 }
1286
1287 #[test]
1288 fn test_record_drift_evaluation_budget_exceeded_flag() {
1289 let registry = MetricsRegistry::new();
1290 registry.record_drift_evaluation(DriftEvaluationSample {
1291 workspace_id: "ws-b",
1292 endpoint: "/orders",
1293 method: "POST",
1294 total: 4,
1295 breaking: 2,
1296 potentially_breaking: 0,
1297 budget_exceeded: true,
1298 });
1299 assert_eq!(
1300 registry
1301 .drift_budget_exceeded
1302 .with_label_values(&["ws-b", "/orders", "POST"])
1303 .get(),
1304 1
1305 );
1306 }
1307
1308 #[test]
1309 fn test_plugin_metrics() {
1310 let registry = MetricsRegistry::new();
1311 registry.record_plugin_execution("test-plugin", true, 0.025);
1312 registry.record_plugin_execution("test-plugin", false, 0.050);
1313 assert!(registry.is_initialized());
1314 }
1315
1316 #[test]
1317 fn test_websocket_metrics() {
1318 let registry = MetricsRegistry::new();
1319 registry.record_ws_message_sent();
1320 registry.record_ws_message_received();
1321 registry.record_ws_connection_established();
1322 registry.record_ws_connection_closed(120.5, "normal");
1323 registry.record_ws_error();
1324 assert!(registry.is_initialized());
1325 }
1326
1327 #[test]
1328 fn test_path_normalization() {
1329 assert_eq!(normalize_path("/api/users/123"), "/api/users/:id");
1330 assert_eq!(
1331 normalize_path("/api/users/550e8400-e29b-41d4-a716-446655440000"),
1332 "/api/users/:id"
1333 );
1334 assert_eq!(normalize_path("/api/users/abc123def456"), "/api/users/:id");
1335 assert_eq!(normalize_path("/api/users/list"), "/api/users/list");
1336 }
1337
1338 #[test]
1339 fn test_path_based_metrics() {
1340 let registry = MetricsRegistry::new();
1341 registry.record_http_request_with_path("/api/users/123", "GET", 200, 0.045);
1342 registry.record_http_request_with_path("/api/users/456", "GET", 200, 0.055);
1343 registry.record_http_request_with_path("/api/posts", "POST", 201, 0.123);
1344 assert!(registry.is_initialized());
1345 }
1346
1347 #[test]
1348 fn test_smtp_metrics() {
1349 let registry = MetricsRegistry::new();
1350 registry.record_smtp_connection_established();
1351 registry.record_smtp_message_received();
1352 registry.record_smtp_message_stored();
1353 registry.record_smtp_connection_closed();
1354 registry.record_smtp_error("timeout");
1355 assert!(registry.is_initialized());
1356 }
1357
1358 #[test]
1359 fn test_system_metrics() {
1360 let registry = MetricsRegistry::new();
1361 registry.update_memory_usage(1024.0 * 1024.0 * 100.0); registry.update_cpu_usage(45.5);
1363 registry.update_thread_count(25.0);
1364 registry.update_uptime(3600.0); assert!(registry.is_initialized());
1366 }
1367
1368 #[test]
1369 fn test_workspace_metrics() {
1370 let registry = MetricsRegistry::new();
1371
1372 registry.record_workspace_request("workspace1", "GET", 200, 0.045);
1374 registry.record_workspace_request("workspace1", "POST", 201, 0.123);
1375 registry.record_workspace_request("workspace2", "GET", 200, 0.055);
1376
1377 registry.update_workspace_active_routes("workspace1", 10);
1379 registry.update_workspace_active_routes("workspace2", 5);
1380
1381 registry.record_workspace_error("workspace1", "validation");
1383 registry.record_workspace_error("workspace2", "timeout");
1384
1385 registry.increment_workspace_routes("workspace1");
1387 registry.decrement_workspace_routes("workspace1");
1388
1389 assert!(registry.is_initialized());
1390 }
1391
1392 #[test]
1393 fn test_workspace_metrics_isolation() {
1394 let registry = MetricsRegistry::new();
1395
1396 registry.record_workspace_request("ws1", "GET", 200, 0.1);
1398 registry.record_workspace_request("ws2", "GET", 200, 0.2);
1399
1400 registry.update_workspace_active_routes("ws1", 5);
1401 registry.update_workspace_active_routes("ws2", 10);
1402
1403 assert!(registry.is_initialized());
1405 }
1406}