Skip to main content

ave_core/
metrics.rs

1use std::sync::{Arc, OnceLock};
2use std::time::Duration;
3
4use prometheus_client::{
5    encoding::EncodeLabelSet,
6    metrics::{counter::Counter, family::Family, histogram::Histogram},
7    registry::Registry,
8};
9
10#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
11struct RequestResultLabels {
12    result: &'static str,
13}
14
15#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
16struct RequestPhaseLabels {
17    phase: &'static str,
18}
19
20#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
21struct ContractPrepareLabels {
22    kind: &'static str,
23    result: &'static str,
24}
25
26#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
27struct ContractExecutionLabels {
28    result: &'static str,
29}
30
31#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
32struct TrackerSyncRoundLabels {
33    result: &'static str,
34}
35
36#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
37struct TrackerSyncUpdateLabels {
38    result: &'static str,
39}
40
41#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
42struct ProtocolEventLabels {
43    protocol: &'static str,
44    result: &'static str,
45}
46
47#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
48struct SchemaEventLabels {
49    actor: &'static str,
50    result: &'static str,
51}
52
53#[derive(Debug)]
54pub struct CoreMetrics {
55    requests: Family<RequestResultLabels, Counter>,
56    request_duration_seconds:
57        Family<RequestResultLabels, Histogram, fn() -> Histogram>,
58    request_phase_duration_seconds:
59        Family<RequestPhaseLabels, Histogram, fn() -> Histogram>,
60    contract_preparations: Family<ContractPrepareLabels, Counter>,
61    contract_prepare_seconds:
62        Family<ContractPrepareLabels, Histogram, fn() -> Histogram>,
63    contract_executions: Family<ContractExecutionLabels, Counter>,
64    contract_execution_seconds:
65        Family<ContractExecutionLabels, Histogram, fn() -> Histogram>,
66    tracker_sync_rounds: Family<TrackerSyncRoundLabels, Counter>,
67    tracker_sync_updates: Family<TrackerSyncUpdateLabels, Counter>,
68    protocol_events: Family<ProtocolEventLabels, Counter>,
69    schema_events: Family<SchemaEventLabels, Counter>,
70}
71
72static CORE_METRICS: OnceLock<Arc<CoreMetrics>> = OnceLock::new();
73
74impl CoreMetrics {
75    fn new() -> Self {
76        Self {
77            requests: Family::default(),
78            request_duration_seconds: Family::new_with_constructor(|| {
79                Histogram::new(vec![
80                    0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0,
81                    60.0, 120.0, 300.0,
82                ])
83            }),
84            request_phase_duration_seconds: Family::new_with_constructor(
85                || {
86                    Histogram::new(vec![
87                        0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0,
88                        60.0, 120.0, 300.0,
89                    ])
90                },
91            ),
92            contract_preparations: Family::default(),
93            contract_prepare_seconds: Family::new_with_constructor(|| {
94                Histogram::new(vec![
95                    0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0,
96                    60.0, 120.0,
97                ])
98            }),
99            contract_executions: Family::default(),
100            contract_execution_seconds: Family::new_with_constructor(|| {
101                Histogram::new(vec![
102                    0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25,
103                    0.5, 1.0, 2.0, 5.0,
104                ])
105            }),
106            tracker_sync_rounds: Family::default(),
107            tracker_sync_updates: Family::default(),
108            protocol_events: Family::default(),
109            schema_events: Family::default(),
110        }
111    }
112
113    fn register_into(&self, registry: &mut Registry) {
114        registry.register(
115            "core_requests",
116            "Core request lifecycle counters labeled by result.",
117            self.requests.clone(),
118        );
119        registry.register(
120            "core_request_duration_seconds",
121            "Total handled request duration labeled by terminal result.",
122            self.request_duration_seconds.clone(),
123        );
124        registry.register(
125            "core_request_phase_duration_seconds",
126            "Duration of the main request phases labeled by phase.",
127            self.request_phase_duration_seconds.clone(),
128        );
129        registry.register(
130            "core_contract_preparations",
131            "Contract preparation attempts labeled by kind and result.",
132            self.contract_preparations.clone(),
133        );
134        registry.register(
135            "core_contract_prepare_seconds",
136            "Contract preparation duration labeled by kind and result.",
137            self.contract_prepare_seconds.clone(),
138        );
139        registry.register(
140            "core_contract_executions",
141            "Contract execution attempts labeled by result.",
142            self.contract_executions.clone(),
143        );
144        registry.register(
145            "core_contract_execution_seconds",
146            "Contract execution duration labeled by result.",
147            self.contract_execution_seconds.clone(),
148        );
149        registry.register(
150            "core_tracker_sync_rounds",
151            "Tracker sync round counters labeled by result.",
152            self.tracker_sync_rounds.clone(),
153        );
154        registry.register(
155            "core_tracker_sync_updates",
156            "Tracker sync update counters labeled by result.",
157            self.tracker_sync_updates.clone(),
158        );
159        registry.register(
160            "core_protocol_events",
161            "Core protocol events labeled by protocol and result.",
162            self.protocol_events.clone(),
163        );
164        registry.register(
165            "core_schema_events",
166            "Evaluation and validation schema actor events labeled by actor and result.",
167            self.schema_events.clone(),
168        );
169    }
170
171    const fn seconds(duration: Duration) -> f64 {
172        duration.as_secs_f64()
173    }
174
175    pub fn observe_request_started(&self) {
176        self.requests
177            .get_or_create(&RequestResultLabels { result: "started" })
178            .inc();
179    }
180
181    pub fn observe_request_invalid(&self) {
182        self.requests
183            .get_or_create(&RequestResultLabels { result: "invalid" })
184            .inc();
185    }
186
187    pub fn observe_request_terminal(
188        &self,
189        result: &'static str,
190        duration: Duration,
191    ) {
192        self.requests
193            .get_or_create(&RequestResultLabels { result })
194            .inc();
195        self.request_duration_seconds
196            .get_or_create(&RequestResultLabels { result })
197            .observe(Self::seconds(duration));
198    }
199
200    pub fn observe_request_phase(
201        &self,
202        phase: &'static str,
203        duration: Duration,
204    ) {
205        self.request_phase_duration_seconds
206            .get_or_create(&RequestPhaseLabels { phase })
207            .observe(Self::seconds(duration));
208    }
209
210    pub fn observe_contract_prepare(
211        &self,
212        kind: &'static str,
213        result: &'static str,
214        duration: Duration,
215    ) {
216        let labels = ContractPrepareLabels { kind, result };
217        self.contract_preparations.get_or_create(&labels).inc();
218        self.contract_prepare_seconds
219            .get_or_create(&labels)
220            .observe(Self::seconds(duration));
221    }
222
223    pub fn observe_contract_execution(
224        &self,
225        result: &'static str,
226        duration: Duration,
227    ) {
228        let labels = ContractExecutionLabels { result };
229        self.contract_executions.get_or_create(&labels).inc();
230        self.contract_execution_seconds
231            .get_or_create(&labels)
232            .observe(Self::seconds(duration));
233    }
234
235    pub fn observe_tracker_sync_round(&self, result: &'static str) {
236        self.tracker_sync_rounds
237            .get_or_create(&TrackerSyncRoundLabels { result })
238            .inc();
239    }
240
241    pub fn observe_tracker_sync_update(&self, result: &'static str) {
242        self.tracker_sync_updates
243            .get_or_create(&TrackerSyncUpdateLabels { result })
244            .inc();
245    }
246
247    pub fn observe_protocol_event(
248        &self,
249        protocol: &'static str,
250        result: &'static str,
251    ) {
252        self.protocol_events
253            .get_or_create(&ProtocolEventLabels { protocol, result })
254            .inc();
255    }
256
257    pub fn observe_schema_event(
258        &self,
259        actor: &'static str,
260        result: &'static str,
261    ) {
262        self.schema_events
263            .get_or_create(&SchemaEventLabels { actor, result })
264            .inc();
265    }
266}
267
268pub fn register(registry: &mut Registry) -> Arc<CoreMetrics> {
269    let metrics = CORE_METRICS
270        .get_or_init(|| Arc::new(CoreMetrics::new()))
271        .clone();
272    metrics.register_into(registry);
273    metrics
274}
275
276pub fn try_core_metrics() -> Option<&'static Arc<CoreMetrics>> {
277    CORE_METRICS.get()
278}
279
280#[cfg(test)]
281mod tests {
282    use std::time::Duration;
283
284    use prometheus_client::{encoding::text::encode, registry::Registry};
285
286    use super::*;
287
288    fn metric_value(metrics: &str, name: &str) -> f64 {
289        metrics
290            .lines()
291            .find_map(|line| {
292                if line.starts_with(name) {
293                    line.split_whitespace().nth(1)?.parse::<f64>().ok()
294                } else {
295                    None
296                }
297            })
298            .unwrap_or(0.0)
299    }
300
301    #[test]
302    fn core_metrics_expose_expected_counter_labels() {
303        let metrics = CoreMetrics::new();
304        let mut registry = Registry::default();
305        metrics.register_into(&mut registry);
306
307        metrics.observe_request_started();
308        metrics.observe_request_invalid();
309        metrics.observe_request_terminal("finished", Duration::from_millis(20));
310        metrics.observe_request_phase("evaluation", Duration::from_millis(10));
311        metrics.observe_contract_prepare(
312            "registered",
313            "cwasm_hit",
314            Duration::from_millis(5),
315        );
316        metrics.observe_contract_prepare(
317            "registered",
318            "skipped",
319            Duration::default(),
320        );
321        metrics.observe_contract_execution("success", Duration::from_millis(1));
322        metrics.observe_tracker_sync_round("completed");
323        metrics.observe_tracker_sync_update("launched");
324        metrics.observe_protocol_event("approval", "approved");
325        metrics.observe_schema_event("validation_schema", "delegated");
326
327        let mut text = String::new();
328        encode(&mut text, &registry).expect("encode metrics");
329
330        assert_eq!(
331            metric_value(&text, "core_requests_total{result=\"started\"}"),
332            1.0
333        );
334        assert_eq!(
335            metric_value(&text, "core_requests_total{result=\"invalid\"}"),
336            1.0
337        );
338        assert_eq!(
339            metric_value(&text, "core_requests_total{result=\"finished\"}"),
340            1.0
341        );
342        assert_eq!(
343            metric_value(
344                &text,
345                "core_contract_preparations_total{kind=\"registered\",result=\"cwasm_hit\"}"
346            ),
347            1.0
348        );
349        assert_eq!(
350            metric_value(
351                &text,
352                "core_contract_preparations_total{kind=\"registered\",result=\"skipped\"}"
353            ),
354            1.0
355        );
356        assert_eq!(
357            metric_value(
358                &text,
359                "core_contract_executions_total{result=\"success\"}"
360            ),
361            1.0
362        );
363        assert_eq!(
364            metric_value(
365                &text,
366                "core_tracker_sync_rounds_total{result=\"completed\"}"
367            ),
368            1.0
369        );
370        assert_eq!(
371            metric_value(
372                &text,
373                "core_tracker_sync_updates_total{result=\"launched\"}"
374            ),
375            1.0
376        );
377        assert_eq!(
378            metric_value(
379                &text,
380                "core_protocol_events_total{protocol=\"approval\",result=\"approved\"}"
381            ),
382            1.0
383        );
384        assert_eq!(
385            metric_value(
386                &text,
387                "core_schema_events_total{actor=\"validation_schema\",result=\"delegated\"}"
388            ),
389            1.0
390        );
391    }
392
393    #[test]
394    fn core_metrics_expose_expected_histogram_series() {
395        let metrics = CoreMetrics::new();
396        let mut registry = Registry::default();
397        metrics.register_into(&mut registry);
398
399        metrics.observe_request_terminal("aborted", Duration::from_millis(30));
400        metrics
401            .observe_request_phase("distribution", Duration::from_millis(12));
402        metrics.observe_contract_prepare(
403            "temporary",
404            "recompiled",
405            Duration::from_millis(8),
406        );
407        metrics.observe_contract_execution("error", Duration::from_millis(2));
408
409        let mut text = String::new();
410        encode(&mut text, &registry).expect("encode metrics");
411
412        assert_eq!(
413            metric_value(
414                &text,
415                "core_request_duration_seconds_count{result=\"aborted\"}"
416            ),
417            1.0
418        );
419        assert_eq!(
420            metric_value(
421                &text,
422                "core_request_phase_duration_seconds_count{phase=\"distribution\"}"
423            ),
424            1.0
425        );
426        assert_eq!(
427            metric_value(
428                &text,
429                "core_contract_prepare_seconds_count{kind=\"temporary\",result=\"recompiled\"}"
430            ),
431            1.0
432        );
433        assert_eq!(
434            metric_value(
435                &text,
436                "core_contract_execution_seconds_count{result=\"error\"}"
437            ),
438            1.0
439        );
440    }
441}