1pub mod config;
45pub(crate) mod env;
46pub mod error;
47pub mod filter;
48#[macro_use]
49pub mod macros;
50pub mod metrics;
51pub mod span;
52pub(crate) mod transport;
53
54pub use config::{
57 ExporterConfig, ExporterConfigBuilder, LogLevel, MetricsConfig, MetricsConfigBuilder,
58 OtelConfig, OtelConfigBuilder, OtlpCredentials, OtlpProtocol, OutputFormat, SamplingStrategy,
59 TracingConfig, TracingConfigBuilder,
60};
61pub use error::{ErrorContext, OtelError, OtelResult};
62pub use filter::FilterBuilder;
63pub use metrics::Metrics;
64use opentelemetry::{KeyValue, trace::TracerProvider};
65use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
66use opentelemetry_sdk::{
67 Resource,
68 logs::SdkLoggerProvider,
69 trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
70};
71pub use span::{InstrumentedResult, SpanExt, TimingContext};
72use tracing_opentelemetry::OpenTelemetryLayer;
73use tracing_subscriber::{Layer, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
74
75use crate::{config::SamplingStrategy as Sampling, filter::build_env_filter};
76
77pub struct OtelGuard {
85 trace_provider: Option<SdkTracerProvider>,
86 log_provider: Option<SdkLoggerProvider>,
87 metrics: Option<Metrics>,
88 service_name: String,
89}
90
91impl OtelGuard {
92 pub const fn metrics(&self) -> Option<&Metrics> {
94 self.metrics.as_ref()
95 }
96
97 pub fn service_name(&self) -> &str {
99 &self.service_name
100 }
101
102 pub fn flush(&self) {
104 if let Some(ref tp) = self.trace_provider {
105 let _ = tp.force_flush();
106 }
107 if let Some(ref lp) = self.log_provider {
108 let _ = lp.force_flush();
109 }
110 }
111
112 pub async fn shutdown(&mut self) -> OtelResult<()> {
122 if let Some(tp) = self.trace_provider.take() {
124 tp.shutdown()
125 .map_err(|e| OtelError::init(format!("trace shutdown: {e}")))?;
126 }
127 if let Some(lp) = self.log_provider.take() {
128 lp.shutdown()
129 .map_err(|e| OtelError::init(format!("log shutdown: {e}")))?;
130 }
131 if let Some(m) = self.metrics.take() {
132 m.shutdown()?;
133 }
134 Ok(())
135 }
136}
137
138impl Drop for OtelGuard {
139 fn drop(&mut self) {
140 if let Some(ref tp) = self.trace_provider {
142 let _ = tp.shutdown();
143 }
144 if let Some(ref lp) = self.log_provider {
145 let _ = lp.shutdown();
146 }
147 if let Some(ref m) = self.metrics {
148 let _ = m.shutdown();
149 }
150 }
151}
152
153impl OtelConfigBuilder {
156 pub async fn init(self) -> OtelResult<OtelGuard> {
163 init_with_config(self.build()).await
164 }
165}
166
167pub async fn init_with_config(config: OtelConfig) -> OtelResult<OtelGuard> {
178 let mut rb = Resource::builder()
180 .with_service_name(config.service_name.clone())
181 .with_attribute(KeyValue::new(
182 "service.version",
183 config.service_version.clone(),
184 ))
185 .with_attribute(KeyValue::new(
186 "deployment.environment.name",
187 config.environment.clone(),
188 ));
189
190 if let Some(ref ns) = config.service_namespace {
191 rb = rb.with_attribute(KeyValue::new("service.namespace", ns.clone()));
192 }
193 if let Some(ref id) = config.service_instance_id {
194 rb = rb.with_attribute(KeyValue::new("service.instance.id", id.clone()));
195 }
196 for (key, value) in &config.custom_attributes {
197 rb = rb.with_attribute(KeyValue::new(key.clone(), value.clone()));
198 }
199
200 let resource = rb.build();
201
202 let mut layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = Vec::new();
204
205 layers.push(Box::new(build_env_filter(&config)));
207
208 if config.enable_console_output {
210 match config.output_format {
211 config::OutputFormat::Pretty => {
212 layers.push(Box::new(fmt::layer().pretty()));
213 }
214 config::OutputFormat::Compact => {
215 layers.push(Box::new(fmt::layer()));
216 }
217 config::OutputFormat::Json => {
218 layers.push(Box::new(fmt::layer().json()));
219 }
220 }
221 }
222
223 let trace_provider = if let Some(ref tc) = config.tracing {
225 let exporter = transport::build_span_exporter(&config.exporter)?;
226
227 let sampler = match tc.sampling {
228 Sampling::AlwaysOn => Sampler::AlwaysOn,
229 Sampling::AlwaysOff => Sampler::AlwaysOff,
230 Sampling::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
231 Sampling::ParentBased => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
232 };
233
234 let provider = SdkTracerProvider::builder()
235 .with_id_generator(RandomIdGenerator::default())
236 .with_resource(resource.clone())
237 .with_sampler(sampler)
238 .with_batch_exporter(exporter)
239 .build();
240
241 let tracer = provider.tracer(config.service_name.clone());
242 layers.push(Box::new(OpenTelemetryLayer::new(tracer)));
243
244 Some(provider)
245 } else {
246 None
247 };
248
249 let log_provider = if config.logging {
251 let exporter = transport::build_log_exporter(&config.exporter)?;
252
253 let provider = SdkLoggerProvider::builder()
254 .with_resource(resource.clone())
255 .with_batch_exporter(exporter)
256 .build();
257
258 layers.push(Box::new(OpenTelemetryTracingBridge::new(&provider)));
259
260 Some(provider)
261 } else {
262 None
263 };
264
265 let metrics = if config.metrics.is_some() {
267 Some(Metrics::new(&config, resource)?)
268 } else {
269 None
270 };
271
272 tracing_subscriber::registry()
274 .with(layers)
275 .try_init()
276 .map_err(|_| OtelError::SubscriberAlreadySet)?;
277
278 tracing::info!(
279 service.name = %config.service_name,
280 service.version = %config.service_version,
281 environment = %config.environment,
282 "Observability initialized"
283 );
284
285 Ok(OtelGuard {
286 trace_provider,
287 log_provider,
288 metrics,
289 service_name: config.service_name,
290 })
291}