log-fastly 0.9.1

Implementation of the `log` façade for Fastly Compute@Edge
Documentation
//! Implementation of the [`log`] logging façade for Fastly Compute@Edge.
//!
//! With this logger configured, [`log`'s logging statements][log-use] will send log messages to
//! your chosen [Real-Time Log Streaming][rtls] endpoints. You should initialize the logger as soon
//! as your program starts. Logging statements will not do anything before initialization.
//!
//! See the [Fastly
//! documentation](https://docs.fastly.com/en/guides/integrations#_logging-endpoints) for more
//! information about configuring logging endpoints for your service.
//!
//! # Getting started
//!
//! All you need to get started is your endpoint name and the level of log messages you want to
//! emit. For example, if you have an endpoint called `my_endpoint`, and you only want to emit log
//! messages at the "Warn" or "Error" level, you can use [`init_simple()`]:
//!
//! ```no_run
//! log_fastly::init_simple("my_endpoint", log::LevelFilter::Warn);
//! log::warn!("This will be written to my_endpoint...");
//! log::info!("...but this won't");
//! ```
//!
//! # Advanced configuration
//!
//! For more precise control, including multiple endpoints and default endpoints for different
//! logging levels, use the [`Builder`] interface. The first example is equivalent to:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Warn)
//!     .default_endpoint("my_endpoint")
//!     .init();
//! ```
//!
//! The [`Builder::max_level()`] option sets the most verbose level of logging that will be
//! emitted. Logging statements above that level, like `log::info!()` in the first example, will do
//! nothing.
//!
//! **Note:** The default level is `LevelFilter::Off`, which emits no logging at all. You'll want to
//! change this for most configurations.
//!
//! [`Builder::default_endpoint()`] sets the endpoint used whenever a logging statement is called
//! without a `target` field to specify its endpoint. With the default endpoint set to
//! `my_endpoint`, the logging statements in the first example are equivalent to:
//!
//! ```no_run
//! log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
//! log::info!(target: "my_endpoint", "...but this won't");
//! ```
//!
//! ## Use with Compute@Edge Log Tailing
//!
//! [Compute@Edge Log Tailing][log-tailing] is helpful for getting debugging output quickly from a
//! Compute@Edge program under development by capturing output from `stdout` or `stderr`. To
//! configure logging to output to `stdout` or `stderr` in addition to the specified log endpoint,
//! enable echoing when building the logger:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Warn)
//!     .default_endpoint("my_endpoint")
//!     .echo_stdout(true)
//!     .init();
//! ```
//!
//! [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
//!
//! ## Multiple endpoints
//!
//! Setting an endpoint as the default will automatically register it for use with the logger, but
//! you can register additional endpoints with [`Builder::endpoint()`]:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Warn)
//!     .default_endpoint("my_endpoint")
//!     .endpoint("my_other_endpoint")
//!     .init();
//! log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
//! log::warn!(target: "my_other_endpoint", "...but this will be written to my_other_endpoint");
//! ```
//!
//! ## Per-endpoint logging levels
//!
//! You can also set a per-endpoint logging level, though levels higher than `max_level` are always
//! ignored:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Warn)
//!     .default_endpoint("my_endpoint")
//!     .endpoint_level("my_other_endpoint", log::LevelFilter::Trace)
//!     .endpoint_level("error_only", log::LevelFilter::Error)
//!     .init();
//! log::warn!(target: "my_other_endpoint", "This will be written to my_other_endpoint...");
//! log::trace!(target: "my_other_endpoint", "...but this won't, because max_level wins");
//! log::error!(target: "error_only", "This will be written to error_only...");
//! log::warn!(target: "error_only", "...but this won't, because the endpoint's level is lower");
//! ```
//!
//! ## Per-level default endpoints
//!
//! In the previous examples, the same endpoint is set as the default for all logging levels. You
//! can also specify default endpoints for individual levels using
//! [`Builder::default_level_endpoint()`]. The defaults are combined in order, so you can specify an
//! overall default endpoint, and then as many level-specific endpoints as you need:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Info)
//!     .default_endpoint("my_endpoint")
//!     .default_level_endpoint("error_only", log::Level::Error)
//!     .init();
//! log::info!("This will be written to my_endpoint...");
//! log::warn!(".. and this will too.");
//! log::error!("But this will be written to error_only");
//! ```
//!
//! ## Module name filters
//!
//! In addition to per-endpoint logging levels, you can set logging levels based on the name of the
//! Rust module that contains the logging statement.
//!
//! No filtering is done based on the module name by default, but if any module name patterns are
//! specified, only log statements that match one of the patterns will be emitted. For example, if
//! your application is a crate called `my_app`, you can filter out any messages other than
//! `my_app`'s :
//!
//! ```no_run
//! # mod some_dependency { pub fn function_that_logs() {} }
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Info)
//!     .default_endpoint("my_endpoint")
//!     .filter_module("my_app", log::LevelFilter::Warn)
//!     .init();
//! log::warn!("This will be written to my_endpoint");
//! // This won't emit any log messages, because no patterns match `some_dependency`
//! some_dependency::function_that_logs();
//! ```
//!
//! The filter expressions support the full syntax of the [`regex`][regex] crate. This is
//! particularly useful if your patterns overlap, but you'd like to still treat them distinctly. For
//! example, suppose you want to set the log level of the module `my_app::my_module` to be more
//! restrictive than the top level `my_app` module. You can use `$` to make sure the
//! less-restrictive pattern matches to the end of the module name, instead of matching any module
//! name that contains `my_app`:
//!
//! ```no_run
//! mod my_module {
//!     pub fn do_a_thing() {
//!         log::warn!("This won't be written, because this module's max level is Error");
//!     }
//! }
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Info)
//!     .default_endpoint("my_endpoint")
//!     .filter_module("my_app$", log::LevelFilter::Warn)
//!     .filter_module("my_app::my_module", log::LevelFilter::Error)
//!     .init();
//! log::warn!("This will be written to my_endpoint");
//! // This won't emit any log messages, because "my_app$" doesn't match, and "my_app::my_module"
//! // is limited to Error
//! my_module::do_a_thing();
//! ```
//!
//! # Registering endpoints
//!
//! All endpoints used by your logging statements must be registered when the logger is created. The
//! following functions automatically register an endpoint if it is not already registered.
//!
//! - [`init_simple()`]
//! - [`Builder::endpoint()`]
//! - [`Builder::endpoint_level()`]
//! - [`Builder::default_endpoint()`]
//! - [`Builder::default_level_endpoint()`]
//!
//! You can pass the endpoint name as a string, or an explicit [`fastly::log::Endpoint`] value. The
//! following examples are equivalent:
//!
//! ```no_run
//! log_fastly::init_simple("my_endpoint", log::LevelFilter::Info);
//! ```
//!
//! ```no_run
//! log_fastly::init_simple(
//!     fastly::log::Endpoint::from_name("my_endpoint"),
//!     log::LevelFilter::Info,
//! );
//! ```
//!
//! If a logging statement uses `target: "my_endpoint"` but `my_endpoint` is not registered, the
//! message will be logged to the default endpoint for that level, if one exists.
//!
//! # Endpoint and module name collisions
//!
//! Due to a [limitation of `log`][log-issue], logging from a module with the same name as one of
//! your endpoints will cause logs to be sent to that endpoint, even if no `target` is specified,
//! and the endpoint is not a default. For example, if you have an endpoint named `my_app`, and your
//! application is a crate also called `my_app`:
//!
//! ```no_run
//! log_fastly::Logger::builder()
//!     .max_level(log::LevelFilter::Info)
//!     .default_endpoint("my_endpoint")
//!     .endpoint("my_app")
//!     .init();
//! log::info!("This will be written to my_app, even though my_endpoint is the default");
//! log::info!(
//!     target: "my_endpoint",
//!     "This will be written to my_endpoint, because the target is explicit",
//! );
//! ```
//!
//! We hope to address this issue in future releases, but the current workarounds are to either make
//! sure your endpoint names and crate name are distinct, or use the explicit target syntax whenever
//! logging from your top-level module.
//!
//! [log]: https://docs.rs/log
//! [log-use]: https://docs.rs/log#use
//! [rtls]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features
//! [regex]: https://docs.rs/regex#syntax
//! [log-issue]: https://github.com/rust-lang/log/issues/390

