Skip to main content

rust_serv/metrics/
prometheus.rs

1//! Prometheus format exporter
2
3use super::collector::MetricsCollector;
4use super::counter::Counter;
5use super::gauge::Gauge;
6use super::histogram::Histogram;
7
8/// Prometheus text format exporter
9pub struct PrometheusExporter {
10    collector: MetricsCollector,
11    namespace: String,
12}
13
14impl PrometheusExporter {
15    /// Create a new Prometheus exporter
16    pub fn new(collector: MetricsCollector) -> Self {
17        Self {
18            collector,
19            namespace: "rust_serv".to_string(),
20        }
21    }
22
23    /// Create a new Prometheus exporter with custom namespace
24    pub fn with_namespace(collector: MetricsCollector, namespace: impl Into<String>) -> Self {
25        Self {
26            collector,
27            namespace: namespace.into(),
28        }
29    }
30
31    /// Export metrics in Prometheus text format
32    pub fn export(&self) -> String {
33        let mut output = String::new();
34        
35        // Export counters
36        for counter in self.collector.all_counters() {
37            output.push_str(&self.format_counter(&counter));
38            output.push('\n');
39        }
40        
41        // Export gauges
42        for gauge in self.collector.all_gauges() {
43            output.push_str(&self.format_gauge(&gauge));
44            output.push('\n');
45        }
46        
47        // Export histograms
48        for histogram in self.collector.all_histograms() {
49            output.push_str(&self.format_histogram(&histogram));
50            output.push('\n');
51        }
52        
53        output.trim_end().to_string()
54    }
55
56    /// Format a counter in Prometheus format
57    fn format_counter(&self, counter: &Counter) -> String {
58        let metric_name = self.namespaced_name(counter.name());
59        format!(
60            "# HELP {} {}\n# TYPE {} counter\n{} {}",
61            metric_name,
62            counter.help(),
63            metric_name,
64            metric_name,
65            counter.get()
66        )
67    }
68
69    /// Format a gauge in Prometheus format
70    fn format_gauge(&self, gauge: &Gauge) -> String {
71        let metric_name = self.namespaced_name(gauge.name());
72        format!(
73            "# HELP {} {}\n# TYPE {} gauge\n{} {}",
74            metric_name,
75            gauge.help(),
76            metric_name,
77            metric_name,
78            gauge.get()
79        )
80    }
81
82    /// Format a histogram in Prometheus format
83    fn format_histogram(&self, histogram: &Histogram) -> String {
84        let base_name = self.namespaced_name(histogram.name());
85        let mut output = String::new();
86        
87        // HELP and TYPE
88        output.push_str(&format!("# HELP {} {}\n", base_name, histogram.help()));
89        output.push_str(&format!("# TYPE {} histogram\n", base_name));
90        
91        // Bucket lines (cumulative)
92        let bucket_counts = histogram.bucket_counts();
93        let boundaries = histogram.boundaries();
94        
95        for (idx, count) in bucket_counts.iter().enumerate() {
96            if idx < boundaries.len() {
97                output.push_str(&format!(
98                    "{}_bucket{{le=\"{}\"}} {}\n",
99                    base_name, boundaries[idx], count
100                ));
101            } else {
102                // +Inf bucket
103                output.push_str(&format!(
104                    "{}_bucket{{le=\"+Inf\"}} {}\n",
105                    base_name, count
106                ));
107            }
108        }
109        
110        // Sum and count
111        output.push_str(&format!("{}_sum {}\n", base_name, histogram.sum()));
112        output.push_str(&format!("{}_count {}", base_name, histogram.count()));
113        
114        output.trim_end().to_string()
115    }
116
117    /// Create a namespaced metric name
118    fn namespaced_name(&self, name: &str) -> String {
119        format!("{}_{}", self.namespace, name)
120    }
121
122    /// Get the collector
123    pub fn collector(&self) -> &MetricsCollector {
124        &self.collector
125    }
126
127    /// Get mutable collector
128    pub fn collector_mut(&mut self) -> &mut MetricsCollector {
129        &mut self.collector
130    }
131
132    /// Get the namespace
133    pub fn namespace(&self) -> &str {
134        &self.namespace
135    }
136
137    /// Set the namespace
138    pub fn set_namespace(&mut self, namespace: impl Into<String>) {
139        self.namespace = namespace.into();
140    }
141}
142
143impl Clone for PrometheusExporter {
144    fn clone(&self) -> Self {
145        Self {
146            collector: MetricsCollector::new(),
147            namespace: self.namespace.clone(),
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn create_test_collector() -> MetricsCollector {
157        MetricsCollector::new()
158    }
159
160    #[test]
161    fn test_exporter_creation() {
162        let collector = create_test_collector();
163        let exporter = PrometheusExporter::new(collector);
164        assert_eq!(exporter.namespace(), "rust_serv");
165    }
166
167    #[test]
168    fn test_exporter_with_namespace() {
169        let collector = create_test_collector();
170        let exporter = PrometheusExporter::with_namespace(collector, "my_app");
171        assert_eq!(exporter.namespace(), "my_app");
172    }
173
174    #[test]
175    fn test_export_empty() {
176        let collector = create_test_collector();
177        let exporter = PrometheusExporter::new(collector);
178        let output = exporter.export();
179        assert!(output.is_empty());
180    }
181
182    #[test]
183    fn test_export_counter() {
184        let collector = create_test_collector();
185        let counter = collector.create_counter("requests_total", "Total requests");
186        counter.inc();
187        counter.inc();
188        
189        let exporter = PrometheusExporter::new(collector);
190        let output = exporter.export();
191        
192        assert!(output.contains("# HELP rust_serv_requests_total Total requests"));
193        assert!(output.contains("# TYPE rust_serv_requests_total counter"));
194        assert!(output.contains("rust_serv_requests_total 2"));
195    }
196
197    #[test]
198    fn test_export_gauge() {
199        let collector = create_test_collector();
200        let gauge = collector.create_gauge("active_connections", "Active connections");
201        gauge.set(42);
202        
203        let exporter = PrometheusExporter::new(collector);
204        let output = exporter.export();
205        
206        assert!(output.contains("# HELP rust_serv_active_connections Active connections"));
207        assert!(output.contains("# TYPE rust_serv_active_connections gauge"));
208        assert!(output.contains("rust_serv_active_connections 42"));
209    }
210
211    #[test]
212    fn test_export_histogram() {
213        let collector = create_test_collector();
214        let hist = collector.create_histogram("request_duration", "Request duration");
215        hist.observe(0.05);
216        hist.observe(0.15);
217        
218        let exporter = PrometheusExporter::new(collector);
219        let output = exporter.export();
220        
221        assert!(output.contains("# HELP rust_serv_request_duration Request duration"));
222        assert!(output.contains("# TYPE rust_serv_request_duration histogram"));
223        assert!(output.contains("rust_serv_request_duration_bucket"));
224        assert!(output.contains("rust_serv_request_duration_sum"));
225        assert!(output.contains("rust_serv_request_duration_count"));
226        assert!(output.contains("le=\"+Inf\""));
227    }
228
229    #[test]
230    fn test_export_multiple_metrics() {
231        let collector = create_test_collector();
232        
233        let counter = collector.create_counter("requests", "Total requests");
234        counter.inc();
235        
236        let gauge = collector.create_gauge("connections", "Active connections");
237        gauge.set(5);
238        
239        let exporter = PrometheusExporter::new(collector);
240        let output = exporter.export();
241        
242        assert!(output.contains("rust_serv_requests 1"));
243        assert!(output.contains("rust_serv_connections 5"));
244    }
245
246    #[test]
247    fn test_set_namespace() {
248        let collector = create_test_collector();
249        let mut exporter = PrometheusExporter::new(collector);
250        exporter.set_namespace("new_namespace");
251        
252        assert_eq!(exporter.namespace(), "new_namespace");
253    }
254
255    #[test]
256    fn test_exporter_clone() {
257        let collector = create_test_collector();
258        let exporter = PrometheusExporter::with_namespace(collector, "test_ns");
259        
260        let cloned = exporter.clone();
261        assert_eq!(cloned.namespace(), "test_ns");
262    }
263
264    #[test]
265    fn test_collector_access() {
266        let collector = create_test_collector();
267        let mut exporter = PrometheusExporter::new(collector);
268        
269        // Access collector
270        let _ = exporter.collector();
271        
272        // Access mutable collector
273        let collector = exporter.collector_mut();
274        collector.create_counter("new_counter", "New counter");
275        
276        assert!(exporter.collector().get_counter("new_counter").is_some());
277    }
278
279    #[test]
280    fn test_histogram_bucket_cumulative() {
281        let collector = create_test_collector();
282        let hist = collector.create_histogram("latency", "Latency");
283        
284        // Observe values in different buckets
285        hist.observe(0.001);
286        hist.observe(0.05);
287        hist.observe(0.5);
288        
289        let exporter = PrometheusExporter::new(collector);
290        let output = exporter.export();
291        
292        assert!(output.contains("rust_serv_latency_bucket"));
293        assert!(output.contains("rust_serv_latency_count 3"));
294    }
295
296    #[test]
297    fn test_negative_gauge_value() {
298        let collector = create_test_collector();
299        let gauge = collector.create_gauge("temperature", "Temperature");
300        gauge.set(-10);
301        
302        let exporter = PrometheusExporter::new(collector);
303        let output = exporter.export();
304        
305        assert!(output.contains("rust_serv_temperature -10"));
306    }
307
308    #[test]
309    fn test_large_counter_value() {
310        let collector = create_test_collector();
311        let counter = collector.create_counter("bytes", "Total bytes");
312        counter.add(1_000_000_000);
313        
314        let exporter = PrometheusExporter::new(collector);
315        let output = exporter.export();
316        
317        assert!(output.contains("rust_serv_bytes 1000000000"));
318    }
319}