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 serde_json::{json, Value};

fn capture_record(message: &str) -> Value {
    provide_telemetry::enable_json_capture_for_tests();
    let _guard = provide_telemetry::set_trace_context(
        Some("0af7651916cd43dd8448eb211c80319c".to_string()),
        Some("b7ad6b7169203331".to_string()),
    );
    provide_telemetry::get_logger(Some("probe")).info(message);
    let output = String::from_utf8(provide_telemetry::take_json_capture()).expect("utf8 output");
    for line in output.lines() {
        if let Ok(value) = serde_json::from_str::<Value>(line) {
            return value;
        }
    }
    panic!("no JSON object found in output: {output:?}");
}

fn emit_debug_capture(name: &str, message: &str) -> Vec<Value> {
    provide_telemetry::enable_json_capture_for_tests();
    provide_telemetry::get_logger(Some(name)).debug(message);
    let output = String::from_utf8(provide_telemetry::take_json_capture()).expect("utf8 output");
    output
        .lines()
        .filter_map(|line| serde_json::from_str::<Value>(line).ok())
        .collect()
}

fn has_message(records: &[Value], message: &str) -> bool {
    records.iter().any(|rec| {
        rec.get("message")
            .and_then(Value::as_str)
            .map(|m| m == message)
            .unwrap_or(false)
    })
}

