use serde::{Deserialize, Serialize};
const DEFAULT_SERVICE_NAME: &str = "quiver";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OtlpConfig {
pub endpoint: String,
pub service_name: String,
pub timeout_secs: u64,
}
impl Default for OtlpConfig {
fn default() -> Self {
Self {
endpoint: String::new(),
service_name: DEFAULT_SERVICE_NAME.to_owned(),
timeout_secs: DEFAULT_TIMEOUT_SECS,
}
}
}
impl OtlpConfig {
#[must_use]
pub fn is_enabled(&self) -> bool {
!self.endpoint.trim().is_empty()
}
pub fn apply_env_overrides(&mut self) -> Result<(), String> {
if let Ok(v) = std::env::var("QUIVER_OTLP_ENDPOINT") {
self.endpoint = v;
}
if let Ok(v) = std::env::var("QUIVER_OTLP_SERVICE_NAME") {
self.service_name = v;
}
if let Ok(v) = std::env::var("QUIVER_OTLP_TIMEOUT_SECS") {
self.timeout_secs = v
.parse()
.map_err(|_| format!("QUIVER_OTLP_TIMEOUT_SECS must be an integer, got {v:?}"))?;
}
Ok(())
}
}
#[cfg(feature = "otlp")]
mod live {
use std::sync::OnceLock;
use std::time::Duration;
use super::OtlpConfig;
static PROVIDER: OnceLock<opentelemetry_sdk::trace::SdkTracerProvider> = OnceLock::new();
pub fn build_provider(
cfg: &OtlpConfig,
) -> Result<opentelemetry_sdk::trace::SdkTracerProvider, String> {
use opentelemetry_otlp::WithExportConfig;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&cfg.endpoint)
.with_timeout(Duration::from_secs(cfg.timeout_secs))
.build()
.map_err(|e| format!("building OTLP span exporter: {e}"))?;
let resource = opentelemetry_sdk::Resource::builder()
.with_service_name(cfg.service_name.clone())
.build();
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build();
Ok(provider)
}
pub fn store_provider(provider: opentelemetry_sdk::trace::SdkTracerProvider) {
let _ = PROVIDER.set(provider);
}
pub fn shutdown() {
if let Some(provider) = PROVIDER.get() {
let _ = provider.shutdown();
}
}
}
#[cfg(feature = "otlp")]
pub use live::{build_provider, shutdown, store_provider};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_by_default() {
let c = OtlpConfig::default();
assert!(!c.is_enabled());
assert_eq!(c.service_name, "quiver");
assert_eq!(c.timeout_secs, 10);
}
#[test]
fn enabled_when_endpoint_set_and_defaults_apply() {
let c: OtlpConfig =
serde_json::from_value(serde_json::json!({"endpoint":"http://localhost:4317"}))
.unwrap();
assert!(c.is_enabled());
assert_eq!(c.service_name, "quiver");
assert_eq!(c.timeout_secs, 10);
}
#[test]
fn whitespace_endpoint_is_not_enabled() {
let c: OtlpConfig = serde_json::from_value(serde_json::json!({"endpoint":" "})).unwrap();
assert!(!c.is_enabled());
}
#[test]
fn fields_deserialize() {
let c: OtlpConfig = serde_json::from_value(serde_json::json!({
"endpoint":"http://collector:4317","service_name":"q-prod","timeout_secs":3
}))
.unwrap();
assert_eq!(c.service_name, "q-prod");
assert_eq!(c.timeout_secs, 3);
assert!(c.is_enabled());
}
#[test]
fn env_overrides_apply() {
unsafe {
std::env::set_var("QUIVER_OTLP_ENDPOINT", "http://envhost:4317");
std::env::set_var("QUIVER_OTLP_SERVICE_NAME", "from-env");
std::env::set_var("QUIVER_OTLP_TIMEOUT_SECS", "7");
}
let mut c = OtlpConfig::default();
c.apply_env_overrides().unwrap();
assert_eq!(c.endpoint, "http://envhost:4317");
assert_eq!(c.service_name, "from-env");
assert_eq!(c.timeout_secs, 7);
unsafe { std::env::set_var("QUIVER_OTLP_TIMEOUT_SECS", "soon") }
assert!(OtlpConfig::default().apply_env_overrides().is_err());
unsafe {
std::env::remove_var("QUIVER_OTLP_ENDPOINT");
std::env::remove_var("QUIVER_OTLP_SERVICE_NAME");
std::env::remove_var("QUIVER_OTLP_TIMEOUT_SECS");
}
}
}