opentelemetry_configuration/
config.rs

1//! Configuration types for the OpenTelemetry SDK.
2//!
3//! These types are designed to be deserialised from multiple sources using
4//! figment, supporting layered configuration from defaults, files, and
5//! environment variables.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// Compute environment for resource attribute detection.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ComputeEnvironment {
15    /// Automatically detect the compute environment.
16    /// Runs generic detectors (host, OS, process) and probes for Lambda/K8s.
17    #[default]
18    Auto,
19    /// AWS Lambda - generic detectors + Lambda-specific attributes (faas.*)
20    Lambda,
21    /// Kubernetes - generic detectors + K8s-specific attributes
22    Kubernetes,
23    /// No automatic detection - only use explicitly configured attributes.
24    None,
25}
26
27/// OTLP export protocol.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Protocol {
31    /// gRPC protocol (default port 4317).
32    Grpc,
33    /// HTTP with Protocol Buffers encoding (default port 4318).
34    #[default]
35    #[serde(alias = "http_binary", alias = "http-binary")]
36    HttpBinary,
37    /// HTTP with JSON encoding (default port 4318).
38    #[serde(alias = "http_json", alias = "http-json")]
39    HttpJson,
40}
41
42impl Protocol {
43    /// Returns the default endpoint for this protocol.
44    #[must_use]
45    pub fn default_endpoint(&self) -> &'static str {
46        match self {
47            Protocol::Grpc => "http://localhost:4317",
48            Protocol::HttpBinary | Protocol::HttpJson => "http://localhost:4318",
49        }
50    }
51
52    /// Returns the default port for this protocol.
53    #[must_use]
54    pub fn default_port(&self) -> u16 {
55        match self {
56            Protocol::Grpc => 4317,
57            Protocol::HttpBinary | Protocol::HttpJson => 4318,
58        }
59    }
60}
61
62/// Complete OpenTelemetry SDK configuration.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(default)]
65pub struct OtelSdkConfig {
66    /// Endpoint configuration.
67    pub endpoint: EndpointConfig,
68
69    /// Resource configuration.
70    pub resource: ResourceConfig,
71
72    /// Traces configuration.
73    pub traces: SignalConfig,
74
75    /// Metrics configuration.
76    pub metrics: SignalConfig,
77
78    /// Logs configuration.
79    pub logs: SignalConfig,
80
81    /// Whether to initialise the tracing subscriber.
82    pub init_tracing_subscriber: bool,
83
84    /// Name for the instrumentation scope (otel.library.name).
85    /// Defaults to `service_name` if set, otherwise "opentelemetry-configuration".
86    pub instrumentation_scope_name: Option<String>,
87}
88
89impl Default for OtelSdkConfig {
90    fn default() -> Self {
91        Self {
92            endpoint: EndpointConfig::default(),
93            resource: ResourceConfig::default(),
94            traces: SignalConfig::default_enabled(),
95            metrics: SignalConfig::default_enabled(),
96            logs: SignalConfig::default_enabled(),
97            init_tracing_subscriber: true,
98            instrumentation_scope_name: None,
99        }
100    }
101}
102
103impl OtelSdkConfig {
104    /// Returns the effective endpoint URL, using protocol defaults if not specified.
105    #[must_use]
106    pub fn effective_endpoint(&self) -> String {
107        self.endpoint
108            .url
109            .clone()
110            .unwrap_or_else(|| self.endpoint.protocol.default_endpoint().to_string())
111    }
112
113    /// Returns the endpoint URL for a specific signal type.
114    #[must_use]
115    pub fn signal_endpoint(&self, signal_path: &str) -> String {
116        let base = self.effective_endpoint();
117        let base = base.trim_end_matches('/');
118
119        match self.endpoint.protocol {
120            Protocol::Grpc => base.to_string(),
121            Protocol::HttpBinary | Protocol::HttpJson => {
122                format!("{base}{signal_path}")
123            }
124        }
125    }
126}
127
128/// Endpoint configuration.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(default)]
131pub struct EndpointConfig {
132    /// OTLP endpoint URL.
133    ///
134    /// If not specified, uses the protocol's default:
135    /// - gRPC: `http://localhost:4317`
136    /// - HTTP: `http://localhost:4318`
137    pub url: Option<String>,
138
139    /// Export protocol.
140    pub protocol: Protocol,
141
142    /// Request timeout.
143    #[serde(with = "humantime_serde")]
144    pub timeout: Duration,
145
146    /// HTTP headers for authentication or customisation.
147    #[serde(default)]
148    pub headers: HashMap<String, String>,
149}
150
151impl Default for EndpointConfig {
152    fn default() -> Self {
153        Self {
154            url: None,
155            protocol: Protocol::default(),
156            timeout: Duration::from_secs(10),
157            headers: HashMap::new(),
158        }
159    }
160}
161
162/// Resource configuration.
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164#[serde(default)]
165pub struct ResourceConfig {
166    /// Service name.
167    pub service_name: Option<String>,
168
169    /// Service version.
170    pub service_version: Option<String>,
171
172    /// Deployment environment (e.g., "production", "staging").
173    pub deployment_environment: Option<String>,
174
175    /// Additional resource attributes.
176    #[serde(default)]
177    pub attributes: HashMap<String, String>,
178
179    /// Compute environment for automatic resource detection.
180    #[serde(default)]
181    pub compute_environment: ComputeEnvironment,
182}
183
184impl ResourceConfig {
185    /// Creates a new resource config with a service name.
186    pub fn with_service_name(name: impl Into<String>) -> Self {
187        Self {
188            service_name: Some(name.into()),
189            ..Default::default()
190        }
191    }
192}
193
194/// Configuration for an individual signal type (traces, metrics, logs).
195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
196#[serde(default)]
197pub struct SignalConfig {
198    /// Whether this signal is enabled.
199    pub enabled: bool,
200
201    /// Batch export configuration.
202    pub batch: BatchConfig,
203}
204
205impl SignalConfig {
206    /// Creates a default config with the signal enabled.
207    #[must_use]
208    pub fn default_enabled() -> Self {
209        Self {
210            enabled: true,
211            batch: BatchConfig::default(),
212        }
213    }
214}
215
216/// Batch exporter configuration.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(default)]
219pub struct BatchConfig {
220    /// Maximum queue size.
221    pub max_queue_size: usize,
222
223    /// Maximum batch size for export.
224    pub max_export_batch_size: usize,
225
226    /// Scheduled delay between exports.
227    #[serde(with = "humantime_serde")]
228    pub scheduled_delay: Duration,
229
230    /// Maximum time to wait for export to complete.
231    #[serde(with = "humantime_serde")]
232    pub export_timeout: Duration,
233}
234
235impl Default for BatchConfig {
236    fn default() -> Self {
237        Self {
238            max_queue_size: 2048,
239            max_export_batch_size: 512,
240            scheduled_delay: Duration::from_secs(5),
241            export_timeout: Duration::from_secs(30),
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_protocol_default_endpoint() {
252        assert_eq!(Protocol::Grpc.default_endpoint(), "http://localhost:4317");
253        assert_eq!(
254            Protocol::HttpBinary.default_endpoint(),
255            "http://localhost:4318"
256        );
257        assert_eq!(
258            Protocol::HttpJson.default_endpoint(),
259            "http://localhost:4318"
260        );
261    }
262
263    #[test]
264    fn test_otel_sdk_config_effective_endpoint() {
265        let config = OtelSdkConfig::default();
266        assert_eq!(config.effective_endpoint(), "http://localhost:4318");
267
268        let mut config = OtelSdkConfig::default();
269        config.endpoint.protocol = Protocol::Grpc;
270        assert_eq!(config.effective_endpoint(), "http://localhost:4317");
271
272        let mut config = OtelSdkConfig::default();
273        config.endpoint.url = Some("http://collector:4318".to_string());
274        assert_eq!(config.effective_endpoint(), "http://collector:4318");
275    }
276
277    #[test]
278    fn signal_endpoint_appends_path_for_http_protocols() {
279        let config = OtelSdkConfig::default();
280        assert_eq!(
281            config.signal_endpoint("/v1/traces"),
282            "http://localhost:4318/v1/traces"
283        );
284    }
285
286    #[test]
287    fn signal_endpoint_strips_trailing_slash_before_appending() {
288        let mut config = OtelSdkConfig::default();
289        config.endpoint.url = Some("http://collector:4318/".to_string());
290        assert_eq!(
291            config.signal_endpoint("/v1/traces"),
292            "http://collector:4318/v1/traces"
293        );
294    }
295
296    #[test]
297    fn signal_endpoint_returns_base_only_for_grpc() {
298        let mut config = OtelSdkConfig::default();
299        config.endpoint.protocol = Protocol::Grpc;
300        assert_eq!(
301            config.signal_endpoint("/v1/traces"),
302            "http://localhost:4317"
303        );
304    }
305
306    #[test]
307    fn test_resource_config_with_service_name() {
308        let config = ResourceConfig::with_service_name("my-service");
309        assert_eq!(config.service_name, Some("my-service".to_string()));
310    }
311
312    #[test]
313    fn test_batch_config_defaults() {
314        let config = BatchConfig::default();
315        assert_eq!(config.max_queue_size, 2048);
316        assert_eq!(config.max_export_batch_size, 512);
317        assert_eq!(config.scheduled_delay, Duration::from_secs(5));
318        assert_eq!(config.export_timeout, Duration::from_secs(30));
319    }
320}