//! log: a filtered view of the event journal — everything am did. use super::*; use crate::events::{self, Event}; use crate::output::print_json; use serde::Serialize; /// Filter journal events: agent, kind list, since date, and a max count. /// Returns events in reverse chronological order (most recent first). pub fn filter_events( app: &App, agent: Option<&str>, kinds: Option<&str>, since: Option<&str>, limit: usize, ) -> Result> { let mut events = events::read_events(&app.events_dir(), 0); if let Some(a) = agent { events.retain(|e| e.agent.as_deref() == Some(a)); } if let Some(list) = kinds { let wanted: Vec<&str> = list.split(',').map(|s| s.trim()).collect(); events.retain(|e| wanted.iter().any(|w| e.kind.as_str() == *w)); } if let Some(s) = since { let cut = parse_since(s)?; events.retain(|e| { chrono::DateTime::parse_from_rfc3339(&e.ts) .map(|t| t >= cut) .unwrap_or(false) }); } events.reverse(); // most recent first events.truncate(limit); Ok(events) } /// Live view: print matching events as they arrive (issue #38). fn follow_loop(app: &App, agent: Option<&str>, kinds: Option<&str>, since: Option<&str>) -> Result { app.log.info("following the journal (Ctrl-C to stop)"); let mut seen: usize = 0; loop { let events = filter_events(app, agent, kinds, since, 200)?; let mut rendered = String::new(); for e in events.iter().rev() { rendered.push_str(&e.ts); rendered.push(' '); rendered.push_str(e.kind.as_str()); if let Some(a) = &e.agent { rendered.push(' '); rendered.push_str(a); } rendered.push('\n'); } let bytes = rendered.as_bytes(); if bytes.len() > seen { print!("{}", text_after(&rendered, seen)); use std::io::Write; let _ = std::io::stdout().flush(); } seen = bytes.len(); std::thread::sleep(std::time::Duration::from_secs(1)); } } pub fn parse_since(s: &str) -> Result> { if let Ok(t) = chrono::DateTime::parse_from_rfc3339(s) { return Ok(t); } chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") .map_err(|_| anyhow!("invalid date '{s}' — expected YYYY-MM-DD or an RFC 3339 timestamp"))? .and_hms_opt(0, 0, 0) .map(|d| chrono::DateTime::from_naive_utc_and_offset(d, chrono::FixedOffset::east_opt(0).unwrap())) .ok_or_else(|| anyhow!("invalid date '{s}'")) } #[derive(Serialize)] pub struct LogRow { pub ts: String, pub kind: String, pub agent: Option, pub pid: Option, pub exit_code: Option, pub duration_s: Option, pub session: Option, } impl From<&Event> for LogRow { fn from(e: &Event) -> Self { LogRow { ts: e.ts.clone(), kind: e.kind.as_str().to_string(), agent: e.agent.clone(), pid: e.pid, exit_code: e.exit_code, duration_s: e.duration_s, session: e.session.clone(), } } } /// Rows for the web API (issue #54): same shape as `am log --json`. pub fn rows( app: &App, agent: Option<&str>, kinds: Option<&str>, since: Option<&str>, limit: usize, ) -> Result> { Ok(filter_events(app, agent, kinds, since, limit)? .iter() .map(LogRow::from) .collect()) } /// Chars of text after byte offset seen (UTF-8 safe). pub fn text_after(text: &str, seen: usize) -> String { text.char_indices() .filter(|(i, _)| *i >= seen) .map(|(_, c)| c) .collect() } pub fn run( app: &App, agent: Option<&str>, kinds: Option<&str>, since: Option<&str>, limit: usize, follow: bool, ) -> Result { if follow { return follow_loop(app, agent, kinds, since); } let events = filter_events(app, agent, kinds, since, limit)?; if app.json() { let rows: Vec = events.iter().map(LogRow::from).collect(); print_json(&rows); return Ok(0); } for e in &events { let mut parts = vec![e.ts.clone(), e.kind.as_str().to_string()]; if let Some(a) = &e.agent { parts.push(a.clone()); } if let Some(p) = e.pid { parts.push(format!("pid={p}")); } if let Some(c) = e.exit_code { parts.push(format!("exit={c}")); } if let Some(d) = e.duration_s { parts.push(format!("dur={d}s")); } println!("{}", parts.join(" ")); } if events.is_empty() { app.log.info("no matching events in the journal"); } Ok(0) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_since_accepts_date_and_timestamp() { let d = parse_since("2026-08-01").unwrap(); assert_eq!(d.format("%Y-%m-%d").to_string(), "2026-08-01"); assert!(parse_since("2026-08-01T00:00:00Z").is_ok()); assert!(parse_since("hier").is_err()); } }