allframe_core/otel/
builder.rs1use std::env;
24
25pub struct ObservabilityBuilder {
27 service_name: String,
28 service_version: Option<String>,
29 environment: Option<String>,
30 otlp_endpoint: Option<String>,
31 json_logging: bool,
32 log_level: String,
33}
34
35impl ObservabilityBuilder {
36 pub fn new(service_name: impl Into<String>) -> Self {
38 Self {
39 service_name: service_name.into(),
40 service_version: None,
41 environment: None,
42 otlp_endpoint: None,
43 json_logging: false,
44 log_level: "info".to_string(),
45 }
46 }
47
48 pub fn service_version(mut self, version: impl Into<String>) -> Self {
50 self.service_version = Some(version.into());
51 self
52 }
53
54 pub fn environment(mut self, env: impl Into<String>) -> Self {
56 self.environment = Some(env.into());
57 self
58 }
59
60 pub fn environment_from_env(mut self) -> Self {
62 self.environment = env::var("ENVIRONMENT")
63 .or_else(|_| env::var("ENV"))
64 .ok();
65 self
66 }
67
68 pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
70 self.otlp_endpoint = Some(endpoint.into());
71 self
72 }
73
74 pub fn otlp_endpoint_from_env(mut self) -> Self {
76 self.otlp_endpoint = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
77 self
78 }
79
80 pub fn json_logging(mut self) -> Self {
82 self.json_logging = true;
83 self
84 }
85
86 pub fn log_level(mut self, level: impl Into<String>) -> Self {
88 self.log_level = level.into();
89 self
90 }
91
92 pub fn log_level_from_env(mut self) -> Self {
94 if let Ok(level) = env::var("RUST_LOG") {
95 self.log_level = level;
96 }
97 self
98 }
99
100 #[cfg(feature = "otel-otlp")]
105 pub fn build(self) -> Result<ObservabilityGuard, ObservabilityError> {
106 use opentelemetry::trace::TracerProvider as _;
107 use opentelemetry_otlp::WithExportConfig;
108 use opentelemetry_sdk::trace::TracerProvider;
109 use tracing_subscriber::layer::SubscriberExt;
110 use tracing_subscriber::util::SubscriberInitExt;
111 use tracing_subscriber::EnvFilter;
112
113 let mut resource_attrs = vec![opentelemetry::KeyValue::new(
115 "service.name",
116 self.service_name.clone(),
117 )];
118
119 if let Some(version) = &self.service_version {
120 resource_attrs.push(opentelemetry::KeyValue::new(
121 "service.version",
122 version.clone(),
123 ));
124 }
125
126 if let Some(env) = &self.environment {
127 resource_attrs.push(opentelemetry::KeyValue::new(
128 "deployment.environment",
129 env.clone(),
130 ));
131 }
132
133 let resource = opentelemetry_sdk::Resource::new(resource_attrs);
134
135 let tracer_provider = if let Some(endpoint) = &self.otlp_endpoint {
137 let exporter = opentelemetry_otlp::SpanExporter::builder()
139 .with_tonic()
140 .with_endpoint(endpoint)
141 .build()
142 .map_err(|e| ObservabilityError::ExporterInit(e.to_string()))?;
143
144 TracerProvider::builder()
145 .with_batch_exporter(exporter)
146 .with_resource(resource)
147 .build()
148 } else {
149 TracerProvider::builder().with_resource(resource).build()
151 };
152
153 let tracer = tracer_provider.tracer(self.service_name.clone());
154
155 let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
157
158 let env_filter =
160 EnvFilter::try_new(&self.log_level).unwrap_or_else(|_| EnvFilter::new("info"));
161
162 if self.json_logging {
164 let fmt_layer = tracing_subscriber::fmt::layer()
165 .json()
166 .with_target(true)
167 .with_thread_ids(true)
168 .with_file(true)
169 .with_line_number(true);
170
171 tracing_subscriber::registry()
172 .with(env_filter)
173 .with(fmt_layer)
174 .with(telemetry_layer)
175 .try_init()
176 .map_err(|e| ObservabilityError::SubscriberInit(e.to_string()))?;
177 } else {
178 let fmt_layer = tracing_subscriber::fmt::layer()
179 .with_target(true)
180 .with_thread_ids(false)
181 .with_file(false)
182 .with_line_number(false);
183
184 tracing_subscriber::registry()
185 .with(env_filter)
186 .with(fmt_layer)
187 .with(telemetry_layer)
188 .try_init()
189 .map_err(|e| ObservabilityError::SubscriberInit(e.to_string()))?;
190 }
191
192 Ok(ObservabilityGuard {
193 _tracer_provider: Some(tracer_provider),
194 })
195 }
196
197 #[cfg(not(feature = "otel-otlp"))]
199 pub fn build(self) -> Result<ObservabilityGuard, ObservabilityError> {
200 #[cfg(feature = "otel")]
202 {
203 eprintln!(
205 "Warning: otel-otlp feature not enabled, OTLP export disabled for {}",
206 self.service_name
207 );
208 }
209
210 Ok(ObservabilityGuard {
211 #[cfg(feature = "otel-otlp")]
212 _tracer_provider: None,
213 })
214 }
215}
216
217pub struct ObservabilityGuard {
221 #[cfg(feature = "otel-otlp")]
222 _tracer_provider: Option<opentelemetry_sdk::trace::TracerProvider>,
223}
224
225impl Drop for ObservabilityGuard {
226 fn drop(&mut self) {
227 #[cfg(feature = "otel-otlp")]
228 if let Some(provider) = self._tracer_provider.take() {
229 if let Err(e) = provider.shutdown() {
231 eprintln!("Error shutting down tracer provider: {:?}", e);
232 }
233 }
234 }
235}
236
237#[derive(Debug)]
239pub enum ObservabilityError {
240 ExporterInit(String),
242 SubscriberInit(String),
244 Config(String),
246}
247
248impl std::fmt::Display for ObservabilityError {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 match self {
251 ObservabilityError::ExporterInit(msg) => {
252 write!(f, "Failed to initialize exporter: {}", msg)
253 }
254 ObservabilityError::SubscriberInit(msg) => {
255 write!(f, "Failed to initialize subscriber: {}", msg)
256 }
257 ObservabilityError::Config(msg) => write!(f, "Configuration error: {}", msg),
258 }
259 }
260}
261
262impl std::error::Error for ObservabilityError {}
263
264pub type Observability = ObservabilityBuilder;
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_builder_creation() {
273 let builder = ObservabilityBuilder::new("test-service");
274 assert_eq!(builder.service_name, "test-service");
275 assert!(builder.service_version.is_none());
276 assert!(builder.environment.is_none());
277 assert!(builder.otlp_endpoint.is_none());
278 assert!(!builder.json_logging);
279 assert_eq!(builder.log_level, "info");
280 }
281
282 #[test]
283 fn test_builder_fluent_api() {
284 let builder = ObservabilityBuilder::new("test-service")
285 .service_version("1.0.0")
286 .environment("production")
287 .otlp_endpoint("http://localhost:4317")
288 .json_logging()
289 .log_level("debug");
290
291 assert_eq!(builder.service_version, Some("1.0.0".to_string()));
292 assert_eq!(builder.environment, Some("production".to_string()));
293 assert_eq!(
294 builder.otlp_endpoint,
295 Some("http://localhost:4317".to_string())
296 );
297 assert!(builder.json_logging);
298 assert_eq!(builder.log_level, "debug");
299 }
300
301 #[test]
302 fn test_observability_error_display() {
303 let err = ObservabilityError::ExporterInit("connection refused".into());
304 assert!(err.to_string().contains("connection refused"));
305 }
306}