Skip to main content

stratum_apps/monitoring/
prometheus_metrics.rs

1//! Prometheus metrics definitions for SV2 monitoring
2
3use prometheus::{Gauge, GaugeVec, Opts, Registry};
4
5/// Prometheus metrics for the monitoring server.
6/// Metrics are optional - only registered when the corresponding monitoring type is enabled.
7#[derive(Clone)]
8pub struct PrometheusMetrics {
9    pub registry: Registry,
10    // System metrics
11    pub sv2_uptime_seconds: Gauge,
12    // Server metrics (upstream connection)
13    pub sv2_server_channels: Option<GaugeVec>,
14    pub sv2_server_hashrate_total: Option<Gauge>,
15    pub sv2_server_channel_hashrate: Option<GaugeVec>,
16    pub sv2_server_shares_accepted_total: Option<GaugeVec>,
17    pub sv2_server_shares_rejected_total: Option<GaugeVec>,
18    pub sv2_server_blocks_found_total: Option<Gauge>,
19    // Clients metrics (downstream connections)
20    pub sv2_clients_total: Option<Gauge>,
21    pub sv2_client_channels: Option<GaugeVec>,
22    pub sv2_client_hashrate_total: Option<Gauge>,
23    pub sv2_client_channel_hashrate: Option<GaugeVec>,
24    pub sv2_client_shares_accepted_total: Option<GaugeVec>,
25    pub sv2_client_shares_rejected_total: Option<GaugeVec>,
26    pub sv2_client_blocks_found_total: Option<Gauge>,
27    // SV1 metrics
28    pub sv1_clients_total: Option<Gauge>,
29    pub sv1_hashrate_total: Option<Gauge>,
30}
31
32impl PrometheusMetrics {
33    pub fn new(
34        enable_server_metrics: bool,
35        enable_clients_metrics: bool,
36        enable_sv1_metrics: bool,
37    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
38        let registry = Registry::new();
39
40        // System metrics (always enabled)
41        let sv2_uptime_seconds = Gauge::new("sv2_uptime_seconds", "Server uptime in seconds")?;
42        registry.register(Box::new(sv2_uptime_seconds.clone()))?;
43
44        // Server metrics (upstream connection)
45        let (
46            sv2_server_channels,
47            sv2_server_hashrate_total,
48            sv2_server_channel_hashrate,
49            sv2_server_shares_accepted_total,
50            sv2_server_shares_rejected_total,
51            sv2_server_blocks_found_total,
52        ) = if enable_server_metrics {
53            let channels = GaugeVec::new(
54                Opts::new("sv2_server_channels", "Number of server channels by type"),
55                &["channel_type"],
56            )?;
57            registry.register(Box::new(channels.clone()))?;
58
59            let hashrate = Gauge::new(
60                "sv2_server_hashrate_total",
61                "Total hashrate for channels opened with the server",
62            )?;
63            registry.register(Box::new(hashrate.clone()))?;
64
65            let channel_hashrate = GaugeVec::new(
66                Opts::new(
67                    "sv2_server_channel_hashrate",
68                    "Hashrate for individual server channels",
69                ),
70                &["channel_id", "user_identity"],
71            )?;
72            registry.register(Box::new(channel_hashrate.clone()))?;
73
74            let shares_accepted = GaugeVec::new(
75                Opts::new(
76                    "sv2_server_shares_accepted_total",
77                    "Total shares accepted per server channel",
78                ),
79                &["channel_id", "user_identity"],
80            )?;
81            registry.register(Box::new(shares_accepted.clone()))?;
82
83            let shares_rejected = GaugeVec::new(
84                Opts::new(
85                    "sv2_server_shares_rejected_total",
86                    "Total shares rejected per server channel by error code",
87                ),
88                &["channel_id", "user_identity", "error_code"],
89            )?;
90            registry.register(Box::new(shares_rejected.clone()))?;
91
92            let blocks_found = Gauge::new(
93                "sv2_server_blocks_found_total",
94                "Total blocks found across all current server channels",
95            )?;
96            registry.register(Box::new(blocks_found.clone()))?;
97
98            (
99                Some(channels),
100                Some(hashrate),
101                Some(channel_hashrate),
102                Some(shares_accepted),
103                Some(shares_rejected),
104                Some(blocks_found),
105            )
106        } else {
107            (None, None, None, None, None, None)
108        };
109
110        // Clients metrics (downstream connections)
111        let (
112            sv2_clients_total,
113            sv2_client_channels,
114            sv2_client_hashrate_total,
115            sv2_client_channel_hashrate,
116            sv2_client_shares_accepted_total,
117            sv2_client_shares_rejected_total,
118            sv2_client_blocks_found_total,
119        ) = if enable_clients_metrics {
120            let clients_total =
121                Gauge::new("sv2_clients_total", "Total number of connected clients")?;
122            registry.register(Box::new(clients_total.clone()))?;
123
124            let channels = GaugeVec::new(
125                Opts::new("sv2_client_channels", "Number of client channels by type"),
126                &["channel_type"],
127            )?;
128            registry.register(Box::new(channels.clone()))?;
129
130            let hashrate = Gauge::new(
131                "sv2_client_hashrate_total",
132                "Total hashrate for channels opened with clients",
133            )?;
134            registry.register(Box::new(hashrate.clone()))?;
135
136            let channel_hashrate = GaugeVec::new(
137                Opts::new(
138                    "sv2_client_channel_hashrate",
139                    "Hashrate for individual client channels",
140                ),
141                &["client_id", "channel_id", "user_identity"],
142            )?;
143            registry.register(Box::new(channel_hashrate.clone()))?;
144
145            let shares_accepted = GaugeVec::new(
146                Opts::new(
147                    "sv2_client_shares_accepted_total",
148                    "Total shares accepted per client channel",
149                ),
150                &["client_id", "channel_id", "user_identity"],
151            )?;
152            registry.register(Box::new(shares_accepted.clone()))?;
153
154            let shares_rejected = GaugeVec::new(
155                Opts::new(
156                    "sv2_client_shares_rejected_total",
157                    "Total shares rejected per client channel by error code",
158                ),
159                &["client_id", "channel_id", "user_identity", "error_code"],
160            )?;
161            registry.register(Box::new(shares_rejected.clone()))?;
162
163            let blocks_found = Gauge::new(
164                "sv2_client_blocks_found_total",
165                "Total blocks found across all current client channels",
166            )?;
167            registry.register(Box::new(blocks_found.clone()))?;
168
169            (
170                Some(clients_total),
171                Some(channels),
172                Some(hashrate),
173                Some(channel_hashrate),
174                Some(shares_accepted),
175                Some(shares_rejected),
176                Some(blocks_found),
177            )
178        } else {
179            (None, None, None, None, None, None, None)
180        };
181
182        // SV1 metrics
183        let (sv1_clients_total, sv1_hashrate_total) = if enable_sv1_metrics {
184            let clients = Gauge::new("sv1_clients_total", "Total number of SV1 clients")?;
185            registry.register(Box::new(clients.clone()))?;
186
187            let hashrate = Gauge::new("sv1_hashrate_total", "Total hashrate from SV1 clients")?;
188            registry.register(Box::new(hashrate.clone()))?;
189
190            (Some(clients), Some(hashrate))
191        } else {
192            (None, None)
193        };
194
195        Ok(Self {
196            registry,
197            sv2_uptime_seconds,
198            sv2_server_channels,
199            sv2_server_hashrate_total,
200            sv2_server_channel_hashrate,
201            sv2_server_shares_accepted_total,
202            sv2_server_shares_rejected_total,
203            sv2_server_blocks_found_total,
204            sv2_clients_total,
205            sv2_client_channels,
206            sv2_client_hashrate_total,
207            sv2_client_channel_hashrate,
208            sv2_client_shares_accepted_total,
209            sv2_client_shares_rejected_total,
210            sv2_client_blocks_found_total,
211            sv1_clients_total,
212            sv1_hashrate_total,
213        })
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use prometheus::Encoder;
221
222    #[test]
223    fn uptime_metric_always_registered() {
224        let m = PrometheusMetrics::new(false, false, false).unwrap();
225        m.sv2_uptime_seconds.set(42.0);
226
227        let families = m.registry.gather();
228        let names: Vec<&str> = families.iter().map(|f| f.get_name()).collect();
229        assert!(names.contains(&"sv2_uptime_seconds"));
230    }
231
232    #[test]
233    fn server_metrics_enabled() {
234        let m = PrometheusMetrics::new(true, false, false).unwrap();
235        assert!(m.sv2_server_channels.is_some());
236        assert!(m.sv2_server_hashrate_total.is_some());
237        assert!(m.sv2_server_channel_hashrate.is_some());
238        assert!(m.sv2_server_shares_accepted_total.is_some());
239        assert!(m.sv2_server_shares_rejected_total.is_some());
240        // clients and sv1 should be None
241        assert!(m.sv2_clients_total.is_none());
242        assert!(m.sv1_clients_total.is_none());
243    }
244
245    #[test]
246    fn clients_metrics_enabled() {
247        let m = PrometheusMetrics::new(false, true, false).unwrap();
248        assert!(m.sv2_clients_total.is_some());
249        assert!(m.sv2_client_channels.is_some());
250        assert!(m.sv2_client_hashrate_total.is_some());
251        assert!(m.sv2_client_channel_hashrate.is_some());
252        assert!(m.sv2_client_shares_accepted_total.is_some());
253        assert!(m.sv2_client_shares_rejected_total.is_some());
254        // server and sv1 should be None
255        assert!(m.sv2_server_channels.is_none());
256        assert!(m.sv1_clients_total.is_none());
257    }
258
259    #[test]
260    fn sv1_metrics_enabled() {
261        let m = PrometheusMetrics::new(false, false, true).unwrap();
262        assert!(m.sv1_clients_total.is_some());
263        assert!(m.sv1_hashrate_total.is_some());
264        // server and clients should be None
265        assert!(m.sv2_server_channels.is_none());
266        assert!(m.sv2_clients_total.is_none());
267    }
268
269    #[test]
270    fn all_metrics_disabled_only_uptime() {
271        let m = PrometheusMetrics::new(false, false, false).unwrap();
272        assert!(m.sv2_server_channels.is_none());
273        assert!(m.sv2_clients_total.is_none());
274        assert!(m.sv1_clients_total.is_none());
275
276        let families = m.registry.gather();
277        // Only uptime should be registered (with default value 0)
278        assert_eq!(families.len(), 1);
279        assert_eq!(families[0].get_name(), "sv2_uptime_seconds");
280    }
281
282    #[test]
283    fn metrics_encode_to_prometheus_text_format() {
284        let m = PrometheusMetrics::new(true, true, true).unwrap();
285        m.sv2_uptime_seconds.set(123.0);
286        m.sv2_server_hashrate_total.as_ref().unwrap().set(1000.0);
287        m.sv2_clients_total.as_ref().unwrap().set(5.0);
288        m.sv1_clients_total.as_ref().unwrap().set(3.0);
289
290        let encoder = prometheus::TextEncoder::new();
291        let families = m.registry.gather();
292        let mut buf = Vec::new();
293        encoder.encode(&families, &mut buf).unwrap();
294        let output = String::from_utf8(buf).unwrap();
295
296        assert!(output.contains("sv2_uptime_seconds 123"));
297        assert!(output.contains("sv2_server_hashrate_total 1000"));
298        assert!(output.contains("sv2_clients_total 5"));
299        assert!(output.contains("sv1_clients_total 3"));
300    }
301}