use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::Duration;
use opentelemetry::KeyValue;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{BatchConfigBuilder, BatchSpanProcessor, SdkTracerProvider};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SpanProcessorMode {
#[default]
Batch,
Simple,
}
#[derive(Debug, Clone)]
pub struct OtlpConfig {
pub(crate) endpoint: String,
pub(crate) service_name: Option<String>,
pub(crate) headers: HashMap<String, String>,
pub(crate) resource_attributes: Vec<(String, String)>,
pub(crate) processor: SpanProcessorMode,
pub(crate) max_queue_size: Option<usize>,
pub(crate) scheduled_delay: Option<Duration>,
pub(crate) max_export_batch_size: Option<usize>,
}
impl OtlpConfig {
#[must_use]
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
service_name: None,
headers: HashMap::new(),
resource_attributes: Vec::new(),
processor: SpanProcessorMode::Batch,
max_queue_size: None,
scheduled_delay: None,
max_export_batch_size: None,
}
}
#[must_use]
pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = Some(name.into());
self
}
#[must_use]
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_resource_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.resource_attributes.push((key.into(), value.into()));
self
}
#[must_use]
pub fn with_processor(mut self, mode: SpanProcessorMode) -> Self {
self.processor = mode;
self
}
#[must_use]
pub fn with_max_queue_size(mut self, max_queue_size: usize) -> Self {
self.max_queue_size = Some(max_queue_size);
self
}
#[must_use]
pub fn with_scheduled_delay(mut self, scheduled_delay: Duration) -> Self {
self.scheduled_delay = Some(scheduled_delay);
self
}
#[must_use]
pub fn with_max_export_batch_size(mut self, max_export_batch_size: usize) -> Self {
self.max_export_batch_size = Some(max_export_batch_size);
self
}
}
pub(crate) fn build_layer<S>(
config: &OtlpConfig,
) -> Result<
(tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>, SdkTracerProvider),
crate::Error,
>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
let mut builder = SpanExporter::builder().with_http().with_endpoint(&config.endpoint);
if !config.headers.is_empty() {
builder = builder.with_headers(config.headers.clone());
}
let exporter = builder.build().map_err(|e| crate::Error::OtlpInit(e.to_string()))?;
let mut attrs: Vec<KeyValue> =
config.resource_attributes.iter().map(|(k, v)| KeyValue::new(k.clone(), v.clone())).collect();
if let Some(name) = config.service_name.as_deref() {
attrs.push(KeyValue::new("service.name", name.to_owned()));
}
let resource = Resource::builder().with_attributes(attrs).build();
let provider_builder = SdkTracerProvider::builder().with_resource(resource);
let provider = match config.processor {
SpanProcessorMode::Batch => {
let mut batch_config = BatchConfigBuilder::default();
if let Some(size) = config.max_queue_size {
batch_config = batch_config.with_max_queue_size(size);
}
if let Some(delay) = config.scheduled_delay {
batch_config = batch_config.with_scheduled_delay(delay);
}
if let Some(size) = config.max_export_batch_size {
batch_config = batch_config.with_max_export_batch_size(size);
}
let processor =
BatchSpanProcessor::builder(exporter).with_batch_config(batch_config.build()).build();
provider_builder.with_span_processor(processor).build()
}
SpanProcessorMode::Simple => provider_builder.with_simple_exporter(exporter).build(),
};
let tracer = provider.tracer("kamu-logging");
let layer = tracing_opentelemetry::layer().with_tracer(tracer);
Ok((layer, provider))
}
static OTLP_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
pub(crate) fn store_provider(provider: SdkTracerProvider) {
let _ = OTLP_PROVIDER.set(provider);
}
pub fn flush_otlp() -> Result<(), crate::Error> {
match OTLP_PROVIDER.get() {
Some(provider) => provider.force_flush().map_err(|e| crate::Error::OtlpInit(e.to_string())),
None => Ok(()),
}
}
pub fn shutdown_otlp() -> Result<(), crate::Error> {
let Some(provider) = OTLP_PROVIDER.get() else {
return Ok(());
};
match provider.shutdown() {
Ok(()) | Err(opentelemetry_sdk::error::OTelSdkError::AlreadyShutdown) => Ok(()),
Err(err) => Err(crate::Error::OtlpInit(err.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_to_batch_with_unset_knobs() {
let cfg = OtlpConfig::new("http://localhost:4318");
assert_eq!(cfg.processor, SpanProcessorMode::Batch);
assert_eq!(cfg.max_queue_size, None);
assert_eq!(cfg.scheduled_delay, None);
assert_eq!(cfg.max_export_batch_size, None);
}
#[test]
fn span_processor_mode_default_is_batch() {
assert_eq!(SpanProcessorMode::default(), SpanProcessorMode::Batch);
}
#[test]
fn with_processor_overrides_mode() {
let cfg = OtlpConfig::new("http://localhost:4318").with_processor(SpanProcessorMode::Simple);
assert_eq!(cfg.processor, SpanProcessorMode::Simple);
}
#[test]
fn tuning_builders_set_fields() {
let cfg = OtlpConfig::new("http://localhost:4318")
.with_max_queue_size(4096)
.with_scheduled_delay(Duration::from_secs(2))
.with_max_export_batch_size(1024);
assert_eq!(cfg.max_queue_size, Some(4096));
assert_eq!(cfg.scheduled_delay, Some(Duration::from_secs(2)));
assert_eq!(cfg.max_export_batch_size, Some(1024));
}
#[test]
fn flush_and_shutdown_are_noops_without_init() {
assert!(flush_otlp().is_ok());
assert!(shutdown_otlp().is_ok());
}
}