1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#[cfg(test)]
mod tests;

/// Re-export all of tracing for downstream consumers.
pub use tracing;

use atty::Stream;
use std::io;
use tracing::{
    dispatcher::SetGlobalDefaultError, subscriber::set_global_default,
};
use tracing_subscriber::{
    fmt, layer::SubscriberExt, EnvFilter, Layer, Registry,
};

/// Basic invocation. Simply call `jacklog::init(Some("mybin=debug"))` to
/// automatically set a default log level, that can be overridden via the usual
/// `RUST_LOG` env var.
///
/// All logs will go to STDERR. If jacklog detects that STDERR is NOT a TTY,
/// then logs will be output as JSON, ready to be parsed by log aggregators. If
/// STDERR IS a TTY, then logs will be output nicely formatted.
///
/// # Errors
///
/// Will return an error if there's an issue setting the global default subscriber.
pub fn init<T: AsRef<str>>(
    default_level: Option<&T>,
) -> Result<(), SetGlobalDefaultError> {
    let level_layer = parse_from_env(default_level);

    start(Some(level_layer))
}

/// Set the log level using 0-indexed integers, starting from 0=off. Especially
/// useful if writing a CLI and accepting verbosity settings via number of
/// flags (-v, -vv, -vvv, etc.).  Will be overridden via the usual `RUST_LOG`
/// env var, if it's present.
///
/// In what is probably the most common use case for this method, where a
/// repeating flag is used to increase verbosity, do the following math for the
/// behavior you want:
///
/// Default of warn, with increasing verbosity when number of -v arguments
/// is one or more:
///
/// 2 + (number of -v flags)
///
/// Default of no logging, with increasing verbosity when number of -v arguments
/// is one or more:
///
/// 0 + (number of -v flags)
///
/// Default of info, with increasing verbosity when number of -v arguments
/// is one or more:
///
/// 3 + (number of -v flags)
///
/// You probably get the idea.
///
/// # Errors
///
/// Will return an error if there's an issue setting the global subscriber.
pub fn from_level(
    level: u8,
    crates: Option<&[&str]>,
) -> Result<(), SetGlobalDefaultError> {
    // Figure out which filter to use.
    let level_layer = env_filter_from_level_and_crates(level, crates);

    start(Some(level_layer))
}

/// Encapsulate the common options we always use when starting the logging.
fn start(level_layer: Option<EnvFilter>) -> Result<(), SetGlobalDefaultError> {
    // If stderr is a tty, then we'll print nicely human-formatted,
    // colorized text.
    let format_layer = if atty::is(Stream::Stderr) {
        fmt::layer().with_ansi(true).with_writer(io::stderr).boxed()
    // Otherwise, we'll use uncolored JSON. This is opinionated, but hey, that's
    // what this crate is for.
    } else {
        fmt::layer().json().with_writer(io::stderr).boxed()
    };

    // Stack up all our layers.
    let subscriber = Registry::default().with(format_layer).with(level_layer);

    set_global_default(subscriber)
}

/// Figure out the level based on a 0-indexed hierarchy, with 0 being "off".
fn env_filter_from_level_and_crates(
    level: u8,
    crates: Option<&[&str]>,
) -> EnvFilter {
    // Convert the integer level to a string level name.
    let level = match level {
        0 => "off",
        1 => "error",
        2 => "warn",
        3 => "info",
        4 => "debug",
        _ => "trace",
    };

    let level = if let Some(crates) = crates {
        crates
            .iter()
            .map(|c| format!("{c}={level}"))
            .collect::<Vec<String>>()
            .join(",")
    } else {
        level.to_string()
    };

    // Set up the filter to use, defaulting to the level passed in.
    EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level))
}

fn parse_from_env<T: AsRef<str>>(default_level: Option<&T>) -> EnvFilter {
    // Set up the filter to use, defaulting to just the warn log level for all crates/modules.
    EnvFilter::try_from_default_env().unwrap_or_else(|_| {
        EnvFilter::new(default_level.map_or("warn", AsRef::as_ref))
    })
}