//! Telemetry bootstrap: structured logs, plus optional OpenTelemetry export.
//!
//! [`init`] installs the process wide [`tracing`] subscriber. It always writes
//! human readable records to stdout, and when it is explicitly opted into it
//! mirrors the same spans and log records to an OpenTelemetry collector over
//! OTLP/HTTP. The two signals are switched on independently: shipping traces
//! does not require shipping logs, or the reverse. The exporter only speaks
//! OTLP, so any backend that ingests it will do -- Jaeger, or OpenObserve for
//! one:
//!
//! ```shell
//! $ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:2
//! $ OTEL_TRACES_EXPORTER=otlp OTEL_LOGS_EXPORTER=otlp cargo run
//! ```
//!
//! OpenObserve takes the same signals on its HTTP port, with the org in the
//! path and basic auth in the standard headers variable:
//!
//! ```shell
//! $ docker run --rm -p 5080:5080 -e ZO_ROOT_USER_EMAIL=you@example.com \
//!     -e ZO_ROOT_USER_PASSWORD=... openobserve/openobserve
//! $ OTEL_TRACES_EXPORTER=otlp \
//!   OTEL_LOGS_EXPORTER=otlp \
//!   OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5080/api/default \
//!   OTEL_EXPORTER_OTLP_HEADERS='Authorization=Basic <base64 of email:password>' \
//!   cargo run
//! ```
//!
//! # Configuration
//!
//! Everything is driven by the environment, so the collector can be changed
//! without touching the code.
//!
//! | Variable | Effect | Default |
//! |---|---|---|
//! | `RUST_LOG` | Filter directives for the console output | [`DEFAULT_CONSOLE_FILTER`] |
//! | `OTEL_TRACES_EXPORTER` | Export spans when the list contains `otlp` | unset: console only |
//! | `OTEL_LOGS_EXPORTER` | Export log records when the list contains `otlp` | unset: console only |
//! | `OTEL_SDK_DISABLED` | `true` forces both exporters off, whatever the above says | `false` |
//! | `OTEL_TRACES_FILTER` | Filter directives for the exported spans | [`DEFAULT_SPANS_FILTER`] |
//! | `OTEL_LOGS_FILTER` | Filter directives for the exported log records | [`DEFAULT_LOGS_FILTER`] |
//! | `OTEL_TRACES_SAMPLER`, `OTEL_TRACES_SAMPLER_ARG` | Which traces to keep, see [`sampler`] | `parentbased_always_on` |
//! | `OTEL_SERVICE_NAME` | `service.name` resource attribute | crate name |
//! | `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, e.g. `deployment.environment.name=develop` | unset |
//! | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, `OTEL_EXPORTER_OTLP_ENDPOINT` | Collector address, in that order of precedence. The signal specific ones are complete URLs, used verbatim; the last is a base URL and gains the signal's path, see [`Signal::path`]. | [`DEFAULT_ENDPOINT`] |
//! | `OTEL_EXPORTER_OTLP_TIMEOUT` | Export timeout, in milliseconds | SDK default |
//! | `OTEL_EXPORTER_OTLP_HEADERS` | Extra headers, `key=value` pairs separated by commas. Read by the exporter itself, not here. | unset |
//!
//! The console and the exporters are filtered independently on purpose: turning
//! `RUST_LOG` down to `warn` to quiet the terminal should not silently stop
//! telemetry from reaching the collector.
//!
//! [`DEFAULT_LOGS_FILTER`] leaves out the OpenTelemetry SDK's own diagnostics.
//! They are exported through `tracing` like everything else, so a collector
//! that stops answering would otherwise report its failure to itself and queue
//! that report for export -- a feedback loop that repeats for as long as the
//! collector is down. On the console they stay visible.

use std::env;

use actix_web::Error;
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::{KeyValue, Value, global};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::{ExporterBuildError, LogExporter, SpanExporter, WithExportConfig};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::logs::{BatchLogProcessor, LogBatch, SdkLogger, SdkLoggerProvider};
use opentelemetry_sdk::trace::{
    BatchSpanProcessor, Sampler, SdkTracer, SdkTracerProvider, SpanData,
};
use tokio::runtime::{Builder, Runtime};
use tracing::{Span, info};
use tracing_actix_web::{RootSpanBuilder, TracingLogger, root_span};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::{Layer, SubscriberExt};
use tracing_subscriber::util::SubscriberInitExt;

