1use 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
14static 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 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 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 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 *ACTIVITY_LOGGER.lock().unwrap() = Some(logger);
73
74 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 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, 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 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 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 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 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 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 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 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 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 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 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 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
282pub fn is_logging_enabled() -> bool {
284 ACTIVITY_LOGGER.lock().unwrap().is_some()
285}
286
287#[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}