1use opentelemetry::trace::TracerProvider;
26use opentelemetry_sdk::trace::SdkTracerProvider;
27use opentelemetry_sdk::Resource;
28use tracing_opentelemetry::OpenTelemetryLayer;
29use tracing_subscriber::layer::SubscriberExt;
30use tracing_subscriber::util::SubscriberInitExt;
31use tracing_subscriber::EnvFilter;
32
33#[cfg(feature = "otel-grpc")]
34use opentelemetry_otlp::WithExportConfig;
35
36use crate::config::TelemetryConfig;
37
38pub struct TelemetryGuard {
41 tracer_provider: Option<SdkTracerProvider>,
42}
43
44impl Drop for TelemetryGuard {
45 fn drop(&mut self) {
46 if let Some(provider) = self.tracer_provider.take() {
47 if let Err(e) = provider.shutdown() {
48 tracing::warn!(error = %e, "OpenTelemetry tracer provider shutdown failed");
49 }
50 }
51 }
52}
53
54pub fn init_tracing(config: &TelemetryConfig) -> anyhow::Result<TelemetryGuard> {
62 if config.otel_disabled {
63 tracing::info!("OpenTelemetry tracing is disabled");
64 return Ok(TelemetryGuard {
65 tracer_provider: None,
66 });
67 }
68
69 let service_name = config
70 .otel_service_name
71 .clone()
72 .unwrap_or_else(|| "ravenclaws".to_string());
73
74 let endpoint = config
75 .otel_endpoint
76 .clone()
77 .unwrap_or_else(|| "http://localhost:4317".to_string());
78
79 let resource = Resource::builder()
80 .with_attribute(opentelemetry::KeyValue::new(
81 "service.name",
82 service_name.clone(),
83 ))
84 .with_attribute(opentelemetry::KeyValue::new(
85 "service.version",
86 env!("CARGO_PKG_VERSION"),
87 ))
88 .build();
89
90 #[cfg(feature = "otel-grpc")]
92 let tracer_provider = {
93 let exporter = opentelemetry_otlp::SpanExporter::builder()
94 .with_tonic()
95 .with_endpoint(&endpoint)
96 .build()
97 .map_err(|e| anyhow::anyhow!("Failed to create OTLP span exporter: {}", e))?;
98
99 SdkTracerProvider::builder()
100 .with_resource(resource)
101 .with_batch_exporter(exporter)
102 .build()
103 };
104
105 #[cfg(not(feature = "otel-grpc"))]
106 let tracer_provider = {
107 #[cfg(feature = "otel-stdout")]
109 {
110 let exporter = opentelemetry_stdout::SpanExporter::default();
111 SdkTracerProvider::builder()
112 .with_resource(resource)
113 .with_simple_exporter(exporter)
114 .build()
115 }
116 #[cfg(not(feature = "otel-stdout"))]
117 {
118 tracing::warn!(
119 "OpenTelemetry tracing requested but no exporter feature enabled. \
120 Enable 'otel-grpc' or 'otel-stdout' feature."
121 );
122 SdkTracerProvider::builder().with_resource(resource).build()
123 }
124 };
125
126 let tracer = tracer_provider.tracer("ravenclaws");
127 let telemetry_layer = OpenTelemetryLayer::new(tracer);
128
129 let registry = tracing_subscriber::registry()
133 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "ravenclaws=info".into()))
134 .with(tracing_subscriber::fmt::layer().json())
135 .with(telemetry_layer);
136
137 if registry.try_init().is_err() {
138 tracing::warn!(
139 "Tracing subscriber already initialized — OpenTelemetry layer not registered. \
140 Set RAVENCLAW_OTEL_DISABLED=true if this is unexpected."
141 );
142 }
143
144 tracing::info!(
145 endpoint = %endpoint,
146 service = %service_name,
147 "OpenTelemetry tracing initialized"
148 );
149
150 Ok(TelemetryGuard {
151 tracer_provider: Some(tracer_provider),
152 })
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_telemetry_config_default() {
161 let config = TelemetryConfig::default();
162 assert!(config.otel_endpoint.is_none());
163 assert!(config.otel_service_name.is_none());
164 assert!(!config.otel_disabled);
165 }
166
167 #[test]
168 fn test_telemetry_config_disabled() {
169 let config = TelemetryConfig {
170 otel_disabled: true,
171 ..TelemetryConfig::default()
172 };
173 let guard = init_tracing(&config).unwrap();
174 assert!(guard.tracer_provider.is_none());
175 }
176
177 #[test]
178 fn test_telemetry_guard_drop_no_panic() {
179 let guard = TelemetryGuard {
180 tracer_provider: None,
181 };
182 drop(guard); }
184
185 #[test]
186 fn test_telemetry_config_custom() {
187 let config = TelemetryConfig {
188 otel_endpoint: Some("http://jaeger:4317".to_string()),
189 otel_service_name: Some("my-ravenclaws".to_string()),
190 otel_disabled: false,
191 };
192 assert_eq!(config.otel_endpoint.as_deref(), Some("http://jaeger:4317"));
193 assert_eq!(config.otel_service_name.as_deref(), Some("my-ravenclaws"));
194 }
195}