⎇ 48e41331a104ba136b34e7695dccb08ef9188703
crates/ente-journald-compat/src/journalctl.rs
Historia
Quién tocó qué
crudo
journal-query CLI, machined, polkit con grants reales, GNOME boot test
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}