cargo/util/
logger.rs

1//! Build analysis logging infrastructure.
2
3use std::hash::Hash;
4use std::io::{BufWriter, Write};
5use std::mem::ManuallyDrop;
6use std::path::Path;
7use std::sync::mpsc;
8use std::sync::mpsc::Sender;
9use std::thread::JoinHandle;
10
11use anyhow::Context as _;
12use cargo_util::paths;
13
14use crate::CargoResult;
15use crate::core::Workspace;
16use crate::util::log_message::LogMessage;
17use crate::util::short_hash;
18
19/// Logger for `-Zbuild-analysis`.
20pub struct BuildLogger {
21    tx: ManuallyDrop<Sender<LogMessage>>,
22    run_id: RunId,
23    handle: Option<JoinHandle<()>>,
24}
25
26impl BuildLogger {
27    /// Creates a logger if `-Zbuild-analysis` is enabled.
28    pub fn maybe_new(ws: &Workspace<'_>) -> CargoResult<Option<Self>> {
29        let analysis = ws.gctx().build_config()?.analysis.as_ref();
30        match (analysis, ws.gctx().cli_unstable().build_analysis) {
31            (Some(analysis), true) if analysis.enabled => Ok(Some(Self::new(ws)?)),
32            (Some(_), false) => {
33                ws.gctx().shell().warn(
34                    "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it",
35                )?;
36                Ok(None)
37            }
38            _ => Ok(None),
39        }
40    }
41
42    fn new(ws: &Workspace<'_>) -> CargoResult<Self> {
43        let run_id = Self::generate_run_id(ws);
44
45        let log_dir = ws.gctx().home().join("log");
46        paths::create_dir_all(log_dir.as_path_unlocked())?;
47
48        let filename = format!("{run_id}.jsonl");
49        let log_file = log_dir.open_rw_exclusive_create(
50            Path::new(&filename),
51            ws.gctx(),
52            "build analysis log",
53        )?;
54
55        let (tx, rx) = mpsc::channel::<LogMessage>();
56
57        let run_id_str = run_id.to_string();
58        let handle = std::thread::spawn(move || {
59            let mut writer = BufWriter::new(log_file);
60            for msg in rx {
61                let _ = msg.write_json_log(&mut writer, &run_id_str);
62            }
63            let _ = writer.flush();
64        });
65
66        Ok(Self {
67            tx: ManuallyDrop::new(tx),
68            run_id,
69            handle: Some(handle),
70        })
71    }
72
73    /// Generates a unique run ID.
74    pub fn generate_run_id(ws: &Workspace<'_>) -> RunId {
75        RunId::new(&ws.root())
76    }
77
78    /// Returns the run ID for this build session.
79    pub fn run_id(&self) -> &RunId {
80        &self.run_id
81    }
82
83    /// Logs a message.
84    pub fn log(&self, msg: LogMessage) {
85        let _ = self.tx.send(msg);
86    }
87}
88
89impl Drop for BuildLogger {
90    fn drop(&mut self) {
91        // SAFETY: tx is dropped exactly once here to signal thread shutdown.
92        // ManuallyDrop prevents automatic drop after this impl runs.
93        unsafe {
94            ManuallyDrop::drop(&mut self.tx);
95        }
96
97        if let Some(handle) = self.handle.take() {
98            let _ = handle.join();
99        }
100    }
101}
102
103/// A unique identifier for a Cargo invocation.
104#[derive(Clone)]
105pub struct RunId {
106    timestamp: jiff::Timestamp,
107    hash: String,
108}
109
110impl RunId {
111    const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ";
112
113    pub fn new<H: Hash>(h: &H) -> RunId {
114        RunId {
115            timestamp: jiff::Timestamp::now(),
116            hash: short_hash(h),
117        }
118    }
119
120    pub fn timestamp(&self) -> &jiff::Timestamp {
121        &self.timestamp
122    }
123
124    /// Checks whether ID was generated from the same workspace.
125    pub fn same_workspace(&self, other: &RunId) -> bool {
126        self.hash == other.hash
127    }
128}
129
130impl std::fmt::Display for RunId {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        let hash = &self.hash;
133        let timestamp = self.timestamp.strftime(Self::FORMAT);
134        write!(f, "{timestamp}-{hash}")
135    }
136}
137
138impl std::str::FromStr for RunId {
139    type Err = anyhow::Error;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        let msg =
143            || format!("expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `{s}`");
144        let Some((timestamp, hash)) = s.rsplit_once('-') else {
145            anyhow::bail!(msg());
146        };
147
148        if hash.len() != 16 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
149            anyhow::bail!(msg());
150        }
151        let timestamp = jiff::civil::DateTime::strptime(Self::FORMAT, timestamp)
152            .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC))
153            .map(|zoned| zoned.timestamp())
154            .with_context(msg)?;
155
156        Ok(RunId {
157            timestamp,
158            hash: hash.into(),
159        })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn run_id_round_trip() {
169        let id = "20060724T012128000Z-b0fd440798ab3cfb";
170        assert_eq!(id, &id.parse::<RunId>().unwrap().to_string());
171    }
172}