Skip to main content

fraiseql_auth/
monitoring.rs

1//! Authentication monitoring and observability.
2//!
3//! Provides [`AuthEvent`] for structured event logging, [`AuthMetrics`] for
4//! in-process counters, and [`OperationTimer`] for latency measurement.
5use std::time::Instant;
6
7use serde::Serialize;
8use tracing::{Level, info, span, warn};
9
10/// A structured log record for a single authentication event.
11///
12/// Constructed with [`AuthEvent::new`] and populated via builder methods.
13/// Call [`AuthEvent::log`] to emit the record through `tracing`.
14///
15/// # Example
16///
17/// ```rust
18/// use fraiseql_auth::AuthEvent;
19/// let event = AuthEvent::new("login")
20///     .with_user_id("user123".to_string())
21///     .with_provider("google".to_string())
22///     .success(42.5);
23/// event.log();
24/// ```
25#[derive(Debug, Serialize)]
26pub struct AuthEvent {
27    /// Name of the authentication event (e.g., `"login"`, `"token_refresh"`).
28    pub event:       String,
29    /// Optional authenticated user ID associated with this event.
30    pub user_id:     Option<String>,
31    /// OAuth provider name (e.g., `"google"`, `"okta"`).
32    pub provider:    Option<String>,
33    /// Outcome: `"started"`, `"success"`, or `"error"`.
34    pub status:      String,
35    /// Duration of the operation in milliseconds.
36    pub duration_ms: f64,
37    /// Error message if the operation failed.
38    pub error:       Option<String>,
39    /// RFC 3339 timestamp of when this event was created.
40    pub timestamp:   String,
41    /// Optional correlation ID for tracing a request across services.
42    pub request_id:  Option<String>,
43}
44
45impl AuthEvent {
46    /// Create a new event record in the `"started"` state.
47    #[must_use]
48    pub fn new(event: &str) -> Self {
49        Self {
50            event:       event.to_string(),
51            user_id:     None,
52            provider:    None,
53            status:      "started".to_string(),
54            duration_ms: 0.0,
55            error:       None,
56            timestamp:   chrono::Utc::now().to_rfc3339(),
57            request_id:  None,
58        }
59    }
60
61    /// Set the user ID associated with this event.
62    #[must_use]
63    pub fn with_user_id(mut self, user_id: String) -> Self {
64        self.user_id = Some(user_id);
65        self
66    }
67
68    /// Set the OAuth provider name for this event.
69    #[must_use]
70    pub fn with_provider(mut self, provider: String) -> Self {
71        self.provider = Some(provider);
72        self
73    }
74
75    /// Set the request correlation ID for distributed tracing.
76    #[must_use]
77    pub fn with_request_id(mut self, request_id: String) -> Self {
78        self.request_id = Some(request_id);
79        self
80    }
81
82    /// Mark the event as successful and record its duration.
83    #[must_use]
84    pub fn success(mut self, duration_ms: f64) -> Self {
85        self.status = "success".to_string();
86        self.duration_ms = duration_ms;
87        self
88    }
89
90    /// Mark the event as failed, recording the error and duration.
91    #[must_use]
92    pub fn error(mut self, error: String, duration_ms: f64) -> Self {
93        self.status = "error".to_string();
94        self.error = Some(error);
95        self.duration_ms = duration_ms;
96        self
97    }
98
99    /// Emit this event through `tracing` at the appropriate level.
100    ///
101    /// Successful events are logged at `INFO`; errors at `WARN`.
102    /// Events in the `"started"` state are silently dropped.
103    pub fn log(&self) {
104        match self.status.as_str() {
105            "success" => {
106                info!(
107                    event = %self.event,
108                    user_id = ?self.user_id,
109                    provider = ?self.provider,
110                    duration_ms = self.duration_ms,
111                    "Authentication event",
112                );
113            },
114            "error" => {
115                warn!(
116                    event = %self.event,
117                    error = ?self.error,
118                    duration_ms = self.duration_ms,
119                    "Authentication error",
120                );
121            },
122            _ => {},
123        }
124    }
125}
126
127/// In-process counters for authentication operations.
128///
129/// These counters are for lightweight observability within a single process.
130/// For production monitoring, export these values to a metrics system such as
131/// Prometheus.  All fields are plain `u64`; thread-safe mutation requires an
132/// outer `Mutex` or `RwLock`.
133#[derive(Debug, Clone)]
134pub struct AuthMetrics {
135    /// Total number of authentication attempts (successful + failed).
136    pub total_auth_attempts:        u64,
137    /// Number of authentication attempts that succeeded.
138    pub successful_authentications: u64,
139    /// Number of authentication attempts that failed.
140    pub failed_authentications:     u64,
141    /// Number of access tokens issued since startup.
142    pub tokens_issued:              u64,
143    /// Number of access tokens refreshed since startup.
144    pub tokens_refreshed:           u64,
145    /// Number of sessions explicitly revoked since startup.
146    pub sessions_revoked:           u64,
147}
148
149impl AuthMetrics {
150    /// Create a new `AuthMetrics` with all counters initialized to zero.
151    #[must_use]
152    pub const fn new() -> Self {
153        Self {
154            total_auth_attempts:        0,
155            successful_authentications: 0,
156            failed_authentications:     0,
157            tokens_issued:              0,
158            tokens_refreshed:           0,
159            sessions_revoked:           0,
160        }
161    }
162
163    /// Increment the total authentication attempts counter.
164    pub const fn record_attempt(&mut self) {
165        self.total_auth_attempts += 1;
166    }
167
168    /// Increment the successful authentications counter.
169    pub const fn record_success(&mut self) {
170        self.successful_authentications += 1;
171    }
172
173    /// Increment the failed authentications counter.
174    pub const fn record_failure(&mut self) {
175        self.failed_authentications += 1;
176    }
177
178    /// Increment the tokens issued counter.
179    pub const fn record_token_issued(&mut self) {
180        self.tokens_issued += 1;
181    }
182
183    /// Increment the tokens refreshed counter.
184    pub const fn record_token_refreshed(&mut self) {
185        self.tokens_refreshed += 1;
186    }
187
188    /// Increment the sessions revoked counter.
189    pub const fn record_session_revoked(&mut self) {
190        self.sessions_revoked += 1;
191    }
192
193    /// Return the success rate as a percentage (0–100).
194    ///
195    /// Returns `0.0` when no attempts have been recorded yet.
196    #[must_use]
197    pub fn success_rate(&self) -> f64 {
198        if self.total_auth_attempts == 0 {
199            0.0
200        } else {
201            #[allow(clippy::cast_precision_loss)] // Reason: acceptable precision for metrics/timing
202            let result = (self.successful_authentications as f64)
203                / (self.total_auth_attempts as f64)
204                * 100.0;
205            result
206        }
207    }
208}
209
210impl Default for AuthMetrics {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216/// A wall-clock timer for measuring the duration of authentication operations.
217///
218/// The timer starts immediately on construction via [`OperationTimer::start`].
219/// Call [`OperationTimer::finish`] to log the elapsed time and discard the timer,
220/// or read [`OperationTimer::elapsed_ms`] to sample without consuming.
221pub struct OperationTimer {
222    start:     Instant,
223    operation: String,
224}
225
226impl OperationTimer {
227    /// Start timing `operation` and open a tracing span at `DEBUG` level.
228    pub fn start(operation: &str) -> Self {
229        let span = span!(Level::DEBUG, "operation", %operation);
230        let _guard = span.enter();
231
232        Self {
233            start:     Instant::now(),
234            operation: operation.to_string(),
235        }
236    }
237
238    /// Return the elapsed time in milliseconds since this timer was started.
239    #[must_use]
240    pub fn elapsed_ms(&self) -> f64 {
241        self.start.elapsed().as_secs_f64() * 1000.0
242    }
243
244    /// Log the completed operation at `INFO` level with its elapsed duration.
245    pub fn finish(self) {
246        let elapsed = self.elapsed_ms();
247        info!(
248            operation = %self.operation,
249            duration_ms = elapsed,
250            "Operation completed",
251        );
252    }
253}