wick_logger/
options.rs

1use std::cmp;
2use std::str::FromStr;
3
4use tracing::{Level, Metadata};
5use tracing_subscriber::layer::Context;
6
7#[derive(Debug, Clone, derive_builder::Builder)]
8#[non_exhaustive]
9#[builder(default, derive(Debug), setter(into))]
10/// Logging options.
11pub struct LoggingOptions {
12  /// Turns on verbose logging.
13  pub verbose: bool,
14
15  /// The endpoint to send jaeger-format traces.
16  pub otlp_endpoint: Option<String>,
17
18  /// The application doing the logging.
19  pub app_name: String,
20
21  /// Log filtering options
22  pub levels: LogFilters,
23}
24
25impl LoggingOptions {
26  /// Create a new instance with the given log level.
27  #[must_use]
28  pub fn with_level(level: LogLevel) -> Self {
29    Self {
30      levels: LogFilters::with_level(level),
31      ..Default::default()
32    }
33  }
34}
35
36impl Default for LoggingOptions {
37  fn default() -> Self {
38    Self {
39      verbose: Default::default(),
40      otlp_endpoint: Default::default(),
41      app_name: "app".to_owned(),
42      levels: Default::default(),
43    }
44  }
45}
46
47#[derive(Debug, Default, Clone, derive_builder::Builder)]
48#[builder(default)]
49#[non_exhaustive]
50/// The filter configuration per log event destination.
51pub struct LogFilters {
52  /// The log level for the open telemetry events.
53  pub telemetry: FilterOptions,
54  /// The log level for the events printed to STDERR.
55  pub stderr: FilterOptions,
56}
57
58impl LogFilters {
59  /// Create a new filter configuration with the given log level.
60  #[must_use]
61  pub fn with_level(level: LogLevel) -> Self {
62    Self {
63      telemetry: FilterOptions {
64        level,
65        ..Default::default()
66      },
67      stderr: FilterOptions {
68        level,
69        ..Default::default()
70      },
71    }
72  }
73}
74
75/// Options for filtering logs.
76#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct FilterOptions {
79  /// The default log level for anything that does not match an include or exclude filter.
80  pub level: LogLevel,
81  /// The targets and their log levels.
82  pub filter: Vec<TargetLevel>,
83}
84
85impl Default for FilterOptions {
86  fn default() -> Self {
87    Self {
88      level: LogLevel::Info,
89      filter: vec![],
90    }
91  }
92}
93
94impl FilterOptions {
95  /// Create a new instance with the given log level.
96  #[must_use]
97  pub fn new(level: LogLevel, filter: Vec<TargetLevel>) -> Self {
98    Self { level, filter }
99  }
100
101  fn test_enabled(&self, module: &str, level: Level) -> bool {
102    let matches = self.filter.iter().filter(|config| module.starts_with(&config.target));
103    let match_hit = matches.fold(None, |acc, next| {
104      let enabled = next.modifier.compare(filter_as_usize(level), next.level as usize);
105      let next_len = next.target.len();
106      acc.map_or(Some((next_len, enabled)), |(last_len, last_enabled)| {
107        match next_len.cmp(&last_len) {
108          cmp::Ordering::Greater => {
109            // if we're more specific, use the most recent match result.
110            Some((next_len, enabled))
111          }
112          cmp::Ordering::Equal => {
113            // if we're the same specifity, keep testing
114            Some((last_len, enabled && last_enabled))
115          }
116          cmp::Ordering::Less => {
117            // otherwise, keep the last match result
118            Some((last_len, last_enabled))
119          }
120        }
121      })
122    });
123    match_hit.map_or(self.level >= level, |(_, enabled)| enabled)
124  }
125}
126
127impl<S> tracing_subscriber::layer::Filter<S> for FilterOptions
128where
129  S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
130{
131  fn enabled(&self, metadata: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
132    let enabled = metadata.target().starts_with("wick")
133      || metadata.target().starts_with("flow")
134      || metadata.target().starts_with("wasmrs");
135
136    metadata.is_span() || enabled
137  }
138
139  fn event_enabled(&self, event: &tracing::Event<'_>, _cx: &Context<'_, S>) -> bool {
140    let module = event.metadata().target().split("::").next().unwrap_or_default();
141    let level = event.metadata().level();
142    self.test_enabled(module, *level)
143  }
144}
145
146/// The log level for specific targets.
147#[derive(Debug, Default, PartialEq, Clone)]
148#[non_exhaustive]
149pub struct TargetLevel {
150  /// The target (module name).
151  pub target: String,
152  /// The level to log at.
153  pub level: LogLevel,
154  /// The modifier that controls how to use this log level.
155  pub modifier: LogModifier,
156}
157
158impl TargetLevel {
159  /// Create a new instance for the given target, log level, and modifier.
160  pub fn new<T: Into<String>>(target: T, level: LogLevel, modifier: LogModifier) -> Self {
161    Self {
162      target: target.into(),
163      level,
164      modifier,
165    }
166  }
167
168  /// Create a new negated instance for the given target and log level.
169  #[must_use]
170  pub fn not<T: Into<String>>(target: T, level: LogLevel) -> Self {
171    Self::new(target, level, LogModifier::Not)
172  }
173
174  /// Create a new instance that matches the given target and any log level greater than the one specified.
175  #[must_use]
176  pub fn gt<T: Into<String>>(target: T, level: LogLevel) -> Self {
177    Self::new(target, level, LogModifier::GreaterThan)
178  }
179
180  /// Create a new instance that matches the given target and any log level greater than or equal to the one specified.
181  #[must_use]
182  pub fn gte<T: Into<String>>(target: T, level: LogLevel) -> Self {
183    Self::new(target, level, LogModifier::GreaterThanOrEqualTo)
184  }
185
186  /// Create a new instance that matches the given target and any log level less than or equal to the one specified.
187  #[must_use]
188  pub fn lt<T: Into<String>>(target: T, level: LogLevel) -> Self {
189    Self::new(target, level, LogModifier::LessThan)
190  }
191
192  /// Create a new instance that matches the given target and any log level less than or equal to the one specified.
193  #[must_use]
194  pub fn lte<T: Into<String>>(target: T, level: LogLevel) -> Self {
195    Self::new(target, level, LogModifier::LessThanOrEqualTo)
196  }
197
198  /// Create a new instance that matches the given target and any log level equal to the one specified.
199  #[must_use]
200  pub fn is<T: Into<String>>(target: T, level: LogLevel) -> Self {
201    Self::new(target, level, LogModifier::Equal)
202  }
203}
204
205impl LoggingOptions {
206  /// Set the name of the application doing the logging.
207  pub fn name<T: Into<String>>(&self, name: T) -> Self {
208    Self {
209      app_name: name.into(),
210      ..self.clone()
211    }
212  }
213}
214
215#[derive(Debug, Clone, PartialEq, Copy)]
216#[non_exhaustive]
217/// Whether to include logs higher, lower, equal, or to not include them at all.
218pub enum LogModifier {
219  /// Do not log the associated level.
220  Not,
221  /// Only log events greater than the associated level.
222  GreaterThan,
223  /// Only log events greater than or equal to the associated level.
224  GreaterThanOrEqualTo,
225  /// Only log events less than the associated level.
226  LessThan,
227  /// Only log events less than or equal to the associated level.
228  LessThanOrEqualTo,
229  /// Only log events equal to the associated level.
230  Equal,
231}
232
233impl Default for LogModifier {
234  fn default() -> Self {
235    Self::LessThanOrEqualTo
236  }
237}
238
239impl std::fmt::Display for LogModifier {
240  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241    match self {
242      LogModifier::Not => write!(f, "!="),
243      LogModifier::GreaterThan => write!(f, ">"),
244      LogModifier::GreaterThanOrEqualTo => write!(f, ">="),
245      LogModifier::LessThan => write!(f, "<"),
246      LogModifier::LessThanOrEqualTo => write!(f, "<="),
247      LogModifier::Equal => write!(f, "="),
248    }
249  }
250}
251
252impl LogModifier {
253  const fn compare(self, a: usize, b: usize) -> bool {
254    match self {
255      LogModifier::Not => a != b,
256      LogModifier::GreaterThan => a > b,
257      LogModifier::GreaterThanOrEqualTo => a >= b,
258      LogModifier::LessThan => a < b,
259      LogModifier::LessThanOrEqualTo => a <= b,
260      LogModifier::Equal => a == b,
261    }
262  }
263}
264
265impl FromStr for LogModifier {
266  type Err = ();
267
268  fn from_str(s: &str) -> Result<Self, Self::Err> {
269    match s {
270      "!=" => Ok(LogModifier::Not),
271      ">" => Ok(LogModifier::GreaterThan),
272      ">=" => Ok(LogModifier::GreaterThanOrEqualTo),
273      "<" => Ok(LogModifier::LessThan),
274      "<=" => Ok(LogModifier::LessThanOrEqualTo),
275      "=" | "==" => Ok(LogModifier::Equal),
276
277      _ => Err(()),
278    }
279  }
280}
281
282/// The log levels.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
284#[non_exhaustive]
285#[repr(usize)]
286pub enum LogLevel {
287  /// No logging.
288  Quiet = 0,
289  /// Only log errors.
290  Error = 1,
291  /// Only log warnings and errors.
292  Warn = 2,
293  /// The default log level.
294  Info = 3,
295  /// Log debug messages.
296  Debug = 4,
297  /// Log trace messages.
298  Trace = 5,
299}
300
301impl Default for LogLevel {
302  fn default() -> Self {
303    Self::Info
304  }
305}
306
307impl FromStr for LogLevel {
308  type Err = ();
309
310  fn from_str(s: &str) -> Result<Self, Self::Err> {
311    match s.to_lowercase().as_str() {
312      "quiet" => Ok(LogLevel::Quiet),
313      "error" => Ok(LogLevel::Error),
314      "warn" => Ok(LogLevel::Warn),
315      "info" => Ok(LogLevel::Info),
316      "debug" => Ok(LogLevel::Debug),
317      "trace" => Ok(LogLevel::Trace),
318      _ => Err(()),
319    }
320  }
321}
322
323impl std::fmt::Display for LogLevel {
324  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325    match self {
326      LogLevel::Quiet => write!(f, "QUIET"),
327      LogLevel::Error => write!(f, "ERROR"),
328      LogLevel::Warn => write!(f, "WARN"),
329      LogLevel::Info => write!(f, "INFO"),
330      LogLevel::Debug => write!(f, "DEBUG"),
331      LogLevel::Trace => write!(f, "TRACE"),
332    }
333  }
334}
335
336impl PartialEq<Level> for LogLevel {
337  #[inline(always)]
338  fn eq(&self, other: &Level) -> bool {
339    match self {
340      LogLevel::Quiet => false,
341      LogLevel::Error => other.eq(&Level::ERROR),
342      LogLevel::Warn => other.eq(&Level::WARN),
343      LogLevel::Info => other.eq(&Level::INFO),
344      LogLevel::Debug => other.eq(&Level::DEBUG),
345      LogLevel::Trace => other.eq(&Level::TRACE),
346    }
347  }
348}
349
350impl PartialOrd<Level> for LogLevel {
351  #[inline(always)]
352  fn partial_cmp(&self, other: &Level) -> Option<cmp::Ordering> {
353    Some((*self as usize).cmp(&filter_as_usize(*other)))
354  }
355
356  #[inline(always)]
357  fn lt(&self, other: &Level) -> bool {
358    (*self as usize) < filter_as_usize(*other)
359  }
360
361  #[inline(always)]
362  fn le(&self, other: &Level) -> bool {
363    (*self as usize) <= filter_as_usize(*other)
364  }
365
366  #[inline(always)]
367  fn gt(&self, other: &Level) -> bool {
368    (*self as usize) > filter_as_usize(*other)
369  }
370
371  #[inline(always)]
372  fn ge(&self, other: &Level) -> bool {
373    (*self as usize) >= filter_as_usize(*other)
374  }
375}
376
377#[inline(always)]
378const fn filter_as_usize(x: Level) -> usize {
379  (match x {
380    Level::ERROR => 0,
381    Level::WARN => 1,
382    Level::INFO => 2,
383    Level::DEBUG => 3,
384    Level::TRACE => 4,
385  } + 1)
386}
387
388#[cfg(test)]
389mod test {
390
391  use super::*;
392
393  #[test]
394  fn test_modifier_compare() {
395    assert!(LogModifier::Equal.compare(2, 2));
396    assert!(LogModifier::GreaterThan.compare(4, 2));
397    assert!(LogModifier::GreaterThanOrEqualTo.compare(4, 2));
398    assert!(LogModifier::GreaterThanOrEqualTo.compare(2, 2));
399    assert!(LogModifier::Not.compare(4, 3));
400    assert!(LogModifier::LessThan.compare(1, 2));
401    assert!(LogModifier::LessThanOrEqualTo.compare(1, 2));
402    assert!(LogModifier::LessThanOrEqualTo.compare(2, 2));
403  }
404
405  #[test]
406  fn test_modifier_compare_level() {
407    assert!(LogModifier::Equal.compare(filter_as_usize(Level::TRACE), LogLevel::Trace as usize));
408    assert!(LogModifier::GreaterThan.compare(filter_as_usize(Level::TRACE), LogLevel::Warn as usize));
409    assert!(LogModifier::GreaterThanOrEqualTo.compare(filter_as_usize(Level::INFO), LogLevel::Info as usize));
410    assert!(LogModifier::GreaterThanOrEqualTo.compare(filter_as_usize(Level::TRACE), LogLevel::Debug as usize));
411    assert!(LogModifier::Not.compare(filter_as_usize(Level::ERROR), LogLevel::Trace as usize));
412    assert!(LogModifier::LessThan.compare(filter_as_usize(Level::INFO), LogLevel::Debug as usize));
413    assert!(LogModifier::LessThanOrEqualTo.compare(filter_as_usize(Level::TRACE), LogLevel::Trace as usize));
414    assert!(LogModifier::LessThanOrEqualTo.compare(filter_as_usize(Level::INFO), LogLevel::Trace as usize));
415  }
416
417  #[allow(clippy::needless_pass_by_value)]
418  fn opts<const K: usize>(default: LogLevel, targets: [TargetLevel; K]) -> FilterOptions {
419    FilterOptions {
420      level: default,
421      filter: targets.to_vec(),
422    }
423  }
424
425  #[test]
426  fn test_default_level() {
427    assert!(opts(LogLevel::Info, []).test_enabled("wick", Level::INFO));
428    assert!(!opts(LogLevel::Info, []).test_enabled("wick", Level::TRACE));
429  }
430
431  #[rstest::rstest]
432  #[case(LogLevel::Info, [TargetLevel::lte("wick",LogLevel::Trace)], "wick", Level::TRACE, true)]
433  #[case(LogLevel::Info, [TargetLevel::lte("wick",LogLevel::Info),TargetLevel::lte("wick_packet",LogLevel::Trace)], "wick_packet", Level::TRACE, true)]
434  #[case(LogLevel::Info, [
435    TargetLevel::lte("a",LogLevel::Info),
436    TargetLevel::not("ab",LogLevel::Trace),
437    TargetLevel::lte("abc",LogLevel::Trace)
438  ], "abcdef", Level::TRACE, true)]
439  #[case(LogLevel::Info, [
440    TargetLevel::lte("a",LogLevel::Info),
441    TargetLevel::lte("abc",LogLevel::Trace),
442    TargetLevel::not("ab",LogLevel::Trace),
443  ], "abcdef", Level::TRACE, true)]
444  fn test_specificity<const K: usize>(
445    #[case] default: LogLevel,
446    #[case] filter: [TargetLevel; K],
447    #[case] span_target: &str,
448    #[case] span_level: Level,
449    #[case] expect_enabled: bool,
450  ) {
451    assert_eq!(
452      opts(default, filter).test_enabled(span_target, span_level),
453      expect_enabled
454    );
455  }
456
457  #[rstest::rstest]
458  #[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, false)]
459  #[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
460  #[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
461  #[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
462  #[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
463  fn test_not(
464    #[case] default: LogLevel,
465    #[case] target_level: LogLevel,
466    #[case] span_level: Level,
467    #[case] expect_enabled: bool,
468  ) {
469    assert_eq!(
470      opts(default, [TargetLevel::not("wick", target_level)]).test_enabled("wick", span_level),
471      expect_enabled
472    );
473  }
474
475  #[rstest::rstest]
476  #[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, false)]
477  #[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
478  #[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
479  #[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
480  #[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
481  fn test_lt(
482    #[case] default: LogLevel,
483    #[case] target_level: LogLevel,
484    #[case] span_level: Level,
485    #[case] expect_enabled: bool,
486  ) {
487    assert_eq!(
488      opts(default, [TargetLevel::lt("wick", target_level)]).test_enabled("wick", span_level),
489      expect_enabled
490    );
491  }
492
493  #[rstest::rstest]
494  #[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, true)]
495  #[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
496  #[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
497  #[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
498  #[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
499  fn test_lte(
500    #[case] default: LogLevel,
501    #[case] target_level: LogLevel,
502    #[case] span_level: Level,
503    #[case] expect_enabled: bool,
504  ) {
505    assert_eq!(
506      opts(default, [TargetLevel::lte("wick", target_level)]).test_enabled("wick", span_level),
507      expect_enabled
508    );
509  }
510
511  #[rstest::rstest]
512  #[case(LogLevel::Info, LogLevel::Info, Level::TRACE, true)]
513  #[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, true)]
514  #[case(LogLevel::Info, LogLevel::Info, Level::INFO, true)]
515  #[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
516  #[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
517  fn test_gte(
518    #[case] default: LogLevel,
519    #[case] target_level: LogLevel,
520    #[case] span_level: Level,
521    #[case] expect_enabled: bool,
522  ) {
523    assert_eq!(
524      opts(default, [TargetLevel::gte("wick", target_level)]).test_enabled("wick", span_level),
525      expect_enabled
526    );
527  }
528
529  #[rstest::rstest]
530  #[case(LogLevel::Info, LogLevel::Info, Level::TRACE, true)]
531  #[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, true)]
532  #[case(LogLevel::Info, LogLevel::Info, Level::INFO, false)]
533  #[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
534  #[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
535  fn test_gt(
536    #[case] default: LogLevel,
537    #[case] target_level: LogLevel,
538    #[case] span_level: Level,
539    #[case] expect_enabled: bool,
540  ) {
541    assert_eq!(
542      opts(default, [TargetLevel::gt("wick", target_level)]).test_enabled("wick", span_level),
543      expect_enabled
544    );
545  }
546
547  #[rstest::rstest]
548  #[case(LogLevel::Info, LogLevel::Info, Level::TRACE, false)]
549  #[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, false)]
550  #[case(LogLevel::Info, LogLevel::Info, Level::INFO, true)]
551  #[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
552  #[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
553  fn test_eq(
554    #[case] default: LogLevel,
555    #[case] target_level: LogLevel,
556    #[case] span_level: Level,
557    #[case] expect_enabled: bool,
558  ) {
559    assert_eq!(
560      opts(default, [TargetLevel::is("wick", target_level)]).test_enabled("wick", span_level),
561      expect_enabled
562    );
563  }
564}