use crate::utils::from_env::{EnvItemInfo, FromEnv, FromEnvErr, FromEnvVar};
use opentelemetry::{trace::TracerProvider, KeyValue};
use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
use opentelemetry_semantic_conventions::{
attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION},
SCHEMA_URL,
};
use tracing_subscriber::{EnvFilter, Layer};
use url::Url;
const OTEL_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
const OTEL_LEVEL: &str = "OTEL_LEVEL";
const OTEL_ENVIRONMENT: &str = "OTEL_ENVIRONMENT_NAME";
#[derive(Debug)]
pub struct OtelGuard(SdkTracerProvider, EnvFilter);
impl OtelGuard {
fn tracer(&self, s: &'static str) -> opentelemetry_sdk::trace::Tracer {
self.0.tracer(s)
}
pub fn layer<S>(&self) -> impl Layer<S>
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let tracer = self.tracer("tracing-otel-subscriber");
tracing_opentelemetry::layer()
.with_tracer(tracer)
.with_filter(self.1.clone())
}
}
impl Drop for OtelGuard {
fn drop(&mut self) {
if let Err(err) = self.0.shutdown() {
eprintln!("{err:?}");
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct OtelConfig {
pub endpoint: Url,
pub level: EnvFilter,
pub environment: String,
}
impl FromEnv for OtelConfig {
fn inventory() -> Vec<&'static EnvItemInfo> {
vec![
&EnvItemInfo {
var: OTEL_ENDPOINT,
description:
"OTLP endpoint to send traces to, a url. If missing, disables OTLP exporting.",
optional: true,
},
&EnvItemInfo {
var: OTEL_LEVEL,
description: "OTLP level to export. Follows the RUST_LOG env filter format. e.g. `OTEL_LEVEL=warn,my_crate=info`. Defaults to the value of `RUST_LOG` if not present.",
optional: true,
},
&EnvItemInfo {
var: OTEL_ENVIRONMENT,
description: "OTLP environment name, a string",
optional: true,
},
]
}
fn from_env() -> Result<Self, FromEnvErr> {
let endpoint = Url::from_env_var(OTEL_ENDPOINT)?;
let level = if std::env::var(OTEL_LEVEL)
.as_ref()
.map(String::len)
.unwrap_or_default()
> 0
{
EnvFilter::from_env(OTEL_LEVEL)
} else {
EnvFilter::from_default_env()
};
let environment = String::from_env_var(OTEL_ENVIRONMENT).unwrap_or("unknown".into());
Ok(Self {
endpoint,
level,
environment,
})
}
}
impl OtelConfig {
pub fn load() -> Option<Self> {
Self::from_env().ok()
}
fn resource(&self) -> Resource {
Resource::builder()
.with_schema_url(
[
KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")),
KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")),
KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, self.environment.clone()),
],
SCHEMA_URL,
)
.build()
}
pub fn provider(&self) -> OtelGuard {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.build()
.unwrap();
let provider = SdkTracerProvider::builder()
.with_resource(self.resource())
.with_batch_exporter(exporter)
.build();
OtelGuard(provider, self.level.clone())
}
pub fn into_guard_and_layer<S>(self) -> (OtelGuard, impl Layer<S>)
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let guard = self.provider();
let layer = guard.layer();
(guard, layer)
}
}
#[cfg(test)]
mod test {
use super::*;
const URL: &str = "http://localhost:4317";
fn clear_env() {
std::env::remove_var(OTEL_ENDPOINT);
std::env::remove_var(OTEL_LEVEL);
std::env::remove_var(OTEL_ENVIRONMENT);
}
fn run_clear_env<F>(f: F)
where
F: FnOnce(),
{
f();
clear_env();
}
#[test]
#[serial_test::serial]
fn test_env_read() {
run_clear_env(|| {
std::env::set_var(OTEL_ENDPOINT, URL);
std::env::set_var(OTEL_LEVEL, "debug");
let cfg = OtelConfig::load().unwrap();
assert_eq!(cfg.endpoint, URL.parse().unwrap());
assert_eq!(
cfg.level.max_level_hint(),
Some(tracing::Level::DEBUG.into())
);
assert_eq!(cfg.environment, "unknown");
})
}
#[test]
#[serial_test::serial]
fn test_env_read_level() {
run_clear_env(|| {
std::env::set_var(OTEL_ENDPOINT, URL);
std::env::set_var(OTEL_LEVEL, "warn,my_app=info");
let cfg = OtelConfig::load().unwrap();
let s = cfg.level.to_string();
let iter = s.split(",");
assert!(iter.clone().any(|x| x == "warn"));
assert!(iter.clone().any(|x| x == "my_app=info"));
})
}
#[test]
#[serial_test::serial]
fn invalid_url() {
run_clear_env(|| {
std::env::set_var(OTEL_ENDPOINT, "not a url");
let cfg = OtelConfig::load();
assert!(cfg.is_none());
})
}
}