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 //
412 // SAFETY: We purposefully allow the `clippy::unwrap_in_result` lint below in this function.
413 // We can use expect here, because the directives are defined as a constant value which must be
414 // able to be parsed.
415 //
416 // FIXME (@Techassi): This attribute can be used on individual unwrap and expect calls since
417 // Rust 1.91.0. We should move this attribute to not contaminate an unnecessarily large scope
418 // once we bump the toolchain to 1.91.0.
419 // See https://github.com/rust-lang/rust-clippy/pull/15445
420 #[allow(clippy::unwrap_in_result)]
421 pub fn init(mut self) -> Result<Tracing> {
422 let mut layers: Vec<Box<dyn Layer<Registry> + Sync + Send>> = Vec::new();
423
424 if let ConsoleLogSettings::Enabled {
425 common_settings,
426 log_format,
427 } = &self.console_log_settings
428 {
429 let env_filter_layer = env_filter_builder(
430 common_settings.environment_variable,
431 common_settings.default_level,
432 );
433
434 // NOTE (@NickLarsenNZ): There is no elegant way to build the layer depending on formats because the types
435 // returned from each subscriber "modifier" function is different (sometimes with different generics).
436 match log_format {
437 Format::Plain => {
438 let console_output_layer =
439 tracing_subscriber::fmt::layer().with_filter(env_filter_layer);
440 layers.push(console_output_layer.boxed());
441 }
442 Format::Json => {
443 let console_output_layer = tracing_subscriber::fmt::layer()
444 .json()
445 .with_filter(env_filter_layer);
446 layers.push(console_output_layer.boxed());
447 }
448 };
449 }
450
451 if let FileLogSettings::Enabled {
452 common_settings,
453 file_log_dir,
454 rotation_period,
455 filename_suffix,
456 max_log_files,
457 } = &self.file_log_settings
458 {
459 let env_filter_layer = env_filter_builder(
460 common_settings.environment_variable,
461 common_settings.default_level,
462 );
463
464 let file_appender = RollingFileAppender::builder()
465 .rotation(rotation_period.clone())
466 .filename_prefix(self.service_name.to_string())
467 .filename_suffix(filename_suffix);
468
469 let file_appender = if let Some(max_log_files) = max_log_files {
470 file_appender.max_log_files(*max_log_files)
471 } else {
472 file_appender
473 };
474
475 let file_appender = file_appender
476 .build(file_log_dir)
477 .context(InitRollingFileAppenderSnafu)?;
478
479 layers.push(
480 tracing_subscriber::fmt::layer()
481 .json()
482 .with_writer(file_appender)
483 .with_filter(env_filter_layer)
484 .boxed(),
485 );
486 }
487
488 if let OtlpLogSettings::Enabled { common_settings } = &self.otlp_log_settings {
489 let env_filter_layer = env_filter_builder(
490 common_settings.environment_variable,
491 common_settings.default_level,
492 )
493 // TODO (@NickLarsenNZ): Remove this directive once https://github.com/open-telemetry/opentelemetry-rust/issues/761 is resolved
494 .add_directive("h2=off".parse().expect("invalid directive"));
495
496 let log_exporter = LogExporter::builder()
497 .with_tonic()
498 .build()
499 .context(InstallOtelLogExporterSnafu)?;
500
501 let logger_provider = SdkLoggerProvider::builder()
502 .with_batch_exporter(log_exporter)
503 .with_resource(
504 Resource::builder()
505 .with_service_name(self.service_name)
506 .build(),
507 )
508 .build();
509
510 // Convert `tracing::Event` to OpenTelemetry logs
511 layers.push(
512 OpenTelemetryTracingBridge::new(&logger_provider)
513 .with_filter(env_filter_layer)
514 .boxed(),
515 );
516 self.logger_provider = Some(logger_provider);
517 }
518
519 if let OtlpTraceSettings::Enabled { common_settings } = &self.otlp_trace_settings {
520 let env_filter_layer = env_filter_builder(
521 // todo, deref?
522 common_settings.environment_variable,
523 common_settings.default_level,
524 )
525 // TODO (@NickLarsenNZ): Remove this directive once https://github.com/open-telemetry/opentelemetry-rust/issues/761 is resolved
526 .add_directive("h2=off".parse().expect("invalid directive"));
527
528 let trace_exporter = SpanExporter::builder()
529 .with_tonic()
530 .build()
531 .context(InstallOtelTraceExporterSnafu)?;
532
533 let tracer_provider = SdkTracerProvider::builder()
534 .with_batch_exporter(trace_exporter)
535 .with_resource(
536 Resource::builder()
537 .with_service_name(self.service_name)
538 .build(),
539 )
540 .build();
541
542 let tracer = tracer_provider.tracer(self.service_name);
543
544 layers.push(
545 tracing_opentelemetry::layer()
546 .with_tracer(tracer)
547 .with_filter(env_filter_layer)
548 .boxed(),
549 );
550 self.tracer_provider = Some(tracer_provider);
551
552 opentelemetry::global::set_text_map_propagator(
553 // NOTE (@NickLarsenNZ): There are various propagators. Eg: TraceContextPropagator
554 // standardises HTTP headers to propagate trace-id, parent-id, etc... while the
555 // BaggagePropagator sets a "baggage" header with the value being key=value pairs. There
556 // are other kinds too. There is also B3 and Jaeger, and some legacy stuff like OT Trace
557 // and OpenCensus.
558 // See: https://opentelemetry.io/docs/specs/otel/context/api-propagators/
559 TraceContextPropagator::new(),
560 );
561 }
562
563 if !layers.is_empty() {
564 // Add the layers to the tracing_subscriber Registry (console,
565 // tracing (OTLP), logging (OTLP))
566 tracing::subscriber::set_global_default(tracing_subscriber::registry().with(layers))
567 .context(SetGlobalDefaultSubscriberSnafu)?;
568 }
569
570 // IMPORTANT: we must return self, otherwise Drop will be called and uninitialise tracing
571 Ok(self)
572 }
573}
574
575impl Drop for Tracing {
576 fn drop(&mut self) {
577 tracing::debug!(
578 opentelemetry.tracing.enabled = self.otlp_trace_settings.is_enabled(),
579 opentelemetry.logger.enabled = self.otlp_log_settings.is_enabled(),
580 "shutting down opentelemetry OTLP providers"
581 );
582
583 if let Some(tracer_provider) = &self.tracer_provider
584 && let Err(error) = tracer_provider.shutdown()
585 {
586 tracing::error!(%error, "unable to shutdown TracerProvider")
587 }
588
589 if let Some(logger_provider) = &self.logger_provider
590 && let Err(error) = logger_provider.shutdown()
591 {
592 tracing::error!(%error, "unable to shutdown LoggerProvider");
593 }
594 }
595}
596
597/// This trait is only used for the typestate builder and cannot be implemented
598/// outside of this crate.
599///
600/// The only reason it has pub visibility is because it needs to be at least as
601/// visible as the types that use it.
602#[doc(hidden)]
603pub trait BuilderState: private::Sealed {}
604
605/// This private module holds the [`Sealed`][1] trait that is used by the
606/// [`BuilderState`], so that it cannot be implemented outside of this crate.
607///
608/// We impl Sealed for any types that will use the trait that we want to
609/// restrict impls on. In this case, the [`BuilderState`] trait.
610///
611/// [1]: private::Sealed
612#[doc(hidden)]
613mod private {
614 use super::*;
615
616 pub trait Sealed {}
617
618 impl Sealed for builder_state::PreServiceName {}
619 impl Sealed for builder_state::Config {}
620}
621
622/// This module holds the possible states that the builder is in.
623///
624/// Each state will implement [`BuilderState`] (with no methods), and the
625/// Builder struct ([`TracingBuilder`]) itself will be implemented with
626/// each state as a generic parameter.
627/// This allows only the methods to be called when the builder is in the
628/// applicable state.
629#[doc(hidden)]
630mod builder_state {
631 /// The initial state, before the service name is set.
632 #[derive(Default)]
633 pub struct PreServiceName;
634
635 /// The state that allows you to configure the supported [`Subscriber`][1]
636 /// [`Layer`][2].
637 ///
638 /// [1]: tracing::Subscriber
639 /// [2]: tracing_subscriber::layer::Layer
640 #[derive(Default)]
641 pub struct Config;
642}
643
644// Make the states usable
645#[doc(hidden)]
646impl BuilderState for builder_state::PreServiceName {}
647
648#[doc(hidden)]
649impl BuilderState for builder_state::Config {}
650
651/// Makes it easy to build a valid [`Tracing`] instance.
652#[derive(Default)]
653pub struct TracingBuilder<S: BuilderState> {
654 service_name: Option<&'static str>,
655 console_log_settings: ConsoleLogSettings,
656 file_log_settings: FileLogSettings,
657 otlp_log_settings: OtlpLogSettings,
658 otlp_trace_settings: OtlpTraceSettings,
659
660 /// Allow the generic to be used (needed for impls).
661 _marker: std::marker::PhantomData<S>,
662}
663
664impl TracingBuilder<builder_state::PreServiceName> {
665 /// Set the service name used in OTLP exports, and console output.
666 ///
667 /// A service name is required for valid OTLP telemetry.
668 pub fn service_name(self, service_name: &'static str) -> TracingBuilder<builder_state::Config> {
669 TracingBuilder {
670 service_name: Some(service_name),
671 ..Default::default()
672 }
673 }
674}
675
676impl TracingBuilder<builder_state::Config> {
677 /// Enable the console output tracing subscriber and set the default
678 /// [`LevelFilter`] which is overridable through the given environment
679 /// variable.
680 pub fn with_console_output(
681 self,
682 console_log_settings: impl Into<ConsoleLogSettings>,
683 ) -> TracingBuilder<builder_state::Config> {
684 TracingBuilder {
685 service_name: self.service_name,
686 console_log_settings: console_log_settings.into(),
687 otlp_log_settings: self.otlp_log_settings,
688 otlp_trace_settings: self.otlp_trace_settings,
689 file_log_settings: self.file_log_settings,
690 _marker: self._marker,
691 }
692 }
693
694 /// Enable the file output tracing subscriber and set the default
695 /// [`LevelFilter`] which is overridable through the given environment
696 /// variable.
697 pub fn with_file_output(
698 self,
699 file_log_settings: impl Into<FileLogSettings>,
700 ) -> TracingBuilder<builder_state::Config> {
701 TracingBuilder {
702 service_name: self.service_name,
703 console_log_settings: self.console_log_settings,
704 file_log_settings: file_log_settings.into(),
705 otlp_log_settings: self.otlp_log_settings,
706 otlp_trace_settings: self.otlp_trace_settings,
707 _marker: self._marker,
708 }
709 }
710
711 /// Enable the OTLP logging subscriber and set the default [`LevelFilter`]
712 /// which is overridable through the given environment variable.
713 ///
714 /// You can configure the OTLP log exports through the variables defined
715 /// in the opentelemetry crates. See [`Tracing`].
716 pub fn with_otlp_log_exporter(
717 self,
718 otlp_log_settings: impl Into<OtlpLogSettings>,
719 ) -> TracingBuilder<builder_state::Config> {
720 TracingBuilder {
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 ) -> TracingBuilder<builder_state::Config> {
739 TracingBuilder {
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///
781#[cfg_attr(
782 feature = "clap",
783 doc = r#"
784```
785# use stackable_telemetry::tracing::TelemetryOptions;
786use clap::Parser;
787
788#[derive(Parser)]
789struct Cli {
790 #[arg(short, long)]
791 namespace: String,
792
793 #[clap(flatten)]
794 telemetry_arguments: TelemetryOptions,
795}
796```
797"#
798)]
799#[cfg_attr(
800 feature = "clap",
801 derive(clap::Args, PartialEq, Eq),
802 command(next_help_heading = "Telemetry Options")
803)]
804#[derive(Debug, Default)]
805pub struct TelemetryOptions {
806 /// Disable console logs.
807 #[cfg_attr(feature = "clap", arg(long, env, group = "console_log"))]
808 pub console_log_disabled: bool,
809
810 /// Console log format.
811 #[cfg_attr(
812 feature = "clap",
813 arg(long, env, group = "console_log", default_value_t)
814 )]
815 pub console_log_format: Format,
816
817 /// Enable logging to files located in the specified DIRECTORY.
818 #[cfg_attr(
819 feature = "clap",
820 arg(long, env, value_name = "DIRECTORY", group = "file_log")
821 )]
822 pub file_log_directory: Option<PathBuf>,
823
824 /// Time PERIOD after which log files are rolled over.
825 #[cfg_attr(
826 feature = "clap",
827 arg(long, env, value_name = "PERIOD", requires = "file_log")
828 )]
829 pub file_log_rotation_period: Option<RotationPeriod>,
830
831 /// Maximum NUMBER of log files to keep.
832 #[cfg_attr(
833 feature = "clap",
834 arg(long, env, value_name = "NUMBER", requires = "file_log")
835 )]
836 pub file_log_max_files: Option<usize>,
837
838 /// Enable exporting OpenTelemetry traces via OTLP.
839 #[cfg_attr(feature = "clap", arg(long, env))]
840 pub otel_trace_exporter_enabled: bool,
841
842 /// Enable exporting OpenTelemetry logs via OTLP.
843 #[cfg_attr(feature = "clap", arg(long, env))]
844 pub otel_log_exporter_enabled: bool,
845}
846
847/// Supported periods when the log file is rolled over.
848#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
849#[derive(Clone, Debug, Default, PartialEq, Eq, strum::Display, strum::EnumString)]
850#[strum(serialize_all = "snake_case")]
851#[allow(missing_docs)]
852pub enum RotationPeriod {
853 Minutely,
854 Hourly,
855 Daily,
856
857 #[default]
858 Never,
859}
860
861impl From<RotationPeriod> for Rotation {
862 fn from(value: RotationPeriod) -> Self {
863 match value {
864 RotationPeriod::Minutely => Self::MINUTELY,
865 RotationPeriod::Hourly => Self::HOURLY,
866 RotationPeriod::Daily => Self::DAILY,
867 RotationPeriod::Never => Self::NEVER,
868 }
869 }
870}
871
872#[cfg(test)]
873mod test {
874 use std::path::PathBuf;
875
876 use rstest::rstest;
877 use settings::Settings;
878 use tracing::level_filters::LevelFilter;
879 use tracing_appender::rolling::Rotation;
880
881 use super::*;
882
883 #[test]
884 fn builder_basic_construction() {
885 let trace_guard = Tracing::builder().service_name("test").build();
886
887 assert_eq!(trace_guard.service_name, "test");
888 }
889
890 #[test]
891 fn builder_with_console_output() {
892 let trace_guard = Tracing::builder()
893 .service_name("test")
894 .with_console_output(
895 Settings::builder()
896 .with_environment_variable("ABC_A")
897 .with_default_level(LevelFilter::TRACE)
898 .build(),
899 )
900 .with_console_output(
901 Settings::builder()
902 .with_environment_variable("ABC_B")
903 .with_default_level(LevelFilter::DEBUG)
904 .build(),
905 )
906 .build();
907
908 assert_eq!(
909 trace_guard.console_log_settings,
910 ConsoleLogSettings::Enabled {
911 common_settings: Settings {
912 environment_variable: "ABC_B",
913 default_level: LevelFilter::DEBUG
914 },
915 log_format: Default::default()
916 }
917 );
918
919 assert!(trace_guard.file_log_settings.is_disabled());
920 assert!(trace_guard.otlp_log_settings.is_disabled());
921 assert!(trace_guard.otlp_trace_settings.is_disabled());
922 }
923
924 #[test]
925 fn builder_with_console_output_double() {
926 let trace_guard = Tracing::builder()
927 .service_name("test")
928 .with_console_output(("ABC_A", LevelFilter::TRACE))
929 .build();
930
931 assert_eq!(
932 trace_guard.console_log_settings,
933 ConsoleLogSettings::Enabled {
934 common_settings: Settings {
935 environment_variable: "ABC_A",
936 default_level: LevelFilter::TRACE,
937 },
938 log_format: Default::default()
939 }
940 )
941 }
942
943 #[rstest]
944 #[case(false)]
945 #[case(true)]
946 fn builder_with_console_output_triple(#[case] enabled: bool) {
947 let trace_guard = Tracing::builder()
948 .service_name("test")
949 .with_console_output(("ABC_A", LevelFilter::TRACE, enabled))
950 .build();
951
952 let expected = match enabled {
953 true => ConsoleLogSettings::Enabled {
954 common_settings: Settings {
955 environment_variable: "ABC_A",
956 default_level: LevelFilter::TRACE,
957 },
958 log_format: Default::default(),
959 },
960 false => ConsoleLogSettings::Disabled,
961 };
962
963 assert_eq!(trace_guard.console_log_settings, expected)
964 }
965
966 #[test]
967 fn builder_with_all() {
968 let trace_guard = Tracing::builder()
969 .service_name("test")
970 .with_console_output(
971 Settings::builder()
972 .with_environment_variable("ABC_CONSOLE")
973 .with_default_level(LevelFilter::INFO)
974 .build(),
975 )
976 .with_file_output(
977 Settings::builder()
978 .with_environment_variable("ABC_FILE")
979 .with_default_level(LevelFilter::INFO)
980 .file_log_settings_builder(PathBuf::from("/abc_file_dir"), "tracing-rs.json")
981 .build(),
982 )
983 .with_otlp_log_exporter(
984 Settings::builder()
985 .with_environment_variable("ABC_OTLP_LOG")
986 .with_default_level(LevelFilter::DEBUG)
987 .build(),
988 )
989 .with_otlp_trace_exporter(
990 Settings::builder()
991 .with_environment_variable("ABC_OTLP_TRACE")
992 .with_default_level(LevelFilter::TRACE)
993 .build(),
994 )
995 .build();
996
997 assert_eq!(
998 trace_guard.console_log_settings,
999 ConsoleLogSettings::Enabled {
1000 common_settings: Settings {
1001 environment_variable: "ABC_CONSOLE",
1002 default_level: LevelFilter::INFO
1003 },
1004 log_format: Default::default()
1005 }
1006 );
1007 assert_eq!(
1008 trace_guard.file_log_settings,
1009 FileLogSettings::Enabled {
1010 common_settings: Settings {
1011 environment_variable: "ABC_FILE",
1012 default_level: LevelFilter::INFO
1013 },
1014 file_log_dir: PathBuf::from("/abc_file_dir"),
1015 rotation_period: Rotation::NEVER,
1016 filename_suffix: "tracing-rs.json".to_owned(),
1017 max_log_files: None,
1018 }
1019 );
1020 assert_eq!(
1021 trace_guard.otlp_log_settings,
1022 OtlpLogSettings::Enabled {
1023 common_settings: Settings {
1024 environment_variable: "ABC_OTLP_LOG",
1025 default_level: LevelFilter::DEBUG
1026 },
1027 }
1028 );
1029 assert_eq!(
1030 trace_guard.otlp_trace_settings,
1031 OtlpTraceSettings::Enabled {
1032 common_settings: Settings {
1033 environment_variable: "ABC_OTLP_TRACE",
1034 default_level: LevelFilter::TRACE
1035 }
1036 }
1037 );
1038 }
1039
1040 #[test]
1041 fn builder_with_options() {
1042 let enable_console_output = true;
1043 let enable_filelog_output = true;
1044 let enable_otlp_trace = true;
1045 let enable_otlp_log = false;
1046
1047 let tracing_guard = Tracing::builder()
1048 .service_name("test")
1049 .with_console_output(enable_console_output.then(|| {
1050 Settings::builder()
1051 .with_environment_variable("ABC_CONSOLE")
1052 .build()
1053 }))
1054 .with_file_output(enable_filelog_output.then(|| {
1055 Settings::builder()
1056 .with_environment_variable("ABC_FILELOG")
1057 .file_log_settings_builder("/dev/null", "tracing-rs.json")
1058 .build()
1059 }))
1060 .with_otlp_trace_exporter(enable_otlp_trace.then(|| {
1061 Settings::builder()
1062 .with_environment_variable("ABC_OTLP_TRACE")
1063 .build()
1064 }))
1065 .with_otlp_log_exporter(enable_otlp_log.then(|| {
1066 Settings::builder()
1067 .with_environment_variable("ABC_OTLP_LOG")
1068 .build()
1069 }))
1070 .build();
1071
1072 assert!(tracing_guard.console_log_settings.is_enabled());
1073 assert!(tracing_guard.file_log_settings.is_enabled());
1074 assert!(tracing_guard.otlp_trace_settings.is_enabled());
1075 assert!(tracing_guard.otlp_log_settings.is_disabled());
1076 }
1077
1078 #[test]
1079 fn pre_configured() {
1080 let tracing = Tracing::pre_configured(
1081 "test",
1082 TelemetryOptions {
1083 console_log_disabled: false,
1084 console_log_format: Default::default(),
1085 file_log_directory: None,
1086 file_log_rotation_period: None,
1087 file_log_max_files: None,
1088 otel_trace_exporter_enabled: true,
1089 otel_log_exporter_enabled: false,
1090 },
1091 );
1092
1093 assert!(tracing.otlp_trace_settings.is_enabled());
1094 }
1095}