1use crate::StorageMetrics;
21use std::fmt::Write;
22
23#[derive(Debug, Clone)]
25pub struct PrometheusExporter {
26 namespace: String,
28 labels: Vec<(String, String)>,
30}
31
32impl PrometheusExporter {
33 pub fn new(namespace: String) -> Self {
35 Self {
36 namespace,
37 labels: Vec::new(),
38 }
39 }
40
41 pub fn with_label(mut self, key: String, value: String) -> Self {
43 self.labels.push((key, value));
44 self
45 }
46
47 pub fn export(&self, metrics: &StorageMetrics) -> String {
49 let mut output = String::new();
50 let labels = self.format_labels();
51
52 macro_rules! write_metric {
54 ($name:expr, $type:expr, $help:expr, $value:expr) => {
55 writeln!(output, "# HELP {}_{} {}", self.namespace, $name, $help)
56 .expect("write to String is infallible");
57 writeln!(output, "# TYPE {}_{} {}", self.namespace, $name, $type)
58 .expect("write to String is infallible");
59 writeln!(output, "{}_{}{} {}", self.namespace, $name, labels, $value)
60 .expect("write to String is infallible");
61 };
62 }
63
64 write_metric!(
66 "put_total",
67 "counter",
68 "Total number of put operations",
69 metrics.put_count
70 );
71 write_metric!(
72 "get_total",
73 "counter",
74 "Total number of get operations",
75 metrics.get_count
76 );
77 write_metric!(
78 "has_total",
79 "counter",
80 "Total number of has operations",
81 metrics.has_count
82 );
83 write_metric!(
84 "delete_total",
85 "counter",
86 "Total number of delete operations",
87 metrics.delete_count
88 );
89
90 write_metric!(
92 "get_hits_total",
93 "counter",
94 "Total number of successful gets",
95 metrics.get_hits
96 );
97 write_metric!(
98 "get_misses_total",
99 "counter",
100 "Total number of failed gets",
101 metrics.get_misses
102 );
103 write_metric!(
104 "cache_hit_rate",
105 "gauge",
106 "Cache hit rate (0.0 to 1.0)",
107 metrics.cache_hit_rate()
108 );
109
110 write_metric!(
112 "bytes_written_total",
113 "counter",
114 "Total bytes written",
115 metrics.bytes_written
116 );
117 write_metric!(
118 "bytes_read_total",
119 "counter",
120 "Total bytes read",
121 metrics.bytes_read
122 );
123
124 write_metric!(
126 "put_latency_microseconds",
127 "gauge",
128 "Average put operation latency in microseconds",
129 metrics.avg_put_latency_us
130 );
131 write_metric!(
132 "get_latency_microseconds",
133 "gauge",
134 "Average get operation latency in microseconds",
135 metrics.avg_get_latency_us
136 );
137 write_metric!(
138 "has_latency_microseconds",
139 "gauge",
140 "Average has operation latency in microseconds",
141 metrics.avg_has_latency_us
142 );
143 write_metric!(
144 "peak_put_latency_microseconds",
145 "gauge",
146 "Peak put operation latency in microseconds",
147 metrics.peak_put_latency_us
148 );
149 write_metric!(
150 "peak_get_latency_microseconds",
151 "gauge",
152 "Peak get operation latency in microseconds",
153 metrics.peak_get_latency_us
154 );
155 write_metric!(
156 "operation_latency_microseconds",
157 "gauge",
158 "Average operation latency in microseconds",
159 metrics.avg_operation_latency_us()
160 );
161
162 write_metric!(
164 "errors_total",
165 "counter",
166 "Total number of errors encountered",
167 metrics.error_count
168 );
169
170 output
171 }
172
173 fn format_labels(&self) -> String {
175 if self.labels.is_empty() {
176 String::new()
177 } else {
178 let label_str = self
179 .labels
180 .iter()
181 .map(|(k, v)| format!("{k}=\"{v}\""))
182 .collect::<Vec<_>>()
183 .join(",");
184 format!("{{{label_str}}}")
185 }
186 }
187
188 pub fn export_http(&self, metrics: &StorageMetrics) -> (String, String) {
190 let body = self.export(metrics);
191 let content_type = "text/plain; version=0.0.4; charset=utf-8".to_string();
192 (content_type, body)
193 }
194}
195
196#[derive(Debug, Default)]
198pub struct PrometheusExporterBuilder {
199 namespace: Option<String>,
200 labels: Vec<(String, String)>,
201}
202
203impl PrometheusExporterBuilder {
204 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn namespace(mut self, namespace: String) -> Self {
211 self.namespace = Some(namespace);
212 self
213 }
214
215 pub fn label(mut self, key: String, value: String) -> Self {
217 self.labels.push((key, value));
218 self
219 }
220
221 pub fn instance(self, instance: String) -> Self {
223 self.label("instance".to_string(), instance)
224 }
225
226 pub fn job(self, job: String) -> Self {
228 self.label("job".to_string(), job)
229 }
230
231 pub fn build(self) -> PrometheusExporter {
233 let namespace = self
234 .namespace
235 .unwrap_or_else(|| "ipfrs_storage".to_string());
236 PrometheusExporter {
237 namespace,
238 labels: self.labels,
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_prometheus_export_basic() {
249 let metrics = StorageMetrics {
250 put_count: 100,
251 get_count: 200,
252 get_hits: 180,
253 get_misses: 20,
254 bytes_written: 1024000,
255 bytes_read: 2048000,
256 ..StorageMetrics::default()
257 };
258
259 let exporter = PrometheusExporter::new("test".to_string());
260 let output = exporter.export(&metrics);
261
262 assert!(output.contains("# HELP test_put_total"));
264 assert!(output.contains("# TYPE test_put_total counter"));
265 assert!(output.contains("test_put_total 100"));
266 assert!(output.contains("test_get_total 200"));
267 assert!(output.contains("test_get_hits_total 180"));
268 assert!(output.contains("test_get_misses_total 20"));
269 assert!(output.contains("test_bytes_written_total 1024000"));
270 assert!(output.contains("test_bytes_read_total 2048000"));
271 }
272
273 #[test]
274 fn test_prometheus_export_with_labels() {
275 let metrics = StorageMetrics::default();
276 let exporter = PrometheusExporter::new("test".to_string())
277 .with_label("instance".to_string(), "node1".to_string())
278 .with_label("datacenter".to_string(), "us-west".to_string());
279
280 let output = exporter.export(&metrics);
281
282 assert!(output.contains("{instance=\"node1\",datacenter=\"us-west\"}"));
284 }
285
286 #[test]
287 fn test_prometheus_export_cache_hit_rate() {
288 let metrics = StorageMetrics {
289 get_hits: 90,
290 get_misses: 10,
291 ..StorageMetrics::default()
292 };
293
294 let exporter = PrometheusExporter::new("test".to_string());
295 let output = exporter.export(&metrics);
296
297 assert!(output.contains("test_cache_hit_rate 0.9"));
299 }
300
301 #[test]
302 fn test_prometheus_export_builder() {
303 let exporter = PrometheusExporterBuilder::new()
304 .namespace("custom".to_string())
305 .instance("node1".to_string())
306 .job("storage".to_string())
307 .label("region".to_string(), "us-east".to_string())
308 .build();
309
310 let metrics = StorageMetrics::default();
311 let output = exporter.export(&metrics);
312
313 assert!(output.contains("custom_put_total"));
314 assert!(output.contains("instance=\"node1\""));
315 assert!(output.contains("job=\"storage\""));
316 assert!(output.contains("region=\"us-east\""));
317 }
318
319 #[test]
320 fn test_http_export() {
321 let metrics = StorageMetrics::default();
322 let exporter = PrometheusExporter::new("test".to_string());
323 let (content_type, body) = exporter.export_http(&metrics);
324
325 assert_eq!(content_type, "text/plain; version=0.0.4; charset=utf-8");
326 assert!(body.contains("# HELP"));
327 assert!(body.contains("# TYPE"));
328 }
329}