// Warnings (other than unused variables) in doctests are promoted to errors.
#![doc(test(attr(deny(warnings))))]
#![doc(test(attr(allow(dead_code))))]
#![doc(test(attr(allow(unused_variables))))]
#![warn(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::invalid_codeblock_attributes)]

#[cfg(feature = "native-test-stubs")]
mod native_test_stubs;
#[cfg(feature = "native-test-stubs")]
pub use native_test_stubs::*;

#[cfg(not(feature = "native-test-stubs"))]
use fastly::{
    error::{anyhow, Error},
    log::Endpoint,
};
use log::{Level, LevelFilter, Log, Metadata, Record};
use regex::{RegexSet, RegexSetBuilder};
use std::collections::HashMap;
use std::io::Write;

/// An implementation of the [`log::Log`] trait for Fastly Compute@Edge.
///
/// Create and initialize a `Logger` by using the builder returned by [`Logger::builder()`].
#[derive(Debug)]
pub struct Logger {
    /// All of the endpoints that are registered with the logger.
    endpoints: HashMap<String, (Endpoint, Option<LevelFilter>)>,
    /// Default endpoints for each of the log levels.
    default_endpoints: HashMap<Level, Endpoint>,
    /// The maximum log level for all endpoints.
    max_level: LevelFilter,
    /// An optional matcher for each log level
    module_filters: Option<HashMap<Level, RegexSet>>,
    /// Whether to echo all log messages to `stdout`.
    echo_stdout: bool,
    /// Whether to echo all log messages to `stderr`.
    echo_stderr: bool,
}

