#[cfg(not(any(feature = "grpc", feature = "http")))]
compile_error!("at least one transport feature must be enabled: `grpc` or `http`");
#[cfg(feature = "testing")]
pub mod testing;
#[cfg(feature = "axum")]
pub mod axum_middleware;
#[cfg(feature = "org-context")]
pub mod span_enrichment;
use opentelemetry::KeyValue;
use opentelemetry::propagation::TextMapCompositePropagator;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
Resource,
logs::SdkLoggerProvider,
metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
propagation::{BaggagePropagator, TraceContextPropagator},
trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider},
};
use opentelemetry_semantic_conventions::attribute::{
DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
};
use std::error::Error;
use std::time::Duration;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[derive(Debug, Clone)]
pub enum TraceSampler {
AlwaysOn,
AlwaysOff,
TraceIdRatio(f64),
ParentBased(Box<TraceSampler>),
}
impl TraceSampler {
fn into_sdk_sampler(self) -> Sampler {
match self {
TraceSampler::AlwaysOn => Sampler::AlwaysOn,
TraceSampler::AlwaysOff => Sampler::AlwaysOff,
TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
TraceSampler::ParentBased(inner) => {
Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
}
}
}
}
fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
let name = match std::env::var("OTEL_TRACES_SAMPLER") {
Ok(v) => v,
Err(_) => return Ok(None),
};
let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
let sampler = match name.as_str() {
"always_on" => TraceSampler::AlwaysOn,
"always_off" => TraceSampler::AlwaysOff,
"traceidratio" => {
let ratio = arg
.as_deref()
.unwrap_or("1.0")
.parse::<f64>()
.unwrap_or(1.0);
TraceSampler::TraceIdRatio(ratio)
}
"parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
"parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
"parentbased_traceidratio" => {
let ratio = arg
.as_deref()
.unwrap_or("1.0")
.parse::<f64>()
.unwrap_or(1.0);
TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
}
unknown => {
return Err(format!(
"OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
Valid values: always_on, always_off, traceidratio, \
parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
)
.into());
}
};
Ok(Some(sampler))
}
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
pub struct TelemetryHandles {
pub tracer_provider: SdkTracerProvider,
pub meter_provider: Option<SdkMeterProvider>,
pub logger_provider: Option<SdkLoggerProvider>,
shutdown_timeout: Duration,
}
impl TelemetryHandles {
pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
self.tracer_provider.shutdown()?;
if let Some(mp) = &self.meter_provider {
mp.shutdown()?;
}
if let Some(lp) = &self.logger_provider {
lp.shutdown()?;
}
Ok(())
}
}
impl Drop for TelemetryHandles {
fn drop(&mut self) {
let tracer_provider = self.tracer_provider.clone();
let meter_provider = self.meter_provider.clone();
let logger_provider = self.logger_provider.clone();
let timeout = self.shutdown_timeout;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
if let Err(e) = tracer_provider.shutdown() {
tracing::warn!("tracer provider shutdown error: {e}");
}
if let Some(mp) = meter_provider
&& let Err(e) = mp.shutdown()
{
tracing::warn!("meter provider shutdown error: {e}");
}
if let Some(lp) = logger_provider
&& let Err(e) = lp.shutdown()
{
tracing::warn!("logger provider shutdown error: {e}");
}
let _ = tx.send(());
});
if rx.recv_timeout(timeout).is_err() {
tracing::warn!(
"telemetry shutdown did not complete within {timeout:?}; \
some spans/metrics may not have been exported"
);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportProtocol {
#[cfg(feature = "grpc")]
Grpc,
#[cfg(feature = "http")]
HttpProtobuf,
}
fn protocol_from_env() -> Option<ExportProtocol> {
let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
match val.trim() {
#[cfg(feature = "grpc")]
"grpc" => Some(ExportProtocol::Grpc),
#[cfg(feature = "http")]
"http/protobuf" => Some(ExportProtocol::HttpProtobuf),
_ => None,
}
}
pub struct Telemetry;
impl Telemetry {
pub fn builder(service_name: &str) -> TelemetryBuilder {
TelemetryBuilder {
service_name: Some(service_name.to_string()),
service_version: None,
deployment_environment: None,
sampler: None,
metrics: true,
logs: false,
protocol: None,
max_export_batch_size: None,
metric_export_interval: None,
export_timeout: None,
shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
extra_layers: Vec::new(),
extra_metric_readers: Vec::new(),
}
}
pub fn from_env() -> TelemetryBuilder {
TelemetryBuilder {
service_name: None,
service_version: None,
deployment_environment: None,
sampler: None,
metrics: true,
logs: false,
protocol: None,
max_export_batch_size: None,
metric_export_interval: None,
export_timeout: None,
shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
extra_layers: Vec::new(),
extra_metric_readers: Vec::new(),
}
}
}
#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
pub struct TelemetryBuilder {
service_name: Option<String>,
service_version: Option<String>,
deployment_environment: Option<String>,
sampler: Option<TraceSampler>,
metrics: bool,
logs: bool,
protocol: Option<ExportProtocol>,
max_export_batch_size: Option<usize>,
metric_export_interval: Option<Duration>,
export_timeout: Option<Duration>,
shutdown_timeout: Duration,
extra_layers: Vec<
Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
>,
extra_metric_readers: Vec<MeterProviderInstaller>,
}
type MeterProviderInstaller =
Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
impl TelemetryBuilder {
pub fn with_version(mut self, version: &str) -> Self {
self.service_version = Some(version.to_string());
self
}
pub fn with_environment(mut self, environment: &str) -> Self {
self.deployment_environment = Some(environment.to_string());
self
}
pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
self.sampler = Some(sampler);
self
}
pub fn with_metrics(mut self, enabled: bool) -> Self {
self.metrics = enabled;
self
}
pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
self.protocol = Some(protocol);
self
}
pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
self.max_export_batch_size = Some(size);
self
}
pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
self.metric_export_interval = Some(interval);
self
}
pub fn with_logs(mut self, enabled: bool) -> Self {
self.logs = enabled;
self
}
pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
self.export_timeout = Some(timeout);
self
}
pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
self.shutdown_timeout = timeout;
self
}
pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
where
F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
{
self.extra_metric_readers.push(Box::new(setup));
self
}
pub fn with_layer<L>(mut self, layer: L) -> Self
where
L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
{
self.extra_layers.push(Box::new(layer));
self
}
pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
if let Some(interval) = self.metric_export_interval
&& interval.is_zero()
{
return Err("metric_export_interval must be greater than zero".into());
}
let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
#[cfg(feature = "grpc")]
{
ExportProtocol::Grpc
}
#[cfg(all(not(feature = "grpc"), feature = "http"))]
{
ExportProtocol::HttpProtobuf
}
});
let default_endpoint = match protocol {
#[cfg(feature = "grpc")]
ExportProtocol::Grpc => "http://localhost:4317",
#[cfg(feature = "http")]
ExportProtocol::HttpProtobuf => "http://localhost:4318",
};
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
.unwrap_or_else(|_| default_endpoint.to_string());
let export_timeout = self.export_timeout.or_else(timeout_from_env);
let service_name = self.service_name.unwrap_or_else(|| {
std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
});
let resource = build_resource(
&service_name,
self.service_version.as_deref(),
self.deployment_environment.as_deref(),
);
let sampler = match self.sampler {
Some(s) => s,
None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
};
let trace_exporter = build_span_exporter(protocol, &endpoint, export_timeout)?;
let batch_processor = if let Some(size) = self.max_export_batch_size {
BatchSpanProcessor::builder(trace_exporter)
.with_batch_config(
BatchConfigBuilder::default()
.with_max_export_batch_size(size)
.build(),
)
.build()
} else {
BatchSpanProcessor::builder(trace_exporter).build()
};
let tracer_provider = SdkTracerProvider::builder()
.with_resource(resource.clone())
.with_sampler(sampler.into_sdk_sampler())
.with_span_processor(batch_processor)
.build();
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
let propagator = TextMapCompositePropagator::new(vec![
Box::new(TraceContextPropagator::new()),
Box::new(BaggagePropagator::new()),
]);
opentelemetry::global::set_text_map_propagator(propagator);
let meter_provider = if self.metrics {
let metric_exporter = build_metric_exporter(protocol, &endpoint, export_timeout)?;
let periodic_reader = if let Some(interval) = self.metric_export_interval {
PeriodicReader::builder(metric_exporter)
.with_interval(interval)
.build()
} else {
PeriodicReader::builder(metric_exporter).build()
};
let mut mp_builder = SdkMeterProvider::builder()
.with_resource(resource.clone())
.with_reader(periodic_reader);
for installer in self.extra_metric_readers {
mp_builder = installer(mp_builder);
}
let mp = mp_builder.build();
opentelemetry::global::set_meter_provider(mp.clone());
Some(mp)
} else {
None
};
let logger_provider = if self.logs {
let log_exporter = build_log_exporter(protocol, &endpoint, export_timeout)?;
let lp = SdkLoggerProvider::builder()
.with_resource(resource)
.with_batch_exporter(log_exporter)
.build();
Some(lp)
} else {
None
};
let otel_layer = tracing_opentelemetry::layer();
let registry = tracing_subscriber::registry()
.with(self.extra_layers)
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer())
.with(otel_layer);
if let Some(lp) = &logger_provider {
registry
.with(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(lp))
.try_init()
.ok();
} else {
registry.try_init().ok();
}
Ok(TelemetryHandles {
tracer_provider,
meter_provider,
logger_provider,
shutdown_timeout: self.shutdown_timeout,
})
}
}
pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
Telemetry::builder(service_name).init()
}
pub fn init_telemetry_with_sampler(
service_name: &str,
sampler: Option<TraceSampler>,
) -> Result<TelemetryHandles, Box<dyn Error>> {
let builder = Telemetry::builder(service_name);
match sampler {
Some(s) => builder.with_sampler(s),
None => builder, }
.init()
}
fn timeout_from_env() -> Option<Duration> {
let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
let ms: u64 = ms.trim().parse().ok()?;
Some(Duration::from_millis(ms))
}
fn build_span_exporter(
protocol: ExportProtocol,
endpoint: &str,
timeout: Option<Duration>,
) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
match protocol {
#[cfg(feature = "grpc")]
ExportProtocol::Grpc => {
let mut b = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
#[cfg(feature = "http")]
ExportProtocol::HttpProtobuf => {
let mut b = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
}
}
fn build_metric_exporter(
protocol: ExportProtocol,
endpoint: &str,
timeout: Option<Duration>,
) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
match protocol {
#[cfg(feature = "grpc")]
ExportProtocol::Grpc => {
let mut b = opentelemetry_otlp::MetricExporter::builder()
.with_tonic()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
#[cfg(feature = "http")]
ExportProtocol::HttpProtobuf => {
let mut b = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
}
}
fn build_log_exporter(
protocol: ExportProtocol,
endpoint: &str,
timeout: Option<Duration>,
) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
match protocol {
#[cfg(feature = "grpc")]
ExportProtocol::Grpc => {
let mut b = opentelemetry_otlp::LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
#[cfg(feature = "http")]
ExportProtocol::HttpProtobuf => {
let mut b = opentelemetry_otlp::LogExporter::builder()
.with_http()
.with_endpoint(endpoint);
if let Some(t) = timeout {
b = b.with_timeout(t);
}
Ok(b.build()?)
}
}
}
pub fn build_resource(
service_name: &str,
service_version: Option<&str>,
deployment_environment: Option<&str>,
) -> Resource {
let hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_default();
let mut builder = Resource::builder()
.with_service_name(service_name.to_string())
.with_attributes([
KeyValue::new(HOST_NAME, hostname),
KeyValue::new(PROCESS_PID, std::process::id() as i64),
]);
if let Some(version) = service_version {
builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
}
if let Some(env) = deployment_environment {
builder =
builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
}
builder.build()
}
#[cfg(feature = "axum")]
pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
axum_middleware::OtelTraceLayer
}
#[cfg(all(feature = "axum", feature = "org-context"))]
pub fn org_context_span_enricher_layer() -> axum_middleware::OrgContextSpanEnricher {
axum_middleware::OrgContextSpanEnricher
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_contains_all_attributes_when_provided() {
let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
assert_eq!(
resource.get(&opentelemetry::Key::new("service.name")),
Some(opentelemetry::Value::from("test-svc")),
);
assert_eq!(
resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
Some(opentelemetry::Value::from("1.2.3")),
);
assert_eq!(
resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
Some(opentelemetry::Value::from("staging")),
);
assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
assert!(
resource
.get(&opentelemetry::Key::new(PROCESS_PID))
.is_some()
);
}
#[test]
fn resource_graceful_when_optional_values_omitted() {
let resource = build_resource("test-svc", None, None);
assert_eq!(
resource.get(&opentelemetry::Key::new("service.name")),
Some(opentelemetry::Value::from("test-svc")),
);
assert!(
resource
.get(&opentelemetry::Key::new(SERVICE_VERSION))
.is_none()
);
assert!(
resource
.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
.is_none()
);
assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
assert!(
resource
.get(&opentelemetry::Key::new(PROCESS_PID))
.is_some()
);
}
#[test]
fn trace_sampler_ratio_converts_to_sdk() {
let sampler = TraceSampler::TraceIdRatio(0.5);
let sdk = sampler.into_sdk_sampler();
assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
}
#[test]
fn trace_sampler_parent_based_converts_to_sdk() {
let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
let sdk = sampler.into_sdk_sampler();
let debug = format!("{sdk:?}");
assert!(debug.contains("ParentBased"));
assert!(debug.contains("0.25"));
}
unsafe fn set_env(key: &str, val: &str) {
unsafe {
std::env::set_var(key, val);
}
}
unsafe fn remove_env(key: &str) {
unsafe {
std::env::remove_var(key);
}
}
#[test]
fn sampler_from_env_reads_traceidratio() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "traceidratio");
set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(
matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
remove_env("OTEL_TRACES_SAMPLER_ARG");
}
}
#[test]
fn sampler_from_env_returns_none_when_unset() {
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
assert!(sampler_from_env().expect("should not error").is_none());
}
#[test]
fn sampler_from_env_reads_parentbased_traceidratio() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(
matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
remove_env("OTEL_TRACES_SAMPLER_ARG");
}
}
#[test]
fn sampler_from_env_parentbased_always_on() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(
matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn sampler_from_env_parentbased_always_off() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(
matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn sampler_from_env_always_on() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "always_on");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(matches!(sampler, TraceSampler::AlwaysOn));
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn sampler_from_env_always_off() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "always_off");
}
let sampler = sampler_from_env()
.expect("should not error")
.expect("should return Some");
assert!(matches!(sampler, TraceSampler::AlwaysOff));
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn sampler_from_env_unknown_returns_error() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
}
let err = sampler_from_env().expect_err("unknown sampler should produce an error");
assert!(
err.to_string().contains("unknown_sampler"),
"error message should include the unknown name, got: {err}"
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn trace_sampler_always_on_converts_to_sdk() {
let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
assert_eq!(format!("{sdk:?}"), "AlwaysOn");
}
#[test]
fn trace_sampler_always_off_converts_to_sdk() {
let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
assert_eq!(format!("{sdk:?}"), "AlwaysOff");
}
#[test]
fn builder_has_sensible_defaults() {
let builder = Telemetry::builder("test-svc");
assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
assert!(builder.service_version.is_none());
assert!(builder.deployment_environment.is_none());
assert!(builder.sampler.is_none());
assert!(builder.metrics);
assert!(!builder.logs);
assert!(builder.protocol.is_none());
assert!(builder.max_export_batch_size.is_none());
assert!(builder.metric_export_interval.is_none());
assert!(builder.export_timeout.is_none());
}
#[test]
fn from_env_builder_has_no_service_name() {
let builder = Telemetry::from_env();
assert!(builder.service_name.is_none());
}
#[test]
fn with_export_timeout_stores_value() {
let timeout = Duration::from_secs(5);
let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
assert_eq!(builder.export_timeout, Some(timeout));
}
#[test]
fn timeout_from_env_reads_milliseconds() {
unsafe {
set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
}
let t = timeout_from_env();
assert_eq!(t, Some(Duration::from_millis(5000)));
unsafe {
remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
}
}
#[test]
fn timeout_from_env_returns_none_when_unset() {
unsafe {
remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
}
assert_eq!(timeout_from_env(), None);
}
#[test]
fn service_name_from_env_used_when_none_given() {
let builder = Telemetry::from_env();
assert!(builder.service_name.is_none());
}
#[test]
fn explicit_service_name_overrides_env_var() {
let builder = Telemetry::builder("explicit-svc");
assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
}
#[test]
fn from_env_builder_service_name_is_none() {
let builder = Telemetry::from_env();
assert!(builder.service_name.is_none());
}
#[test]
fn init_returns_error_for_unknown_otel_traces_sampler() {
unsafe {
set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
}
let result = Telemetry::builder("test-svc").with_metrics(false).init();
let err = result
.err()
.expect("unknown sampler env var should cause init to fail");
assert!(
err.to_string().contains("not_a_real_sampler"),
"error should name the unknown sampler, got: {err}"
);
unsafe {
remove_env("OTEL_TRACES_SAMPLER");
}
}
#[test]
fn with_max_export_batch_size_stores_value() {
let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
assert_eq!(builder.max_export_batch_size, Some(1024));
}
#[test]
fn with_metric_export_interval_stores_value() {
let interval = Duration::from_secs(30);
let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
assert_eq!(builder.metric_export_interval, Some(interval));
}
#[test]
fn init_rejects_zero_metric_export_interval() {
let err = Telemetry::builder("test-svc")
.with_metric_export_interval(Duration::ZERO)
.with_metrics(false)
.init()
.err()
.expect("expected error for zero interval");
assert!(
err.to_string().contains("metric_export_interval"),
"error message should mention metric_export_interval, got: {err}"
);
}
#[test]
fn builder_with_custom_values() {
let builder = Telemetry::builder("test-svc")
.with_version("2.0.0")
.with_environment("production")
.with_sampler(TraceSampler::TraceIdRatio(0.5))
.with_metrics(false);
assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
assert_eq!(
builder.deployment_environment.as_deref(),
Some("production")
);
assert!(
matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
);
assert!(!builder.metrics);
}
#[test]
#[cfg(feature = "grpc")]
fn builder_with_protocol_grpc() {
let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
}
#[test]
#[cfg(feature = "http")]
fn builder_with_protocol_http() {
let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
}
#[test]
#[cfg(feature = "grpc")]
fn protocol_from_env_reads_grpc() {
unsafe {
set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
}
assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
unsafe {
remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
}
}
#[test]
#[cfg(feature = "http")]
fn protocol_from_env_reads_http_protobuf() {
unsafe {
set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
}
assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
unsafe {
remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
}
}
#[test]
fn protocol_from_env_returns_none_when_unset() {
unsafe {
remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
}
assert_eq!(protocol_from_env(), None);
}
#[test]
fn protocol_from_env_returns_none_for_unknown() {
unsafe {
set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
}
assert_eq!(protocol_from_env(), None);
unsafe {
remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
}
}
#[test]
fn builder_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TelemetryBuilder>();
}
#[test]
fn with_shutdown_timeout_stores_value() {
let timeout = Duration::from_secs(10);
let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
assert_eq!(builder.shutdown_timeout, timeout);
}
#[test]
fn default_shutdown_timeout_is_five_seconds() {
let builder = Telemetry::builder("test-svc");
assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
}
#[cfg(feature = "testing")]
#[test]
fn drop_completes_within_shutdown_timeout() {
let mut handles = crate::Telemetry::testing("drop-timeout-test");
handles.shutdown_timeout = Duration::from_millis(100);
let start = std::time::Instant::now();
drop(handles);
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(500),
"drop took {elapsed:?}, expected < 500 ms"
);
}
}