Skip to main content

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}