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