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