Files
agent-manager/src/commands/log_cmd.rs
T

121 lines
3.4 KiB
Rust

//! 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<Vec<Event>> {
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)
}
fn parse_since(s: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
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)]
struct LogRow {
ts: String,
kind: String,
agent: Option<String>,
pid: Option<u32>,
exit_code: Option<i32>,
duration_s: Option<u64>,
session: Option<String>,
}
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(),
}
}
}
pub fn run(
app: &App,
agent: Option<&str>,
kinds: Option<&str>,
since: Option<&str>,
limit: usize,
) -> Result<i32> {
let events = filter_events(app, agent, kinds, since, limit)?;
if app.json() {
let rows: Vec<LogRow> = 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());
}
}