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