Skip to main content

ipfrs_storage/
prometheus.rs

1//! Prometheus metrics exporter
2//!
3//! This module provides integration with Prometheus for production monitoring.
4//! It exports storage metrics in Prometheus text format for scraping.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use ipfrs_storage::{PrometheusExporter, StorageMetrics};
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! let metrics = StorageMetrics::default();
13//! let exporter = PrometheusExporter::new("ipfrs_storage".to_string());
14//! let prometheus_text = exporter.export(&metrics);
15//! println!("{}", prometheus_text);
16//! # Ok(())
17//! # }
18//! ```
19
20use crate::StorageMetrics;
21use std::fmt::Write;
22
23/// Prometheus metrics exporter
24#[derive(Debug, Clone)]
25pub struct PrometheusExporter {
26    /// Namespace for metrics (e.g., "ipfrs_storage")
27    namespace: String,
28    /// Additional labels to add to all metrics
29    labels: Vec<(String, String)>,
30}
31
32impl PrometheusExporter {
33    /// Create a new Prometheus exporter
34    pub fn new(namespace: String) -> Self {
35        Self {
36            namespace,
37            labels: Vec::new(),
38        }
39    }
40
41    /// Add a label to all exported metrics
42    pub fn with_label(mut self, key: String, value: String) -> Self {
43        self.labels.push((key, value));
44        self
45    }
46
47    /// Export metrics in Prometheus text format
48    pub fn export(&self, metrics: &StorageMetrics) -> String {
49        let mut output = String::new();
50        let labels = self.format_labels();
51
52        // Helper macro to write metrics
53        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        // Operation counters
65        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        // Cache metrics
91        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        // Bytes transferred
111        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        // Latency metrics
125        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        // Error metrics
163        write_metric!(
164            "errors_total",
165            "counter",
166            "Total number of errors encountered",
167            metrics.error_count
168        );
169
170        output
171    }
172
173    /// Format labels for Prometheus
174    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    /// Export metrics as HTTP response body (suitable for /metrics endpoint)
189    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/// Builder for creating a Prometheus exporter with multiple configurations
197#[derive(Debug, Default)]
198pub struct PrometheusExporterBuilder {
199    namespace: Option<String>,
200    labels: Vec<(String, String)>,
201}
202
203impl PrometheusExporterBuilder {
204    /// Create a new builder
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Set the namespace for metrics
210    pub fn namespace(mut self, namespace: String) -> Self {
211        self.namespace = Some(namespace);
212        self
213    }
214
215    /// Add a label to all metrics
216    pub fn label(mut self, key: String, value: String) -> Self {
217        self.labels.push((key, value));
218        self
219    }
220
221    /// Add the instance label (common for Prometheus)
222    pub fn instance(self, instance: String) -> Self {
223        self.label("instance".to_string(), instance)
224    }
225
226    /// Add the job label (common for Prometheus)
227    pub fn job(self, job: String) -> Self {
228        self.label("job".to_string(), job)
229    }
230
231    /// Build the exporter
232    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        // Check that output contains expected metrics
263        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        // Check that labels are included
283        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        // Cache hit rate should be 0.9
298        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}