main
use std::collections::HashMap;
use std::time::Duration;
use std::{env, fmt};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
pub const DEFAULT_METRIC_INTERVAL: Duration = Duration::from_secs(60);
pub const DEFAULT_LOG_FILTER: &str = "info";
const TRACE_PATH: &str = "/v1/traces";
const LOG_PATH: &str = "/v1/logs";
const METRIC_PATH: &str = "/v1/metrics";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamplerKind {
AlwaysOn,
AlwaysOff,
ParentBasedAlwaysOn,
ParentBasedAlwaysOff,
TraceIdRatio,
ParentBasedTraceIdRatio,
}
impl SamplerKind {
pub fn parse(raw: &str) -> Self {
match raw.trim().to_ascii_lowercase().as_str() {
"always_on" => Self::AlwaysOn,
"always_off" => Self::AlwaysOff,
"traceidratio" => Self::TraceIdRatio,
"parentbased_always_off" => Self::ParentBasedAlwaysOff,
"parentbased_traceidratio" => Self::ParentBasedTraceIdRatio,
_ => Self::ParentBasedAlwaysOn,
}
}
}
#[derive(Clone)]
pub struct SignalConfig {
pub enabled: bool,
pub endpoint: Option<String>,
pub headers: HashMap<String, String>,
}
// OTLP headers 常带 Authorization:Debug 输出里只能出现键名。
impl fmt::Debug for SignalConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut keys: Vec<&str> = self.headers.keys().map(String::as_str).collect();
keys.sort_unstable();
formatter
.debug_struct("SignalConfig")
.field("enabled", &self.enabled)
.field("endpoint", &self.endpoint)
.field("headers", &keys)
.finish()
}
}
impl SignalConfig {
pub fn disabled() -> Self {
Self {
enabled: false,
endpoint: None,
headers: HashMap::new(),
}
}
pub fn enabled(endpoint: impl Into<String>) -> Self {
Self {
enabled: true,
endpoint: Some(endpoint.into()),
headers: HashMap::new(),
}
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self.enabled = true;
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn url(&self, path: &str) -> Option<String> {
if !self.enabled {
return None;
}
self.endpoint
.as_deref()
.map(|base| join_endpoint(base, path))
}
fn from_env(
suffix: &str,
common_endpoint: Option<&str>,
common_headers: &HashMap<String, String>,
) -> Self {
let endpoint = env_var(&format!("OTEL_EXPORTER_OTLP_{suffix}_ENDPOINT"))
.or_else(|| common_endpoint.map(str::to_owned));
let mut headers = common_headers.clone();
headers.extend(parse_key_values(
&env_var(&format!("OTEL_EXPORTER_OTLP_{suffix}_HEADERS")).unwrap_or_default(),
));
Self {
enabled: endpoint.is_some(),
endpoint,
headers,
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsConfig {
pub signal: SignalConfig,
pub interval: Duration,
}
impl MetricsConfig {
pub fn disabled() -> Self {
Self {
signal: SignalConfig::disabled(),
interval: DEFAULT_METRIC_INTERVAL,
}
}
pub fn enabled(endpoint: impl Into<String>) -> Self {
Self {
signal: SignalConfig::enabled(endpoint),
interval: DEFAULT_METRIC_INTERVAL,
}
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.signal = self.signal.endpoint(endpoint);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.signal = self.signal.header(key, value);
self
}
pub fn interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
pub fn url(&self) -> Option<String> {
self.signal.url(METRIC_PATH)
}
}
#[derive(Clone)]
pub struct TelemetryConfig {
pub disabled: bool,
pub service_name: String,
pub service_version: Option<String>,
pub environment: Option<String>,
pub attributes: Vec<(String, String)>,
pub headers: HashMap<String, String>,
pub timeout: Duration,
pub sampler: SamplerKind,
pub sample_ratio: f64,
pub traces: SignalConfig,
pub metrics: MetricsConfig,
pub logs: SignalConfig,
pub log_filter: String,
pub stdout: bool,
}
impl fmt::Debug for TelemetryConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut keys: Vec<&str> = self.headers.keys().map(String::as_str).collect();
keys.sort_unstable();
formatter
.debug_struct("TelemetryConfig")
.field("disabled", &self.disabled)
.field("service_name", &self.service_name)
.field("service_version", &self.service_version)
.field("environment", &self.environment)
.field("attributes", &self.attributes)
.field("headers", &keys)
.field("timeout", &self.timeout)
.field("sampler", &self.sampler)
.field("sample_ratio", &self.sample_ratio)
.field("traces", &self.traces)
.field("metrics", &self.metrics)
.field("logs", &self.logs)
.field("log_filter", &self.log_filter)
.field("stdout", &self.stdout)
.finish()
}
}
impl TelemetryConfig {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
disabled: false,
service_name: service_name.into(),
service_version: None,
environment: None,
attributes: Vec::new(),
headers: HashMap::new(),
timeout: DEFAULT_TIMEOUT,
sampler: SamplerKind::ParentBasedAlwaysOn,
sample_ratio: 1.0,
traces: SignalConfig::disabled(),
metrics: MetricsConfig::disabled(),
logs: SignalConfig::disabled(),
log_filter: DEFAULT_LOG_FILTER.to_owned(),
stdout: true,
}
}
pub fn from_env(service_name: impl Into<String>) -> Self {
let mut config = Self::new(service_name);
if env_bool("OTEL_SDK_DISABLED").unwrap_or(false) {
config.disabled = true;
}
if let Some(name) = env_var("OTEL_SERVICE_NAME") {
config.service_name = name;
}
config.attributes =
parse_key_values(&env_var("OTEL_RESOURCE_ATTRIBUTES").unwrap_or_default());
for (key, value) in &config.attributes {
match key.as_str() {
"service.version" => config.service_version = Some(value.clone()),
"deployment.environment" => config.environment = Some(value.clone()),
_ => {}
}
}
let common_endpoint = env_var("OTEL_EXPORTER_OTLP_ENDPOINT");
config.headers =
parse_key_values(&env_var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default())
.into_iter()
.collect();
config.timeout = env_u64("OTEL_EXPORTER_OTLP_TIMEOUT")
.map(Duration::from_millis)
.unwrap_or(DEFAULT_TIMEOUT);
config.sampler = SamplerKind::parse(&env_var("OTEL_TRACES_SAMPLER").unwrap_or_default());
config.sample_ratio = {
let name = "OTEL_TRACES_SAMPLER_ARG";
env_var(name).and_then(|value| value.parse().ok())
}
.unwrap_or(1.0);
config.traces =
SignalConfig::from_env("TRACES", common_endpoint.as_deref(), &config.headers);
config.logs = SignalConfig::from_env("LOGS", common_endpoint.as_deref(), &config.headers);
config.metrics = MetricsConfig {
signal: SignalConfig::from_env("METRICS", common_endpoint.as_deref(), &config.headers),
interval: env_u64("OTEL_METRIC_EXPORT_INTERVAL")
.map(Duration::from_millis)
.unwrap_or(DEFAULT_METRIC_INTERVAL),
};
config.log_filter = env_var("OTEL_LOG_FILTER")
.or_else(|| env_var("RUST_LOG"))
.unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned());
config.stdout = env_bool("OTEL_LOG_STDOUT").unwrap_or(true);
config
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn traces_url(&self) -> Option<String> {
if self.disabled {
return None;
}
self.traces.url(TRACE_PATH)
}
pub fn logs_url(&self) -> Option<String> {
if self.disabled {
return None;
}
self.logs.url(LOG_PATH)
}
pub fn metrics_url(&self) -> Option<String> {
if self.disabled {
return None;
}
self.metrics.url()
}
}
pub fn join_endpoint(base: &str, path: &str) -> String {
if base.ends_with(path) {
return base.to_owned();
}
match base.strip_suffix('/') {
Some(trimmed) => format!("{trimmed}{path}"),
None => format!("{base}{path}"),
}
}
pub fn parse_key_values(raw: &str) -> Vec<(String, String)> {
raw.split(',')
.filter_map(|pair| pair.split_once('='))
.map(|(key, value)| (key.trim().to_owned(), value.trim().to_owned()))
.filter(|(key, _)| !key.is_empty())
.collect()
}
fn env_var(name: &str) -> Option<String> {
env::var(name)
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
fn env_bool(name: &str) -> Option<bool> {
env_var(name).map(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
}
fn env_u64(name: &str) -> Option<u64> {
env_var(name).and_then(|value| value.parse().ok())
}