provide-telemetry 0.7.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.
//
use std::sync::MutexGuard;

use tokio::runtime::Builder;

use provide_telemetry::testing::acquire_test_state_lock;
use provide_telemetry::{
    get_circuit_state, get_health_snapshot, release, run_with_resilience, set_exporter_policy,
    set_queue_policy, set_sampling_policy, should_sample, try_acquire, ExporterPolicy, QueuePolicy,
    SamplingPolicy, Signal,
};

#[cfg(feature = "otel")]
#[path = "../examples/support/e2e_shared.rs"]
mod e2e_shared;

#[path = "../examples/support/basic_telemetry.rs"]
mod basic_telemetry;
#[path = "../examples/support/data_governance.rs"]
mod data_governance;
#[path = "../examples/support/error_degradation.rs"]
mod error_degradation;
#[path = "../examples/support/error_sessions.rs"]
mod error_sessions;
#[path = "../examples/support/exporter_resilience.rs"]
mod exporter_resilience;
#[path = "../examples/support/full_hardening.rs"]
mod full_hardening;
#[path = "../examples/support/lazy_loading.rs"]
mod lazy_loading;
#[path = "../examples/support/performance_metrics.rs"]
mod performance_metrics;
#[path = "../examples/support/pii_cardinality.rs"]
mod pii_cardinality;
#[path = "../examples/support/runtime_reconfigure.rs"]
mod runtime_reconfigure;
#[path = "../examples/support/sampling_backpressure.rs"]
mod sampling_backpressure;
#[path = "../examples/support/security_hardening.rs"]
mod security_hardening;
#[path = "../examples/support/slo_health.rs"]
mod slo_health;
#[path = "../examples/support/w3c_propagation.rs"]
mod w3c_propagation;

fn reset_policies() {
    provide_telemetry::sampling::_reset_sampling_for_tests();
    provide_telemetry::backpressure::_reset_backpressure_for_tests();
    provide_telemetry::resilience::_reset_resilience_for_tests();
    provide_telemetry::health::_reset_health_for_tests();
}

/// Acquire the shared test-state lock AND reset all policies so each test
/// starts from a known clean state even if a prior test panicked mid-run.
fn acquire_fresh_lock() -> MutexGuard<'static, ()> {
    let guard = acquire_test_state_lock();
    reset_policies();
    guard
}

#[cfg(feature = "otel")]
fn restore_var(key: &str, previous: Option<String>) {
    match previous {
        Some(value) => std::env::set_var(key, value),
        None => std::env::remove_var(key),
    }
}

#[cfg(feature = "otel")]
#[test]
fn integration_test_e2e_tracer_provider_builds_with_http_exporter() {
    let endpoint_key = "OTEL_EXPORTER_OTLP_ENDPOINT";
    let headers_key = "OTEL_EXPORTER_OTLP_HEADERS";
    let previous_endpoint = std::env::var(endpoint_key).ok();
    let previous_headers = std::env::var(headers_key).ok();
    std::env::set_var(endpoint_key, "http://localhost:5080/api/default");
    std::env::set_var(headers_key, "Authorization=Basic%20test");

    let result = e2e_shared::init_tracer_provider("rust-e2e-test");

    restore_var(endpoint_key, previous_endpoint);
    restore_var(headers_key, previous_headers);

    assert!(
        result.is_ok(),
        "expected OTLP tracer provider to build for E2E helper, got {result:?}"
    );
}

#[test]
fn integration_test_basic_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = basic_telemetry::run_demo().expect("basic telemetry example should succeed");

    assert_eq!(summary.iterations, 3);
    assert_eq!(summary.logged_events, 6);
    assert_eq!(summary.counter_total, 3.0);
    assert_eq!(summary.gauge_value, 1.0);
    assert_eq!(summary.histogram_count, 3);
    assert_eq!(summary.histogram_total, 37.5);
    assert_eq!(summary.context_keys_after_clear, 0);
    assert_eq!(summary.unbound_key.as_deref(), Some("region"));
}

