Skip to main content

st/
activity_logger.rs

1// Activity Logger - Transparent logging of all Smart Tree operations
2// "Sunlight is the best disinfectant!" - Hue
3
4use anyhow::Result;
5use chrono::{DateTime, Utc};
6use once_cell::sync::Lazy;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::fs::{self, OpenOptions};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14// Global logger instance
15static ACTIVITY_LOGGER: Lazy<Mutex<Option<ActivityLogger>>> = Lazy::new(|| Mutex::new(None));
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct LogEntry {
19    pub timestamp: DateTime<Utc>,
20    pub session_id: String,
21    pub event_type: String,
22    pub operation: String,
23    pub details: Value,
24    pub path: Option<String>,
25    pub mode: Option<String>,
26    pub flags: Vec<String>,
27    pub duration_ms: Option<u64>,
28    pub error: Option<String>,
29    pub user: String,
30    pub version: String,
31}
32
33pub struct ActivityLogger {
34    log_path: PathBuf,
35    session_id: String,
36    start_time: std::time::Instant,
37    operation_count: u64,
38}
39
40impl ActivityLogger {
41    /// Initialize the global logger
42    pub fn init(log_path: Option<String>) -> Result<()> {
43        let path = if let Some(p) = log_path {
44            PathBuf::from(shellexpand::tilde(&p).to_string())
45        } else {
46            // Default to ~/.st/st.jsonl
47            let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
48            let st_dir = home.join(".st");
49            fs::create_dir_all(&st_dir)?;
50            st_dir.join("st.jsonl")
51        };
52
53        // Generate session ID
54        let session_id = format!(
55            "{}-{}",
56            Utc::now().format("%Y%m%d-%H%M%S"),
57            uuid::Uuid::new_v4()
58                .to_string()
59                .chars()
60                .take(8)
61                .collect::<String>()
62        );
63
64        let logger = ActivityLogger {
65            log_path: path.clone(),
66            session_id: session_id.clone(),
67            start_time: std::time::Instant::now(),
68            operation_count: 0,
69        };
70
71        // Store in global
72        *ACTIVITY_LOGGER.lock().unwrap() = Some(logger);
73
74        // Log initialization
75        Self::log_event(
76            "startup",
77            "initialize",
78            serde_json::json!({
79                "log_path": path.to_string_lossy(),
80                "session_id": session_id,
81                "pid": std::process::id(),
82                "args": std::env::args().collect::<Vec<_>>(),
83                "cwd": std::env::current_dir().ok().map(|p| p.to_string_lossy().to_string()),
84            }),
85        )?;
86
87        Ok(())
88    }
89
90    /// Log an event
91    pub fn log_event(event_type: &str, operation: &str, details: Value) -> Result<()> {
92        let logger_guard = ACTIVITY_LOGGER.lock().unwrap();
93        if let Some(logger) = logger_guard.as_ref() {
94            let entry = LogEntry {
95                timestamp: Utc::now(),
96                session_id: logger.session_id.clone(),
97                event_type: event_type.to_string(),
98                operation: operation.to_string(),
99                details,
100                path: std::env::current_dir()
101                    .ok()
102                    .map(|p| p.to_string_lossy().to_string()),
103                mode: None, // Will be filled by specific operations
104                flags: std::env::args().skip(1).collect(),
105                duration_ms: Some(logger.start_time.elapsed().as_millis() as u64),
106                error: None,
107                user: whoami::username(),
108                version: env!("CARGO_PKG_VERSION").to_string(),
109            };
110
111            // Append to JSONL file
112            let mut file = OpenOptions::new()
113                .create(true)
114                .append(true)
115                .open(&logger.log_path)?;
116
117            writeln!(file, "{}", serde_json::to_string(&entry)?)?;
118        }
119        Ok(())
120    }
121
122    /// Log an error
123    pub fn log_error(operation: &str, error: &str, context: Value) -> Result<()> {
124        let logger_guard = ACTIVITY_LOGGER.lock().unwrap();
125        if let Some(logger) = logger_guard.as_ref() {
126            let entry = LogEntry {
127                timestamp: Utc::now(),
128                session_id: logger.session_id.clone(),
129                event_type: "error".to_string(),
130                operation: operation.to_string(),
131                details: context,
132                path: std::env::current_dir()
133                    .ok()
134                    .map(|p| p.to_string_lossy().to_string()),
135                mode: None,
136                flags: std::env::args().skip(1).collect(),
137                duration_ms: Some(logger.start_time.elapsed().as_millis() as u64),
138                error: Some(error.to_string()),
139                user: whoami::username(),
140                version: env!("CARGO_PKG_VERSION").to_string(),
141            };
142
143            let mut file = OpenOptions::new()
144                .create(true)
145                .append(true)
146                .open(&logger.log_path)?;
147
148            writeln!(file, "{}", serde_json::to_string(&entry)?)?;
149        }
150        Ok(())
151    }
152
153    /// Log a scan operation
154    pub fn log_scan(path: &Path, mode: &str, file_count: usize, dir_count: usize) -> Result<()> {
155        Self::log_event(
156            "scan",
157            "directory_scan",
158            serde_json::json!({
159                "path": path.to_string_lossy(),
160                "mode": mode,
161                "file_count": file_count,
162                "directory_count": dir_count,
163                "total_items": file_count + dir_count,
164            }),
165        )
166    }
167
168    /// Log MCP operations
169    pub fn log_mcp(method: &str, params: &Value, result: Option<&Value>) -> Result<()> {
170        Self::log_event(
171            "mcp",
172            method,
173            serde_json::json!({
174                "params": params,
175                "result": result,
176                "success": result.is_some(),
177            }),
178        )
179    }
180
181    /// Log hook operations
182    pub fn log_hook(hook_type: &str, action: &str, details: Value) -> Result<()> {
183        Self::log_event("hook", &format!("{}_{}", hook_type, action), details)
184    }
185
186    /// Log memory operations
187    pub fn log_memory(operation: &str, keywords: &[String], context: Option<&str>) -> Result<()> {
188        Self::log_event(
189            "memory",
190            operation,
191            serde_json::json!({
192                "keywords": keywords,
193                "context_preview": context.map(|c| {
194                    if c.len() > 100 {
195                        format!("{}...", &c[..100])
196                    } else {
197                        c.to_string()
198                    }
199                }),
200            }),
201        )
202    }
203
204    /// Log performance metrics
205    pub fn log_performance(
206        operation: &str,
207        duration_ms: u64,
208        items_processed: usize,
209    ) -> Result<()> {
210        Self::log_event(
211            "performance",
212            operation,
213            serde_json::json!({
214                "duration_ms": duration_ms,
215                "items_processed": items_processed,
216                "items_per_second": if duration_ms > 0 {
217                    items_processed as f64 / (duration_ms as f64 / 1000.0)
218                } else {
219                    0.0
220                },
221            }),
222        )
223    }
224
225    /// Log consciousness operations
226    pub fn log_consciousness(operation: &str, state: &str, details: Value) -> Result<()> {
227        Self::log_event(
228            "consciousness",
229            operation,
230            serde_json::json!({
231                "state": state,
232                "details": details,
233            }),
234        )
235    }
236
237    /// Get session statistics
238    pub fn get_session_stats() -> Result<Value> {
239        let logger_guard = ACTIVITY_LOGGER.lock().unwrap();
240        if let Some(logger) = logger_guard.as_ref() {
241            // Count events in current session
242            let content = fs::read_to_string(&logger.log_path)?;
243            let session_events: Vec<LogEntry> = content
244                .lines()
245                .filter_map(|line| serde_json::from_str::<LogEntry>(line).ok())
246                .filter(|entry| entry.session_id == logger.session_id)
247                .collect();
248
249            let event_types: std::collections::HashMap<String, usize> =
250                session_events
251                    .iter()
252                    .fold(std::collections::HashMap::new(), |mut acc, entry| {
253                        *acc.entry(entry.event_type.clone()).or_insert(0) += 1;
254                        acc
255                    });
256
257            Ok(serde_json::json!({
258                "session_id": logger.session_id,
259                "duration_seconds": logger.start_time.elapsed().as_secs(),
260                "total_events": session_events.len(),
261                "event_types": event_types,
262                "log_file": logger.log_path.to_string_lossy(),
263            }))
264        } else {
265            Ok(serde_json::json!({
266                "status": "logging_disabled"
267            }))
268        }
269    }
270
271    /// Shutdown logging and write final stats
272    pub fn shutdown() -> Result<()> {
273        let logger_guard = ACTIVITY_LOGGER.lock().unwrap();
274        if let Some(_logger) = logger_guard.as_ref() {
275            let stats = Self::get_session_stats()?;
276            Self::log_event("shutdown", "finalize", stats)?;
277        }
278        Ok(())
279    }
280}
281
282/// Check if logging is enabled
283pub fn is_logging_enabled() -> bool {
284    ACTIVITY_LOGGER.lock().unwrap().is_some()
285}
286
287/// Quick log macro for convenience
288#[macro_export]
289macro_rules! log_activity {
290    ($event:expr, $operation:expr) => {
291        if $crate::activity_logger::is_logging_enabled() {
292            let _ = $crate::activity_logger::ActivityLogger::log_event(
293                $event,
294                $operation,
295                serde_json::json!({}),
296            );
297        }
298    };
299    ($event:expr, $operation:expr, $details:expr) => {
300        if $crate::activity_logger::is_logging_enabled() {
301            let _ =
302                $crate::activity_logger::ActivityLogger::log_event($event, $operation, $details);
303        }
304    };
305}