impl Log for Logger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        self.lookup_endpoint(metadata, None).is_some()
    }

    fn log(&self, record: &Record) {
        if let Some(endpoint) = self.lookup_endpoint(record.metadata(), record.module_path()) {
            // Collapse the formatting of the message into a String, so that we only call the
            // underlying `write` once. This prevents the message from being split into multiple
            // lines in surprising ways, depending on how the format arguments are structured.
            let msg = format!("{}", record.args());
            // Given the type, we don't really have much of a choice about what to do if this goes
            // wrong; panicking is probably not the right thing.
            let _ = write!(endpoint.clone(), "{}", msg);
            if self.echo_stdout {
                println!("{}", msg);
            }
            if self.echo_stderr {
                eprintln!("{}", msg);
            }
        }
    }

    fn flush(&self) {}
}

impl Logger {
    /// Get a new [`Builder`].
    pub fn builder() -> Builder {
        Builder::new()
    }

    fn max_level(&self) -> LevelFilter {
        self.max_level
    }

    /// Look up an endpoint for a log message.
    ///
    /// Returns `None` if no endpoint was found, or if the filter settings would prevent this
    /// message from being emitted.
    fn lookup_endpoint(&self, metadata: &Metadata, module_path: Option<&str>) -> Option<&Endpoint> {
        let level = metadata.level();
        // Immediately filter out any messages above the max level
        if level > self.max_level {
            return None;
        }
        if let Some(module_path) = module_path {
            // If there is a module filter for this level, try to match it
            if let Some(filter) = self.module_filters.as_ref().and_then(|fs| fs.get(&level)) {
                if !filter.is_match(module_path) {
                    return None;
                }
            }
        }
        // If the target matches an endpoint, apply the endpoint-specific filter
        if let Some((endpoint, filter)) = self.endpoints.get(metadata.target()) {
            if let Some(filter) = filter {
                if level <= *filter {
                    Some(endpoint)
                } else {
                    None
                }
            } else {
                // If there is no filter, and the level didn't exceed the overall max, log it
                Some(endpoint)
            }
        } else {
            // For unrecognized targets, log it if there's a default endpoint for the level
            self.default_endpoints.get(&level)
        }
    }
}

