Skip to main content

cargo/util/
logger.rs

1//! Build analysis logging infrastructure.
2
3use std::cell::RefCell;
4use std::hash::Hash;
5use std::io::{BufWriter, Write};
6use std::mem::ManuallyDrop;
7use std::path::Path;
8use std::sync::mpsc::{self, Sender};
9use std::thread::JoinHandle;
10
11use anyhow::Context as _;
12use cargo_util::paths;
13
14use crate::CargoResult;
15use crate::core::Workspace;
16use crate::core::compiler::BuildConfig;
17use crate::util::log_message::LogMessage;
18use crate::util::short_hash;
19
20// for newer `cargo report` commands
21struct FileLogger {
22    tx: ManuallyDrop<Sender<LogMessage>>,
23    handle: Option<JoinHandle<()>>,
24}
25
26impl FileLogger {
27    /// Creates a logger if `-Zbuild-analysis` is enabled
28    fn maybe_new(ws: &Workspace<'_>, run_id: &RunId) -> CargoResult<Option<FileLogger>> {
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 => {
32                let log_dir = ws.gctx().home().join("log");
33                paths::create_dir_all(log_dir.as_path_unlocked())?;
34
35                let filename = format!("{run_id}.jsonl");
36                let log_file = log_dir.open_rw_exclusive_create(
37                    Path::new(&filename),
38                    ws.gctx(),
39                    "build analysis log",
40                )?;
41
42                let (tx, rx) = mpsc::channel::<LogMessage>();
43
44                let run_id_str = run_id.to_string();
45                let handle = std::thread::spawn(move || {
46                    let mut writer = BufWriter::new(log_file);
47                    for msg in rx {
48                        let _ = msg.write_json_log(&mut writer, &run_id_str);
49                    }
50                    let _ = writer.flush();
51                });
52
53                Ok(Some(Self {
54                    tx: ManuallyDrop::new(tx),
55                    handle: Some(handle),
56                }))
57            }
58            (Some(_), false) => {
59                ws.gctx().shell().warn(
60                    "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it",
61                )?;
62                Ok(None)
63            }
64            _ => Ok(None),
65        }
66    }
67}
68
69impl Drop for FileLogger {
70    fn drop(&mut self) {
71        // SAFETY: tx is dropped exactly once here to signal thread shutdown.
72        // ManuallyDrop prevents automatic drop after this impl runs.
73        unsafe {
74            ManuallyDrop::drop(&mut self.tx);
75        }
76
77        if let Some(handle) = self.handle.take() {
78            let _ = handle.join();
79        }
80    }
81}
82
83/// For legacy `cargo build --timings` flag
84struct InMemoryLogger {
85    // using mutex to hide mutability
86    logs: RefCell<Vec<LogMessage>>,
87}
88
89impl InMemoryLogger {
90    fn maybe_new(options: &BuildConfig) -> Option<Self> {
91        if options.timing_report {
92            Some(Self {
93                logs: RefCell::new(Vec::new()),
94            })
95        } else {
96            None
97        }
98    }
99}
100
101/// Logger for `-Zbuild-analysis`.
102pub struct BuildLogger {
103    run_id: RunId,
104    file_logger: Option<FileLogger>,
105    in_memory_logger: Option<InMemoryLogger>,
106}
107
108impl BuildLogger {
109    pub fn maybe_new(ws: &Workspace<'_>, options: &BuildConfig) -> CargoResult<Option<Self>> {
110        let run_id = Self::generate_run_id(ws);
111        let file_logger = FileLogger::maybe_new(ws, &run_id)?;
112        let in_memory_logger = InMemoryLogger::maybe_new(options);
113
114        if file_logger.is_none() && in_memory_logger.is_none() {
115            return Ok(None);
116        }
117
118        Ok(Some(Self {
119            run_id,
120            file_logger,
121            in_memory_logger,
122        }))
123    }
124
125    /// Generates a unique run ID.
126    pub fn generate_run_id(ws: &Workspace<'_>) -> RunId {
127        RunId::new(&ws.root())
128    }
129
130    /// Returns the run ID for this build session.
131    pub fn run_id(&self) -> &RunId {
132        &self.run_id
133    }
134
135    /// Logs a message.
136    pub fn log(&self, msg: LogMessage) {
137        if let Some(ref logger) = self.in_memory_logger {
138            let mut borrowed = logger.logs.try_borrow_mut().expect(
139                "Unable to get a mutable reference to in-memory logger; please file a bug report",
140            );
141            borrowed.push(msg.clone());
142        };
143
144        if let Some(ref logger) = self.file_logger {
145            let _ = logger.tx.send(msg);
146        };
147    }
148
149    pub fn get_logs(&self) -> Option<Vec<LogMessage>> {
150        self.in_memory_logger.as_ref().map(|l| {
151            l.logs
152                .try_borrow()
153                .expect("Unable to get a reference to in-memory logger; please file a bug report")
154                .clone()
155        })
156    }
157}
158
159/// A unique identifier for a Cargo invocation.
160#[derive(Clone)]
161pub struct RunId {
162    timestamp: jiff::Timestamp,
163    hash: String,
164}
165
166impl RunId {
167    const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ";
168
169    pub fn new<H: Hash>(h: &H) -> RunId {
170        RunId {
171            timestamp: jiff::Timestamp::now(),
172            hash: short_hash(h),
173        }
174    }
175
176    pub fn timestamp(&self) -> &jiff::Timestamp {
177        &self.timestamp
178    }
179
180    /// Checks whether ID was generated from the same workspace.
181    pub fn same_workspace(&self, other: &RunId) -> bool {
182        self.hash == other.hash
183    }
184}
185
186impl std::fmt::Display for RunId {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        let hash = &self.hash;
189        let timestamp = self.timestamp.strftime(Self::FORMAT);
190        write!(f, "{timestamp}-{hash}")
191    }
192}
193
194impl std::str::FromStr for RunId {
195    type Err = anyhow::Error;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        let msg =
199            || format!("expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `{s}`");
200        let Some((timestamp, hash)) = s.rsplit_once('-') else {
201            anyhow::bail!(msg());
202        };
203
204        if hash.len() != 16 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
205            anyhow::bail!(msg());
206        }
207        let timestamp = jiff::civil::DateTime::strptime(Self::FORMAT, timestamp)
208            .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC))
209            .map(|zoned| zoned.timestamp())
210            .with_context(msg)?;
211
212        Ok(RunId {
213            timestamp,
214            hash: hash.into(),
215        })
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn run_id_round_trip() {
225        let id = "20060724T012128000Z-b0fd440798ab3cfb";
226        assert_eq!(id, &id.parse::<RunId>().unwrap().to_string());
227    }
228}