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}
39
40impl PetriLogSource {
41    /// Returns a log file for the given name.
42    ///
43    /// The name should not have an extension; `.log` will be appended
44    /// automatically.
45    pub fn log_file(&self, name: &str) -> anyhow::Result<PetriLogFile> {
46        use std::collections::hash_map::Entry;
47
48        let mut log_files = self.0.log_files.lock();
49        let log_file = match log_files.entry(name.to_owned()) {
50            Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
51            Entry::Vacant(vacant_entry) => {
52                let mut path = self.0.root_path.join(name);
53                // Note that .log is preferred to .txt at least partially
54                // because WSL2 and Defender reportedly conspire to make
55                // cross-OS .txt file accesses extremely slow.
56                path.set_extension("log");
57                let file = File::create(&path)?;
58                // Write the path to the file in junit attachment syntax to
59                // stdout to ensure the file is attached to the test result.
60                println!("[[ATTACHMENT|{}]]", path.display());
61                vacant_entry
62                    .insert(PetriLogFile(Arc::new(LogFileInner {
63                        file,
64                        json_log: self.0.json_log.clone(),
65                        source: name.to_owned(),
66                    })))
67                    .clone()
68            }
69        };
70        Ok(log_file)
71    }
72
73    fn attachment_path(&self, name: &str) -> PathBuf {
74        let mut attachments = self.0.attachments.lock();
75        let next = attachments.entry(name.to_owned()).or_default();
76        let name = Path::new(name);
77        let name = if *next == 0 {
78            name
79        } else {
80            let base = name.file_stem().unwrap().to_str().unwrap();
81            let extension = name.extension().unwrap_or_default();
82            &Path::new(&format!("{}_{}", base, *next)).with_extension(extension)
83        };
84        *next += 1;
85        self.0.root_path.join(name)
86    }
87
88    /// Creates a file with the given name and returns a handle to it.
89    ///
90    /// If the file already exists, a unique name is generated by appending
91    /// a number to the base name.
92    pub fn create_attachment(&self, filename: &str) -> anyhow::Result<File> {
93        let path = self.attachment_path(filename);
94        let file = File::create(&path)?;
95        self.trace_attachment(&path);
96        Ok(file)
97    }
98
99    /// Writes the given data to a file with the given name.
100    ///
101    /// If the file already exists, a unique name is generated by appending
102    /// a number to the base name.
103    pub fn write_attachment(&self, filename: &str, mut data: impl Read) -> anyhow::Result<PathBuf> {
104        let path = self.attachment_path(filename);
105        let mut file = File::create(&path)?;
106        std::io::copy(&mut data, &mut file)?;
107        self.trace_attachment(&path);
108        Ok(path)
109    }
110
111    /// Copies the given file path to a file with the given name.
112    ///
113    /// If the file already exists, a unique name is generated by appending
114    /// a number to the base name.
115    pub fn copy_attachment(
116        &self,
117        attachment_filename: &str,
118        source_path: &Path,
119    ) -> anyhow::Result<()> {
120        let dest_path = self.attachment_path(attachment_filename);
121        fs_err::copy(source_path, &dest_path)?;
122        self.trace_attachment(&dest_path);
123        Ok(())
124    }
125
126    fn trace_attachment(&self, path: &Path) {
127        // Just write the relative path to the JSON log.
128        self.0
129            .json_log
130            .write_attachment(path.file_name().unwrap().as_ref());
131        println!("[[ATTACHMENT|{}]]", path.display());
132    }
133
134    /// Records that a test with the given name is running in this directory.
135    ///
136    /// This is written up front, before the test body runs, so that tooling
137    /// can identify the test even if the process never gets as far as
138    /// [`Self::log_test_result`] -- because it was killed by a timeout, or
139    /// crashed hard enough to skip unwinding. A directory with this file but
140    /// no result marker is a test that died without reporting.
141    pub fn log_test_start(&self, name: &str) {
142        fs_err::write(self.0.root_path.join("petri.test"), name).unwrap();
143    }
144
145    /// Traces and logs the result of a test run in the format expected by our tooling.
146    pub fn log_test_result(&self, r: &anyhow::Result<()>, unstable: bool) {
147        let (result_path, contents) = match &r {
148            Ok(()) => {
149                tracing::info!("test passed");
150                ("petri.passed", String::new())
151            }
152            Err(err) if unstable => {
153                tracing::warn!(
154                    error = err.as_ref() as &dyn std::error::Error,
155                    "unstable test failed"
156                );
157                ("petri.failed_unstable", format!("{err:#}"))
158            }
159            Err(err) => {
160                tracing::error!(
161                    error = err.as_ref() as &dyn std::error::Error,
162                    "test failed"
163                );
164                ("petri.failed", format!("{err:#}"))
165            }
166        };
167        // Write a file to the output directory to indicate whether the test
168        // passed, for easy scanning via tools. For a failure the file holds
169        // the error, so that tooling can report why without parsing the log.
170        fs_err::write(self.0.root_path.join(result_path), contents).unwrap();
171    }
172
173    /// Returns the output directory for log files.
174    pub fn output_dir(&self) -> &Path {
175        &self.0.root_path
176    }
177}
178
179#[derive(Clone, Debug)]
180struct JsonLog(Arc<File>);
181
182impl JsonLog {
183    fn write_json(&self, v: &impl serde::Serialize) {
184        let v = serde_json::to_vec(v);
185        if let Ok(mut v) = v {
186            v.push(b'\n');
187            // Write once to avoid interleaving JSON entries.
188            let _ = self.0.as_ref().write_all(&v);
189        }
190    }
191
192    fn write_entry(&self, timestamp: Option<Timestamp>, level: Level, source: &str, buf: &[u8]) {
193        #[derive(serde::Serialize)]
194        struct JsonEntry<'a> {
195            timestamp: Timestamp,
196            source: &'a str,
197            severity: &'a str,
198            message: &'a str,
199        }
200        let message = String::from_utf8_lossy(buf);
201        self.write_json(&JsonEntry {
202            timestamp: timestamp.unwrap_or_else(Timestamp::now),
203            source,
204            severity: level.as_str(),
205            message: message.trim_ascii(),
206        });
207    }
208
209    fn write_attachment(&self, path: &Path) {
210        #[derive(serde::Serialize)]
211        struct JsonEntry<'a> {
212            timestamp: Timestamp,
213            attachment: &'a Path,
214        }
215        self.write_json(&JsonEntry {
216            timestamp: Timestamp::now(),
217            attachment: path,
218        });
219    }
220}
221
222#[derive(Debug)]
223struct LogFileInner {
224    file: File,
225    json_log: JsonLog,
226    source: String,
227}
228
229impl LogFileInner {
230    fn write_stdout(&self, buf: &[u8]) {
231        let mut stdout = std::io::stdout().lock();
232        write!(stdout, "[{:>10}] ", self.source).unwrap();
233        stdout.write_all(buf).unwrap();
234    }
235}
236
237struct LogWriter<'a> {
238    inner: &'a LogFileInner,
239    level: Level,
240    timestamp: Option<Timestamp>,
241}
242
243impl std::io::Write for LogWriter<'_> {
244    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
245        // Write to the JSONL file.
246        self.inner
247            .json_log
248            .write_entry(self.timestamp, self.level, &self.inner.source, buf);
249        // Write to the specific log file.
250        let _ = (&self.inner.file).write_all(buf);
251        // Write to stdout, prefixed with the source.
252        self.inner.write_stdout(buf);
253        Ok(buf.len())
254    }
255
256    fn flush(&mut self) -> std::io::Result<()> {
257        Ok(())
258    }
259}
260
261/// A log file for writing test output.
262///
263/// Generally, you should use [`tracing`] for test-generated logging. This type
264/// is for writing fully-formed text entries that come from an external source,
265/// such as another process or a guest serial port.
266#[derive(Clone, Debug)]
267pub struct PetriLogFile(Arc<LogFileInner>);
268
269impl PetriLogFile {
270    /// Write a log entry with the given format arguments.
271    pub fn write_entry_fmt(
272        &self,
273        timestamp: Option<Timestamp>,
274        level: Level,
275        args: std::fmt::Arguments<'_>,
276    ) {
277        // Convert to a single string to write to the file to ensure the entry
278        // does not get interleaved with other log entries.
279        let _ = LogWriter {
280            inner: &self.0,
281            level,
282            timestamp,
283        }
284        .write_all(format!("{}\n", args).as_bytes());
285    }
286
287    /// Write a log entry with the given message.
288    pub fn write_entry(&self, message: impl std::fmt::Display) {
289        self.write_entry_fmt(None, Level::INFO, format_args!("{}", message));
290    }
291}
292
293/// Write a formatted log entry to the given [`PetriLogFile`].
294#[macro_export]
295macro_rules! log {
296    ($file:expr, $($arg:tt)*) => {
297        <$crate::PetriLogFile>::write_entry_fmt(&$file, format_args!($($arg)*))
298    };
299}
300
301/// Initialize Petri tracing with the given output path for log files.
302///
303/// Events go to three places:
304/// - `petri.jsonl`, in newline-separated JSON format.
305/// - standard output, in human readable format.
306/// - a log file, in human readable format. This file is `petri.log`, except
307///   for events whose target ends in `.log`, which go to separate files named by
308///   the target.
309pub fn try_init_tracing(
310    root_path: &Path,
311    default_level: LevelFilter,
312) -> anyhow::Result<PetriLogSource> {
313    let targets =
314        if let Ok(var) = std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG")) {
315            var.parse().unwrap()
316        } else {
317            Targets::new().with_default(default_level)
318        };
319
320    // Canonicalize so that printed attachment paths are most likely to work.
321    let root_path = root_path.fs_err_canonicalize()?;
322    let jsonl = File::create(root_path.join("petri.jsonl"))?;
323    let logger = PetriLogSource(Arc::new(LogSourceInner {
324        json_log: JsonLog(Arc::new(jsonl)),
325        root_path,
326        log_files: Default::default(),
327        attachments: Default::default(),
328    }));
329
330    let petri_log = logger.log_file("petri")?;
331
332    tracing_subscriber::fmt()
333        .compact()
334        .with_ansi(false) // avoid polluting logs with escape sequences
335        .log_internal_errors(true)
336        .with_writer(PetriWriter(petri_log))
337        .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
338        .with_max_level(LevelFilter::TRACE)
339        .finish()
340        .with(targets)
341        .try_init()?;
342
343    Ok(logger)
344}
345
346struct PetriWriter(PetriLogFile);
347
348impl<'a> MakeWriter<'a> for PetriWriter {
349    type Writer = LogWriter<'a>;
350
351    fn make_writer(&'a self) -> Self::Writer {
352        LogWriter {
353            inner: &self.0.0,
354            level: Level::INFO,
355            timestamp: None,
356        }
357    }
358
359    fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
360        LogWriter {
361            inner: &self.0.0,
362            level: *meta.level(),
363            timestamp: None,
364        }
365    }
366}
367
368/// Logs lines from `reader` into `log_file`.
369///
370/// Attempts to parse lines as `SyslogParsedEntry`, extracting the log level.
371/// Passes through any non-conforming logs.
372pub async fn log_task(
373    log_file: PetriLogFile,
374    reader: impl AsyncRead + Unpin + Send + 'static,
375    name: &str,
376) -> anyhow::Result<()> {
377    tracing::info!("connected to {name}");
378    let mut buf = Vec::new();
379    let mut reader = BufReader::new(reader);
380    loop {
381        buf.clear();
382        match (&mut reader).take(256).read_until(b'\n', &mut buf).await {
383            Ok(0) => {
384                tracing::info!("disconnected from {name}: EOF");
385                return Ok(());
386            }
387            Err(e) => {
388                tracing::info!("disconnected from {name}: error: {e:#}");
389                return Err(e.into());
390            }
391            _ => {}
392        }
393
394        let string_buf = String::from_utf8_lossy(&buf);
395        let string_buf_trimmed = string_buf.trim_end();
396
397        if let Some(message) = kmsg::SyslogParsedEntry::new(string_buf_trimmed) {
398            let level = kernel_level_to_tracing_level(message.level);
399            log_file.write_entry_fmt(None, level, format_args!("{}", message.display(false)));
400        } else {
401            log_file.write_entry(string_buf_trimmed);
402        }
403    }
404}
405
406/// Maps kernel log levels to tracing levels.
407fn kernel_level_to_tracing_level(kernel_level: u8) -> Level {
408    match kernel_level {
409        0..=3 => Level::ERROR,
410        4 => Level::WARN,
411        5..=6 => Level::INFO,
412        7 => Level::DEBUG,
413        _ => Level::INFO,
414    }
415}
416
417/// read from the kmsg stream and write entries to the log
418pub async fn kmsg_log_task(
419    log_file: PetriLogFile,
420    diag_client: diag_client::DiagClient,
421) -> anyhow::Result<()> {
422    loop {
423        diag_client.wait_for_server().await?;
424        let mut kmsg = diag_client.kmsg(true).await?;
425        tracing::info!("kmsg connected");
426        while let Some(data) = kmsg.next().await {
427            match data {
428                Ok(data) => {
429                    let message = KmsgParsedEntry::new(&data).unwrap();
430                    let level = kernel_level_to_tracing_level(message.level);
431                    log_file.write_entry_fmt(
432                        None,
433                        level,
434                        format_args!("{}", message.display(false)),
435                    );
436                }
437                Err(err) => {
438                    tracing::info!("kmsg disconnected: {err:#}");
439                    break;
440                }
441            }
442        }
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_kernel_level_to_tracing_level() {
452        // Test emergency to error levels (0-3)
453        assert_eq!(kernel_level_to_tracing_level(0), Level::ERROR);
454        assert_eq!(kernel_level_to_tracing_level(1), Level::ERROR);
455        assert_eq!(kernel_level_to_tracing_level(2), Level::ERROR);
456        assert_eq!(kernel_level_to_tracing_level(3), Level::ERROR);
457
458        // Test warning level (4)
459        assert_eq!(kernel_level_to_tracing_level(4), Level::WARN);
460
461        // Test notice and info levels (5-6)
462        assert_eq!(kernel_level_to_tracing_level(5), Level::INFO);
463        assert_eq!(kernel_level_to_tracing_level(6), Level::INFO);
464
465        // Test debug level (7)
466        assert_eq!(kernel_level_to_tracing_level(7), Level::DEBUG);
467
468        // Test unknown level (fallback)
469        assert_eq!(kernel_level_to_tracing_level(8), Level::INFO);
470        assert_eq!(kernel_level_to_tracing_level(255), Level::INFO);
471    }
472}