cubecl_runtime/config/
logger.rs

1use super::GlobalConfig;
2use crate::config::{
3    autotune::AutotuneLogLevel, compilation::CompilationLogLevel, profiling::ProfilingLogLevel,
4};
5use alloc::{string::ToString, sync::Arc, vec::Vec};
6use core::fmt::Display;
7use hashbrown::HashMap;
8
9#[cfg(std_io)]
10use std::{
11    fs::{File, OpenOptions},
12    io::{BufWriter, Write},
13    path::PathBuf,
14};
15
16/// Configuration for logging in CubeCL, parameterized by a log level type.
17///
18/// Note that you can use multiple loggers at the same time.
19#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
20#[serde(bound = "")]
21pub struct LoggerConfig<L: LogLevel> {
22    /// Path to the log file, if file logging is enabled (requires `std` feature).
23    #[serde(default)]
24    #[cfg(std_io)]
25    pub file: Option<PathBuf>,
26
27    /// Whether to append to the log file (true) or overwrite it (false). Defaults to true.
28    ///
29    /// ## Notes
30    ///
31    /// This parameter might get ignored based on other loggers config.
32    #[serde(default = "append_default")]
33    pub append: bool,
34
35    /// Whether to log to standard output.
36    #[serde(default)]
37    pub stdout: bool,
38
39    /// Whether to log to standard error.
40    #[serde(default)]
41    pub stderr: bool,
42
43    /// Optional crate-level logging configuration (e.g., info, debug, trace).
44    #[serde(default)]
45    pub log: Option<LogCrateLevel>,
46
47    /// The log level for this logger, determining verbosity.
48    #[serde(default)]
49    pub level: L,
50}
51
52impl<L: LogLevel> Default for LoggerConfig<L> {
53    fn default() -> Self {
54        Self {
55            #[cfg(std_io)]
56            file: None,
57            append: true,
58            #[cfg(feature = "autotune-checks")]
59            stdout: true,
60            #[cfg(not(feature = "autotune-checks"))]
61            stdout: false,
62            stderr: false,
63            log: None,
64            level: L::default(),
65        }
66    }
67}
68
69/// Log levels using the `log` crate.
70///
71/// This enum defines verbosity levels for crate-level logging.
72#[derive(
73    Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq,
74)]
75pub enum LogCrateLevel {
76    /// Logs informational messages.
77    #[default]
78    #[serde(rename = "info")]
79    Info,
80
81    /// Logs debugging messages.
82    #[serde(rename = "debug")]
83    Debug,
84
85    /// Logs trace-level messages.
86    #[serde(rename = "trace")]
87    Trace,
88}
89
90impl LogLevel for u32 {}
91
92fn append_default() -> bool {
93    true
94}
95
96/// Trait for types that can be used as log levels in `LoggerConfig`.
97pub trait LogLevel:
98    serde::de::DeserializeOwned + serde::Serialize + Clone + Copy + core::fmt::Debug + Default
99{
100}
101
102/// Central logging utility for CubeCL, managing multiple log outputs.
103#[derive(Debug)]
104pub struct Logger {
105    /// Collection of logger instances (file, stdout, stderr, or crate-level).
106    loggers: Vec<LoggerKind>,
107
108    /// Indices of loggers used for compilation logging.
109    compilation_index: Vec<usize>,
110
111    /// Indices of loggers used for profiling logging.
112    profiling_index: Vec<usize>,
113
114    /// Indices of loggers used for autotuning logging.
115    autotune_index: Vec<usize>,
116
117    /// Global configuration for logging settings.
118    pub config: Arc<GlobalConfig>,
119}
120
121impl Default for Logger {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl Logger {
128    /// Creates a new `Logger` instance based on the global configuration.
129    ///
130    /// Initializes loggers for compilation, profiling, and autotuning based on the settings in
131    /// `GlobalConfig`.
132    ///
133    /// Note that creating a logger is quite expensive.
134    pub fn new() -> Self {
135        let config = GlobalConfig::get();
136        let mut loggers = Vec::new();
137        let mut compilation_index = Vec::new();
138        let mut profiling_index = Vec::new();
139        let mut autotune_index = Vec::new();
140
141        #[derive(Hash, PartialEq, Eq)]
142        enum LoggerId {
143            #[cfg(std_io)]
144            File(PathBuf),
145            #[cfg(feature = "std")]
146            Stdout,
147            #[cfg(feature = "std")]
148            Stderr,
149            LogCrate(LogCrateLevel),
150        }
151
152        let mut logger2index = HashMap::<LoggerId, usize>::new();
153
154        fn new_logger<S: Clone, ID: Fn(S) -> LoggerId, LG: Fn(S) -> LoggerKind>(
155            setting_index: &mut Vec<usize>,
156            loggers: &mut Vec<LoggerKind>,
157            logger2index: &mut HashMap<LoggerId, usize>,
158            state: S,
159            func_id: ID,
160            func_logger: LG,
161        ) {
162            let id = func_id(state.clone());
163
164            if let Some(index) = logger2index.get(&id) {
165                setting_index.push(*index);
166            } else {
167                let logger = func_logger(state);
168                let index = loggers.len();
169                logger2index.insert(id, index);
170                loggers.push(logger);
171                setting_index.push(index);
172            }
173        }
174
175        fn register_logger<L: LogLevel>(
176            #[allow(unused_variables)] kind: &LoggerConfig<L>, // not used in no-std
177            #[allow(unused_variables)] append: bool,           // not used in no-std
178            level: Option<LogCrateLevel>,
179            setting_index: &mut Vec<usize>,
180            loggers: &mut Vec<LoggerKind>,
181            logger2index: &mut HashMap<LoggerId, usize>,
182        ) {
183            #[cfg(std_io)]
184            if let Some(file) = &kind.file {
185                new_logger(
186                    setting_index,
187                    loggers,
188                    logger2index,
189                    (file, append),
190                    |(file, _append)| LoggerId::File(file.clone()),
191                    |(file, append)| LoggerKind::File(FileLogger::new(file, append)),
192                );
193            }
194
195            #[cfg(feature = "std")]
196            if kind.stdout {
197                new_logger(
198                    setting_index,
199                    loggers,
200                    logger2index,
201                    (),
202                    |_| LoggerId::Stdout,
203                    |_| LoggerKind::Stdout,
204                );
205            }
206
207            #[cfg(feature = "std")]
208            if kind.stderr {
209                new_logger(
210                    setting_index,
211                    loggers,
212                    logger2index,
213                    (),
214                    |_| LoggerId::Stderr,
215                    |_| LoggerKind::Stderr,
216                );
217            }
218
219            if let Some(level) = level {
220                new_logger(
221                    setting_index,
222                    loggers,
223                    logger2index,
224                    level,
225                    LoggerId::LogCrate,
226                    LoggerKind::Log,
227                );
228            }
229        }
230
231        if let CompilationLogLevel::Disabled = config.compilation.logger.level {
232        } else {
233            register_logger(
234                &config.compilation.logger,
235                config.compilation.logger.append,
236                config.compilation.logger.log,
237                &mut compilation_index,
238                &mut loggers,
239                &mut logger2index,
240            )
241        }
242
243        if let ProfilingLogLevel::Disabled = config.profiling.logger.level {
244        } else {
245            register_logger(
246                &config.profiling.logger,
247                config.profiling.logger.append,
248                config.profiling.logger.log,
249                &mut profiling_index,
250                &mut loggers,
251                &mut logger2index,
252            )
253        }
254
255        if let AutotuneLogLevel::Disabled = config.autotune.logger.level {
256        } else {
257            register_logger(
258                &config.autotune.logger,
259                config.autotune.logger.append,
260                config.autotune.logger.log,
261                &mut autotune_index,
262                &mut loggers,
263                &mut logger2index,
264            )
265        }
266
267        Self {
268            loggers,
269            compilation_index,
270            profiling_index,
271            autotune_index,
272            config,
273        }
274    }
275
276    /// Logs a message for compilation, directing it to all configured compilation loggers.
277    pub fn log_compilation<S: Display>(&mut self, msg: &S) {
278        let length = self.compilation_index.len();
279        if length > 1 {
280            let msg = msg.to_string();
281            for i in 0..length {
282                let index = self.compilation_index[i];
283                self.log(&msg, index)
284            }
285        } else if let Some(index) = self.compilation_index.first() {
286            self.log(&msg, *index)
287        }
288    }
289
290    /// Logs a message for profiling, directing it to all configured profiling loggers.
291    pub fn log_profiling<S: Display>(&mut self, msg: &S) {
292        let length = self.profiling_index.len();
293        if length > 1 {
294            let msg = msg.to_string();
295            for i in 0..length {
296                let index = self.profiling_index[i];
297                self.log(&msg, index)
298            }
299        } else if let Some(index) = self.profiling_index.first() {
300            self.log(&msg, *index)
301        }
302    }
303
304    /// Logs a message for autotuning, directing it to all configured autotuning loggers.
305    pub fn log_autotune<S: Display>(&mut self, msg: &S) {
306        let length = self.autotune_index.len();
307        if length > 1 {
308            let msg = msg.to_string();
309            for i in 0..length {
310                let index = self.autotune_index[i];
311                self.log(&msg, index)
312            }
313        } else if let Some(index) = self.autotune_index.first() {
314            self.log(&msg, *index)
315        }
316    }
317
318    /// Returns the current autotune log level from the global configuration.
319    pub fn log_level_autotune(&self) -> AutotuneLogLevel {
320        self.config.autotune.logger.level
321    }
322
323    /// Returns the current compilation log level from the global configuration.
324    pub fn log_level_compilation(&self) -> CompilationLogLevel {
325        self.config.compilation.logger.level
326    }
327
328    /// Returns the current profiling log level from the global configuration.
329    pub fn log_level_profiling(&self) -> ProfilingLogLevel {
330        self.config.profiling.logger.level
331    }
332
333    fn log<S: Display>(&mut self, msg: &S, index: usize) {
334        let logger = &mut self.loggers[index];
335        logger.log(msg);
336    }
337}
338
339/// Binary log level for enabling or disabling logging.
340///
341/// This enum provides a simple on/off toggle for logging.
342#[derive(Default, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
343pub enum BinaryLogLevel {
344    /// Logging is disabled.
345    #[default]
346    #[serde(rename = "disabled")]
347    Disabled,
348
349    /// Logging is fully enabled.
350    #[serde(rename = "full")]
351    Full,
352}
353
354impl LogLevel for BinaryLogLevel {}
355
356/// Represents different types of loggers.
357#[derive(Debug)]
358enum LoggerKind {
359    /// Logs to a file.
360    #[cfg(std_io)]
361    File(FileLogger),
362
363    /// Logs to standard output.
364    #[cfg(feature = "std")]
365    Stdout,
366
367    /// Logs to standard error.
368    #[cfg(feature = "std")]
369    Stderr,
370
371    /// Logs using the `log` crate with a specified level.
372    Log(LogCrateLevel),
373}
374
375impl LoggerKind {
376    fn log<S: Display>(&mut self, msg: &S) {
377        match self {
378            #[cfg(std_io)]
379            LoggerKind::File(file_logger) => file_logger.log(msg),
380            #[cfg(feature = "std")]
381            LoggerKind::Stdout => println!("{msg}"),
382            #[cfg(feature = "std")]
383            LoggerKind::Stderr => eprintln!("{msg}"),
384            LoggerKind::Log(level) => match level {
385                LogCrateLevel::Info => log::info!("{msg}"),
386                LogCrateLevel::Trace => log::debug!("{msg}"),
387                LogCrateLevel::Debug => log::trace!("{msg}"),
388            },
389        }
390    }
391}
392
393/// Logger that writes messages to a file.
394#[derive(Debug)]
395#[cfg(std_io)]
396struct FileLogger {
397    writer: BufWriter<File>,
398}
399
400#[cfg(std_io)]
401impl FileLogger {
402    // Creates a new file logger.
403    fn new(path: &PathBuf, append: bool) -> Self {
404        let file = OpenOptions::new()
405            .write(true)
406            .append(append)
407            .create(true)
408            .open(path)
409            .unwrap();
410
411        Self {
412            writer: BufWriter::new(file),
413        }
414    }
415
416    // Logs a message to the file, flushing the buffer to ensure immediate write.
417    fn log<S: Display>(&mut self, msg: &S) {
418        writeln!(self.writer, "{msg}").expect("Should be able to log debug information.");
419        self.writer.flush().expect("Can complete write operation.");
420    }
421}