ri/core/analytics.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Log Analytics Module
21//!
22//! This module provides logging analytics functionality for Ri, tracking hook events and generating
23//! comprehensive analytics reports. It implements a service module that monitors the application
24//! lifecycle and generates JSON reports with event statistics.
25//!
26//! ## Key Components
27//!
28//! - **RiLogAnalyticsModule**: Main analytics module that implements `ServiceModule`
29//! - **AnalyticsState**: Internal struct for tracking analytics data
30//!
31//! ## Design Principles
32//!
33//! 1. **Non-Intrusive**: Operates by listening to hook events without modifying core functionality
34//! 2. **Performance-Focused**: Uses efficient data structures for event tracking
35//! 3. **Configurable**: Can be enabled/disabled through configuration
36//! 4. **Comprehensive**: Tracks events by kind, phase, and module
37//! 5. **Persistent**: Generates JSON reports that can be analyzed later
38//! 6. **Non-Critical**: Fails gracefully if analytics operations encounter errors
39
40use std::collections::HashMap as FxHashMap;
41use std::sync::{Arc, Mutex};
42use std::time::{SystemTime, UNIX_EPOCH};
43
44use crate::core::{RiResult, RiServiceContext, RiError, ServiceModule};
45use crate::hooks::{RiHookBus, RiHookEvent, RiHookKind};
46use serde_json::json;
47
48/// Internal analytics state struct.
49///
50/// This struct tracks various metrics about hook events, including:
51/// - Total number of events
52/// - Events per hook kind
53/// - Events per module phase
54/// - Events per module name
55#[derive(Default)]
56struct AnalyticsState {
57 /// Total number of hook events processed
58 total_events: u64,
59 /// Number of events per hook kind
60 per_kind: FxHashMap<String, u64>,
61 /// Number of events per module phase
62 per_phase: FxHashMap<String, u64>,
63 /// Number of events per module name
64 per_module: FxHashMap<String, u64>,
65}
66
67/// Log analytics module for Ri.
68///
69/// This module implements the `ServiceModule` trait and provides analytics functionality
70/// by listening to hook events and generating comprehensive reports.
71///
72/// ## Usage
73///
74/// The module is automatically added by the `RiAppBuilder` and doesn't need to be explicitly
75/// configured in most cases. It can be enabled/disabled through the configuration file.
76///
77/// ## Configuration
78///
79/// ```yaml
80/// analytics:
81/// enabled: true # Enable or disable analytics
82/// ```
83#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
84pub struct RiLogAnalyticsModule {
85 /// Shared analytics state protected by a mutex
86 state: Arc<Mutex<AnalyticsState>>,
87 /// Whether analytics is enabled
88 enabled: bool,
89}
90
91impl Default for RiLogAnalyticsModule {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl RiLogAnalyticsModule {
98 /// Creates a new instance of the log analytics module.
99 ///
100 /// Returns a new `RiLogAnalyticsModule` with default settings.
101 pub fn new() -> Self {
102 RiLogAnalyticsModule {
103 state: Arc::new(Mutex::new(AnalyticsState::default())),
104 enabled: true,
105 }
106 }
107
108 /// Returns whether analytics collection is currently enabled.
109 pub fn is_enabled(&self) -> bool {
110 self.enabled
111 }
112
113 /// Returns all hook kinds that the analytics module tracks.
114 ///
115 /// This method returns a static slice of all hook kinds that the analytics module
116 /// registers handlers for.
117 fn all_kinds() -> &'static [RiHookKind] {
118 use RiHookKind::*;
119 const KINDS: [RiHookKind; 9] = [
120 Startup,
121 Shutdown,
122 BeforeModulesInit,
123 AfterModulesInit,
124 BeforeModulesStart,
125 AfterModulesStart,
126 BeforeModulesShutdown,
127 AfterModulesShutdown,
128 ConfigReload,
129 ];
130 &KINDS
131 }
132
133 /// Returns a string label for the given hook kind.
134 ///
135 /// This method converts a `RiHookKind` enum variant to a human-readable string.
136 ///
137 /// # Parameters
138 ///
139 /// - `kind`: The hook kind to get a label for
140 ///
141 /// # Returns
142 ///
143 /// A static string label for the hook kind
144 fn kind_label(kind: RiHookKind) -> &'static str {
145 match kind {
146 RiHookKind::Startup => "Startup",
147 RiHookKind::Shutdown => "Shutdown",
148 RiHookKind::BeforeModulesInit => "BeforeModulesInit",
149 RiHookKind::AfterModulesInit => "AfterModulesInit",
150 RiHookKind::BeforeModulesStart => "BeforeModulesStart",
151 RiHookKind::AfterModulesStart => "AfterModulesStart",
152 RiHookKind::BeforeModulesShutdown => "BeforeModulesShutdown",
153 RiHookKind::AfterModulesShutdown => "AfterModulesShutdown",
154 RiHookKind::ConfigReload => "ConfigReload",
155 }
156 }
157
158 /// Registers hook handlers for all tracked hook kinds.
159 ///
160 /// This method registers a handler for each hook kind that updates the analytics state
161 /// whenever a hook event is triggered.
162 ///
163 /// # Parameters
164 ///
165 /// - `hooks`: The hook bus to register handlers with
166 fn register_handlers(&self, hooks: &mut RiHookBus) {
167 for kind in Self::all_kinds() {
168 let state = self.state.clone();
169 let id = format!("dms.analytics.{}", Self::kind_label(*kind));
170 hooks.register(*kind, id, move |_ctx, event: &RiHookEvent| {
171 let mut guard = state
172 .lock()
173 .map_err(|_| RiError::Other("analytics state poisoned".to_string()))?;
174 guard.total_events = guard.total_events.saturating_add(1);
175 let kind_label = Self::kind_label(event.kind).to_string();
176 *guard.per_kind.entry(kind_label).or_insert(0) += 1;
177 if let Some(phase) = &event.phase {
178 *guard.per_phase.entry(phase.as_str().to_string()).or_insert(0) += 1;
179 }
180 if let Some(module) = &event.module {
181 *guard.per_module.entry(module.clone()).or_insert(0) += 1;
182 }
183 Ok(())
184 });
185 }
186 }
187
188 /// Flushes the analytics summary to a JSON file.
189 ///
190 /// This method generates a JSON summary of the analytics state and writes it to a file
191 /// in the observability directory.
192 ///
193 /// # Parameters
194 ///
195 /// - `ctx`: The service context to use for file operations and logging
196 ///
197 /// # Returns
198 ///
199 /// A `RiResult` indicating success or failure
200 fn flush_summary(&self, ctx: &mut RiServiceContext) -> RiResult<()> {
201 let snapshot = {
202 let guard = self
203 .state
204 .lock()
205 .map_err(|_| RiError::Other("analytics state poisoned".to_string()))?;
206 json!({
207 "timestamp": SystemTime::now()
208 .duration_since(UNIX_EPOCH)
209 .map(|d| d.as_secs())
210 .unwrap_or(0),
211 "total_events": guard.total_events,
212 "per_kind": guard.per_kind,
213 "per_phase": guard.per_phase,
214 "per_module": guard.per_module,
215 })
216 };
217
218 let fs = ctx.fs();
219 let output = fs.observability_dir().join("lifecycle_analytics.json");
220 fs.write_json(&output, &snapshot)?;
221 let logger = ctx.logger();
222 let _ = logger.info("Ri.LogAnalytics", format!("summary_path={}", output.display()));
223 Ok(())
224 }
225}
226
227impl ServiceModule for RiLogAnalyticsModule {
228 /// Returns the name of the analytics module.
229 ///
230 /// This name is used for identification, logging, and dependency resolution.
231 fn name(&self) -> &str {
232 "Ri.LogAnalytics"
233 }
234
235 /// Indicates if the analytics module is critical to the operation of the system.
236 ///
237 /// The analytics module is non-critical, meaning it can fail without causing the entire
238 /// system to fail.
239 fn is_critical(&self) -> bool {
240 false
241 }
242
243 /// Initializes the analytics module.
244 ///
245 /// This method:
246 /// 1. Reads the analytics configuration from the service context
247 /// 2. Enables or disables the module based on configuration
248 /// 3. Registers hook handlers if the module is enabled
249 ///
250 /// # Parameters
251 ///
252 /// - `ctx`: The service context containing configuration and hooks
253 ///
254 /// # Returns
255 ///
256 /// A `RiResult` indicating success or failure
257 fn init(&mut self, ctx: &mut RiServiceContext) -> RiResult<()> {
258 let binding = ctx.config();
259 let cfg = binding.config();
260 self.enabled = cfg.get_bool("analytics.enabled").unwrap_or(true);
261 if !self.enabled {
262 return Ok(());
263 }
264 let hooks: &mut RiHookBus = ctx.hooks_mut();
265 self.register_handlers(hooks);
266 Ok(())
267 }
268
269 /// Flushes analytics data after the application has shutdown.
270 ///
271 /// This method generates a final analytics report and writes it to a file after
272 /// all modules have been shutdown.
273 ///
274 /// # Parameters
275 ///
276 /// - `ctx`: The service context containing file system and logging capabilities
277 ///
278 /// # Returns
279 ///
280 /// A `RiResult` indicating success or failure
281 fn after_shutdown(&mut self, ctx: &mut RiServiceContext) -> RiResult<()> {
282 if !self.enabled {
283 return Ok(());
284 }
285 self.flush_summary(ctx)
286 }
287}