clnrm_core/telemetry/
init.rs

1//! Telemetry initialization for clnrm
2//!
3//! Provides comprehensive OpenTelemetry setup with support for multiple exporters
4//! and proper resource configuration.
5
6use crate::error::Result;
7use crate::telemetry::config::{ExporterConfig, OtlpProtocol, TelemetryConfig};
8use crate::telemetry::exporters::{
9    create_span_exporter, validate_exporter_config, SpanExporterType,
10};
11use opentelemetry::global;
12use opentelemetry::trace::TracerProvider;
13use opentelemetry::KeyValue;
14use opentelemetry_sdk::{
15    trace::{self, RandomIdGenerator, Sampler},
16    Resource,
17};
18use tracing_opentelemetry;
19use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
20
21/// Handle for managing telemetry lifecycle
22#[derive(Debug)]
23pub struct TelemetryHandle {
24    config: TelemetryConfig,
25}
26
27impl TelemetryHandle {
28    /// Create a disabled telemetry handle
29    pub fn disabled() -> Self {
30        Self {
31            config: TelemetryConfig {
32                enabled: false,
33                ..Default::default()
34            },
35        }
36    }
37
38    /// Check if telemetry is enabled
39    pub fn is_enabled(&self) -> bool {
40        self.config.enabled
41    }
42
43    /// Get the service name
44    pub fn service_name(&self) -> &str {
45        &self.config.service_name
46    }
47
48    /// Get the service version
49    pub fn service_version(&self) -> &str {
50        &self.config.service_version
51    }
52
53    /// Shutdown telemetry (no-op for disabled handle)
54    pub fn shutdown(self) -> Result<()> {
55        if self.config.enabled {
56            // Shutdown is handled automatically by the SDK
57            // No explicit shutdown needed in OpenTelemetry 0.31.0
58        }
59        Ok(())
60    }
61}
62
63/// Builder for telemetry configuration
64pub struct TelemetryBuilder {
65    config: TelemetryConfig,
66}
67
68impl TelemetryBuilder {
69    /// Create a new telemetry builder
70    pub fn new(config: TelemetryConfig) -> Self {
71        Self { config }
72    }
73
74    /// Initialize telemetry with the given configuration
75    pub fn init(self) -> Result<TelemetryHandle> {
76        if !self.config.enabled {
77            return Ok(TelemetryHandle::disabled());
78        }
79
80        // Initialize tracing without resource for now
81        self.init_tracing()?;
82
83        // Initialize metrics without resource for now
84        self.init_metrics()?;
85
86        Ok(TelemetryHandle {
87            config: self.config,
88        })
89    }
90
91    /// Create OpenTelemetry resource
92    fn create_resource(&self) -> Result<Resource> {
93        // Build resource with service information
94        let mut resource_builder = Resource::builder_empty()
95            .with_service_name(self.config.service_name.clone())
96            .with_attributes([
97                KeyValue::new("service.version", self.config.service_version.clone()),
98                KeyValue::new("telemetry.sdk.language", "rust"),
99                KeyValue::new("telemetry.sdk.name", "opentelemetry"),
100                KeyValue::new("telemetry.sdk.version", "0.31.0"),
101                KeyValue::new(
102                    "service.instance.id",
103                    format!("clnrm-{}", std::process::id()),
104                ),
105            ]);
106
107        // Add custom resource attributes from configuration
108        for (key, value) in &self.config.resource_attributes {
109            resource_builder =
110                resource_builder.with_attributes([KeyValue::new(key.clone(), value.clone())]);
111        }
112
113        let resource = resource_builder.build();
114        Ok(resource)
115    }
116
117    /// Create exporters from configuration
118    fn create_exporters(&self) -> Result<Vec<SpanExporterType>> {
119        let mut exporters = Vec::new();
120
121        for exporter_config in &self.config.exporters {
122            // Validate configuration first
123            validate_exporter_config(exporter_config)?;
124
125            // Create the exporter
126            let exporter = create_span_exporter(exporter_config)?;
127            exporters.push(exporter);
128        }
129
130        Ok(exporters)
131    }
132
133    /// Initialize tracing with OpenTelemetry
134    fn init_tracing(&self) -> Result<()> {
135        // Create resource with service information
136        let resource = self.create_resource()?;
137
138        let tracer_provider_builder = trace::SdkTracerProvider::builder()
139            .with_sampler(Sampler::TraceIdRatioBased(
140                self.config.sampling.trace_sampling_ratio,
141            ))
142            .with_id_generator(RandomIdGenerator::default())
143            .with_resource(resource);
144
145        // Create exporters based on configuration
146        let exporters = self.create_exporters()?;
147
148        // Use the first exporter for now (multi-exporter support can be added later)
149        if let Some(exporter) = exporters.into_iter().next() {
150            let tracer_provider = tracer_provider_builder
151                .with_batch_exporter(exporter)
152                .build();
153            let tracer = tracer_provider.tracer("clnrm");
154
155            global::set_tracer_provider(tracer_provider);
156
157            tracing_subscriber::registry()
158                .with(tracing_opentelemetry::layer().with_tracer(tracer))
159                .with(tracing_subscriber::fmt::layer())
160                .init();
161        } else {
162            // Fallback to in-memory exporter if no exporters configured
163            let exporter = opentelemetry_sdk::trace::InMemorySpanExporter::default();
164            let tracer_provider = tracer_provider_builder
165                .with_batch_exporter(exporter)
166                .build();
167            let tracer = tracer_provider.tracer("clnrm");
168
169            global::set_tracer_provider(tracer_provider);
170
171            tracing_subscriber::registry()
172                .with(tracing_opentelemetry::layer().with_tracer(tracer))
173                .with(tracing_subscriber::fmt::layer())
174                .init();
175        }
176
177        Ok(())
178    }
179
180    /// Initialize metrics with OpenTelemetry
181    fn init_metrics(&self) -> Result<()> {
182        // Create resource with service information
183        let resource = self.create_resource()?;
184
185        // Initialize metrics provider with resource
186        let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
187            .with_resource(resource)
188            .build();
189
190        global::set_meter_provider(meter_provider);
191
192        Ok(())
193    }
194}
195
196/// Initialize telemetry with default configuration
197pub fn init_default() -> Result<TelemetryHandle> {
198    let config = TelemetryConfig::default();
199    TelemetryBuilder::new(config).init()
200}
201
202/// Initialize telemetry with OTLP configuration
203pub fn init_otlp(endpoint: &str) -> Result<TelemetryHandle> {
204    let config = TelemetryConfig {
205        enabled: true,
206        exporters: vec![ExporterConfig::Otlp {
207            endpoint: endpoint.to_string(),
208            protocol: OtlpProtocol::HttpProto,
209            headers: std::collections::HashMap::new(),
210        }],
211        ..Default::default()
212    };
213    TelemetryBuilder::new(config).init()
214}
215
216/// Initialize telemetry with stdout configuration for development
217pub fn init_stdout() -> Result<TelemetryHandle> {
218    let config = TelemetryConfig {
219        enabled: true,
220        exporters: vec![ExporterConfig::Stdout { pretty_print: true }],
221        ..Default::default()
222    };
223    TelemetryBuilder::new(config).init()
224}