Skip to main content

petri/
tracing.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use 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/// A source of [`PetriLogFile`] log files for test output.
29#[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        // Canonicalize so that printed attachment paths are most likely to work.
45        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    /// Create file-only logs without installing a tracing subscriber or writing
61    /// attachment markers to stdout. Suitable for standalone command runners.
62    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    /// Returns a log file for the given name.
76    ///
77    /// The name should not have an extension; `.log` will be appended
78    /// automatically.
79    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                // Note that .log is preferred to .txt at least partially
94                // because WSL2 and Defender reportedly conspire to make
95                // cross-OS .txt file accesses extremely slow.
96                path.set_extension("log");
97                let file = File::create(&path)?;
98                // Write the path to the file in junit attachment syntax to
99                // stdout to ensure the file is attached to the test result.
100                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    /// Creates a file with the given name and returns a handle to it.
145    ///
146    /// If the file already exists, a unique name is generated by appending
147    /// a number to the base name.
148    pub fn create_attachment(&self, filename: &str) -> anyhow::Result<File> {
149        self.create_attachment_no_prefix(&self.filename(filename))
150    }
151
152    /// Creates a file with the given name and returns a handle to it.
153    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    /// Writes the given data to a file with the given name.
161    ///
162    /// If the file already exists, a unique name is generated by appending
163    /// a number to the base name.
164    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    /// Copies the given file path to a file with the given name.
173    ///
174    /// If the file already exists, a unique name is generated by appending
175    /// a number to the base name.
176    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        // Just write the relative path to the JSON log.
189        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    /// Records that a test with the given name is running in this directory.
198    ///
199    /// This is written up front, before the test body runs, so that tooling
200    /// can identify the test even if the process never gets as far as
201    /// [`Self::log_test_result`] -- because it was killed by a timeout, or
202    /// crashed hard enough to skip unwinding. A directory with this file but
203    /// no result marker is a test that died without reporting.
204    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    /// Traces and logs the result of a test run in the format expected by our tooling.
209    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        // Write a file to the output directory to indicate whether the test
231        // passed, for easy scanning via tools. For a failure the file holds
232        // the error, so that tooling can report why without parsing the log.
233        fs_err::write(self.0.root_path.join(self.filename(result_path)), contents).unwrap();
234    }
235
236    /// Returns the output directory for log files.
237    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            // Write once to avoid interleaving JSON entries.
251            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        // Write to the JSONL file.
313        self.inner
314            .json_log
315            .write_entry(self.timestamp, self.level, &self.inner.source, buf);
316        // Write to the specific log file.
317        let _ = (&self.inner.file).write_all(buf);
318        // Write to stdout, prefixed with the source.
319        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/// A log file for writing test output.
329///
330/// Generally, you should use [`tracing`] for test-generated logging. This type
331/// is for writing fully-formed text entries that come from an external source,
332/// such as another process or a guest serial port.
333#[derive(Clone, Debug)]
334pub struct PetriLogFile(Arc<LogFileInner>);
335
336impl PetriLogFile {
337    /// Write a log entry with the given format arguments.
338    pub fn write_entry_fmt(
339        &self,
340        timestamp: Option<Timestamp>,
341        level: Level,
342        args: std::fmt::Arguments<'_>,
343    ) {
344        // Convert to a single string to write to the file to ensure the entry
345        // does not get interleaved with other log entries.
346        let _ = LogWriter {
347            inner: &self.0,
348            level,
349            timestamp,
350        }
351        .write_all(format!("{}\n", args).as_bytes());
352    }
353
354    /// Write a log entry with the given message.
355    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/// Write a formatted log entry to the given [`PetriLogFile`].
361#[macro_export]
362macro_rules! log {
363    ($file:expr, $($arg:tt)*) => {
364        <$crate::PetriLogFile>::write_entry_fmt(&$file, format_args!($($arg)*))
365    };
366}
367
368/// Initialize Petri tracing with the given output path for log files.
369///
370/// Events go to three places:
371/// - `petri.jsonl`, in newline-separated JSON format.
372/// - standard output, in human readable format.
373/// - a log file, in human readable format. This file is `petri.log`, except
374///   for events whose target ends in `.log`, which go to separate files named by
375///   the target.
376pub 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    // Canonicalize so that printed attachment paths are most likely to work.
389    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) // avoid polluting logs with escape sequences
396        .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
429/// Logs lines from `reader` into `log_file`.
430///
431/// Attempts to parse lines as `SyslogParsedEntry`, extracting the log level.
432/// Passes through any non-conforming logs.
433pub 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
472/// Maps kernel log levels to tracing levels.
473fn 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
483/// read from the kmsg stream and write entries to the log
484pub 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        // Test emergency to error levels (0-3)
519        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        // Test warning level (4)
525        assert_eq!(kernel_level_to_tracing_level(4), Level::WARN);
526
527        // Test notice and info levels (5-6)
528        assert_eq!(kernel_level_to_tracing_level(5), Level::INFO);
529        assert_eq!(kernel_level_to_tracing_level(6), Level::INFO);
530
531        // Test debug level (7)
532        assert_eq!(kernel_level_to_tracing_level(7), Level::DEBUG);
533
534        // Test unknown level (fallback)
535        assert_eq!(kernel_level_to_tracing_level(8), Level::INFO);
536        assert_eq!(kernel_level_to_tracing_level(255), Level::INFO);
537    }
538}