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