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}