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