Skip to main content
Glama
by 8b-is
activity_logger.rs10.1 kB
// Activity Logger - Transparent logging of all Smart Tree operations // "Sunlight is the best disinfectant!" - Hue use anyhow::Result; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs::{self, OpenOptions}; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::Mutex; // Global logger instance static ACTIVITY_LOGGER: Lazy<Mutex<Option<ActivityLogger>>> = Lazy::new(|| Mutex::new(None)); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { pub timestamp: DateTime<Utc>, pub session_id: String, pub event_type: String, pub operation: String, pub details: Value, pub path: Option<String>, pub mode: Option<String>, pub flags: Vec<String>, pub duration_ms: Option<u64>, pub error: Option<String>, pub user: String, pub version: String, } pub struct ActivityLogger { log_path: PathBuf, session_id: String, start_time: std::time::Instant, operation_count: u64, } impl ActivityLogger { /// Initialize the global logger pub fn init(log_path: Option<String>) -> Result<()> { let path = if let Some(p) = log_path { PathBuf::from(shellexpand::tilde(&p).to_string()) } else { // Default to ~/.st/st.jsonl let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let st_dir = home.join(".st"); fs::create_dir_all(&st_dir)?; st_dir.join("st.jsonl") }; // Generate session ID let session_id = format!( "{}-{}", Utc::now().format("%Y%m%d-%H%M%S"), uuid::Uuid::new_v4() .to_string() .chars() .take(8) .collect::<String>() ); let logger = ActivityLogger { log_path: path.clone(), session_id: session_id.clone(), start_time: std::time::Instant::now(), operation_count: 0, }; // Store in global *ACTIVITY_LOGGER.lock().unwrap() = Some(logger); // Log initialization Self::log_event( "startup", "initialize", serde_json::json!({ "log_path": path.to_string_lossy(), "session_id": session_id, "pid": std::process::id(), "args": std::env::args().collect::<Vec<_>>(), "cwd": std::env::current_dir().ok().map(|p| p.to_string_lossy().to_string()), }), )?; Ok(()) } /// Log an event pub fn log_event(event_type: &str, operation: &str, details: Value) -> Result<()> { let logger_guard = ACTIVITY_LOGGER.lock().unwrap(); if let Some(logger) = logger_guard.as_ref() { let entry = LogEntry { timestamp: Utc::now(), session_id: logger.session_id.clone(), event_type: event_type.to_string(), operation: operation.to_string(), details, path: std::env::current_dir() .ok() .map(|p| p.to_string_lossy().to_string()), mode: None, // Will be filled by specific operations flags: std::env::args().skip(1).collect(), duration_ms: Some(logger.start_time.elapsed().as_millis() as u64), error: None, user: whoami::username(), version: env!("CARGO_PKG_VERSION").to_string(), }; // Append to JSONL file let mut file = OpenOptions::new() .create(true) .append(true) .open(&logger.log_path)?; writeln!(file, "{}", serde_json::to_string(&entry)?)?; } Ok(()) } /// Log an error pub fn log_error(operation: &str, error: &str, context: Value) -> Result<()> { let logger_guard = ACTIVITY_LOGGER.lock().unwrap(); if let Some(logger) = logger_guard.as_ref() { let entry = LogEntry { timestamp: Utc::now(), session_id: logger.session_id.clone(), event_type: "error".to_string(), operation: operation.to_string(), details: context, path: std::env::current_dir() .ok() .map(|p| p.to_string_lossy().to_string()), mode: None, flags: std::env::args().skip(1).collect(), duration_ms: Some(logger.start_time.elapsed().as_millis() as u64), error: Some(error.to_string()), user: whoami::username(), version: env!("CARGO_PKG_VERSION").to_string(), }; let mut file = OpenOptions::new() .create(true) .append(true) .open(&logger.log_path)?; writeln!(file, "{}", serde_json::to_string(&entry)?)?; } Ok(()) } /// Log a scan operation pub fn log_scan(path: &Path, mode: &str, file_count: usize, dir_count: usize) -> Result<()> { Self::log_event( "scan", "directory_scan", serde_json::json!({ "path": path.to_string_lossy(), "mode": mode, "file_count": file_count, "directory_count": dir_count, "total_items": file_count + dir_count, }), ) } /// Log MCP operations pub fn log_mcp(method: &str, params: &Value, result: Option<&Value>) -> Result<()> { Self::log_event( "mcp", method, serde_json::json!({ "params": params, "result": result, "success": result.is_some(), }), ) } /// Log hook operations pub fn log_hook(hook_type: &str, action: &str, details: Value) -> Result<()> { Self::log_event("hook", &format!("{}_{}", hook_type, action), details) } /// Log memory operations pub fn log_memory(operation: &str, keywords: &[String], context: Option<&str>) -> Result<()> { Self::log_event( "memory", operation, serde_json::json!({ "keywords": keywords, "context_preview": context.map(|c| { if c.len() > 100 { format!("{}...", &c[..100]) } else { c.to_string() } }), }), ) } /// Log performance metrics pub fn log_performance( operation: &str, duration_ms: u64, items_processed: usize, ) -> Result<()> { Self::log_event( "performance", operation, serde_json::json!({ "duration_ms": duration_ms, "items_processed": items_processed, "items_per_second": if duration_ms > 0 { items_processed as f64 / (duration_ms as f64 / 1000.0) } else { 0.0 }, }), ) } /// Log consciousness operations pub fn log_consciousness(operation: &str, state: &str, details: Value) -> Result<()> { Self::log_event( "consciousness", operation, serde_json::json!({ "state": state, "details": details, }), ) } /// Get session statistics pub fn get_session_stats() -> Result<Value> { let logger_guard = ACTIVITY_LOGGER.lock().unwrap(); if let Some(logger) = logger_guard.as_ref() { // Count events in current session let content = fs::read_to_string(&logger.log_path)?; let session_events: Vec<LogEntry> = content .lines() .filter_map(|line| serde_json::from_str::<LogEntry>(line).ok()) .filter(|entry| entry.session_id == logger.session_id) .collect(); let event_types: std::collections::HashMap<String, usize> = session_events .iter() .fold(std::collections::HashMap::new(), |mut acc, entry| { *acc.entry(entry.event_type.clone()).or_insert(0) += 1; acc }); Ok(serde_json::json!({ "session_id": logger.session_id, "duration_seconds": logger.start_time.elapsed().as_secs(), "total_events": session_events.len(), "event_types": event_types, "log_file": logger.log_path.to_string_lossy(), })) } else { Ok(serde_json::json!({ "status": "logging_disabled" })) } } /// Shutdown logging and write final stats pub fn shutdown() -> Result<()> { let logger_guard = ACTIVITY_LOGGER.lock().unwrap(); if let Some(_logger) = logger_guard.as_ref() { let stats = Self::get_session_stats()?; Self::log_event("shutdown", "finalize", stats)?; } Ok(()) } } /// Check if logging is enabled pub fn is_logging_enabled() -> bool { ACTIVITY_LOGGER.lock().unwrap().is_some() } /// Quick log macro for convenience #[macro_export] macro_rules! log_activity { ($event:expr, $operation:expr) => { if $crate::activity_logger::is_logging_enabled() { let _ = $crate::activity_logger::ActivityLogger::log_event( $event, $operation, serde_json::json!({}), ); } }; ($event:expr, $operation:expr, $details:expr) => { if $crate::activity_logger::is_logging_enabled() { let _ = $crate::activity_logger::ActivityLogger::log_event($event, $operation, $details); } }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server