log_fastly/
lib.rs

1//! Implementation of the [`log`] logging façade for Fastly Compute.
2//!
3//! With this logger configured, [`log`'s logging statements][log-use] will send log messages to
4//! your chosen [Real-Time Log Streaming][rtls] endpoints. You should initialize the logger as soon
5//! as your program starts. Logging statements will not do anything before initialization.
6//!
7//! See the [Fastly
8//! documentation](https://docs.fastly.com/en/guides/integrations#_logging-endpoints) for more
9//! information about configuring logging endpoints for your service.
10//!
11//! # Getting started
12//!
13//! All you need to get started is your endpoint name and the level of log messages you want to
14//! emit. For example, if you have an endpoint called `my_endpoint`, and you only want to emit log
15//! messages at the "Warn" or "Error" level, you can use [`init_simple()`]:
16//!
17//! ```no_run
18//! log_fastly::init_simple("my_endpoint", log::LevelFilter::Warn);
19//! log::warn!("This will be written to my_endpoint...");
20//! log::info!("...but this won't");
21//! ```
22//!
23//! # Advanced configuration
24//!
25//! For more precise control, including multiple endpoints and default endpoints for different
26//! logging levels, use the [`Builder`] interface. The first example is equivalent to:
27//!
28//! ```no_run
29//! log_fastly::Logger::builder()
30//!     .max_level(log::LevelFilter::Warn)
31//!     .default_endpoint("my_endpoint")
32//!     .init();
33//! ```
34//!
35//! The [`Builder::max_level()`] option sets the most verbose level of logging that will be
36//! emitted. Logging statements above that level, like `log::info!()` in the first example, will do
37//! nothing.
38//!
39//! **Note:** The default level is `LevelFilter::Off`, which emits no logging at all. You'll want to
40//! change this for most configurations.
41//!
42//! [`Builder::default_endpoint()`] sets the endpoint used whenever a logging statement is called
43//! without a `target` field to specify its endpoint. With the default endpoint set to
44//! `my_endpoint`, the logging statements in the first example are equivalent to:
45//!
46//! ```no_run
47//! log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
48//! log::info!(target: "my_endpoint", "...but this won't");
49//! ```
50//!
51//! ## Use with Compute Log Tailing
52//!
53//! [Compute Log Tailing][log-tailing] is helpful for getting debugging output quickly from a
54//! Compute program under development by capturing output from `stdout` or `stderr`. To
55//! configure logging to output to `stdout` or `stderr` in addition to the specified log endpoint,
56//! enable echoing when building the logger:
57//!
58//! ```no_run
59//! log_fastly::Logger::builder()
60//!     .max_level(log::LevelFilter::Warn)
61//!     .default_endpoint("my_endpoint")
62//!     .echo_stdout(true)
63//!     .init();
64//! ```
65//!
66//! [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
67//!
68//! ## Multiple endpoints
69//!
70//! Setting an endpoint as the default will automatically register it for use with the logger, but
71//! you can register additional endpoints with [`Builder::endpoint()`]:
72//!
73//! ```no_run
74//! log_fastly::Logger::builder()
75//!     .max_level(log::LevelFilter::Warn)
76//!     .default_endpoint("my_endpoint")
77//!     .endpoint("my_other_endpoint")
78//!     .init();
79//! log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
80//! log::warn!(target: "my_other_endpoint", "...but this will be written to my_other_endpoint");
81//! ```
82//!
83//! ## Per-endpoint logging levels
84//!
85//! You can also set a per-endpoint logging level, though levels higher than `max_level` are always
86//! ignored:
87//!
88//! ```no_run
89//! log_fastly::Logger::builder()
90//!     .max_level(log::LevelFilter::Warn)
91//!     .default_endpoint("my_endpoint")
92//!     .endpoint_level("my_other_endpoint", log::LevelFilter::Trace)
93//!     .endpoint_level("error_only", log::LevelFilter::Error)
94//!     .init();
95//! log::warn!(target: "my_other_endpoint", "This will be written to my_other_endpoint...");
96//! log::trace!(target: "my_other_endpoint", "...but this won't, because max_level wins");
97//! log::error!(target: "error_only", "This will be written to error_only...");
98//! log::warn!(target: "error_only", "...but this won't, because the endpoint's level is lower");
99//! ```
100//!
101//! ## Per-level default endpoints
102//!
103//! In the previous examples, the same endpoint is set as the default for all logging levels. You
104//! can also specify default endpoints for individual levels using
105//! [`Builder::default_level_endpoint()`]. The defaults are combined in order, so you can specify an
106//! overall default endpoint, and then as many level-specific endpoints as you need:
107//!
108//! ```no_run
109//! log_fastly::Logger::builder()
110//!     .max_level(log::LevelFilter::Info)
111//!     .default_endpoint("my_endpoint")
112//!     .default_level_endpoint("error_only", log::Level::Error)
113//!     .init();
114//! log::info!("This will be written to my_endpoint...");
115//! log::warn!(".. and this will too.");
116//! log::error!("But this will be written to error_only");
117//! ```
118//!
119//! ## Module name filters
120//!
121//! In addition to per-endpoint logging levels, you can set logging levels based on the name of the
122//! Rust module that contains the logging statement.
123//!
124//! No filtering is done based on the module name by default, but if any module name patterns are
125//! specified, only log statements that match one of the patterns will be emitted. For example, if
126//! your application is a crate called `my_app`, you can filter out any messages other than
127//! `my_app`'s :
128//!
129//! ```no_run
130//! # mod some_dependency { pub fn function_that_logs() {} }
131//! log_fastly::Logger::builder()
132//!     .max_level(log::LevelFilter::Info)
133//!     .default_endpoint("my_endpoint")
134//!     .filter_module("my_app", log::LevelFilter::Warn)
135//!     .init();
136//! log::warn!("This will be written to my_endpoint");
137//! // This won't emit any log messages, because no patterns match `some_dependency`
138//! some_dependency::function_that_logs();
139//! ```
140//!
141//! The filter expressions support the full syntax of the [`regex`][regex] crate. This is
142//! particularly useful if your patterns overlap, but you'd like to still treat them distinctly. For
143//! example, suppose you want to set the log level of the module `my_app::my_module` to be more
144//! restrictive than the top level `my_app` module. You can use `$` to make sure the
145//! less-restrictive pattern matches to the end of the module name, instead of matching any module
146//! name that contains `my_app`:
147//!
148//! ```no_run
149//! mod my_module {
150//!     pub fn do_a_thing() {
151//!         log::warn!("This won't be written, because this module's max level is Error");
152//!     }
153//! }
154//! log_fastly::Logger::builder()
155//!     .max_level(log::LevelFilter::Info)
156//!     .default_endpoint("my_endpoint")
157//!     .filter_module("my_app$", log::LevelFilter::Warn)
158//!     .filter_module("my_app::my_module", log::LevelFilter::Error)
159//!     .init();
160//! log::warn!("This will be written to my_endpoint");
161//! // This won't emit any log messages, because "my_app$" doesn't match, and "my_app::my_module"
162//! // is limited to Error
163//! my_module::do_a_thing();
164//! ```
165//!
166//! # Registering endpoints
167//!
168//! All endpoints used by your logging statements must be registered when the logger is created. The
169//! following functions automatically register an endpoint if it is not already registered.
170//!
171//! - [`init_simple()`]
172//! - [`Builder::endpoint()`]
173//! - [`Builder::endpoint_level()`]
174//! - [`Builder::default_endpoint()`]
175//! - [`Builder::default_level_endpoint()`]
176//!
177//! You can pass the endpoint name as a string, or an explicit [`fastly::log::Endpoint`] value. The
178//! following examples are equivalent:
179//!
180//! ```no_run
181//! log_fastly::init_simple("my_endpoint", log::LevelFilter::Info);
182//! ```
183//!
184//! ```no_run
185//! log_fastly::init_simple(
186//!     fastly::log::Endpoint::from_name("my_endpoint"),
187//!     log::LevelFilter::Info,
188//! );
189//! ```
190//!
191//! If a logging statement uses `target: "my_endpoint"` but `my_endpoint` is not registered, the
192//! message will be logged to the default endpoint for that level, if one exists.
193//!
194//! # Endpoint and module name collisions
195//!
196//! Due to a [limitation of `log`][log-issue], logging from a module with the same name as one of
197//! your endpoints will cause logs to be sent to that endpoint, even if no `target` is specified,
198//! and the endpoint is not a default. For example, if you have an endpoint named `my_app`, and your
199//! application is a crate also called `my_app`:
200//!
201//! ```no_run
202//! log_fastly::Logger::builder()
203//!     .max_level(log::LevelFilter::Info)
204//!     .default_endpoint("my_endpoint")
205//!     .endpoint("my_app")
206//!     .init();
207//! log::info!("This will be written to my_app, even though my_endpoint is the default");
208//! log::info!(
209//!     target: "my_endpoint",
210//!     "This will be written to my_endpoint, because the target is explicit",
211//! );
212//! ```
213//!
214//! We hope to address this issue in future releases, but the current workarounds are to either make
215//! sure your endpoint names and crate name are distinct, or use the explicit target syntax whenever
216//! logging from your top-level module.
217//!
218//! [log]: https://docs.rs/log
219//! [log-use]: https://docs.rs/log#use
220//! [rtls]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features
221//! [regex]: https://docs.rs/regex#syntax
222//! [log-issue]: https://github.com/rust-lang/log/issues/390
223
224// Warnings (other than unused variables) in doctests are promoted to errors.
225#![doc(test(attr(deny(warnings))))]
226#![doc(test(attr(allow(dead_code))))]
227#![doc(test(attr(allow(unused_variables))))]
228#![warn(missing_docs)]
229#![deny(rustdoc::broken_intra_doc_links)]
230#![deny(rustdoc::invalid_codeblock_attributes)]
231
232#[cfg(feature = "native-test-stubs")]
233mod native_test_stubs;
234#[cfg(feature = "native-test-stubs")]
235pub use native_test_stubs::*;
236
237#[cfg(not(feature = "native-test-stubs"))]
238use fastly::{
239    error::{anyhow, Error},
240    log::Endpoint,
241};
242use log::{Level, LevelFilter, Log, Metadata, Record};
243use regex::{RegexSet, RegexSetBuilder};
244use std::collections::HashMap;
245use std::io::Write;
246
247/// An implementation of the [`log::Log`] trait for Fastly Compute.
248///
249/// Create and initialize a `Logger` by using the builder returned by [`Logger::builder()`].
250#[derive(Debug)]
251pub struct Logger {
252    /// All of the endpoints that are registered with the logger.
253    endpoints: HashMap<String, (Endpoint, Option<LevelFilter>)>,
254    /// Default endpoints for each of the log levels.
255    default_endpoints: HashMap<Level, Endpoint>,
256    /// The maximum log level for all endpoints.
257    max_level: LevelFilter,
258    /// An optional matcher for each log level
259    module_filters: Option<HashMap<Level, RegexSet>>,
260    /// Whether to echo all log messages to `stdout`.
261    echo_stdout: bool,
262    /// Whether to echo all log messages to `stderr`.
263    echo_stderr: bool,
264}
265
266impl Log for Logger {
267    fn enabled(&self, metadata: &Metadata) -> bool {
268        self.lookup_endpoint(metadata, None).is_some()
269    }
270
271    fn log(&self, record: &Record) {
272        if let Some(endpoint) = self.lookup_endpoint(record.metadata(), record.module_path()) {
273            // Collapse the formatting of the message into a String, so that we only call the
274            // underlying `write` once. This prevents the message from being split into multiple
275            // lines in surprising ways, depending on how the format arguments are structured.
276            let msg = format!("{}", record.args());
277            // Given the type, we don't really have much of a choice about what to do if this goes
278            // wrong; panicking is probably not the right thing.
279            let _ = write!(endpoint.clone(), "{}", msg);
280            if self.echo_stdout {
281                println!("{}", msg);
282            }
283            if self.echo_stderr {
284                eprintln!("{}", msg);
285            }
286        }
287    }
288
289    fn flush(&self) {}
290}
291
292impl Logger {
293    /// Get a new [`Builder`].
294    pub fn builder() -> Builder {
295        Builder::new()
296    }
297
298    fn max_level(&self) -> LevelFilter {
299        self.max_level
300    }
301
302    /// Look up an endpoint for a log message.
303    ///
304    /// Returns `None` if no endpoint was found, or if the filter settings would prevent this
305    /// message from being emitted.
306    fn lookup_endpoint(&self, metadata: &Metadata, module_path: Option<&str>) -> Option<&Endpoint> {
307        let level = metadata.level();
308        // Immediately filter out any messages above the max level
309        if level > self.max_level {
310            return None;
311        }
312        if let Some(module_path) = module_path {
313            // If there is a module filter for this level, try to match it
314            if let Some(filter) = self.module_filters.as_ref().and_then(|fs| fs.get(&level)) {
315                if !filter.is_match(module_path) {
316                    return None;
317                }
318            }
319        }
320        // If the target matches an endpoint, apply the endpoint-specific filter
321        if let Some((endpoint, filter)) = self.endpoints.get(metadata.target()) {
322            if let Some(filter) = filter {
323                if level <= *filter {
324                    Some(endpoint)
325                } else {
326                    None
327                }
328            } else {
329                // If there is no filter, and the level didn't exceed the overall max, log it
330                Some(endpoint)
331            }
332        } else {
333            // For unrecognized targets, log it if there's a default endpoint for the level
334            self.default_endpoints.get(&level)
335        }
336    }
337}
338
339/// A builder type for [`Logger`].
340///
341/// You can use this builder to register endpoints, set default endpoints, control the levels of
342/// logging messages emitted, and filter messages based on module name.
343///
344/// A `Builder` is consumed by calling [`Builder::init()`] or [`Builder::try_init()`] to build and
345/// automatically initialize the logger, or by calling [`Builder::build()`] to build the logger for
346/// manual initialization or nesting within another logger. After you call any of these methods, you
347/// can no longer call any of them on that same builder.
348#[derive(Debug)]
349pub struct Builder {
350    inner: Result<Inner, Error>,
351}
352
353#[derive(Debug)]
354struct Inner {
355    /// All of the endpoints that are registered with the logger.
356    endpoints: HashMap<String, (Endpoint, Option<LevelFilter>)>,
357    /// Default endpoints for each of the log levels.
358    default_endpoints: HashMap<Level, Endpoint>,
359    /// The maximum log level for all endpoints.
360    max_level: LevelFilter,
361    /// The module patterns we will match for each log level
362    module_filters: HashMap<LevelFilter, Vec<String>>,
363    /// Whether to echo all log messages to `stdout`.
364    echo_stdout: bool,
365    /// Whether to echo all log messages to `stderr`.
366    echo_stderr: bool,
367}
368
369impl Default for Builder {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375impl Builder {
376    /// Create a new `Builder`.
377    ///
378    /// By default, no endpoints are registered, the maximum log level is set to `Off`, and no
379    /// module name filtering is done.
380    pub fn new() -> Self {
381        let inner = Inner {
382            endpoints: HashMap::new(),
383            default_endpoints: HashMap::new(),
384            max_level: LevelFilter::Off,
385            module_filters: HashMap::new(),
386            echo_stdout: false,
387            echo_stderr: false,
388        };
389        Self { inner: Ok(inner) }
390    }
391
392    /// Run a closure on the inner field, if it exists.
393    fn with_inner<F: FnOnce(&mut Inner) -> R, R>(&mut self, f: F) {
394        // It's fine not to use this result, because we're deferring errors to `build()`
395        let _ = self.inner.as_mut().map(f);
396    }
397
398    /// Run a closure on the inner field if it exists, and a fallible argument.
399    ///
400    /// If the argument is an `Err`, this replaces `inner` with that `Err`.
401    fn with_inner_and_then<A, F, R, E>(&mut self, arg: Result<A, E>, f: F)
402    where
403        F: FnOnce(&mut Inner, A) -> R,
404        E: Into<Error>,
405    {
406        match arg {
407            Ok(x) => self.with_inner(|i| f(i, x)),
408            Err(e) => self.inner = Err(e.into()),
409        }
410    }
411
412    /// Register an endpoint.
413    ///
414    /// ```no_run
415    /// log_fastly::Logger::builder()
416    ///     .max_level(log::LevelFilter::Trace)
417    ///     .endpoint("my_endpoint")
418    ///     .init();
419    /// log::info!(target: "my_endpoint", "Hello");
420    /// ```
421    pub fn endpoint<E>(&mut self, endpoint: E) -> &mut Self
422    where
423        E: TryInto<Endpoint>,
424        <E as TryInto<Endpoint>>::Error: Into<Error>,
425    {
426        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
427            i.endpoints
428                .insert(endpoint.name().to_owned(), (endpoint, None))
429        });
430        self
431    }
432
433    /// Register an endpoint and set the maximum logging level for its messages.
434    ///
435    /// ```no_run
436    /// log_fastly::Logger::builder()
437    ///     .max_level(log::LevelFilter::Trace)
438    ///     .endpoint_level("debug_endpoint", log::LevelFilter::Debug)
439    ///     .init();
440    /// log::info!(target: "debug_endpoint", "This will be written to debug_endpoint...");
441    /// log::trace!(target: "debug_endpoint", "...but this won't be...");
442    /// ```
443    pub fn endpoint_level<E>(&mut self, endpoint: E, level: LevelFilter) -> &mut Self
444    where
445        E: TryInto<Endpoint>,
446        <E as TryInto<Endpoint>>::Error: Into<Error>,
447    {
448        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
449            i.endpoints
450                .insert(endpoint.name().to_owned(), (endpoint, Some(level)))
451        });
452        self
453    }
454
455    /// Set the default endpoint for all messages.
456    ///
457    /// The default endpoint is used when the logging statement does not use the `target:
458    /// "endpoint"` syntax.
459    ///
460    /// This overrides any previous default endpoints, set either by this method or by
461    /// [`Builder::default_level_endpoint()`].
462    ///
463    /// ```no_run
464    /// log_fastly::Logger::builder()
465    ///     .max_level(log::LevelFilter::Info)
466    ///     .default_level_endpoint("error_only", log::Level::Error)
467    ///     .default_endpoint("my_endpoint")
468    ///     .endpoint("other_endpoint")
469    ///     .init();
470    /// log::info!("This will be written to my_endpoint...");
471    /// log::error!("...and this will too");
472    /// log::warn!(target: "other_endpoint", "This will go to other_endpoint, though");
473    /// ```
474    pub fn default_endpoint<E>(&mut self, endpoint: E) -> &mut Self
475    where
476        E: TryInto<Endpoint>,
477        <E as TryInto<Endpoint>>::Error: Into<Error>,
478    {
479        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
480            for level in &[
481                Level::Error,
482                Level::Warn,
483                Level::Info,
484                Level::Debug,
485                Level::Trace,
486            ] {
487                i.default_endpoints.insert(*level, endpoint.clone());
488            }
489        });
490        self
491    }
492
493    /// Set the default endpoint for all messages of the given level.
494    ///
495    /// The default endpoint is used when the logging statement does not use the `target:
496    /// "endpoint"` syntax.
497    ///
498    /// This overrides any previous default endpoints set for this level, either by this method or
499    /// by [`Builder::default_endpoint()`].
500    ///
501    /// ```no_run
502    /// log_fastly::Logger::builder()
503    ///     .max_level(log::LevelFilter::Info)
504    ///     .default_endpoint("my_endpoint")
505    ///     .default_level_endpoint("error_only", log::Level::Error)
506    ///     .endpoint("other_endpoint")
507    ///     .init();
508    /// log::info!("This will be written to my_endpoint...");
509    /// log::error!("...but this will be written to error_only");
510    /// log::error!(target: "other_endpoint", "This will go to other_endpoint, though");
511    /// ```
512    pub fn default_level_endpoint<E>(&mut self, endpoint: E, level: Level) -> &mut Self
513    where
514        E: TryInto<Endpoint>,
515        <E as TryInto<Endpoint>>::Error: Into<Error>,
516    {
517        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
518            i.default_endpoints.insert(level, endpoint.clone());
519            i.endpoints
520                .insert(endpoint.name().to_owned(), (endpoint, None))
521        });
522        self
523    }
524
525    /// Set the maximum logging level for all messages.
526    ///
527    /// No messages that exceed this level will be emitted, even if a higher level is set for a
528    /// specific endpoint or module name.
529    ///
530    /// **Note:** The default level is `LevelFilter::Off`, which emits no logging at all. You'll
531    /// want to change this for most configurations.
532    ///
533    /// ```no_run
534    /// log_fastly::Builder::new()
535    ///     .max_level(log::LevelFilter::Warn)
536    ///     .endpoint_level("my_endpoint", log::LevelFilter::Info)
537    ///     .init();
538    /// log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
539    /// log::info!(target: "my_endpoint", "...but this won't");
540    /// ```
541    pub fn max_level(&mut self, level: LevelFilter) -> &mut Self {
542        self.with_inner(|i| i.max_level = level);
543        self
544    }
545
546    /// Set a logging level for modules whose names match the given [regular
547    /// expression][regex-syntax].
548    ///
549    /// By default, logging statements in any module are emitted if their level is within
550    /// [`Builder::max_level()`] and the level for their target endpoint. If you configure filters
551    /// with this method, the name of the module calling the logging statement must also match one
552    /// of the patterns, and be within the filter's specified level.
553    ///
554    /// ```no_run
555    /// mod my_module {
556    ///     pub fn do_a_thing() {
557    ///         log::warn!("This won't be written, because this module's max level is Error...");
558    ///         log::error!("...but this will be written");
559    ///     }
560    /// }
561    ///
562    /// log_fastly::Logger::builder()
563    ///     .max_level(log::LevelFilter::Info)
564    ///     .default_endpoint("my_endpoint")
565    ///     .filter_module("my_module", log::LevelFilter::Error)
566    ///     .init();
567    /// log::info!("This won't be written, because it's not in my_module");
568    /// // This will only emit one log message, because "my_app$" doesn't match, and
569    /// // "my_app::my_module" is limited to Error
570    /// my_module::do_a_thing();
571    /// ```
572    ///
573    /// [regex-syntax]: https://docs.rs/regex#syntax
574    pub fn filter_module(&mut self, regex: &str, level: LevelFilter) -> &mut Self {
575        self.with_inner(|i| {
576            i.module_filters
577                .entry(level)
578                .or_insert_with(|| vec![])
579                .push(regex.to_owned())
580        });
581        self
582    }
583
584    /// Set whether all log messages should be echoed to `stdout` (`false` by default).
585    ///
586    /// If this is set to `true`, all logging statements will write the message to `stdout` in
587    /// addition to the specified endpoint. This is particularly useful when debugging with
588    /// [Compute Log Tailing][log-tailing].
589    ///
590    /// [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
591    pub fn echo_stdout(&mut self, enabled: bool) -> &mut Self {
592        self.with_inner(|i| i.echo_stdout = enabled);
593        self
594    }
595
596    /// Set whether all log messages should be echoed to `stderr` (`false` by default).
597    ///
598    /// If this is set to `true`, all logging statements will write the message to `stderr` in
599    /// addition to the specified endpoint. This is particularly useful when debugging with
600    /// [Compute Log Tailing][log-tailing].
601    ///
602    /// [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
603    pub fn echo_stderr(&mut self, enabled: bool) -> &mut Self {
604        self.with_inner(|i| i.echo_stderr = enabled);
605        self
606    }
607
608    /// Build the logger and initialize it as the global logger.
609    ///
610    /// ```no_run
611    /// log_fastly::Builder::new()
612    ///     .default_endpoint("my_endpoint")
613    ///     .init();
614    /// log::info!("Hello");
615    /// ```
616    ///
617    /// # Panics
618    ///
619    /// This may panic for any of the reasons that [`Builder::try_init()`] would return an error.
620    pub fn init(&mut self) {
621        self.try_init().expect("log_fastly::Builder::init() failed")
622    }
623
624    /// Build the logger and initialize it as the global logger.
625    ///
626    /// This will fail with an `Err` value if a global logger is already initialized, or for any of
627    /// the reasons that [`Builder::build()`] can fail.
628    ///
629    /// ```no_run
630    /// log_fastly::Builder::new()
631    ///     .default_endpoint("my_endpoint")
632    ///     .try_init()
633    ///     .unwrap();
634    /// log::info!("Hello");
635    /// ```
636    pub fn try_init(&mut self) -> Result<(), Error> {
637        let logger = self.build()?;
638
639        // Get the overall max level while we still own the logger
640        let max_level = logger.max_level();
641
642        // Now try to install the logger
643        let res = log::set_boxed_logger(Box::new(logger));
644
645        if res.is_ok() {
646            // And finally set the max level in the global `log` state, if we successfully installed
647            // the logger
648            log::set_max_level(max_level);
649        }
650
651        res.map_err(Into::into)
652    }
653
654    /// Build a `Logger`, using up this builder.
655    ///
656    /// This is mainly useful if you want to manually initialize the logger, or nest it within
657    /// another [`log::Log`] implementation.
658    ///
659    /// This will fail with an `Err` value if:
660    ///
661    /// - This builder has already been used to build a `Logger`.
662    ///
663    /// - Any of the registered endpoint names are invalid.
664    ///
665    /// - Any of the module name filters set by [`Builder::filter_module()`] have invalid [regex
666    /// syntax][regex-syntax].
667    ///
668    /// ```no_run
669    /// let logger = log_fastly::Builder::new()
670    ///     .default_endpoint("my_endpoint")
671    ///     .build()
672    ///     .unwrap();
673    /// log::set_boxed_logger(Box::new(logger)).unwrap();
674    /// ```
675    ///
676    /// [regex-syntax]: https://docs.rs/regex#syntax
677    pub fn build(&mut self) -> Result<Logger, Error> {
678        let inner = std::mem::replace(
679            &mut self.inner,
680            Err(anyhow!("Builder can only be built once")),
681        )?;
682
683        // Calculate the overall max logging level, taking into account all the endpoint-specific
684        // and default filters.
685
686        // First, find the max endpoint-specific log level.
687        let endpoint_max = inner
688            .endpoints
689            .values()
690            // If an endpoint doesn't have a filter, it's only constrained by the overall
691            // `max_level`.
692            .map(|e| e.1.unwrap_or_else(LevelFilter::max))
693            .max()
694            .unwrap_or(LevelFilter::Off);
695        // Then, find the highest log level that has a default logger.
696        let default_max = inner
697            .default_endpoints
698            .keys()
699            .max()
700            .map(Level::to_level_filter)
701            .unwrap_or(LevelFilter::Off);
702        // Take whichever of these is bigger, but then clamp it with our `max_level` which takes
703        // precedence.
704        let max_level = std::cmp::min(inner.max_level, std::cmp::max(endpoint_max, default_max));
705
706        let module_filters = generate_module_filters(inner.module_filters)?;
707
708        Ok(Logger {
709            endpoints: inner.endpoints,
710            default_endpoints: inner.default_endpoints,
711            max_level,
712            module_filters,
713            echo_stdout: inner.echo_stdout,
714            echo_stderr: inner.echo_stderr,
715        })
716    }
717}
718
719/// Build a [`RegexSet`] for each logging level from the specified filters.
720fn generate_module_filters(
721    mut regex_map: HashMap<LevelFilter, Vec<String>>,
722) -> Result<Option<HashMap<Level, RegexSet>>, Error> {
723    if regex_map.is_empty() {
724        Ok(None)
725    } else {
726        // build from highest level to lowest, extending the set as we go so that each set contains
727        // all of the regexes specified for the levels above
728        let levels = [
729            LevelFilter::Trace,
730            LevelFilter::Debug,
731            LevelFilter::Info,
732            LevelFilter::Warn,
733            LevelFilter::Error,
734        ];
735        let mut module_filters = HashMap::with_capacity(levels.len());
736        let mut running_regexes = vec![];
737        for level in levels.iter() {
738            if let Some(regexes) = regex_map.remove(&level) {
739                running_regexes.extend(regexes);
740            }
741            let matcher = RegexSetBuilder::new(running_regexes.iter()).build()?;
742            let level = level
743                .to_level()
744                .expect("only iterating on LevelFilters that match a Level");
745            module_filters.insert(level, matcher);
746        }
747        Ok(Some(module_filters))
748    }
749}
750
751/// Initialize logging with a single endpoint filtered by log level.
752///
753/// For advanced configuration, see the [`Builder`] type, and the [module-level
754/// documentation](index.html#advanced-configuration).
755///
756/// ```no_run
757/// log_fastly::init_simple("my_endpoint", log::LevelFilter::Warn);
758/// log::warn!("This will be written to my_endpoint...");
759/// log::info!("...but this won't");
760/// ```
761pub fn init_simple<E>(endpoint: E, level: LevelFilter)
762where
763    E: TryInto<Endpoint>,
764    <E as TryInto<Endpoint>>::Error: Into<Error>,
765{
766    Logger::builder()
767        .default_endpoint(endpoint)
768        .max_level(level)
769        .try_init()
770        .expect("log_fastly::init_simple() failed");
771}