#[cfg(feature = "metrics")]
pub const DEFAULT_METRICS_BUDGET: usize = 8192;
pub const DEFAULT_SERVICE_VERSION: &str = "0.0.0";
pub const DEFAULT_LOG_LEVEL: &str = "info";
pub const DEFAULT_SAMPLE_RATE: f32 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum LogFormat {
#[default]
Json,
Pretty,
}
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
#[cfg_attr(not(feature = "otlp"), allow(dead_code))]
pub(crate) service_name: String,
pub(crate) service_version: String,
pub(crate) log_level: String,
pub(crate) log_format: LogFormat,
#[cfg(feature = "metrics")]
pub(crate) metrics_budget: usize,
#[cfg(feature = "otlp")]
pub(crate) otlp_endpoint: Option<String>,
pub(crate) sample_rate: f32,
}
impl TelemetryConfig {
#[must_use]
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
service_version: DEFAULT_SERVICE_VERSION.to_owned(),
log_level: DEFAULT_LOG_LEVEL.to_owned(),
log_format: LogFormat::default(),
#[cfg(feature = "metrics")]
metrics_budget: DEFAULT_METRICS_BUDGET,
#[cfg(feature = "otlp")]
otlp_endpoint: None,
sample_rate: DEFAULT_SAMPLE_RATE,
}
}
#[must_use]
pub fn version(mut self, version: impl Into<String>) -> Self {
self.service_version = version.into();
self
}
#[must_use]
pub fn log_format(mut self, format: LogFormat) -> Self {
self.log_format = format;
self
}
#[must_use]
pub fn log_level(mut self, level: impl Into<String>) -> Self {
self.log_level = level.into();
self
}
#[cfg(feature = "metrics")]
#[must_use]
pub fn metrics_budget(mut self, budget: usize) -> Self {
self.metrics_budget = budget;
self
}
#[cfg(feature = "otlp")]
#[must_use]
pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.otlp_endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn sample_rate(mut self, rate: f32) -> Self {
self.sample_rate = if rate.is_nan() {
DEFAULT_SAMPLE_RATE
} else {
rate.clamp(0.0, 1.0)
};
self
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
use super::*;
use crate::error::TelemetryError;
#[test]
fn defaults_match_estate_documentation() {
let cfg = TelemetryConfig::new("payments-api");
assert_eq!(cfg.service_name, "payments-api");
assert_eq!(cfg.service_version, DEFAULT_SERVICE_VERSION);
assert_eq!(cfg.log_level, DEFAULT_LOG_LEVEL);
assert_eq!(cfg.log_format, LogFormat::Json);
assert_eq!(cfg.sample_rate, 1.0);
#[cfg(feature = "metrics")]
assert_eq!(cfg.metrics_budget, DEFAULT_METRICS_BUDGET);
#[cfg(feature = "otlp")]
assert!(cfg.otlp_endpoint.is_none());
}
#[test]
fn builder_chain_sets_every_field() {
let cfg = TelemetryConfig::new("svc")
.version("1.2.3")
.log_level("debug,hyper=warn")
.log_format(LogFormat::Pretty)
.sample_rate(0.25);
assert_eq!(cfg.service_version, "1.2.3");
assert_eq!(cfg.log_level, "debug,hyper=warn");
assert_eq!(cfg.log_format, LogFormat::Pretty);
assert_eq!(cfg.sample_rate, 0.25);
#[cfg(feature = "metrics")]
let cfg = cfg.metrics_budget(1024);
#[cfg(feature = "otlp")]
let cfg = cfg.otlp_endpoint("http://localhost:4317");
#[cfg(feature = "metrics")]
assert_eq!(cfg.metrics_budget, 1024);
#[cfg(feature = "otlp")]
assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://localhost:4317"));
let _ = cfg;
}
#[test]
fn setters_overwrite_previous_values() {
let cfg = TelemetryConfig::new("svc")
.log_level("info")
.log_level("warn")
.version("a")
.version("b")
.log_format(LogFormat::Pretty)
.log_format(LogFormat::Json);
assert_eq!(cfg.log_level, "warn");
assert_eq!(cfg.service_version, "b");
assert_eq!(cfg.log_format, LogFormat::Json);
}
#[test]
fn sample_rate_clamps_into_unit_range() {
assert_eq!(TelemetryConfig::new("s").sample_rate(2.0).sample_rate, 1.0);
assert_eq!(TelemetryConfig::new("s").sample_rate(-0.5).sample_rate, 0.0);
assert_eq!(
TelemetryConfig::new("s").sample_rate(0.125).sample_rate,
0.125
);
assert_eq!(TelemetryConfig::new("s").sample_rate(0.0).sample_rate, 0.0);
assert_eq!(TelemetryConfig::new("s").sample_rate(1.0).sample_rate, 1.0);
}
#[test]
fn sample_rate_nan_falls_back_to_full_sampling() {
assert_eq!(
TelemetryConfig::new("s").sample_rate(f32::NAN).sample_rate,
DEFAULT_SAMPLE_RATE
);
}
#[test]
fn log_format_traits() {
assert_eq!(LogFormat::default(), LogFormat::Json);
assert_ne!(LogFormat::Json, LogFormat::Pretty);
assert_eq!(format!("{:?}", LogFormat::Pretty), "Pretty");
let copied = LogFormat::Json;
assert_eq!(copied, LogFormat::Json);
}
#[test]
fn config_is_clone_and_debug() {
let cfg = TelemetryConfig::new("svc").version("9.9.9");
let cloned = cfg.clone();
assert_eq!(cloned.service_name, "svc");
let debug = format!("{cfg:?}");
assert!(debug.contains("TelemetryConfig"));
assert!(debug.contains("svc"));
}
#[test]
fn empty_service_name_is_accepted() {
let cfg = TelemetryConfig::new("");
assert_eq!(cfg.service_name, "");
}
#[test]
fn unparseable_log_level_is_a_typed_error() {
assert!(matches!(
crate::telemetry::build_filter("a=b=c"),
Err(TelemetryError::InitFailed(_))
));
}
#[test]
fn composite_directives_parse() {
crate::telemetry::build_filter("info,hyper=warn").unwrap();
}
}