provide-telemetry 0.5.0

Cross-language telemetry helpers with privacy, resilience, and OTLP support.
Documentation
// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
// SPDX-License-Identifier: Apache-2.0
// SPDX-Comment: Part of provide-telemetry.
//
mod runtime_test_support;

use provide_telemetry::{
    get_runtime_status, reconfigure_telemetry, reload_runtime_from_env, setup_telemetry,
    shutdown_telemetry, update_runtime_config, BackpressureConfig, ExporterPolicyConfig,
    RuntimeOverrides, SLOConfig, SamplingConfig, SecurityConfig, TelemetryConfig,
};
use runtime_test_support::*;

#[test]
fn runtime_test_get_runtime_config_none_before_setup() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();
        assert!(provide_telemetry::get_runtime_config().is_none());
    });
}

#[test]
fn runtime_test_get_runtime_status_before_setup_uses_fallback() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();

        let status = get_runtime_status();

        assert!(!status.setup_done);
        assert!(!status.providers.logs);
        assert!(!status.providers.traces);
        assert!(!status.providers.metrics);
        assert!(status.fallback.logs);
        assert!(status.fallback.traces);
        assert!(status.fallback.metrics);
    });
}

#[test]
fn runtime_test_get_runtime_status_after_setup_reports_signal_enablement() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();
        setup_telemetry().expect("setup should succeed");

        let status = get_runtime_status();

        assert!(status.setup_done);
        assert!(status.signals.logs);
        assert!(status.signals.traces);
        assert!(status.signals.metrics);
        assert!(status.fallback.logs);
        assert!(status.fallback.traces);
        assert!(status.fallback.metrics);
    });
}

#[test]
fn runtime_test_setup_is_idempotent() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();

        let first = setup_telemetry().expect("first setup should succeed");
        let second = setup_telemetry().expect("second setup should succeed");

        assert_eq!(first.service_name, second.service_name);
        assert!(provide_telemetry::get_runtime_config().is_some());
    });
}

#[test]
fn runtime_test_get_runtime_config_returns_defensive_copy() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();
        setup_telemetry().expect("setup should succeed");

        let mut local = provide_telemetry::get_runtime_config().expect("config should exist");
        local.service_name = "mutated-locally".to_string();

        let again = provide_telemetry::get_runtime_config().expect("config should still exist");
        assert_ne!(again.service_name, "mutated-locally");
    });
}

#[test]
fn runtime_test_update_runtime_config_applies_hot_fields() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();
        setup_telemetry().expect("setup should succeed");

        let updated = update_runtime_config(RuntimeOverrides {
            sampling: Some(SamplingConfig {
                logs_rate: 0.25,
                traces_rate: 1.0,
                metrics_rate: 1.0,
            }),
            backpressure: Some(BackpressureConfig {
                logs_maxsize: 9,
                traces_maxsize: 0,
                metrics_maxsize: 0,
            }),
            exporter: Some(ExporterPolicyConfig {
                logs_retries: 2,
                traces_retries: 0,
                metrics_retries: 0,
                logs_backoff_seconds: 0.5,
                traces_backoff_seconds: 0.0,
                metrics_backoff_seconds: 0.0,
                logs_timeout_seconds: 7.5,
                traces_timeout_seconds: 10.0,
                metrics_timeout_seconds: 10.0,
                logs_shutdown_timeout_seconds: 5.0,
                logs_fail_open: false,
                traces_fail_open: true,
                metrics_fail_open: true,
            }),
            security: Some(SecurityConfig {
                max_attr_value_length: 2048,
                max_attr_count: 32,
                max_nesting_depth: 8,
            }),
            slo: Some(SLOConfig {
                enable_red_metrics: true,
                enable_use_metrics: false,
            }),
            pii_max_depth: Some(3),
            strict_schema: Some(true),
            event_schema: None,
            logging: None,
        })
        .expect("update should succeed");

        assert_eq!(updated.sampling.logs_rate, 0.25);
        assert_eq!(updated.backpressure.logs_maxsize, 9);
        assert_eq!(updated.exporter.logs_retries, 2);
        assert_eq!(updated.security.max_attr_value_length, 2048);
        assert!(updated.slo.enable_red_metrics);
        assert_eq!(updated.pii_max_depth, 3);
        assert!(updated.strict_schema);
    });
}

#[test]
fn runtime_test_reload_runtime_from_env_preserves_cold_fields_and_updates_hot_fields() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(
        &[
            ("PROVIDE_TELEMETRY_SERVICE_NAME", "initial-service"),
            ("PROVIDE_SAMPLING_LOGS_RATE", "1.0"),
        ],
        || {
            reset_runtime();
            let initial = setup_telemetry().expect("setup should succeed");
            assert_eq!(initial.service_name, "initial-service");

            std::env::set_var("PROVIDE_TELEMETRY_SERVICE_NAME", "reloaded-service");
            std::env::set_var("PROVIDE_SAMPLING_LOGS_RATE", "0.5");

            let reloaded = reload_runtime_from_env().expect("reload should succeed");
            assert_eq!(reloaded.service_name, "initial-service");
            assert_eq!(reloaded.sampling.logs_rate, 0.5);
        },
    );
}

#[test]
fn runtime_test_reconfigure_telemetry_applies_cold_fields() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();

        #[cfg(not(feature = "otel"))]
        setup_telemetry().expect("setup should succeed");

        let target = TelemetryConfig {
            service_name: "reconfigured-service".to_string(),
            environment: "prod".to_string(),
            ..TelemetryConfig::default()
        };

        let updated = reconfigure_telemetry(Some(target)).expect("reconfigure should succeed");
        assert_eq!(updated.service_name, "reconfigured-service");
        assert_eq!(updated.environment, "prod");
    });
}

#[test]
fn runtime_test_shutdown_clears_setup_state() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();
        setup_telemetry().expect("setup should succeed");

        shutdown_telemetry().expect("shutdown should succeed");

        assert!(provide_telemetry::get_runtime_config().is_none());
    });
}

#[test]
fn runtime_test_update_runtime_config_requires_setup() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();

        let err = update_runtime_config(RuntimeOverrides::default())
            .expect_err("update before setup must fail");
        assert!(err.message.contains("setup_telemetry"));
    });
}

#[test]
fn runtime_test_reload_runtime_from_env_requires_setup_and_surfaces_parse_errors() {
    let _guard = runtime_lock().lock().expect("runtime lock poisoned");
    with_env(&[], || {
        reset_runtime();

        let err = reload_runtime_from_env().expect_err("reload before setup must fail");
        assert!(err.message.contains("setup_telemetry"));
    });

    with_env(&[], || {
        reset_runtime();
        setup_telemetry().expect("setup should succeed");
        std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "not-a-bool");

        let err = reload_runtime_from_env().expect_err("invalid env must fail reload");
        assert!(err.message.contains("PROVIDE_LOG_INCLUDE_TIMESTAMP"));
        std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
    });
}