foxy/opentelemetry/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! OpenTelemetry bootstrap for Foxy
6//!
7//! This module centralises **all** tracing/OpenTelemetry initialisation so the
8//! rest of the code base only needs a single call.
9//!
10//! * Feature‑gated behind `opentelemetry` – compiling without the feature
11//!   turns every public item into a no‑op.
12//! * Reads `endpoint`, `service_name`, **optional custom request headers** and
13//!   **static resource attributes** from the proxy configuration block.
14//! * Uses the **new 0.29 API** (no `new_exporter`, no `pipeline` helpers).
15//! * Builds a **batch** tracing provider and installs `tracing_subscriber`
16//!   with `EnvFilter` so runtime `RUST_LOG` works as before.
17
18#![allow(clippy::single_match)]
19
20use std::collections::HashMap;
21use std::fmt::Display;
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24#[cfg(feature = "opentelemetry")]
25use {
26    opentelemetry_otlp::{WithTonicConfig},
27    opentelemetry::{global, KeyValue},
28    opentelemetry_sdk::{trace::SdkTracerProvider, Resource},
29    opentelemetry_otlp::{SpanExporter, WithExportConfig},
30    tonic::metadata::{MetadataMap, MetadataValue},
31    opentelemetry_sdk::propagation::TraceContextPropagator,
32    opentelemetry_semantic_conventions::attribute::{
33        SERVICE_VERSION, SERVICE_INSTANCE_ID, DEPLOYMENT_ENVIRONMENT
34    }
35};
36
37/// Errors that can occur during OpenTelemetry operations.
38#[derive(Error, Debug)]
39pub enum OpenTelemetryError {
40    /// Configuration error
41    #[error("configuration error: {0}")]
42    ConfigError(#[from] crate::config::error::ConfigError),
43
44    /// OpenTelemetry initialization error
45    #[error("OpenTelemetry initialization error: {0}")]
46    InitError(String),
47}
48
49/// Configuration for the OpenTelemetry integration.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct OpenTelemetryConfig {
52    /// The endpoint URL for the OpenTelemetry collector.
53    #[serde(default = "default_endpoint")]
54    pub endpoint: String,
55
56    /// The service name to use for traces.
57    #[serde(default = "default_service_name")]
58    pub service_name: String,
59
60    /// Whether to include request and response headers in spans.
61    #[serde(default = "default_include_headers")]
62    pub include_headers: bool,
63
64    /// Whether to include request and response bodies in spans.
65    #[serde(default = "default_include_bodies")]
66    pub include_bodies: bool,
67
68    /// Maximum body size to include in spans (in bytes).
69    #[serde(default = "default_max_body_size")]
70    pub max_body_size: usize,
71
72    /// Custom span annotations to add to all spans.
73    /// These are key-value pairs that will be added as attributes to all spans.
74    #[serde(default)]
75    pub span_annotations: HashMap<String, String>,
76
77    /// Custom headers to add to the OpenTelemetry collector requests.
78    /// These are key-value pairs that will be added as headers to all collector requests.
79    #[serde(default)]
80    pub collector_headers: HashMap<String, String>,
81
82    /// Custom resource attributes to add to the OpenTelemetry resource.
83    /// These are key-value pairs that will be added as resource attributes to all spans.
84    /// Resource attributes are different from span annotations as they are applied at the tracer level
85    /// and appear on all spans created by the tracer.
86    #[serde(default)]
87    pub resource_attributes: HashMap<String, String>,
88}
89
90fn default_endpoint() -> String {
91    "http://localhost:4317".to_string()
92}
93
94fn default_service_name() -> String {
95    "foxy-proxy".to_string()
96}
97
98fn default_include_headers() -> bool {
99    true
100}
101
102fn default_include_bodies() -> bool {
103    false
104}
105
106fn default_max_body_size() -> usize {
107    1024
108}
109
110impl Default for OpenTelemetryConfig {
111    fn default() -> Self {
112        Self {
113            endpoint: default_endpoint(),
114            service_name: default_service_name(),
115            include_headers: default_include_headers(),
116            include_bodies: default_include_bodies(),
117            max_body_size: default_max_body_size(),
118            span_annotations: HashMap::new(),
119            collector_headers: HashMap::new(),
120            resource_attributes: HashMap::new(),
121        }
122    }
123}
124
125impl Display for OpenTelemetryConfig {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "OpenTelemetryConfig {{ endpoint: {}, service_name: {}, include_headers: {}, include_bodies: {}, max_body_size: {}, span_annotations: {:?}, collector_headers: {:?}, resource_attributes: {:?} }}", self.endpoint, self.service_name, self.include_headers, self.include_bodies, self.max_body_size, self.span_annotations, self.collector_headers, self.resource_attributes)
128    }
129}
130
131/// Initialise tracing + OpenTelemetry. Safe to call once.
132#[cfg(feature = "opentelemetry")]
133pub fn init(config: Option<OpenTelemetryConfig>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
134    
135    if config.is_some() && ! config.as_ref().unwrap().endpoint.is_empty() {
136        let config_ref = config.as_ref().unwrap();
137
138        global::set_text_map_propagator(TraceContextPropagator::new());
139
140        // ── exporter ───────────────────────────────────────────────
141        let mut exporter_builder = SpanExporter::builder()
142            .with_tonic()
143            .with_endpoint(config_ref.endpoint.clone());
144
145        if !config_ref.collector_headers.is_empty() {
146            let mut meta = MetadataMap::with_capacity(config_ref.collector_headers.len());
147            for (k, v) in &config_ref.collector_headers {
148                if let (Ok(key), Ok(val)) = (
149                    k.parse::<tonic::metadata::MetadataKey<_>>(),
150                    MetadataValue::try_from(v.as_str()),
151                ) {
152                    meta.insert(key, val);
153                }
154            }
155            exporter_builder = exporter_builder.with_metadata(meta);
156        }
157        let exporter = exporter_builder.build().expect("An error occurred building the OpenTelemetry exporter");
158
159        // ── resource ───────────────────────────────────────────────
160        let svc_version   = env!("CARGO_PKG_VERSION");
161        let deploy_env    = std::env::var("FOXY_DEPLOY_ENV").unwrap_or_else(|_| "local".into());
162        let instance_id   = hostname::get()
163            .ok()
164            .and_then(|h| h.into_string().ok())
165            .unwrap_or_else(|| "unknown-host".into());
166        
167        let mut res_builder = Resource::builder().with_service_name(config_ref.service_name.clone())
168            .with_attribute(KeyValue::new(SERVICE_VERSION, svc_version))
169            .with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT, deploy_env))
170            .with_attribute(KeyValue::new(SERVICE_INSTANCE_ID, instance_id));
171        
172        
173        if !config_ref.resource_attributes.is_empty() {
174            let attrs = config_ref.resource_attributes.iter().map(|(k, v)| KeyValue::new(k.clone(), v.clone()));
175            res_builder = res_builder.with_attributes(attrs);
176        };
177        
178        let resource = res_builder.build();
179
180        // ── tracer provider ────────────────────────────────────────
181        let provider = SdkTracerProvider::builder()
182            .with_batch_exporter(exporter)
183            .with_resource(resource)
184            .build();
185        global::set_tracer_provider(provider);
186    }
187
188    Ok(())
189}
190
191/// No‑op version when the feature is disabled.
192#[cfg(not(feature = "opentelemetry"))]
193pub fn init(_cfg: Option<&serde_json::Value>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Ok(()) }
194