Skip to main content

cubecl_common/config/
logger.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::fmt::Display;
4use hashbrown::HashMap;
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8#[cfg(std_io)]
9use std::{
10    fs::{File, OpenOptions},
11    io::{BufWriter, Write},
12    path::PathBuf,
13};
14
15#[cfg(feature = "std")]
16use std::{eprintln, println};
17
18/// Configuration for a log sink, parameterized by a subsystem-specific log level.
19///
20/// Multiple sinks can be enabled at the same time (e.g. both `stdout` and a file).
21#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
22#[serde(bound = "")]
23pub struct LoggerConfig<L: LogLevel> {
24    /// Path to the log file, if file logging is enabled (requires filesystem access).
25    #[serde(default)]
26    #[cfg(std_io)]
27    pub file: Option<PathBuf>,
28
29    /// Whether to append to the log file (true) or overwrite it (false). Defaults to true.
30    ///
31    /// ## Notes
32    ///
33    /// This parameter might get ignored based on other loggers config.
34    #[serde(default = "append_default")]
35    pub append: bool,
36
37    /// Whether to log to standard output.
38    #[serde(default)]
39    pub stdout: bool,
40
41    /// Whether to log to standard error.
42    #[serde(default)]
43    pub stderr: bool,
44
45    /// Optional crate-level logging configuration (e.g., info, debug, trace).
46    #[serde(default)]
47    pub log: Option<LogCrateLevel>,
48
49    /// The verbosity level for this subsystem.
50    #[serde(default)]
51    pub level: L,
52}
53
54impl<L: LogLevel> Default for LoggerConfig<L> {
55    fn default() -> Self {
56        Self {
57            #[cfg(std_io)]
58            file: None,
59            append: true,
60            stdout: false,
61            stderr: false,
62            log: None,
63            level: L::default(),
64        }
65    }
66}
67
68/// Log levels forwarded to the `log` crate.
69#[derive(
70    Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq,
71)]
72pub enum LogCrateLevel {
73    /// Informational messages.
74    #[default]
75    #[serde(rename = "info")]
76    Info,
77
78    /// Debugging messages.
79    #[serde(rename = "debug")]
80    Debug,
81
82    /// Trace-level messages.
83    #[serde(rename = "trace")]
84    Trace,
85}
86
87fn append_default() -> bool {
88    true
89}
90
91/// Trait for types usable as a subsystem-specific log level.
92pub trait LogLevel:
93    DeserializeOwned + Serialize + Clone + Copy + core::fmt::Debug + Default
94{
95}
96
97impl LogLevel for u32 {}
98
99/// Registry of log sinks, deduplicating by output target so that multiple subsystems can share
100/// a single file or stdout/stderr stream.
101#[derive(Debug, Default)]
102pub struct LoggerSinks {
103    loggers: Vec<LoggerKind>,
104    logger2index: HashMap<LoggerId, usize>,
105}
106
107impl LoggerSinks {
108    /// Creates an empty registry.
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Registers every sink described by `config` and returns their indices.
114    ///
115    /// Subsequent calls with a sink already registered (same file path, stdout, stderr or
116    /// log-crate level) reuse the existing index instead of creating a new sink.
117    pub fn register<L: LogLevel>(&mut self, config: &LoggerConfig<L>) -> Vec<usize> {
118        let mut indices = Vec::new();
119
120        #[cfg(std_io)]
121        if let Some(file) = &config.file {
122            self.insert(&mut indices, LoggerId::File(file.clone()), || {
123                LoggerKind::File(FileLogger::new(file, config.append))
124            });
125        }
126
127        #[cfg(feature = "std")]
128        if config.stdout {
129            self.insert(&mut indices, LoggerId::Stdout, || LoggerKind::Stdout);
130        }
131
132        #[cfg(feature = "std")]
133        if config.stderr {
134            self.insert(&mut indices, LoggerId::Stderr, || LoggerKind::Stderr);
135        }
136
137        if let Some(level) = config.log {
138            self.insert(&mut indices, LoggerId::LogCrate(level), || {
139                LoggerKind::Log(level)
140            });
141        }
142
143        indices
144    }
145
146    /// Writes `msg` to every sink in `indices`.
147    pub fn log<S: Display>(&mut self, indices: &[usize], msg: &S) {
148        match indices.len() {
149            0 => {}
150            1 => self.loggers[indices[0]].log(msg),
151            _ => {
152                let msg = msg.to_string();
153                for &index in indices {
154                    self.loggers[index].log(&msg);
155                }
156            }
157        }
158    }
159
160    fn insert<F: FnOnce() -> LoggerKind>(
161        &mut self,
162        indices: &mut Vec<usize>,
163        id: LoggerId,
164        make: F,
165    ) {
166        if let Some(index) = self.logger2index.get(&id) {
167            indices.push(*index);
168        } else {
169            let index = self.loggers.len();
170            self.loggers.push(make());
171            self.logger2index.insert(id, index);
172            indices.push(index);
173        }
174    }
175}
176
177#[derive(Debug, Hash, PartialEq, Eq)]
178enum LoggerId {
179    #[cfg(std_io)]
180    File(PathBuf),
181    #[cfg(feature = "std")]
182    Stdout,
183    #[cfg(feature = "std")]
184    Stderr,
185    LogCrate(LogCrateLevel),
186}
187
188#[derive(Debug)]
189enum LoggerKind {
190    #[cfg(std_io)]
191    File(FileLogger),
192    #[cfg(feature = "std")]
193    Stdout,
194    #[cfg(feature = "std")]
195    Stderr,
196    Log(LogCrateLevel),
197}
198
199impl LoggerKind {
200    fn log<S: Display>(&mut self, msg: &S) {
201        match self {
202            #[cfg(std_io)]
203            LoggerKind::File(file_logger) => file_logger.log(msg),
204            #[cfg(feature = "std")]
205            LoggerKind::Stdout => println!("{msg}"),
206            #[cfg(feature = "std")]
207            LoggerKind::Stderr => eprintln!("{msg}"),
208            LoggerKind::Log(level) => match level {
209                LogCrateLevel::Info => log::info!("{msg}"),
210                LogCrateLevel::Debug => log::debug!("{msg}"),
211                LogCrateLevel::Trace => log::trace!("{msg}"),
212            },
213        }
214    }
215}
216
217#[cfg(std_io)]
218#[derive(Debug)]
219struct FileLogger {
220    writer: BufWriter<File>,
221}
222
223#[cfg(std_io)]
224impl FileLogger {
225    fn new(path: &PathBuf, append: bool) -> Self {
226        if let Some(parent) = path.parent() {
227            std::fs::create_dir_all(parent).unwrap();
228        }
229        let file = OpenOptions::new()
230            .write(true)
231            .append(append)
232            .create(true)
233            .open(path)
234            .unwrap();
235
236        Self {
237            writer: BufWriter::new(file),
238        }
239    }
240
241    fn log<S: Display>(&mut self, msg: &S) {
242        writeln!(self.writer, "{msg}").expect("Should be able to log debug information.");
243        self.writer.flush().expect("Can complete write operation.");
244    }
245}