Skip to main content

tracing_kickstart/
trace.rs

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