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