use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::SdkTracerProvider;
use serde::{Deserialize, Serialize};
use tracing_opentelemetry::OpenTelemetryLayer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OtelTracingProtocol {
#[default]
Grpc,
Http,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtelTracingConfig {
pub endpoint: String,
pub protocol: OtelTracingProtocol,
pub service_name: String,
pub batch_scheduled_delay_ms: u64,
pub batch_max_queue_size: usize,
}
impl Default for OtelTracingConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:4317".into(),
protocol: OtelTracingProtocol::Grpc,
service_name: env!("CARGO_PKG_NAME").into(),
batch_scheduled_delay_ms: 5_000,
batch_max_queue_size: 2_048,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum OtelTracingError {
#[error("OTLP {protocol:?} span exporter: {source}")]
ExporterBuild {
protocol: OtelTracingProtocol,
source: opentelemetry_otlp::ExporterBuildError,
},
}
fn resolve(config: &OtelTracingConfig) -> OtelTracingConfig {
let mut resolved = config.clone();
if let Ok(v) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
resolved.endpoint = v;
}
if let Ok(v) = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
resolved.protocol = match v.as_str() {
"http/protobuf" | "http" => OtelTracingProtocol::Http,
_ => OtelTracingProtocol::Grpc,
};
}
if let Ok(v) = std::env::var("OTEL_SERVICE_NAME") {
resolved.service_name = v;
}
resolved
}
fn build_span_exporter(
protocol: OtelTracingProtocol,
endpoint: &str,
) -> Result<opentelemetry_otlp::SpanExporter, OtelTracingError> {
let result = match protocol {
OtelTracingProtocol::Grpc => opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build(),
OtelTracingProtocol::Http => opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint)
.build(),
};
result.map_err(|source| OtelTracingError::ExporterBuild { protocol, source })
}
pub fn build_tracer_layer<S>(
config: &OtelTracingConfig,
) -> Result<
(
OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>,
SdkTracerProvider,
),
OtelTracingError,
>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
let resolved = resolve(config);
let exporter = build_span_exporter(resolved.protocol, &resolved.endpoint)?;
let resource = Resource::builder()
.with_service_name(resolved.service_name.clone())
.build();
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build();
let tracer = provider.tracer("hyperi-rustlib");
opentelemetry::global::set_tracer_provider(provider.clone());
let layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing::info!(
endpoint = %resolved.endpoint,
protocol = ?resolved.protocol,
service_name = %resolved.service_name,
scheduled_delay_ms = resolved.batch_scheduled_delay_ms,
"OTel tracing layer built"
);
Ok((layer, provider))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_default_round_trip() {
let cfg = OtelTracingConfig::default();
assert_eq!(cfg.protocol, OtelTracingProtocol::Grpc);
assert!(!cfg.endpoint.is_empty());
assert!(!cfg.service_name.is_empty());
}
#[test]
fn resolve_picks_up_env_overrides() {
temp_env::with_vars(
[
(
"OTEL_EXPORTER_OTLP_ENDPOINT",
Some("http://my-collector:4317"),
),
("OTEL_EXPORTER_OTLP_PROTOCOL", Some("http/protobuf")),
("OTEL_SERVICE_NAME", Some("test-service")),
],
|| {
let r = resolve(&OtelTracingConfig::default());
assert_eq!(r.endpoint, "http://my-collector:4317");
assert_eq!(r.protocol, OtelTracingProtocol::Http);
assert_eq!(r.service_name, "test-service");
},
);
}
}