/// Comma separated list of exporters to use, per the OpenTelemetry SDK spec.
const TRACES_EXPORTER_VAR: &str = "OTEL_TRACES_EXPORTER";
/// Same, for log records.
const LOGS_EXPORTER_VAR: &str = "OTEL_LOGS_EXPORTER";
/// Kills the whole SDK regardless of the variables above.
const SDK_DISABLED_VAR: &str = "OTEL_SDK_DISABLED";
/// Filter directives for the exported spans.
const SPANS_FILTER_VAR: &str = "OTEL_TRACES_FILTER";
/// Filter directives for the exported log records.
const LOGS_FILTER_VAR: &str = "OTEL_LOGS_FILTER";
/// Which traces to record, and at which rate.
const SAMPLER_VAR: &str = "OTEL_TRACES_SAMPLER";
const SAMPLER_ARG_VAR: &str = "OTEL_TRACES_SAMPLER_ARG";
/// `service.name`, the resource attribute a collector groups telemetry by.
const SERVICE_NAME_VAR: &str = "OTEL_SERVICE_NAME";
/// Collector address, signal specific first as the spec prescribes.
const TRACES_ENDPOINT_VAR: &str = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
const LOGS_ENDPOINT_VAR: &str = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT";
const ENDPOINT_VAR: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";

/// OTLP/HTTP port a stock collector or Jaeger container listens on.
const DEFAULT_ENDPOINT: &str = "http://localhost:4318";

/// Applied to stdout when `RUST_LOG` is unset. The chatty crates stay quiet so
/// that what remains is worth reading. The SDK is included because it greets
/// the first logger with an info record complaining about an empty scope name,
/// which is its own business rather than the operator's; its errors still get
/// through.
const DEFAULT_CONSOLE_FILTER: &str =
    "info,actix_server=warn,sqlx=warn,sea_orm=warn,opentelemetry_sdk=warn,frogue=debug";

/// Applied to the span exporter when [`SPANS_FILTER_VAR`] is unset.
///
/// `debug` rather than the console's `info`: the spans that make a trace worth
/// reading - the database round trips, the password hashing - sit at `debug`
/// so they do not clutter the terminal, and the exporter has a filter of its
/// own precisely so that turning the console down does not silence them.
const DEFAULT_SPANS_FILTER: &str = "sea_orm=info,html5ever=warn,debug";

/// Applied to the log exporter when [`LOGS_FILTER_VAR`] is unset.
///
/// Unlike [`DEFAULT_SPANS_FILTER`], this starts from the console's levels: a
/// log record is worth less than the span it sits in, and third-party `debug`
/// logging is mostly transport noise. `frogue` keeps its own `debug` records,
/// which are the reason to ship logs at all.
///
/// The OpenTelemetry crates are off on purpose: their `internal-logs`
/// diagnostics travel through `tracing`, so exporting them would make a failed
/// export queue another record for export, forever, every batch interval.
const DEFAULT_LOGS_FILTER: &str = "info,actix_server=warn,sqlx=warn,sea_orm=warn,frogue=debug,opentelemetry=off,opentelemetry_sdk=off,opentelemetry-otlp=off";

/// Reports a problem with the telemetry setup itself.
///
/// This module runs before the subscriber exists and after it is being torn
/// down, so these notes go straight to stderr rather than through `tracing`,
/// which is also where an operator will be looking for them.
fn note(message: impl std::fmt::Display) {
    eprintln!("frogue telemetry: {message}");
}

