Skip to main content

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}