1use std::collections::HashMap;
16use std::fmt;
17
18use serde::Deserialize;
19use serde::Serialize;
20
21#[derive(Clone, Serialize, Deserialize)]
22#[serde(default)]
23pub struct ObservabilityConfig {
24 pub enabled: bool,
25 pub service_name: String,
26 pub service_namespace: String,
27 pub service_instance_id: String,
28 pub service_version: String,
29 pub environment: String,
30 pub cluster: String,
31 pub node_type: String,
32 pub node_id: String,
33 pub metrics: MetricsConfig,
34 pub traces: TracesConfig,
35 pub logs: LogsConfig,
36 pub otlp: OtlpConfig,
37 pub prometheus: PrometheusConfig,
38 pub subscriber_install_policy: SubscriberInstallPolicy,
39 pub resource_attributes: HashMap<String, String>,
40}
41
42impl fmt::Debug for ObservabilityConfig {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter
45 .debug_struct("ObservabilityConfig")
46 .field("enabled", &self.enabled)
47 .field("service_name", &self.service_name)
48 .field("service_namespace", &self.service_namespace)
49 .field("service_instance_id_configured", &!self.service_instance_id.is_empty())
50 .field("service_version", &self.service_version)
51 .field("environment", &self.environment)
52 .field("cluster", &self.cluster)
53 .field("node_type", &self.node_type)
54 .field("node_id", &self.node_id)
55 .field("metrics", &self.metrics)
56 .field("traces", &self.traces)
57 .field("logs", &self.logs)
58 .field("otlp", &self.otlp)
59 .field("prometheus", &self.prometheus)
60 .field("subscriber_install_policy", &self.subscriber_install_policy)
61 .field("resource_attributes_count", &self.resource_attributes.len())
62 .finish()
63 }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67#[serde(default)]
68pub struct TelemetryBootstrapConfig {
69 pub observability: ObservabilityConfig,
70 pub logging: LoggingConfig,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(default)]
75pub struct LoggingConfig {
76 pub enabled: bool,
77 pub filter: String,
78 pub console: ConsoleLogConfig,
79 pub file: FileLogConfig,
80 pub reload: ReloadConfig,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(default)]
85pub struct ConsoleLogConfig {
86 pub enabled: bool,
87 pub format: LogFormat,
88 pub ansi: bool,
89 pub target: bool,
90 pub thread_ids: bool,
91 pub thread_names: bool,
92 pub line_number: bool,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(default)]
97pub struct FileLogConfig {
98 pub enabled: bool,
99 pub directory: String,
100 pub file_name_prefix: String,
101 pub rotation: LogRotation,
102 pub format: LogFormat,
103 pub non_blocking: NonBlockingLogConfig,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(default)]
108pub struct NonBlockingLogConfig {
109 pub buffered_lines_limit: usize,
110 pub lossy: bool,
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(default)]
115pub struct ReloadConfig {
116 pub enabled: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(default)]
121pub struct MetricsConfig {
122 pub enabled: bool,
123 pub exporter: MetricsExporter,
124 pub export_interval_millis: u64,
125 pub export_timeout_millis: u64,
126 pub cardinality_limit: usize,
127 pub sample_ratio: f64,
128 pub topic_label_enabled: bool,
129 pub consumer_group_label_enabled: bool,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(default)]
134pub struct TracesConfig {
135 pub enabled: bool,
136 pub exporter: TraceExporter,
137 pub sample_ratio: f64,
138 pub propagate_context: bool,
139 pub record_message_id: bool,
140 pub record_message_keys: bool,
141 pub record_body_size: bool,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(default)]
146pub struct LogsConfig {
147 pub enabled: bool,
148 pub exporter: LogsExporter,
149}
150
151#[derive(Clone, Serialize, Deserialize)]
152#[serde(default)]
153pub struct OtlpConfig {
154 pub endpoint: String,
155 pub protocol: OtlpProtocol,
156 pub headers: HashMap<String, String>,
157 pub timeout_millis: u64,
158}
159
160impl fmt::Debug for OtlpConfig {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 formatter
163 .debug_struct("OtlpConfig")
164 .field("endpoint_configured", &!self.endpoint.is_empty())
165 .field("protocol", &self.protocol)
166 .field("headers_count", &self.headers.len())
167 .field("timeout_millis", &self.timeout_millis)
168 .finish()
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(default)]
174pub struct PrometheusConfig {
175 pub host: String,
176 pub port: u16,
177 pub path: String,
178}
179
180#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
181#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
182pub struct ObservabilityOverrides {
183 pub environment: Option<String>,
184 pub service_instance_id: Option<String>,
185 pub resource_attributes: Option<HashMap<String, String>>,
186 pub metrics: MetricsOverrides,
187 pub traces: TracesOverrides,
188 pub logs: LogsOverrides,
189 pub otlp: OtlpOverrides,
190 pub prometheus: PrometheusOverrides,
191}
192
193impl fmt::Debug for ObservabilityOverrides {
194 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
195 formatter
196 .debug_struct("ObservabilityOverrides")
197 .field("environment_configured", &self.environment.is_some())
198 .field("service_instance_id_configured", &self.service_instance_id.is_some())
199 .field(
200 "resource_attributes_count",
201 &self.resource_attributes.as_ref().map(HashMap::len),
202 )
203 .field("metrics", &self.metrics)
204 .field("traces", &self.traces)
205 .field("logs", &self.logs)
206 .field("otlp", &self.otlp)
207 .field("prometheus", &self.prometheus)
208 .finish()
209 }
210}
211
212#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
213#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
214pub struct MetricsOverrides {
215 pub exporter: Option<MetricsExporter>,
216 pub export_interval_millis: Option<u64>,
217 pub export_timeout_millis: Option<u64>,
218 pub cardinality_limit: Option<usize>,
219 pub sample_ratio: Option<f64>,
220 pub topic_label_enabled: Option<bool>,
221 pub consumer_group_label_enabled: Option<bool>,
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
225#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
226pub struct TracesOverrides {
227 pub exporter: Option<TraceExporter>,
228 pub sample_ratio: Option<f64>,
229 pub propagate_context: Option<bool>,
230 pub record_message_id: Option<bool>,
231 pub record_message_keys: Option<bool>,
232 pub record_body_size: Option<bool>,
233}
234
235#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
236#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
237pub struct LogsOverrides {
238 pub exporter: Option<LogsExporter>,
239}
240
241#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
242#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
243pub struct OtlpOverrides {
244 pub endpoint: Option<String>,
245 pub protocol: Option<OtlpProtocol>,
246 pub headers: Option<HashMap<String, String>>,
247 pub timeout_millis: Option<u64>,
248}
249
250impl fmt::Debug for OtlpOverrides {
251 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252 formatter
253 .debug_struct("OtlpOverrides")
254 .field("endpoint_configured", &self.endpoint.is_some())
255 .field("protocol", &self.protocol)
256 .field("headers_count", &self.headers.as_ref().map(HashMap::len))
257 .field("timeout_millis", &self.timeout_millis)
258 .finish()
259 }
260}
261
262#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
263#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
264pub struct PrometheusOverrides {
265 pub host: Option<String>,
266 pub port: Option<u16>,
267 pub path: Option<String>,
268}
269
270impl ObservabilityOverrides {
271 pub fn apply_to(&self, target: &mut ObservabilityConfig) {
277 if let Some(environment) = &self.environment {
278 target.environment = environment.clone();
279 }
280 if let Some(service_instance_id) = &self.service_instance_id {
281 target.service_instance_id = service_instance_id.clone();
282 }
283 if let Some(resource_attributes) = &self.resource_attributes {
284 target.resource_attributes = resource_attributes.clone();
285 }
286
287 if let Some(exporter) = self.metrics.exporter {
288 target.metrics.exporter = exporter;
289 target.metrics.enabled = exporter.is_enabled();
290 }
291 if let Some(export_interval_millis) = self.metrics.export_interval_millis {
292 target.metrics.export_interval_millis = export_interval_millis;
293 }
294 if let Some(export_timeout_millis) = self.metrics.export_timeout_millis {
295 target.metrics.export_timeout_millis = export_timeout_millis;
296 }
297 if let Some(cardinality_limit) = self.metrics.cardinality_limit {
298 target.metrics.cardinality_limit = cardinality_limit;
299 }
300 if let Some(sample_ratio) = self.metrics.sample_ratio {
301 target.metrics.sample_ratio = sample_ratio;
302 }
303 if let Some(topic_label_enabled) = self.metrics.topic_label_enabled {
304 target.metrics.topic_label_enabled = topic_label_enabled;
305 }
306 if let Some(consumer_group_label_enabled) = self.metrics.consumer_group_label_enabled {
307 target.metrics.consumer_group_label_enabled = consumer_group_label_enabled;
308 }
309
310 if let Some(exporter) = self.traces.exporter {
311 target.traces.exporter = exporter;
312 target.traces.enabled = !matches!(exporter, TraceExporter::Disable);
313 }
314 if let Some(sample_ratio) = self.traces.sample_ratio {
315 target.traces.sample_ratio = sample_ratio;
316 }
317 if let Some(propagate_context) = self.traces.propagate_context {
318 target.traces.propagate_context = propagate_context;
319 }
320 if let Some(record_message_id) = self.traces.record_message_id {
321 target.traces.record_message_id = record_message_id;
322 }
323 if let Some(record_message_keys) = self.traces.record_message_keys {
324 target.traces.record_message_keys = record_message_keys;
325 }
326 if let Some(record_body_size) = self.traces.record_body_size {
327 target.traces.record_body_size = record_body_size;
328 }
329
330 if let Some(exporter) = self.logs.exporter {
331 target.logs.exporter = exporter;
332 target.logs.enabled = !matches!(exporter, LogsExporter::Disable);
333 }
334
335 if let Some(endpoint) = &self.otlp.endpoint {
336 target.otlp.endpoint = endpoint.clone();
337 }
338 if let Some(protocol) = self.otlp.protocol {
339 target.otlp.protocol = protocol;
340 }
341 if let Some(headers) = &self.otlp.headers {
342 target.otlp.headers = headers.clone();
343 }
344 if let Some(timeout_millis) = self.otlp.timeout_millis {
345 target.otlp.timeout_millis = timeout_millis;
346 }
347
348 if let Some(host) = &self.prometheus.host {
349 target.prometheus.host = host.clone();
350 }
351 if let Some(port) = self.prometheus.port {
352 target.prometheus.port = port;
353 }
354 if let Some(path) = &self.prometheus.path {
355 target.prometheus.path = path.clone();
356 }
357
358 target.enabled = target.metrics.enabled || target.traces.enabled || target.logs.enabled;
359 }
360}
361
362#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
363#[serde(rename_all = "snake_case")]
364pub enum MetricsExporter {
365 Disable,
366 OtlpGrpc,
367 Prometheus,
368 Log,
369}
370
371impl MetricsExporter {
372 pub const fn is_enabled(self) -> bool {
373 !matches!(self, Self::Disable)
374 }
375}
376
377#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
378#[serde(rename_all = "snake_case")]
379pub enum TraceExporter {
380 Disable,
381 OtlpGrpc,
382 Log,
383}
384
385#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
386#[serde(rename_all = "snake_case")]
387pub enum LogsExporter {
388 Disable,
389 OtlpGrpc,
390 Log,
391}
392
393#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
394#[serde(rename_all = "snake_case")]
395pub enum OtlpProtocol {
396 Grpc,
397 HttpBinary,
398 HttpJson,
399}
400
401#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
402#[serde(rename_all = "snake_case")]
403pub enum LogFormat {
404 #[default]
405 Compact,
406 Pretty,
407 Json,
408}
409
410#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
411#[serde(rename_all = "snake_case")]
412pub enum LogRotation {
413 Never,
414 Hourly,
415 #[default]
416 Daily,
417}
418
419#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
420#[serde(rename_all = "snake_case")]
421pub enum SubscriberInstallPolicy {
422 Required,
423 #[default]
424 BestEffort,
425}
426
427#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
428pub struct SubscriberInstallStatus {
429 pub attempted: bool,
430 pub installed: bool,
431}
432
433impl SubscriberInstallStatus {
434 pub fn attempted(installed: bool) -> Self {
435 Self {
436 attempted: true,
437 installed,
438 }
439 }
440}
441
442impl Default for ObservabilityConfig {
443 fn default() -> Self {
444 Self {
445 enabled: false,
446 service_name: "rocketmq-rust".to_string(),
447 service_namespace: "rocketmq".to_string(),
448 service_instance_id: String::new(),
449 service_version: env!("CARGO_PKG_VERSION").to_string(),
450 environment: "dev".to_string(),
451 cluster: "DefaultCluster".to_string(),
452 node_type: "unknown".to_string(),
453 node_id: String::new(),
454 metrics: MetricsConfig::default(),
455 traces: TracesConfig::default(),
456 logs: LogsConfig::default(),
457 otlp: OtlpConfig::default(),
458 prometheus: PrometheusConfig::default(),
459 subscriber_install_policy: SubscriberInstallPolicy::default(),
460 resource_attributes: HashMap::new(),
461 }
462 }
463}
464
465impl Default for LoggingConfig {
466 fn default() -> Self {
467 Self {
468 enabled: true,
469 filter: crate::log_filter::DEFAULT_LOG_FILTER.to_string(),
470 console: ConsoleLogConfig::default(),
471 file: FileLogConfig::default(),
472 reload: ReloadConfig::default(),
473 }
474 }
475}
476
477impl Default for ConsoleLogConfig {
478 fn default() -> Self {
479 Self {
480 enabled: true,
481 format: LogFormat::Compact,
482 ansi: true,
483 target: true,
484 thread_ids: true,
485 thread_names: true,
486 line_number: true,
487 }
488 }
489}
490
491impl Default for FileLogConfig {
492 fn default() -> Self {
493 Self {
494 enabled: false,
495 directory: "logs".to_string(),
496 file_name_prefix: "rocketmq-rust".to_string(),
497 rotation: LogRotation::Daily,
498 format: LogFormat::Json,
499 non_blocking: NonBlockingLogConfig::default(),
500 }
501 }
502}
503
504impl Default for NonBlockingLogConfig {
505 fn default() -> Self {
506 Self {
507 buffered_lines_limit: 65_536,
508 lossy: false,
509 }
510 }
511}
512
513impl Default for MetricsConfig {
514 fn default() -> Self {
515 Self {
516 enabled: false,
517 exporter: MetricsExporter::Disable,
518 export_interval_millis: 30_000,
519 export_timeout_millis: 3_000,
520 cardinality_limit: 10_000,
521 sample_ratio: 1.0,
522 topic_label_enabled: true,
523 consumer_group_label_enabled: true,
524 }
525 }
526}
527
528impl Default for TracesConfig {
529 fn default() -> Self {
530 Self {
531 enabled: false,
532 exporter: TraceExporter::Disable,
533 sample_ratio: 0.01,
534 propagate_context: true,
535 record_message_id: false,
536 record_message_keys: false,
537 record_body_size: true,
538 }
539 }
540}
541
542impl Default for LogsConfig {
543 fn default() -> Self {
544 Self {
545 enabled: false,
546 exporter: LogsExporter::Disable,
547 }
548 }
549}
550
551impl Default for OtlpConfig {
552 fn default() -> Self {
553 Self {
554 endpoint: "http://127.0.0.1:4317".to_string(),
555 protocol: OtlpProtocol::Grpc,
556 headers: HashMap::new(),
557 timeout_millis: 3_000,
558 }
559 }
560}
561
562impl Default for PrometheusConfig {
563 fn default() -> Self {
564 Self {
565 host: "127.0.0.1".to_string(),
566 port: 5557,
567 path: "/metrics".to_string(),
568 }
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 const ENDPOINT_CANARY: &str = "https://collector.invalid:4317/v1/traces?token=ENDPOINT_CANARY";
577 const HEADER_KEY_CANARY: &str = "x-header-key-canary";
578 const HEADER_VALUE_CANARY: &str = "Bearer HEADER_VALUE_CANARY";
579 const RESOURCE_KEY_CANARY: &str = "resource.key.canary";
580 const RESOURCE_VALUE_CANARY: &str = "RESOURCE_VALUE_CANARY";
581
582 fn assert_sensitive_debug_values_are_absent(output: &str) {
583 for canary in [
584 ENDPOINT_CANARY,
585 HEADER_KEY_CANARY,
586 HEADER_VALUE_CANARY,
587 RESOURCE_KEY_CANARY,
588 RESOURCE_VALUE_CANARY,
589 ] {
590 assert!(!output.contains(canary), "Debug output exposed {canary}: {output}");
591 }
592 }
593
594 #[test]
595 fn observability_config_debug_redacts_otlp_and_resource_values() {
596 let mut config = ObservabilityConfig::default();
597 config.otlp.endpoint = ENDPOINT_CANARY.to_string();
598 config
599 .otlp
600 .headers
601 .insert(HEADER_KEY_CANARY.to_string(), HEADER_VALUE_CANARY.to_string());
602 config
603 .resource_attributes
604 .insert(RESOURCE_KEY_CANARY.to_string(), RESOURCE_VALUE_CANARY.to_string());
605
606 let otlp_debug = format!("{:?}", config.otlp);
607 let config_debug = format!("{config:?}");
608
609 assert_sensitive_debug_values_are_absent(&otlp_debug);
610 assert_sensitive_debug_values_are_absent(&config_debug);
611 assert!(otlp_debug.contains("endpoint_configured"));
612 assert!(otlp_debug.contains("headers_count"));
613 assert!(config_debug.contains("resource_attributes_count"));
614 }
615
616 #[test]
617 fn observability_overrides_debug_redacts_otlp_and_resource_values() {
618 let overrides = ObservabilityOverrides {
619 resource_attributes: Some(HashMap::from([(
620 RESOURCE_KEY_CANARY.to_string(),
621 RESOURCE_VALUE_CANARY.to_string(),
622 )])),
623 otlp: OtlpOverrides {
624 endpoint: Some(ENDPOINT_CANARY.to_string()),
625 headers: Some(HashMap::from([(
626 HEADER_KEY_CANARY.to_string(),
627 HEADER_VALUE_CANARY.to_string(),
628 )])),
629 ..OtlpOverrides::default()
630 },
631 ..ObservabilityOverrides::default()
632 };
633
634 let otlp_debug = format!("{:?}", overrides.otlp);
635 let overrides_debug = format!("{overrides:?}");
636
637 assert_sensitive_debug_values_are_absent(&otlp_debug);
638 assert_sensitive_debug_values_are_absent(&overrides_debug);
639 assert!(otlp_debug.contains("endpoint_configured"));
640 assert!(otlp_debug.contains("headers_count"));
641 assert!(overrides_debug.contains("resource_attributes_count"));
642 }
643
644 #[test]
645 fn default_config_is_disabled_and_local_only() {
646 let config = ObservabilityConfig::default();
647
648 assert!(!config.enabled);
649 assert!(!config.metrics.enabled);
650 assert!(!config.traces.enabled);
651 assert!((config.metrics.sample_ratio - 1.0).abs() < f64::EPSILON);
652 assert_eq!(config.prometheus.host, "127.0.0.1");
653 assert!(!config.traces.record_message_id);
654 assert!(!config.traces.record_message_keys);
655 assert_eq!(config.subscriber_install_policy, SubscriberInstallPolicy::BestEffort);
656 }
657
658 #[test]
659 fn subscriber_install_policy_uses_snake_case_serde() {
660 let policy = serde_json::from_str::<SubscriberInstallPolicy>("\"required\"")
661 .expect("required policy should deserialize");
662
663 assert_eq!(policy, SubscriberInstallPolicy::Required);
664 assert_eq!(
665 serde_json::to_string(&SubscriberInstallPolicy::BestEffort).expect("policy should serialize"),
666 "\"best_effort\""
667 );
668 }
669
670 #[test]
671 fn logging_config_defaults_to_console_info_filter() {
672 let config = LoggingConfig::default();
673
674 assert!(config.enabled);
675 assert_eq!(config.filter, "info");
676 assert!(config.console.enabled);
677 assert_eq!(config.console.format, LogFormat::Compact);
678 assert!(config.console.ansi);
679 assert!(config.console.target);
680 assert!(config.console.thread_ids);
681 assert!(config.console.thread_names);
682 assert!(config.console.line_number);
683 assert!(!config.file.enabled);
684 assert_eq!(config.file.rotation, LogRotation::Daily);
685 assert_eq!(config.file.format, LogFormat::Json);
686 assert_eq!(config.file.non_blocking.buffered_lines_limit, 65_536);
687 assert!(!config.file.non_blocking.lossy);
688 assert!(!config.reload.enabled);
689 }
690
691 #[test]
692 fn logging_config_serde_roundtrip_preserves_enums() {
693 let config = LoggingConfig {
694 filter: "rocketmq_store=debug,rocketmq_transport=warn".to_string(),
695 console: ConsoleLogConfig {
696 format: LogFormat::Pretty,
697 ..ConsoleLogConfig::default()
698 },
699 file: FileLogConfig {
700 enabled: true,
701 rotation: LogRotation::Hourly,
702 format: LogFormat::Json,
703 non_blocking: NonBlockingLogConfig {
704 buffered_lines_limit: 4_096,
705 lossy: true,
706 },
707 ..FileLogConfig::default()
708 },
709 reload: ReloadConfig { enabled: true },
710 ..LoggingConfig::default()
711 };
712
713 let payload = serde_json::to_string(&config).expect("logging config should serialize");
714 assert!(payload.contains("\"pretty\""));
715 assert!(payload.contains("\"hourly\""));
716
717 let decoded = serde_json::from_str::<LoggingConfig>(&payload).expect("logging config should deserialize");
718
719 assert_eq!(decoded, config);
720 }
721
722 #[test]
723 fn invalid_logging_enum_values_are_rejected() {
724 assert!(serde_json::from_str::<LogFormat>("\"camelCase\"").is_err());
725 assert!(serde_json::from_str::<LogRotation>("\"weekly\"").is_err());
726 }
727
728 #[test]
729 fn telemetry_bootstrap_default_keeps_observability_disabled_and_logging_ready() {
730 let config = TelemetryBootstrapConfig::default();
731
732 assert!(!config.observability.enabled);
733 assert!(config.logging.enabled);
734 assert!(config.logging.console.enabled);
735 assert!(!config.logging.file.enabled);
736 }
737}