use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComputeEnvironment {
#[default]
Auto,
Lambda,
Kubernetes,
None,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Grpc,
#[default]
#[serde(alias = "http_binary", alias = "http-binary")]
HttpBinary,
#[serde(alias = "http_json", alias = "http-json")]
HttpJson,
}
impl Protocol {
#[must_use]
pub fn default_endpoint(&self) -> &'static str {
match self {
Protocol::Grpc => "http://localhost:4317",
Protocol::HttpBinary | Protocol::HttpJson => "http://localhost:4318",
}
}
#[must_use]
pub fn default_port(&self) -> u16 {
match self {
Protocol::Grpc => 4317,
Protocol::HttpBinary | Protocol::HttpJson => 4318,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OtelSdkConfig {
pub endpoint: EndpointConfig,
pub resource: ResourceConfig,
pub traces: SignalConfig,
pub metrics: SignalConfig,
pub logs: SignalConfig,
pub init_tracing_subscriber: bool,
pub instrumentation_scope_name: Option<String>,
}
impl Default for OtelSdkConfig {
fn default() -> Self {
Self {
endpoint: EndpointConfig::default(),
resource: ResourceConfig::default(),
traces: SignalConfig::default_enabled(),
metrics: SignalConfig::default_enabled(),
logs: SignalConfig::default_enabled(),
init_tracing_subscriber: true,
instrumentation_scope_name: None,
}
}
}
impl OtelSdkConfig {
#[must_use]
pub fn effective_endpoint(&self) -> String {
self.endpoint
.url
.clone()
.unwrap_or_else(|| self.endpoint.protocol.default_endpoint().to_string())
}
#[must_use]
pub fn signal_endpoint(&self, signal_path: &str) -> String {
let base = self.effective_endpoint();
let base = base.trim_end_matches('/');
match self.endpoint.protocol {
Protocol::Grpc => base.to_string(),
Protocol::HttpBinary | Protocol::HttpJson => {
format!("{base}{signal_path}")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EndpointConfig {
pub url: Option<String>,
pub protocol: Protocol,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
#[serde(default)]
pub headers: HashMap<String, String>,
}
impl Default for EndpointConfig {
fn default() -> Self {
Self {
url: None,
protocol: Protocol::default(),
timeout: Duration::from_secs(10),
headers: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ResourceConfig {
pub service_name: Option<String>,
pub service_version: Option<String>,
pub deployment_environment: Option<String>,
#[serde(default)]
pub attributes: HashMap<String, String>,
#[serde(default)]
pub compute_environment: ComputeEnvironment,
}
impl ResourceConfig {
pub fn with_service_name(name: impl Into<String>) -> Self {
Self {
service_name: Some(name.into()),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SignalConfig {
pub enabled: bool,
pub batch: BatchConfig,
}
impl SignalConfig {
#[must_use]
pub fn default_enabled() -> Self {
Self {
enabled: true,
batch: BatchConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct BatchConfig {
pub max_queue_size: usize,
pub max_export_batch_size: usize,
#[serde(with = "humantime_serde")]
pub scheduled_delay: Duration,
#[serde(with = "humantime_serde")]
pub export_timeout: Duration,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
max_queue_size: 2048,
max_export_batch_size: 512,
scheduled_delay: Duration::from_secs(5),
export_timeout: Duration::from_secs(30),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protocol_default_endpoint() {
assert_eq!(Protocol::Grpc.default_endpoint(), "http://localhost:4317");
assert_eq!(
Protocol::HttpBinary.default_endpoint(),
"http://localhost:4318"
);
assert_eq!(
Protocol::HttpJson.default_endpoint(),
"http://localhost:4318"
);
}
#[test]
fn test_otel_sdk_config_effective_endpoint() {
let config = OtelSdkConfig::default();
assert_eq!(config.effective_endpoint(), "http://localhost:4318");
let mut config = OtelSdkConfig::default();
config.endpoint.protocol = Protocol::Grpc;
assert_eq!(config.effective_endpoint(), "http://localhost:4317");
let mut config = OtelSdkConfig::default();
config.endpoint.url = Some("http://collector:4318".to_string());
assert_eq!(config.effective_endpoint(), "http://collector:4318");
}
#[test]
fn signal_endpoint_appends_path_for_http_protocols() {
let config = OtelSdkConfig::default();
assert_eq!(
config.signal_endpoint("/v1/traces"),
"http://localhost:4318/v1/traces"
);
}
#[test]
fn signal_endpoint_strips_trailing_slash_before_appending() {
let mut config = OtelSdkConfig::default();
config.endpoint.url = Some("http://collector:4318/".to_string());
assert_eq!(
config.signal_endpoint("/v1/traces"),
"http://collector:4318/v1/traces"
);
}
#[test]
fn signal_endpoint_returns_base_only_for_grpc() {
let mut config = OtelSdkConfig::default();
config.endpoint.protocol = Protocol::Grpc;
assert_eq!(
config.signal_endpoint("/v1/traces"),
"http://localhost:4317"
);
}
#[test]
fn test_resource_config_with_service_name() {
let config = ResourceConfig::with_service_name("my-service");
assert_eq!(config.service_name, Some("my-service".to_string()));
}
#[test]
fn test_batch_config_defaults() {
let config = BatchConfig::default();
assert_eq!(config.max_queue_size, 2048);
assert_eq!(config.max_export_batch_size, 512);
assert_eq!(config.scheduled_delay, Duration::from_secs(5));
assert_eq!(config.export_timeout, Duration::from_secs(30));
}
}