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}