rscale 0.1.0

Self-hosted Rust control plane for operating a single tailnet with Tailscale clients
Documentation
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

use crate::config::{LogFormat, TelemetryConfig};
use crate::error::{AppError, AppResult};

pub fn init(config: &TelemetryConfig) -> AppResult<()> {
    let filter = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new(config.filter.clone()))
        .map_err(|err| AppError::Bootstrap(format!("invalid log filter: {err}")))?;

    match config.format {
        LogFormat::Json => tracing_subscriber::registry()
            .with(filter)
            .with(tracing_subscriber::fmt::layer().json())
            .try_init(),
        LogFormat::Pretty => tracing_subscriber::registry()
            .with(filter)
            .with(tracing_subscriber::fmt::layer().pretty())
            .try_init(),
        LogFormat::Compact => tracing_subscriber::registry()
            .with(filter)
            .with(tracing_subscriber::fmt::layer().compact())
            .try_init(),
    }
    .map_err(|err| AppError::Bootstrap(format!("failed to initialize telemetry: {err}")))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use std::error::Error;
    use std::sync::OnceLock;

    use super::*;

    static TELEMETRY_INIT_RESULT: OnceLock<Result<(), String>> = OnceLock::new();
    type TestResult<T = ()> = Result<T, Box<dyn Error>>;

    #[test]
    fn init_accepts_default_config() {
        let result = TELEMETRY_INIT_RESULT
            .get_or_init(|| init(&TelemetryConfig::default()).map_err(|err| err.to_string()));
        assert!(
            result.is_ok(),
            "default telemetry config should initialize successfully: {result:?}"
        );
    }

    #[test]
    fn init_rejects_invalid_filter_expression() -> TestResult {
        let err = match init(&TelemetryConfig {
            filter: "[".to_string(),
            format: LogFormat::Json,
        }) {
            Ok(_) => return Err(std::io::Error::other("invalid filter should be rejected").into()),
            Err(err) => err,
        };
        assert!(
            matches!(err, AppError::Bootstrap(message) if message.contains("invalid log filter"))
        );
        Ok(())
    }
}