opentelemetry_configuration/
config.rs1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ComputeEnvironment {
15 #[default]
18 Auto,
19 Lambda,
21 Kubernetes,
23 None,
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Protocol {
31 Grpc,
33 #[default]
35 #[serde(alias = "http_binary", alias = "http-binary")]
36 HttpBinary,
37 #[serde(alias = "http_json", alias = "http-json")]
39 HttpJson,
40}
41
42impl Protocol {
43 pub fn default_endpoint(&self) -> &'static str {
45 match self {
46 Protocol::Grpc => "http://localhost:4317",
47 Protocol::HttpBinary | Protocol::HttpJson => "http://localhost:4318",
48 }
49 }
50
51 pub fn default_port(&self) -> u16 {
53 match self {
54 Protocol::Grpc => 4317,
55 Protocol::HttpBinary | Protocol::HttpJson => 4318,
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(default)]
63pub struct OtelSdkConfig {
64 pub endpoint: EndpointConfig,
66
67 pub resource: ResourceConfig,
69
70 pub traces: SignalConfig,
72
73 pub metrics: SignalConfig,
75
76 pub logs: SignalConfig,
78
79 pub init_tracing_subscriber: bool,
81
82 pub instrumentation_scope_name: Option<String>,
85}
86
87impl Default for OtelSdkConfig {
88 fn default() -> Self {
89 Self {
90 endpoint: EndpointConfig::default(),
91 resource: ResourceConfig::default(),
92 traces: SignalConfig::default_enabled(),
93 metrics: SignalConfig::default_enabled(),
94 logs: SignalConfig::default_enabled(),
95 init_tracing_subscriber: true,
96 instrumentation_scope_name: None,
97 }
98 }
99}
100
101impl OtelSdkConfig {
102 pub fn effective_endpoint(&self) -> String {
104 self.endpoint
105 .url
106 .clone()
107 .unwrap_or_else(|| self.endpoint.protocol.default_endpoint().to_string())
108 }
109
110 pub fn signal_endpoint(&self, signal_path: &str) -> String {
112 let base = self.effective_endpoint();
113 let base = base.trim_end_matches('/');
114
115 match self.endpoint.protocol {
116 Protocol::Grpc => base.to_string(),
117 Protocol::HttpBinary | Protocol::HttpJson => {
118 format!("{}{}", base, signal_path)
119 }
120 }
121 }
122
123 pub fn merge(mut self, other: Self) -> Self {
125 self.endpoint = self.endpoint.merge(other.endpoint);
126 self.resource = self.resource.merge(other.resource);
127 self.traces = self.traces.merge(other.traces);
128 self.metrics = self.metrics.merge(other.metrics);
129 self.logs = self.logs.merge(other.logs);
130
131 if other.init_tracing_subscriber != Self::default().init_tracing_subscriber {
132 self.init_tracing_subscriber = other.init_tracing_subscriber;
133 }
134
135 self
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(default)]
142pub struct EndpointConfig {
143 pub url: Option<String>,
149
150 pub protocol: Protocol,
152
153 #[serde(with = "humantime_serde")]
155 pub timeout: Duration,
156
157 #[serde(default)]
159 pub headers: HashMap<String, String>,
160}
161
162impl Default for EndpointConfig {
163 fn default() -> Self {
164 Self {
165 url: None,
166 protocol: Protocol::default(),
167 timeout: Duration::from_secs(10),
168 headers: HashMap::new(),
169 }
170 }
171}
172
173impl EndpointConfig {
174 pub fn merge(mut self, other: Self) -> Self {
176 if other.url.is_some() {
177 self.url = other.url;
178 }
179 if other.protocol != Protocol::default() {
180 self.protocol = other.protocol;
181 }
182 if other.timeout != Self::default().timeout {
183 self.timeout = other.timeout;
184 }
185 self.headers.extend(other.headers);
186 self
187 }
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192#[serde(default)]
193pub struct ResourceConfig {
194 pub service_name: Option<String>,
196
197 pub service_version: Option<String>,
199
200 pub deployment_environment: Option<String>,
202
203 #[serde(default)]
205 pub attributes: HashMap<String, String>,
206
207 #[serde(default)]
209 pub compute_environment: ComputeEnvironment,
210}
211
212impl ResourceConfig {
213 pub fn with_service_name(name: impl Into<String>) -> Self {
215 Self {
216 service_name: Some(name.into()),
217 ..Default::default()
218 }
219 }
220
221 pub fn merge(mut self, other: Self) -> Self {
223 if other.service_name.is_some() {
224 self.service_name = other.service_name;
225 }
226 if other.service_version.is_some() {
227 self.service_version = other.service_version;
228 }
229 if other.deployment_environment.is_some() {
230 self.deployment_environment = other.deployment_environment;
231 }
232 self.attributes.extend(other.attributes);
233 if other.compute_environment != ComputeEnvironment::default() {
234 self.compute_environment = other.compute_environment;
235 }
236 self
237 }
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242#[serde(default)]
243pub struct SignalConfig {
244 pub enabled: bool,
246
247 pub batch: BatchConfig,
249}
250
251impl SignalConfig {
252 pub fn default_enabled() -> Self {
254 Self {
255 enabled: true,
256 batch: BatchConfig::default(),
257 }
258 }
259
260 pub fn merge(mut self, other: Self) -> Self {
262 self.enabled = other.enabled;
263 self.batch = self.batch.merge(other.batch);
264 self
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(default)]
271pub struct BatchConfig {
272 pub max_queue_size: usize,
274
275 pub max_export_batch_size: usize,
277
278 #[serde(with = "humantime_serde")]
280 pub scheduled_delay: Duration,
281
282 #[serde(with = "humantime_serde")]
284 pub export_timeout: Duration,
285}
286
287impl Default for BatchConfig {
288 fn default() -> Self {
289 Self {
290 max_queue_size: 2048,
291 max_export_batch_size: 512,
292 scheduled_delay: Duration::from_secs(5),
293 export_timeout: Duration::from_secs(30),
294 }
295 }
296}
297
298impl BatchConfig {
299 pub fn merge(mut self, other: Self) -> Self {
301 let default = Self::default();
302 if other.max_queue_size != default.max_queue_size {
303 self.max_queue_size = other.max_queue_size;
304 }
305 if other.max_export_batch_size != default.max_export_batch_size {
306 self.max_export_batch_size = other.max_export_batch_size;
307 }
308 if other.scheduled_delay != default.scheduled_delay {
309 self.scheduled_delay = other.scheduled_delay;
310 }
311 if other.export_timeout != default.export_timeout {
312 self.export_timeout = other.export_timeout;
313 }
314 self
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_compute_environment_default() {
324 assert_eq!(ComputeEnvironment::default(), ComputeEnvironment::Auto);
325 }
326
327 #[test]
328 fn test_compute_environment_serde() {
329 let env: ComputeEnvironment = serde_json::from_str(r#""auto""#).unwrap();
330 assert_eq!(env, ComputeEnvironment::Auto);
331
332 let env: ComputeEnvironment = serde_json::from_str(r#""lambda""#).unwrap();
333 assert_eq!(env, ComputeEnvironment::Lambda);
334
335 let env: ComputeEnvironment = serde_json::from_str(r#""kubernetes""#).unwrap();
336 assert_eq!(env, ComputeEnvironment::Kubernetes);
337
338 let env: ComputeEnvironment = serde_json::from_str(r#""none""#).unwrap();
339 assert_eq!(env, ComputeEnvironment::None);
340 }
341
342 #[test]
343 fn test_protocol_default() {
344 assert_eq!(Protocol::default(), Protocol::HttpBinary);
345 }
346
347 #[test]
348 fn test_protocol_default_endpoint() {
349 assert_eq!(Protocol::Grpc.default_endpoint(), "http://localhost:4317");
350 assert_eq!(
351 Protocol::HttpBinary.default_endpoint(),
352 "http://localhost:4318"
353 );
354 assert_eq!(
355 Protocol::HttpJson.default_endpoint(),
356 "http://localhost:4318"
357 );
358 }
359
360 #[test]
361 fn test_protocol_serde() {
362 let protocol: Protocol = serde_json::from_str(r#""grpc""#).unwrap();
363 assert_eq!(protocol, Protocol::Grpc);
364
365 let protocol: Protocol = serde_json::from_str(r#""httpbinary""#).unwrap();
366 assert_eq!(protocol, Protocol::HttpBinary);
367
368 let protocol: Protocol = serde_json::from_str(r#""http_binary""#).unwrap();
369 assert_eq!(protocol, Protocol::HttpBinary);
370
371 let protocol: Protocol = serde_json::from_str(r#""http-json""#).unwrap();
372 assert_eq!(protocol, Protocol::HttpJson);
373 }
374
375 #[test]
376 fn test_otel_sdk_config_effective_endpoint() {
377 let config = OtelSdkConfig::default();
378 assert_eq!(config.effective_endpoint(), "http://localhost:4318");
379
380 let mut config = OtelSdkConfig::default();
381 config.endpoint.protocol = Protocol::Grpc;
382 assert_eq!(config.effective_endpoint(), "http://localhost:4317");
383
384 let mut config = OtelSdkConfig::default();
385 config.endpoint.url = Some("http://collector:4318".to_string());
386 assert_eq!(config.effective_endpoint(), "http://collector:4318");
387 }
388
389 #[test]
390 fn test_otel_sdk_config_signal_endpoint() {
391 let config = OtelSdkConfig::default();
392 assert_eq!(
393 config.signal_endpoint("/v1/traces"),
394 "http://localhost:4318/v1/traces"
395 );
396
397 let mut config = OtelSdkConfig::default();
398 config.endpoint.protocol = Protocol::Grpc;
399 assert_eq!(
400 config.signal_endpoint("/v1/traces"),
401 "http://localhost:4317"
402 );
403 }
404
405 #[test]
406 fn test_resource_config_with_service_name() {
407 let config = ResourceConfig::with_service_name("my-service");
408 assert_eq!(config.service_name, Some("my-service".to_string()));
409 }
410
411 #[test]
412 fn test_resource_config_merge() {
413 let base = ResourceConfig {
414 service_name: Some("base".to_string()),
415 service_version: Some("1.0.0".to_string()),
416 attributes: [("key1".to_string(), "value1".to_string())]
417 .into_iter()
418 .collect(),
419 ..Default::default()
420 };
421
422 let override_config = ResourceConfig {
423 service_name: Some("override".to_string()),
424 attributes: [("key2".to_string(), "value2".to_string())]
425 .into_iter()
426 .collect(),
427 ..Default::default()
428 };
429
430 let merged = base.merge(override_config);
431 assert_eq!(merged.service_name, Some("override".to_string()));
432 assert_eq!(merged.service_version, Some("1.0.0".to_string()));
433 assert_eq!(merged.attributes.get("key1"), Some(&"value1".to_string()));
434 assert_eq!(merged.attributes.get("key2"), Some(&"value2".to_string()));
435 }
436
437 #[test]
438 fn test_signal_config_default() {
439 let config = SignalConfig::default();
440 assert!(!config.enabled);
441
442 let config = SignalConfig::default_enabled();
443 assert!(config.enabled);
444 }
445
446 #[test]
447 fn test_batch_config_defaults() {
448 let config = BatchConfig::default();
449 assert_eq!(config.max_queue_size, 2048);
450 assert_eq!(config.max_export_batch_size, 512);
451 assert_eq!(config.scheduled_delay, Duration::from_secs(5));
452 assert_eq!(config.export_timeout, Duration::from_secs(30));
453 }
454
455 #[test]
456 fn test_endpoint_config_merge() {
457 let base = EndpointConfig {
458 url: Some("http://base:4318".to_string()),
459 headers: [("auth".to_string(), "token1".to_string())]
460 .into_iter()
461 .collect(),
462 ..Default::default()
463 };
464
465 let override_config = EndpointConfig {
466 url: Some("http://override:4318".to_string()),
467 headers: [("x-custom".to_string(), "value".to_string())]
468 .into_iter()
469 .collect(),
470 ..Default::default()
471 };
472
473 let merged = base.merge(override_config);
474 assert_eq!(merged.url, Some("http://override:4318".to_string()));
475 assert_eq!(merged.headers.get("auth"), Some(&"token1".to_string()));
476 assert_eq!(merged.headers.get("x-custom"), Some(&"value".to_string()));
477 }
478}