nautilus_common/logging/
config.rs1use std::{env, str::FromStr};
50
51use ahash::AHashMap;
52use log::LevelFilter;
53use ustr::Ustr;
54
55#[cfg_attr(
57 feature = "python",
58 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common")
59)]
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct LoggerConfig {
62 pub stdout_level: LevelFilter,
64 pub fileout_level: LevelFilter,
66 pub component_level: AHashMap<Ustr, LevelFilter>,
68 pub log_components_only: bool,
70 pub is_colored: bool,
72 pub print_config: bool,
74}
75
76impl Default for LoggerConfig {
77 fn default() -> Self {
79 Self {
80 stdout_level: LevelFilter::Info,
81 fileout_level: LevelFilter::Off,
82 component_level: AHashMap::new(),
83 log_components_only: false,
84 is_colored: true,
85 print_config: false,
86 }
87 }
88}
89
90impl LoggerConfig {
91 #[must_use]
93 pub fn new(
94 stdout_level: LevelFilter,
95 fileout_level: LevelFilter,
96 component_level: AHashMap<Ustr, LevelFilter>,
97 log_components_only: bool,
98 is_colored: bool,
99 print_config: bool,
100 ) -> Self {
101 Self {
102 stdout_level,
103 fileout_level,
104 component_level,
105 log_components_only,
106 is_colored,
107 print_config,
108 }
109 }
110
111 pub fn from_spec(spec: &str) -> anyhow::Result<Self> {
124 let mut config = Self::default();
125
126 for kv in spec.split(';') {
127 let kv = kv.trim();
128 if kv.is_empty() {
129 continue;
130 }
131
132 let kv_lower = kv.to_lowercase();
133
134 if !kv.contains('=') {
136 match kv_lower.as_str() {
137 "log_components_only" => config.log_components_only = true,
138 "is_colored" => config.is_colored = true,
139 "print_config" => config.print_config = true,
140 _ => anyhow::bail!("Invalid spec pair: {kv}"),
141 }
142 continue;
143 }
144
145 let parts: Vec<&str> = kv.splitn(2, '=').collect();
146 if parts.len() != 2 {
147 anyhow::bail!("Invalid spec pair: {kv}");
148 }
149
150 let k = parts[0].trim();
151 let v = parts[1].trim();
152 let k_lower = k.to_lowercase();
153
154 match k_lower.as_str() {
155 "is_colored" => {
156 config.is_colored = parse_bool_value(v);
157 }
158 "log_components_only" => {
159 config.log_components_only = parse_bool_value(v);
160 }
161 "print_config" => {
162 config.print_config = parse_bool_value(v);
163 }
164 "stdout" => {
165 config.stdout_level = parse_level(v)?;
166 }
167 "fileout" => {
168 config.fileout_level = parse_level(v)?;
169 }
170 _ => {
172 let lvl = parse_level(v)?;
173 config.component_level.insert(Ustr::from(k), lvl);
174 }
175 }
176 }
177
178 Ok(config)
179 }
180
181 pub fn from_env() -> anyhow::Result<Self> {
187 let spec = env::var("NAUTILUS_LOG")?;
188 Self::from_spec(&spec)
189 }
190}
191
192fn parse_bool_value(v: &str) -> bool {
196 !matches!(v.to_lowercase().as_str(), "false" | "0" | "no")
197}
198
199fn parse_level(v: &str) -> anyhow::Result<LevelFilter> {
201 LevelFilter::from_str(v).map_err(|_| anyhow::anyhow!("Invalid log level: {v}"))
202}
203
204#[cfg(test)]
205mod tests {
206 use rstest::rstest;
207
208 use super::*;
209
210 #[rstest]
211 fn test_default_config() {
212 let config = LoggerConfig::default();
213 assert_eq!(config.stdout_level, LevelFilter::Info);
214 assert_eq!(config.fileout_level, LevelFilter::Off);
215 assert!(config.component_level.is_empty());
216 assert!(!config.log_components_only);
217 assert!(config.is_colored);
218 assert!(!config.print_config);
219 }
220
221 #[rstest]
222 fn test_from_spec_stdout_and_fileout() {
223 let config = LoggerConfig::from_spec("stdout=Debug;fileout=Error").unwrap();
224 assert_eq!(config.stdout_level, LevelFilter::Debug);
225 assert_eq!(config.fileout_level, LevelFilter::Error);
226 }
227
228 #[rstest]
229 fn test_from_spec_case_insensitive_levels() {
230 let config = LoggerConfig::from_spec("stdout=debug;fileout=ERROR").unwrap();
231 assert_eq!(config.stdout_level, LevelFilter::Debug);
232 assert_eq!(config.fileout_level, LevelFilter::Error);
233 }
234
235 #[rstest]
236 fn test_from_spec_case_insensitive_keys() {
237 let config = LoggerConfig::from_spec("STDOUT=Info;FILEOUT=Debug").unwrap();
238 assert_eq!(config.stdout_level, LevelFilter::Info);
239 assert_eq!(config.fileout_level, LevelFilter::Debug);
240 }
241
242 #[rstest]
243 fn test_from_spec_empty_string() {
244 let config = LoggerConfig::from_spec("").unwrap();
245 assert_eq!(config, LoggerConfig::default());
246 }
247
248 #[rstest]
249 fn test_from_spec_with_whitespace() {
250 let config = LoggerConfig::from_spec(" stdout = Info ; fileout = Debug ").unwrap();
251 assert_eq!(config.stdout_level, LevelFilter::Info);
252 assert_eq!(config.fileout_level, LevelFilter::Debug);
253 }
254
255 #[rstest]
256 fn test_from_spec_trailing_semicolon() {
257 let config = LoggerConfig::from_spec("stdout=Warn;").unwrap();
258 assert_eq!(config.stdout_level, LevelFilter::Warn);
259 }
260
261 #[rstest]
262 fn test_from_spec_bare_is_colored() {
263 let config = LoggerConfig::from_spec("is_colored").unwrap();
264 assert!(config.is_colored);
265 }
266
267 #[rstest]
268 fn test_from_spec_is_colored_true() {
269 let config = LoggerConfig::from_spec("is_colored=true").unwrap();
270 assert!(config.is_colored);
271 }
272
273 #[rstest]
274 fn test_from_spec_is_colored_false() {
275 let config = LoggerConfig::from_spec("is_colored=false").unwrap();
276 assert!(!config.is_colored);
277 }
278
279 #[rstest]
280 fn test_from_spec_is_colored_zero() {
281 let config = LoggerConfig::from_spec("is_colored=0").unwrap();
282 assert!(!config.is_colored);
283 }
284
285 #[rstest]
286 fn test_from_spec_is_colored_no() {
287 let config = LoggerConfig::from_spec("is_colored=no").unwrap();
288 assert!(!config.is_colored);
289 }
290
291 #[rstest]
292 fn test_from_spec_is_colored_case_insensitive() {
293 let config = LoggerConfig::from_spec("IS_COLORED=FALSE").unwrap();
294 assert!(!config.is_colored);
295 }
296
297 #[rstest]
298 fn test_from_spec_print_config() {
299 let config = LoggerConfig::from_spec("print_config").unwrap();
300 assert!(config.print_config);
301 }
302
303 #[rstest]
304 fn test_from_spec_print_config_false() {
305 let config = LoggerConfig::from_spec("print_config=false").unwrap();
306 assert!(!config.print_config);
307 }
308
309 #[rstest]
310 fn test_from_spec_log_components_only() {
311 let config = LoggerConfig::from_spec("log_components_only").unwrap();
312 assert!(config.log_components_only);
313 }
314
315 #[rstest]
316 fn test_from_spec_log_components_only_false() {
317 let config = LoggerConfig::from_spec("log_components_only=false").unwrap();
318 assert!(!config.log_components_only);
319 }
320
321 #[rstest]
322 fn test_from_spec_component_level() {
323 let config = LoggerConfig::from_spec("RiskEngine=Error;DataEngine=Debug").unwrap();
324 assert_eq!(
325 config.component_level[&Ustr::from("RiskEngine")],
326 LevelFilter::Error
327 );
328 assert_eq!(
329 config.component_level[&Ustr::from("DataEngine")],
330 LevelFilter::Debug
331 );
332 }
333
334 #[rstest]
335 fn test_from_spec_component_preserves_case() {
336 let config = LoggerConfig::from_spec("MyComponent=Info").unwrap();
338 assert!(
339 config
340 .component_level
341 .contains_key(&Ustr::from("MyComponent"))
342 );
343 assert!(
344 !config
345 .component_level
346 .contains_key(&Ustr::from("mycomponent"))
347 );
348 }
349
350 #[rstest]
351 fn test_from_spec_full_example() {
352 let config = LoggerConfig::from_spec(
353 "stdout=Info;fileout=Debug;RiskEngine=Error;is_colored;print_config",
354 )
355 .unwrap();
356
357 assert_eq!(config.stdout_level, LevelFilter::Info);
358 assert_eq!(config.fileout_level, LevelFilter::Debug);
359 assert_eq!(
360 config.component_level[&Ustr::from("RiskEngine")],
361 LevelFilter::Error
362 );
363 assert!(config.is_colored);
364 assert!(config.print_config);
365 }
366
367 #[rstest]
368 fn test_from_spec_disabled_colors() {
369 let config = LoggerConfig::from_spec("stdout=Info;is_colored=false;fileout=Debug").unwrap();
370 assert!(!config.is_colored);
371 assert_eq!(config.stdout_level, LevelFilter::Info);
372 assert_eq!(config.fileout_level, LevelFilter::Debug);
373 }
374
375 #[rstest]
376 fn test_from_spec_invalid_level() {
377 let result = LoggerConfig::from_spec("stdout=InvalidLevel");
378 assert!(result.is_err());
379 assert!(
380 result
381 .unwrap_err()
382 .to_string()
383 .contains("Invalid log level")
384 );
385 }
386
387 #[rstest]
388 fn test_from_spec_invalid_bare_flag() {
389 let result = LoggerConfig::from_spec("unknown_flag");
390 assert!(result.is_err());
391 assert!(
392 result
393 .unwrap_err()
394 .to_string()
395 .contains("Invalid spec pair")
396 );
397 }
398
399 #[rstest]
400 fn test_from_spec_missing_value() {
401 let result = LoggerConfig::from_spec("stdout=");
403 assert!(result.is_err());
404 }
405
406 #[rstest]
407 #[case("Off", LevelFilter::Off)]
408 #[case("Error", LevelFilter::Error)]
409 #[case("Warn", LevelFilter::Warn)]
410 #[case("Info", LevelFilter::Info)]
411 #[case("Debug", LevelFilter::Debug)]
412 #[case("Trace", LevelFilter::Trace)]
413 fn test_all_log_levels(#[case] level_str: &str, #[case] expected: LevelFilter) {
414 let config = LoggerConfig::from_spec(&format!("stdout={level_str}")).unwrap();
415 assert_eq!(config.stdout_level, expected);
416 }
417}