/// Installs the global subscriber and returns the handle that owns the
/// pipelines.
///
/// The returned [`Telemetry`] must outlive the server: dropping it shuts the
/// providers down, which is what flushes the spans and log records still
/// sitting in their batch queues.
#[must_use = "telemetry is torn down as soon as the guard is dropped"]
pub fn init() -> Telemetry {
    let console = tracing_subscriber::fmt::layer()
        .with_target(true)
        .with_writer(std::io::stderr)
        .with_filter(filter_from_env("RUST_LOG", DEFAULT_CONSOLE_FILTER));

    // Resolved before the subscriber goes up, so that a bad configuration can
    // only be reported on stderr.
    let traces = Traces::from_env();
    let logs = Logs::from_env();
    let spans_filter = filter_from_env(SPANS_FILTER_VAR, DEFAULT_SPANS_FILTER);
    let logs_filter = filter_from_env(LOGS_FILTER_VAR, DEFAULT_LOGS_FILTER);

    // A signal that is not exported is an absent layer rather than a second
    // subscriber built without it: `Option<Layer>` is a layer that does
    // nothing, which keeps the stack flat however many signals are switched on.
    tracing_subscriber::registry()
        .with(console)
        .with(
            traces
                .as_ref()
                .map(|traces| OpenTelemetryLayer::new(traces.tracer()).with_filter(spans_filter)),
        )
        .with(
            logs.as_ref()
                .map(|logs| logs.bridge().with_filter(logs_filter)),
        )
        .init();

    match (&traces, &logs) {
        (Some(traces), Some(logs)) => info!(
            spans = %traces.endpoint,
            logs = %logs.endpoint,
            "exporting spans and log records over OTLP"
        ),
        (Some(traces), None) => {
            info!(endpoint = %traces.endpoint, "exporting spans over OTLP")
        }
        (None, Some(logs)) => info!(endpoint = %logs.endpoint, "exporting log records over OTLP"),
        (None, None) => info!("export disabled, records are only written to the console"),
    }

    Telemetry {
        _traces: traces,
        _logs: logs,
        request_logger: TracingLogger::new(),
    }
}

/// The process wide telemetry pipeline.
///
/// Kept alive by `main` so that spans and log records written while the server
/// is shutting down still have somewhere to go.
pub struct Telemetry {
    /// Held for its `Drop`, which drains the exporter. `None` when span export
    /// is off.
    _traces: Option<Traces>,
    /// Same, for log records.
    _logs: Option<Logs>,
    request_logger: TracingLogger<ErrorAwareRootSpanBuilder>,
}

impl Telemetry {
    /// Middleware opening a span per incoming request, closing it once the
    /// response body has been streamed out.
    ///
    /// Cheap to [`Clone`], so hand out one per actix worker.
    #[must_use]
    pub fn request_logger(&self) -> TracingLogger<ErrorAwareRootSpanBuilder> {
        self.request_logger.clone()
    }
}

/// The root span per request, like
/// [`tracing_actix_web::DefaultRootSpanBuilder`] but with one correction: a
/// `5xx` built by a handler is an error.
///
/// The default only considers a response failed when it carries an attached
/// error, so a handler that answers a bare
/// `HttpResponse::InternalServerError()` (which is how every failure path in
/// this crate reports a database problem) would be recorded as
/// `otel.status_code=OK`. Marking the status from the response code makes those
/// traces come out red in a trace UI, where the operator looks for them.
pub struct ErrorAwareRootSpanBuilder;

impl RootSpanBuilder for ErrorAwareRootSpanBuilder {
    fn on_request_start(request: &ServiceRequest) -> Span {
        root_span!(level = tracing::Level::INFO, request)
    }

    fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
        match outcome {
            Ok(response) => {
                if let Some(error) = response.response().error() {
                    record_failure(span, response.status(), error.as_response_error());
                } else {
                    let code: i32 = response.response().status().as_u16().into();
                    span.record("http.status_code", code);

                    if response.response().status().is_server_error() {
                        span.record("otel.status_code", "ERROR");
                    } else {
                        span.record("otel.status_code", "OK");
                    }
                }
            }
            Err(error) => {
                let response_error = error.as_response_error();
                record_failure(span, response_error.status_code(), response_error);
            }
        }
    }
}

