Skip to main content

reinhardt_auth/sessions/
analytics.rs

1//! Session analytics and monitoring
2//!
3//! This module provides analytics and monitoring capabilities for session operations.
4//! It enables tracking of session creation, access, deletion, and expiration events.
5//!
6//! ## Available Analytics Backends
7//!
8//! - **LoggerAnalytics**: Log events using `tracing` (always available)
9//! - **PrometheusAnalytics**: Export metrics to Prometheus (feature: `analytics-prometheus`)
10//!
11//! ## Example
12//!
13//! ```rust,no_run
14//! use reinhardt_auth::sessions::analytics::{InstrumentedSessionBackend, LoggerAnalytics};
15//! use reinhardt_auth::sessions::backends::InMemorySessionBackend;
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! let backend = InMemorySessionBackend::new();
19//! let analytics = LoggerAnalytics::new();
20//! let instrumented = InstrumentedSessionBackend::new(backend, analytics);
21//!
22//! // All session operations are automatically tracked
23//! # Ok(())
24//! # }
25//! ```
26
27use super::backends::{SessionBackend, SessionError};
28use async_trait::async_trait;
29use chrono::{DateTime, Utc};
30use serde::{Deserialize, Serialize};
31use std::sync::Arc;
32
33// Submodules
34mod logger;
35pub use logger::LoggerAnalytics;
36
37#[cfg(feature = "analytics-prometheus")]
38mod prometheus;
39#[cfg(feature = "analytics-prometheus")]
40pub use self::prometheus::PrometheusAnalytics;
41
42/// Deletion reason for session analytics
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum DeletionReason {
45	/// Session was explicitly deleted by user or application
46	Explicit,
47	/// Session expired due to TTL
48	Expired,
49	/// Session was invalidated (e.g., logout)
50	Invalidated,
51	/// Session was replaced (e.g., key rotation)
52	Replaced,
53}
54
55/// Session event for analytics
56///
57/// This enum represents all session-related events that can be tracked.
58///
59/// # Example
60///
61/// ```rust
62/// use reinhardt_auth::sessions::analytics::SessionEvent;
63/// use chrono::Utc;
64///
65/// let event = SessionEvent::Created {
66///     session_key: "session_123".to_string(),
67///     size_bytes: 1024,
68///     ttl_secs: Some(3600),
69///     timestamp: Utc::now(),
70/// };
71/// ```
72#[derive(Debug, Clone)]
73pub enum SessionEvent {
74	/// Session was created
75	Created {
76		/// The session key identifier.
77		session_key: String,
78		/// Size of the session data in bytes.
79		size_bytes: usize,
80		/// Time-to-live in seconds, if set.
81		ttl_secs: Option<u64>,
82		/// When the session was created.
83		timestamp: DateTime<Utc>,
84	},
85	/// Session was accessed
86	Accessed {
87		/// The session key identifier.
88		session_key: String,
89		/// Access latency in milliseconds.
90		latency_ms: u64,
91		/// Whether the session data was found (cache hit).
92		hit: bool,
93		/// When the access occurred.
94		timestamp: DateTime<Utc>,
95	},
96	/// Session was deleted
97	Deleted {
98		/// The session key identifier.
99		session_key: String,
100		/// The reason the session was deleted.
101		reason: DeletionReason,
102		/// When the deletion occurred.
103		timestamp: DateTime<Utc>,
104	},
105	/// Session expired
106	Expired {
107		/// The session key identifier.
108		session_key: String,
109		/// Age of the session in seconds at expiration.
110		age_secs: u64,
111		/// When the expiration occurred.
112		timestamp: DateTime<Utc>,
113	},
114}
115
116/// Session analytics trait
117///
118/// Implement this trait to create custom analytics backends.
119///
120/// # Example
121///
122/// ```rust
123/// use reinhardt_auth::sessions::analytics::{SessionAnalytics, SessionEvent};
124/// use async_trait::async_trait;
125///
126/// struct MyAnalytics;
127///
128/// #[async_trait]
129/// impl SessionAnalytics for MyAnalytics {
130///     async fn record_event(&self, event: SessionEvent) {
131///         // Custom analytics implementation
132///         println!("Event: {:?}", event);
133///     }
134/// }
135/// ```
136#[async_trait]
137pub trait SessionAnalytics: Send + Sync {
138	/// Record a session event
139	async fn record_event(&self, event: SessionEvent);
140}
141
142/// Composite analytics backend
143///
144/// Allows recording events to multiple analytics backends simultaneously.
145///
146/// # Example
147///
148/// ```rust,no_run
149/// use reinhardt_auth::sessions::analytics::{CompositeAnalytics, LoggerAnalytics};
150///
151/// let mut composite = CompositeAnalytics::new();
152/// composite.add(LoggerAnalytics::new());
153/// // composite.add(PrometheusAnalytics::new()); // If prometheus feature is enabled
154/// ```
155#[derive(Clone)]
156pub struct CompositeAnalytics {
157	backends: Vec<Arc<dyn SessionAnalytics>>,
158}
159
160impl CompositeAnalytics {
161	/// Create a new composite analytics backend
162	pub fn new() -> Self {
163		Self {
164			backends: Vec::new(),
165		}
166	}
167
168	/// Add an analytics backend
169	pub fn add<A: SessionAnalytics + 'static>(&mut self, analytics: A) {
170		self.backends.push(Arc::new(analytics));
171	}
172}
173
174impl Default for CompositeAnalytics {
175	fn default() -> Self {
176		Self::new()
177	}
178}
179
180#[async_trait]
181impl SessionAnalytics for CompositeAnalytics {
182	async fn record_event(&self, event: SessionEvent) {
183		for backend in &self.backends {
184			backend.record_event(event.clone()).await;
185		}
186	}
187}
188
189/// Instrumented session backend wrapper
190///
191/// This wrapper automatically tracks all session operations and records
192/// events to the configured analytics backend.
193///
194/// # Example
195///
196/// ```rust,no_run
197/// use reinhardt_auth::sessions::analytics::{InstrumentedSessionBackend, LoggerAnalytics};
198/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
199///
200/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
201/// let backend = InMemorySessionBackend::new();
202/// let analytics = LoggerAnalytics::new();
203/// let instrumented = InstrumentedSessionBackend::new(backend, analytics);
204///
205/// // All operations are automatically tracked
206/// # Ok(())
207/// # }
208/// ```
209#[derive(Clone)]
210pub struct InstrumentedSessionBackend<B, A> {
211	backend: B,
212	analytics: A,
213}
214
215impl<B, A> InstrumentedSessionBackend<B, A>
216where
217	B: SessionBackend + Clone,
218	A: SessionAnalytics + Clone,
219{
220	/// Create a new instrumented session backend
221	///
222	/// # Example
223	///
224	/// ```rust,no_run
225	/// use reinhardt_auth::sessions::analytics::{InstrumentedSessionBackend, LoggerAnalytics};
226	/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
227	///
228	/// let backend = InMemorySessionBackend::new();
229	/// let analytics = LoggerAnalytics::new();
230	/// let instrumented = InstrumentedSessionBackend::new(backend, analytics);
231	/// ```
232	pub fn new(backend: B, analytics: A) -> Self {
233		Self { backend, analytics }
234	}
235
236	/// Get a reference to the underlying backend
237	pub fn backend(&self) -> &B {
238		&self.backend
239	}
240
241	/// Get a reference to the analytics backend
242	pub fn analytics(&self) -> &A {
243		&self.analytics
244	}
245}
246
247#[async_trait]
248impl<B, A> SessionBackend for InstrumentedSessionBackend<B, A>
249where
250	B: SessionBackend + Clone,
251	A: SessionAnalytics + Clone,
252{
253	async fn load<T>(&self, session_key: &str) -> Result<Option<T>, SessionError>
254	where
255		T: for<'de> Deserialize<'de> + Serialize + Send + Sync,
256	{
257		let start = std::time::Instant::now();
258		let result = self.backend.load(session_key).await;
259		let latency_ms = start.elapsed().as_millis() as u64;
260
261		let hit = result.as_ref().map(|opt| opt.is_some()).unwrap_or(false);
262
263		self.analytics
264			.record_event(SessionEvent::Accessed {
265				session_key: session_key.to_string(),
266				latency_ms,
267				hit,
268				timestamp: Utc::now(),
269			})
270			.await;
271
272		result
273	}
274
275	async fn save<T>(
276		&self,
277		session_key: &str,
278		data: &T,
279		ttl: Option<u64>,
280	) -> Result<(), SessionError>
281	where
282		T: Serialize + Send + Sync,
283	{
284		// Serialize to measure size
285		let serialized = serde_json::to_vec(data)
286			.map_err(|e| SessionError::SerializationError(e.to_string()))?;
287		let size_bytes = serialized.len();
288
289		let result = self.backend.save(session_key, data, ttl).await;
290
291		if result.is_ok() {
292			self.analytics
293				.record_event(SessionEvent::Created {
294					session_key: session_key.to_string(),
295					size_bytes,
296					ttl_secs: ttl,
297					timestamp: Utc::now(),
298				})
299				.await;
300		}
301
302		result
303	}
304
305	async fn delete(&self, session_key: &str) -> Result<(), SessionError> {
306		let result = self.backend.delete(session_key).await;
307
308		if result.is_ok() {
309			self.analytics
310				.record_event(SessionEvent::Deleted {
311					session_key: session_key.to_string(),
312					reason: DeletionReason::Explicit,
313					timestamp: Utc::now(),
314				})
315				.await;
316		}
317
318		result
319	}
320
321	async fn exists(&self, session_key: &str) -> Result<bool, SessionError> {
322		self.backend.exists(session_key).await
323	}
324}
325
326#[cfg(test)]
327mod tests {
328	use super::*;
329	use crate::sessions::InMemorySessionBackend;
330
331	#[tokio::test]
332	async fn test_instrumented_backend_save() {
333		let backend = InMemorySessionBackend::new();
334		let analytics = LoggerAnalytics::new();
335		let instrumented = InstrumentedSessionBackend::new(backend, analytics);
336
337		let data = serde_json::json!({"key": "value"});
338
339		instrumented
340			.save("test_key", &data, Some(3600))
341			.await
342			.unwrap();
343
344		let loaded: Option<serde_json::Value> = instrumented.load("test_key").await.unwrap();
345		assert_eq!(loaded.unwrap(), data);
346	}
347
348	#[tokio::test]
349	async fn test_instrumented_backend_delete() {
350		let backend = InMemorySessionBackend::new();
351		let analytics = LoggerAnalytics::new();
352		let instrumented = InstrumentedSessionBackend::new(backend, analytics);
353
354		let data = serde_json::json!({"key": "value"});
355
356		instrumented.save("test_key", &data, None).await.unwrap();
357		assert!(instrumented.exists("test_key").await.unwrap());
358
359		instrumented.delete("test_key").await.unwrap();
360		assert!(!instrumented.exists("test_key").await.unwrap());
361	}
362
363	#[tokio::test]
364	async fn test_composite_analytics() {
365		let mut composite = CompositeAnalytics::new();
366		composite.add(LoggerAnalytics::new());
367
368		let backend = InMemorySessionBackend::new();
369		let instrumented = InstrumentedSessionBackend::new(backend, composite);
370
371		let data = serde_json::json!({"key": "value"});
372		instrumented.save("test_key", &data, None).await.unwrap();
373	}
374}