use std::time::Duration;
#[derive(Debug, Clone)]
pub struct OtelConfig {
pub service_name: String,
pub endpoint: Option<String>,
pub enabled: bool,
pub sample_rate: f64,
pub batch_size: usize,
pub export_interval: Duration,
}
impl Default for OtelConfig {
fn default() -> Self {
Self {
service_name: "vex-api".to_string(),
endpoint: None,
enabled: false,
sample_rate: 1.0,
batch_size: 512,
export_interval: Duration::from_secs(5),
}
}
}
impl OtelConfig {
pub fn from_env() -> Self {
let service_name =
std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "vex-api".to_string());
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
let sample_rate = std::env::var("OTEL_TRACES_SAMPLER_ARG")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1.0);
Self {
service_name,
endpoint: endpoint.clone(),
enabled: endpoint.is_some(),
sample_rate,
..Default::default()
}
}
pub fn development() -> Self {
Self {
service_name: "vex-api-dev".to_string(),
endpoint: None,
enabled: true,
sample_rate: 1.0,
batch_size: 1,
export_interval: Duration::from_secs(1),
}
}
pub fn production(endpoint: &str) -> Self {
Self {
service_name: "vex-api".to_string(),
endpoint: Some(endpoint.to_string()),
enabled: true,
sample_rate: 0.1, batch_size: 512,
export_interval: Duration::from_secs(5),
}
}
}
#[allow(unused_variables)]
pub fn init_tracing(config: &OtelConfig) {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,vex=debug"));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false);
#[cfg(feature = "otel")]
if config.enabled {
if let Some(ref endpoint) = config.endpoint {
use opentelemetry::KeyValue;
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::{
trace::{BatchSpanProcessor, Sampler, TracerProvider},
Resource,
};
use opentelemetry::trace::TracerProvider as _;
tracing::info!(
service = %config.service_name,
endpoint = %endpoint,
sample_rate = config.sample_rate,
"OpenTelemetry OTLP tracing enabled"
);
let resource = Resource::new(vec![KeyValue::new(
"service.name",
config.service_name.clone(),
)]);
let exporter_result = SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build();
match exporter_result {
Ok(exporter) => {
let processor =
BatchSpanProcessor::builder(exporter, opentelemetry_sdk::runtime::Tokio)
.build();
let provider = TracerProvider::builder()
.with_sampler(Sampler::TraceIdRatioBased(config.sample_rate))
.with_resource(resource)
.with_span_processor(processor)
.build();
opentelemetry::global::set_tracer_provider(provider.clone());
let tracer = provider.tracer(config.service_name.clone());
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(otel_layer)
.init();
return;
}
Err(e) => {
eprintln!("OTLP init failed, falling back to console: {}", e);
}
}
}
}
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
}
pub trait VexSpanExt {
fn record_user_id(&self, user_id: &str);
fn record_agent_id(&self, agent_id: &str);
fn record_request_id(&self, request_id: &str);
}
impl VexSpanExt for tracing::Span {
fn record_user_id(&self, user_id: &str) {
self.record("user_id", user_id);
}
fn record_agent_id(&self, agent_id: &str) {
self.record("agent_id", agent_id);
}
fn record_request_id(&self, request_id: &str) {
self.record("request_id", request_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_otel_config_default() {
let config = OtelConfig::default();
assert_eq!(config.service_name, "vex-api");
assert!(!config.enabled);
assert!(config.endpoint.is_none());
}
#[test]
fn test_otel_config_production() {
let config = OtelConfig::production("http://otel:4317");
assert!(config.enabled);
assert_eq!(config.endpoint, Some("http://otel:4317".to_string()));
assert_eq!(config.sample_rate, 0.1);
}
}