/// A builder type for [`Logger`].
///
/// You can use this builder to register endpoints, set default endpoints, control the levels of
/// logging messages emitted, and filter messages based on module name.
///
/// A `Builder` is consumed by calling [`Builder::init()`] or [`Builder::try_init()`] to build and
/// automatically initialize the logger, or by calling [`Builder::build()`] to build the logger for
/// manual initialization or nesting within another logger. After you call any of these methods, you
/// can no longer call any of them on that same builder.
#[derive(Debug)]
pub struct Builder {
    inner: Result<Inner, Error>,
}

#[derive(Debug)]
struct Inner {
    /// All of the endpoints that are registered with the logger.
    endpoints: HashMap<String, (Endpoint, Option<LevelFilter>)>,
    /// Default endpoints for each of the log levels.
    default_endpoints: HashMap<Level, Endpoint>,
    /// The maximum log level for all endpoints.
    max_level: LevelFilter,
    /// The module patterns we will match for each log level
    module_filters: HashMap<LevelFilter, Vec<String>>,
    /// Whether to echo all log messages to `stdout`.
    echo_stdout: bool,
    /// Whether to echo all log messages to `stderr`.
    echo_stderr: bool,
}

impl Default for Builder {
    fn default() -> Self {
        Self::new()
    }
}

impl Builder {
    /// Create a new `Builder`.
    ///
    /// By default, no endpoints are registered, the maximum log level is set to `Off`, and no
    /// module name filtering is done.
    pub fn new() -> Self {
        let inner = Inner {
            endpoints: HashMap::new(),
            default_endpoints: HashMap::new(),
            max_level: LevelFilter::Off,
            module_filters: HashMap::new(),
            echo_stdout: false,
            echo_stderr: false,
        };
        Self { inner: Ok(inner) }
    }

    /// Run a closure on the inner field, if it exists.
    fn with_inner<F: FnOnce(&mut Inner) -> R, R>(&mut self, f: F) {
        // It's fine not to use this result, because we're deferring errors to `build()`
        let _ = self.inner.as_mut().map(f);
    }

    /// Run a closure on the inner field if it exists, and a fallible argument.
    ///
    /// If the argument is an `Err`, this replaces `inner` with that `Err`.
    fn with_inner_and_then<A, F, R, E>(&mut self, arg: Result<A, E>, f: F)
    where
        F: FnOnce(&mut Inner, A) -> R,
        E: Into<Error>,
    {
        match arg {
            Ok(x) => self.with_inner(|i| f(i, x)),
            Err(e) => self.inner = Err(e.into()),
        }
    }