#[test]
fn integration_test_w3c_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = w3c_propagation::run_demo().expect("w3c propagation example should succeed");

    assert_eq!(
        summary.http_trace_id.as_deref(),
        Some("4bf92f3577b34da6a3ce929d0e0e4736") // pragma: allowlist secret
    );
    assert_eq!(summary.manual_trace_id_after_clear, None);
    assert_eq!(
        summary.nested_outer_restored.as_deref(),
        Some("1111111111111111ffffffffffffffff")
    );
    assert_eq!(summary.nested_after_clear, None);
}

#[test]
fn integration_test_sampling_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = sampling_backpressure::run_demo().expect("sampling example should succeed");

    assert!(!summary.logs_routine_sampled);
    assert!(summary.logs_critical_sampled);
    assert!(summary.first_trace_ticket_acquired);
    assert!(!summary.second_trace_ticket_acquired);
    assert!(summary.third_trace_ticket_acquired);
    assert!(summary.dropped_traces >= 1);
}

#[test]
fn integration_test_runtime_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = runtime_reconfigure::run_demo().expect("runtime example should succeed");

    assert_eq!(summary.before_logs_rate, 1.0);
    assert_eq!(summary.after_update_logs_rate, 0.0);
    assert_eq!(summary.after_reconfigure_logs_rate, 1.0);
    assert_eq!(summary.after_reload_logs_rate, 1.0);
}

#[test]
fn integration_test_pii_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = pii_cardinality::run_demo().expect("pii/cardinality example should succeed");

    assert_eq!(summary.hashed_email_len, 12);
    assert!(summary.credit_card_removed);
    assert_eq!(summary.truncated_password.as_deref(), Some("hunt..."));
    assert_eq!(summary.cardinality_max_values, Some(1));
    assert_eq!(summary.cardinality_ttl_seconds, Some(1.0));
}

#[test]
fn integration_test_resilience_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = exporter_resilience::run_demo().expect("resilience example should succeed");

    assert!(summary.fail_open_result_is_none);
    assert!(summary.fail_closed_is_error);
    assert!(summary.timeout_result_is_none);
    assert_eq!(summary.metrics_circuit_state.as_str(), "open");
    assert!(summary.metrics_open_count >= 1);
}

#[test]
fn integration_test_slo_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = slo_health::run_demo().expect("slo example should succeed");

    assert_eq!(summary.classify_404.as_deref(), Some("client_error"));
    assert_eq!(summary.classify_503.as_deref(), Some("server_error"));
    assert_eq!(summary.classify_200.as_deref(), Some("unclassified"));
}

#[test]
fn integration_test_full_hardening_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = full_hardening::run_demo().expect("hardening example should succeed");

    assert_eq!(summary.pii_rules_active, 2);
    assert_eq!(summary.cardinality_limit_max, Some(3));
    assert_eq!(summary.queue_traces_maxsize, 2);
    assert_eq!(summary.metrics_circuit_state.as_str(), "open");
}

#[test]
fn integration_test_error_degradation_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = error_degradation::run_demo().expect("error/degradation example should succeed");

    assert!(summary.configuration_error_seen);
    assert!(summary.event_schema_error_seen);
    assert!(summary.telemetry_error_catchall_count >= 2);
}

#[test]
fn integration_test_performance_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = performance_metrics::run_demo().expect("performance example should succeed");

    assert!(summary.event_ns > 0.0);
    assert!(summary.counter_ns > 0.0);
    assert!(summary.should_sample_ns > 0.0);
}

#[test]
fn integration_test_lazy_loading_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = lazy_loading::run_demo().expect("lazy loading example should succeed");

    assert!(!summary.slo_loaded_before_classify);
    assert!(!summary.metrics_loaded_before_use);
    assert!(summary.slo_loaded_after_classify);
    assert!(summary.metrics_loaded_after_use);
}

#[test]
fn integration_test_error_sessions_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = error_sessions::run_demo().expect("error/session example should succeed");

    assert_eq!(summary.value_error_a, summary.value_error_b);
    assert_ne!(summary.value_error_a, summary.type_error);
    assert!(!summary.runtime_error_fingerprint.is_empty());
    assert_eq!(summary.session_before, None);
    assert_eq!(summary.session_after_bind.as_deref(), Some("sess-demo-42"));
    assert_eq!(summary.session_after_clear, None);
}

