use crate::error::{ObservabilityError, Result};
use crate::telemetry::TelemetryConfig;
use opentelemetry::global;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider};
pub async fn init_tracing(config: &TelemetryConfig, resource: Resource) -> Result<()> {
let sampler = create_sampler(config.sampling_rate);
let tracer_provider = if let Some(ref endpoint) = config.otlp_endpoint {
#[cfg(feature = "otlp")]
{
create_otlp_tracer_provider(endpoint, sampler, resource).await?
}
#[cfg(not(feature = "otlp"))]
{
return Err(ObservabilityError::ConfigError(
"OTLP feature not enabled".to_string(),
));
}
} else if let Some(ref _jaeger_ep) = config.jaeger_endpoint {
#[cfg(feature = "jaeger")]
{
create_jaeger_tracer_provider(_jaeger_ep, sampler, resource)?
}
#[cfg(not(feature = "jaeger"))]
{
return Err(ObservabilityError::ConfigError(
"Jaeger feature not enabled".to_string(),
));
}
} else {
create_stdout_tracer_provider(sampler, resource)?
};
global::set_tracer_provider(tracer_provider);
Ok(())
}
fn create_sampler(sampling_rate: f64) -> Sampler {
if sampling_rate >= 1.0 {
Sampler::AlwaysOn
} else if sampling_rate <= 0.0 {
Sampler::AlwaysOff
} else {
Sampler::TraceIdRatioBased(sampling_rate)
}
}
#[cfg(feature = "otlp")]
async fn create_otlp_tracer_provider(
endpoint: &str,
sampler: Sampler,
resource: Resource,
) -> Result<SdkTracerProvider> {
use opentelemetry_otlp::WithExportConfig;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|e| ObservabilityError::TraceExportFailed(e.to_string()))?;
let provider = SdkTracerProvider::builder()
.with_sampler(sampler)
.with_id_generator(RandomIdGenerator::default())
.with_resource(resource)
.with_batch_exporter(exporter)
.build();
Ok(provider)
}
#[cfg(feature = "jaeger")]
fn create_jaeger_tracer_provider(
endpoint: &str,
sampler: Sampler,
resource: Resource,
) -> Result<SdkTracerProvider> {
tracing::warn!(
"Jaeger agent endpoint '{}' specified, but opentelemetry-jaeger is deprecated. \
Consider using OTLP endpoint with Jaeger's native OTLP support instead.",
endpoint
);
let exporter = opentelemetry_stdout::SpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_sampler(sampler)
.with_id_generator(RandomIdGenerator::default())
.with_resource(resource)
.with_simple_exporter(exporter)
.build();
Ok(provider)
}
fn create_stdout_tracer_provider(
sampler: Sampler,
resource: Resource,
) -> Result<SdkTracerProvider> {
let exporter = opentelemetry_stdout::SpanExporter::default();
let provider = SdkTracerProvider::builder()
.with_sampler(sampler)
.with_id_generator(RandomIdGenerator::default())
.with_resource(resource)
.with_simple_exporter(exporter)
.build();
Ok(provider)
}
pub struct SpanBuilder {
name: String,
attributes: Vec<(String, String)>,
}
impl SpanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
attributes: Vec::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.push((key.into(), value.into()));
self
}
pub fn start(self) -> tracing::Span {
let span = tracing::info_span!(
target: "oxigdal",
"custom_span",
span.name = %self.name
);
for (key, value) in self.attributes {
span.record(key.as_str(), tracing::field::display(&value));
}
span
}
}
pub mod context {
use opentelemetry::global;
use opentelemetry::propagation::{Extractor, Injector};
use std::collections::HashMap;
pub fn extract_from_headers(headers: &HashMap<String, String>) -> opentelemetry::Context {
global::get_text_map_propagator(|propagator| propagator.extract(&HeaderExtractor(headers)))
}
pub fn inject_to_headers(
context: &opentelemetry::Context,
headers: &mut HashMap<String, String>,
) {
global::get_text_map_propagator(|propagator| {
propagator.inject_context(context, &mut HeaderInjector(headers))
})
}
struct HeaderExtractor<'a>(&'a HashMap<String, String>);
impl<'a> Extractor for HeaderExtractor<'a> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).map(|v| v.as_str())
}
fn keys(&self) -> Vec<&str> {
self.0.keys().map(|k| k.as_str()).collect()
}
}
struct HeaderInjector<'a>(&'a mut HashMap<String, String>);
impl<'a> Injector for HeaderInjector<'a> {
fn set(&mut self, key: &str, value: String) {
self.0.insert(key.to_string(), value);
}
}
}
pub enum SamplingStrategy {
Always,
Never,
Probabilistic(f64),
RateLimited(u32),
}
impl SamplingStrategy {
pub fn to_sampler(&self) -> Sampler {
match self {
SamplingStrategy::Always => Sampler::AlwaysOn,
SamplingStrategy::Never => Sampler::AlwaysOff,
SamplingStrategy::Probabilistic(rate) => {
Sampler::TraceIdRatioBased(rate.clamp(0.0, 1.0))
}
SamplingStrategy::RateLimited(_rate) => {
Sampler::ParentBased(Box::new(Sampler::AlwaysOn))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sampler_creation() {
let sampler = create_sampler(1.0);
assert!(matches!(sampler, Sampler::AlwaysOn));
let sampler = create_sampler(0.0);
assert!(matches!(sampler, Sampler::AlwaysOff));
let sampler = create_sampler(0.5);
assert!(matches!(sampler, Sampler::TraceIdRatioBased(_)));
}
#[test]
fn test_span_builder() {
let _guard = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.finish(),
);
let span = SpanBuilder::new("test_span")
.with_attribute("key1", "value1")
.with_attribute("key2", "value2")
.start();
assert!(span.metadata().is_some());
assert_eq!(
span.metadata().expect("span should have metadata").name(),
"custom_span"
);
}
#[test]
fn test_sampling_strategy() {
let strategy = SamplingStrategy::Always;
assert!(matches!(strategy.to_sampler(), Sampler::AlwaysOn));
let strategy = SamplingStrategy::Never;
assert!(matches!(strategy.to_sampler(), Sampler::AlwaysOff));
let strategy = SamplingStrategy::Probabilistic(0.5);
assert!(matches!(
strategy.to_sampler(),
Sampler::TraceIdRatioBased(_)
));
}
}