/// Records the details of a failed request on its root span.
///
/// Copied from the default builder, which keeps the function private; the two
/// have to agree on the field names for a collector to make sense of them.
fn record_failure(span: Span, status_code: StatusCode, response_error: &dyn ResponseError) {
    // Pre-formatting the errors is a workaround for
    // https://github.com/tokio-rs/tracing/issues/1565.
    let message = format!("{response_error}");
    let details = format!("{response_error:?}");
    span.record("exception.message", tracing::field::display(message));
    span.record("exception.details", tracing::field::display(details));

    let code: i32 = status_code.as_u16().into();
    span.record("http.status_code", code);

    // A client error is the caller's mistake and leaves the span green; an
    // error the server is responsible for is what the operator needs to see.
    if status_code.is_client_error() {
        span.record("otel.status_code", "OK");
    } else {
        span.record("otel.status_code", "ERROR");
    }
}

/// OTLP/HTTP span pipeline.
struct Traces {
    /// The provider the layer records into. Dropping it flushes and closes the
    /// HTTP client, so it must not be dropped while the layer is still live.
    provider: SdkTracerProvider,
    /// Resolved collector address, reported at startup.
    endpoint: String,
}

impl Traces {
    /// Builds the pipeline, or returns `None` when export is not wanted.
    fn from_env() -> Option<Self> {
        if export_disabled() || !lists_exporter(TRACES_EXPORTER_VAR, "otlp") {
            return None;
        }

        let endpoint = endpoint(Signal::Traces);
        let runtime = export_runtime()?;
        let exporter = build_exporter(&runtime, "span", || {
            SpanExporter::builder()
                .with_http()
                .with_endpoint(&endpoint)
                .build()
        })?;

        let processor = BatchSpanProcessor::builder(ExportOnRuntime { exporter, runtime }).build();

        let provider = SdkTracerProvider::builder()
            .with_sampler(sampler())
            .with_resource(resource())
            .with_span_processor(processor)
            .build();

        // Also publishes the provider globally, so libraries instrumented
        // outside of `tracing` can pick it up.
        global::set_tracer_provider(provider.clone());

        Some(Self { provider, endpoint })
    }

    /// The tracer the OpenTelemetry layer records through.
    ///
    /// Named after the crate so that a collector receiving spans from several
    /// services can tell Frogue apart from the libraries it depends on.
    fn tracer(&self) -> SdkTracer {
        self.provider.tracer(env!("CARGO_PKG_NAME"))
    }
}

impl Drop for Traces {
    fn drop(&mut self) {
        // `shutdown` drains the batch queue before closing the channel, so
        // simply dropping the provider would lose whatever is still pending.
        if let Err(err) = self.provider.shutdown() {
            note(format!("could not shut the tracer provider down: {err}"));
        }
    }
}

/// OTLP/HTTP log pipeline.
struct Logs {
    /// Same contract as [`Traces::provider`]: dropping it drains the batch
    /// queue, so it must outlive the layer.
    provider: SdkLoggerProvider,
    /// Resolved collector address, reported at startup.
    endpoint: String,
}

impl Logs {
    /// Builds the pipeline, or returns `None` when export is not wanted.
    fn from_env() -> Option<Self> {
        if export_disabled() || !lists_exporter(LOGS_EXPORTER_VAR, "otlp") {
            return None;
        }

        let endpoint = endpoint(Signal::Logs);
        let runtime = export_runtime()?;
        let exporter = build_exporter(&runtime, "log", || {
            LogExporter::builder()
                .with_http()
                .with_endpoint(&endpoint)
                .build()
        })?;

        let processor = BatchLogProcessor::builder(ExportOnRuntime { exporter, runtime }).build();

        let provider = SdkLoggerProvider::builder()
            .with_resource(resource())
            .with_log_processor(processor)
            .build();

        Some(Self { provider, endpoint })
    }

    /// The subscriber layer turning events into log records.
    ///
    /// A record written inside a span that the trace layer recorded carries
    /// that span's context, so a log lands next to the trace that caused it
    /// even though the two leave through different exporters.
    fn bridge(&self) -> OpenTelemetryTracingBridge<SdkLoggerProvider, SdkLogger> {
        OpenTelemetryTracingBridge::new(&self.provider)
    }
}

impl Drop for Logs {
    fn drop(&mut self) {
        if let Err(err) = self.provider.shutdown() {
            note(format!("could not shut the logger provider down: {err}"));
        }
    }
}

