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::HashMap;

use serde::{Deserialize, Serialize};

mod from_env;
mod parse;
pub(crate) mod probe;
mod redact;
mod validate;

pub use redact::redact_config;

/// Ceiling on exporter retries per signal, shared with the resilience layer's
/// `MAX_EXPORT_ATTEMPTS` (retries + the first attempt). Mirrors TypeScript's
/// `MAX_EXPORT_ATTEMPTS = 101`, so the same `PROVIDE_EXPORTER_*_RETRIES` value
/// is accepted or rejected identically in every language.
pub(crate) const MAX_EXPORTER_RETRIES: usize = 100;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct RuntimeOverrides {
    pub sampling: Option<SamplingConfig>,
    pub backpressure: Option<BackpressureConfig>,
    pub exporter: Option<ExporterPolicyConfig>,
    pub security: Option<SecurityConfig>,
    pub slo: Option<SLOConfig>,
    pub pii_max_depth: Option<usize>,
    pub strict_schema: Option<bool>,
    pub event_schema: Option<EventSchemaConfig>,
    /// Hot-reloadable logging overrides. When `Some(cfg)`, the logger is
    /// reconfigured so subsequent log events honor the new level, format,
    /// and module-level thresholds. Matches Python's reference behavior.
    pub logging: Option<LoggingConfig>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
    pub level: String,
    pub fmt: String,
    /// Whether to include an ISO 8601 timestamp in JSON log output.
    /// Controlled by `PROVIDE_LOG_INCLUDE_TIMESTAMP` (default: true).
    pub include_timestamp: bool,
    pub otlp_headers: HashMap<String, String>,
    /// OTLP endpoint URL for logs export. Falls back to the shared
    /// `OTEL_EXPORTER_OTLP_ENDPOINT` when `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`
    /// is unset. `None` means no endpoint configured.
    pub otlp_endpoint: Option<String>,
    /// Per-signal kill switch for the OTLP log provider. When false, the
    /// logger provider is skipped even if `otlp_endpoint` is set — useful
    /// to escape shutdown hangs against unreachable collectors without
    /// unsetting `OTEL_EXPORTER_OTLP_ENDPOINT`. Controlled by
    /// `PROVIDE_LOG_OTLP_ENABLED` (default: true).
    pub otlp_enabled: bool,
    /// OTLP transport protocol for logs. Empty string means default
    /// (resolved at exporter-build time to `http/protobuf`). Values:
    /// `http/protobuf`, `http/json`, `grpc` (the latter requires the
    /// `otel-grpc` cargo feature).
    pub otlp_protocol: String,
    /// Per-module log level overrides. Keys are module-name prefixes
    /// (longest-prefix wins); values are level strings (TRACE/DEBUG/
    /// INFO/WARN/ERROR). Controlled by `PROVIDE_LOG_MODULE_LEVELS`.
    pub module_levels: HashMap<String, String>,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            level: "INFO".to_string(),
            fmt: "console".to_string(),
            include_timestamp: true,
            otlp_headers: HashMap::new(),
            otlp_endpoint: None,
            otlp_enabled: true,
            otlp_protocol: String::new(),
            module_levels: HashMap::new(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct TracingConfig {
    pub enabled: bool,
    /// Per-signal sample rate for traces (PROVIDE_TRACE_SAMPLE_RATE).
    /// Combined with sampling.traces_rate via min() in apply_policies.
    pub sample_rate: f64,
    pub otlp_headers: HashMap<String, String>,
    /// OTLP endpoint URL for traces export. Falls back to the shared
    /// `OTEL_EXPORTER_OTLP_ENDPOINT` when `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
    /// is unset.
    pub otlp_endpoint: Option<String>,
    /// OTLP transport protocol for traces. See `LoggingConfig::otlp_protocol`.
    pub otlp_protocol: String,
}

impl Default for TracingConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            sample_rate: 1.0,
            otlp_headers: HashMap::new(),
            otlp_endpoint: None,
            otlp_protocol: String::new(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct MetricsConfig {
    pub enabled: bool,
    pub otlp_headers: HashMap<String, String>,
    /// OTLP endpoint URL for metrics export. Falls back to the shared
    /// `OTEL_EXPORTER_OTLP_ENDPOINT` when `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`
    /// is unset.
    pub otlp_endpoint: Option<String>,
    /// OTLP transport protocol for metrics. See `LoggingConfig::otlp_protocol`.
    pub otlp_protocol: String,
    /// How often (in milliseconds) the `PeriodicReader` pushes metrics to the
    /// OTLP endpoint. Parsed from `OTEL_METRIC_EXPORT_INTERVAL` (OTel spec).
    /// Default: 60 000 ms (60 seconds).
    pub metric_export_interval_ms: u64,
}

fn default_metric_export_interval_ms() -> u64 {
    60_000
}

impl Default for MetricsConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            otlp_headers: HashMap::new(),
            otlp_endpoint: None,
            otlp_protocol: String::new(),
            metric_export_interval_ms: default_metric_export_interval_ms(),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct EventSchemaConfig {
    pub strict_event_name: bool,
    pub required_keys: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SamplingConfig {
    pub logs_rate: f64,
    pub traces_rate: f64,
    pub metrics_rate: f64,
}

impl Default for SamplingConfig {
    fn default() -> Self {
        Self {
            logs_rate: 1.0,
            traces_rate: 1.0,
            metrics_rate: 1.0,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct BackpressureConfig {
    pub logs_maxsize: usize,
    pub traces_maxsize: usize,
    pub metrics_maxsize: usize,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ExporterPolicyConfig {
    pub logs_retries: usize,
    pub traces_retries: usize,
    pub metrics_retries: usize,
    pub logs_backoff_seconds: f64,
    pub traces_backoff_seconds: f64,
    pub metrics_backoff_seconds: f64,
    pub logs_timeout_seconds: f64,
    pub traces_timeout_seconds: f64,
    pub metrics_timeout_seconds: f64,
    /// Hard deadline for `shutdown_telemetry(None)`'s flush+shutdown sequence
    /// per signal (seconds). When the OTLP endpoint is unreachable the OTel
    /// SDK's `force_flush()`/`shutdown()` can sit in its internal retry
    /// loop; this deadline forces `shutdown_telemetry(None)` to return. Mirrors
    /// `PROVIDE_EXPORTER_LOGS_SHUTDOWN_TIMEOUT_SECONDS`. Default 5.0.
    pub logs_shutdown_timeout_seconds: f64,
    pub logs_fail_open: bool,
    pub traces_fail_open: bool,
    pub metrics_fail_open: bool,
}

impl Default for ExporterPolicyConfig {
    fn default() -> Self {
        Self {
            logs_retries: 0,
            traces_retries: 0,
            metrics_retries: 0,
            logs_backoff_seconds: 0.0,
            traces_backoff_seconds: 0.0,
            metrics_backoff_seconds: 0.0,
            logs_timeout_seconds: 10.0,
            traces_timeout_seconds: 10.0,
            metrics_timeout_seconds: 10.0,
            logs_shutdown_timeout_seconds: 5.0,
            logs_fail_open: true,
            traces_fail_open: true,
            metrics_fail_open: true,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SLOConfig {
    pub enable_red_metrics: bool,
    pub enable_use_metrics: bool,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SecurityConfig {
    pub max_attr_value_length: usize,
    pub max_attr_count: usize,
    pub max_nesting_depth: usize,
}

impl Default for SecurityConfig {
    fn default() -> Self {
        Self {
            max_attr_value_length: 1024,
            max_attr_count: 64,
            max_nesting_depth: 8,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct TelemetryConfig {
    pub service_name: String,
    pub environment: String,
    pub version: String,
    pub strict_schema: bool,
    pub pii_max_depth: usize,
    pub logging: LoggingConfig,
    pub tracing: TracingConfig,
    pub metrics: MetricsConfig,
    pub event_schema: EventSchemaConfig,
    pub sampling: SamplingConfig,
    pub backpressure: BackpressureConfig,
    pub exporter: ExporterPolicyConfig,
    pub slo: SLOConfig,
    pub security: SecurityConfig,
}

impl Default for TelemetryConfig {
    fn default() -> Self {
        Self {
            service_name: "provide-service".to_string(),
            environment: "dev".to_string(),
            version: "0.0.0".to_string(),
            strict_schema: false,
            pii_max_depth: 8,
            logging: LoggingConfig::default(),
            tracing: TracingConfig::default(),
            metrics: MetricsConfig::default(),
            event_schema: EventSchemaConfig::default(),
            sampling: SamplingConfig::default(),
            backpressure: BackpressureConfig::default(),
            exporter: ExporterPolicyConfig::default(),
            slo: SLOConfig::default(),
            security: SecurityConfig::default(),
        }
    }
}