pub mod config;
pub(crate) mod env;
pub mod error;
pub mod filter;
#[macro_use]
pub mod macros;
pub mod metrics;
pub mod span;
pub(crate) mod transport;
pub use config::{
ExporterConfig, ExporterConfigBuilder, LogLevel, MetricsConfig, MetricsConfigBuilder,
OtelConfig, OtelConfigBuilder, OtlpCredentials, OtlpProtocol, OutputFormat, SamplingStrategy,
TracingConfig, TracingConfigBuilder,
};
pub use error::{ErrorContext, OtelError, OtelResult};
pub use filter::FilterBuilder;
pub use metrics::Metrics;
use opentelemetry::{KeyValue, trace::TracerProvider};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_sdk::{
Resource,
logs::SdkLoggerProvider,
trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
};
pub use span::{InstrumentedResult, SpanExt, TimingContext};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{Layer, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
use crate::{config::SamplingStrategy as Sampling, filter::build_env_filter};
pub struct OtelGuard {
trace_provider: Option<SdkTracerProvider>,
log_provider: Option<SdkLoggerProvider>,
metrics: Option<Metrics>,
service_name: String,
}
impl OtelGuard {
pub const fn metrics(&self) -> Option<&Metrics> {
self.metrics.as_ref()
}
pub fn service_name(&self) -> &str {
&self.service_name
}
pub fn flush(&self) {
if let Some(ref tp) = self.trace_provider {
let _ = tp.force_flush();
}
if let Some(ref lp) = self.log_provider {
let _ = lp.force_flush();
}
}
pub async fn shutdown(&mut self) -> OtelResult<()> {
if let Some(tp) = self.trace_provider.take() {
tp.shutdown()
.map_err(|e| OtelError::init(format!("trace shutdown: {e}")))?;
}
if let Some(lp) = self.log_provider.take() {
lp.shutdown()
.map_err(|e| OtelError::init(format!("log shutdown: {e}")))?;
}
if let Some(m) = self.metrics.take() {
m.shutdown()?;
}
Ok(())
}
}
impl Drop for OtelGuard {
fn drop(&mut self) {
if let Some(ref tp) = self.trace_provider {
let _ = tp.shutdown();
}
if let Some(ref lp) = self.log_provider {
let _ = lp.shutdown();
}
if let Some(ref m) = self.metrics {
let _ = m.shutdown();
}
}
}
impl OtelConfigBuilder {
pub async fn init(self) -> OtelResult<OtelGuard> {
init_with_config(self.build()).await
}
}
pub async fn init_with_config(config: OtelConfig) -> OtelResult<OtelGuard> {
let mut rb = Resource::builder()
.with_service_name(config.service_name.clone())
.with_attribute(KeyValue::new(
"service.version",
config.service_version.clone(),
))
.with_attribute(KeyValue::new(
"deployment.environment.name",
config.environment.clone(),
));
if let Some(ref ns) = config.service_namespace {
rb = rb.with_attribute(KeyValue::new("service.namespace", ns.clone()));
}
if let Some(ref id) = config.service_instance_id {
rb = rb.with_attribute(KeyValue::new("service.instance.id", id.clone()));
}
for (key, value) in &config.custom_attributes {
rb = rb.with_attribute(KeyValue::new(key.clone(), value.clone()));
}
let resource = rb.build();
let mut layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = Vec::new();
layers.push(Box::new(build_env_filter(&config)));
if config.enable_console_output {
match config.output_format {
config::OutputFormat::Pretty => {
layers.push(Box::new(fmt::layer().pretty()));
}
config::OutputFormat::Compact => {
layers.push(Box::new(fmt::layer()));
}
config::OutputFormat::Json => {
layers.push(Box::new(fmt::layer().json()));
}
}
}
let trace_provider = if let Some(ref tc) = config.tracing {
let exporter = transport::build_span_exporter(&config.exporter)?;
let sampler = match tc.sampling {
Sampling::AlwaysOn => Sampler::AlwaysOn,
Sampling::AlwaysOff => Sampler::AlwaysOff,
Sampling::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
Sampling::ParentBased => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
};
let provider = SdkTracerProvider::builder()
.with_id_generator(RandomIdGenerator::default())
.with_resource(resource.clone())
.with_sampler(sampler)
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer(config.service_name.clone());
layers.push(Box::new(OpenTelemetryLayer::new(tracer)));
Some(provider)
} else {
None
};
let log_provider = if config.logging {
let exporter = transport::build_log_exporter(&config.exporter)?;
let provider = SdkLoggerProvider::builder()
.with_resource(resource.clone())
.with_batch_exporter(exporter)
.build();
layers.push(Box::new(OpenTelemetryTracingBridge::new(&provider)));
Some(provider)
} else {
None
};
let metrics = if config.metrics.is_some() {
Some(Metrics::new(&config, resource)?)
} else {
None
};
tracing_subscriber::registry()
.with(layers)
.try_init()
.map_err(|_| OtelError::SubscriberAlreadySet)?;
tracing::info!(
service.name = %config.service_name,
service.version = %config.service_version,
environment = %config.environment,
"Observability initialized"
);
Ok(OtelGuard {
trace_provider,
log_provider,
metrics,
service_name: config.service_name,
})
}