#![allow(non_snake_case)]
use std::collections::HashMap as FxHashMap;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::{RiResult, RiServiceContext, RiError, ServiceModule};
use crate::hooks::{RiHookBus, RiHookEvent, RiHookKind};
use serde_json::json;
#[derive(Default)]
struct AnalyticsState {
total_events: u64,
per_kind: FxHashMap<String, u64>,
per_phase: FxHashMap<String, u64>,
per_module: FxHashMap<String, u64>,
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiLogAnalyticsModule {
state: Arc<Mutex<AnalyticsState>>,
enabled: bool,
}
impl Default for RiLogAnalyticsModule {
fn default() -> Self {
Self::new()
}
}
impl RiLogAnalyticsModule {
pub fn new() -> Self {
RiLogAnalyticsModule {
state: Arc::new(Mutex::new(AnalyticsState::default())),
enabled: true,
}
}
fn all_kinds() -> &'static [RiHookKind] {
use RiHookKind::*;
const KINDS: [RiHookKind; 9] = [
Startup,
Shutdown,
BeforeModulesInit,
AfterModulesInit,
BeforeModulesStart,
AfterModulesStart,
BeforeModulesShutdown,
AfterModulesShutdown,
ConfigReload,
];
&KINDS
}
fn kind_label(kind: RiHookKind) -> &'static str {
match kind {
RiHookKind::Startup => "Startup",
RiHookKind::Shutdown => "Shutdown",
RiHookKind::BeforeModulesInit => "BeforeModulesInit",
RiHookKind::AfterModulesInit => "AfterModulesInit",
RiHookKind::BeforeModulesStart => "BeforeModulesStart",
RiHookKind::AfterModulesStart => "AfterModulesStart",
RiHookKind::BeforeModulesShutdown => "BeforeModulesShutdown",
RiHookKind::AfterModulesShutdown => "AfterModulesShutdown",
RiHookKind::ConfigReload => "ConfigReload",
}
}
fn register_handlers(&self, hooks: &mut RiHookBus) {
for kind in Self::all_kinds() {
let state = self.state.clone();
let id = format!("dms.analytics.{}", Self::kind_label(*kind));
hooks.register(*kind, id, move |_ctx, event: &RiHookEvent| {
let mut guard = state
.lock()
.map_err(|_| RiError::Other("analytics state poisoned".to_string()))?;
guard.total_events = guard.total_events.saturating_add(1);
let kind_label = Self::kind_label(event.kind).to_string();
*guard.per_kind.entry(kind_label).or_insert(0) += 1;
if let Some(phase) = &event.phase {
*guard.per_phase.entry(phase.as_str().to_string()).or_insert(0) += 1;
}
if let Some(module) = &event.module {
*guard.per_module.entry(module.clone()).or_insert(0) += 1;
}
Ok(())
});
}
}
fn flush_summary(&self, ctx: &mut RiServiceContext) -> RiResult<()> {
let snapshot = {
let guard = self
.state
.lock()
.map_err(|_| RiError::Other("analytics state poisoned".to_string()))?;
json!({
"timestamp": SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
"total_events": guard.total_events,
"per_kind": guard.per_kind,
"per_phase": guard.per_phase,
"per_module": guard.per_module,
})
};
let fs = ctx.fs();
let output = fs.observability_dir().join("lifecycle_analytics.json");
fs.write_json(&output, &snapshot)?;
let logger = ctx.logger();
let _ = logger.info("Ri.LogAnalytics", format!("summary_path={}", output.display()));
Ok(())
}
}
impl ServiceModule for RiLogAnalyticsModule {
fn name(&self) -> &str {
"Ri.LogAnalytics"
}
fn is_critical(&self) -> bool {
false
}
fn init(&mut self, ctx: &mut RiServiceContext) -> RiResult<()> {
let binding = ctx.config();
let cfg = binding.config();
self.enabled = cfg.get_bool("analytics.enabled").unwrap_or(true);
if !self.enabled {
return Ok(());
}
let hooks: &mut RiHookBus = ctx.hooks_mut();
self.register_handlers(hooks);
Ok(())
}
fn after_shutdown(&mut self, ctx: &mut RiServiceContext) -> RiResult<()> {
if !self.enabled {
return Ok(());
}
self.flush_summary(ctx)
}
}