/// Whether [`SDK_DISABLED_VAR`] turns every exporter off, as the spec says.
fn export_disabled() -> bool {
    is_truthy(SDK_DISABLED_VAR)
}

/// Starts the runtime an exporter is driven on.
///
/// Exporting needs a tokio reactor to make progress, and the batch processors
/// drain batches on a bare OS thread that has none -- so each exporter is
/// wrapped in a runtime of our own. `ExportOnRuntime` documents the details.
fn export_runtime() -> Option<Runtime> {
    match Builder::new_current_thread().enable_all().build() {
        Ok(runtime) => Some(runtime),
        Err(err) => {
            note(format!(
                "could not start the export runtime, carrying on without it: {err}"
            ));
            None
        }
    }
}

/// Builds an exporter inside the runtime that will later drive it, so that
/// anything the HTTP client captures at construction refers to that runtime.
fn build_exporter<E>(
    runtime: &Runtime,
    signal: &str,
    build: impl FnOnce() -> Result<E, ExporterBuildError>,
) -> Option<E> {
    let built = {
        let _guard = runtime.enter();
        build()
    };

    match built {
        Ok(exporter) => Some(exporter),
        Err(err) => {
            note(format!(
                "could not build the OTLP {signal} exporter, carrying on without it: {err}"
            ));
            None
        }
    }
}

/// Gives the batch processors an exporter they can drive.
///
/// The stable [`BatchSpanProcessor`] and [`BatchLogProcessor`] queue and batch
/// on a thread of their own, then await each export with
/// `futures_executor::block_on`. That drives the future but supplies no
/// reactor, and an asynchronous HTTP client needs one, so the request would sit
/// there until the export timed out.
///
/// Wrapping the exporter sidesteps that: every batch is handed to a runtime
/// owned here and awaited on the calling thread. The processors only ever call
/// `export` from their own plain thread, never from a runtime thread, so
/// [`Runtime::block_on`] -- which panics on a runtime thread -- is exactly as
/// safe to call as the blocking wait it replaces.
#[derive(Debug)]
struct ExportOnRuntime<E> {
    exporter: E,
    /// A current-thread runtime: `block_on` drives it from the waiting thread,
    /// so it needs no worker threads of its own.
    runtime: Runtime,
}

impl<E: opentelemetry_sdk::trace::SpanExporter> opentelemetry_sdk::trace::SpanExporter
    for ExportOnRuntime<E>
{
    async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
        self.runtime.block_on(self.exporter.export(batch))
    }

    fn force_flush(&self) -> OTelSdkResult {
        self.exporter.force_flush()
    }

    fn shutdown(&self) -> OTelSdkResult {
        self.exporter.shutdown()
    }

    fn set_resource(&mut self, resource: &Resource) {
        self.exporter.set_resource(resource);
    }
}

impl<E: opentelemetry_sdk::logs::LogExporter> opentelemetry_sdk::logs::LogExporter
    for ExportOnRuntime<E>
{
    async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult {
        self.runtime.block_on(self.exporter.export(batch))
    }

    fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> OTelSdkResult {
        self.exporter.shutdown_with_timeout(timeout)
    }

    fn shutdown(&self) -> OTelSdkResult {
        self.exporter.shutdown()
    }

    fn set_resource(&mut self, resource: &Resource) {
        self.exporter.set_resource(resource);
    }
}

/// Describes the process to the collector.
fn resource() -> Resource {
    let service_name =
        non_empty(SERVICE_NAME_VAR).unwrap_or_else(|| env!("CARGO_PKG_NAME").to_owned());

    // `Resource::builder` already folds in `OTEL_RESOURCE_ATTRIBUTES` and
    // `OTEL_SERVICE_NAME`; repeating the name here only supplies the fallback
    // for when neither is set, as the merge order lets the later value win.
    Resource::builder()
        .with_service_name(service_name)
        .with_attributes([KeyValue::new(
            "service.version",
            Value::from(env!("CARGO_PKG_VERSION")),
        )])
        .build()
}

