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, opentelemetry_sdk::runtime::Tokio)
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 env_filter =
157 EnvFilter::try_new(&self.log_level).unwrap_or_else(|_| EnvFilter::new("info"));
158
159 if self.json_logging {
162 let fmt_layer = tracing_subscriber::fmt::layer()
163 .json()
164 .with_target(true)
165 .with_thread_ids(true)
166 .with_file(true)
167 .with_line_number(true);
168
169 let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
170
171 tracing_subscriber::registry()
172 .with(env_filter)
173 .with(telemetry_layer)
174 .with(fmt_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 let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
185
186 tracing_subscriber::registry()
187 .with(env_filter)
188 .with(telemetry_layer)
189 .with(fmt_layer)
190 .try_init()
191 .map_err(|e| ObservabilityError::SubscriberInit(e.to_string()))?;
192 }
193
194 Ok(ObservabilityGuard {
195 _tracer_provider: Some(tracer_provider),
196 })
197 }
198
199 #[cfg(not(feature = "otel-otlp"))]
201 pub fn build(self) -> Result<ObservabilityGuard, ObservabilityError> {
202 #[cfg(feature = "otel")]
204 {
205 eprintln!(
207 "Warning: otel-otlp feature not enabled, OTLP export disabled for {}",
208 self.service_name
209 );
210 }
211
212 Ok(ObservabilityGuard {
213 #[cfg(feature = "otel-otlp")]
214 _tracer_provider: None,
215 })
216 }
217}
218
219pub struct ObservabilityGuard {
223 #[cfg(feature = "otel-otlp")]
224 _tracer_provider: Option<opentelemetry_sdk::trace::TracerProvider>,
225}
226
227impl Drop for ObservabilityGuard {
228 fn drop(&mut self) {
229 #[cfg(feature = "otel-otlp")]
230 if let Some(provider) = self._tracer_provider.take() {
231 if let Err(e) = provider.shutdown() {
233 eprintln!("Error shutting down tracer provider: {:?}", e);
234 }
235 }
236 }
237}
238
239#[derive(Debug)]
241pub enum ObservabilityError {
242 ExporterInit(String),
244 SubscriberInit(String),
246 Config(String),
248}
249
250impl std::fmt::Display for ObservabilityError {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 ObservabilityError::ExporterInit(msg) => {
254 write!(f, "Failed to initialize exporter: {}", msg)
255 }
256 ObservabilityError::SubscriberInit(msg) => {
257 write!(f, "Failed to initialize subscriber: {}", msg)
258 }
259 ObservabilityError::Config(msg) => write!(f, "Configuration error: {}", msg),
260 }
261 }
262}
263
264impl std::error::Error for ObservabilityError {}
265
266pub type Observability = ObservabilityBuilder;
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_builder_creation() {
275 let builder = ObservabilityBuilder::new("test-service");
276 assert_eq!(builder.service_name, "test-service");
277 assert!(builder.service_version.is_none());
278 assert!(builder.environment.is_none());
279 assert!(builder.otlp_endpoint.is_none());
280 assert!(!builder.json_logging);
281 assert_eq!(builder.log_level, "info");
282 }
283
284 #[test]
285 fn test_builder_fluent_api() {
286 let builder = ObservabilityBuilder::new("test-service")
287 .service_version("1.0.0")
288 .environment("production")
289 .otlp_endpoint("http://localhost:4317")
290 .json_logging()
291 .log_level("debug");
292
293 assert_eq!(builder.service_version, Some("1.0.0".to_string()));
294 assert_eq!(builder.environment, Some("production".to_string()));
295 assert_eq!(
296 builder.otlp_endpoint,
297 Some("http://localhost:4317".to_string())
298 );
299 assert!(builder.json_logging);
300 assert_eq!(builder.log_level, "debug");
301 }
302
303 #[test]
304 fn test_observability_error_display() {
305 let err = ObservabilityError::ExporterInit("connection refused".into());
306 assert!(err.to_string().contains("connection refused"));
307 }
308}