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 super::*;
use rstest::rstest;
use std::collections::BTreeMap;

use crate::backpressure::{_reset_backpressure_for_tests, set_queue_policy, QueuePolicy};
use crate::health::get_health_snapshot;
use crate::sampling::{_reset_sampling_for_tests, set_sampling_policy, SamplingPolicy};
use crate::testing::{acquire_test_state_lock, reset_telemetry_state, reset_trace_context};

#[test]
fn tracer_test_tracer_names_match_contract() {
    assert_eq!(tracer.name(), "provide.telemetry");
    assert_eq!(get_tracer(Some("custom.tracer")).name(), "custom.tracer");
}

#[rstest]
#[case(0)]
#[case(1)]
#[case(15)]
#[case(16)]
#[case(17)]
#[case(31)]
#[case(32)]
#[case(33)]
#[case(64)]
fn tracer_test_next_hex_has_exact_requested_length(#[case] requested_len: usize) {
    let value = next_hex(requested_len);

    assert_eq!(value.len(), requested_len);
    assert!(value.chars().all(|character| character.is_ascii_hexdigit()));
}

#[test]
fn tracer_test_next_hex_respects_requested_length_and_advances() {
    let first = next_hex(16);
    let second = next_hex(16);
    let long = next_hex(32);
    let (long_left, long_right) = long.split_at(16);

    assert_eq!(first.len(), 16);
    assert_eq!(second.len(), 16);
    assert_eq!(long.len(), 32);
    assert_ne!(first, second);
    assert!(u64::from_str_radix(&first, 16).is_ok());
    assert!(u64::from_str_radix(long_left, 16).is_ok());
    assert!(u64::from_str_radix(long_right, 16).is_ok());
}

#[test]
fn tracer_test_start_span_sets_trace_context_and_restores_on_drop() {
    let _guard = acquire_test_state_lock();
    reset_telemetry_state();

    let test_tracer = get_tracer(Some("unit.test"));
    let span = test_tracer.start_span("work.span");
    let active = get_trace_context();

    assert_eq!(span.trace_id().len(), 32);
    assert_eq!(span.span_id().len(), 16);
    assert_eq!(
        active.get("trace_id"),
        Some(&Some(span.trace_id().to_string()))
    );
    assert_eq!(
        active.get("span_id"),
        Some(&Some(span.span_id().to_string()))
    );

    span.set_attribute("kind", "internal");
    span.record_error("boom");
    drop(span);

    let cleared = get_trace_context();
    assert_eq!(cleared.get("trace_id"), Some(&None));
    assert_eq!(cleared.get("span_id"), Some(&None));
}

#[test]
fn tracer_test_trace_sets_context_inside_callback_and_emits() {
    let _guard = acquire_test_state_lock();
    reset_telemetry_state();

    let before = get_health_snapshot().emitted_traces;
    let result = trace("unit.trace", || {
        let active = get_trace_context();
        assert!(active.get("trace_id").and_then(|v| v.as_ref()).is_some());
        assert!(active.get("span_id").and_then(|v| v.as_ref()).is_some());
        7_u32
    });

    assert_eq!(result, 7);
    assert_eq!(get_health_snapshot().emitted_traces, before + 1);
    let cleared = get_trace_context();
    assert_eq!(cleared.get("trace_id"), Some(&None));
    assert_eq!(cleared.get("span_id"), Some(&None));
}

#[test]
fn tracer_test_trace_sampling_zero_skips_context_and_emission() {
    let _guard = acquire_test_state_lock();
    reset_telemetry_state();
    _reset_sampling_for_tests();
    set_sampling_policy(
        Signal::Traces,
        SamplingPolicy {
            default_rate: 0.0,
            overrides: BTreeMap::new(),
        },
    )
    .expect("sampling policy should set");

    let before = get_health_snapshot();
    let result = trace("unit.trace", || {
        let active = get_trace_context();
        assert_eq!(active.get("trace_id"), Some(&None));
        assert_eq!(active.get("span_id"), Some(&None));
        11_i32
    });

    assert_eq!(result, 11);
    let after = get_health_snapshot();
    assert_eq!(after.emitted_traces, before.emitted_traces);
    assert!(
        after.dropped_traces > before.dropped_traces,
        "sampling rejection must increment dropped traces at least once"
    );
}

#[test]
fn tracer_test_trace_queue_full_skips_context_and_emission() {
    let _guard = acquire_test_state_lock();
    reset_telemetry_state();
    _reset_backpressure_for_tests();
    _reset_sampling_for_tests();
    reset_trace_context();
    set_queue_policy(QueuePolicy {
        logs_maxsize: 0,
        traces_maxsize: 1,
        metrics_maxsize: 0,
    });

    let held = try_acquire(Signal::Traces).expect("first acquire should succeed");
    let before = get_health_snapshot();
    let result = trace("unit.trace", || {
        let active = get_trace_context();
        assert_eq!(active.get("trace_id"), Some(&None));
        assert_eq!(active.get("span_id"), Some(&None));
        13_i32
    });
    release(held);

    assert_eq!(result, 13);
    let after = get_health_snapshot();
    assert_eq!(after.emitted_traces, before.emitted_traces);
    assert!(
        after.dropped_traces > before.dropped_traces,
        "queue rejection must increment dropped traces at least once"
    );
}

#[test]
fn tracer_test_active_trace_drop_without_ticket_is_a_noop() {
    let _guard = acquire_test_state_lock();
    reset_telemetry_state();

    let active = ActiveTrace {
        ticket: None,
        noop_span: None,
        #[cfg(feature = "otel")]
        otel_span: None,
    };

    drop(active);
}
// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
// SPDX-License-Identifier: Apache-2.0
// SPDX-Comment: Part of provide-telemetry.
//