stackable_telemetry/tracing/
mod.rs

1//! This module contains functionality to initialize tracing Subscribers for
2//! console output, file output, and OpenTelemetry OTLP export for traces and logs.
3//!
4//! It is intended to be used by the Stackable Data Platform operators and
5//! webhooks, but it should be generic enough to be used in any application.
6//!
7//! To get started, see [`Tracing`].
8
9use std::{io::IsTerminal, ops::Not, path::PathBuf};
10
11use opentelemetry::trace::TracerProvider;
12use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
13use opentelemetry_otlp::{ExporterBuildError, LogExporter, SpanExporter};
14use opentelemetry_sdk::{
15    Resource, logs::SdkLoggerProvider, propagation::TraceContextPropagator,
16    trace::SdkTracerProvider,
17};
18use snafu::{ResultExt as _, Snafu};
19use tracing::{level_filters::LevelFilter, subscriber::SetGlobalDefaultError};
20use tracing_appender::rolling::{InitError, RollingFileAppender};
21use tracing_subscriber::{EnvFilter, Layer, Registry, filter::Directive, layer::SubscriberExt};
22
23use crate::tracing::settings::*;
24
25pub mod settings;
26
27type Result<T, E = Error> = std::result::Result<T, E>;
28
29/// Errors which can be encountered when initialising [`Tracing`].
30#[derive(Debug, Snafu)]
31pub enum Error {
32    /// Indicates that [`Tracing`] failed to install the OpenTelemetry trace exporter.
33    #[snafu(display("unable to install opentelemetry trace exporter"))]
34    InstallOtelTraceExporter {
35        #[allow(missing_docs)]
36        source: ExporterBuildError,
37    },
38
39    /// Indicates that [`Tracing`] failed to install the OpenTelemetry log exporter.
40    #[snafu(display("unable to install opentelemetry log exporter"))]
41    InstallOtelLogExporter {
42        #[allow(missing_docs)]
43        source: ExporterBuildError,
44    },
45
46    /// Indicates that [`Tracing`] failed to install the rolling file appender.
47    #[snafu(display("failed to initialize rolling file appender"))]
48    InitRollingFileAppender {
49        #[allow(missing_docs)]
50        source: InitError,
51    },
52
53    /// Indicates that [`Tracing`] failed to set the global default subscriber.
54    #[snafu(display("unable to set the global default subscriber"))]
55    SetGlobalDefaultSubscriber {
56        #[allow(missing_docs)]
57        source: SetGlobalDefaultError,
58    },
59}
60
61#[rustfmt::skip]
62/// Easily initialize a set of pre-configured [`Subscriber`][1] layers.
63///
64/// # Usage
65///
66/// ## Tracing Guard
67///
68/// The configured subscribers are active as long as the tracing guard returned by [`Tracing::init`]
69/// is in scope and not dropped. Dropping it results in subscribers being shut down, which can lead
70/// to loss of telemetry data when done before exiting the application. This is why it is important
71/// to hold onto the guard as long as required.
72///
73/// <div class="warning">
74///
75/// Name the guard variable appropriately, do not just use `let _ = ...`, as that will drop
76/// immediately.
77///
78/// </div>
79///
80/// ```
81/// # use stackable_telemetry::tracing::{Tracing, Error};
82/// #[tokio::main]
83/// async fn main() -> Result<(), Error> {
84///     let _tracing_guard = Tracing::builder() // < Scope starts here
85///         .service_name("test")               // |
86///         .build()                            // |
87///         .init()?;                           // |
88///                                             // |
89///     tracing::info!("log a message");        // |
90///     Ok(())                                  // < Scope ends here, guard is dropped
91/// }
92/// ```
93///
94/// ## Pre-configured Tracing Instance
95///
96/// There are two different styles to configure a [`Tracing`] instance: Using an opinionated pre-
97/// configured instance or a fully customizable builder. The first option should be suited for
98/// pretty much all operators by using sane defaults and applying best practices out-of-the-box.
99/// [`Tracing::pre_configured`] lists details about environment variables, filter levels and
100/// defaults used.
101///
102/// ```
103/// use stackable_telemetry::tracing::{Error, TelemetryOptions, Tracing};
104///
105/// #[tokio::main]
106/// async fn main() -> Result<(), Error> {
107///     let options = TelemetryOptions {
108///         console_log_disabled: false,
109///         console_log_format: Default::default(),
110///         file_log_directory: None,
111///         file_log_rotation_period: None,
112///         file_log_max_files: Some(6),
113///         otel_trace_exporter_enabled: true,
114///         otel_log_exporter_enabled: true,
115///     };
116///
117///     let _tracing_guard = Tracing::pre_configured("test", options).init()?;
118///
119///     tracing::info!("log a message");
120///
121///     Ok(())
122/// }
123/// ```
124///
125/// Also see the documentation for [`TelemetryOptions`] which details how it can be used as CLI
126/// arguments via `clap`. Additionally see [this section](#environment-variables-and-cli-arguments)
127/// in the docs for a full list of environment variables and CLI arguments used by the pre-configured
128/// instance.
129///
130/// ## Builders
131///
132/// When choosing the builder, there are two different styles to configure individual subscribers:
133/// Using the sophisticated [`SettingsBuilder`] or the simplified tuple style for basic
134/// configuration. Currently, three different subscribers are supported: console output, OTLP log
135/// export, and OTLP trace export.
136///
137/// ### Basic Configuration
138///
139/// A basic configuration of subscribers can be done by using 2-tuples or 3-tuples, also called
140/// doubles and triples. Using tuples, the subscriber can be enabled/disabled and it's environment
141/// variable and default level can be set.
142///
143/// ```
144/// use stackable_telemetry::tracing::{Error, Tracing, settings::Settings};
145/// use tracing_subscriber::filter::LevelFilter;
146///
147/// #[tokio::main]
148/// async fn main() -> Result<(), Error> {
149///     // This can come from a `clap` argument for example. The enabled builder
150///     // function below allows enabling/disabling certain subscribers during
151///     // runtime.
152///     let otlp_log_flag = false;
153///
154///     let _tracing_guard = Tracing::builder()
155///         .service_name("test")
156///         .with_console_output(("TEST_CONSOLE", LevelFilter::INFO))
157///         .with_otlp_log_exporter(("TEST_OTLP_LOG", LevelFilter::DEBUG, otlp_log_flag))
158///         .build()
159///         .init()?;
160///
161///     tracing::info!("log a message");
162///
163///     Ok(())
164/// }
165/// ```
166///
167/// ### Advanced Configuration
168///
169/// More advanced configurations can be done via the [`Settings::builder`] function. Each
170/// subscriber provides specific settings based on a common set of options. These options can be
171/// customized via the following methods:
172///
173/// - [`SettingsBuilder::console_log_settings_builder`]
174/// - [`SettingsBuilder::otlp_log_settings_builder`]
175/// - [`SettingsBuilder::otlp_trace_settings_builder`]
176///
177/// ```
178/// # use stackable_telemetry::tracing::{Tracing, Error, settings::Settings};
179/// # use tracing_subscriber::filter::LevelFilter;
180/// #[tokio::main]
181/// async fn main() -> Result<(), Error> {
182///     // Control the otlp_log subscriber at runtime
183///     let otlp_log_flag = false;
184///
185///     let _tracing_guard = Tracing::builder()
186///         .service_name("test")
187///         .with_console_output(
188///             Settings::builder()
189///                 .with_environment_variable("CONSOLE_LOG")
190///                 .with_default_level(LevelFilter::INFO)
191///                 .build(),
192///         )
193///         .with_file_output(
194///             Settings::builder()
195///                 .with_environment_variable("FILE_LOG")
196///                 .with_default_level(LevelFilter::INFO)
197///                 .file_log_settings_builder("/tmp/logs", "operator.log")
198///                 .build(),
199///         )
200///         .with_otlp_log_exporter(otlp_log_flag.then(|| {
201///             Settings::builder()
202///                 .with_environment_variable("OTLP_LOG")
203///                 .with_default_level(LevelFilter::DEBUG)
204///                 .build()
205///         }))
206///         .with_otlp_trace_exporter(
207///             Settings::builder()
208///                 .with_environment_variable("OTLP_TRACE")
209///                 .with_default_level(LevelFilter::TRACE)
210///                 .build(),
211///         )
212///         .build()
213///         .init()?;
214///
215///     tracing::info!("log a message");
216///
217///     Ok(())
218/// }
219/// ```
220///
221/// ## Environment Variables and CLI Arguments
222///
223/// <div class="warning">
224///
225/// It should be noted that the CLI arguments (listed in parentheses) are only available when the
226/// `clap` feature is enabled.
227///
228/// </div>
229///
230/// ### Console logs
231///
232/// - `CONSOLE_LOG_DISABLED` (`--console-log-disabled`): Disables console logs when set to `true`.
233/// - `CONSOLE_LOG_FORMAT` (`--console-log-format`): Set the format for the console logs.
234/// - `CONSOLE_LOG_LEVEL`: Set the log level for the console logs.
235///
236/// ### File logs
237///
238/// - `FILE_LOG_DIRECTORY` (`--file-log-directory`): Enable the file logs and set the file log directory.
239/// - `FILE_LOG_ROTATION_PERIOD` (`--file-log-rotation-period`): Set the rotation period of log files.
240/// - `FILE_LOG_MAX_FILES` (`--file-log-max-files`): Set the maximum number of log files to keep.
241/// - `FILE_LOG_LEVEL`: Set the log level for file logs.
242///
243/// ### OTEL logs
244///
245/// - `OTEL_LOG_EXPORTER_ENABLED` (`--otel-log-exporter-enabled`): Enable exporting OTEL logs.
246/// - `OTEL_LOG_EXPORTER_LEVEL`: Set the log level for OTEL logs.
247///
248/// ### OTEL traces
249///
250/// - `OTEL_TRACE_EXPORTER_ENABLED` (`--otel-trace-exporter-enabled`): Enable exporting OTEL traces.
251/// - `OTEL_TRACE_EXPORTER_LEVEL`: Set the log level for OTEL traces.
252///
253/// # Additional Configuration
254///
255/// You can configure the OTLP trace and log exports through the variables defined in the opentelemetry crates:
256///
257/// - `OTEL_EXPORTER_OTLP_COMPRESSION` (defaults to none, but can be set to `gzip`).
258/// - `OTEL_EXPORTER_OTLP_ENDPOINT` (defaults to `http://localhost:4317`, with the `grpc-tonic` feature (default)).
259/// - `OTEL_EXPORTER_OTLP_TIMEOUT`
260/// - `OTEL_EXPORTER_OTLP_HEADERS`
261///
262/// _See the defaults in the [opentelemetry-otlp][2] crate._
263///
264/// ## Tracing exporter overrides
265///
266/// OTLP Exporter settings:
267///
268/// - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
269/// - `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`
270/// - `OTEL_EXPORTER_OTLP_TRACES_COMPRESSION`
271/// - `OTEL_EXPORTER_OTLP_TRACES_HEADERS`
272///
273/// General Span and Trace settings:
274///
275/// - `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT`
276/// - `OTEL_SPAN_EVENT_COUNT_LIMIT`
277/// - `OTEL_SPAN_LINK_COUNT_LIMIT`
278/// - `OTEL_TRACES_SAMPLER` (Defaults to `parentbased_always_on`. If "traceidratio" or "parentbased_traceidratio", then `OTEL_TRACES_SAMPLER_ARG`)
279///
280/// Batch Span Processor settings:
281///
282/// - `OTEL_BSP_MAX_QUEUE_SIZE`
283/// - `OTEL_BSP_SCHEDULE_DELAY`
284/// - `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
285/// - `OTEL_BSP_EXPORT_TIMEOUT`
286/// - `OTEL_BSP_MAX_CONCURRENT_EXPORTS`
287///
288/// _See defaults in the opentelemetry_sdk crate under [trace::config][3] and [trace::span_processor][4]._
289///
290/// ## Log exporter overrides
291///
292/// OTLP exporter settings:
293///
294/// - `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION`
295/// - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`
296/// - `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT`
297/// - `OTEL_EXPORTER_OTLP_LOGS_HEADERS`
298///
299/// Batch Log Record Processor settings:
300///
301/// - `OTEL_BLRP_MAX_QUEUE_SIZE`
302/// - `OTEL_BLRP_SCHEDULE_DELAY`
303/// - `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`
304/// - `OTEL_BLRP_EXPORT_TIMEOUT`
305///
306/// _See defaults in the opentelemetry_sdk crate under [log::log_processor][5]._
307///
308/// [1]: tracing::Subscriber
309/// [2]: https://docs.rs/opentelemetry-otlp/latest/src/opentelemetry_otlp/exporter/mod.rs.html
310/// [3]: https://docs.rs/opentelemetry_sdk/latest/src/opentelemetry_sdk/trace/config.rs.html
311/// [4]: https://docs.rs/opentelemetry_sdk/latest/src/opentelemetry_sdk/trace/span_processor.rs.html
312/// [5]: https://docs.rs/opentelemetry_sdk/latest/src/opentelemetry_sdk/logs/log_processor.rs.html
313pub struct Tracing {
314    service_name: &'static str,
315    console_log_settings: ConsoleLogSettings,
316    file_log_settings: FileLogSettings,
317    otlp_log_settings: OtlpLogSettings,
318    otlp_trace_settings: OtlpTraceSettings,
319
320    logger_provider: Option<SdkLoggerProvider>,
321    tracer_provider: Option<SdkTracerProvider>,
322}
323
324impl Tracing {
325    /// The environment variable used to set the console log level filter.
326    pub const CONSOLE_LOG_LEVEL_ENV: &str = "CONSOLE_LOG_LEVEL";
327    /// The environment variable used to set the rolling file log level filter.
328    pub const FILE_LOG_LEVEL_ENV: &str = "FILE_LOG_LEVEL";
329    /// The filename used for the rolling file logs.
330    pub const FILE_LOG_SUFFIX: &str = "tracing-rs.json";
331    /// The environment variable used to set the OTEL log level filter.
332    pub const OTEL_LOG_EXPORTER_LEVEL_ENV: &str = "OTEL_LOG_EXPORTER_LEVEL";
333    /// The environment variable used to set the OTEL trace level filter.
334    pub const OTEL_TRACE_EXPORTER_LEVEL_ENV: &str = "OTEL_TRACE_EXPORTER_LEVEL";
335
336    /// Creates and returns a [`TracingBuilder`].
337    pub fn builder() -> TracingBuilder<builder_state::PreServiceName> {
338        TracingBuilder::default()
339    }
340
341    /// Creates an returns a pre-configured [`Tracing`] instance which can be initialized by
342    /// calling [`Tracing::init()`].
343    ///
344    /// Also see [this section](#environment-variables-and-cli-arguments) in the docs for all full
345    /// list of environment variables and CLI arguments used by the pre-configured instance.
346    ///
347    /// ### Default Levels
348    ///
349    /// - Console logs: INFO
350    /// - File logs: INFO
351    /// - OTEL logs: INFO
352    /// - OTEL traces: INFO
353    ///
354    /// ### Default Values
355    ///
356    /// - If `rolling_logs_period` is [`None`], this function will use a default value of
357    ///   [`RotationPeriod::Never`].
358    pub fn pre_configured(service_name: &'static str, options: TelemetryOptions) -> Self {
359        let TelemetryOptions {
360            console_log_disabled,
361            console_log_format,
362            file_log_directory,
363            file_log_rotation_period,
364            file_log_max_files,
365            otel_trace_exporter_enabled,
366            otel_log_exporter_enabled,
367        } = options;
368
369        let file_log_rotation_period = file_log_rotation_period.unwrap_or_default();
370
371        Self::builder()
372            .service_name(service_name)
373            .with_console_output(console_log_disabled.not().then(|| {
374                Settings::builder()
375                    .with_environment_variable(Self::CONSOLE_LOG_LEVEL_ENV)
376                    .with_default_level(LevelFilter::INFO)
377                    .console_log_settings_builder()
378                    .with_log_format(console_log_format)
379                    .build()
380            }))
381            .with_file_output(file_log_directory.map(|log_directory| {
382                Settings::builder()
383                    .with_environment_variable(Self::FILE_LOG_LEVEL_ENV)
384                    .with_default_level(LevelFilter::INFO)
385                    .file_log_settings_builder(log_directory, Self::FILE_LOG_SUFFIX)
386                    .with_rotation_period(file_log_rotation_period)
387                    .with_max_files(file_log_max_files)
388                    .build()
389            }))
390            .with_otlp_log_exporter((
391                Self::OTEL_LOG_EXPORTER_LEVEL_ENV,
392                LevelFilter::INFO,
393                otel_log_exporter_enabled,
394            ))
395            .with_otlp_trace_exporter((
396                Self::OTEL_TRACE_EXPORTER_LEVEL_ENV,
397                LevelFilter::INFO,
398                otel_trace_exporter_enabled,
399            ))
400            .build()
401    }
402
403    /// Initialize the configured tracing subscribers, returning a guard that
404    /// will shutdown the subscribers when dropped.
405    ///
406    /// <div class="warning">
407    /// Name the guard variable appropriately, do not just use <code>let _ =</code>, as that will drop
408    /// immediately.
409    /// </div>
410    //
411    // SAFETY: We purposefully allow the `clippy::unwrap_in_result` lint below in this function.
412    // We can use expect here, because the directives are defined as a constant value which must be
413    // able to be parsed.
414    //
415    // FIXME (@Techassi): This attribute can be used on individual unwrap and expect calls since
416    // Rust 1.91.0. We should move this attribute to not contaminate an unnecessarily large scope
417    // once we bump the toolchain to 1.91.0.
418    // See https://github.com/rust-lang/rust-clippy/pull/15445
419    #[allow(clippy::unwrap_in_result)]
420    pub fn init(mut self) -> Result<Tracing> {
421        let mut layers: Vec<Box<dyn Layer<Registry> + Sync + Send>> = Vec::new();
422
423        if let ConsoleLogSettings::Enabled {
424            common_settings,
425            log_format,
426        } = &self.console_log_settings
427        {
428            let env_filter_layer = env_filter_builder(
429                common_settings.environment_variable,
430                common_settings.default_level,
431            );
432
433            // NOTE (@NickLarsenNZ): There is no elegant way to build the layer depending on formats because the types
434            // returned from each subscriber "modifier" function is different (sometimes with different generics).
435            // tracing-subscriber does not auto-detect whether stdout is a terminal
436            // (https://github.com/tokio-rs/tracing/issues/1160), so we check explicitly.
437            let use_ansi = std::io::stdout().is_terminal();
438
439            match log_format {
440                Format::Plain => {
441                    let console_output_layer = tracing_subscriber::fmt::layer()
442                        .with_ansi(use_ansi)
443                        .with_filter(env_filter_layer);
444                    layers.push(console_output_layer.boxed());
445                }
446                Format::Json => {
447                    let console_output_layer = tracing_subscriber::fmt::layer()
448                        .json()
449                        .with_ansi(use_ansi)
450                        .with_filter(env_filter_layer);
451                    layers.push(console_output_layer.boxed());
452                }
453            };
454        }
455
456        if let FileLogSettings::Enabled {
457            common_settings,
458            file_log_dir,
459            rotation_period,
460            filename_suffix,
461            max_log_files,
462        } = &self.file_log_settings
463        {
464            let env_filter_layer = env_filter_builder(
465                common_settings.environment_variable,
466                common_settings.default_level,
467            );
468
469            let file_appender = RollingFileAppender::builder()
470                .rotation(rotation_period.clone())
471                .filename_prefix(self.service_name.to_string())
472                .filename_suffix(filename_suffix);
473
474            let file_appender = if let Some(max_log_files) = max_log_files {
475                file_appender.max_log_files(*max_log_files)
476            } else {
477                file_appender
478            };
479
480            let file_appender = file_appender
481                .build(file_log_dir)
482                .context(InitRollingFileAppenderSnafu)?;
483
484            layers.push(
485                tracing_subscriber::fmt::layer()
486                    .json()
487                    .with_writer(file_appender)
488                    .with_filter(env_filter_layer)
489                    .boxed(),
490            );
491        }
492
493        if let OtlpLogSettings::Enabled { common_settings } = &self.otlp_log_settings {
494            let env_filter_layer = env_filter_builder(
495                common_settings.environment_variable,
496                common_settings.default_level,
497            )
498            // TODO (@NickLarsenNZ): Remove this directive once https://github.com/open-telemetry/opentelemetry-rust/issues/761 is resolved
499            .add_directive("h2=off".parse().expect("invalid directive"));
500
501            let log_exporter = LogExporter::builder()
502                .with_tonic()
503                .build()
504                .context(InstallOtelLogExporterSnafu)?;
505
506            let logger_provider = SdkLoggerProvider::builder()
507                .with_batch_exporter(log_exporter)
508                .with_resource(
509                    Resource::builder()
510                        .with_service_name(self.service_name)
511                        .build(),
512                )
513                .build();
514
515            // Convert `tracing::Event` to OpenTelemetry logs
516            layers.push(
517                OpenTelemetryTracingBridge::new(&logger_provider)
518                    .with_filter(env_filter_layer)
519                    .boxed(),
520            );
521            self.logger_provider = Some(logger_provider);
522        }
523
524        if let OtlpTraceSettings::Enabled { common_settings } = &self.otlp_trace_settings {
525            let env_filter_layer = env_filter_builder(
526                // todo, deref?
527                common_settings.environment_variable,
528                common_settings.default_level,
529            )
530            // TODO (@NickLarsenNZ): Remove this directive once https://github.com/open-telemetry/opentelemetry-rust/issues/761 is resolved
531            .add_directive("h2=off".parse().expect("invalid directive"));
532
533            let trace_exporter = SpanExporter::builder()
534                .with_tonic()
535                .build()
536                .context(InstallOtelTraceExporterSnafu)?;
537
538            let tracer_provider = SdkTracerProvider::builder()
539                .with_batch_exporter(trace_exporter)
540                .with_resource(
541                    Resource::builder()
542                        .with_service_name(self.service_name)
543                        .build(),
544                )
545                .build();
546
547            let tracer = tracer_provider.tracer(self.service_name);
548
549            layers.push(
550                tracing_opentelemetry::layer()
551                    .with_tracer(tracer)
552                    .with_filter(env_filter_layer)
553                    .boxed(),
554            );
555            self.tracer_provider = Some(tracer_provider);
556
557            opentelemetry::global::set_text_map_propagator(
558                // NOTE (@NickLarsenNZ): There are various propagators. Eg: TraceContextPropagator
559                // standardises HTTP headers to propagate trace-id, parent-id, etc... while the
560                // BaggagePropagator sets a "baggage" header with the value being key=value pairs. There
561                // are other kinds too. There is also B3 and Jaeger, and some legacy stuff like OT Trace
562                // and OpenCensus.
563                // See: https://opentelemetry.io/docs/specs/otel/context/api-propagators/
564                TraceContextPropagator::new(),
565            );
566        }
567
568        if !layers.is_empty() {
569            // Add the layers to the tracing_subscriber Registry (console,
570            // tracing (OTLP), logging (OTLP))
571            tracing::subscriber::set_global_default(tracing_subscriber::registry().with(layers))
572                .context(SetGlobalDefaultSubscriberSnafu)?;
573        }
574
575        // IMPORTANT: we must return self, otherwise Drop will be called and uninitialise tracing
576        Ok(self)
577    }
578}
579
580impl Drop for Tracing {
581    fn drop(&mut self) {
582        tracing::debug!(
583            opentelemetry.tracing.enabled = self.otlp_trace_settings.is_enabled(),
584            opentelemetry.logger.enabled = self.otlp_log_settings.is_enabled(),
585            "shutting down opentelemetry OTLP providers"
586        );
587
588        if let Some(tracer_provider) = &self.tracer_provider
589            && let Err(error) = tracer_provider.shutdown()
590        {
591            tracing::error!(%error, "unable to shutdown TracerProvider")
592        }
593
594        if let Some(logger_provider) = &self.logger_provider
595            && let Err(error) = logger_provider.shutdown()
596        {
597            tracing::error!(%error, "unable to shutdown LoggerProvider");
598        }
599    }
600}
601
602/// This trait is only used for the typestate builder and cannot be implemented
603/// outside of this crate.
604///
605/// The only reason it has pub visibility is because it needs to be at least as
606/// visible as the types that use it.
607#[doc(hidden)]
608pub trait BuilderState: private::Sealed {}
609
610/// This private module holds the [`Sealed`][1] trait that is used by the
611/// [`BuilderState`], so that it cannot be implemented outside of this crate.
612///
613/// We impl Sealed for any types that will use the trait that we want to
614/// restrict impls on. In this case, the [`BuilderState`] trait.
615///
616/// [1]: private::Sealed
617#[doc(hidden)]
618mod private {
619    use super::*;
620
621    pub trait Sealed {}
622
623    impl Sealed for builder_state::PreServiceName {}
624    impl Sealed for builder_state::Config {}
625}
626
627/// This module holds the possible states that the builder is in.
628///
629/// Each state will implement [`BuilderState`] (with no methods), and the
630/// Builder struct ([`TracingBuilder`]) itself will be implemented with
631/// each state as a generic parameter.
632/// This allows only the methods to be called when the builder is in the
633/// applicable state.
634#[doc(hidden)]
635mod builder_state {
636    /// The initial state, before the service name is set.
637    #[derive(Default)]
638    pub struct PreServiceName;
639
640    /// The state that allows you to configure the supported [`Subscriber`][1]
641    /// [`Layer`][2].
642    ///
643    /// [1]: tracing::Subscriber
644    /// [2]: tracing_subscriber::layer::Layer
645    #[derive(Default)]
646    pub struct Config;
647}
648
649// Make the states usable
650#[doc(hidden)]
651impl BuilderState for builder_state::PreServiceName {}
652
653#[doc(hidden)]
654impl BuilderState for builder_state::Config {}
655
656/// Makes it easy to build a valid [`Tracing`] instance.
657#[derive(Default)]
658pub struct TracingBuilder<S: BuilderState> {
659    service_name: Option<&'static str>,
660    console_log_settings: ConsoleLogSettings,
661    file_log_settings: FileLogSettings,
662    otlp_log_settings: OtlpLogSettings,
663    otlp_trace_settings: OtlpTraceSettings,
664
665    /// Allow the generic to be used (needed for impls).
666    _marker: std::marker::PhantomData<S>,
667}
668
669impl TracingBuilder<builder_state::PreServiceName> {
670    /// Set the service name used in OTLP exports, and console output.
671    ///
672    /// A service name is required for valid OTLP telemetry.
673    pub fn service_name(self, service_name: &'static str) -> TracingBuilder<builder_state::Config> {
674        TracingBuilder {
675            service_name: Some(service_name),
676            ..Default::default()
677        }
678    }
679}
680
681impl TracingBuilder<builder_state::Config> {
682    /// Enable the console output tracing subscriber and set the default
683    /// [`LevelFilter`] which is overridable through the given environment
684    /// variable.
685    pub fn with_console_output(
686        self,
687        console_log_settings: impl Into<ConsoleLogSettings>,
688    ) -> TracingBuilder<builder_state::Config> {
689        TracingBuilder {
690            service_name: self.service_name,
691            console_log_settings: console_log_settings.into(),
692            otlp_log_settings: self.otlp_log_settings,
693            otlp_trace_settings: self.otlp_trace_settings,
694            file_log_settings: self.file_log_settings,
695            _marker: self._marker,
696        }
697    }
698
699    /// Enable the file output tracing subscriber and set the default
700    /// [`LevelFilter`] which is overridable through the given environment
701    /// variable.
702    pub fn with_file_output(
703        self,
704        file_log_settings: impl Into<FileLogSettings>,
705    ) -> TracingBuilder<builder_state::Config> {
706        TracingBuilder {
707            service_name: self.service_name,
708            console_log_settings: self.console_log_settings,
709            file_log_settings: file_log_settings.into(),
710            otlp_log_settings: self.otlp_log_settings,
711            otlp_trace_settings: self.otlp_trace_settings,
712            _marker: self._marker,
713        }
714    }
715
716    /// Enable the OTLP logging subscriber and set the default [`LevelFilter`]
717    /// which is overridable through the given environment variable.
718    ///
719    /// You can configure the OTLP log exports through the variables defined
720    /// in the opentelemetry crates. See [`Tracing`].
721    pub fn with_otlp_log_exporter(
722        self,
723        otlp_log_settings: impl Into<OtlpLogSettings>,
724    ) -> TracingBuilder<builder_state::Config> {
725        TracingBuilder {
726            service_name: self.service_name,
727            console_log_settings: self.console_log_settings,
728            otlp_log_settings: otlp_log_settings.into(),
729            otlp_trace_settings: self.otlp_trace_settings,
730            file_log_settings: self.file_log_settings,
731            _marker: self._marker,
732        }
733    }
734
735    /// Enable the OTLP tracing subscriber and set the default [`LevelFilter`]
736    /// which is overridable through the given environment variable.
737    ///
738    /// You can configure the OTLP trace exports through the variables defined
739    /// in the opentelemetry crates. See [`Tracing`].
740    pub fn with_otlp_trace_exporter(
741        self,
742        otlp_trace_settings: impl Into<OtlpTraceSettings>,
743    ) -> TracingBuilder<builder_state::Config> {
744        TracingBuilder {
745            service_name: self.service_name,
746            console_log_settings: self.console_log_settings,
747            otlp_log_settings: self.otlp_log_settings,
748            otlp_trace_settings: otlp_trace_settings.into(),
749            file_log_settings: self.file_log_settings,
750            _marker: self._marker,
751        }
752    }
753
754    /// Consumes self and returns a valid [`Tracing`] instance.
755    ///
756    /// Once built, you can call [`Tracing::init`] to enable the configured
757    /// tracing subscribers.
758    pub fn build(self) -> Tracing {
759        Tracing {
760            service_name: self
761                .service_name
762                .expect("service_name must be configured at this point"),
763            console_log_settings: self.console_log_settings,
764            otlp_log_settings: self.otlp_log_settings,
765            otlp_trace_settings: self.otlp_trace_settings,
766            file_log_settings: self.file_log_settings,
767            logger_provider: None,
768            tracer_provider: None,
769        }
770    }
771}
772
773/// Create an [`EnvFilter`] configured with the given environment variable and default [`Directive`].
774fn env_filter_builder(env_var: &str, default_directive: impl Into<Directive>) -> EnvFilter {
775    EnvFilter::builder()
776        .with_env_var(env_var)
777        .with_default_directive(default_directive.into())
778        .from_env_lossy()
779}
780
781/// Contains options which can be passed to [`Tracing::pre_configured()`].
782///
783/// Additionally, this struct can be used as operator CLI arguments. This functionality is only
784/// available if the feature `clap` is enabled.
785#[cfg_attr(
786    feature = "clap",
787    doc = r#"
788```
789# use stackable_telemetry::tracing::TelemetryOptions;
790use clap::Parser;
791
792#[derive(Parser)]
793struct Cli {
794    #[arg(short, long)]
795    namespace: String,
796
797    #[clap(flatten)]
798    telemetry_arguments: TelemetryOptions,
799}
800```
801"#
802)]
803#[cfg_attr(
804    feature = "clap",
805    derive(clap::Args, PartialEq, Eq),
806    command(next_help_heading = "Telemetry Options")
807)]
808#[derive(Debug, Default)]
809pub struct TelemetryOptions {
810    /// Disable console logs.
811    #[cfg_attr(feature = "clap", arg(long, env, group = "console_log"))]
812    pub console_log_disabled: bool,
813
814    /// Console log format.
815    #[cfg_attr(
816        feature = "clap",
817        arg(long, env, group = "console_log", default_value_t)
818    )]
819    pub console_log_format: Format,
820
821    /// Enable logging to files located in the specified DIRECTORY.
822    #[cfg_attr(
823        feature = "clap",
824        arg(long, env, value_name = "DIRECTORY", group = "file_log")
825    )]
826    pub file_log_directory: Option<PathBuf>,
827
828    /// Time PERIOD after which log files are rolled over.
829    #[cfg_attr(
830        feature = "clap",
831        arg(long, env, value_name = "PERIOD", requires = "file_log")
832    )]
833    pub file_log_rotation_period: Option<RotationPeriod>,
834
835    /// Maximum NUMBER of log files to keep.
836    #[cfg_attr(
837        feature = "clap",
838        arg(long, env, value_name = "NUMBER", requires = "file_log")
839    )]
840    pub file_log_max_files: Option<usize>,
841
842    /// Enable exporting OpenTelemetry traces via OTLP.
843    #[cfg_attr(feature = "clap", arg(long, env))]
844    pub otel_trace_exporter_enabled: bool,
845
846    /// Enable exporting OpenTelemetry logs via OTLP.
847    #[cfg_attr(feature = "clap", arg(long, env))]
848    pub otel_log_exporter_enabled: bool,
849}
850
851/// Supported periods when the log file is rolled over.
852#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
853#[derive(Clone, Debug, Default, PartialEq, Eq, strum::Display, strum::EnumString)]
854#[strum(serialize_all = "snake_case")]
855#[allow(missing_docs)]
856pub enum RotationPeriod {
857    Minutely,
858    Hourly,
859    Daily,
860
861    #[default]
862    Never,
863}
864
865impl From<RotationPeriod> for Rotation {
866    fn from(value: RotationPeriod) -> Self {
867        match value {
868            RotationPeriod::Minutely => Self::MINUTELY,
869            RotationPeriod::Hourly => Self::HOURLY,
870            RotationPeriod::Daily => Self::DAILY,
871            RotationPeriod::Never => Self::NEVER,
872        }
873    }
874}
875
876#[cfg(test)]
877mod test {
878    use std::path::PathBuf;
879
880    use rstest::rstest;
881    use settings::Settings;
882    use tracing::level_filters::LevelFilter;
883    use tracing_appender::rolling::Rotation;
884
885    use super::*;
886
887    #[test]
888    fn builder_basic_construction() {
889        let trace_guard = Tracing::builder().service_name("test").build();
890
891        assert_eq!(trace_guard.service_name, "test");
892    }
893
894    #[test]
895    fn builder_with_console_output() {
896        let trace_guard = Tracing::builder()
897            .service_name("test")
898            .with_console_output(
899                Settings::builder()
900                    .with_environment_variable("ABC_A")
901                    .with_default_level(LevelFilter::TRACE)
902                    .build(),
903            )
904            .with_console_output(
905                Settings::builder()
906                    .with_environment_variable("ABC_B")
907                    .with_default_level(LevelFilter::DEBUG)
908                    .build(),
909            )
910            .build();
911
912        assert_eq!(
913            trace_guard.console_log_settings,
914            ConsoleLogSettings::Enabled {
915                common_settings: Settings {
916                    environment_variable: "ABC_B",
917                    default_level: LevelFilter::DEBUG
918                },
919                log_format: Default::default()
920            }
921        );
922
923        assert!(trace_guard.file_log_settings.is_disabled());
924        assert!(trace_guard.otlp_log_settings.is_disabled());
925        assert!(trace_guard.otlp_trace_settings.is_disabled());
926    }
927
928    #[test]
929    fn builder_with_console_output_double() {
930        let trace_guard = Tracing::builder()
931            .service_name("test")
932            .with_console_output(("ABC_A", LevelFilter::TRACE))
933            .build();
934
935        assert_eq!(
936            trace_guard.console_log_settings,
937            ConsoleLogSettings::Enabled {
938                common_settings: Settings {
939                    environment_variable: "ABC_A",
940                    default_level: LevelFilter::TRACE,
941                },
942                log_format: Default::default()
943            }
944        )
945    }
946
947    #[rstest]
948    #[case(false)]
949    #[case(true)]
950    fn builder_with_console_output_triple(#[case] enabled: bool) {
951        let trace_guard = Tracing::builder()
952            .service_name("test")
953            .with_console_output(("ABC_A", LevelFilter::TRACE, enabled))
954            .build();
955
956        let expected = match enabled {
957            true => ConsoleLogSettings::Enabled {
958                common_settings: Settings {
959                    environment_variable: "ABC_A",
960                    default_level: LevelFilter::TRACE,
961                },
962                log_format: Default::default(),
963            },
964            false => ConsoleLogSettings::Disabled,
965        };
966
967        assert_eq!(trace_guard.console_log_settings, expected)
968    }
969
970    #[test]
971    fn builder_with_all() {
972        let trace_guard = Tracing::builder()
973            .service_name("test")
974            .with_console_output(
975                Settings::builder()
976                    .with_environment_variable("ABC_CONSOLE")
977                    .with_default_level(LevelFilter::INFO)
978                    .build(),
979            )
980            .with_file_output(
981                Settings::builder()
982                    .with_environment_variable("ABC_FILE")
983                    .with_default_level(LevelFilter::INFO)
984                    .file_log_settings_builder(PathBuf::from("/abc_file_dir"), "tracing-rs.json")
985                    .build(),
986            )
987            .with_otlp_log_exporter(
988                Settings::builder()
989                    .with_environment_variable("ABC_OTLP_LOG")
990                    .with_default_level(LevelFilter::DEBUG)
991                    .build(),
992            )
993            .with_otlp_trace_exporter(
994                Settings::builder()
995                    .with_environment_variable("ABC_OTLP_TRACE")
996                    .with_default_level(LevelFilter::TRACE)
997                    .build(),
998            )
999            .build();
1000
1001        assert_eq!(
1002            trace_guard.console_log_settings,
1003            ConsoleLogSettings::Enabled {
1004                common_settings: Settings {
1005                    environment_variable: "ABC_CONSOLE",
1006                    default_level: LevelFilter::INFO
1007                },
1008                log_format: Default::default()
1009            }
1010        );
1011        assert_eq!(
1012            trace_guard.file_log_settings,
1013            FileLogSettings::Enabled {
1014                common_settings: Settings {
1015                    environment_variable: "ABC_FILE",
1016                    default_level: LevelFilter::INFO
1017                },
1018                file_log_dir: PathBuf::from("/abc_file_dir"),
1019                rotation_period: Rotation::NEVER,
1020                filename_suffix: "tracing-rs.json".to_owned(),
1021                max_log_files: None,
1022            }
1023        );
1024        assert_eq!(
1025            trace_guard.otlp_log_settings,
1026            OtlpLogSettings::Enabled {
1027                common_settings: Settings {
1028                    environment_variable: "ABC_OTLP_LOG",
1029                    default_level: LevelFilter::DEBUG
1030                },
1031            }
1032        );
1033        assert_eq!(
1034            trace_guard.otlp_trace_settings,
1035            OtlpTraceSettings::Enabled {
1036                common_settings: Settings {
1037                    environment_variable: "ABC_OTLP_TRACE",
1038                    default_level: LevelFilter::TRACE
1039                }
1040            }
1041        );
1042    }
1043
1044    #[test]
1045    fn builder_with_options() {
1046        let enable_console_output = true;
1047        let enable_filelog_output = true;
1048        let enable_otlp_trace = true;
1049        let enable_otlp_log = false;
1050
1051        let tracing_guard = Tracing::builder()
1052            .service_name("test")
1053            .with_console_output(enable_console_output.then(|| {
1054                Settings::builder()
1055                    .with_environment_variable("ABC_CONSOLE")
1056                    .build()
1057            }))
1058            .with_file_output(enable_filelog_output.then(|| {
1059                Settings::builder()
1060                    .with_environment_variable("ABC_FILELOG")
1061                    .file_log_settings_builder("/dev/null", "tracing-rs.json")
1062                    .build()
1063            }))
1064            .with_otlp_trace_exporter(enable_otlp_trace.then(|| {
1065                Settings::builder()
1066                    .with_environment_variable("ABC_OTLP_TRACE")
1067                    .build()
1068            }))
1069            .with_otlp_log_exporter(enable_otlp_log.then(|| {
1070                Settings::builder()
1071                    .with_environment_variable("ABC_OTLP_LOG")
1072                    .build()
1073            }))
1074            .build();
1075
1076        assert!(tracing_guard.console_log_settings.is_enabled());
1077        assert!(tracing_guard.file_log_settings.is_enabled());
1078        assert!(tracing_guard.otlp_trace_settings.is_enabled());
1079        assert!(tracing_guard.otlp_log_settings.is_disabled());
1080    }
1081
1082    #[test]
1083    fn pre_configured() {
1084        let tracing = Tracing::pre_configured(
1085            "test",
1086            TelemetryOptions {
1087                console_log_disabled: false,
1088                console_log_format: Default::default(),
1089                file_log_directory: None,
1090                file_log_rotation_period: None,
1091                file_log_max_files: None,
1092                otel_trace_exporter_enabled: true,
1093                otel_log_exporter_enabled: false,
1094            },
1095        );
1096
1097        assert!(tracing.otlp_trace_settings.is_enabled());
1098    }
1099}