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 Protocol {
15 Grpc,
17 #[default]
19 #[serde(alias = "http_binary", alias = "http-binary")]
20 HttpBinary,
21 #[serde(alias = "http_json", alias = "http-json")]
23 HttpJson,
24}
25
26impl Protocol {
27 pub fn default_endpoint(&self) -> &'static str {
29 match self {
30 Protocol::Grpc => "http://localhost:4317",
31 Protocol::HttpBinary | Protocol::HttpJson => "http://localhost:4318",
32 }
33 }
34
35 pub fn default_port(&self) -> u16 {
37 match self {
38 Protocol::Grpc => 4317,
39 Protocol::HttpBinary | Protocol::HttpJson => 4318,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(default)]
47pub struct OtelSdkConfig {
48 pub endpoint: EndpointConfig,
50
51 pub resource: ResourceConfig,
53
54 pub traces: SignalConfig,
56
57 pub metrics: SignalConfig,
59
60 pub logs: SignalConfig,
62
63 pub init_tracing_subscriber: bool,
65}
66
67impl Default for OtelSdkConfig {
68 fn default() -> Self {
69 Self {
70 endpoint: EndpointConfig::default(),
71 resource: ResourceConfig::default(),
72 traces: SignalConfig::default_enabled(),
73 metrics: SignalConfig::default_enabled(),
74 logs: SignalConfig::default_enabled(),
75 init_tracing_subscriber: true,
76 }
77 }
78}
79
80impl OtelSdkConfig {
81 pub fn effective_endpoint(&self) -> String {
83 self.endpoint
84 .url
85 .clone()
86 .unwrap_or_else(|| self.endpoint.protocol.default_endpoint().to_string())
87 }
88
89 pub fn signal_endpoint(&self, signal_path: &str) -> String {
91 let base = self.effective_endpoint();
92 let base = base.trim_end_matches('/');
93
94 match self.endpoint.protocol {
95 Protocol::Grpc => base.to_string(),
96 Protocol::HttpBinary | Protocol::HttpJson => {
97 format!("{}{}", base, signal_path)
98 }
99 }
100 }
101
102 pub fn merge(mut self, other: Self) -> Self {
104 self.endpoint = self.endpoint.merge(other.endpoint);
105 self.resource = self.resource.merge(other.resource);
106 self.traces = self.traces.merge(other.traces);
107 self.metrics = self.metrics.merge(other.metrics);
108 self.logs = self.logs.merge(other.logs);
109
110 if other.init_tracing_subscriber != Self::default().init_tracing_subscriber {
111 self.init_tracing_subscriber = other.init_tracing_subscriber;
112 }
113
114 self
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(default)]
121pub struct EndpointConfig {
122 pub url: Option<String>,
128
129 pub protocol: Protocol,
131
132 #[serde(with = "humantime_serde")]
134 pub timeout: Duration,
135
136 #[serde(default)]
138 pub headers: HashMap<String, String>,
139}
140
141impl Default for EndpointConfig {
142 fn default() -> Self {
143 Self {
144 url: None,
145 protocol: Protocol::default(),
146 timeout: Duration::from_secs(10),
147 headers: HashMap::new(),
148 }
149 }
150}
151
152impl EndpointConfig {
153 pub fn merge(mut self, other: Self) -> Self {
155 if other.url.is_some() {
156 self.url = other.url;
157 }
158 if other.protocol != Protocol::default() {
159 self.protocol = other.protocol;
160 }
161 if other.timeout != Self::default().timeout {
162 self.timeout = other.timeout;
163 }
164 self.headers.extend(other.headers);
165 self
166 }
167}
168
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
171#[serde(default)]
172pub struct ResourceConfig {
173 pub service_name: Option<String>,
177
178 pub service_version: Option<String>,
180
181 pub deployment_environment: Option<String>,
183
184 #[serde(default)]
186 pub attributes: HashMap<String, String>,
187
188 #[serde(default = "default_true")]
190 pub detect_lambda: bool,
191}
192
193fn default_true() -> bool {
194 true
195}
196
197impl ResourceConfig {
198 pub fn with_service_name(name: impl Into<String>) -> Self {
200 Self {
201 service_name: Some(name.into()),
202 ..Default::default()
203 }
204 }
205
206 pub fn merge(mut self, other: Self) -> Self {
208 if other.service_name.is_some() {
209 self.service_name = other.service_name;
210 }
211 if other.service_version.is_some() {
212 self.service_version = other.service_version;
213 }
214 if other.deployment_environment.is_some() {
215 self.deployment_environment = other.deployment_environment;
216 }
217 self.attributes.extend(other.attributes);
218 if !other.detect_lambda {
219 self.detect_lambda = false;
220 }
221 self
222 }
223
224 pub fn detect_from_environment(&mut self) {
226 if !self.detect_lambda {
227 return;
228 }
229
230 if self.service_name.is_none() {
231 self.service_name = std::env::var("AWS_LAMBDA_FUNCTION_NAME").ok();
232 }
233
234 if self.service_version.is_none() {
235 self.service_version = std::env::var("AWS_LAMBDA_FUNCTION_VERSION").ok();
236 }
237
238 if let Ok(region) = std::env::var("AWS_REGION") {
239 self.attributes
240 .entry("cloud.region".to_string())
241 .or_insert(region);
242 }
243
244 if let Ok(memory) = std::env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") {
245 self.attributes
246 .entry("faas.max_memory".to_string())
247 .or_insert(memory);
248 }
249
250 self.attributes
251 .entry("cloud.provider".to_string())
252 .or_insert_with(|| "aws".to_string());
253
254 self.attributes
255 .entry("faas.instance".to_string())
256 .or_insert_with(|| std::env::var("AWS_LAMBDA_LOG_STREAM_NAME").unwrap_or_default());
257 }
258}
259
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
262#[serde(default)]
263pub struct SignalConfig {
264 pub enabled: bool,
266
267 pub batch: BatchConfig,
269}
270
271impl SignalConfig {
272 pub fn default_enabled() -> Self {
274 Self {
275 enabled: true,
276 batch: BatchConfig::default(),
277 }
278 }
279
280 pub fn merge(mut self, other: Self) -> Self {
282 self.enabled = other.enabled;
283 self.batch = self.batch.merge(other.batch);
284 self
285 }
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(default)]
291pub struct BatchConfig {
292 pub max_queue_size: usize,
294
295 pub max_export_batch_size: usize,
297
298 #[serde(with = "humantime_serde")]
300 pub scheduled_delay: Duration,
301
302 #[serde(with = "humantime_serde")]
304 pub export_timeout: Duration,
305}
306
307impl Default for BatchConfig {
308 fn default() -> Self {
309 Self {
310 max_queue_size: 2048,
311 max_export_batch_size: 512,
312 scheduled_delay: Duration::from_secs(5),
313 export_timeout: Duration::from_secs(30),
314 }
315 }
316}
317
318impl BatchConfig {
319 pub fn merge(mut self, other: Self) -> Self {
321 let default = Self::default();
322 if other.max_queue_size != default.max_queue_size {
323 self.max_queue_size = other.max_queue_size;
324 }
325 if other.max_export_batch_size != default.max_export_batch_size {
326 self.max_export_batch_size = other.max_export_batch_size;
327 }
328 if other.scheduled_delay != default.scheduled_delay {
329 self.scheduled_delay = other.scheduled_delay;
330 }
331 if other.export_timeout != default.export_timeout {
332 self.export_timeout = other.export_timeout;
333 }
334 self
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
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}