Skip to main content

snix_tracing/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3#[cfg(feature = "clap")]
4use clap_verbosity_flag::{InfoLevel, LogLevel, Verbosity};
5#[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
6use enumset::EnumSet;
7use std::sync::LazyLock;
8use tracing::Level;
9use tracing_indicatif::{
10    IndicatifLayer, IndicatifWriter, filter::IndicatifFilter, style::ProgressStyle,
11    util::FilteredFormatFields, writer,
12};
13use tracing_subscriber::{
14    EnvFilter, Layer, Registry,
15    layer::{Identity, SubscriberExt},
16    util::SubscriberInitExt as _,
17};
18
19#[cfg(feature = "otlp")]
20use opentelemetry_sdk::{
21    Resource, propagation::TraceContextPropagator, resource::SdkProvidedResourceDetector,
22};
23#[cfg(feature = "tracy")]
24use tracing_tracy::TracyLayer;
25
26pub mod propagate;
27
28/// A classical progress bar.
29pub static PB_PROGRESS_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
30    ProgressStyle::with_template(
31        "{span_child_prefix} {wide_msg} {bar:10} ({elapsed}) {pos:>7}/{len:7}",
32    )
33    .expect("invalid progress template")
34});
35
36/// Used for file transfers, where we know an exact number of bytes and showing a transfer speed makes sense.
37pub static PB_TRANSFER_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
38    ProgressStyle::with_template(
39        "{span_child_prefix} {wide_msg} {binary_bytes:>7}/{binary_total_bytes:7}@{decimal_bytes_per_sec} ({elapsed}) {bar:10} "
40    )
41    .expect("invalid progress template")
42});
43pub static PB_SPINNER_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
44    ProgressStyle::with_template(
45        "{span_child_prefix}{spinner} {wide_msg} ({elapsed}) {pos:>7}/{len:7}",
46    )
47    .expect("invalid progress template")
48});
49
50/// Used for long-running operations without a known total.
51pub static PB_SPINNER_LONG_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
52    ProgressStyle::with_template("{span_child_prefix}{spinner} {wide_msg} ({elapsed}) {pos:>7}/?")
53        .expect("invalid progress template")
54});
55
56#[derive(thiserror::Error, Debug)]
57pub enum Error {
58    #[error(transparent)]
59    Init(#[from] tracing_subscriber::util::TryInitError),
60
61    #[cfg(feature = "otlp")]
62    #[error(transparent)]
63    OTEL(#[from] opentelemetry_sdk::error::OTelSdkError),
64}
65
66#[derive(Clone)]
67pub struct TracingHandle {
68    stdout_writer: IndicatifWriter<writer::Stdout>,
69    stderr_writer: IndicatifWriter<writer::Stderr>,
70
71    #[cfg(feature = "chrome")]
72    #[allow(dead_code)]
73    chrome_guard: Option<std::rc::Rc<tracing_chrome::FlushGuard>>,
74
75    #[cfg(feature = "otlp")]
76    meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
77
78    #[cfg(feature = "otlp")]
79    tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
80}
81
82impl TracingHandle {
83    /// Returns a writer for [std::io::Stdout] that ensures its output will not be clobbered by
84    /// active progress bars.
85    ///
86    /// Instead of `println!(...)` prefer `writeln!(handle.get_stdout_writer(), ...)`
87    pub fn get_stdout_writer(&self) -> IndicatifWriter<writer::Stdout> {
88        // clone is fine here because its only a wrapper over an `Arc`
89        self.stdout_writer.clone()
90    }
91
92    /// Returns a writer for [std::io::Stderr] that ensures its output will not be clobbered by
93    /// active progress bars.
94    ///
95    /// Instead of `println!(...)` prefer `writeln!(handle.get_stderr_writer(), ...)`.
96    pub fn get_stderr_writer(&self) -> IndicatifWriter<writer::Stderr> {
97        // clone is fine here because its only a wrapper over an `Arc`
98        self.stderr_writer.clone()
99    }
100
101    /// This will flush possible attached tracing providers, e.g. otlp exported, if enabled.
102    /// If there is none enabled this will result in a noop.
103    ///
104    /// It will wait until the flush is complete.
105    pub async fn flush(&self) -> Result<(), Error> {
106        #[cfg(feature = "otlp")]
107        {
108            if let Some(tracer_provider) = &self.tracer_provider {
109                tracer_provider.force_flush()?;
110            }
111            if let Some(meter_provider) = &self.meter_provider {
112                meter_provider.force_flush()?;
113            }
114        }
115        Ok(())
116    }
117
118    /// This will flush all attached tracing providers and will wait until the flush is completed, then call shutdown.
119    /// If no tracing providers like otlp are attached then this will be a noop.
120    ///
121    /// This should only be called on a regular shutdown.
122    pub async fn shutdown(&mut self) -> Result<(), Error> {
123        self.flush().await?;
124        #[cfg(feature = "otlp")]
125        {
126            use tokio::task::spawn_blocking;
127            if let Some(tracer_provider) = self.tracer_provider.take() {
128                spawn_blocking(move || tracer_provider.shutdown())
129                    .await
130                    .map_err(|err| {
131                        Error::OTEL(opentelemetry_sdk::error::OTelSdkError::InternalFailure(
132                            err.to_string(),
133                        ))
134                    })??;
135            }
136            if let Some(meter_provider) = self.meter_provider.take() {
137                spawn_blocking(move || meter_provider.shutdown())
138                    .await
139                    .map_err(|err| {
140                        Error::OTEL(opentelemetry_sdk::error::OTelSdkError::InternalFailure(
141                            err.to_string(),
142                        ))
143                    })??;
144            }
145        }
146        #[cfg(feature = "tracy")]
147        {
148            if tracing_tracy::client::Client::is_running() {
149                unsafe { tracing_tracy::client::sys::___tracy_shutdown_profiler() }
150            }
151        }
152
153        Ok(())
154    }
155}
156
157#[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
158#[derive(enumset::EnumSetType, Debug)]
159#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
160pub enum Tracer {
161    #[cfg(feature = "otlp")]
162    Otlp,
163    #[cfg(feature = "tracy")]
164    Tracy,
165    #[cfg(feature = "chrome")]
166    ChromeStyle,
167}
168
169#[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
170impl Tracer {
171    /// Return the tracer kind as a str
172    pub fn as_str(&self) -> &'static str {
173        match self {
174            #[cfg(feature = "otlp")]
175            Tracer::Otlp => "otlp",
176            #[cfg(feature = "tracy")]
177            Tracer::Tracy => "tracy",
178            #[cfg(feature = "chrome")]
179            Tracer::ChromeStyle => "chrome-style",
180        }
181    }
182}
183
184#[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
185impl std::fmt::Display for Tracer {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        f.write_str(self.as_str())
188    }
189}
190
191/// Encodes the verbosity level chosen by the user through CLI arguments.
192#[derive(Clone, Debug, PartialEq, Eq)]
193enum ChosenLevel {
194    /// Not set. We still store the default level passed as a type argument in Verbosity
195    Unset(Level),
196    /// No output at all requested (quiet mode)
197    NoOutput,
198    /// Specific log level selected
199    Level(Level),
200}
201
202#[must_use = "Don't forget to call build() to enable tracing."]
203pub struct TracingBuilder {
204    // Can be used to disable progress bars entirely,
205    // even though they would still match the chosen level
206    disable_progress_bars: bool,
207
208    #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
209    tracers: EnumSet<Tracer>,
210
211    // The desired verbosity level
212    level: ChosenLevel,
213}
214
215impl Default for TracingBuilder {
216    fn default() -> Self {
217        Self {
218            #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
219            tracers: Default::default(),
220            level: ChosenLevel::Unset(Level::INFO),
221            disable_progress_bars: false,
222        }
223    }
224}
225
226impl TracingBuilder {
227    #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
228    /// Enable the given tracer
229    pub fn enable_tracer(mut self, tracer: Tracer) -> TracingBuilder {
230        self.tracers.insert(tracer);
231        self
232    }
233
234    #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
235    /// Enable the given tracers
236    pub fn enable_tracers<I>(mut self, tracers: I) -> TracingBuilder
237    where
238        I: IntoIterator<Item = Tracer>,
239    {
240        self.tracers.extend(tracers);
241        self
242    }
243
244    /// Disable progress bars explicitly, even though they would still match the chosen log level.
245    pub fn disable_progress_bars(mut self) -> TracingBuilder {
246        self.disable_progress_bars = true;
247        self
248    }
249
250    /// This will setup tracing based on the configuration passed in.
251    /// It will setup a stderr writer output layer and configure EnvFilter to honor RUST_LOG.
252    /// The EnvFilter will be applied to all configured layers, also otlp.
253    ///
254    /// It will also configure otlp if the feature is enabled and a service_name was provided. It
255    /// will then correctly setup a channel which is later used for flushing the provider.
256    pub fn build(self) -> Result<TracingHandle, Error> {
257        self.build_with_additional(Identity::new())
258    }
259
260    /// Similar to `build()` but allows passing in an additional tracing [`Layer`].
261    ///
262    /// This method is generic over the `Layer` to avoid the runtime cost of dynamic dispatch.
263    /// While it only allows passing a single `Layer`, it can be composed of multiple ones:
264    ///
265    /// ```ignore
266    /// build_with_additional(
267    ///   fmt::layer()
268    ///     .and_then(some_other_layer)
269    ///     .and_then(yet_another_layer)
270    ///     .with_filter(my_filter)
271    /// )
272    /// ```
273    /// [`Layer`]: tracing_subscriber::layer::Layer
274    pub fn build_with_additional<L>(self, additional_layer: L) -> Result<TracingHandle, Error>
275    where
276        L: Layer<Registry> + Send + Sync + 'static,
277    {
278        // Set up the tracing subscriber.
279        let indicatif_layer = IndicatifLayer::new().with_progress_style(PB_SPINNER_STYLE.clone());
280        let stdout_writer = indicatif_layer.get_stdout_writer();
281        let stderr_writer = indicatif_layer.get_stderr_writer();
282
283        let layered = tracing_subscriber::fmt::Layer::new()
284            .fmt_fields(FilteredFormatFields::new(
285                tracing_subscriber::fmt::format::DefaultFields::new(),
286                |field| field.name() != "indicatif.pb_show",
287            ))
288            .with_writer(indicatif_layer.get_stderr_writer())
289            .compact()
290            .with_filter(construct_filter(self.level.to_owned()))
291            .and_then((!self.disable_progress_bars).then(|| {
292                indicatif_layer.with_filter(
293                    // only show progress for spans with indicatif.pb_show field being set
294                    IndicatifFilter::new(false),
295                )
296            }));
297
298        #[cfg(feature = "chrome")]
299        let (layered, chrome_guard) = if self.tracers.contains(Tracer::ChromeStyle) {
300            let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
301                .include_args(true)
302                .trace_style(tracing_chrome::TraceStyle::Async)
303                .build();
304            (
305                Layer::and_then(layered, Some(chrome_layer)),
306                Some(std::rc::Rc::new(guard)),
307            )
308        } else {
309            (Layer::and_then(layered, None), None)
310        };
311
312        #[cfg(feature = "otlp")]
313        let mut g_tracer_provider = None;
314        #[cfg(feature = "otlp")]
315        let mut g_meter_provider = None;
316
317        // Setup otlp if a service_name is configured
318        #[cfg(feature = "otlp")]
319        let layered = Layer::and_then(layered, {
320            self.tracers.contains(Tracer::Otlp).then(|| {
321                use opentelemetry::trace::TracerProvider;
322
323                // register a text map propagator for trace propagation
324                opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
325
326                let tracer_provider =
327                    gen_tracer_provider().expect("Unable to configure trace provider");
328
329                let meter_provider =
330                    gen_meter_provider().expect("Unable to configure meter provider");
331
332                // Register the returned meter provider as the global one.
333                // FUTUREWORK: store in the struct and provide getter too?
334                opentelemetry::global::set_meter_provider(meter_provider.clone());
335
336                g_tracer_provider = Some(tracer_provider.clone());
337                g_meter_provider = Some(meter_provider);
338
339                // Create a tracing layer with the configured tracer
340                tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("snix"))
341            })
342        });
343
344        #[cfg(feature = "tracy")]
345        let layered = Layer::and_then(
346            layered,
347            self.tracers.contains(Tracer::Tracy).then(|| {
348                let _client = tracing_tracy::client::Client::start();
349                TracyLayer::default()
350            }),
351        );
352
353        tracing_subscriber::registry()
354            // TODO: if additional_layer has global filters, there is a risk that it will disable the "default" ones,
355            // while it could be solved by registering `additional_layer` last, it requires boxing `additional_layer`.
356            .with(additional_layer)
357            .with(layered)
358            .try_init()?;
359
360        #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
361        if !self.tracers.is_empty() {
362            let tracers = std::fmt::from_fn(|f| {
363                for (idx, tracer) in self.tracers.iter().enumerate() {
364                    if idx > 0 {
365                        f.write_str(",")?;
366                    }
367                    write!(f, "{tracer}")?;
368                }
369                Ok(())
370            });
371            tracing::debug!(%tracers, "started tracing");
372        }
373        Ok(TracingHandle {
374            stdout_writer,
375            stderr_writer,
376
377            #[cfg(feature = "otlp")]
378            meter_provider: g_meter_provider,
379            #[cfg(feature = "otlp")]
380            tracer_provider: g_tracer_provider,
381            #[cfg(feature = "chrome")]
382            chrome_guard,
383        })
384    }
385
386    #[cfg(feature = "clap")]
387    /// Configure with verbosity flags.
388    pub fn handle_verbosity_flags<L: LogLevel>(mut self, args: &Verbosity<L>) -> Self {
389        if args.is_silent() {
390            self.level = ChosenLevel::NoOutput;
391            self.disable_progress_bars = true;
392            return self;
393        }
394
395        use std::io::IsTerminal;
396        if !std::io::stderr().is_terminal() {
397            self.disable_progress_bars = true
398        }
399
400        if args.is_present() {
401            self.level = ChosenLevel::Level(args.tracing_level().expect("not silent"));
402        } else {
403            self.level = ChosenLevel::Unset(args.tracing_level().expect("not silent"))
404        }
405
406        self
407    }
408
409    #[cfg(feature = "clap")]
410    /// Configure with the tracing-related args.
411    pub fn handle_tracing_args<L: LogLevel>(
412        #[allow(unused_mut)] mut self,
413        args: &TracingArgs<L>,
414    ) -> Self {
415        #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
416        {
417            self = self.enable_tracers(args.tracers());
418        }
419
420        self.handle_verbosity_flags(&args.verbosity)
421    }
422}
423
424#[cfg(feature = "otlp")]
425fn gen_resources() -> Resource {
426    // use SdkProvidedResourceDetector.detect to detect resources.
427    Resource::builder()
428        .with_detector(Box::new(SdkProvidedResourceDetector))
429        .build()
430}
431
432/// Returns an OTLP tracer, and the TX part of a channel, which can be used
433/// to request flushes (and signal back the completion of the flush).
434#[cfg(feature = "otlp")]
435fn gen_tracer_provider()
436-> Result<opentelemetry_sdk::trace::SdkTracerProvider, opentelemetry_otlp::ExporterBuildError> {
437    use opentelemetry_otlp::{ExportConfig, SpanExporter, WithExportConfig};
438
439    let exporter = SpanExporter::builder()
440        .with_tonic()
441        .with_export_config(ExportConfig::default())
442        .build()?;
443
444    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
445        .with_batch_exporter(exporter)
446        .with_resource(gen_resources())
447        .build();
448    // Unclear how to configure this
449    // let batch_config = BatchConfigBuilder::default()
450    //     // the default values for `max_export_batch_size` is set to 512, which we will fill
451    //     // pretty quickly, which will then result in an export. We want to make sure that
452    //     // the export is only done once the schedule is met and not as soon as 512 spans
453    //     // are collected.
454    //     .with_max_export_batch_size(4096)
455    //     // analog to default config `max_export_batch_size * 4`
456    //     .with_max_queue_size(4096 * 4)
457    //     // only force an export to the otlp collector every 10 seconds to reduce the amount
458    //     // of error messages if an otlp collector is not available
459    //     .with_scheduled_delay(std::time::Duration::from_secs(10))
460    //     .build();
461
462    // use opentelemetry_sdk::trace::BatchSpanProcessor;
463    // let batch_span_processor = BatchSpanProcessor::builder(exporter, runtime::Tokio)
464    //     .with_batch_config(batch_config)
465    //     .build();
466
467    Ok(tracer_provider)
468}
469
470// Metric export interval should be less than or equal to 15s
471// if the metrics may be converted to Prometheus metrics.
472// Prometheus' query engine and compatible implementations
473// require ~4 data points / interval for range queries,
474// so queries ranging over 1m requre <= 15s scrape intervals.
475// OTEL SDKS also respect the env var `OTEL_METRIC_EXPORT_INTERVAL` (no underscore prefix).
476const _OTEL_METRIC_EXPORT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
477
478#[cfg(feature = "otlp")]
479fn gen_meter_provider()
480-> Result<opentelemetry_sdk::metrics::SdkMeterProvider, opentelemetry_otlp::ExporterBuildError> {
481    use std::time::Duration;
482
483    use opentelemetry_otlp::WithExportConfig;
484    use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
485    let exporter = opentelemetry_otlp::MetricExporter::builder()
486        .with_tonic()
487        .with_timeout(Duration::from_secs(10))
488        .build()?;
489
490    let reader = PeriodicReader::builder(exporter)
491        .with_interval(_OTEL_METRIC_EXPORT_INTERVAL)
492        .build();
493
494    Ok(SdkMeterProvider::builder()
495        .with_reader(reader)
496        .with_resource(gen_resources())
497        .build())
498}
499
500/// A `TypedValueParser` for `EnumSet<Tracer>` that parses either a single tracer or
501/// an empty string.
502///
503/// This will always return either a single element set or an empty set depending
504/// on the input string either being the name of a tracer or empty.
505#[cfg(all(
506    feature = "clap",
507    any(feature = "otlp", feature = "tracy", feature = "chrome")
508))]
509#[derive(Clone, Debug, Default)]
510struct TracersValueParser(clap::builder::EnumValueParser<Tracer>);
511#[cfg(all(
512    feature = "clap",
513    any(feature = "otlp", feature = "tracy", feature = "chrome")
514))]
515impl clap::builder::TypedValueParser for TracersValueParser {
516    type Value = EnumSet<Tracer>;
517
518    fn parse_ref(
519        &self,
520        cmd: &clap::Command,
521        arg: Option<&clap::Arg>,
522        value: &std::ffi::OsStr,
523    ) -> Result<Self::Value, clap::Error> {
524        if value.is_empty() {
525            return Ok(EnumSet::empty());
526        }
527        self.0.parse_ref(cmd, arg, value).map(EnumSet::only)
528    }
529
530    fn possible_values(
531        &self,
532    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
533        self.0.possible_values()
534    }
535}
536
537#[cfg(feature = "clap")]
538#[derive(clap::Parser, Clone)]
539pub struct TracingArgs<L: LogLevel = InfoLevel> {
540    #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
541    /// Which tracers to enable.
542    #[arg(long, action(clap::ArgAction::Append), env, value_parser=TracersValueParser::default(), value_delimiter=',')]
543    tracer: Vec<EnumSet<Tracer>>,
544
545    #[clap(flatten)]
546    verbosity: Verbosity<L>,
547}
548
549#[cfg(feature = "clap")]
550impl<L: LogLevel> TracingArgs<L> {
551    #[cfg(any(feature = "otlp", feature = "tracy", feature = "chrome"))]
552    pub fn tracers(&self) -> EnumSet<Tracer> {
553        self.tracer
554            .iter()
555            .cloned()
556            .fold(EnumSet::empty(), |ret, next| ret.union(next))
557    }
558}
559
560/// Helper assembling a filter filtering events for the [ChosenLevel].
561fn construct_filter<S>(level: ChosenLevel) -> impl tracing_subscriber::layer::Filter<S> {
562    let mut b = EnvFilter::builder();
563    if let ChosenLevel::Unset(level) = level {
564        b = b.with_default_directive(level.to_owned().into());
565    }
566    let mut f = b.from_env().expect("invalid RUST_LOG");
567    if let ChosenLevel::Level(level) = level {
568        f = f.add_directive(level.to_owned().into());
569    }
570    f
571}