#[test]
fn integration_test_security_hardening_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = security_hardening::run_demo().expect("security example should succeed");

    assert!(summary.secret_redacted);
    assert!(summary.password_redacted);
    assert_eq!(summary.depth_preserved_leaf.as_deref(), Some("deep"));
}

#[test]
fn integration_test_data_governance_example_summary_matches_demo_flow() {
    let _guard = acquire_fresh_lock();
    let summary = data_governance::run_demo().expect("data governance example should succeed");

    assert!(summary.full_logs_debug_allowed);
    assert!(!summary.none_traces_allowed);
    assert_eq!(summary.redacted_ssn.as_deref(), Some("***"));
    assert_eq!(summary.hashed_card_len, Some(12));
    assert!(summary.diagnosis_dropped);
    assert!(summary.api_key_dropped);
    assert_eq!(summary.ssn_class.as_deref(), Some("PII"));
    assert_eq!(summary.card_class.as_deref(), Some("PCI"));
    assert_eq!(summary.receipt_action.as_deref(), Some("redact"));
    assert!(summary.receipt_hmac_prefix_len >= 8);
}

#[test]
fn integration_test_sampling_drop_increments_health() {
    let _guard = acquire_fresh_lock();
    set_sampling_policy(
        Signal::Logs,
        SamplingPolicy {
            default_rate: 0.0,
            overrides: Default::default(),
        },
    )
    .expect("policy should set");

    let keep = should_sample(Signal::Logs, Some("event-name")).expect("sampling should work");
    assert!(!keep);

    let snapshot = get_health_snapshot();
    assert_eq!(snapshot.dropped_logs, 1);
}

#[test]
fn integration_test_bounded_queue_drop_increments_health() {
    let _guard = acquire_fresh_lock();
    set_queue_policy(QueuePolicy {
        logs_maxsize: 1,
        traces_maxsize: 0,
        metrics_maxsize: 0,
    });

    let ticket = try_acquire(Signal::Logs).expect("first acquire should succeed");
    let dropped = try_acquire(Signal::Logs);
    assert!(dropped.is_none());

    let snapshot = get_health_snapshot();
    assert_eq!(snapshot.dropped_logs, 1);

    release(ticket);
    let reacquired = try_acquire(Signal::Logs);
    assert!(
        reacquired.is_some(),
        "release should restore queue capacity"
    );
    if let Some(ticket) = reacquired {
        release(ticket);
    }
}

#[test]
fn integration_test_circuit_breaker_trips_after_three_timeouts() {
    let _guard = acquire_fresh_lock();
    let runtime = Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime");

    runtime.block_on(async {
        set_exporter_policy(
            Signal::Logs,
            ExporterPolicy {
                retries: 0,
                backoff_seconds: 0.0,
                timeout_seconds: 0.05,
                fail_open: true,
                allow_blocking_in_event_loop: false,
            },
        )
        .expect("policy should set");

        // The 10x margin makes the wrapper timeout deterministic, while the
        // finite operation ensures a timeout-bypass mutation fails promptly.
        for _ in 0..3 {
            let result = run_with_resilience(Signal::Logs, || async {
                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
                Ok::<_, provide_telemetry::TelemetryError>(())
            })
            .await
            .expect("timeout should be fail-open");
            assert!(result.is_none());
        }

        let state = get_circuit_state(Signal::Logs).expect("state should be available");
        assert_eq!(state.0, "open");

        let short_circuit = run_with_resilience(Signal::Logs, || async {
            Ok::<_, provide_telemetry::TelemetryError>(())
        })
        .await
        .expect("open circuit should fail open");
        assert!(short_circuit.is_none());
    });

    let snapshot = get_health_snapshot();
    assert!(snapshot.export_failures_logs >= 3);
    assert!(snapshot.circuit_open_count_logs >= 1);
    assert_eq!(snapshot.circuit_state_logs, "open");
}