/// Resolves [`SAMPLER_VAR`] and [`SAMPLER_ARG_VAR`] as the OpenTelemetry SDK
/// spec describes, falling back to `parentbased_always_on`.
fn sampler() -> Sampler {
    let requested = env::var(SAMPLER_VAR).unwrap_or_default();
    let requested = requested.trim();

    // `parentbased_*` keeps whatever decision the incoming span already made
    // and only defers to the inner sampler for root spans.
    let (name, parent_based) = match requested.strip_prefix("parentbased_") {
        Some(inner) => (inner, true),
        // An unset variable means the spec default, which is parent based.
        None if requested.is_empty() => ("always_on", true),
        None => (requested, false),
    };

    let sampler = match name {
        "always_on" => Sampler::AlwaysOn,
        "always_off" => Sampler::AlwaysOff,
        "traceidratio" => ratio()
            .map(Sampler::TraceIdRatioBased)
            .unwrap_or_else(missing_ratio),
        unsupported => {
            if !unsupported.is_empty() {
                note(format!(
                    "unsupported {SAMPLER_VAR}={unsupported:?}, keeping every trace"
                ));
            }
            Sampler::AlwaysOn
        }
    };

    if parent_based {
        Sampler::ParentBased(Box::new(sampler))
    } else {
        sampler
    }
}

/// The fraction of traces to keep, as given by [`SAMPLER_ARG_VAR`].
fn ratio() -> Option<f64> {
    non_empty(SAMPLER_ARG_VAR)?.parse().ok()
}

/// Stands in for a ratio sampler that was asked for without a usable rate.
fn missing_ratio() -> Sampler {
    note(format!(
        "{SAMPLER_ARG_VAR} is missing or not a number, keeping every trace"
    ));
    Sampler::AlwaysOn
}

/// One OTLP signal: the variable holding its complete URL, and the path it
/// takes on a base URL.
#[derive(Clone, Copy)]
enum Signal {
    Traces,
    Logs,
}

impl Signal {
    /// The signal specific endpoint variable, which the spec makes a complete
    /// URL used verbatim.
    fn endpoint_var(self) -> &'static str {
        match self {
            Self::Traces => TRACES_ENDPOINT_VAR,
            Self::Logs => LOGS_ENDPOINT_VAR,
        }
    }

    /// Path the OTLP/HTTP spec puts this signal on.
    fn path(self) -> &'static str {
        match self {
            Self::Traces => "v1/traces",
            Self::Logs => "v1/logs",
        }
    }
}

/// Resolves the collector address for `signal`.
///
/// Follows the OTLP spec for the HTTP transport: the signal specific variable
/// is a complete URL and is used verbatim, while the general one is a base URL
/// that gains the signal path.
fn endpoint(signal: Signal) -> String {
    if let Some(endpoint) = non_empty(signal.endpoint_var()) {
        return endpoint;
    }
    let base = non_empty(ENDPOINT_VAR).unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
    let path = signal.path();
    if base.ends_with('/') {
        format!("{base}{path}")
    } else {
        format!("{base}/{path}")
    }
}

/// Builds a filter from `var`, falling back to `default` when it is unset or
/// malformed so that a typo never silences the process entirely.
fn filter_from_env(var: &str, default: &str) -> EnvFilter {
    let Some(directives) = non_empty(var) else {
        return EnvFilter::new(default);
    };

    match EnvFilter::builder().parse(directives.as_str()) {
        Ok(filter) => filter,
        Err(err) => {
            note(format!(
                "could not parse {var}={directives:?}, using {default:?}: {err}"
            ));
            EnvFilter::new(default)
        }
    }
}

/// Whether `var` holds a comma separated list naming `exporter`.
fn lists_exporter(var: &str, exporter: &str) -> bool {
    non_empty(var).is_some_and(|list| {
        list.split(',')
            .any(|entry| entry.trim().eq_ignore_ascii_case(exporter))
    })
}

/// Whether `var` holds one of the spellings the spec accepts for `true`.
fn is_truthy(var: &str) -> bool {
    non_empty(var).is_some_and(|value| value.eq_ignore_ascii_case("true") || value == "1")
}

/// Reads `var`, treating unset, empty and blank as absent.
fn non_empty(var: &str) -> Option<String> {
    env::var(var)
        .ok()
        .map(|value| value.trim().to_owned())
        .filter(|value| !value.is_empty())
}
