Skip to main content

lexe_logger/
lib.rs

1//! Common logger configuration for non-SGX lexe services.
2//!
3//! See also: the `logger` module in the `public/lexe-ln` crate for log config
4//! in enclaves.
5
6use std::{env, io, str::FromStr};
7
8use anyhow::anyhow;
9#[cfg(doc)]
10use lexe_api::trace::TraceId;
11use lexe_api::{define_trace_id_fns, trace};
12use tracing::{Level, level_filters::LevelFilter};
13use tracing_subscriber::{
14    Registry,
15    filter::{Filtered, Targets},
16    fmt::{
17        Layer as FmtLayer,
18        format::{Compact, DefaultFields, Format},
19    },
20    layer::{Layer as LayerTrait, Layered, SubscriberExt},
21    util::SubscriberInitExt,
22};
23
24/// Initialize a global `tracing` logger.
25///
26/// + The logger will print enabled `tracing` events and spans to stderr.
27/// + You can change the log level or module filtering with an appropriate
28///   `RUST_LOG` env var set. Read more about the syntax here:
29///   <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html>
30///
31/// Panics if a logger is already initialized. This will fail if used in tests,
32/// since multiple test threads will compete to set the global logger.
33pub fn init() {
34    try_init().expect("Failed to setup logger");
35}
36
37/// [`init`] but defaults to the given `RUST_LOG` value if not set in env.
38pub fn init_with_default(rust_log_default: &str) {
39    try_init_with_default(rust_log_default).expect("Failed to set up logger")
40}
41
42/// Use this to initialize the global logger in tests.
43#[cfg(any(test, feature = "test-utils"))]
44pub fn init_for_testing() {
45    // Don't panic if there's already a logger setup.
46    // Multiple tests might try setting the global logger.
47    let _ = try_init();
48}
49
50/// Try to initialize a global logger.
51/// Returns `Err` if another global logger is already set.
52pub fn try_init() -> anyhow::Result<()> {
53    // If `RUST_LOG` isn't set, use "off" to initialize a no-op subscriber so
54    // that all the `TraceId` infrastructure still works somewhat normally.
55    try_init_with_default("off")
56}
57
58/// [`try_init`] but defaults to the given `RUST_LOG` value if not set in env.
59pub fn try_init_with_default(rust_log_default: &str) -> anyhow::Result<()> {
60    let rust_log_env = env::var("RUST_LOG");
61    let rust_log = rust_log_env.as_deref().unwrap_or(rust_log_default);
62
63    subscriber(rust_log)
64        .try_init()
65        .context("Logger already initialized")?;
66
67    define_trace_id_fns!(SubscriberType);
68    trace::GET_TRACE_ID_FN
69        .set(get_trace_id_from_span)
70        .map_err(|_| anyhow!("GET_TRACE_ID_FN already set"))?;
71    trace::INSERT_TRACE_ID_FN
72        .set(insert_trace_id_into_span)
73        .map_err(|_| anyhow!("INSERT_TRACE_ID_FN already set"))?;
74
75    Ok(())
76}
77
78/// The full type of our subscriber which is downcasted to when recovering
79/// [`TraceId`]s. If having trouble naming this correctly, change this to some
80/// dummy value (e.g. `u32`) and the compiler will tell you what it should be.
81type SubscriberType = Layered<
82    Filtered<
83        FmtLayer<Registry, DefaultFields, Format<Compact>, fn() -> io::Stderr>,
84        Targets,
85        Registry,
86    >,
87    Registry,
88>;
89
90/// Generates our [`tracing::Subscriber`] impl by parsing a simplified target
91/// filter from the passed in RUST_LOG value. We parse the targets list manually
92/// because the `env_filter` brings in too many deps (like regex) for SGX.
93/// Defaults to INFO logs if we can't parse the targets filter.
94///
95/// This function is extracted so that we can check the correctness of the
96/// `SubscriberType` type alias, which allows us to downcast back to our
97/// subscriber to recover `TraceId`s.
98fn subscriber(rust_log: &str) -> SubscriberType {
99    // TODO(phlip9): non-blocking writer for prod
100    // see: https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/index.html
101    let targets = Targets::from_str(rust_log)
102        .inspect_err(|e| eprintln!("Invalid RUST_LOG; using INFO: {e}"))
103        .unwrap_or_else(|_| Targets::new().with_default(Level::INFO));
104
105    let clamped_targets =
106        if cfg!(any(test, debug_assertions, feature = "test-utils")) {
107            // Allow TRACE logs in tests / debug builds.
108            targets
109        } else {
110            // Disallow TRACE logs in production.
111            enforce_log_policy(targets)
112        };
113
114    let stderr_log = tracing_subscriber::fmt::Layer::default()
115        .compact()
116        .with_level(true)
117        .with_target(true)
118        .with_writer(io::stderr as fn() -> io::Stderr)
119        // Enable colored outputs.
120        // TODO(max): This should be disabled when outputting to files - a
121        //            second subscriber is probably needed.
122        .with_ansi(true)
123        .with_filter(clamped_targets);
124
125    tracing_subscriber::registry().with(stderr_log)
126}
127
128/// Disallows TRACE logs as a default or for any specific target.
129fn enforce_log_policy(targets: Targets) -> Targets {
130    /// Sets a level to DEBUG if it is currently specified as TRACE.
131    fn clamp_level(level: LevelFilter) -> LevelFilter {
132        if level == LevelFilter::TRACE {
133            LevelFilter::DEBUG
134        } else {
135            level
136        }
137    }
138
139    // Disallow TRACE. Set the default level to INFO if no default is set.
140    let clamped_default = match targets.default_level() {
141        Some(level) => clamp_level(level),
142        None => LevelFilter::INFO,
143    };
144
145    let targets = targets
146        .into_iter()
147        .map(|(target, level)| (target, clamp_level(level)))
148        .collect::<Targets>();
149
150    targets.with_default(clamped_default)
151}
152
153#[cfg(test)]
154mod test {
155    use std::env;
156
157    use lexe_api::trace::TraceId;
158
159    use super::*;
160
161    #[test]
162    fn get_and_insert_trace_ids() {
163        // The test won't work properly if RUST_LOG is empty or "off".
164        match env::var("RUST_LOG").ok() {
165            Some(v) if v.starts_with("off") => return,
166            Some(_) => (),
167            None => return,
168        }
169
170        init_for_testing();
171        TraceId::get_and_insert_test_impl();
172    }
173}