1#[cfg(feature = "otel-traces")]
5use crate::CleanroomError;
6
7#[cfg(feature = "otel-traces")]
8pub mod json_exporter;
9
10#[cfg(feature = "otel-traces")]
11use {
12 opentelemetry::{
13 global, propagation::TextMapCompositePropagator, trace::TracerProvider, KeyValue,
14 },
15 opentelemetry_sdk::{
16 error::OTelSdkResult,
17 propagation::{BaggagePropagator, TraceContextPropagator},
18 trace::{Sampler, SdkTracerProvider, SpanExporter},
19 Resource,
20 },
21 tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry},
22};
23
24#[cfg(feature = "otel-metrics")]
25use opentelemetry_sdk::metrics::SdkMeterProvider;
26
27#[cfg(feature = "otel-traces")]
28use tracing_opentelemetry::OpenTelemetryLayer;
29
30#[derive(Clone, Debug)]
32pub enum Export {
33 OtlpHttp { endpoint: &'static str },
35 OtlpGrpc { endpoint: &'static str },
37 Stdout,
39 StdoutNdjson,
41}
42
43#[cfg(feature = "otel-traces")]
45#[derive(Debug)]
46enum SpanExporterType {
47 Otlp(Box<opentelemetry_otlp::SpanExporter>),
48 #[cfg(feature = "otel-stdout")]
49 Stdout(opentelemetry_stdout::SpanExporter),
50 NdjsonStdout(json_exporter::NdjsonStdoutExporter),
51}
52
53#[cfg(feature = "otel-traces")]
54#[allow(refining_impl_trait)]
55impl SpanExporter for SpanExporterType {
56 fn export(
57 &self,
58 batch: Vec<opentelemetry_sdk::trace::SpanData>,
59 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = OTelSdkResult> + Send + '_>> {
60 match self {
61 SpanExporterType::Otlp(exporter) => Box::pin(exporter.as_ref().export(batch)),
62 #[cfg(feature = "otel-stdout")]
63 SpanExporterType::Stdout(exporter) => Box::pin(exporter.export(batch)),
64 SpanExporterType::NdjsonStdout(exporter) => Box::pin(exporter.export(batch)),
65 }
66 }
67
68 fn shutdown(&mut self) -> OTelSdkResult {
69 match self {
70 SpanExporterType::Otlp(exporter) => exporter.as_mut().shutdown(),
71 #[cfg(feature = "otel-stdout")]
72 SpanExporterType::Stdout(exporter) => exporter.shutdown(),
73 SpanExporterType::NdjsonStdout(exporter) => exporter.shutdown(),
74 }
75 }
76}
77
78#[derive(Clone, Debug)]
80pub struct OtelConfig {
81 pub service_name: &'static str,
82 pub deployment_env: &'static str, pub sample_ratio: f64, pub export: Export,
85 pub enable_fmt_layer: bool, pub headers: Option<std::collections::HashMap<String, String>>, }
88
89pub struct OtelGuard {
91 #[cfg(feature = "otel-traces")]
92 tracer_provider: SdkTracerProvider,
93 #[cfg(feature = "otel-metrics")]
94 meter_provider: Option<SdkMeterProvider>,
95 #[cfg(feature = "otel-logs")]
96 logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
97}
98
99impl Drop for OtelGuard {
100 fn drop(&mut self) {
101 #[cfg(feature = "otel-traces")]
102 {
103 let _ = self.tracer_provider.shutdown();
104 }
105 #[cfg(feature = "otel-metrics")]
106 {
107 if let Some(mp) = self.meter_provider.take() {
108 let _ = mp.shutdown();
109 }
110 }
111 #[cfg(feature = "otel-logs")]
112 {
113 if let Some(lp) = self.logger_provider.take() {
114 let _ = lp.shutdown();
115 }
116 }
117 }
118}
119
120#[cfg(feature = "otel-traces")]
122pub fn init_otel(cfg: OtelConfig) -> Result<OtelGuard, CleanroomError> {
123 global::set_text_map_propagator(TextMapCompositePropagator::new(vec![
125 Box::new(TraceContextPropagator::new()),
126 Box::new(BaggagePropagator::new()),
127 ]));
128
129 let resource = Resource::builder_empty()
131 .with_service_name(cfg.service_name)
132 .with_attributes([
133 KeyValue::new("deployment.environment", cfg.deployment_env),
134 KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
135 KeyValue::new("telemetry.sdk.language", "rust"),
136 KeyValue::new("telemetry.sdk.name", "opentelemetry"),
137 KeyValue::new("telemetry.sdk.version", "0.31.0"),
138 ])
139 .build();
140
141 let sampler = Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(cfg.sample_ratio)));
143
144 let span_exporter = match cfg.export {
146 Export::OtlpHttp { endpoint } => {
147 std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint);
149
150 if let Some(ref headers) = cfg.headers {
152 for (key, value) in headers {
153 let env_key = format!("OTEL_EXPORTER_OTLP_HEADERS_{}", key.to_uppercase());
154 std::env::set_var(env_key, value);
155 }
156 }
157
158 let exporter = opentelemetry_otlp::SpanExporter::builder()
159 .with_http()
160 .build()
161 .map_err(|e| {
162 CleanroomError::internal_error(format!(
163 "Failed to create OTLP HTTP exporter: {}",
164 e
165 ))
166 })?;
167 SpanExporterType::Otlp(Box::new(exporter))
168 }
169 Export::OtlpGrpc { endpoint } => {
170 std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint);
172
173 if let Some(ref headers) = cfg.headers {
175 for (key, value) in headers {
176 let env_key = format!("OTEL_EXPORTER_OTLP_HEADERS_{}", key.to_uppercase());
177 std::env::set_var(env_key, value);
178 }
179 }
180
181 let exporter = opentelemetry_otlp::SpanExporter::builder()
182 .with_tonic()
183 .build()
184 .map_err(|e| {
185 CleanroomError::internal_error(format!(
186 "Failed to create OTLP gRPC exporter: {}",
187 e
188 ))
189 })?;
190 SpanExporterType::Otlp(Box::new(exporter))
191 }
192 #[cfg(feature = "otel-stdout")]
193 Export::Stdout => SpanExporterType::Stdout(opentelemetry_stdout::SpanExporter::default()),
194 #[cfg(not(feature = "otel-stdout"))]
195 Export::Stdout => {
196 return Err(CleanroomError::internal_error(
197 "Stdout export requires 'otel-stdout' feature",
198 ));
199 }
200 Export::StdoutNdjson => SpanExporterType::NdjsonStdout(json_exporter::NdjsonStdoutExporter::new()),
201 };
202
203 let tp = opentelemetry_sdk::trace::SdkTracerProvider::builder()
205 .with_batch_exporter(span_exporter)
206 .with_sampler(sampler)
207 .with_resource(resource.clone())
208 .build();
209
210 let otel_layer = OpenTelemetryLayer::new(tp.tracer("clnrm"));
212 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
213
214 let fmt_layer = if cfg.enable_fmt_layer {
215 Some(tracing_subscriber::fmt::layer().compact())
216 } else {
217 None
218 };
219
220 let subscriber = Registry::default()
221 .with(env_filter)
222 .with(otel_layer)
223 .with(fmt_layer);
224
225 tracing::subscriber::set_global_default(subscriber).ok();
226
227 #[cfg(feature = "otel-metrics")]
229 let meter_provider = {
230 use opentelemetry_sdk::metrics::SdkMeterProvider;
231 let provider = SdkMeterProvider::builder()
234 .with_resource(resource.clone())
235 .build();
236 Some(provider)
237 };
238
239 #[cfg(feature = "otel-logs")]
241 let logger_provider = {
242 use opentelemetry_sdk::logs::SdkLoggerProvider;
243 let provider = SdkLoggerProvider::builder()
246 .with_resource(resource.clone())
247 .build();
248 Some(provider)
249 };
250
251 #[cfg(feature = "otel-metrics")]
253 if let Some(ref mp) = meter_provider {
254 global::set_meter_provider(mp.clone());
255 }
256
257 Ok(OtelGuard {
261 tracer_provider: tp,
262 #[cfg(feature = "otel-metrics")]
263 meter_provider,
264 #[cfg(feature = "otel-logs")]
265 logger_provider,
266 })
267}
268
269#[cfg(feature = "otel-traces")]
271pub mod validation {
272 use crate::error::Result;
273
274 pub fn is_otel_initialized() -> bool {
276 true
279 }
280
281 pub fn span_exists(operation_name: &str) -> Result<bool> {
284 unimplemented!(
290 "span_exists: Requires in-memory span exporter. \
291 Future implementation will query captured spans for operation: {}",
292 operation_name
293 )
294 }
295
296 pub fn capture_test_spans() -> Result<usize> {
299 unimplemented!("capture_test_spans: Requires in-memory span exporter configuration")
305 }
306}
307
308#[cfg(feature = "otel-metrics")]
310pub mod metrics {
311 use opentelemetry::{global, KeyValue};
312
313 pub fn increment_counter(name: &str, value: u64, attributes: Vec<KeyValue>) {
316 let meter = global::meter("clnrm");
317 let counter = meter.u64_counter(name.to_string()).build();
318 counter.add(value, &attributes);
319 }
320
321 pub fn record_histogram(name: &str, value: f64, attributes: Vec<KeyValue>) {
323 let meter = global::meter("clnrm");
324 let histogram = meter.f64_histogram(name.to_string()).build();
325 histogram.record(value, &attributes);
326 }
327
328 pub fn record_test_duration(test_name: &str, duration_ms: f64, success: bool) {
330 let meter = global::meter("clnrm");
331 let histogram = meter
332 .f64_histogram("test.duration_ms")
333 .with_description("Test execution duration in milliseconds")
334 .build();
335
336 let attributes = vec![
337 KeyValue::new("test.name", test_name.to_string()),
338 KeyValue::new("test.success", success),
339 ];
340
341 histogram.record(duration_ms, &attributes);
342 }
343
344 pub fn record_container_operation(operation: &str, duration_ms: f64, container_type: &str) {
346 let meter = global::meter("clnrm");
347 let histogram = meter
348 .f64_histogram("container.operation_duration_ms")
349 .with_description("Container operation duration in milliseconds")
350 .build();
351
352 let attributes = vec![
353 KeyValue::new("container.operation", operation.to_string()),
354 KeyValue::new("container.type", container_type.to_string()),
355 ];
356
357 histogram.record(duration_ms, &attributes);
358 }
359
360 pub fn increment_test_counter(test_name: &str, result: &str) {
362 let meter = global::meter("clnrm");
363 let counter = meter
364 .u64_counter("test.executions")
365 .with_description("Number of test executions")
366 .build();
367
368 let attributes = vec![
369 KeyValue::new("test.name", test_name.to_string()),
370 KeyValue::new("test.result", result.to_string()),
371 ];
372
373 counter.add(1, &attributes);
374 }
375}
376
377#[cfg(feature = "otel-logs")]
379pub fn add_otel_logs_layer() {
380 let _ = tracing_subscriber::fmt::try_init();
384}
385
386#[cfg(feature = "otel-traces")]
389pub mod spans {
390 use tracing::{span, Level};
391
392 pub fn run_span(config_path: &str, test_count: usize) -> tracing::Span {
395 span!(
396 Level::INFO,
397 "clnrm.run",
398 clnrm.version = env!("CARGO_PKG_VERSION"),
399 test.config = config_path,
400 test.count = test_count,
401 otel.kind = "internal",
402 component = "runner",
403 )
404 }
405
406 pub fn step_span(step_name: &str, step_index: usize) -> tracing::Span {
409 span!(
410 Level::INFO,
411 "clnrm.step",
412 step.name = step_name,
413 step.index = step_index,
414 otel.kind = "internal",
415 component = "step_executor",
416 )
417 }
418
419 pub fn test_span(test_name: &str) -> tracing::Span {
422 span!(
423 Level::INFO,
424 "clnrm.test",
425 test.name = test_name,
426 test.hermetic = true,
427 otel.kind = "internal",
428 component = "test_executor",
429 )
430 }
431
432 pub fn plugin_registry_span(plugin_count: usize) -> tracing::Span {
435 span!(
436 Level::INFO,
437 "clnrm.plugin.registry",
438 plugin.count = plugin_count,
439 otel.kind = "internal",
440 component = "plugin_registry",
441 )
442 }
443
444 pub fn service_start_span(service_name: &str, service_type: &str) -> tracing::Span {
447 span!(
448 Level::INFO,
449 "clnrm.service.start",
450 service.name = service_name,
451 service.type = service_type,
452 otel.kind = "internal",
453 component = "service_manager",
454 )
455 }
456
457 pub fn container_start_span(image: &str, container_id: &str) -> tracing::Span {
460 span!(
461 Level::INFO,
462 "clnrm.container.start",
463 container.image = image,
464 container.id = container_id,
465 otel.kind = "internal",
466 component = "container_backend",
467 )
468 }
469
470 pub fn container_exec_span(container_id: &str, command: &str) -> tracing::Span {
473 span!(
474 Level::INFO,
475 "clnrm.container.exec",
476 container.id = container_id,
477 command = command,
478 otel.kind = "internal",
479 component = "container_backend",
480 )
481 }
482
483 pub fn container_stop_span(container_id: &str) -> tracing::Span {
486 span!(
487 Level::INFO,
488 "clnrm.container.stop",
489 container.id = container_id,
490 otel.kind = "internal",
491 component = "container_backend",
492 )
493 }
494
495 pub fn command_execute_span(command: &str) -> tracing::Span {
498 span!(
499 Level::INFO,
500 "clnrm.command.execute",
501 command = command,
502 otel.kind = "internal",
503 component = "command_executor",
504 )
505 }
506
507 pub fn assertion_span(assertion_type: &str) -> tracing::Span {
510 span!(
511 Level::INFO,
512 "clnrm.assertion.validate",
513 assertion.type = assertion_type,
514 otel.kind = "internal",
515 component = "validator",
516 )
517 }
518}
519
520#[cfg(feature = "otel-traces")]
523pub mod events {
524 use opentelemetry::trace::{Span, Status};
525 use opentelemetry::KeyValue;
526
527 pub fn record_container_start<S: Span>(span: &mut S, image: &str, container_id: &str) {
529 span.add_event(
530 "container.start",
531 vec![
532 KeyValue::new("container.image", image.to_string()),
533 KeyValue::new("container.id", container_id.to_string()),
534 ],
535 );
536 }
537
538 pub fn record_container_exec<S: Span>(span: &mut S, command: &str, exit_code: i32) {
540 span.add_event(
541 "container.exec",
542 vec![
543 KeyValue::new("command", command.to_string()),
544 KeyValue::new("exit_code", exit_code.to_string()),
545 ],
546 );
547 }
548
549 pub fn record_container_stop<S: Span>(span: &mut S, container_id: &str, exit_code: i32) {
551 span.add_event(
552 "container.stop",
553 vec![
554 KeyValue::new("container.id", container_id.to_string()),
555 KeyValue::new("exit_code", exit_code.to_string()),
556 ],
557 );
558 }
559
560 pub fn record_step_start<S: Span>(span: &mut S, step_name: &str) {
562 span.add_event(
563 "step.start",
564 vec![KeyValue::new("step.name", step_name.to_string())],
565 );
566 }
567
568 pub fn record_step_complete<S: Span>(span: &mut S, step_name: &str, status: &str) {
570 span.add_event(
571 "step.complete",
572 vec![
573 KeyValue::new("step.name", step_name.to_string()),
574 KeyValue::new("status", status.to_string()),
575 ],
576 );
577 }
578
579 pub fn record_test_result<S: Span>(span: &mut S, test_name: &str, passed: bool) {
581 let status_str = if passed { "pass" } else { "fail" };
582 span.add_event(
583 "test.result",
584 vec![
585 KeyValue::new("test.name", test_name.to_string()),
586 KeyValue::new("result", status_str.to_string()),
587 ],
588 );
589
590 if !passed {
591 span.set_status(Status::error("Test failed"));
592 }
593 }
594
595 pub fn record_error<S: Span>(span: &mut S, error_type: &str, error_message: &str) {
597 span.add_event(
598 "error",
599 vec![
600 KeyValue::new("error.type", error_type.to_string()),
601 KeyValue::new("error.message", error_message.to_string()),
602 ],
603 );
604 span.set_status(Status::error(error_message.to_string()));
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_export_enum_variants() {
615 let http_export = Export::OtlpHttp {
616 endpoint: "http://localhost:4318",
617 };
618 let grpc_export = Export::OtlpGrpc {
619 endpoint: "http://localhost:4317",
620 };
621 let stdout_export = Export::Stdout;
622 let ndjson_export = Export::StdoutNdjson;
623
624 assert!(matches!(http_export, Export::OtlpHttp { .. }));
625 assert!(matches!(grpc_export, Export::OtlpGrpc { .. }));
626 assert!(matches!(stdout_export, Export::Stdout));
627 assert!(matches!(ndjson_export, Export::StdoutNdjson));
628 }
629
630 #[test]
631 fn test_otel_config_creation() {
632 let config = OtelConfig {
633 service_name: "test-service",
634 deployment_env: "test",
635 sample_ratio: 1.0,
636 export: Export::Stdout,
637 enable_fmt_layer: true,
638 headers: None,
639 };
640
641 assert_eq!(config.service_name, "test-service");
642 assert_eq!(config.deployment_env, "test");
643 assert_eq!(config.sample_ratio, 1.0);
644 assert!(config.enable_fmt_layer);
645 }
646
647 #[cfg(feature = "otel-traces")]
648 #[test]
649 fn test_otel_initialization_with_stdout() {
650 use opentelemetry::trace::{Span, Tracer};
651
652 let config = OtelConfig {
653 service_name: "test-service",
654 deployment_env: "test",
655 sample_ratio: 1.0,
656 export: Export::Stdout,
657 enable_fmt_layer: false, headers: None,
659 };
660
661 let result = init_otel(config);
662 assert!(
663 result.is_ok(),
664 "OTel initialization should succeed with stdout export"
665 );
666
667 let tracer = opentelemetry::global::tracer("test");
669 let mut span = tracer.start("test-span");
670 span.end();
671 }
672
673 #[cfg(feature = "otel-traces")]
674 #[test]
675 fn test_otel_initialization_with_http_fallback() {
676 let config = OtelConfig {
677 service_name: "test-service",
678 deployment_env: "test",
679 sample_ratio: 1.0,
680 export: Export::OtlpHttp {
681 endpoint: "http://localhost:4318",
682 },
683 enable_fmt_layer: false,
684 headers: None,
685 };
686
687 let result = init_otel(config);
688 assert!(
689 result.is_ok(),
690 "OTel initialization should succeed with HTTP fallback to stdout"
691 );
692 }
693
694 #[cfg(feature = "otel-traces")]
695 #[test]
696 fn test_otel_initialization_with_grpc_fallback() {
697 let config = OtelConfig {
700 service_name: "test-service",
701 deployment_env: "test",
702 sample_ratio: 1.0,
703 export: Export::OtlpGrpc {
704 endpoint: "http://localhost:4317",
705 },
706 enable_fmt_layer: false,
707 headers: None,
708 };
709
710 assert_eq!(config.service_name, "test-service");
712 assert_eq!(config.deployment_env, "test");
713 assert_eq!(config.sample_ratio, 1.0);
714 assert!(!config.enable_fmt_layer);
715 }
716
717 #[test]
718 fn test_otel_guard_drop() -> Result<(), crate::error::CleanroomError> {
719 let config = OtelConfig {
721 service_name: "test-service",
722 deployment_env: "test",
723 sample_ratio: 1.0,
724 export: Export::Stdout,
725 enable_fmt_layer: false,
726 headers: None,
727 };
728
729 #[cfg(feature = "otel-traces")]
730 {
731 let guard = init_otel(config)?;
732 drop(guard); }
734
735 #[cfg(not(feature = "otel-traces"))]
736 {
737 assert_eq!(config.service_name, "test-service");
739 }
740
741 Ok(())
742 }
743
744 #[test]
745 fn test_otel_config_clone() {
746 let config = OtelConfig {
747 service_name: "test-service",
748 deployment_env: "test",
749 sample_ratio: 0.5,
750 export: Export::OtlpHttp {
751 endpoint: "http://localhost:4318",
752 },
753 enable_fmt_layer: false,
754 headers: None,
755 };
756
757 let cloned = config.clone();
758 assert_eq!(cloned.service_name, config.service_name);
759 assert_eq!(cloned.sample_ratio, config.sample_ratio);
760 }
761
762 #[cfg(feature = "otel-traces")]
767 #[test]
768 fn test_sample_ratios() {
769 let ratios = vec![0.0, 0.1, 0.5, 1.0];
770
771 for ratio in ratios {
772 let config = OtelConfig {
773 service_name: "test-service",
774 deployment_env: "test",
775 sample_ratio: ratio,
776 export: Export::OtlpHttp {
777 endpoint: "http://localhost:4318",
778 },
779 enable_fmt_layer: false,
780 headers: None,
781 };
782
783 assert_eq!(config.sample_ratio, ratio);
784 }
785 }
786
787 #[test]
788 fn test_export_debug_format() {
789 let http = Export::OtlpHttp {
790 endpoint: "http://localhost:4318",
791 };
792 let debug_str = format!("{:?}", http);
793 assert!(debug_str.contains("OtlpHttp"));
794 assert!(debug_str.contains("4318"));
795 }
796
797 #[cfg(feature = "otel-traces")]
798 #[test]
799 fn test_deployment_environments() {
800 let envs = vec!["dev", "staging", "prod"];
801
802 for env in envs {
803 let config = OtelConfig {
804 service_name: "test-service",
805 deployment_env: env,
806 sample_ratio: 1.0,
807 export: Export::OtlpHttp {
808 endpoint: "http://localhost:4318",
809 },
810 enable_fmt_layer: true,
811 headers: None,
812 };
813
814 assert_eq!(config.deployment_env, env);
815 }
816 }
817
818 #[test]
819 fn test_export_clone() {
820 let http_export = Export::OtlpHttp {
821 endpoint: "http://localhost:4318",
822 };
823 let cloned = http_export.clone();
824
825 match cloned {
826 Export::OtlpHttp { endpoint } => assert_eq!(endpoint, "http://localhost:4318"),
827 _ => panic!("Expected OtlpHttp variant"),
828 }
829 }
830
831 #[test]
832 fn test_otel_config_debug_format() {
833 let config = OtelConfig {
834 service_name: "debug-test",
835 deployment_env: "debug",
836 sample_ratio: 0.75,
837 export: Export::OtlpGrpc {
838 endpoint: "http://localhost:4317",
839 },
840 enable_fmt_layer: true,
841 headers: None,
842 };
843
844 let debug_str = format!("{:?}", config);
845 assert!(debug_str.contains("debug-test"));
846 assert!(debug_str.contains("debug"));
847 assert!(debug_str.contains("0.75"));
848 }
849
850 #[cfg(feature = "otel-traces")]
851 #[test]
852 fn test_otel_config_with_different_exports() {
853 let http_config = OtelConfig {
854 service_name: "http-service",
855 deployment_env: "test",
856 sample_ratio: 1.0,
857 export: Export::OtlpHttp {
858 endpoint: "http://localhost:4318",
859 },
860 enable_fmt_layer: false,
861 headers: None,
862 };
863
864 let grpc_config = OtelConfig {
865 service_name: "grpc-service",
866 deployment_env: "test",
867 sample_ratio: 1.0,
868 export: Export::OtlpGrpc {
869 endpoint: "http://localhost:4317",
870 },
871 enable_fmt_layer: false,
872 headers: None,
873 };
874
875 assert_eq!(http_config.service_name, "http-service");
876 assert_eq!(grpc_config.service_name, "grpc-service");
877
878 match http_config.export {
879 Export::OtlpHttp { endpoint } => assert_eq!(endpoint, "http://localhost:4318"),
880 _ => panic!("Expected OtlpHttp variant"),
881 }
882
883 match grpc_config.export {
884 Export::OtlpGrpc { endpoint } => assert_eq!(endpoint, "http://localhost:4317"),
885 _ => panic!("Expected OtlpGrpc variant"),
886 }
887 }
888
889 #[test]
890 fn test_export_stdout_variant() {
891 let stdout_export = Export::Stdout;
892 assert!(matches!(stdout_export, Export::Stdout));
893
894 let ndjson_export = Export::StdoutNdjson;
895 assert!(matches!(ndjson_export, Export::StdoutNdjson));
896 }
897}