Skip to main content

tracing_kickstart/
trace.rs

1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use tracing_error::ErrorLayer;
4use std::collections::HashMap;
5use std::fmt;
6use std::fs::OpenOptions;
7use std::time::Duration;
8use tracing_subscriber::{EnvFilter, Layer as _};
9use tracing_subscriber::layer::SubscriberExt;
10use tracing_subscriber::util::SubscriberInitExt;
11
12// opentelemetry - base
13use opentelemetry::{Key, KeyValue, Value};
14use opentelemetry_sdk::resource::Resource;
15#[cfg(feature = "detector_telemetry")]
16use opentelemetry_sdk::resource::TelemetryResourceDetector;
17use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
18#[cfg(feature = "detector_hostresource")]
19use opentelemetry_resource_detectors::HostResourceDetector;
20#[cfg(feature = "detector_os")]
21use opentelemetry_resource_detectors::OsResourceDetector;
22#[cfg(feature = "detector_process")]
23use opentelemetry_resource_detectors::ProcessResourceDetector;
24
25use opentelemetry_semantic_conventions::attribute;
26
27// opentelemetry - traces
28use opentelemetry_otlp::SpanExporter;
29use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
30#[cfg(not(feature = "tokio_console"))]
31use opentelemetry::trace::TracerProvider as _; // for tracer trait
32#[cfg(not(feature = "tokio_console"))]
33use tracing_opentelemetry::OpenTelemetryLayer;
34
35// opentelemetry - metrics
36use opentelemetry_otlp::MetricExporter;
37use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
38use tracing_opentelemetry::MetricsLayer;
39#[cfg(feature = "exponential_histograms")]
40use opentelemetry_sdk::metrics::{Aggregation, InstrumentKind, Stream};
41
42// opentelemetry - logs
43use opentelemetry_otlp::LogExporter;
44use opentelemetry_sdk::logs::SdkLoggerProvider;
45#[cfg(not(feature = "tokio_console"))]
46use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
47
48pub use opentelemetry_otlp::ExporterBuildError;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TracingOtelConfig {
52    collector_url: String,
53
54    #[serde(default, skip_serializing)]
55    collector_auth_header: Option<SecretString>,
56}
57impl TracingOtelConfig {
58    pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
59        Self {
60            collector_url,
61            collector_auth_header,
62        }
63    }
64    pub fn collector_url(&self) -> &str {
65        &self.collector_url
66    }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TracingConfig {
71    /// Custom env filter which takes priority over RUST_LOG
72    ///
73    /// This is beneficial when loading app conf from env,
74    /// as it allows overriding the env filter without setting a global RUST_LOG
75    #[serde(default)]
76    filter: Option<String>,
77
78    #[serde(default)]
79    log_file_path: Option<String>,
80
81    #[serde(default = "TracingConfig::ansi_output_default")]
82    ansi_output: bool,
83
84    /// If set, will be user as the value for the `deployment.environment.name` attribute
85    #[serde(default)]
86    deployment_env: Option<String>,
87
88    /// If set, will configure the metrics export period (duration in seconds)
89    metrics_interval: Option<u64>,
90
91    #[serde(default, flatten)]
92    otel_config: Option<TracingOtelConfig>,
93}
94impl TracingConfig {
95    pub fn new(
96        collector_url: Option<String>,
97        collector_auth_header: Option<SecretString>,
98        log_file_path: Option<String>,
99        ansi_output: Option<bool>,
100        filter: Option<String>,
101        deployment_env: Option<String>,
102        metrics_interval: Option<u64>,
103    ) -> Self {
104        Self {
105            filter,
106            log_file_path,
107            deployment_env,
108            metrics_interval,
109            ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
110            otel_config: collector_url.map(|url| TracingOtelConfig {
111                collector_url: url,
112                collector_auth_header,
113            }),
114        }
115    }
116    pub fn metrics_interval_duration(&self) -> Option<Duration> {
117        self.metrics_interval.map(Duration::from_secs)
118    }
119    pub fn log_file_path(&self) -> Option<&str> {
120        self.log_file_path.as_deref()
121    }
122    pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
123        &self.otel_config
124    }
125    pub fn ansi_output_default() -> bool {
126        true
127    }
128}
129impl Default for TracingConfig {
130    fn default() -> Self {
131        Self {
132            ansi_output: Self::ansi_output_default(),
133            filter: None,
134            deployment_env: None,
135            log_file_path: None,
136            otel_config: None,
137            metrics_interval: None,
138        }
139    }
140}
141
142// -- custom attributes + attribute helpers
143
144pub mod custom_attribute {
145    pub const DEPLOYMENT_BUILD_TYPE: &str = "deployment.build_type";
146    #[cfg(feature = "attrs_crate_name")]
147    pub const SERVICE_CRATE_NAME: &str = "service.crate_name";
148    #[cfg(feature = "attrs_version_expanded")]
149    pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
150    #[cfg(feature = "attrs_version_expanded")]
151    pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
152    #[cfg(feature = "attrs_version_expanded")]
153    pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
154
155    #[cfg(feature = "attrs_origin")]
156    pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
157    #[cfg(feature = "attrs_origin")]
158    pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
159}
160#[rustfmt::skip]
161pub fn get_build_env() -> &'static str {
162    #[cfg(debug_assertions)]
163    { "debug" }
164    #[cfg(not(debug_assertions))]
165    { "release" }
166}
167pub fn get_origin_package_name() -> Option<&'static str> {
168    let package_name = env!("CARGO_PKG_NAME");
169    if package_name.is_empty() {
170        None
171    } else {
172        Some(package_name)
173    }
174}
175pub fn get_origin_crate_name() -> Option<&'static str> {
176    let package_name = env!("CARGO_CRATE_NAME");
177    if package_name.is_empty() {
178        None
179    } else {
180        Some(package_name)
181    }
182}
183
184fn build_otel_resource(service_attrs: &ServiceAttributeStore, deployment_env: Option<String>, custom_attrs: Vec<KeyValue>) -> Resource {
185    // root/primary service name + package name
186    let mut builder = Resource::builder_empty()
187    .with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
188
189    #[cfg(feature = "attrs_crate_name")]
190    {
191        builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
192    }
193
194    // version
195    builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_attrs.version));
196
197    #[cfg(feature = "attrs_version_expanded")] {
198        builder = builder
199        .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, service_attrs.version_major))
200        .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, service_attrs.version_minor))
201        .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, service_attrs.version_patch));
202    }
203
204    #[cfg(feature = "attrs_origin")]
205    {
206        // returns the name of the package that contains the associated tracing call
207        if let Some(origin_package_name) = get_origin_package_name() {
208            builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
209        }
210        if let Some(origin_crate_name) = get_origin_crate_name() {
211            builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
212        }
213    }
214
215    // build mode: release/debug
216    builder = builder.with_attribute(KeyValue::new(custom_attribute::DEPLOYMENT_BUILD_TYPE, get_build_env()));
217
218    // deployment env set from config/runtime env
219    if let Some(env) = deployment_env {
220        builder = builder.with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, env));
221    }
222
223    // custom resource attrs
224    for attr in custom_attrs {
225        builder = builder.with_attribute(attr);
226    }
227
228    #[cfg(feature = "detector_telemetry")]
229    {
230        // telemetry sdk stack attrs
231        builder = builder.with_detector(Box::new(TelemetryResourceDetector));
232    }
233    #[cfg(feature = "detector_hostresource")]
234    {
235        // host id, host arch
236        builder = builder.with_detector(Box::new(HostResourceDetector::default()));
237    }
238    #[cfg(feature = "detector_process")]
239    {
240        // process args, pid
241        builder = builder.with_detector(Box::new(ProcessResourceDetector));
242    }
243    #[cfg(feature = "detector_os")]
244    {
245        // os
246        builder = builder.with_detector(Box::new(OsResourceDetector));
247    }
248
249    builder.build()
250}
251
252fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
253    let mut headers: HashMap<String, String> = HashMap::new();
254
255    // add auth headers if provided
256    if let Some(auth_header) = auth_header_val.as_ref() {
257        headers.insert("Authorization".into(), auth_header.expose_secret().into());
258    }
259
260    headers
261}
262
263// Construct TracerProvider for OpenTelemetryLayer
264fn init_otel_traces_provider(
265    collector_endpoint: &str,
266    headers: HashMap<String, String>,
267    resource: Resource,
268) -> Result<SdkTracerProvider, ExporterBuildError> {
269    let exporter = SpanExporter::builder()
270        .with_http()
271        .with_headers(headers)
272        .with_endpoint(format!("{collector_endpoint}/v1/traces"))
273        .with_protocol(Protocol::HttpBinary)
274        // .with_timeout(std::time::Duration::from_secs(3))
275        .build()?;
276
277    let provider = SdkTracerProvider::builder()
278        // Customize sampling strategy
279        .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
280        .with_resource(resource)
281        .with_batch_exporter(exporter)
282        .build();
283
284    Ok(provider)
285}
286fn init_otel_logs_provider(
287    collector_endpoint: &str,
288    headers: HashMap<String, String>,
289    resource: Resource,
290) -> Result<SdkLoggerProvider, ExporterBuildError> {
291    let exporter = LogExporter::builder()
292        .with_http()
293        .with_headers(headers)
294        .with_endpoint(format!("{collector_endpoint}/v1/logs"))
295        .with_protocol(Protocol::HttpBinary)
296        // .with_timeout(std::time::Duration::from_secs(3))
297        .build()?;
298
299    let provider = SdkLoggerProvider::builder()
300        .with_resource(resource)
301        .with_batch_exporter(exporter)
302        .build();
303
304    Ok(provider)
305}
306fn init_otel_metrics_provider(
307    collector_endpoint: &str,
308    headers: HashMap<String, String>,
309    resource: Resource,
310    interval: Option<Duration>,
311) -> Result<SdkMeterProvider, ExporterBuildError> {
312    let exporter = MetricExporter::builder()
313        .with_http()
314        .with_headers(headers)
315        .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
316        .with_protocol(Protocol::HttpBinary)
317        // .with_timeout(std::time::Duration::from_secs(3))
318        .build()?;
319
320    let mut periodic = PeriodicReader::builder(exporter);
321    if let Some(duration) = interval {
322        periodic = periodic.with_interval(duration);
323    }
324    let mut builder = SdkMeterProvider::builder();
325    builder = builder
326        .with_resource(resource)
327        .with_reader(periodic.build());
328    #[cfg(feature = "exponential_histograms")]
329    {
330        builder = builder.with_view(|inst| {
331            if let InstrumentKind::Histogram = inst.kind() {
332                let s = Stream::builder()
333                    .with_aggregation(Aggregation::Base2ExponentialHistogram {
334                        max_size: 160,
335                        max_scale: 20,
336                        record_min_max: true,
337                    })
338                    .build()
339                    .unwrap();
340                Some(s)
341            } else {
342                None
343            }
344        });
345    }
346    let provider = builder.build();
347
348    Ok(provider)
349}
350
351/// Compile-time attributes to be provided by the owning application/service.
352///
353/// Used as a set of parameters to pass to [`init()`]
354///
355/// Service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
356///
357/// ```
358/// use tracing_kickstart::TracingConfig;
359///
360/// let attrs = tracing_kickstart::build_attrs!();
361/// let conf = TracingConfig::default();
362/// let custom_fallback_env_filter = None;
363/// let extra_resource_attrs = None;
364///
365/// let tracing_providers = tracing_kickstart::init(attrs, &conf, custom_fallback_env_filter, extra_resource_attrs).unwrap();
366/// ```
367#[derive(Debug, Clone)]
368pub struct ServiceAttributeStore {
369    pub crate_name: &'static str,
370    pub pkg_name: &'static str,
371    pub version: &'static str,
372    pub version_major: &'static str,
373    pub version_minor: &'static str,
374    pub version_patch: &'static str,
375}
376impl ServiceAttributeStore {
377    pub fn dump(&self) {
378        let service_name = self.pkg_name;
379        let crate_name = self.crate_name;
380        let service_version = self.version;
381        let service_version_major = self.version_major;
382        let service_version_minor = self.version_minor;
383        let service_version_patch = self.version_patch;
384        let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -");
385        let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -");
386        let build_env = get_build_env();
387
388        println!();
389        println!("Resolved tracing attributes");
390        println!("--------------------");
391        println!("service_name (pkg_name): {service_name}");
392        println!("service_crate_name:      {crate_name}");
393        println!("service_version:         {service_version}");
394        println!("service_version_major:   {service_version_major}");
395        println!("service_version_minor:   {service_version_minor}");
396        println!("service_version_patch:   {service_version_patch}");
397        println!("origin_pkg_name:         {origin_pkg_name}");
398        println!("origin_crate_name:       {origin_crate_name}");
399        println!("build_env:               {build_env}");
400        println!();
401    }
402}
403
404/// Generates service attributes using env! calls.
405///
406/// This is done using a macro to allow for the `env!(..)` calls to be scoped from the
407/// parent package/crate, rather than from `tracing-kickstart`.
408#[macro_export]
409macro_rules! build_attrs {
410    // This macro takes an argument of designator `ident` and
411    // creates a function named `$func_name`.
412    // The `ident` designator is used for variable/function names.
413    () => (
414        tracing_kickstart::ServiceAttributeStore {
415            crate_name: env!("CARGO_CRATE_NAME"),
416            pkg_name: env!("CARGO_PKG_NAME"),
417            version: env!("CARGO_PKG_VERSION"),
418            version_major: env!("CARGO_PKG_VERSION_MAJOR"),
419            version_minor: env!("CARGO_PKG_VERSION_MINOR"),
420            version_patch: env!("CARGO_PKG_VERSION_PATCH"),
421        }
422    )
423}
424fn validate_non_empty_filter_str(filter: &str, source_name: &'static str) -> bool {
425    if filter.is_empty() {
426        println!("Ignoring empty filter string sourced from {source_name}");
427        false
428    } else {
429        true
430    }
431}
432fn validate_non_empty_filter(filter: &EnvFilter, source_name: &'static str) -> bool {
433    let filter_str = filter.to_string();
434    validate_non_empty_filter_str(&filter_str, source_name)
435}
436
437
438/// Initialize tracing
439///
440/// Note: service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
441///
442/// ---
443///
444/// # Examples
445///
446/// Basic usage
447///
448/// ```
449/// use tracing_kickstart::TracingConfig;
450///
451/// let attrs = tracing_kickstart::build_attrs!();
452/// let conf = TracingConfig::default();
453/// let tracing_providers = tracing_kickstart::init(
454///     attrs, &conf, None, None,
455/// ).unwrap();
456///
457/// tracing_providers.register_globally(); // optional
458///
459/// // do some work..
460///
461/// tracing_providers.shutdown();
462/// ```
463///
464/// Extra customization
465///
466/// ```
467/// use tracing_kickstart::TracingConfig;
468///
469/// let attrs = tracing_kickstart::build_attrs!();
470/// let conf = TracingConfig::default();
471///
472/// let custom_env_filter = Some("warn,example_app=debug");
473/// let extra_resource_attrs = Some(vec![
474///     ("region".into(), "canada".into()),
475/// ]);
476///
477/// let tracing_providers = tracing_kickstart::init(attrs, &conf, custom_env_filter, extra_resource_attrs).unwrap();
478///
479/// // Optionally register all configured providers globally
480/// tracing_providers.register_globally();
481///
482/// // do some work..
483///
484/// tracing_providers.shutdown();
485/// ```
486///
487/// ## `EnvFilter`
488///
489/// The EnvFilter is resolved using the first available from:
490/// - `TracingConfig::filter` (typically set from app config env var, e.g. `APP__TRACING__FILTER=app=warn`)
491/// - `RUST_LOG` env var
492/// - The `default_env_filter` parameter in this function (used to overide the default fallback)
493/// - default fallback (library defined, set to `"info,{crate_name}=debug`)
494/// ---
495/// Regardless of how the `EnvFilter` is resolved, all required filters for `console_subscriber` will be added
496/// **if the console_subscriber** feature flag is enabled.
497// if tracing config is none, otel providers won't be handled
498pub fn init(
499    service_attrs: ServiceAttributeStore,
500    config: &TracingConfig,
501    default_env_filter: Option<&str>,
502    custom_resource_attrs: Option<Vec<(Key, Value)>>,
503) -> Result<TraceProviders, ExporterBuildError> {
504    // resolve the env filter in the following priority
505    let base_filter: EnvFilter = {
506        // config env filter
507        if let Some(filter_str) = &config.filter && validate_non_empty_filter_str(filter_str, "provided config") {
508            println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
509            filter_str.into()
510        }
511        // `RUST LOG`
512        else if let Ok(filter) = EnvFilter::try_from_default_env() && validate_non_empty_filter(&filter, "RUST_LOG") {
513            println!("Resolved tracing EnvFilter from `RUST_LOG`: {:?}", filter.to_string());
514            filter
515        }
516        // function parameter (`default_env_filter`)
517        else if let Some(filter_str) = default_env_filter && validate_non_empty_filter_str(filter_str, "`tracing_kickstart::init()`") {
518            println!("Resolving tracing EnvFilter from `tracing_kickstart::init(.., default_env_filter)`: {filter_str:?}");
519            filter_str.into()
520        }
521        // library-defined fallback env filter
522        else {
523            let filter_str = format!(
524                "info,{}=debug",
525                service_attrs.crate_name
526            );
527            println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
528            filter_str.into()
529        }
530    };
531
532    // add env filters for tokio console subscriber (controlled by feature flag)
533    #[cfg(feature = "tokio_console")]
534    let registry_filter = EnvFilter::new(format!("{base_filter},tokio=trace,runtime=trace"));
535    #[cfg(not(feature = "tokio_console"))]
536    let registry_filter = base_filter.clone();
537
538    // print the resolved env filter
539    println!("Using base tracing filters: {base_filter}");
540    if registry_filter.to_string() != base_filter.to_string() {
541        println!("Registry tracing filter: {registry_filter}");
542    }
543
544    // build base layers
545    let layer = tracing_subscriber::registry()
546        .with(registry_filter)
547        .with(ErrorLayer::default());
548
549    // stdout layer
550    let layer = layer.with(
551        tracing_subscriber::fmt::layer()
552        .with_ansi(config.ansi_output)
553        .with_filter(base_filter.clone()) // use less permissive filter for stdout/logs
554    );
555
556    // conditionally add log file layer if path is provided in config
557    let file_logging_layer = config.log_file_path.as_ref().map(|path| {
558        let file = OpenOptions::new()
559        .write(true)
560        .create(true)
561        .truncate(true)
562        .open(path)
563        .expect("Log file should be writable");
564
565        tracing_subscriber::fmt::layer()
566        .with_ansi(false)
567        .with_writer(file)
568        .with_filter(base_filter)
569    });
570    let layer = layer.with(file_logging_layer);
571
572    // conditionally add tokio console layer
573    #[cfg(feature = "tokio_console")]
574    let layer = layer.with(console_subscriber::spawn());
575
576    // default has all 3 provider field options set to None
577    let mut providers_handle = TraceProviders::default();
578
579    // init open telemetry providers
580    if let Some(otel_config) = &config.otel_config {
581        println!("Initializing OTEL config");
582        let endpoint = &otel_config.collector_url;
583        let headers = build_otel_headers(&otel_config.collector_auth_header);
584        let custom_attrs = custom_resource_attrs
585            .unwrap_or_default()
586            .into_iter()
587            .map(|(k,v)| KeyValue::new(k, v))
588            .collect();
589        let resource = build_otel_resource(&service_attrs, config.deployment_env.clone(), custom_attrs);
590
591        // traces
592        let traces_provider = init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
593        // - add tracing layer for tracing/span -> otel/trace
594        // skipped when tokio console is enabled to prevent flooding
595        #[cfg(not(feature = "tokio_console"))]
596        let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)).with_level(true));
597        providers_handle.traces = Some(traces_provider);
598
599        // logs
600        let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
601        // - add tracing layer for tracing -> otel/logs
602        // skipped when tokio console is enabled to prevent flooding
603        #[cfg(not(feature = "tokio_console"))]
604        let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
605        providers_handle.logs = Some(logs_provider);
606
607        // metrics
608        let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource, config.metrics_interval_duration())?;
609        // - add layer for tracing events -> otel/metrics
610        let layer = layer.with(MetricsLayer::new(metrics_provider.clone()));
611        providers_handle.metrics = Some(metrics_provider);
612
613        layer.init();
614        println!("{:-<1$}", "-", 30);
615        tracing::info!("OTEL tracing configured");
616    } else {
617        layer.init();
618        println!("{:-<1$}", "-", 30);
619        tracing::warn!("OTEL tracing disabled");
620    }
621
622    Ok(providers_handle)
623}
624
625// ---- Struct for containing otel providers
626
627// TODO: alternatively use `opentelemetry::global::set_x_provider()` fns
628#[derive(Default, Clone)]
629pub struct TraceProviders {
630    pub traces: Option<SdkTracerProvider>,
631    pub logs: Option<SdkLoggerProvider>,
632    pub metrics: Option<SdkMeterProvider>,
633}
634impl TraceProviders {
635    /// Calls `opentelemetry::global::set_x_provider(..); for all configured providers, where applicable`
636    pub fn register_globally(&self) {
637        // register traces
638        if let Some(provider) = &self.traces {
639            tracing::info!("Traces provider registered globally");
640            opentelemetry::global::set_tracer_provider(provider.clone());
641        }
642        // register metrics
643        if let Some(provider) = &self.metrics {
644            tracing::info!("Metrics provider registered globally");
645            opentelemetry::global::set_meter_provider(provider.clone());
646        }
647    }
648
649    /// Triggers shutdown for each provider that has been set
650    pub fn shutdown(self) {
651        // shutdown traces
652        if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
653            println!("error shutting down traces provider: {error}");
654        }
655        // shutdown logs
656        if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
657            println!("error shutting down logs provider: {error}");
658        }
659        // shutdown metrics
660        if let Some(provider) = self.metrics && let Err(error) = provider.shutdown() {
661            println!("error shutting down metrics provider: {error}");
662        }
663    }
664}
665impl fmt::Debug for TraceProviders {
666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667        write!(f, "TraceProviders (")?;
668        let mut i = 0;
669        if self.traces.is_some() {
670            write!(f, "SdkTracerProvider")?;
671            i += 1;
672        }
673        if self.logs.is_some() {
674            if i > 0 {
675                write!(f, ", ")?;
676            }
677            write!(f, "SdkLoggerProvider")?;
678            i += 1;
679        }
680        if self.metrics.is_some() {
681            if i > 0 {
682                write!(f, ", ")?;
683            }
684            write!(f, "SdkMeterProvider")?;
685        }
686        write!(f, ")")
687    }
688}