use std::time::Duration;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::TracerProvider;
use opentelemetry_sdk::Resource;
use rainier_support::{Error, Result};
#[derive(Debug, Clone)]
pub struct Otlp {
endpoint: String,
service_name: String,
service_version: Option<String>,
environment: Option<String>,
timeout: Duration,
sample_ratio: f64,
}
impl Otlp {
pub fn new(endpoint: impl Into<String>, service_name: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
service_name: service_name.into(),
service_version: None,
environment: None,
timeout: Duration::from_secs(3),
sample_ratio: 1.0,
}
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.service_version = Some(version.into());
self
}
pub fn environment(mut self, environment: impl Into<String>) -> Self {
self.environment = Some(environment.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn sample_ratio(mut self, ratio: f64) -> Self {
self.sample_ratio = ratio.clamp(0.0, 1.0);
self
}
pub fn build(&self) -> Result<TracerProvider> {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&self.endpoint)
.with_timeout(self.timeout)
.build()
.map_err(|e| Error::internal(format!("could not build the OTLP exporter: {e}")))?;
let mut attributes = vec![KeyValue::new("service.name", self.service_name.clone())];
if let Some(version) = &self.service_version {
attributes.push(KeyValue::new("service.version", version.clone()));
}
if let Some(environment) = &self.environment {
attributes.push(KeyValue::new("deployment.environment", environment.clone()));
}
let sampler = if self.sample_ratio >= 1.0 {
opentelemetry_sdk::trace::Sampler::AlwaysOn
} else if self.sample_ratio <= 0.0 {
opentelemetry_sdk::trace::Sampler::AlwaysOff
} else {
opentelemetry_sdk::trace::Sampler::ParentBased(Box::new(
opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(self.sample_ratio),
))
};
Ok(TracerProvider::builder()
.with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
.with_sampler(sampler)
.with_resource(Resource::new(attributes))
.build())
}
pub fn layer<S>(
&self,
) -> Result<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
let provider = self.build()?;
let tracer = provider.tracer(self.service_name.clone());
opentelemetry::global::set_tracer_provider(provider);
Ok(tracing_opentelemetry::layer().with_tracer(tracer))
}
}
pub fn shutdown() {
opentelemetry::global::shutdown_tracer_provider();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_ratio_outside_the_range_is_clamped_rather_than_believed() {
assert_eq!(Otlp::new("http://localhost:4317", "test").sample_ratio(5.0).sample_ratio, 1.0);
assert_eq!(Otlp::new("http://localhost:4317", "test").sample_ratio(-1.0).sample_ratio, 0.0);
}
#[test]
fn the_defaults_are_the_safe_ones() {
let otlp = Otlp::new("http://localhost:4317", "test");
assert_eq!(otlp.sample_ratio, 1.0, "sample everything until told otherwise");
assert_eq!(otlp.timeout, Duration::from_secs(3), "a collector must not become latency");
}
#[test]
fn the_resource_attributes_are_optional() {
let otlp =
Otlp::new("http://localhost:4317", "test").version("1.2.3").environment("production");
assert_eq!(otlp.service_version.as_deref(), Some("1.2.3"));
assert_eq!(otlp.environment.as_deref(), Some("production"));
}
}