Skip to main content

mockforge_observability/prometheus/
metrics.rs

1//! Prometheus metrics definitions and registry
2
3use 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/// A single drift evaluation sample to feed into
12/// [`MetricsRegistry::record_drift_evaluation`]. Borrows the string labels
13/// so callers don't have to allocate per-request.
14///
15/// `workspace_id` should be the empty string when the request isn't tied to a
16/// specific tenant (the global "drift-by-endpoint" series remains useful and
17/// the dashboards collapse the label).
18#[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/// Global metrics registry for MockForge
30#[derive(Clone)]
31pub struct MetricsRegistry {
32    registry: Arc<Registry>,
33
34    // Request metrics by protocol
35    pub requests_total: IntCounterVec,
36    pub requests_duration_seconds: HistogramVec,
37    pub requests_in_flight: IntGaugeVec,
38
39    // Request metrics by path (endpoint-specific)
40    pub requests_by_path_total: IntCounterVec,
41    pub request_duration_by_path_seconds: HistogramVec,
42    pub average_latency_by_path_seconds: GaugeVec,
43
44    // Workspace-specific metrics
45    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    // Error metrics
51    pub errors_total: IntCounterVec,
52    pub error_rate: GaugeVec,
53
54    // Plugin metrics
55    pub plugin_executions_total: IntCounterVec,
56    pub plugin_execution_duration_seconds: HistogramVec,
57    pub plugin_errors_total: IntCounterVec,
58
59    // WebSocket specific metrics
60    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    // SMTP specific metrics
68    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    // MQTT specific metrics
75    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    // System metrics
85    pub memory_usage_bytes: Gauge,
86    pub cpu_usage_percent: Gauge,
87    pub thread_count: Gauge,
88    pub uptime_seconds: Gauge,
89
90    // Scenario metrics (for Phase 4)
91    pub active_scenario_mode: IntGauge,
92    pub chaos_triggers_total: IntCounter,
93
94    // Business/SLO metrics
95    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    // Marketplace metrics
102    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    // Contract-drift metrics (issue #678) — emitted whenever
112    // `DriftBudgetEngine::evaluate*` runs against a request via the HTTP
113    // drift-tracking middleware. Labelled by workspace and endpoint so the
114    // MockOps UI's DriftPercentageDashboard can break down per-resource.
115    /// Drift severity as a 0.0–100.0 percentage (breaking + potentially-breaking
116    /// changes ÷ total observed mismatches × 100). 0 means no drift detected.
117    pub drift_percentage: GaugeVec,
118    /// Count of total mismatches observed in the last evaluation.
119    pub drift_total_changes: IntGaugeVec,
120    /// Count of breaking changes in the last evaluation. > 0 indicates a
121    /// contract-breaking drift event that should page someone.
122    pub drift_breaking_changes: IntGaugeVec,
123    /// Boolean (0 or 1) — was the configured drift budget exceeded?
124    pub drift_budget_exceeded: IntGaugeVec,
125}
126
127impl MetricsRegistry {
128    /// Create a new metrics registry with all metrics initialized
129    pub fn new() -> Self {
130        let registry = Registry::new();
131
132        // Request metrics (with pillar label)
133        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        // Error metrics (with pillar label)
164        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        // Plugin metrics
180        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        // WebSocket metrics
203        // Path-based request metrics
204        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        // Workspace-specific metrics
235        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        // WebSocket metrics
272        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        // SMTP metrics
311        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        // MQTT metrics
338        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        // System metrics
383        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        // Scenario metrics
397        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        // Business/SLO metrics
410        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        // Marketplace metrics
456        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"], // type: plugin, template, scenario; status: success, error
462        )
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"], // type: plugin, template, scenario
472        )
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"], // type: plugin, template, scenario; status: success, error
481        )
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"], // type: plugin, template, scenario
491        )
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"], // type: plugin, template, scenario; status: success, error
500        )
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"], // type: plugin, template, scenario
510        )
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"], // type: plugin, template, scenario; error_code: validation_failed, not_found, etc.
519        )
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"], // type: plugin, template, scenario
528        )
529        .expect("Failed to create marketplace_items_total metric");
530
531        // Drift metrics (#678). Labelled by workspace + endpoint + method so
532        // both the MockOps drift dashboard and per-API alerting work.
533        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        // Register all metrics
570        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    /// Record a contract-drift evaluation result against the workspace +
807    /// endpoint Prometheus gauges. Closes part of #678.
808    ///
809    /// Callers pass:
810    /// - `workspace_id` — empty string for un-attributed evaluations (legacy)
811    /// - `endpoint` and `method` — the request that was evaluated
812    /// - `total`, `breaking`, `potentially_breaking`, `non_breaking` — counts
813    ///   from `DriftResult`
814    /// - `budget_exceeded` — whether the configured budget tripped
815    ///
816    /// The "drift percentage" is computed as
817    /// `(breaking + potentially_breaking) / total * 100`. A zero-total
818    /// evaluation records 0% drift (the request matched the contract
819    /// exactly).
820    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    /// Get the underlying Prometheus registry
838    pub fn registry(&self) -> &Registry {
839        &self.registry
840    }
841
842    /// Check if the registry is initialized
843    pub fn is_initialized(&self) -> bool {
844        true
845    }
846
847    /// Record an HTTP request
848    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    /// Record an HTTP request with pillar information
853    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    /// Record a gRPC request
871    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    /// Record a gRPC request with pillar information
876    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    /// Record a WebSocket message
893    pub fn record_ws_message_sent(&self) {
894        self.ws_messages_sent.inc();
895    }
896
897    /// Record a WebSocket message received
898    pub fn record_ws_message_received(&self) {
899        self.ws_messages_received.inc();
900    }
901
902    /// Record a GraphQL request
903    pub fn record_graphql_request(&self, operation: &str, status: u16, duration_seconds: f64) {
904        let status_str = status.to_string();
905        // GraphQL requests are categorized under the "contracts" pillar
906        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    /// Record a plugin execution
915    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    /// Increment in-flight requests
924    pub fn increment_in_flight(&self, protocol: &str) {
925        self.requests_in_flight.with_label_values(&[protocol]).inc();
926    }
927
928    /// Decrement in-flight requests
929    pub fn decrement_in_flight(&self, protocol: &str) {
930        self.requests_in_flight.with_label_values(&[protocol]).dec();
931    }
932
933    /// Record an error
934    pub fn record_error(&self, protocol: &str, error_type: &str) {
935        self.record_error_with_pillar(protocol, error_type, "");
936    }
937
938    /// Record an error with pillar information
939    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    /// Update memory usage
945    pub fn update_memory_usage(&self, bytes: f64) {
946        self.memory_usage_bytes.set(bytes);
947    }
948
949    /// Update CPU usage
950    pub fn update_cpu_usage(&self, percent: f64) {
951        self.cpu_usage_percent.set(percent);
952    }
953
954    /// Set active scenario mode (0=healthy, 1=degraded, 2=error, 3=chaos)
955    pub fn set_scenario_mode(&self, mode: i64) {
956        self.active_scenario_mode.set(mode);
957    }
958
959    /// Record a chaos trigger
960    pub fn record_chaos_trigger(&self) {
961        self.chaos_triggers_total.inc();
962    }
963
964    /// Record an HTTP request with path information
965    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    /// Record an HTTP request with path and pillar information
976    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        // Normalize path to avoid cardinality explosion
985        let normalized_path = normalize_path(path);
986        let status_str = status.to_string();
987
988        // Record by path
989        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        // Update average latency (simple moving average approximation)
997        // Note: For production use, consider using a proper moving average or quantiles
998        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        // Also record in the general metrics with pillar
1012        self.record_http_request_with_pillar(method, status, duration_seconds, pillar);
1013    }
1014
1015    /// Record a WebSocket connection established
1016    pub fn record_ws_connection_established(&self) {
1017        self.ws_connections_total.inc();
1018        self.ws_connections_active.inc();
1019    }
1020
1021    /// Record a WebSocket connection closed
1022    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    /// Record a WebSocket error
1030    pub fn record_ws_error(&self) {
1031        self.ws_errors_total.inc();
1032    }
1033
1034    /// Record an SMTP connection established
1035    pub fn record_smtp_connection_established(&self) {
1036        self.smtp_connections_total.inc();
1037        self.smtp_connections_active.inc();
1038    }
1039
1040    /// Record an SMTP connection closed
1041    pub fn record_smtp_connection_closed(&self) {
1042        self.smtp_connections_active.dec();
1043    }
1044
1045    /// Record an SMTP message received
1046    pub fn record_smtp_message_received(&self) {
1047        self.smtp_messages_received_total.inc();
1048    }
1049
1050    /// Record an SMTP message stored
1051    pub fn record_smtp_message_stored(&self) {
1052        self.smtp_messages_stored_total.inc();
1053    }
1054
1055    /// Record an SMTP error
1056    pub fn record_smtp_error(&self, error_type: &str) {
1057        self.smtp_errors_total.with_label_values(&[error_type]).inc();
1058    }
1059
1060    /// Update thread count
1061    pub fn update_thread_count(&self, count: f64) {
1062        self.thread_count.set(count);
1063    }
1064
1065    /// Update uptime
1066    pub fn update_uptime(&self, seconds: f64) {
1067        self.uptime_seconds.set(seconds);
1068    }
1069
1070    // ==================== Workspace-specific metrics ====================
1071
1072    /// Record a workspace request
1073    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    /// Update workspace active routes count
1090    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    /// Record a workspace error
1095    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    /// Increment workspace active routes
1100    pub fn increment_workspace_routes(&self, workspace_id: &str) {
1101        self.workspace_active_routes.with_label_values(&[workspace_id]).inc();
1102    }
1103
1104    /// Decrement workspace active routes
1105    pub fn decrement_workspace_routes(&self, workspace_id: &str) {
1106        self.workspace_active_routes.with_label_values(&[workspace_id]).dec();
1107    }
1108
1109    // ==================== Marketplace metrics ====================
1110
1111    /// Record a marketplace publish operation
1112    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    /// Record a marketplace download operation
1126    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    /// Record a marketplace search operation
1140    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    /// Record a marketplace error
1149    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    /// Update the total number of marketplace items
1154    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
1159/// Normalize path to avoid high cardinality
1160///
1161/// This function replaces dynamic path segments (IDs, UUIDs, etc.) with placeholders
1162/// to prevent metric explosion.
1163fn normalize_path(path: &str) -> String {
1164    let mut segments: Vec<&str> = path.split('/').collect();
1165
1166    for segment in &mut segments {
1167        // Replace UUIDs, numeric IDs, or hex strings with :id placeholder
1168        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
1179/// Check if a string is a UUID
1180fn 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
1190/// Global metrics registry instance
1191static GLOBAL_REGISTRY: Lazy<MetricsRegistry> = Lazy::new(MetricsRegistry::new);
1192
1193/// Get the global metrics registry
1194pub 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        // Verify metrics were recorded (they should not panic)
1215        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        // Verify metrics were recorded (they should not panic)
1225        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        // 1 breaking + 2 potentially_breaking out of 10 total = 30%
1238        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        // No mismatches observed → 0% drift, not a divide-by-zero.
1274        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); // 100 MB
1362        registry.update_cpu_usage(45.5);
1363        registry.update_thread_count(25.0);
1364        registry.update_uptime(3600.0); // 1 hour
1365        assert!(registry.is_initialized());
1366    }
1367
1368    #[test]
1369    fn test_workspace_metrics() {
1370        let registry = MetricsRegistry::new();
1371
1372        // Record workspace requests
1373        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        // Update active routes
1378        registry.update_workspace_active_routes("workspace1", 10);
1379        registry.update_workspace_active_routes("workspace2", 5);
1380
1381        // Record errors
1382        registry.record_workspace_error("workspace1", "validation");
1383        registry.record_workspace_error("workspace2", "timeout");
1384
1385        // Test increment/decrement
1386        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        // Ensure metrics for different workspaces are independent
1397        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        // Both should be tracked independently
1404        assert!(registry.is_initialized());
1405    }
1406}