    /// Register an endpoint.
    ///
    /// ```no_run
    /// log_fastly::Logger::builder()
    ///     .max_level(log::LevelFilter::Trace)
    ///     .endpoint("my_endpoint")
    ///     .init();
    /// log::info!(target: "my_endpoint", "Hello");
    /// ```
    pub fn endpoint<E>(&mut self, endpoint: E) -> &mut Self
    where
        E: TryInto<Endpoint>,
        <E as TryInto<Endpoint>>::Error: Into<Error>,
    {
        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
            i.endpoints
                .insert(endpoint.name().to_owned(), (endpoint, None))
        });
        self
    }

    /// Register an endpoint and set the maximum logging level for its messages.
    ///
    /// ```no_run
    /// log_fastly::Logger::builder()
    ///     .max_level(log::LevelFilter::Trace)
    ///     .endpoint_level("debug_endpoint", log::LevelFilter::Debug)
    ///     .init();
    /// log::info!(target: "debug_endpoint", "This will be written to debug_endpoint...");
    /// log::trace!(target: "debug_endpoint", "...but this won't be...");
    /// ```
    pub fn endpoint_level<E>(&mut self, endpoint: E, level: LevelFilter) -> &mut Self
    where
        E: TryInto<Endpoint>,
        <E as TryInto<Endpoint>>::Error: Into<Error>,
    {
        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
            i.endpoints
                .insert(endpoint.name().to_owned(), (endpoint, Some(level)))
        });
        self
    }

    /// Set the default endpoint for all messages.
    ///
    /// The default endpoint is used when the logging statement does not use the `target:
    /// "endpoint"` syntax.
    ///
    /// This overrides any previous default endpoints, set either by this method or by
    /// [`Builder::default_level_endpoint()`].
    ///
    /// ```no_run
    /// log_fastly::Logger::builder()
    ///     .max_level(log::LevelFilter::Info)
    ///     .default_level_endpoint("error_only", log::Level::Error)
    ///     .default_endpoint("my_endpoint")
    ///     .endpoint("other_endpoint")
    ///     .init();
    /// log::info!("This will be written to my_endpoint...");
    /// log::error!("...and this will too");
    /// log::warn!(target: "other_endpoint", "This will go to other_endpoint, though");
    /// ```
    pub fn default_endpoint<E>(&mut self, endpoint: E) -> &mut Self
    where
        E: TryInto<Endpoint>,
        <E as TryInto<Endpoint>>::Error: Into<Error>,
    {
        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
            for level in &[
                Level::Error,
                Level::Warn,
                Level::Info,
                Level::Debug,
                Level::Trace,
            ] {
                i.default_endpoints.insert(*level, endpoint.clone());
            }
        });
        self
    }

    /// Set the default endpoint for all messages of the given level.
    ///
    /// The default endpoint is used when the logging statement does not use the `target:
    /// "endpoint"` syntax.
    ///
    /// This overrides any previous default endpoints set for this level, either by this method or
    /// by [`Builder::default_endpoint()`].
    ///
    /// ```no_run
    /// log_fastly::Logger::builder()
    ///     .max_level(log::LevelFilter::Info)
    ///     .default_endpoint("my_endpoint")
    ///     .default_level_endpoint("error_only", log::Level::Error)
    ///     .endpoint("other_endpoint")
    ///     .init();
    /// log::info!("This will be written to my_endpoint...");
    /// log::error!("...but this will be written to error_only");
    /// log::error!(target: "other_endpoint", "This will go to other_endpoint, though");
    /// ```
    pub fn default_level_endpoint<E>(&mut self, endpoint: E, level: Level) -> &mut Self
    where
        E: TryInto<Endpoint>,
        <E as TryInto<Endpoint>>::Error: Into<Error>,
    {
        self.with_inner_and_then(endpoint.try_into(), |i, endpoint| {
            i.default_endpoints.insert(level, endpoint.clone());
            i.endpoints
                .insert(endpoint.name().to_owned(), (endpoint, None))
        });
        self
    }

    /// Set the maximum logging level for all messages.
    ///
    /// No messages that exceed this level will be emitted, even if a higher level is set for a
    /// specific endpoint or module name.
    ///
    /// **Note:** The default level is `LevelFilter::Off`, which emits no logging at all. You'll
    /// want to change this for most configurations.
    ///
    /// ```no_run
    /// log_fastly::Builder::new()
    ///     .max_level(log::LevelFilter::Warn)
    ///     .endpoint_level("my_endpoint", log::LevelFilter::Info)
    ///     .init();
    /// log::warn!(target: "my_endpoint", "This will be written to my_endpoint...");
    /// log::info!(target: "my_endpoint", "...but this won't");
    /// ```
    pub fn max_level(&mut self, level: LevelFilter) -> &mut Self {
        self.with_inner(|i| i.max_level = level);
        self
    }

    /// Set a logging level for modules whose names match the given [regular
    /// expression][regex-syntax].
    ///
    /// By default, logging statements in any module are emitted if their level is within
    /// [`Builder::max_level()`] and the level for their target endpoint. If you configure filters
    /// with this method, the name of the module calling the logging statement must also match one
    /// of the patterns, and be within the filter's specified level.
    ///
    /// ```no_run
    /// mod my_module {
    ///     pub fn do_a_thing() {
    ///         log::warn!("This won't be written, because this module's max level is Error...");
    ///         log::error!("...but this will be written");
    ///     }
    /// }
    ///
    /// log_fastly::Logger::builder()
    ///     .max_level(log::LevelFilter::Info)
    ///     .default_endpoint("my_endpoint")
    ///     .filter_module("my_module", log::LevelFilter::Error)
    ///     .init();
    /// log::info!("This won't be written, because it's not in my_module");
    /// // This will only emit one log message, because "my_app$" doesn't match, and
    /// // "my_app::my_module" is limited to Error
    /// my_module::do_a_thing();
    /// ```
    ///
    /// [regex-syntax]: https://docs.rs/regex#syntax
    pub fn filter_module(&mut self, regex: &str, level: LevelFilter) -> &mut Self {
        self.with_inner(|i| {
            i.module_filters
                .entry(level)
                .or_insert_with(|| vec![])
                .push(regex.to_owned())
        });
        self
    }

    /// Set whether all log messages should be echoed to `stdout` (`false` by default).
    ///
    /// If this is set to `true`, all logging statements will write the message to `stdout` in
    /// addition to the specified endpoint. This is particularly useful when debugging with
    /// [Compute@Edge Log Tailing][log-tailing].
    ///
    /// [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
    pub fn echo_stdout(&mut self, enabled: bool) -> &mut Self {
        self.with_inner(|i| i.echo_stdout = enabled);
        self
    }

    /// Set whether all log messages should be echoed to `stderr` (`false` by default).
    ///
    /// If this is set to `true`, all logging statements will write the message to `stderr` in
    /// addition to the specified endpoint. This is particularly useful when debugging with
    /// [Compute@Edge Log Tailing][log-tailing].
    ///
    /// [log-tailing]: https://www.fastly.com/blog/introducing-compute-edge-log-tailing-for-better-observability-and-easier-debugging
    pub fn echo_stderr(&mut self, enabled: bool) -> &mut Self {
        self.with_inner(|i| i.echo_stderr = enabled);
        self
    }

    /// Build the logger and initialize it as the global logger.
    ///
    /// ```no_run
    /// log_fastly::Builder::new()
    ///     .default_endpoint("my_endpoint")
    ///     .init();
    /// log::info!("Hello");
    /// ```
    ///
    /// # Panics
    ///
    /// This may panic for any of the reasons that [`Builder::try_init()`] would return an error.
    pub fn init(&mut self) {
        self.try_init().expect("log_fastly::Builder::init() failed")
    }

    /// Build the logger and initialize it as the global logger.
    ///
    /// This will fail with an `Err` value if a global logger is already initialized, or for any of
    /// the reasons that [`Builder::build()`] can fail.
    ///
    /// ```no_run
    /// log_fastly::Builder::new()
    ///     .default_endpoint("my_endpoint")
    ///     .try_init()
    ///     .unwrap();
    /// log::info!("Hello");
    /// ```
    pub fn try_init(&mut self) -> Result<(), Error> {
        let logger = self.build()?;

        // Get the overall max level while we still own the logger
        let max_level = logger.max_level();

        // Now try to install the logger
        let res = log::set_boxed_logger(Box::new(logger));

        if res.is_ok() {
            // And finally set the max level in the global `log` state, if we successfully installed
            // the logger
            log::set_max_level(max_level);
        }

        res.map_err(Into::into)
    }

    /// Build a `Logger`, using up this builder.
    ///
    /// This is mainly useful if you want to manually initialize the logger, or nest it within
    /// another [`log::Log`] implementation.
    ///
    /// This will fail with an `Err` value if:
    ///
    /// - This builder has already been used to build a `Logger`.
    ///
    /// - Any of the registered endpoint names are invalid.
    ///
    /// - Any of the module name filters set by [`Builder::filter_module()`] have invalid [regex
    /// syntax][regex-syntax].
    ///
    /// ```no_run
    /// let logger = log_fastly::Builder::new()
    ///     .default_endpoint("my_endpoint")
    ///     .build()
    ///     .unwrap();
    /// log::set_boxed_logger(Box::new(logger)).unwrap();
    /// ```
    ///
    /// [regex-syntax]: https://docs.rs/regex#syntax
    pub fn build(&mut self) -> Result<Logger, Error> {
        let inner = std::mem::replace(
            &mut self.inner,
            Err(anyhow!("Builder can only be built once")),
        )?;

        // Calculate the overall max logging level, taking into account all the endpoint-specific
        // and default filters.

        // First, find the max endpoint-specific log level.
        let endpoint_max = inner
            .endpoints
            .values()
            // If an endpoint doesn't have a filter, it's only constrained by the overall
            // `max_level`.
            .map(|e| e.1.unwrap_or_else(LevelFilter::max))
            .max()
            .unwrap_or(LevelFilter::Off);
        // Then, find the highest log level that has a default logger.
        let default_max = inner
            .default_endpoints
            .keys()
            .max()
            .map(Level::to_level_filter)
            .unwrap_or(LevelFilter::Off);
        // Take whichever of these is bigger, but then clamp it with our `max_level` which takes
        // precedence.
        let max_level = std::cmp::min(inner.max_level, std::cmp::max(endpoint_max, default_max));

        let module_filters = generate_module_filters(inner.module_filters)?;

        Ok(Logger {
            endpoints: inner.endpoints,
            default_endpoints: inner.default_endpoints,
            max_level,
            module_filters,
            echo_stdout: inner.echo_stdout,
            echo_stderr: inner.echo_stderr,
        })
    }
}

