1use std::time::Duration;
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum OtlpProtocol {
35 #[default]
37 Grpc,
38 Http,
40}
41
42impl OtlpProtocol {
43 pub const fn as_str(&self) -> &'static str {
45 match self {
46 Self::Grpc => "grpc",
47 Self::Http => "http",
48 }
49 }
50}
51
52impl std::str::FromStr for OtlpProtocol {
53 type Err = String;
54
55 fn from_str(s: &str) -> Result<Self, Self::Err> {
56 match s.to_lowercase().as_str() {
57 "grpc" => Ok(Self::Grpc),
58 "http" | "http/protobuf" => Ok(Self::Http),
59 other => Err(format!("unknown OTLP protocol: {other}")),
60 }
61 }
62}
63
64impl std::fmt::Display for OtlpProtocol {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str(self.as_str())
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default)]
72pub enum SamplingStrategy {
73 #[default]
75 AlwaysOn,
76 AlwaysOff,
78 TraceIdRatio(f64),
80 ParentBased,
82}
83
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
86pub enum LogLevel {
87 Trace,
88 Debug,
89 #[default]
90 Info,
91 Warn,
92 Error,
93}
94
95impl LogLevel {
96 pub const fn as_str(&self) -> &'static str {
98 match self {
99 Self::Trace => "trace",
100 Self::Debug => "debug",
101 Self::Info => "info",
102 Self::Warn => "warn",
103 Self::Error => "error",
104 }
105 }
106}
107
108impl std::str::FromStr for LogLevel {
109 type Err = String;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 match s.to_lowercase().as_str() {
113 "trace" => Ok(Self::Trace),
114 "debug" => Ok(Self::Debug),
115 "info" => Ok(Self::Info),
116 "warn" | "warning" => Ok(Self::Warn),
117 "error" => Ok(Self::Error),
118 other => Err(format!("unknown log level: {other}")),
119 }
120 }
121}
122
123impl std::fmt::Display for LogLevel {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.write_str(self.as_str())
126 }
127}
128
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub enum OutputFormat {
132 #[default]
134 Pretty,
135 Compact,
137 Json,
139}
140
141#[derive(Debug, Clone, Default)]
143pub enum OtlpCredentials {
144 #[default]
146 None,
147 Bearer(String),
149 Basic {
151 username: String,
153 password: String,
155 },
156 Headers(std::collections::HashMap<String, String>),
158}
159
160#[derive(Debug, Clone)]
164pub struct ExporterConfig {
165 pub(crate) endpoint: String,
166 pub(crate) protocol: OtlpProtocol,
167 pub(crate) timeout: Duration,
168 pub(crate) credentials: OtlpCredentials,
169}
170
171impl Default for ExporterConfig {
172 fn default() -> Self {
173 Self {
174 endpoint: "http://localhost:4317".to_string(),
175 protocol: OtlpProtocol::default(),
176 timeout: Duration::from_secs(10),
177 credentials: OtlpCredentials::None,
178 }
179 }
180}
181
182impl ExporterConfig {
183 #[must_use]
185 pub fn builder() -> ExporterConfigBuilder {
186 ExporterConfigBuilder::new()
187 }
188}
189
190#[derive(Debug, Clone)]
192pub struct ExporterConfigBuilder {
193 config: ExporterConfig,
194}
195
196impl Default for ExporterConfigBuilder {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl ExporterConfigBuilder {
203 #[must_use]
205 pub fn new() -> Self {
206 Self {
207 config: ExporterConfig::default(),
208 }
209 }
210
211 #[must_use]
213 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
214 self.config.endpoint = endpoint.into();
215 self
216 }
217
218 #[must_use]
220 pub const fn protocol(mut self, protocol: OtlpProtocol) -> Self {
221 self.config.protocol = protocol;
222 self
223 }
224
225 #[must_use]
227 pub const fn timeout(mut self, timeout: Duration) -> Self {
228 self.config.timeout = timeout;
229 self
230 }
231
232 #[must_use]
234 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
235 self.config.credentials = OtlpCredentials::Bearer(token.into());
236 self
237 }
238
239 #[must_use]
241 pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
242 self.config.credentials = OtlpCredentials::Basic {
243 username: username.into(),
244 password: password.into(),
245 };
246 self
247 }
248
249 #[must_use]
251 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
252 match &mut self.config.credentials {
253 OtlpCredentials::Headers(h) => {
254 h.insert(key.into(), value.into());
255 }
256 _ => {
257 let mut h = std::collections::HashMap::new();
258 h.insert(key.into(), value.into());
259 self.config.credentials = OtlpCredentials::Headers(h);
260 }
261 }
262 self
263 }
264
265 #[must_use]
267 pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
268 self.config.credentials = OtlpCredentials::Headers(headers);
269 self
270 }
271
272 #[must_use]
274 pub fn build(self) -> ExporterConfig {
275 self.config
276 }
277}
278
279#[derive(Debug, Clone)]
281pub struct TracingConfig {
282 pub(crate) sampling: SamplingStrategy,
283 pub(crate) record_exceptions: bool,
284 pub(crate) exception_field_limit: usize,
285 pub(crate) batch_schedule_delay: Duration,
286 pub(crate) max_export_batch_size: usize,
287 pub(crate) max_queue_size: usize,
288}
289
290impl Default for TracingConfig {
291 fn default() -> Self {
292 Self {
293 sampling: SamplingStrategy::default(),
294 record_exceptions: true,
295 exception_field_limit: 1024,
296 batch_schedule_delay: Duration::from_secs(5),
297 max_export_batch_size: 512,
298 max_queue_size: 2048,
299 }
300 }
301}
302
303impl TracingConfig {
304 #[must_use]
306 pub fn builder() -> TracingConfigBuilder {
307 TracingConfigBuilder::new()
308 }
309}
310
311#[derive(Debug, Clone)]
313pub struct TracingConfigBuilder {
314 config: TracingConfig,
315}
316
317impl Default for TracingConfigBuilder {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323impl TracingConfigBuilder {
324 #[must_use]
326 pub fn new() -> Self {
327 Self {
328 config: TracingConfig::default(),
329 }
330 }
331
332 #[must_use]
334 pub const fn sampling(mut self, strategy: SamplingStrategy) -> Self {
335 self.config.sampling = strategy;
336 self
337 }
338
339 #[must_use]
341 pub const fn record_exceptions(mut self, enabled: bool) -> Self {
342 self.config.record_exceptions = enabled;
343 self
344 }
345
346 #[must_use]
348 pub const fn exception_field_limit(mut self, limit: usize) -> Self {
349 self.config.exception_field_limit = limit;
350 self
351 }
352
353 #[must_use]
355 pub const fn batch_schedule_delay(mut self, delay: Duration) -> Self {
356 self.config.batch_schedule_delay = delay;
357 self
358 }
359
360 #[must_use]
362 pub const fn max_export_batch_size(mut self, size: usize) -> Self {
363 self.config.max_export_batch_size = size;
364 self
365 }
366
367 #[must_use]
369 pub const fn max_queue_size(mut self, size: usize) -> Self {
370 self.config.max_queue_size = size;
371 self
372 }
373
374 #[must_use]
376 pub fn build(self) -> TracingConfig {
377 self.config
378 }
379}
380
381#[derive(Debug, Clone)]
383pub struct MetricsConfig {
384 pub(crate) export_interval: Duration,
385}
386
387impl Default for MetricsConfig {
388 fn default() -> Self {
389 Self {
390 export_interval: Duration::from_secs(60),
391 }
392 }
393}
394
395impl MetricsConfig {
396 #[must_use]
398 pub fn builder() -> MetricsConfigBuilder {
399 MetricsConfigBuilder::new()
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct MetricsConfigBuilder {
406 config: MetricsConfig,
407}
408
409impl Default for MetricsConfigBuilder {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415impl MetricsConfigBuilder {
416 #[must_use]
418 pub fn new() -> Self {
419 Self {
420 config: MetricsConfig::default(),
421 }
422 }
423
424 #[must_use]
426 pub const fn export_interval(mut self, interval: Duration) -> Self {
427 self.config.export_interval = interval;
428 self
429 }
430
431 #[must_use]
433 pub fn build(self) -> MetricsConfig {
434 self.config
435 }
436}
437
438#[derive(Debug, Clone)]
444pub struct OtelConfig {
445 pub(crate) service_name: String,
447 pub(crate) service_version: String,
448 pub(crate) environment: String,
449 pub(crate) service_namespace: Option<String>,
450 pub(crate) service_instance_id: Option<String>,
451
452 pub(crate) exporter: ExporterConfig,
454
455 pub(crate) tracing: Option<TracingConfig>,
458 pub(crate) logging: bool,
460 pub(crate) metrics: Option<MetricsConfig>,
462
463 pub(crate) enable_console_output: bool,
465 pub(crate) log_level: LogLevel,
466 pub(crate) output_format: OutputFormat,
467
468 pub(crate) allowed_crates: Vec<String>,
470 pub(crate) custom_filters: Vec<String>,
471
472 pub(crate) custom_attributes: Vec<(String, String)>,
474}
475
476impl Default for OtelConfig {
477 fn default() -> Self {
478 Self {
479 service_name: "unknown-service".to_string(),
480 service_version: "0.0.0".to_string(),
481 environment: "development".to_string(),
482 service_namespace: None,
483 service_instance_id: None,
484 exporter: ExporterConfig::default(),
485 tracing: Some(TracingConfig::default()),
486 logging: true,
487 metrics: Some(MetricsConfig::default()),
488 enable_console_output: true,
489 log_level: LogLevel::Info,
490 output_format: OutputFormat::Pretty,
491 allowed_crates: Vec::new(),
492 custom_filters: Vec::new(),
493 custom_attributes: Vec::new(),
494 }
495 }
496}
497
498impl OtelConfig {
499 #[must_use]
501 pub fn builder() -> OtelConfigBuilder {
502 OtelConfigBuilder::new()
503 }
504}
505
506#[derive(Debug, Clone)]
525pub struct OtelConfigBuilder {
526 service_name: Option<String>,
527 service_version: Option<String>,
528 environment: Option<String>,
529 service_namespace: Option<String>,
530 service_instance_id: Option<String>,
531 exporter: Option<ExporterConfig>,
532 tracing: Option<Option<TracingConfig>>,
535 logging: Option<bool>,
536 metrics: Option<Option<MetricsConfig>>,
537 enable_console_output: Option<bool>,
538 log_level: Option<LogLevel>,
539 output_format: Option<OutputFormat>,
540 allowed_crates: Vec<String>,
541 custom_filters: Vec<String>,
542 custom_attributes: Vec<(String, String)>,
543}
544
545impl Default for OtelConfigBuilder {
546 fn default() -> Self {
547 Self::new()
548 }
549}
550
551impl OtelConfigBuilder {
552 #[must_use]
555 pub fn new() -> Self {
556 Self {
557 service_name: None,
558 service_version: None,
559 environment: None,
560 service_namespace: None,
561 service_instance_id: None,
562 exporter: None,
563 tracing: None,
564 logging: None,
565 metrics: None,
566 enable_console_output: None,
567 log_level: None,
568 output_format: None,
569 allowed_crates: Vec::new(),
570 custom_filters: Vec::new(),
571 custom_attributes: Vec::new(),
572 }
573 }
574
575 #[must_use]
579 pub fn service_name(mut self, name: impl Into<String>) -> Self {
580 self.service_name = Some(name.into());
581 self
582 }
583
584 #[must_use]
586 pub fn service_version(mut self, version: impl Into<String>) -> Self {
587 self.service_version = Some(version.into());
588 self
589 }
590
591 #[must_use]
593 pub fn environment(mut self, env: impl Into<String>) -> Self {
594 self.environment = Some(env.into());
595 self
596 }
597
598 #[must_use]
600 pub fn namespace(mut self, ns: impl Into<String>) -> Self {
601 self.service_namespace = Some(ns.into());
602 self
603 }
604
605 #[must_use]
607 pub fn instance_id(mut self, id: impl Into<String>) -> Self {
608 self.service_instance_id = Some(id.into());
609 self
610 }
611
612 #[must_use]
617 pub fn exporter(mut self, config: ExporterConfig) -> Self {
618 self.exporter = Some(config);
619 self
620 }
621
622 #[must_use]
624 pub fn tracing(mut self, config: TracingConfig) -> Self {
625 self.tracing = Some(Some(config));
626 self
627 }
628
629 #[must_use]
631 pub fn disable_tracing(mut self) -> Self {
632 self.tracing = Some(None);
633 self
634 }
635
636 #[must_use]
638 pub fn logging(mut self, enabled: bool) -> Self {
639 self.logging = Some(enabled);
640 self
641 }
642
643 #[must_use]
645 pub fn metrics(mut self, config: MetricsConfig) -> Self {
646 self.metrics = Some(Some(config));
647 self
648 }
649
650 #[must_use]
652 pub fn disable_metrics(mut self) -> Self {
653 self.metrics = Some(None);
654 self
655 }
656
657 #[must_use]
661 pub fn console_output(mut self, enabled: bool) -> Self {
662 self.enable_console_output = Some(enabled);
663 self
664 }
665
666 #[must_use]
668 pub fn log_level(mut self, level: LogLevel) -> Self {
669 self.log_level = Some(level);
670 self
671 }
672
673 #[must_use]
675 pub fn output_format(mut self, format: OutputFormat) -> Self {
676 self.output_format = Some(format);
677 self
678 }
679
680 #[must_use]
685 pub fn allow_crate(mut self, name: impl Into<String>) -> Self {
686 self.allowed_crates.push(name.into());
687 self
688 }
689
690 #[must_use]
692 pub fn allow_crates(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
693 for name in names {
694 self.allowed_crates.push(name.into());
695 }
696 self
697 }
698
699 #[must_use]
701 pub fn custom_filter(mut self, directive: impl Into<String>) -> Self {
702 self.custom_filters.push(directive.into());
703 self
704 }
705
706 #[must_use]
708 pub fn attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
709 self.custom_attributes.push((key.into(), value.into()));
710 self
711 }
712
713 #[must_use]
719 pub fn build(self) -> OtelConfig {
720 let env = crate::env::read_env();
721
722 let exporter = self.exporter.unwrap_or_else(|| {
724 let mut exp = ExporterConfig::default();
725 if let Some(endpoint) = env.endpoint {
726 exp.endpoint = endpoint;
727 }
728 if let Some(protocol) = env.protocol {
729 exp.protocol = protocol;
730 }
731 if let Some(timeout) = env.timeout {
732 exp.timeout = timeout;
733 }
734 if let Some(headers) = env.headers {
735 exp.credentials = OtlpCredentials::Headers(headers);
736 }
737 exp
738 });
739
740 let tracing = match self.tracing {
742 Some(t) => t,
743 None => {
744 let mut tc = TracingConfig::default();
745 if let Some(sampler) = env.sampler {
746 tc.sampling = sampler;
747 }
748 Some(tc)
749 }
750 };
751
752 let metrics = match self.metrics {
754 Some(m) => m,
755 None => Some(MetricsConfig::default()),
756 };
757
758 OtelConfig {
759 service_name: self
760 .service_name
761 .or(env.service_name)
762 .unwrap_or_else(|| "unknown-service".to_string()),
763 service_version: self.service_version.unwrap_or_else(|| "0.0.0".to_string()),
764 environment: self
765 .environment
766 .unwrap_or_else(|| "development".to_string()),
767 service_namespace: self.service_namespace,
768 service_instance_id: self.service_instance_id,
769 exporter,
770 tracing,
771 logging: self.logging.unwrap_or(true),
772 metrics,
773 enable_console_output: self.enable_console_output.unwrap_or(true),
774 log_level: self.log_level.unwrap_or_default(),
775 output_format: self.output_format.unwrap_or_default(),
776 allowed_crates: self.allowed_crates,
777 custom_filters: self.custom_filters,
778 custom_attributes: self.custom_attributes,
779 }
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn builder_defaults() {
789 let config = OtelConfig::builder().build();
790 assert_eq!(config.service_name, "unknown-service");
791 assert!(config.tracing.is_some());
792 assert!(config.logging);
793 assert!(config.metrics.is_some());
794 assert!(config.enable_console_output);
795 }
796
797 #[test]
798 fn builder_with_service_info() {
799 let config = OtelConfig::builder()
800 .service_name("test-svc")
801 .service_version("1.0.0")
802 .environment("test")
803 .build();
804
805 assert_eq!(config.service_name, "test-svc");
806 assert_eq!(config.service_version, "1.0.0");
807 assert_eq!(config.environment, "test");
808 }
809
810 #[test]
811 fn builder_with_exporter() {
812 let config = OtelConfig::builder()
813 .exporter(
814 ExporterConfig::builder()
815 .endpoint("https://otel.example.com:4317")
816 .bearer_token("my-token")
817 .build(),
818 )
819 .build();
820
821 assert_eq!(config.exporter.endpoint, "https://otel.example.com:4317");
822 assert!(matches!(
823 config.exporter.credentials,
824 OtlpCredentials::Bearer(_)
825 ));
826 }
827
828 #[test]
829 fn builder_disable_tracing() {
830 let config = OtelConfig::builder().disable_tracing().build();
831 assert!(config.tracing.is_none());
832 }
833
834 #[test]
835 fn builder_disable_metrics() {
836 let config = OtelConfig::builder().disable_metrics().build();
837 assert!(config.metrics.is_none());
838 }
839
840 #[test]
841 fn exporter_header_accumulation() {
842 let exp = ExporterConfig::builder()
843 .header("x-api-key", "abc")
844 .header("x-team", "eng")
845 .build();
846
847 match exp.credentials {
848 OtlpCredentials::Headers(h) => {
849 assert_eq!(h.get("x-api-key").unwrap(), "abc");
850 assert_eq!(h.get("x-team").unwrap(), "eng");
851 }
852 _ => panic!("expected Headers"),
853 }
854 }
855
856 #[test]
857 fn protocol_parsing() {
858 assert_eq!("grpc".parse::<OtlpProtocol>().unwrap(), OtlpProtocol::Grpc);
859 assert_eq!("http".parse::<OtlpProtocol>().unwrap(), OtlpProtocol::Http);
860 assert_eq!(
861 "http/protobuf".parse::<OtlpProtocol>().unwrap(),
862 OtlpProtocol::Http
863 );
864 assert!("invalid".parse::<OtlpProtocol>().is_err());
865 }
866
867 #[test]
868 fn log_level_parsing() {
869 assert_eq!("info".parse::<LogLevel>().unwrap(), LogLevel::Info);
870 assert_eq!("WARNING".parse::<LogLevel>().unwrap(), LogLevel::Warn);
871 assert!("invalid".parse::<LogLevel>().is_err());
872 }
873
874 #[test]
875 fn log_level_ordering() {
876 assert!(LogLevel::Trace < LogLevel::Debug);
877 assert!(LogLevel::Debug < LogLevel::Info);
878 assert!(LogLevel::Info < LogLevel::Warn);
879 assert!(LogLevel::Warn < LogLevel::Error);
880 }
881}