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`. `target` names the subsystem
147    /// (e.g. `cubecl::autotune`) — the `log`-crate sink emits it as the
148    /// record's target so consumers can tell subsystems apart; the file and
149    /// stdio sinks are already per-subsystem by configuration and ignore it.
150    pub fn log<S: Display>(&mut self, indices: &[usize], target: &str, msg: &S) {
151        match indices.len() {
152            0 => {}
153            1 => self.loggers[indices[0]].log(target, msg),
154            _ => {
155                let msg = msg.to_string();
156                for &index in indices {
157                    self.loggers[index].log(target, &msg);
158                }
159            }
160        }
161    }
162
163    fn insert<F: FnOnce() -> LoggerKind>(
164        &mut self,
165        indices: &mut Vec<usize>,
166        id: LoggerId,
167        make: F,
168    ) {
169        if let Some(index) = self.logger2index.get(&id) {
170            indices.push(*index);
171        } else {
172            let index = self.loggers.len();
173            self.loggers.push(make());
174            self.logger2index.insert(id, index);
175            indices.push(index);
176        }
177    }
178}
179
180#[derive(Debug, Hash, PartialEq, Eq)]
181enum LoggerId {
182    #[cfg(std_io)]
183    File(PathBuf),
184    #[cfg(feature = "std")]
185    Stdout,
186    #[cfg(feature = "std")]
187    Stderr,
188    LogCrate(LogCrateLevel),
189}
190
191#[derive(Debug)]
192enum LoggerKind {
193    #[cfg(std_io)]
194    File(FileLogger),
195    #[cfg(feature = "std")]
196    Stdout,
197    #[cfg(feature = "std")]
198    Stderr,
199    Log(LogCrateLevel),
200}
201
202impl LoggerKind {
203    fn log<S: Display>(&mut self, target: &str, msg: &S) {
204        match self {
205            #[cfg(std_io)]
206            LoggerKind::File(file_logger) => file_logger.log(msg),
207            #[cfg(feature = "std")]
208            LoggerKind::Stdout => println!("{msg}"),
209            #[cfg(feature = "std")]
210            LoggerKind::Stderr => eprintln!("{msg}"),
211            LoggerKind::Log(level) => {
212                let level = match level {
213                    LogCrateLevel::Info => log::Level::Info,
214                    LogCrateLevel::Debug => log::Level::Debug,
215                    LogCrateLevel::Trace => log::Level::Trace,
216                };
217                log::log!(target: target, level, "{msg}");
218            }
219        }
220    }
221}
222
223#[cfg(std_io)]
224#[derive(Debug)]
225struct FileLogger {
226    writer: BufWriter<File>,
227}
228
229#[cfg(std_io)]
230impl FileLogger {
231    fn new(path: &PathBuf, append: bool) -> Self {
232        if let Some(parent) = path.parent() {
233            std::fs::create_dir_all(parent).unwrap();
234        }
235        let file = OpenOptions::new()
236            .write(true)
237            .append(append)
238            .create(true)
239            .open(path)
240            .unwrap();
241
242        Self {
243            writer: BufWriter::new(file),
244        }
245    }
246
247    fn log<S: Display>(&mut self, msg: &S) {
248        writeln!(self.writer, "{msg}").expect("Should be able to log debug information.");
249        self.writer.flush().expect("Can complete write operation.");
250    }
251}