/// Build a [`RegexSet`] for each logging level from the specified filters.
fn generate_module_filters(
    mut regex_map: HashMap<LevelFilter, Vec<String>>,
) -> Result<Option<HashMap<Level, RegexSet>>, Error> {
    if regex_map.is_empty() {
        Ok(None)
    } else {
        // build from highest level to lowest, extending the set as we go so that each set contains
        // all of the regexes specified for the levels above
        let levels = [
            LevelFilter::Trace,
            LevelFilter::Debug,
            LevelFilter::Info,
            LevelFilter::Warn,
            LevelFilter::Error,
        ];
        let mut module_filters = HashMap::with_capacity(levels.len());
        let mut running_regexes = vec![];
        for level in levels.iter() {
            if let Some(regexes) = regex_map.remove(&level) {
                running_regexes.extend(regexes);
            }
            let matcher = RegexSetBuilder::new(running_regexes.iter()).build()?;
            let level = level
                .to_level()
                .expect("only iterating on LevelFilters that match a Level");
            module_filters.insert(level, matcher);
        }
        Ok(Some(module_filters))
    }
}

/// Initialize logging with a single endpoint filtered by log level.
///
/// For advanced configuration, see the [`Builder`] type, and the [module-level
/// documentation](index.html#advanced-configuration).
///
/// ```no_run
/// log_fastly::init_simple("my_endpoint", log::LevelFilter::Warn);
/// log::warn!("This will be written to my_endpoint...");
/// log::info!("...but this won't");
/// ```
pub fn init_simple<E>(endpoint: E, level: LevelFilter)
where
    E: TryInto<Endpoint>,
    <E as TryInto<Endpoint>>::Error: Into<Error>,
{
    Logger::builder()
        .default_endpoint(endpoint)
        .max_level(level)
        .try_init()
        .expect("log_fastly::init_simple() failed");
}