Skip to main content

function_sdk_rust/
logging.rs

1//! Logging setup for composition functions.
2
3use tracing_subscriber::filter::{LevelFilter, ParseError, Targets};
4use tracing_subscriber::layer::SubscriberExt;
5use tracing_subscriber::util::SubscriberInitExt;
6
7/// The gRPC, HTTP/2 and TLS crates under every function's server. Their
8/// DEBUG output (h2 logs every frame) buries the function's own, so
9/// `--debug` leaves them at INFO.
10const TRANSPORT_TARGETS: [&str; 6] = ["h2", "hyper", "hyper_util", "tonic", "tower", "rustls"];
11
12/// Configures process-wide logging to stderr. Call once, before serving.
13///
14/// Logs are JSON lines at info level. With debug enabled they are
15/// human-readable, at debug level for everything but the gRPC transport
16/// stack. `RUST_LOG`, when set, replaces the level selection in full. It
17/// takes comma-separated `target=level` directives and bare levels, such as
18/// `info,my_function=debug`; a `RUST_LOG` with span or field filters is
19/// ignored with a warning. To hold back more crates under debug, use
20/// [`Builder`].
21pub fn configure(debug: bool) {
22    Builder::new(debug).init();
23}
24
25/// Process-wide logging with options beyond [`configure`]. Create it with
26/// [`Builder::new`], chain the options, then call [`Builder::init`]. New
27/// options arrive as new methods, so code that builds logging keeps
28/// compiling.
29///
30/// ```no_run
31/// function_sdk_rust::logging::Builder::new(true)
32///     .quiet(["wasmtime", "cranelift_codegen"])
33///     .init();
34/// ```
35#[derive(Debug)]
36pub struct Builder {
37    debug: bool,
38    quiet: Vec<String>,
39}
40
41impl Builder {
42    /// Starts from [`configure`]'s defaults for the given `--debug` flag.
43    pub fn new(debug: bool) -> Self {
44        Self {
45            debug,
46            quiet: Vec::new(),
47        }
48    }
49
50    /// Keeps these tracing targets (usually crate names, such as
51    /// `wasmtime`) at INFO under debug, like the gRPC transport stack the
52    /// SDK already holds back. A `RUST_LOG` that is set replaces the level
53    /// selection, quiet targets included.
54    pub fn quiet<I, S>(mut self, targets: I) -> Self
55    where
56        I: IntoIterator<Item = S>,
57        S: Into<String>,
58    {
59        self.quiet.extend(targets.into_iter().map(Into::into));
60        self
61    }
62
63    /// Installs the global subscriber, writing to stderr. Call once,
64    /// before serving.
65    pub fn init(self) {
66        let rust_log = std::env::var("RUST_LOG").ok();
67        let (filter, ignored) = self.filter(rust_log.as_deref());
68        // The fmt builder caps events at INFO by default; the filter decides.
69        let builder = tracing_subscriber::fmt()
70            .with_max_level(LevelFilter::TRACE)
71            .with_writer(std::io::stderr)
72            .with_file(true)
73            .with_line_number(true);
74        if self.debug {
75            builder.finish().with(filter).init();
76        } else {
77            builder.json().finish().with(filter).init();
78        }
79        if let Some(error) = ignored {
80            tracing::warn!(%error, "ignoring RUST_LOG, which takes only target=level directives");
81        }
82    }
83
84    /// The level filter: `rust_log` when it is set, not blank and valid,
85    /// else INFO everywhere - or, under debug, DEBUG everywhere except the
86    /// transport stack and the quiet targets. Also returns why an invalid
87    /// `rust_log` was ignored.
88    fn filter(&self, rust_log: Option<&str>) -> (Targets, Option<ParseError>) {
89        let ignored = match rust_log.map(str::trim).filter(|s| !s.is_empty()) {
90            None => None,
91            Some(set) => match set.parse() {
92                Ok(filter) => return (filter, None),
93                Err(error) => Some(error),
94            },
95        };
96        if !self.debug {
97            return (Targets::new().with_default(LevelFilter::INFO), ignored);
98        }
99        let filter = TRANSPORT_TARGETS
100            .iter()
101            .copied()
102            .chain(self.quiet.iter().map(String::as_str))
103            .fold(
104                Targets::new().with_default(LevelFilter::DEBUG),
105                |filter, target| filter.with_target(target, LevelFilter::INFO),
106            );
107        (filter, ignored)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use tracing::Level;
115
116    fn enabled(builder: &Builder, rust_log: Option<&str>, target: &str, level: Level) -> bool {
117        builder.filter(rust_log).0.would_enable(target, &level)
118    }
119
120    #[test]
121    fn info_by_default() {
122        let builder = Builder::new(false);
123        for rust_log in [None, Some(" ")] {
124            assert!(enabled(&builder, rust_log, "my_function", Level::INFO));
125            assert!(!enabled(&builder, rust_log, "my_function", Level::DEBUG));
126        }
127    }
128
129    #[test]
130    fn debug_leaves_the_transport_at_info() {
131        let builder = Builder::new(true);
132        assert!(enabled(&builder, None, "my_function", Level::DEBUG));
133        for target in TRANSPORT_TARGETS {
134            let module = format!("{target}::proto");
135            assert!(!enabled(&builder, None, &module, Level::DEBUG), "{target}");
136            assert!(enabled(&builder, None, &module, Level::INFO), "{target}");
137        }
138    }
139
140    #[test]
141    fn quiet_targets_join_the_transport_under_debug() {
142        let builder = Builder::new(true).quiet(["wasmtime", "cranelift_codegen"]);
143        assert!(!enabled(&builder, None, "wasmtime::module", Level::DEBUG));
144        assert!(!enabled(&builder, None, "cranelift_codegen", Level::DEBUG));
145        assert!(enabled(&builder, None, "my_function", Level::DEBUG));
146        let builder = Builder::new(false).quiet(["wasmtime"]);
147        assert!(!enabled(&builder, None, "my_function", Level::DEBUG));
148    }
149
150    #[test]
151    fn rust_log_replaces_the_flag_and_the_quiet_targets() {
152        let builder = Builder::new(true).quiet(["wasmtime"]);
153        let rust_log = Some("info,my_function=trace,wasmtime=debug");
154        assert!(enabled(&builder, rust_log, "my_function", Level::TRACE));
155        assert!(enabled(&builder, rust_log, "wasmtime", Level::DEBUG));
156        assert!(!enabled(&builder, rust_log, "h2", Level::DEBUG));
157        assert!(builder.filter(rust_log).1.is_none());
158    }
159
160    #[test]
161    fn an_unsupported_rust_log_falls_back_to_the_flag() {
162        let builder = Builder::new(false);
163        let rust_log = Some("my_function[run{tag=x}]=debug");
164        let (filter, ignored) = builder.filter(rust_log);
165        assert!(ignored.is_some());
166        assert!(filter.would_enable("my_function", &Level::INFO));
167        assert!(!filter.would_enable("my_function", &Level::DEBUG));
168    }
169}