1use std::{
2 error::Error,
3 fmt::{Display, Formatter},
4};
5
6use log::LevelFilter;
7
8use super::directive::DirectiveKind;
9use crate::filter::{Directive, FilterOp};
10
11#[derive(Default, Debug)]
12pub(crate) struct ParseResult {
13 pub(crate) directives: Vec<Directive>,
14 pub(crate) filter: Option<FilterOp>,
15 pub(crate) errors: Vec<String>,
16}
17
18impl ParseResult {
19 fn add_directive(&mut self, directive: Directive) {
20 self.directives.push(directive);
21 }
22
23 fn set_filter(&mut self, filter: FilterOp) {
24 self.filter = Some(filter);
25 }
26
27 fn add_error(&mut self, message: String) {
28 self.errors.push(message);
29 }
30
31 pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> {
32 let Self {
33 directives,
34 filter,
35 errors,
36 } = self;
37 if let Some(error) = errors.into_iter().next() {
38 Err(ParseError { details: error })
39 } else {
40 Ok((directives, filter))
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct ParseError {
48 details: String,
49}
50
51impl Display for ParseError {
52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53 write!(f, "error parsing logger filter: {}", self.details)
54 }
55}
56
57impl Error for ParseError {}
58
59pub(crate) fn parse_spec(s: &str) -> ParseResult {
62 let mut result = ParseResult::default();
63
64 let (spec, filter) = s.rsplit_once('/').unwrap_or((s, ""));
65 if spec.contains('/') {
66 result.add_error(format!("invalid logging spec '{s}': too many '/'"));
67 return result;
68 }
69 let filter = if filter.is_empty() {
70 None
71 } else {
72 Some(filter)
73 };
74 let directives = spec.split(',').map(|s| s.trim());
75 for directive in directives {
76 if directive.is_empty() {
77 continue;
78 }
79
80 let (matcher_spec, level, negated) = match directive.rsplit_once('=') {
81 Some((ms, "")) => {
82 let ms = ms.trim();
83 if ms.contains('=') {
84 result.add_error(format!(
85 "invalid logging spec '{directive}': '=' is not allowed in paths"
86 ));
87 continue;
88 }
89 if let Some(ms) = ms.strip_prefix('-') {
90 (Some(ms), LevelFilter::max(), true)
91 } else {
92 (Some(ms), LevelFilter::max(), false)
93 }
94 }
95 Some((ms, level)) => {
96 let ms = ms.trim();
97 if ms.contains('=') {
98 result.add_error(format!(
99 "invalid logging spec '{directive}': '=' is not allowed in paths"
100 ));
101 continue;
102 }
103 let level = level
104 .trim()
105 .parse::<LevelFilter>()
106 .map_err(|err| format!("invalid logging spec '{directive}': {err}"));
107 match level {
108 Ok(level) => {
109 if let Some(ms) = ms.strip_prefix('-') {
110 (Some(ms), level, true)
111 } else {
112 (Some(ms), level, false)
113 }
114 }
115 Err(err) => {
116 result.add_error(err);
117 continue;
118 }
119 }
120 }
121 None => {
122 let (level, negated) = if let Some(level) = directive.strip_prefix('-') {
123 (level.trim(), true)
124 } else {
125 (directive, false)
126 };
127 match level.parse::<LevelFilter>() {
128 Ok(level) => (None, level, negated),
129 Err(_) => (Some(directive), LevelFilter::max(), negated),
130 }
131 }
132 };
133
134 if let Some(matcher_spec) = matcher_spec {
135 match matcher_spec.split_once(':') {
136 Some((component, "*" | "")) => {
137 result.add_directive(Directive {
138 kind: DirectiveKind::Component {
139 component: component.to_owned(),
140 },
141 level,
142 negated,
143 });
144 }
145 Some((_, topic)) if topic.starts_with(':') => {
148 result.add_directive(Directive {
149 kind: DirectiveKind::Module {
150 module: matcher_spec.to_owned(),
151 },
152 level,
153 negated,
154 });
155 continue;
156 }
157 Some((component, topic)) => {
158 let topic = match FilterOp::new(topic) {
159 Ok(topic) => topic,
160 Err(err) => {
161 result.add_error(format!("invalid logging spec '{directive}': {err}"));
162 continue;
163 }
164 };
165 result.add_directive(Directive {
166 kind: DirectiveKind::Topic {
167 component: component.to_owned(),
168 topic,
169 },
170 level,
171 negated,
172 });
173 }
174 None => {
175 result.add_directive(Directive {
176 kind: DirectiveKind::Component {
177 component: matcher_spec.to_owned(),
178 },
179 level,
180 negated,
181 });
182 }
183 }
184 } else {
185 result.add_directive(Directive {
186 kind: DirectiveKind::Any,
187 level,
188 negated,
189 });
190 }
191 }
192
193 if let Some(filter) = filter {
194 match FilterOp::new(filter) {
195 Ok(filter_op) => result.set_filter(filter_op),
196 Err(err) => result.add_error(format!("invalid regex filter - {err}")),
197 }
198 }
199
200 result
201}
202
203#[cfg(test)]
204mod tests {
205 use log::LevelFilter;
206 use snapbox::{Data, IntoData, assert_data_eq, str};
207
208 use super::{ParseResult, parse_spec};
209 use crate::filter::{ParseError, directive::DirectiveKind, op::FilterOp};
210
211 impl IntoData for ParseError {
212 fn into_data(self) -> Data {
213 self.to_string().into_data()
214 }
215 }
216
217 #[test]
218 fn parse_spec_valid() {
219 let ParseResult {
220 directives: dirs,
221 filter,
222 errors,
223 } = parse_spec(
224 "crate1::mod1=error,crate1::mod2,crate2=debug,component:topic=trace,component2=trace",
225 );
226
227 assert_eq!(dirs.len(), 5);
228 assert_eq!(
229 dirs[0].kind,
230 DirectiveKind::Module {
231 module: "crate1::mod1".to_owned()
232 }
233 );
234 assert_eq!(dirs[0].level, LevelFilter::Error);
235
236 assert_eq!(
237 dirs[1].kind,
238 DirectiveKind::Module {
239 module: "crate1::mod2".to_owned()
240 }
241 );
242 assert_eq!(dirs[1].level, LevelFilter::max());
243
244 assert_eq!(
245 dirs[2].kind,
246 DirectiveKind::Component {
247 component: "crate2".to_owned()
248 }
249 );
250 assert_eq!(dirs[2].level, LevelFilter::Debug);
251 assert!(filter.is_none());
252
253 assert_eq!(
254 dirs[3].kind,
255 DirectiveKind::Topic {
256 component: "component".to_owned(),
257 topic: FilterOp::new("topic").unwrap()
258 }
259 );
260 assert_eq!(dirs[3].level, LevelFilter::Trace);
261 assert!(filter.is_none());
262
263 assert_eq!(
264 dirs[4].kind,
265 DirectiveKind::Component {
266 component: "component2".to_owned()
267 }
268 );
269 assert_eq!(dirs[4].level, LevelFilter::Trace);
270 assert!(filter.is_none());
271
272 assert!(errors.is_empty());
273 }
274
275 #[test]
276 fn parse_spec_invalid_crate() {
277 let ParseResult {
279 directives: dirs,
280 filter,
281 errors,
282 } = parse_spec("crate1::mod1=warn=info,crate2=debug");
283
284 assert_eq!(dirs.len(), 1);
285 assert_eq!(
286 dirs[0].kind,
287 DirectiveKind::Component {
288 component: "crate2".to_owned()
289 }
290 );
291 assert_eq!(dirs[0].level, LevelFilter::Debug);
292 assert!(filter.is_none());
293
294 assert_eq!(errors.len(), 1);
295 assert_data_eq!(
296 &errors[0],
297 str!["invalid logging spec 'crate1::mod1=warn=info': '=' is not allowed in paths"]
298 );
299 }
300
301 #[test]
302 fn parse_spec_invalid_level() {
303 let ParseResult {
305 directives: dirs,
306 filter,
307 errors,
308 } = parse_spec("crate1::mod1=noNumber,crate2=debug");
309
310 assert_eq!(dirs.len(), 1);
311 assert_eq!(
312 dirs[0].kind,
313 DirectiveKind::Component {
314 component: "crate2".to_owned()
315 }
316 );
317 assert_eq!(dirs[0].level, LevelFilter::Debug);
318 assert!(filter.is_none());
319
320 assert_eq!(errors.len(), 1);
321 assert_data_eq!(
322 &errors[0],
323 str![
324 "invalid logging spec 'crate1::mod1=noNumber': attempted to convert a string that \
325 doesn't match an existing log level"
326 ]
327 );
328 }
329
330 #[test]
331 fn parse_spec_string_level() {
332 let ParseResult {
334 directives: dirs,
335 filter,
336 errors,
337 } = parse_spec("crate1::mod1=wrong,crate2=warn");
338
339 assert_eq!(dirs.len(), 1);
340 assert_eq!(
341 dirs[0].kind,
342 DirectiveKind::Component {
343 component: "crate2".to_owned()
344 }
345 );
346 assert_eq!(dirs[0].level, LevelFilter::Warn);
347 assert!(filter.is_none());
348
349 assert_eq!(errors.len(), 1);
350 assert_data_eq!(
351 &errors[0],
352 str![
353 "invalid logging spec 'crate1::mod1=wrong': attempted to convert a string that \
354 doesn't match an existing log level"
355 ]
356 );
357 }
358
359 #[test]
360 fn parse_spec_empty_level() {
361 let ParseResult {
363 directives: dirs,
364 filter,
365 errors,
366 } = parse_spec("crate1::mod1=wrong,crate2=");
367
368 assert_eq!(dirs.len(), 1);
369 assert_eq!(
370 dirs[0].kind,
371 DirectiveKind::Component {
372 component: "crate2".to_owned()
373 }
374 );
375 assert_eq!(dirs[0].level, LevelFilter::max());
376 assert!(filter.is_none());
377
378 assert_eq!(errors.len(), 1);
379 assert_data_eq!(
380 &errors[0],
381 str![
382 "invalid logging spec 'crate1::mod1=wrong': attempted to convert a string that \
383 doesn't match an existing log level"
384 ]
385 );
386 }
387
388 #[test]
389 fn parse_spec_empty_level_isolated() {
390 let ParseResult {
392 directives: dirs,
393 filter,
394 errors,
395 } = parse_spec(""); assert_eq!(dirs.len(), 0);
397 assert!(filter.is_none());
398 assert!(errors.is_empty());
399 }
400
401 #[test]
402 fn parse_spec_blank_level_isolated() {
403 let ParseResult {
406 directives: dirs,
407 filter,
408 errors,
409 } = parse_spec(" "); assert_eq!(dirs.len(), 0);
411 assert!(filter.is_none());
412 assert!(errors.is_empty());
413 }
414
415 #[test]
416 fn parse_spec_blank_level_isolated_comma_only() {
417 let ParseResult {
421 directives: dirs,
422 filter,
423 errors,
424 } = parse_spec(","); assert_eq!(dirs.len(), 0);
426 assert!(filter.is_none());
427 assert!(errors.is_empty());
428 }
429
430 #[test]
431 fn parse_spec_blank_level_isolated_comma_blank() {
432 let ParseResult {
437 directives: dirs,
438 filter,
439 errors,
440 } = parse_spec(", "); assert_eq!(dirs.len(), 0);
442 assert!(filter.is_none());
443 assert!(errors.is_empty());
444 }
445
446 #[test]
447 fn parse_spec_blank_level_isolated_blank_comma() {
448 let ParseResult {
453 directives: dirs,
454 filter,
455 errors,
456 } = parse_spec(" ,"); assert_eq!(dirs.len(), 0);
458 assert!(filter.is_none());
459 assert!(errors.is_empty());
460 }
461
462 #[test]
463 fn parse_spec_global() {
464 let ParseResult {
466 directives: dirs,
467 filter,
468 errors,
469 } = parse_spec("warn,crate2=debug");
470 assert_eq!(dirs.len(), 2);
471 assert_eq!(dirs[0].kind, DirectiveKind::Any);
472 assert_eq!(dirs[0].level, LevelFilter::Warn);
473 assert_eq!(
474 dirs[1].kind,
475 DirectiveKind::Component {
476 component: "crate2".to_owned()
477 }
478 );
479 assert_eq!(dirs[1].level, LevelFilter::Debug);
480 assert!(filter.is_none());
481 assert!(errors.is_empty());
482 }
483
484 #[test]
485 fn parse_spec_global_bare_warn_lc() {
486 let ParseResult {
488 directives: dirs,
489 filter,
490 errors,
491 } = parse_spec("warn");
492 assert_eq!(dirs.len(), 1);
493 assert_eq!(dirs[0].kind, DirectiveKind::Any);
494 assert_eq!(dirs[0].level, LevelFilter::Warn);
495 assert!(filter.is_none());
496 assert!(errors.is_empty());
497 }
498
499 #[test]
500 fn parse_spec_global_bare_warn_uc() {
501 let ParseResult {
503 directives: dirs,
504 filter,
505 errors,
506 } = parse_spec("WARN");
507 assert_eq!(dirs.len(), 1);
508 assert_eq!(dirs[0].kind, DirectiveKind::Any);
509 assert_eq!(dirs[0].level, LevelFilter::Warn);
510 assert!(filter.is_none());
511 assert!(errors.is_empty());
512 }
513
514 #[test]
515 fn parse_spec_global_bare_warn_mixed() {
516 let ParseResult {
518 directives: dirs,
519 filter,
520 errors,
521 } = parse_spec("wArN");
522 assert_eq!(dirs.len(), 1);
523 assert_eq!(dirs[0].kind, DirectiveKind::Any);
524 assert_eq!(dirs[0].level, LevelFilter::Warn);
525 assert!(filter.is_none());
526 assert!(errors.is_empty());
527 }
528
529 #[test]
530 fn parse_spec_valid_filter() {
531 let ParseResult {
532 directives: dirs,
533 filter,
534 errors,
535 } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc");
536 assert_eq!(dirs.len(), 3);
537 assert_eq!(
538 dirs[0].kind,
539 DirectiveKind::Module {
540 module: "crate1::mod1".to_owned()
541 }
542 );
543 assert_eq!(dirs[0].level, LevelFilter::Error);
544
545 assert_eq!(
546 dirs[1].kind,
547 DirectiveKind::Module {
548 module: "crate1::mod2".to_owned()
549 }
550 );
551 assert_eq!(dirs[1].level, LevelFilter::max());
552
553 assert_eq!(
554 dirs[2].kind,
555 DirectiveKind::Component {
556 component: "crate2".to_owned()
557 }
558 );
559 assert_eq!(dirs[2].level, LevelFilter::Debug);
560 assert!(filter.is_some() && filter.unwrap().to_string() == "abc");
561 assert!(errors.is_empty());
562 }
563
564 #[test]
565 fn parse_spec_invalid_crate_filter() {
566 let ParseResult {
567 directives: dirs,
568 filter,
569 errors,
570 } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c");
571
572 assert_eq!(dirs.len(), 1);
573 assert_eq!(
574 dirs[0].kind,
575 DirectiveKind::Component {
576 component: "crate2".to_owned()
577 }
578 );
579 assert_eq!(dirs[0].level, LevelFilter::Debug);
580 assert!(filter.is_some() && filter.unwrap().to_string() == "a.c");
581
582 assert_eq!(errors.len(), 1);
583 assert_data_eq!(
584 &errors[0],
585 str!["invalid logging spec 'crate1::mod1=error=warn': '=' is not allowed in paths"]
586 );
587 }
588
589 #[test]
590 fn parse_spec_empty_with_filter() {
591 let ParseResult {
592 directives: dirs,
593 filter,
594 errors,
595 } = parse_spec("crate1/a*c");
596 assert_eq!(dirs.len(), 1);
597 assert_eq!(
598 dirs[0].kind,
599 DirectiveKind::Component {
600 component: "crate1".to_owned()
601 }
602 );
603 assert_eq!(dirs[0].level, LevelFilter::max());
604 assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
605 assert!(errors.is_empty());
606 }
607
608 #[test]
609 fn parse_spec_with_multiple_filters() {
610 let ParseResult {
611 directives: dirs,
612 filter,
613 errors,
614 } = parse_spec("debug/abc/a.c");
615 assert!(dirs.is_empty());
616 assert!(filter.is_none());
617
618 assert_eq!(errors.len(), 1);
619 assert_data_eq!(&errors[0], str!["invalid logging spec 'debug/abc/a.c': too many '/'"]);
620 }
621
622 #[test]
623 fn parse_spec_multiple_invalid_crates() {
624 let ParseResult {
626 directives: dirs,
627 filter,
628 errors,
629 } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error");
630
631 assert_eq!(dirs.len(), 1);
632 assert_eq!(
633 dirs[0].kind,
634 DirectiveKind::Component {
635 component: "crate2".to_owned()
636 }
637 );
638 assert_eq!(dirs[0].level, LevelFilter::Debug);
639 assert!(filter.is_none());
640
641 assert_eq!(errors.len(), 2);
642 assert_data_eq!(
643 &errors[0],
644 str!["invalid logging spec 'crate1::mod1=warn=info': '=' is not allowed in paths"]
645 );
646 assert_data_eq!(
647 &errors[1],
648 str!["invalid logging spec 'crate3=error=error': '=' is not allowed in paths"]
649 );
650 }
651
652 #[test]
653 fn parse_spec_multiple_invalid_levels() {
654 let ParseResult {
656 directives: dirs,
657 filter,
658 errors,
659 } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid");
660
661 assert_eq!(dirs.len(), 1);
662 assert_eq!(
663 dirs[0].kind,
664 DirectiveKind::Component {
665 component: "crate2".to_owned()
666 }
667 );
668 assert_eq!(dirs[0].level, LevelFilter::Debug);
669 assert!(filter.is_none());
670
671 assert_eq!(errors.len(), 2);
672 assert_data_eq!(
673 &errors[0],
674 str![
675 "invalid logging spec 'crate1::mod1=noNumber': attempted to convert a string that \
676 doesn't match an existing log level"
677 ]
678 );
679 assert_data_eq!(
680 &errors[1],
681 str![
682 "invalid logging spec 'crate3=invalid': attempted to convert a string that \
683 doesn't match an existing log level"
684 ]
685 );
686 }
687
688 #[test]
689 fn parse_spec_invalid_crate_and_level() {
690 let ParseResult {
692 directives: dirs,
693 filter,
694 errors,
695 } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid");
696
697 assert_eq!(dirs.len(), 1);
698 assert_eq!(
699 dirs[0].kind,
700 DirectiveKind::Component {
701 component: "crate2".to_owned()
702 }
703 );
704 assert_eq!(dirs[0].level, LevelFilter::Debug);
705 assert!(filter.is_none());
706
707 assert_eq!(errors.len(), 2);
708 assert_data_eq!(
709 &errors[0],
710 str!["invalid logging spec 'crate1::mod1=debug=info': '=' is not allowed in paths"]
711 );
712 assert_data_eq!(
713 &errors[1],
714 str![
715 "invalid logging spec 'crate3=invalid': attempted to convert a string that \
716 doesn't match an existing log level"
717 ]
718 );
719 }
720
721 #[test]
722 fn parse_error_message_single_error() {
723 let error = parse_spec("crate1::mod1=debug=info,crate2=debug").ok().unwrap_err();
724 assert_data_eq!(
725 error,
726 str![
727 "error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info': '=' \
728 is not allowed in paths"
729 ]
730 );
731 }
732
733 #[test]
734 fn parse_error_message_multiple_errors() {
735 let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid")
736 .ok()
737 .unwrap_err();
738 assert_data_eq!(
739 error,
740 str![
741 "error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info': '=' \
742 is not allowed in paths"
743 ]
744 );
745 }
746}