diff --git a/Cargo.lock b/Cargo.lock index 30ae55f6..f78effea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3278,6 +3278,7 @@ dependencies = [ "tokio", "tower 0.5.2", "tracing", + "tracing-appender", "tracing-opentelemetry", "tracing-subscriber", ] diff --git a/crates/stackable-telemetry/CHANGELOG.md b/crates/stackable-telemetry/CHANGELOG.md index 889682a5..f2162a8d 100644 --- a/crates/stackable-telemetry/CHANGELOG.md +++ b/crates/stackable-telemetry/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Introduce common `Settings` and subscriber specific settings ([#901]). +- Add support for logging to files ([#933]). ### Changed @@ -14,6 +15,7 @@ All notable changes to this project will be documented in this file. - BREAKING: Use the new subscriber settings in the `TracingBuilder` ([#901]). [#901]: https://github.com/stackabletech/operator-rs/pull/901 +[#933]: https://github.com/stackabletech/operator-rs/pull/933 ## [0.2.0] - 2024-07-10 diff --git a/crates/stackable-telemetry/Cargo.toml b/crates/stackable-telemetry/Cargo.toml index e1de9410..b445545c 100644 --- a/crates/stackable-telemetry/Cargo.toml +++ b/crates/stackable-telemetry/Cargo.toml @@ -19,6 +19,7 @@ snafu.workspace = true tokio.workspace = true tower.workspace = true tracing.workspace = true +tracing-appender.workspace = true tracing-opentelemetry.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/stackable-telemetry/src/tracing/mod.rs b/crates/stackable-telemetry/src/tracing/mod.rs index 8493d810..7af9206c 100644 --- a/crates/stackable-telemetry/src/tracing/mod.rs +++ b/crates/stackable-telemetry/src/tracing/mod.rs @@ -1,5 +1,5 @@ //! This module contains functionality to initialise tracing Subscribers for -//! console output, and OpenTelemetry OTLP export for traces and logs. +//! console output, file output, and OpenTelemetry OTLP export for traces and logs. //! //! It is intended to be used by the Stackable Data Platform operators and //! webhooks, but it should be generic enough to be used in any application. @@ -16,6 +16,7 @@ use opentelemetry_sdk::{ use opentelemetry_semantic_conventions::resource; use snafu::{ResultExt as _, Snafu}; use tracing::subscriber::SetGlobalDefaultError; +use tracing_appender::rolling::{InitError, RollingFileAppender, Rotation}; use tracing_subscriber::{filter::Directive, layer::SubscriberExt, EnvFilter, Layer, Registry}; use settings::*; @@ -38,6 +39,9 @@ pub enum Error { #[snafu(display("unable to set the global default subscriber"))] SetGlobalDefaultSubscriber { source: SetGlobalDefaultError }, + + #[snafu(display("failed to initialize rolling file appender"))] + InitRollingFileAppender { source: InitError }, } /// Easily initialize a set of pre-configured [`Subscriber`][1] layers. @@ -214,6 +218,7 @@ pub enum Error { pub struct Tracing { service_name: &'static str, console_log_settings: ConsoleLogSettings, + file_log_settings: FileLogSettings, otlp_log_settings: OtlpLogSettings, otlp_trace_settings: OtlpTraceSettings, logger_provider: Option, @@ -246,6 +251,29 @@ impl Tracing { layers.push(console_output_layer.boxed()); } + if self.file_log_settings.enabled { + let env_filter_layer = env_filter_builder( + self.file_log_settings.common_settings.environment_variable, + self.file_log_settings.default_level, + ); + + let file_appender = RollingFileAppender::builder() + .rotation(Rotation::HOURLY) + .filename_prefix(self.service_name.to_string()) + .filename_suffix("tracing-rs.json") + .max_log_files(6) + .build(&self.file_log_settings.file_log_dir) + .context(InitRollingFileAppenderSnafu)?; + + layers.push( + tracing_subscriber::fmt::layer() + .json() + .with_writer(file_appender) + .with_filter(env_filter_layer) + .boxed(), + ); + } + if self.otlp_log_settings.enabled { let env_filter_layer = env_filter_builder( self.otlp_log_settings.environment_variable, @@ -385,12 +413,6 @@ mod builder_state { #[derive(Default)] pub struct PreServiceName; - /// The state before the [`EnvFilter`][1] environment variable name is set. - /// - /// [1]: tracing_subscriber::filter::EnvFilter - #[derive(Default)] - pub struct PreEnvVar; - /// The state that allows you to configure the supported [`Subscriber`][1] /// [`Layer`][2]. /// @@ -414,6 +436,7 @@ pub struct TracingBuilder { console_log_settings: ConsoleLogSettings, otlp_log_settings: OtlpLogSettings, otlp_trace_settings: OtlpTraceSettings, + file_log_settings: FileLogSettings, /// Allow the generic to be used (needed for impls). _marker: std::marker::PhantomData, @@ -446,6 +469,26 @@ impl TracingBuilder { console_log_settings: console_log_settings.into(), otlp_log_settings: self.otlp_log_settings, otlp_trace_settings: self.otlp_trace_settings, + file_log_settings: self.file_log_settings, + _marker: self._marker, + } + } + + /// Enable the file output tracing subscriber and set the default + /// [`LevelFilter`][1] which is overridable through the given environment + /// variable. + /// + /// [1]: tracing_subscriber::filter::LevelFilter + pub fn with_file_output( + self, + file_log_settings: impl Into, + ) -> TracingBuilder { + TracingBuilder { + service_name: self.service_name, + console_log_settings: self.console_log_settings, + file_log_settings: file_log_settings.into(), + otlp_log_settings: self.otlp_log_settings, + otlp_trace_settings: self.otlp_trace_settings, _marker: self._marker, } } @@ -466,6 +509,7 @@ impl TracingBuilder { console_log_settings: self.console_log_settings, otlp_log_settings: otlp_log_settings.into(), otlp_trace_settings: self.otlp_trace_settings, + file_log_settings: self.file_log_settings, _marker: self._marker, } } @@ -486,6 +530,7 @@ impl TracingBuilder { console_log_settings: self.console_log_settings, otlp_log_settings: self.otlp_log_settings, otlp_trace_settings: otlp_trace_settings.into(), + file_log_settings: self.file_log_settings, _marker: self._marker, } } @@ -502,6 +547,7 @@ impl TracingBuilder { console_log_settings: self.console_log_settings, otlp_log_settings: self.otlp_log_settings, otlp_trace_settings: self.otlp_trace_settings, + file_log_settings: self.file_log_settings, logger_provider: None, } } @@ -517,6 +563,8 @@ fn env_filter_builder(env_var: &str, default_directive: impl Into) -> #[cfg(test)] mod test { + use std::path::PathBuf; + use rstest::rstest; use settings::Settings; use tracing::level_filters::LevelFilter; @@ -618,6 +666,14 @@ mod test { .enabled(true) .build(), ) + .with_file_output( + Settings::builder() + .with_environment_variable("ABC_FILE") + .with_default_level(LevelFilter::INFO) + .enabled(true) + .file_log_settings_builder(PathBuf::from("/abc_file_dir")) + .build(), + ) .with_otlp_log_exporter( Settings::builder() .with_environment_variable("ABC_OTLP_LOG") @@ -645,6 +701,17 @@ mod test { log_format: Default::default() } ); + assert_eq!( + trace_guard.file_log_settings, + FileLogSettings { + common_settings: Settings { + enabled: true, + environment_variable: "ABC_FILE", + default_level: LevelFilter::INFO + }, + file_log_dir: PathBuf::from("/abc_file_dir") + } + ); assert_eq!( trace_guard.otlp_log_settings, OtlpLogSettings { diff --git a/crates/stackable-telemetry/src/tracing/settings/file_log.rs b/crates/stackable-telemetry/src/tracing/settings/file_log.rs new file mode 100644 index 00000000..73ccbda0 --- /dev/null +++ b/crates/stackable-telemetry/src/tracing/settings/file_log.rs @@ -0,0 +1,70 @@ +//! File Log Subscriber Settings. + +use std::{ops::Deref, path::PathBuf}; + +use super::Settings; + +/// Configure specific settings for the File Log subscriber. +#[derive(Debug, Default, PartialEq)] +pub struct FileLogSettings { + /// Common subscriber settings that apply to the File Log Subscriber. + pub common_settings: Settings, + + /// Path to directory for log files. + pub file_log_dir: PathBuf, +} + +impl Deref for FileLogSettings { + type Target = Settings; + + fn deref(&self) -> &Self::Target { + &self.common_settings + } +} + +/// For building [`FileLogSettings`]. +/// +///
+/// Do not use directly, instead use the [`Settings::builder`] associated function. +///
+pub struct FileLogSettingsBuilder { + pub(crate) common_settings: Settings, + pub(crate) file_log_dir: PathBuf, +} + +impl FileLogSettingsBuilder { + /// Consumes self and returns a valid [`FileLogSettings`] instance. + pub fn build(self) -> FileLogSettings { + FileLogSettings { + common_settings: self.common_settings, + file_log_dir: self.file_log_dir, + } + } +} + +#[cfg(test)] +mod test { + use tracing::level_filters::LevelFilter; + + use super::*; + + #[test] + fn builds_settings() { + let expected = FileLogSettings { + common_settings: Settings { + environment_variable: "hello", + default_level: LevelFilter::DEBUG, + enabled: true, + }, + file_log_dir: PathBuf::from("/logs"), + }; + let result = Settings::builder() + .with_environment_variable("hello") + .with_default_level(LevelFilter::DEBUG) + .enabled(true) + .file_log_settings_builder(PathBuf::from("/logs")) + .build(); + + assert_eq!(expected, result); + } +} diff --git a/crates/stackable-telemetry/src/tracing/settings/mod.rs b/crates/stackable-telemetry/src/tracing/settings/mod.rs index 88deb9d7..bb5a1d4a 100644 --- a/crates/stackable-telemetry/src/tracing/settings/mod.rs +++ b/crates/stackable-telemetry/src/tracing/settings/mod.rs @@ -1,10 +1,15 @@ //! Subscriber settings. +use std::path::Path; + use tracing::level_filters::LevelFilter; pub mod console_log; pub use console_log::*; +pub mod file_log; +pub use file_log::*; + pub mod otlp_log; pub use otlp_log::*; @@ -89,6 +94,17 @@ impl SettingsBuilder { self.into() } + /// Set specific [`FileLogSettings`]. + pub fn file_log_settings_builder

(self, path: P) -> FileLogSettingsBuilder + where + P: AsRef, + { + FileLogSettingsBuilder { + common_settings: self.build(), + file_log_dir: path.as_ref().to_path_buf(), + } + } + /// Set specific [`OtlpLogSettings`]. pub fn otlp_log_settings_builder(self) -> OtlpLogSettingsBuilder { self.into()