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, ®istry).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, ®istry).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}