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