dmsc/core/analytics.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC 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 DMSC, 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//! - **DMSCLogAnalyticsModule**: 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;
41use std::sync::{Arc, Mutex};
42use std::time::{SystemTime, UNIX_EPOCH};
43
44use crate::core::{DMSCResult, DMSCServiceContext, DMSCError, ServiceModule};
45use crate::hooks::{DMSCHookBus, DMSCHookEvent, DMSCHookKind};
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: HashMap<String, u64>,
61 /// Number of events per module phase
62 per_phase: HashMap<String, u64>,
63 /// Number of events per module name
64 per_module: HashMap<String, u64>,
65}
66
67/// Log analytics module for DMSC.
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 `DMSCAppBuilder` 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 DMSCLogAnalyticsModule {
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 DMSCLogAnalyticsModule {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl DMSCLogAnalyticsModule {
98 /// Creates a new instance of the log analytics module.
99 ///
100 /// Returns a new `DMSCLogAnalyticsModule` with default settings.
101 pub fn new() -> Self {
102 DMSCLogAnalyticsModule {
103 state: Arc::new(Mutex::new(AnalyticsState::default())),
104 enabled: true,
105 }
106 }
107
108 /// Returns all hook kinds that the analytics module tracks.
109 ///
110 /// This method returns a static slice of all hook kinds that the analytics module
111 /// registers handlers for.
112 fn all_kinds() -> &'static [DMSCHookKind] {
113 use DMSCHookKind::*;
114 const KINDS: [DMSCHookKind; 9] = [
115 Startup,
116 Shutdown,
117 BeforeModulesInit,
118 AfterModulesInit,
119 BeforeModulesStart,
120 AfterModulesStart,
121 BeforeModulesShutdown,
122 AfterModulesShutdown,
123 ConfigReload,
124 ];
125 &KINDS
126 }
127
128 /// Returns a string label for the given hook kind.
129 ///
130 /// This method converts a `DMSCHookKind` enum variant to a human-readable string.
131 ///
132 /// # Parameters
133 ///
134 /// - `kind`: The hook kind to get a label for
135 ///
136 /// # Returns
137 ///
138 /// A static string label for the hook kind
139 fn kind_label(kind: DMSCHookKind) -> &'static str {
140 match kind {
141 DMSCHookKind::Startup => "Startup",
142 DMSCHookKind::Shutdown => "Shutdown",
143 DMSCHookKind::BeforeModulesInit => "BeforeModulesInit",
144 DMSCHookKind::AfterModulesInit => "AfterModulesInit",
145 DMSCHookKind::BeforeModulesStart => "BeforeModulesStart",
146 DMSCHookKind::AfterModulesStart => "AfterModulesStart",
147 DMSCHookKind::BeforeModulesShutdown => "BeforeModulesShutdown",
148 DMSCHookKind::AfterModulesShutdown => "AfterModulesShutdown",
149 DMSCHookKind::ConfigReload => "ConfigReload",
150 }
151 }
152
153 /// Registers hook handlers for all tracked hook kinds.
154 ///
155 /// This method registers a handler for each hook kind that updates the analytics state
156 /// whenever a hook event is triggered.
157 ///
158 /// # Parameters
159 ///
160 /// - `hooks`: The hook bus to register handlers with
161 fn register_handlers(&self, hooks: &mut DMSCHookBus) {
162 for kind in Self::all_kinds() {
163 let state = self.state.clone();
164 let id = format!("dms.analytics.{}", Self::kind_label(*kind));
165 hooks.register(*kind, id, move |_ctx, event: &DMSCHookEvent| {
166 let mut guard = state
167 .lock()
168 .map_err(|_| DMSCError::Other("analytics state poisoned".to_string()))?;
169 guard.total_events = guard.total_events.saturating_add(1);
170 let kind_label = Self::kind_label(event.kind).to_string();
171 *guard.per_kind.entry(kind_label).or_insert(0) += 1;
172 if let Some(phase) = &event.phase {
173 *guard.per_phase.entry(phase.as_str().to_string()).or_insert(0) += 1;
174 }
175 if let Some(module) = &event.module {
176 *guard.per_module.entry(module.clone()).or_insert(0) += 1;
177 }
178 Ok(())
179 });
180 }
181 }
182
183 /// Flushes the analytics summary to a JSON file.
184 ///
185 /// This method generates a JSON summary of the analytics state and writes it to a file
186 /// in the observability directory.
187 ///
188 /// # Parameters
189 ///
190 /// - `ctx`: The service context to use for file operations and logging
191 ///
192 /// # Returns
193 ///
194 /// A `DMSCResult` indicating success or failure
195 fn flush_summary(&self, ctx: &mut DMSCServiceContext) -> DMSCResult<()> {
196 let snapshot = {
197 let guard = self
198 .state
199 .lock()
200 .map_err(|_| DMSCError::Other("analytics state poisoned".to_string()))?;
201 json!({
202 "timestamp": SystemTime::now()
203 .duration_since(UNIX_EPOCH)
204 .map(|d| d.as_secs())
205 .unwrap_or(0),
206 "total_events": guard.total_events,
207 "per_kind": guard.per_kind,
208 "per_phase": guard.per_phase,
209 "per_module": guard.per_module,
210 })
211 };
212
213 let fs = ctx.fs();
214 let output = fs.observability_dir().join("lifecycle_analytics.json");
215 fs.write_json(&output, &snapshot)?;
216 let logger = ctx.logger();
217 let _ = logger.info("DMSC.LogAnalytics", format!("summary_path={}", output.display()));
218 Ok(())
219 }
220}
221
222impl ServiceModule for DMSCLogAnalyticsModule {
223 /// Returns the name of the analytics module.
224 ///
225 /// This name is used for identification, logging, and dependency resolution.
226 fn name(&self) -> &str {
227 "DMSC.LogAnalytics"
228 }
229
230 /// Indicates if the analytics module is critical to the operation of the system.
231 ///
232 /// The analytics module is non-critical, meaning it can fail without causing the entire
233 /// system to fail.
234 fn is_critical(&self) -> bool {
235 false
236 }
237
238 /// Initializes the analytics module.
239 ///
240 /// This method:
241 /// 1. Reads the analytics configuration from the service context
242 /// 2. Enables or disables the module based on configuration
243 /// 3. Registers hook handlers if the module is enabled
244 ///
245 /// # Parameters
246 ///
247 /// - `ctx`: The service context containing configuration and hooks
248 ///
249 /// # Returns
250 ///
251 /// A `DMSCResult` indicating success or failure
252 fn init(&mut self, ctx: &mut DMSCServiceContext) -> DMSCResult<()> {
253 let binding = ctx.config();
254 let cfg = binding.config();
255 self.enabled = cfg.get_bool("analytics.enabled").unwrap_or(true);
256 if !self.enabled {
257 return Ok(());
258 }
259 let hooks: &mut DMSCHookBus = ctx.hooks_mut();
260 self.register_handlers(hooks);
261 Ok(())
262 }
263
264 /// Flushes analytics data after the application has shutdown.
265 ///
266 /// This method generates a final analytics report and writes it to a file after
267 /// all modules have been shutdown.
268 ///
269 /// # Parameters
270 ///
271 /// - `ctx`: The service context containing file system and logging capabilities
272 ///
273 /// # Returns
274 ///
275 /// A `DMSCResult` indicating success or failure
276 fn after_shutdown(&mut self, ctx: &mut DMSCServiceContext) -> DMSCResult<()> {
277 if !self.enabled {
278 return Ok(());
279 }
280 self.flush_summary(ctx)
281 }
282}