1#![cfg_attr(
10 not(test),
11 deny(
12 clippy::unwrap_used,
13 clippy::unreachable,
14 clippy::expect_used,
15 clippy::panic,
16 clippy::unimplemented,
17 clippy::todo
18 )
19)]
20
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::Arc;
24use std::time::Duration;
25
26use tokio_util::sync::CancellationToken;
27
28mod instruments;
29mod provider;
30mod shared;
31pub use instruments::*;
32pub use provider::*;
33pub use shared::InfrastructureMetricsRecorder;
34#[cfg(test)]
35mod instance_tests {
36 include!("tests.rs");
37}
38#[cfg(feature = "otlp")]
39mod otlp;
40#[cfg(feature = "otlp")]
41pub use otlp::*;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum DbMetricBackend {
46 Sqlite,
48 MySql,
50 Postgres,
52 Other,
54}
55
56impl DbMetricBackend {
57 #[must_use]
59 pub const fn as_label(self) -> &'static str {
60 match self {
61 Self::Sqlite => "sqlite",
62 Self::MySql => "mysql",
63 Self::Postgres => "postgres",
64 Self::Other => "other",
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum DbQueryKind {
72 Select,
74 Insert,
76 Update,
78 Delete,
80 With,
82 Transaction,
84 Ddl,
86 Pragma,
88 Other,
90}
91
92impl DbQueryKind {
93 #[must_use]
95 pub const fn as_label(self) -> &'static str {
96 match self {
97 Self::Select => "select",
98 Self::Insert => "insert",
99 Self::Update => "update",
100 Self::Delete => "delete",
101 Self::With => "with",
102 Self::Transaction => "transaction",
103 Self::Ddl => "ddl",
104 Self::Pragma => "pragma",
105 Self::Other => "other",
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct DbQueryMetric {
116 pub backend: DbMetricBackend,
118 pub kind: DbQueryKind,
120 pub failed: bool,
122 pub elapsed: Duration,
124}
125
126impl DbQueryMetric {
127 #[must_use]
129 pub const fn new(
130 backend: DbMetricBackend,
131 kind: DbQueryKind,
132 failed: bool,
133 elapsed: Duration,
134 ) -> Self {
135 Self {
136 backend,
137 kind,
138 failed,
139 elapsed,
140 }
141 }
142
143 #[must_use]
145 pub const fn status_label(&self) -> &'static str {
146 if self.failed { "error" } else { "ok" }
147 }
148}
149
150pub trait DbMetricsRecorder: Send + Sync {
152 fn enabled(&self) -> bool;
154
155 fn record_db_query(&self, metric: &DbQueryMetric);
157}
158
159#[derive(Debug, Default)]
161pub struct NoopDbMetrics;
162
163impl DbMetricsRecorder for NoopDbMetrics {
164 fn enabled(&self) -> bool {
165 false
166 }
167
168 fn record_db_query(&self, _metric: &DbQueryMetric) {}
169}
170
171pub type SharedDbMetricsRecorder = Arc<dyn DbMetricsRecorder>;
173
174#[expect(
179 unused_variables,
180 reason = "Default trait methods intentionally ignore metric inputs so products can implement only the signals they collect."
181)]
182pub trait MetricsRecorder: DbMetricsRecorder + Send + Sync {
183 fn provider(&self) -> Option<MetricsProvider> {
185 None
186 }
187
188 #[cfg(feature = "runtime-health")]
189 fn record_health(
190 &self,
191 _scope: &'static str,
192 _report: &aster_forge_runtime::SystemHealthReport,
193 ) {
194 }
195 fn record_http_request(&self, method: &str, route: &str, status: u16, duration_seconds: f64) {}
203
204 fn record_auth_event(&self, action: &'static str, status: &'static str, reason: &'static str) {}
206
207 fn record_application_event(
209 &self,
210 category: &'static str,
211 event: &'static str,
212 status: &'static str,
213 ) {
214 }
215
216 fn record_config_reload(
218 &self,
219 source: &'static str,
220 decision: &'static str,
221 status: &'static str,
222 changed_keys: u64,
223 duration_seconds: f64,
224 ) {
225 }
226
227 fn record_config_mutation(
229 &self,
230 source: &'static str,
231 operation: &'static str,
232 status: &'static str,
233 changed_keys: u64,
234 ) {
235 }
236
237 fn record_background_task_transition(&self, kind: &'static str, status: &'static str) {}
239
240 fn set_background_tasks_pending(&self, pending: u64) {}
242
243 fn record_external_operation(
245 &self,
246 system: &'static str,
247 operation: &'static str,
248 status: &'static str,
249 duration_seconds: f64,
250 ) {
251 }
252
253 fn system_metrics_updater_task(
255 &self,
256 shutdown_token: CancellationToken,
257 ) -> Option<Pin<Box<dyn Future<Output = ()> + Send + 'static>>> {
258 None
259 }
260}
261
262pub type SharedMetricsRecorder = Arc<dyn MetricsRecorder>;
264
265#[derive(Debug, Default)]
267pub struct NoopMetrics;
268
269impl MetricsRecorder for NoopMetrics {}
270
271impl DbMetricsRecorder for NoopMetrics {
272 fn enabled(&self) -> bool {
273 false
274 }
275
276 fn record_db_query(&self, _metric: &DbQueryMetric) {}
277}
278
279impl NoopMetrics {
280 #[must_use]
282 pub fn new() -> Self {
283 Self
284 }
285
286 #[must_use]
288 pub fn arc() -> SharedMetricsRecorder {
289 Arc::new(Self::new())
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub enum MetricKind {
296 Counter,
298 Gauge,
300 Histogram,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq)]
306pub struct MetricDescriptor {
307 pub subsystem: &'static str,
309 pub name: &'static str,
311 pub help: &'static str,
313 pub kind: MetricKind,
315 pub labels: &'static [&'static str],
317 pub buckets: &'static [f64],
319 pub unit: &'static str,
321}
322
323impl MetricDescriptor {
324 #[must_use]
326 pub const fn with_unit(mut self, unit: &'static str) -> Self {
327 self.unit = unit;
328 self
329 }
330
331 #[must_use]
333 pub const fn counter(
334 subsystem: &'static str,
335 name: &'static str,
336 help: &'static str,
337 labels: &'static [&'static str],
338 ) -> Self {
339 Self {
340 subsystem,
341 name,
342 help,
343 kind: MetricKind::Counter,
344 labels,
345 buckets: &[],
346 unit: "1",
347 }
348 }
349
350 #[must_use]
352 pub const fn gauge(
353 subsystem: &'static str,
354 name: &'static str,
355 help: &'static str,
356 labels: &'static [&'static str],
357 ) -> Self {
358 Self {
359 subsystem,
360 name,
361 help,
362 kind: MetricKind::Gauge,
363 labels,
364 buckets: &[],
365 unit: "1",
366 }
367 }
368
369 #[must_use]
371 pub const fn histogram(
372 subsystem: &'static str,
373 name: &'static str,
374 help: &'static str,
375 labels: &'static [&'static str],
376 ) -> Self {
377 Self {
378 subsystem,
379 name,
380 help,
381 kind: MetricKind::Histogram,
382 labels,
383 buckets: &[],
384 unit: "1",
385 }
386 }
387
388 #[must_use]
390 pub const fn histogram_with_buckets(
391 subsystem: &'static str,
392 name: &'static str,
393 help: &'static str,
394 labels: &'static [&'static str],
395 buckets: &'static [f64],
396 ) -> Self {
397 Self {
398 subsystem,
399 name,
400 help,
401 kind: MetricKind::Histogram,
402 labels,
403 buckets,
404 unit: "1",
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn noop_metrics_reports_disabled() {
415 let recorder = NoopMetrics::new();
416
417 assert!(!recorder.enabled());
418 recorder.record_http_request("GET", "/health", 200, 0.01);
419 recorder.record_auth_event("login", "ok", "password");
420 recorder.record_application_event("config", "updated", "ok");
421 recorder.record_config_reload("api", "reloaded", "ok", 2, 0.01);
422 recorder.record_config_mutation("api", "upsert", "ok", 1);
423 recorder.record_background_task_transition("cleanup", "completed");
424 recorder.set_background_tasks_pending(2);
425 recorder.record_external_operation("oidc", "token", "ok", 0.02);
426 assert!(
427 recorder
428 .system_metrics_updater_task(CancellationToken::new())
429 .is_none()
430 );
431 }
432
433 #[test]
434 fn database_metric_labels_are_stable() {
435 assert_eq!(DbMetricBackend::Sqlite.as_label(), "sqlite");
436 assert_eq!(DbMetricBackend::MySql.as_label(), "mysql");
437 assert_eq!(DbMetricBackend::Postgres.as_label(), "postgres");
438 assert_eq!(DbMetricBackend::Other.as_label(), "other");
439
440 assert_eq!(DbQueryKind::Select.as_label(), "select");
441 assert_eq!(DbQueryKind::Insert.as_label(), "insert");
442 assert_eq!(DbQueryKind::Update.as_label(), "update");
443 assert_eq!(DbQueryKind::Delete.as_label(), "delete");
444 assert_eq!(DbQueryKind::With.as_label(), "with");
445 assert_eq!(DbQueryKind::Transaction.as_label(), "transaction");
446 assert_eq!(DbQueryKind::Ddl.as_label(), "ddl");
447 assert_eq!(DbQueryKind::Pragma.as_label(), "pragma");
448 assert_eq!(DbQueryKind::Other.as_label(), "other");
449
450 let ok = DbQueryMetric::new(
451 DbMetricBackend::Sqlite,
452 DbQueryKind::Select,
453 false,
454 std::time::Duration::from_millis(3),
455 );
456 assert_eq!(ok.status_label(), "ok");
457
458 let failed = DbQueryMetric::new(
459 DbMetricBackend::Sqlite,
460 DbQueryKind::Select,
461 true,
462 std::time::Duration::from_millis(3),
463 );
464 assert_eq!(failed.status_label(), "error");
465 }
466
467 #[test]
468 fn histogram_descriptor_preserves_explicit_buckets() {
469 let descriptor = MetricDescriptor::histogram_with_buckets(
470 "worker",
471 "duration_seconds",
472 "Worker duration.",
473 &["kind"],
474 &[0.1, 1.0],
475 );
476
477 assert_eq!(descriptor.kind, MetricKind::Histogram);
478 assert_eq!(descriptor.labels, &["kind"]);
479 assert_eq!(descriptor.buckets, &[0.1, 1.0]);
480 }
481}