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.
//

#![allow(dead_code)]

use std::collections::HashMap;
use std::env;
use std::sync::OnceLock;
use std::time::Duration;

use opentelemetry::global;
use opentelemetry::propagation::{Extractor, Injector};
use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::trace::SdkTracerProvider;

pub struct HeaderExtractor<'a> {
    headers: &'a HashMap<String, String>,
}

impl<'a> HeaderExtractor<'a> {
    pub fn new(headers: &'a HashMap<String, String>) -> Self {
        Self { headers }
    }
}

impl Extractor for HeaderExtractor<'_> {
    fn get(&self, key: &str) -> Option<&str> {
        self.headers
            .get(&key.to_ascii_lowercase())
            .map(std::string::String::as_str)
    }

    fn keys(&self) -> Vec<&str> {
        self.headers
            .keys()
            .map(std::string::String::as_str)
            .collect()
    }
}

pub struct HeaderInjector<'a> {
    headers: &'a mut HashMap<String, String>,
}

impl<'a> HeaderInjector<'a> {
    pub fn new(headers: &'a mut HashMap<String, String>) -> Self {
        Self { headers }
    }
}

impl Injector for HeaderInjector<'_> {
    fn set(&mut self, key: &str, value: String) {
        self.headers.insert(key.to_ascii_lowercase(), value);
    }
}

pub fn parse_headers_env(value: &str) -> HashMap<String, String> {
    let mut headers = HashMap::new();
    for pair in value.split(',') {
        let Some((key, header_value)) = pair.split_once('=') else {
            continue;
        };
        let key = key.trim();
        if key.is_empty() {
            continue;
        }
        headers.insert(key.to_string(), header_value.trim().to_string());
    }
    headers
}

pub fn traces_endpoint_from_env() -> Result<String, String> {
    if let Ok(value) = env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") {
        return Ok(value);
    }
    if let Ok(value) = env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
        return Ok(format!("{}/v1/traces", value.trim_end_matches('/')));
    }
    Err("OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is required".to_string())
}

// The OTLP exporter uses reqwest's *async* client (Cargo.toml:
// `reqwest-client`), and SimpleSpanProcessor drives each export with
// futures_executor::block_on — reqwest's sockets and timeout need an ambient
// tokio reactor at poll time or they panic with "no reactor running". These
// examples are synchronous binaries, so permanently enter a process-lifetime
// runtime context on the calling thread; the runtime's worker threads drive
// the reactor while the export blocks. All span end/flush/shutdown calls must
// happen on the thread that called init_tracer_provider.
fn enter_export_runtime() {
    static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
    let runtime = RUNTIME.get_or_init(|| {
        tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .thread_name("e2e-otlp-export")
            .build()
            .expect("failed to build tokio runtime for OTLP export")
    });
    std::mem::forget(runtime.enter());
}

pub fn init_tracer_provider(_service_name: &str) -> Result<SdkTracerProvider, String> {
    enter_export_runtime();
    let endpoint = traces_endpoint_from_env()?;
    let headers = env::var("OTEL_EXPORTER_OTLP_HEADERS")
        .map(|value| parse_headers_env(&value))
        .unwrap_or_default();

    global::set_text_map_propagator(TraceContextPropagator::new());

    let exporter = SpanExporter::builder()
        .with_http()
        .with_endpoint(endpoint)
        .with_timeout(Duration::from_secs(10))
        .with_headers(headers)
        .build()
        .map_err(|err| format!("failed to build OTLP exporter: {err}"))?;

    let provider = SdkTracerProvider::builder()
        .with_simple_exporter(exporter)
        .build();

    Ok(provider)
}