rust_serv/metrics/
prometheus.rs1use super::collector::MetricsCollector;
4use super::counter::Counter;
5use super::gauge::Gauge;
6use super::histogram::Histogram;
7
8pub struct PrometheusExporter {
10 collector: MetricsCollector,
11 namespace: String,
12}
13
14impl PrometheusExporter {
15 pub fn new(collector: MetricsCollector) -> Self {
17 Self {
18 collector,
19 namespace: "rust_serv".to_string(),
20 }
21 }
22
23 pub fn with_namespace(collector: MetricsCollector, namespace: impl Into<String>) -> Self {
25 Self {
26 collector,
27 namespace: namespace.into(),
28 }
29 }
30
31 pub fn export(&self) -> String {
33 let mut output = String::new();
34
35 for counter in self.collector.all_counters() {
37 output.push_str(&self.format_counter(&counter));
38 output.push('\n');
39 }
40
41 for gauge in self.collector.all_gauges() {
43 output.push_str(&self.format_gauge(&gauge));
44 output.push('\n');
45 }
46
47 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 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 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 fn format_histogram(&self, histogram: &Histogram) -> String {
84 let base_name = self.namespaced_name(histogram.name());
85 let mut output = String::new();
86
87 output.push_str(&format!("# HELP {} {}\n", base_name, histogram.help()));
89 output.push_str(&format!("# TYPE {} histogram\n", base_name));
90
91 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 output.push_str(&format!(
104 "{}_bucket{{le=\"+Inf\"}} {}\n",
105 base_name, count
106 ));
107 }
108 }
109
110 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 fn namespaced_name(&self, name: &str) -> String {
119 format!("{}_{}", self.namespace, name)
120 }
121
122 pub fn collector(&self) -> &MetricsCollector {
124 &self.collector
125 }
126
127 pub fn collector_mut(&mut self) -> &mut MetricsCollector {
129 &mut self.collector
130 }
131
132 pub fn namespace(&self) -> &str {
134 &self.namespace
135 }
136
137 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 let _ = exporter.collector();
271
272 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 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}