gioser
⎇ 48e41331a104ba136b34e7695dccb08ef9188703 crates/ente-journald-compat/src/journalctl.rs Rust · 197 siq'ikuna · 6.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

journal-query CLI, machined, polkit con grants reales, GNOME boot test 48e41331a · Sergio ·

1//! ente-journalctl: query CLI sobre el journal persistido en CAS.
2//!
3//! Lee el index `~/.local/share/ente/journal/index.log` (líneas
4//! `timestamp_ms:source:unit:sha_hex`), filtra, y para cada match
5//! restituye el blob desde CAS y lo imprime.
6//!
7//! Uso:
8//!   ente-journalctl                       # todo el journal
9//!   ente-journalctl --unit foo.service    # filtra por unit
10//!   ente-journalctl --since 60            # últimos 60 segundos
11//!   ente-journalctl --grep "panic"        # contiene "panic"
12//!   ente-journalctl --tail 20             # últimas 20 entries
13//!   ente-journalctl --json                # output JSON-lines
14
15use std::path::PathBuf;
16
17struct Args {
18    unit: Option<String>,
19    since_secs: Option<u64>,
20    grep: Option<String>,
21    tail: Option<usize>,
22    source: Option<String>,
23    json: bool,
24}
25
26fn parse_args() -> Args {
27    let mut args = std::env::args().skip(1);
28    let mut a = Args {
29        unit: None, since_secs: None, grep: None, tail: None, source: None, json: false,
30    };
31    while let Some(arg) = args.next() {
32        match arg.as_str() {
33            "--unit" | "-u" => a.unit = args.next(),
34            "--since" | "-S" => a.since_secs = args.next().and_then(|s| s.parse().ok()),
35            "--grep" | "-g" => a.grep = args.next(),
36            "--tail" | "-n" => a.tail = args.next().and_then(|s| s.parse().ok()),
37            "--source" => a.source = args.next(),
38            "--json" => a.json = true,
39            "-h" | "--help" => { print_help(); std::process::exit(0); }
40            other => {
41                eprintln!("argumento desconocido: {other}");
42                print_help();
43                std::process::exit(2);
44            }
45        }
46    }
47    a
48}
49
50fn print_help() {
51    eprintln!("ente-journalctl — query CLI del journal persistido en CAS");
52    eprintln!();
53    eprintln!("Filtros:");
54    eprintln!("  --unit, -u <name>    Filtra por unidad (e.g. foo.service)");
55    eprintln!("  --source <s>         journal | syslog");
56    eprintln!("  --since, -S <secs>   Sólo últimos N segundos");
57    eprintln!("  --grep, -g <text>    Contiene <text> en el body decoded");
58    eprintln!("  --tail, -n <N>       Últimas N entries");
59    eprintln!("Output:");
60    eprintln!("  --json               JSON-lines en lugar de pretty");
61}
62
63fn index_path() -> PathBuf {
64    let base = if let Ok(d) = std::env::var("XDG_DATA_HOME") { d }
65        else if let Ok(h) = std::env::var("HOME") { format!("{h}/.local/share") }
66        else { "/var/lib".into() };
67    PathBuf::from(base).join("ente").join("journal").join("index.log")
68}
69
70fn now_ms() -> u128 {
71    std::time::SystemTime::now()
72        .duration_since(std::time::UNIX_EPOCH)
73        .map(|d| d.as_millis())
74        .unwrap_or(0)
75}
76
77#[derive(Debug)]
78struct IndexEntry {
79    timestamp_ms: u128,
80    source: String,
81    unit: String,
82    sha_hex: String,
83}
84
85fn parse_line(line: &str) -> Option<IndexEntry> {
86    let mut parts = line.splitn(4, ':');
87    let ts: u128 = parts.next()?.parse().ok()?;
88    let source = parts.next()?.to_string();
89    let unit = parts.next()?.to_string();
90    let sha = parts.next()?.to_string();
91    if sha.len() != 64 { return None; }
92    Some(IndexEntry { timestamp_ms: ts, source, unit, sha_hex: sha })
93}
94
95fn parse_sha(hex: &str) -> Option<[u8; 32]> {
96    if hex.len() != 64 { return None; }
97    let mut sha = [0u8; 32];
98    for i in 0..32 {
99        sha[i] = u8::from_str_radix(&hex[i*2..i*2+2], 16).ok()?;
100    }
101    Some(sha)
102}
103
104fn main() -> anyhow::Result<()> {
105    let args = parse_args();
106    let path = index_path();
107    if !path.exists() {
108        eprintln!("index no existe: {} — ¿journald-compat ha corrido?", path.display());
109        std::process::exit(1);
110    }
111    let raw = std::fs::read_to_string(&path)?;
112    let mut entries: Vec<IndexEntry> = raw.lines()
113        .filter_map(parse_line)
114        .collect();
115
116    // Filtros
117    let now = now_ms();
118    if let Some(secs) = args.since_secs {
119        let cutoff = now.saturating_sub(secs as u128 * 1000);
120        entries.retain(|e| e.timestamp_ms >= cutoff);
121    }
122    if let Some(unit) = &args.unit {
123        entries.retain(|e| &e.unit == unit);
124    }
125    if let Some(src) = &args.source {
126        entries.retain(|e| &e.source == src);
127    }
128    // tail después de filtros temporales/identidad pero antes de grep —
129    // grep es post porque requiere cargar bytes del CAS.
130
131    let mut out: Vec<(IndexEntry, String)> = entries.into_iter()
132        .filter_map(|e| {
133            let sha = parse_sha(&e.sha_hex)?;
134            let bytes = ente_cas::resolve(&sha).ok()?;
135            let body = String::from_utf8_lossy(&bytes).into_owned();
136            Some((e, body))
137        })
138        .collect();
139
140    if let Some(g) = &args.grep {
141        out.retain(|(_, body)| body.contains(g.as_str()));
142    }
143    if let Some(n) = args.tail {
144        let len = out.len();
145        if len > n { out.drain(..len - n); }
146    }
147
148    for (e, body) in out {
149        if args.json {
150            print_json(&e, &body);
151        } else {
152            print_pretty(&e, &body);
153        }
154    }
155    Ok(())
156}
157
158fn print_pretty(e: &IndexEntry, body: &str) {
159    let secs = e.timestamp_ms / 1000;
160    let ms = e.timestamp_ms % 1000;
161    let header = if e.unit == "-" {
162        format!("{}.{:03} [{}]", secs, ms, e.source)
163    } else {
164        format!("{}.{:03} [{}] {{{}}}", secs, ms, e.source, e.unit)
165    };
166    println!("{header}");
167    // Si es journald native (KEY=value lines), extraer MESSAGE.
168    if body.contains('=') && body.lines().any(|l| l.contains('=')) {
169        for line in body.lines() {
170            if let Some((k, v)) = line.split_once('=') {
171                if k.trim() == "MESSAGE" {
172                    println!("  {v}");
173                    return;
174                }
175            }
176        }
177    }
178    for line in body.trim_end().lines() {
179        println!("  {line}");
180    }
181}
182
183fn print_json(e: &IndexEntry, body: &str) {
184    // JSON-lines básico, sin dependencia de serde — formato simple y estable.
185    let escaped_body = body
186        .replace('\\', "\\\\")
187        .replace('"', "\\\"")
188        .replace('\n', "\\n")
189        .replace('\r', "\\r")
190        .replace('\t', "\\t");
191    let unit_field = if e.unit == "-" { "null".to_string() }
192        else { format!("\"{}\"", e.unit) };
193    println!(
194        r#"{{"timestamp_ms":{},"source":"{}","unit":{},"sha":"{}","body":"{}"}}"#,
195        e.timestamp_ms, e.source, unit_field, e.sha_hex, escaped_body
196    );
197}