fraiseql_server/usage/layer.rs
1//! Tracing subscriber layer that captures `fraiseql::mutation_audit` events.
2//!
3//! Install this layer alongside your normal subscriber to automatically feed
4//! the [`UsageAggregator`] from every mutation executed by the runtime:
5//!
6//! ```rust,ignore
7//! use std::sync::Arc;
8//! use tracing_subscriber::{layer::SubscriberExt, Registry};
9//! use fraiseql_server::usage::{aggregator::UsageAggregator, layer::MutationAuditLayer};
10//!
11//! let aggregator = Arc::new(UsageAggregator::new());
12//! let subscriber = Registry::default()
13//! .with(MutationAuditLayer::new(Arc::clone(&aggregator)));
14//! tracing::subscriber::set_global_default(subscriber).unwrap();
15//! ```
16
17use std::sync::Arc;
18
19use chrono::Utc;
20use tracing::{
21 Event, Subscriber,
22 field::{Field, Visit},
23};
24use tracing_subscriber::{Layer, layer::Context};
25
26use super::{aggregator::UsageAggregator, events::MutationAuditEvent};
27
28/// The tracing target emitted by the FraiseQL mutation executor.
29const MUTATION_AUDIT_TARGET: &str = "fraiseql::mutation_audit";
30
31// ── Field visitor ──────────────────────────────────────────────────────────
32
33/// Extracts the four string fields from a `fraiseql::mutation_audit` event.
34struct AuditFieldVisitor {
35 mutation_name: String,
36 entity_type: String,
37 operation: String,
38 tenant_id: String,
39}
40
41impl AuditFieldVisitor {
42 const fn new() -> Self {
43 Self {
44 mutation_name: String::new(),
45 entity_type: String::new(),
46 operation: String::new(),
47 tenant_id: String::new(),
48 }
49 }
50}
51
52impl Visit for AuditFieldVisitor {
53 /// Called for fields recorded with a plain `&str` value (e.g. `mutation_name = name`).
54 fn record_str(&mut self, field: &Field, value: &str) {
55 match field.name() {
56 "mutation_name" => value.clone_into(&mut self.mutation_name),
57 "entity_type" => value.clone_into(&mut self.entity_type),
58 "operation" => value.clone_into(&mut self.operation),
59 "tenant_id" => value.clone_into(&mut self.tenant_id),
60 _ => {},
61 }
62 }
63
64 /// Called for fields recorded with `%value` (Display-formatted) or `?value`
65 /// (Debug-formatted).
66 ///
67 /// `tracing` wraps `%value` in a `DisplayValue` whose `Debug` impl delegates
68 /// to `Display`, so `format!("{value:?}")` yields the raw Display string.
69 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
70 let s = format!("{value:?}");
71 match field.name() {
72 "mutation_name" => self.mutation_name = s,
73 "entity_type" => self.entity_type = s,
74 "operation" => self.operation = s,
75 "tenant_id" => self.tenant_id = s,
76 _ => {},
77 }
78 }
79}
80
81// ── MutationAuditLayer ─────────────────────────────────────────────────────
82
83/// Tracing [`Layer`] that feeds [`UsageAggregator`] from mutation audit events.
84///
85/// Only events whose target is exactly `"fraiseql::mutation_audit"` are
86/// processed; all other events are ignored with no overhead.
87///
88/// The layer holds an [`Arc<UsageAggregator>`] so it can be cloned cheaply
89/// and the aggregator can be shared with other components (e.g. an HTTP query
90/// endpoint).
91#[derive(Clone)]
92pub struct MutationAuditLayer {
93 aggregator: Arc<UsageAggregator>,
94}
95
96impl MutationAuditLayer {
97 /// Create a new layer backed by the given aggregator.
98 #[must_use]
99 pub const fn new(aggregator: Arc<UsageAggregator>) -> Self {
100 Self { aggregator }
101 }
102
103 /// Return a reference to the underlying aggregator.
104 #[must_use]
105 pub const fn aggregator(&self) -> &Arc<UsageAggregator> {
106 &self.aggregator
107 }
108}
109
110impl<S: Subscriber> Layer<S> for MutationAuditLayer {
111 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
112 if event.metadata().target() != MUTATION_AUDIT_TARGET {
113 return;
114 }
115
116 let mut visitor = AuditFieldVisitor::new();
117 event.record(&mut visitor);
118
119 let period = Utc::now().format("%Y-%m").to_string();
120
121 let audit_event = MutationAuditEvent {
122 mutation_name: visitor.mutation_name,
123 entity_type: visitor.entity_type,
124 operation: visitor.operation,
125 tenant_id: visitor.tenant_id,
126 period,
127 };
128
129 self.aggregator.record(&audit_event);
130 }
131}