fn main() {
    let case = std::env::var("PROVIDE_PARITY_PROBE_CASE").expect("probe case");
    let result = match case.as_str() {
        "lazy_init_logger" => json!({
            "case": case,
            "record": capture_record("log.output.parity"),
        }),
        "lazy_logger_shutdown_re_setup" => {
            let first = capture_record("log.output.parity");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            let second = provide_telemetry::get_runtime_status();
            std::env::set_var("PROVIDE_TELEMETRY_SERVICE_NAME", "probe-restarted");
            std::env::set_var("PROVIDE_TELEMETRY_ENV", "parity-restarted");
            std::env::set_var("PROVIDE_TELEMETRY_VERSION", "9.9.9");
            provide_telemetry::setup_telemetry(None).expect("second setup");
            let third = provide_telemetry::get_runtime_status();
            let restarted = capture_record("log.output.restart");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "first_logger_emitted": first.get("message") == Some(&Value::String("log.output.parity".to_string())),
                "shutdown_cleared_setup": !second.setup_done,
                "shutdown_cleared_providers": !second.providers.logs
                    && !second.providers.traces
                    && !second.providers.metrics,
                "shutdown_fallback_all": second.fallback.logs
                    && second.fallback.traces
                    && second.fallback.metrics,
                "re_setup_done": third.setup_done,
                "second_logger_uses_fresh_config": restarted.get("service") == Some(&Value::String("probe-restarted".to_string()))
                    && restarted.get("env") == Some(&Value::String("parity-restarted".to_string()))
                    && restarted.get("version") == Some(&Value::String("9.9.9".to_string())),
            })
        }
        "strict_schema_rejection" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let record = capture_record("Bad.Event.Ok");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "emitted": true,
                "schema_error": record.get("_schema_error").is_some(),
            })
        }
        "strict_event_name_only" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let record = capture_record("Bad.Event.Ok");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "emitted": true,
                "schema_error": record.get("_schema_error").is_some(),
            })
        }
        "required_keys_rejection" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let record = capture_record("user.auth.ok");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "emitted": true,
                "schema_error": record.get("_schema_error").is_some(),
            })
        }
        "invalid_config" => json!({
            "case": case,
            "raised": provide_telemetry::setup_telemetry(None).is_err(),
        }),
        "fail_open_exporter_init" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let status = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "setup_done": status.setup_done,
                "providers_cleared": !status.providers.logs && !status.providers.traces && !status.providers.metrics,
                "fallback_all": status.fallback.logs && status.fallback.traces && status.fallback.metrics,
            })
        }
        "signal_enablement" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let status = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "setup_done": status.setup_done,
                "logs_enabled": status.signals.logs,
                "traces_enabled": status.signals.traces,
                "metrics_enabled": status.signals.metrics,
            })
        }
        "per_signal_logs_endpoint" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let status = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "setup_done": status.setup_done,
                "logs_provider": status.providers.logs,
                "traces_provider": status.providers.traces,
                "metrics_provider": status.providers.metrics,
            })
        }
        // Counter, gauge and histogram output — the values, not just the flags.
        "metric_instrument_values" => {
            provide_telemetry::setup_telemetry(None).expect("setup");

            let c = provide_telemetry::counter("probe.metric.counter", None, None);
            c.add(1.0, None);
            c.add(2.0, None);
            c.add(4.0, None);

            let g = provide_telemetry::gauge("probe.metric.gauge", None, None);
            g.set(42.0, None);

            let h = provide_telemetry::histogram("probe.metric.histogram", None, None);
            for value in [1.0_f64, 2.0, 3.0] {
                h.record(value, None);
            }

            let result = json!({
                "case": case,
                "counter_value": format!("{}", c.value() as i64),
                "gauge_value": format!("{}", g.value() as i64),
                "histogram_count": format!("{}", h.count()),
                "histogram_total": format!("{}", h.total() as i64),
            });
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            result
        }
        // A host application's own SDK provider must be adopted, and adoption
        // must be gated on the signal being enabled.
        //
        // Rust is the one facade that cannot detect this for itself:
        // opentelemetry::global::tracer_provider() returns an opaque
        // GlobalTracerProvider with no downcast and no is_noop, so the host
        // asserts it via adopt_global_providers instead. The observable flags
        // below are identical to the other three facades — that is the point of
        // the case. shutdown_telemetry releases the assertion (it never owned
        // the host's providers), so the second leg re-asserts it.
        // A host application's own SDK provider must be adopted, and adoption
        // must be gated on the signal being enabled.
        //
        // Rust is the one facade that cannot detect this for itself:
        // opentelemetry::global::tracer_provider() returns an opaque
        // GlobalTracerProvider with no downcast and no is_noop, so the host
        // asserts it via adopt_global_providers instead. The observable flags
        // below are identical to the other three facades — that is the point of
        // the case. shutdown_telemetry releases the assertion (it never owned
        // the host's providers), so the second leg re-asserts it.
        //
        // Gated on the otel feature because the SDK crates it installs are
        // optional deps; the harness lists this case as OTel-required and runs
        // the probe with --features otel.
        "host_provider_adoption" => {
            #[cfg(not(feature = "otel"))]
            {
                panic!("host_provider_adoption requires --features otel");
            }
            #[cfg(feature = "otel")]
            {
                let install_host_provider = || {
                    opentelemetry::global::set_tracer_provider(
                        opentelemetry_sdk::trace::SdkTracerProvider::builder().build(),
                    );
                    provide_telemetry::adopt_global_providers(
                        provide_telemetry::AdoptedProviders::all(),
                    );
                };

                install_host_provider();
                let before = provide_telemetry::get_runtime_status();

                // setup_telemetry(None) reads the environment, so enablement is set there.
                std::env::set_var("PROVIDE_TRACE_ENABLED", "true");
                provide_telemetry::setup_telemetry(None).expect("setup enabled");
                let enabled = provide_telemetry::get_runtime_status();
                provide_telemetry::shutdown_telemetry(None).expect("shutdown");

                install_host_provider();
                std::env::set_var("PROVIDE_TRACE_ENABLED", "false");
                provide_telemetry::setup_telemetry(None).expect("setup disabled");
                let disabled = provide_telemetry::get_runtime_status();
                provide_telemetry::shutdown_telemetry(None).expect("shutdown");
                std::env::remove_var("PROVIDE_TRACE_ENABLED");

                json!({
                    "case": case,
                    "adopted_before_setup": before.providers.traces,
                    "adopted_after_enabled_setup": enabled.providers.traces,
                    "fallback_after_disabled_setup": disabled.fallback.traces,
                })
            }
        }
        "provider_identity_reconfigure" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let before = provide_telemetry::get_runtime_status();
            let service_before = provide_telemetry::get_runtime_config()
                .expect("runtime config")
                .service_name;
            let mut changed = provide_telemetry::get_runtime_config().expect("runtime config");
            changed.service_name = format!("{service_before}-renamed");
            let raised = provide_telemetry::reconfigure_telemetry(Some(changed)).is_err();
            let config_preserved = provide_telemetry::get_runtime_config()
                .expect("runtime config")
                .service_name
                == service_before;
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "providers_active": before.providers.logs || before.providers.traces || before.providers.metrics,
                "raised": raised,
                "config_preserved": config_preserved,
            })
        }
        "shutdown_re_setup" => {
            provide_telemetry::setup_telemetry(None).expect("first setup");
            let first = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            let second = provide_telemetry::get_runtime_status();
            provide_telemetry::setup_telemetry(None).expect("second setup");
            let third = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "first_setup_done": first.setup_done,
                "shutdown_cleared_setup": !second.setup_done,
                "shutdown_cleared_providers": !second.providers.logs
                    && !second.providers.traces
                    && !second.providers.metrics,
                "shutdown_fallback_all": second.fallback.logs
                    && second.fallback.traces
                    && second.fallback.metrics,
                "re_setup_done": third.setup_done,
                "signals_match": first.signals == third.signals,
                "providers_match": first.providers == third.providers,
            })
        }
        "hot_reload_log_level" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let service_before = provide_telemetry::get_runtime_config()
                .expect("runtime config")
                .service_name;
            let before = emit_debug_capture("probe", "hot.level.debug.before");
            let next_logging = provide_telemetry::LoggingConfig {
                level: "DEBUG".to_string(),
                fmt: "json".to_string(),
                include_timestamp: false,
                ..provide_telemetry::LoggingConfig::default()
            };
            provide_telemetry::update_runtime_config(provide_telemetry::RuntimeOverrides {
                logging: Some(next_logging),
                ..provide_telemetry::RuntimeOverrides::default()
            })
            .expect("update must succeed");
            let after = emit_debug_capture("probe", "hot.level.debug.after");
            let cfg = provide_telemetry::get_runtime_config().expect("runtime config");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "first_debug_suppressed": !has_message(&before, "hot.level.debug.before"),
                "second_debug_emitted": has_message(&after, "hot.level.debug.after"),
                "level_config_updated": cfg.logging.level.eq_ignore_ascii_case("DEBUG"),
                "service_preserved": cfg.service_name == service_before,
            })
        }
        "hot_reload_log_format" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let status_before = provide_telemetry::get_runtime_status();
            let service_before = provide_telemetry::get_runtime_config()
                .expect("runtime config")
                .service_name;
            let next_logging = provide_telemetry::LoggingConfig {
                level: "INFO".to_string(),
                fmt: "console".to_string(),
                include_timestamp: false,
                ..provide_telemetry::LoggingConfig::default()
            };
            provide_telemetry::update_runtime_config(provide_telemetry::RuntimeOverrides {
                logging: Some(next_logging),
                ..provide_telemetry::RuntimeOverrides::default()
            })
            .expect("update must succeed");
            let cfg = provide_telemetry::get_runtime_config().expect("runtime config");
            let status_after = provide_telemetry::get_runtime_status();
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "format_config_updated": cfg.logging.fmt.eq_ignore_ascii_case("console"),
                "service_preserved": cfg.service_name == service_before,
                "providers_unchanged": status_before.providers == status_after.providers,
            })
        }
        "hot_reload_module_level" => {
            provide_telemetry::setup_telemetry(None).expect("setup");
            let service_before = provide_telemetry::get_runtime_config()
                .expect("runtime config")
                .service_name;
            let before = emit_debug_capture("probe.child", "hot.module.debug.before");
            // Pure module-only promotion: the global level stays at INFO and
            // only the module override lifts `probe.child` to DEBUG.  All
            // four languages must honour this precise contract.
            let mut next_logging = provide_telemetry::LoggingConfig {
                fmt: "json".to_string(),
                include_timestamp: false,
                ..provide_telemetry::LoggingConfig::default()
            };
            next_logging
                .module_levels
                .insert("probe.child".to_string(), "DEBUG".to_string());
            provide_telemetry::update_runtime_config(provide_telemetry::RuntimeOverrides {
                logging: Some(next_logging),
                ..provide_telemetry::RuntimeOverrides::default()
            })
            .expect("update must succeed");
            let after = emit_debug_capture("probe.child", "hot.module.debug.after");
            let cfg = provide_telemetry::get_runtime_config().expect("runtime config");
            provide_telemetry::shutdown_telemetry(None).expect("shutdown");
            json!({
                "case": case,
                "first_debug_suppressed": !has_message(&before, "hot.module.debug.before"),
                "module_debug_emitted": has_message(&after, "hot.module.debug.after"),
                "module_levels_config_updated": cfg
                    .logging
                    .module_levels
                    .get("probe.child")
                    .map(|v| v.eq_ignore_ascii_case("DEBUG"))
                    .unwrap_or(false),
                "service_preserved": cfg.service_name == service_before,
            })
        }
        _ => panic!("unknown case: {case}"),
    };
    println!(
        "{}",
        serde_json::to_string(&result).expect("serialize result")
    );
}