1use fs_err::File;
5use fs_err::PathExt;
6use futures::AsyncBufReadExt;
7use futures::AsyncRead;
8use futures::AsyncReadExt;
9use futures::StreamExt;
10use futures::io::BufReader;
11use jiff::Timestamp;
12use kmsg::KmsgParsedEntry;
13use parking_lot::Mutex;
14use std::collections::HashMap;
15use std::io::Read;
16use std::io::Write as _;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20use tracing::Level;
21use tracing::level_filters::LevelFilter;
22use tracing_subscriber::filter::Targets;
23use tracing_subscriber::fmt::MakeWriter;
24use tracing_subscriber::fmt::format::FmtSpan;
25use tracing_subscriber::layer::SubscriberExt;
26use tracing_subscriber::util::SubscriberInitExt;
27
28#[derive(Clone, Debug)]
30pub struct PetriLogSource(Arc<LogSourceInner>);
31
32#[derive(Debug)]
33struct LogSourceInner {
34 root_path: PathBuf,
35 json_log: JsonLog,
36 log_files: Mutex<HashMap<String, PetriLogFile>>,
37 attachments: Mutex<HashMap<String, u64>>,
38 prefix: Option<String>,
39 echo: bool,
40}
41
42impl PetriLogSource {
43 fn new(root_path: &Path, prefix: Option<String>) -> anyhow::Result<Self> {
44 let root_path = root_path.fs_err_canonicalize()?;
46 let jsonl_name = Self::filename_inner(prefix.as_deref(), "petri.jsonl");
47 let jsonl = File::create(root_path.join(jsonl_name))?;
48 Ok(Self(Arc::new(LogSourceInner {
49 json_log: JsonLog(Arc::new(jsonl)),
50 root_path,
51 log_files: Default::default(),
52 attachments: Default::default(),
53 prefix,
54 echo: true,
55 })))
56 }
57}
58
59impl PetriLogSource {
60 pub fn file_only(root_path: &Path) -> anyhow::Result<Self> {
63 let root_path = root_path.fs_err_canonicalize()?;
64 let jsonl = File::create(root_path.join("petri.jsonl"))?;
65 Ok(Self(Arc::new(LogSourceInner {
66 root_path,
67 json_log: JsonLog(Arc::new(jsonl)),
68 log_files: Default::default(),
69 attachments: Default::default(),
70 prefix: None,
71 echo: false,
72 })))
73 }
74
75 pub fn log_file(&self, name: &str) -> anyhow::Result<PetriLogFile> {
80 use std::collections::hash_map::Entry;
81
82 let name = if let Some(prefix) = self.0.prefix.as_ref() {
83 format!("{prefix}{name}")
84 } else {
85 name.to_string()
86 };
87
88 let mut log_files = self.0.log_files.lock();
89 let log_file = match log_files.entry(name.clone()) {
90 Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
91 Entry::Vacant(vacant_entry) => {
92 let mut path = self.0.root_path.join(&name);
93 path.set_extension("log");
97 let file = File::create(&path)?;
98 if self.0.echo {
101 println!("[[ATTACHMENT|{}]]", path.display());
102 }
103 vacant_entry
104 .insert(PetriLogFile(Arc::new(LogFileInner {
105 file,
106 json_log: self.0.json_log.clone(),
107 source: name.clone(),
108 echo: self.0.echo,
109 })))
110 .clone()
111 }
112 };
113 Ok(log_file)
114 }
115
116 fn filename_inner(prefix: Option<&str>, name: &str) -> String {
117 if let Some(prefix) = prefix {
118 format!("{prefix}{name}")
119 } else {
120 name.to_string()
121 }
122 }
123
124 fn filename(&self, name: &str) -> String {
125 Self::filename_inner(self.0.prefix.as_deref(), name)
126 }
127
128 fn attachment_path(&self, name: &str) -> PathBuf {
129 let mut attachments = self.0.attachments.lock();
130
131 let next = attachments.entry(name.to_owned()).or_default();
132 let name = Path::new(&name);
133 let name = if *next == 0 {
134 name
135 } else {
136 let base = name.file_stem().unwrap().to_str().unwrap();
137 let extension = name.extension().unwrap_or_default();
138 &Path::new(&format!("{}_{}", base, *next)).with_extension(extension)
139 };
140 *next += 1;
141 self.0.root_path.join(name)
142 }
143
144 pub fn create_attachment(&self, filename: &str) -> anyhow::Result<File> {
149 self.create_attachment_no_prefix(&self.filename(filename))
150 }
151
152 pub fn create_attachment_no_prefix(&self, filename: &str) -> anyhow::Result<File> {
154 let path = self.attachment_path(filename);
155 let file = File::create(&path)?;
156 self.trace_attachment(&path);
157 Ok(file)
158 }
159
160 pub fn write_attachment(&self, filename: &str, mut data: impl Read) -> anyhow::Result<PathBuf> {
165 let path = self.attachment_path(&self.filename(filename));
166 let mut file = File::create(&path)?;
167 std::io::copy(&mut data, &mut file)?;
168 self.trace_attachment(&path);
169 Ok(path)
170 }
171
172 pub fn copy_attachment(
177 &self,
178 attachment_filename: &str,
179 source_path: &Path,
180 ) -> anyhow::Result<()> {
181 let dest_path = self.attachment_path(&self.filename(attachment_filename));
182 fs_err::copy(source_path, &dest_path)?;
183 self.trace_attachment(&dest_path);
184 Ok(())
185 }
186
187 fn trace_attachment(&self, path: &Path) {
188 self.0
190 .json_log
191 .write_attachment(path.file_name().unwrap().as_ref());
192 if self.0.echo {
193 println!("[[ATTACHMENT|{}]]", path.display());
194 }
195 }
196
197 pub fn log_test_start(&self, name: &str) {
205 fs_err::write(self.0.root_path.join(self.filename("petri.test")), name).unwrap();
206 }
207
208 pub fn log_test_result(&self, r: &anyhow::Result<()>, unstable: bool) {
210 let (result_path, contents) = match &r {
211 Ok(()) => {
212 tracing::info!("test passed");
213 ("petri.passed", String::new())
214 }
215 Err(err) if unstable => {
216 tracing::warn!(
217 error = err.as_ref() as &dyn std::error::Error,
218 "unstable test failed"
219 );
220 ("petri.failed_unstable", format!("{err:#}"))
221 }
222 Err(err) => {
223 tracing::error!(
224 error = err.as_ref() as &dyn std::error::Error,
225 "test failed"
226 );
227 ("petri.failed", format!("{err:#}"))
228 }
229 };
230 fs_err::write(self.0.root_path.join(self.filename(result_path)), contents).unwrap();
234 }
235
236 pub fn output_dir(&self) -> &Path {
238 &self.0.root_path
239 }
240}
241
242#[derive(Clone, Debug)]
243struct JsonLog(Arc<File>);
244
245impl JsonLog {
246 fn write_json(&self, v: &impl serde::Serialize) {
247 let v = serde_json::to_vec(v);
248 if let Ok(mut v) = v {
249 v.push(b'\n');
250 let _ = self.0.as_ref().write_all(&v);
252 }
253 }
254
255 fn write_entry(&self, timestamp: Option<Timestamp>, level: Level, source: &str, buf: &[u8]) {
256 #[derive(serde::Serialize)]
257 struct JsonEntry<'a> {
258 timestamp: Timestamp,
259 source: &'a str,
260 severity: &'a str,
261 message: &'a str,
262 }
263 let message = String::from_utf8_lossy(buf);
264 self.write_json(&JsonEntry {
265 timestamp: timestamp.unwrap_or_else(Timestamp::now),
266 source,
267 severity: level.as_str(),
268 message: message.trim_ascii(),
269 });
270 }
271
272 fn write_attachment(&self, path: &Path) {
273 #[derive(serde::Serialize)]
274 struct JsonEntry<'a> {
275 timestamp: Timestamp,
276 attachment: &'a Path,
277 }
278 self.write_json(&JsonEntry {
279 timestamp: Timestamp::now(),
280 attachment: path,
281 });
282 }
283}
284
285#[derive(Debug)]
286struct LogFileInner {
287 file: File,
288 json_log: JsonLog,
289 source: String,
290 echo: bool,
291}
292
293impl LogFileInner {
294 fn write_stdout(&self, buf: &[u8]) {
295 if !self.echo {
296 return;
297 }
298 let mut stdout = std::io::stdout().lock();
299 write!(stdout, "[{:>10}] ", self.source).unwrap();
300 stdout.write_all(buf).unwrap();
301 }
302}
303
304struct LogWriter<'a> {
305 inner: &'a LogFileInner,
306 level: Level,
307 timestamp: Option<Timestamp>,
308}
309
310impl std::io::Write for LogWriter<'_> {
311 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
312 self.inner
314 .json_log
315 .write_entry(self.timestamp, self.level, &self.inner.source, buf);
316 let _ = (&self.inner.file).write_all(buf);
318 self.inner.write_stdout(buf);
320 Ok(buf.len())
321 }
322
323 fn flush(&mut self) -> std::io::Result<()> {
324 Ok(())
325 }
326}
327
328#[derive(Clone, Debug)]
334pub struct PetriLogFile(Arc<LogFileInner>);
335
336impl PetriLogFile {
337 pub fn write_entry_fmt(
339 &self,
340 timestamp: Option<Timestamp>,
341 level: Level,
342 args: std::fmt::Arguments<'_>,
343 ) {
344 let _ = LogWriter {
347 inner: &self.0,
348 level,
349 timestamp,
350 }
351 .write_all(format!("{}\n", args).as_bytes());
352 }
353
354 pub fn write_entry(&self, message: impl std::fmt::Display) {
356 self.write_entry_fmt(None, Level::INFO, format_args!("{}", message));
357 }
358}
359
360#[macro_export]
362macro_rules! log {
363 ($file:expr, $($arg:tt)*) => {
364 <$crate::PetriLogFile>::write_entry_fmt(&$file, format_args!($($arg)*))
365 };
366}
367
368pub fn try_init_tracing(
377 root_path: &Path,
378 default_level: LevelFilter,
379 prefix: Option<String>,
380) -> anyhow::Result<PetriLogSource> {
381 let targets =
382 if let Ok(var) = std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG")) {
383 var.parse().unwrap()
384 } else {
385 Targets::new().with_default(default_level)
386 };
387
388 let logger = PetriLogSource::new(root_path, prefix)?;
390
391 let petri_log = logger.log_file("petri")?;
392
393 tracing_subscriber::fmt()
394 .compact()
395 .with_ansi(false) .log_internal_errors(true)
397 .with_writer(PetriWriter(petri_log))
398 .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
399 .with_max_level(LevelFilter::TRACE)
400 .finish()
401 .with(targets)
402 .try_init()?;
403
404 Ok(logger)
405}
406
407struct PetriWriter(PetriLogFile);
408
409impl<'a> MakeWriter<'a> for PetriWriter {
410 type Writer = LogWriter<'a>;
411
412 fn make_writer(&'a self) -> Self::Writer {
413 LogWriter {
414 inner: &self.0.0,
415 level: Level::INFO,
416 timestamp: None,
417 }
418 }
419
420 fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
421 LogWriter {
422 inner: &self.0.0,
423 level: *meta.level(),
424 timestamp: None,
425 }
426 }
427}
428
429pub async fn log_task(
434 log_file: PetriLogFile,
435 reader: impl AsyncRead + Unpin + Send + 'static,
436 name: &str,
437) -> anyhow::Result<()> {
438 const MAX_LINE_LENGTH: usize = 4096;
439 tracing::info!("connected to {name}");
440 let mut buf = Vec::with_capacity(MAX_LINE_LENGTH);
441 let mut reader = BufReader::new(reader);
442 loop {
443 buf.clear();
444 match (&mut reader)
445 .take(MAX_LINE_LENGTH as u64)
446 .read_until(b'\n', &mut buf)
447 .await
448 {
449 Ok(0) => {
450 tracing::info!("disconnected from {name}: EOF");
451 return Ok(());
452 }
453 Err(e) => {
454 tracing::info!("disconnected from {name}: error: {e:#}");
455 return Err(e.into());
456 }
457 _ => {}
458 }
459
460 let string_buf = String::from_utf8_lossy(&buf);
461 let string_buf_trimmed = string_buf.trim_end();
462
463 if let Some(message) = kmsg::SyslogParsedEntry::new(string_buf_trimmed) {
464 let level = kernel_level_to_tracing_level(message.level);
465 log_file.write_entry_fmt(None, level, format_args!("{}", message.display(false)));
466 } else {
467 log_file.write_entry(string_buf_trimmed);
468 }
469 }
470}
471
472fn kernel_level_to_tracing_level(kernel_level: u8) -> Level {
474 match kernel_level {
475 0..=3 => Level::ERROR,
476 4 => Level::WARN,
477 5..=6 => Level::INFO,
478 7 => Level::DEBUG,
479 _ => Level::INFO,
480 }
481}
482
483pub async fn kmsg_log_task(
485 log_file: PetriLogFile,
486 diag_client: diag_client::DiagClient,
487) -> anyhow::Result<()> {
488 loop {
489 diag_client.wait_for_server().await?;
490 let mut kmsg = diag_client.kmsg(true).await?;
491 tracing::info!("kmsg connected");
492 while let Some(data) = kmsg.next().await {
493 match data {
494 Ok(data) => {
495 let message = KmsgParsedEntry::new(&data).unwrap();
496 let level = kernel_level_to_tracing_level(message.level);
497 log_file.write_entry_fmt(
498 None,
499 level,
500 format_args!("{}", message.display(false)),
501 );
502 }
503 Err(err) => {
504 tracing::info!("kmsg disconnected: {err:#}");
505 break;
506 }
507 }
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_kernel_level_to_tracing_level() {
518 assert_eq!(kernel_level_to_tracing_level(0), Level::ERROR);
520 assert_eq!(kernel_level_to_tracing_level(1), Level::ERROR);
521 assert_eq!(kernel_level_to_tracing_level(2), Level::ERROR);
522 assert_eq!(kernel_level_to_tracing_level(3), Level::ERROR);
523
524 assert_eq!(kernel_level_to_tracing_level(4), Level::WARN);
526
527 assert_eq!(kernel_level_to_tracing_level(5), Level::INFO);
529 assert_eq!(kernel_level_to_tracing_level(6), Level::INFO);
530
531 assert_eq!(kernel_level_to_tracing_level(7), Level::DEBUG);
533
534 assert_eq!(kernel_level_to_tracing_level(8), Level::INFO);
536 assert_eq!(kernel_level_to_tracing_level(255), Level::INFO);
537 }
538}