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::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use crate::backpressure::{release, try_acquire};
use crate::consent::should_allow;
use crate::health::increment_emitted;
use crate::runtime::get_runtime_config;
use crate::sampling::{should_sample, Signal};

static METRICS_INITIALIZED: AtomicBool = AtomicBool::new(false);

#[cfg(feature = "otel")]
fn maybe_record_counter_add(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
    if !crate::otel::metrics::meter_provider_installed() {
        return;
    }
    crate::otel::metrics::record_counter_add(name, value, attributes);
}

#[cfg(feature = "otel")]
fn maybe_record_gauge_set(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
    if !crate::otel::metrics::meter_provider_installed() {
        return;
    }
    crate::otel::metrics::record_gauge_set(name, value, attributes);
}

#[cfg(feature = "otel")]
fn maybe_record_histogram(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
    if !crate::otel::metrics::meter_provider_installed() {
        return;
    }
    crate::otel::metrics::record_histogram(name, value, attributes);
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Meter {
    name: String,
}

impl Meter {
    pub fn name(&self) -> &str {
        &self.name
    }
}

#[derive(Clone, Debug, Default)]
struct CounterState {
    value: f64,
}

#[derive(Clone, Debug)]
pub struct Counter {
    name: String,
    #[allow(dead_code)]
    description: Option<String>,
    #[allow(dead_code)]
    unit: Option<String>,
    state: Arc<Mutex<CounterState>>,
}

impl Counter {
    pub fn add(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
        if !metrics_enabled() {
            return;
        }
        if !should_allow("metrics", None) {
            return;
        }
        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
            return;
        }
        let acquired = try_acquire(Signal::Metrics);
        if acquired.is_none() {
            return;
        }
        let ticket = acquired.expect("metrics ticket must exist after none guard");
        crate::_lock::lock(&self.state).value += value;
        #[cfg(feature = "otel")]
        {
            maybe_record_counter_add(&self.name, value, attributes.as_ref());
        }
        #[cfg(not(feature = "otel"))]
        let _ = &attributes;
        increment_emitted(Signal::Metrics, 1);
        release(ticket);
    }

    pub fn value(&self) -> f64 {
        crate::_lock::lock(&self.state).value
    }
}

#[derive(Clone, Debug, Default)]
struct GaugeState {
    last_value: f64,
}

#[derive(Clone, Debug)]
pub struct Gauge {
    name: String,
    #[allow(dead_code)]
    description: Option<String>,
    #[allow(dead_code)]
    unit: Option<String>,
    state: Arc<Mutex<GaugeState>>,
}

impl Gauge {
    pub fn add(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
        if !metrics_enabled() {
            return;
        }
        if !should_allow("metrics", None) {
            return;
        }
        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
            return;
        }
        let acquired = try_acquire(Signal::Metrics);
        if acquired.is_none() {
            return;
        }
        let ticket = acquired.expect("metrics ticket must exist after none guard");
        #[cfg_attr(not(feature = "otel"), allow(unused_variables))]
        let new_absolute = {
            let mut state = crate::_lock::lock(&self.state);
            state.last_value += value;
            state.last_value
        };
        #[cfg(feature = "otel")]
        {
            maybe_record_gauge_set(&self.name, new_absolute, attributes.as_ref());
        }
        #[cfg(not(feature = "otel"))]
        let _ = &attributes;
        increment_emitted(Signal::Metrics, 1);
        release(ticket);
    }

    pub fn set(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
        if !metrics_enabled() {
            return;
        }
        if !should_allow("metrics", None) {
            return;
        }
        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
            return;
        }
        let acquired = try_acquire(Signal::Metrics);
        if acquired.is_none() {
            return;
        }
        let ticket = acquired.expect("metrics ticket must exist after none guard");
        crate::_lock::lock(&self.state).last_value = value;
        #[cfg(feature = "otel")]
        {
            maybe_record_gauge_set(&self.name, value, attributes.as_ref());
        }
        #[cfg(not(feature = "otel"))]
        let _ = &attributes;
        increment_emitted(Signal::Metrics, 1);
        release(ticket);
    }

    pub fn value(&self) -> f64 {
        crate::_lock::lock(&self.state).last_value
    }
}

#[derive(Clone, Debug, Default)]
struct HistogramState {
    count: usize,
    total: f64,
}

#[derive(Clone, Debug)]
pub struct Histogram {
    name: String,
    #[allow(dead_code)]
    description: Option<String>,
    #[allow(dead_code)]
    unit: Option<String>,
    state: Arc<Mutex<HistogramState>>,
}

impl Histogram {
    pub fn record(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
        if !metrics_enabled() {
            return;
        }
        if !should_allow("metrics", None) {
            return;
        }
        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
            return;
        }
        let acquired = try_acquire(Signal::Metrics);
        if acquired.is_none() {
            return;
        }
        let ticket = acquired.expect("metrics ticket must exist after none guard");
        let mut state = crate::_lock::lock(&self.state);
        state.count += 1;
        state.total += value;
        drop(state);
        #[cfg(feature = "otel")]
        {
            maybe_record_histogram(&self.name, value, attributes.as_ref());
        }
        #[cfg(not(feature = "otel"))]
        let _ = &attributes;
        increment_emitted(Signal::Metrics, 1);
        release(ticket);
    }

    pub fn count(&self) -> usize {
        crate::_lock::lock(&self.state).count
    }

    pub fn total(&self) -> f64 {
        crate::_lock::lock(&self.state).total
    }
}

fn metrics_enabled() -> bool {
    get_runtime_config()
        .map(|config| config.metrics.enabled)
        .unwrap_or(true)
}

pub fn get_meter(name: Option<&str>) -> Meter {
    Meter {
        name: name.unwrap_or("provide.telemetry").to_string(),
    }
}

pub fn counter(name: &str, description: Option<&str>, unit: Option<&str>) -> Counter {
    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
    Counter {
        name: name.to_string(),
        description: description.map(str::to_string),
        unit: unit.map(str::to_string),
        state: Arc::new(Mutex::new(CounterState::default())),
    }
}

pub fn gauge(name: &str, description: Option<&str>, unit: Option<&str>) -> Gauge {
    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
    Gauge {
        name: name.to_string(),
        description: description.map(str::to_string),
        unit: unit.map(str::to_string),
        state: Arc::new(Mutex::new(GaugeState::default())),
    }
}

pub fn histogram(name: &str, description: Option<&str>, unit: Option<&str>) -> Histogram {
    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
    Histogram {
        name: name.to_string(),
        description: description.map(str::to_string),
        unit: unit.map(str::to_string),
        state: Arc::new(Mutex::new(HistogramState::default())),
    }
}

pub fn metrics_initialized_for_tests() -> bool {
    METRICS_INITIALIZED.load(Ordering::SeqCst)
}

pub fn reset_metrics_for_tests() {
    METRICS_INITIALIZED.store(false, Ordering::SeqCst);
}

#[cfg(test)]
#[path = "metrics_tests.rs"]
mod tests;