1pub mod metrics;
40pub mod prelude;
41pub mod propagation;
42pub mod resource;
43pub mod span;
44pub mod tracing;
45
46use std::env;
47use std::sync::OnceLock;
48use thiserror::Error;
49
50static SERVICE_NAME: OnceLock<String> = OnceLock::new();
51
52pub(crate) fn configured_service_name() -> &'static str {
57 SERVICE_NAME
58 .get()
59 .map_or("sideways-otel-service", String::as_str)
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum OtlpProtocol {
65 #[default]
67 Grpc,
68 HttpProtobuf,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum PropagatorKind {
82 TraceContext,
86 Baggage,
90}
91
92#[derive(Debug, Error)]
93pub enum TelemetryError {
94 #[error("OpenTelemetry tracing disabled via OTEL_TRACES_ENABLED=false")]
95 TracingDisabled,
96
97 #[error("OpenTelemetry metrics disabled via OTEL_METRICS_ENABLED=false")]
98 MetricsDisabled,
99
100 #[error("Failed to build OTLP exporter: {0}")]
101 ExporterBuild(String),
102}
103
104#[derive(Debug, Clone)]
106pub struct TelemetryConfig {
107 pub service_name: String,
109 pub resource_attributes: Vec<(String, String)>,
111
112 pub traces_enabled: bool,
114 pub metrics_enabled: bool,
116 pub logs_enabled: bool,
118
119 pub otlp_protocol: OtlpProtocol,
121 pub otlp_endpoint: Option<String>,
126 pub otlp_headers: Vec<(String, String)>,
128
129 pub rust_log: String,
131 pub json_logging: bool,
133
134 pub metrics_export_interval_ms: u64,
136
137 pub propagators: Vec<PropagatorKind>,
141}
142
143impl Default for TelemetryConfig {
144 fn default() -> Self {
145 Self {
146 service_name: "sideways-otel-service".to_string(),
147 resource_attributes: Vec::new(),
148 traces_enabled: true,
149 metrics_enabled: true,
150 logs_enabled: true,
151 otlp_protocol: OtlpProtocol::default(),
152 otlp_endpoint: None,
153 otlp_headers: Vec::new(),
154 rust_log: "info".to_string(),
155 json_logging: false,
156 metrics_export_interval_ms: 60_000,
157 propagators: vec![PropagatorKind::TraceContext, PropagatorKind::Baggage],
158 }
159 }
160}
161
162impl TelemetryConfig {
163 #[must_use]
165 pub fn from_env() -> Self {
166 let mut config = Self::default();
167
168 if let Ok(name) = env::var("OTEL_SERVICE_NAME") {
169 config.service_name = name;
170 }
171 if let Ok(attrs) = env::var("OTEL_RESOURCE_ATTRIBUTES") {
172 config.resource_attributes = Self::parse_pairs(&attrs, '=');
173 }
174
175 if let Ok(enabled) = env::var("OTEL_TRACES_ENABLED")
176 && enabled.eq_ignore_ascii_case("false")
177 {
178 config.traces_enabled = false;
179 }
180 if let Ok(enabled) = env::var("OTEL_METRICS_ENABLED")
181 && enabled.eq_ignore_ascii_case("false")
182 {
183 config.metrics_enabled = false;
184 }
185 if let Ok(enabled) = env::var("OTEL_LOGS_ENABLED")
186 && enabled.eq_ignore_ascii_case("false")
187 {
188 config.logs_enabled = false;
189 }
190
191 if let Ok(protocol) = env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
192 match protocol.as_str() {
193 "grpc" => config.otlp_protocol = OtlpProtocol::Grpc,
194 "http/protobuf" => config.otlp_protocol = OtlpProtocol::HttpProtobuf,
195 other => eprintln!(
196 "⚠️ Unsupported OTEL_EXPORTER_OTLP_PROTOCOL '{other}' (expected 'grpc' or 'http/protobuf'), defaulting to grpc"
197 ),
198 }
199 }
200 if let Ok(endpoint) = env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
201 config.otlp_endpoint = Some(endpoint);
202 }
203 if let Ok(headers) = env::var("OTEL_EXPORTER_OTLP_HEADERS") {
204 config.otlp_headers = Self::parse_pairs(&headers, '=');
205 }
206
207 if let Ok(rust_log) = env::var("RUST_LOG") {
208 config.rust_log = rust_log;
209 }
210 if let Ok(enabled) = env::var("JSON_LOGGING")
211 && enabled.eq_ignore_ascii_case("true")
212 {
213 config.json_logging = true;
214 }
215 if let Ok(interval) = env::var("OTEL_METRIC_EXPORT_INTERVAL")
216 && let Ok(ms) = interval.parse()
217 {
218 config.metrics_export_interval_ms = ms;
219 }
220
221 if let Ok(propagators) = env::var("OTEL_PROPAGATORS") {
222 config.propagators = propagators
223 .split(',')
224 .filter_map(|name| match name.trim() {
225 "tracecontext" => Some(PropagatorKind::TraceContext),
226 "baggage" => Some(PropagatorKind::Baggage),
227 "none" => None,
228 other => {
229 eprintln!(
230 "⚠️ Unsupported OTEL_PROPAGATORS entry '{other}' (expected 'tracecontext', 'baggage', or 'none'), skipping"
231 );
232 None
233 }
234 })
235 .collect();
236 }
237
238 config
239 }
240
241 fn parse_pairs(raw: &str, sep: char) -> Vec<(String, String)> {
244 raw.split(',')
245 .filter_map(|pair| {
246 let mut parts = pair.trim().splitn(2, sep);
247 let key = parts.next()?.trim();
248 let value = parts.next()?.trim();
249 if key.is_empty() {
250 None
251 } else {
252 Some((key.to_string(), value.to_string()))
253 }
254 })
255 .collect()
256 }
257
258 #[must_use]
260 pub fn builder() -> TelemetryConfigBuilder {
261 TelemetryConfigBuilder::default()
262 }
263}
264
265#[derive(Debug, Default)]
267pub struct TelemetryConfigBuilder {
268 config: TelemetryConfig,
269}
270
271impl TelemetryConfigBuilder {
272 #[must_use]
273 pub fn service_name(mut self, name: impl Into<String>) -> Self {
274 self.config.service_name = name.into();
275 self
276 }
277
278 #[must_use]
279 pub fn resource_attributes(mut self, attributes: Vec<(String, String)>) -> Self {
280 self.config.resource_attributes = attributes;
281 self
282 }
283
284 #[must_use]
285 pub fn with_resource_attribute(
286 mut self,
287 key: impl Into<String>,
288 value: impl Into<String>,
289 ) -> Self {
290 self.config
291 .resource_attributes
292 .push((key.into(), value.into()));
293 self
294 }
295
296 #[must_use]
297 pub fn traces_enabled(mut self, enabled: bool) -> Self {
298 self.config.traces_enabled = enabled;
299 self
300 }
301
302 #[must_use]
303 pub fn metrics_enabled(mut self, enabled: bool) -> Self {
304 self.config.metrics_enabled = enabled;
305 self
306 }
307
308 #[must_use]
309 pub fn logs_enabled(mut self, enabled: bool) -> Self {
310 self.config.logs_enabled = enabled;
311 self
312 }
313
314 #[must_use]
315 pub fn otlp_protocol(mut self, protocol: OtlpProtocol) -> Self {
316 self.config.otlp_protocol = protocol;
317 self
318 }
319
320 #[must_use]
321 pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
322 self.config.otlp_endpoint = Some(endpoint.into());
323 self
324 }
325
326 #[must_use]
327 pub fn otlp_headers(mut self, headers: Vec<(String, String)>) -> Self {
328 self.config.otlp_headers = headers;
329 self
330 }
331
332 #[must_use]
333 pub fn with_otlp_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
334 self.config.otlp_headers.push((key.into(), value.into()));
335 self
336 }
337
338 #[must_use]
339 pub fn rust_log(mut self, filter: impl Into<String>) -> Self {
340 self.config.rust_log = filter.into();
341 self
342 }
343
344 #[must_use]
345 pub fn json_logging(mut self, enabled: bool) -> Self {
346 self.config.json_logging = enabled;
347 self
348 }
349
350 #[must_use]
351 pub fn metrics_export_interval_ms(mut self, ms: u64) -> Self {
352 self.config.metrics_export_interval_ms = ms;
353 self
354 }
355
356 #[must_use]
359 pub fn propagators(mut self, propagators: Vec<PropagatorKind>) -> Self {
360 self.config.propagators = propagators;
361 self
362 }
363
364 #[must_use]
365 pub fn build(self) -> TelemetryConfig {
366 self.config
367 }
368}
369
370pub struct Telemetry {
373 pub tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
374 pub meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
375 pub logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
376}
377
378impl Telemetry {
379 pub fn shutdown(&self) {
382 if let Some(tp) = &self.tracer_provider
383 && let Err(err) = tp.shutdown()
384 {
385 eprintln!("⚠️ Sideways OTel: tracer provider shutdown failed: {err}");
386 }
387 if let Some(mp) = &self.meter_provider
388 && let Err(err) = mp.shutdown()
389 {
390 eprintln!("⚠️ Sideways OTel: meter provider shutdown failed: {err}");
391 }
392 if let Some(lp) = &self.logger_provider
393 && let Err(err) = lp.shutdown()
394 {
395 eprintln!("⚠️ Sideways OTel: logger provider shutdown failed: {err}");
396 }
397 }
398}
399
400fn describe_endpoint(config: &TelemetryConfig) -> &str {
403 config.otlp_endpoint.as_deref().unwrap_or(match config.otlp_protocol {
404 OtlpProtocol::Grpc => "http://localhost:4317 (grpc default)",
405 OtlpProtocol::HttpProtobuf => "http://localhost:4318 (http/protobuf default)",
406 })
407}
408
409#[must_use]
448pub fn init_telemetry_layer(config: &TelemetryConfig) -> (Telemetry, tracing::BoxedLayer) {
449 eprintln!("🦀 Sideways OTel: Initializing...");
450
451 let _ = SERVICE_NAME.set(config.service_name.clone());
452
453 propagation::init_propagator(config);
454
455 let resource = resource::build_resource(config);
456
457 let endpoint_description = describe_endpoint(config);
458
459 let (tracer_provider, logger_provider, layer) = if config.traces_enabled {
460 match tracing::init_otlp_tracing(config, resource.clone()) {
461 Ok((tp, lp, layer)) => {
462 eprintln!("✅ Sideways OTel: tracing initialized -> {endpoint_description}");
463 if lp.is_some() {
464 eprintln!("✅ Sideways OTel: log export initialized");
465 }
466 (Some(tp), lp, layer)
467 }
468 Err(err) => {
469 eprintln!("⚠️ Sideways OTel: tracing unavailable: {err}");
470 (None, None, tracing::console_layer(config))
471 }
472 }
473 } else {
474 eprintln!("📊 Sideways OTel: tracing disabled");
475 (None, None, tracing::console_layer(config))
476 };
477
478 let meter_provider = if config.metrics_enabled {
479 match metrics::init_otlp_metrics(config, resource) {
480 Ok(mp) => {
481 eprintln!("✅ Sideways OTel: metrics initialized -> {endpoint_description}");
482 Some(mp)
483 }
484 Err(err) => {
485 eprintln!("⚠️ Sideways OTel: metrics unavailable: {err}");
486 None
487 }
488 }
489 } else {
490 eprintln!("📊 Sideways OTel: metrics disabled");
491 None
492 };
493
494 (
495 Telemetry {
496 tracer_provider,
497 meter_provider,
498 logger_provider,
499 },
500 layer,
501 )
502}
503
504#[must_use]
517pub fn init_telemetry(config: &TelemetryConfig) -> Telemetry {
518 use tracing_subscriber::layer::SubscriberExt;
519 use tracing_subscriber::util::SubscriberInitExt;
520
521 let (telemetry, layer) = init_telemetry_layer(config);
522
523 if let Err(err) = tracing_subscriber::registry().with(layer).try_init() {
524 eprintln!("❌ Sideways OTel: failed to install global tracing subscriber: {err}");
525 }
526
527 telemetry
528}