cargo/util/
logger.rs

1//! Build analysis logging infrastructure.
2
3use std::io::{BufWriter, Write};
4use std::mem::ManuallyDrop;
5use std::path::Path;
6use std::sync::mpsc;
7use std::sync::mpsc::Sender;
8use std::thread::JoinHandle;
9
10use cargo_util::paths;
11
12use crate::CargoResult;
13use crate::core::Workspace;
14use crate::util::log_message::LogMessage;
15use crate::util::short_hash;
16
17/// Logger for `-Zbuild-analysis`.
18pub struct BuildLogger {
19    tx: ManuallyDrop<Sender<LogMessage>>,
20    run_id: String,
21    handle: Option<JoinHandle<()>>,
22}
23
24impl BuildLogger {
25    /// Creates a logger if `-Zbuild-analysis` is enabled.
26    pub fn maybe_new(ws: &Workspace<'_>) -> CargoResult<Option<Self>> {
27        let analysis = ws.gctx().build_config()?.analysis.as_ref();
28        match (analysis, ws.gctx().cli_unstable().build_analysis) {
29            (Some(analysis), true) if analysis.enabled => Ok(Some(Self::new(ws)?)),
30            (Some(_), false) => {
31                ws.gctx().shell().warn(
32                    "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it",
33                )?;
34                Ok(None)
35            }
36            _ => Ok(None),
37        }
38    }
39
40    fn new(ws: &Workspace<'_>) -> CargoResult<Self> {
41        let run_id = Self::generate_run_id(ws)?;
42
43        let log_dir = ws.gctx().home().join("log");
44        paths::create_dir_all(log_dir.as_path_unlocked())?;
45
46        let filename = format!("{run_id}.jsonl");
47        let log_file = log_dir.open_rw_exclusive_create(
48            Path::new(&filename),
49            ws.gctx(),
50            "build analysis log",
51        )?;
52
53        let (tx, rx) = mpsc::channel::<LogMessage>();
54
55        let run_id_clone = run_id.clone();
56        let handle = std::thread::spawn(move || {
57            let mut writer = BufWriter::new(log_file);
58            for msg in rx {
59                let _ = msg.write_json_log(&mut writer, &run_id_clone);
60            }
61            let _ = writer.flush();
62        });
63
64        Ok(Self {
65            tx: ManuallyDrop::new(tx),
66            run_id,
67            handle: Some(handle),
68        })
69    }
70
71    /// Generates a unique run ID.
72    ///
73    /// The format is `{timestamp}-{hash}`, with `:` and `.` in the timestamp
74    /// removed to make it safe for filenames.
75    /// For example, `20251024T194502773638Z-f891d525d52ecab3`.
76    pub fn generate_run_id(ws: &Workspace<'_>) -> CargoResult<String> {
77        let hash = short_hash(&ws.root());
78        let timestamp = jiff::Timestamp::now().to_string().replace([':', '.'], "");
79        Ok(format!("{timestamp}-{hash}"))
80    }
81
82    /// Returns the run ID for this build session.
83    pub fn run_id(&self) -> &str {
84        &self.run_id
85    }
86
87    /// Logs a message.
88    pub fn log(&self, msg: LogMessage) {
89        let _ = self.tx.send(msg);
90    }
91}
92
93impl Drop for BuildLogger {
94    fn drop(&mut self) {
95        // SAFETY: tx is dropped exactly once here to signal thread shutdown.
96        // ManuallyDrop prevents automatic drop after this impl runs.
97        unsafe {
98            ManuallyDrop::drop(&mut self.tx);
99        }
100
101        if let Some(handle) = self.handle.take() {
102            let _ = handle.join();
103        }
104    }
105}