diff --git a/Cargo.lock b/Cargo.lock index cd65ed84..ae26ee28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3100,6 +3100,7 @@ dependencies = [ "time", "tokio", "tracing", + "tracing-appender", "tracing-opentelemetry", "tracing-subscriber", "url", @@ -3329,10 +3330,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -3341,6 +3344,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3555,6 +3568,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index 206be686..cfa45a18 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -42,6 +42,7 @@ tracing-opentelemetry.workspace = true tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true +tracing-appender = "0.2.3" [dev-dependencies] rstest.workspace = true diff --git a/crates/stackable-operator/src/logging/mod.rs b/crates/stackable-operator/src/logging/mod.rs index 00296b8f..fba1c5d2 100644 --- a/crates/stackable-operator/src/logging/mod.rs +++ b/crates/stackable-operator/src/logging/mod.rs @@ -1,5 +1,19 @@ +use std::{ + io::{sink, Sink}, + path::PathBuf, +}; + use tracing; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; +use tracing_appender::rolling::RollingFileAppender; +use tracing_subscriber::{ + fmt::{ + writer::{EitherWriter, MakeWriterExt as _}, + MakeWriter, + }, + layer::SubscriberExt, + util::SubscriberInitExt, + EnvFilter, Registry, +}; pub mod controller; mod k8s_events; @@ -22,6 +36,9 @@ impl Default for TracingTarget { /// We force users to provide a variable name so it can be different per product. /// We encourage it to be the product name plus `_LOG`, e.g. `FOOBAR_OPERATOR_LOG`. /// If no environment variable is provided, the maximum log level is set to INFO. +/// +/// Log output can be copied to a file by setting `{env}_DIRECTORY` (e.g. `FOOBAR_OPERATOR_DIRECTORY`) +/// to a directory path. This file will be rotated regularly. pub fn initialize_logging(env: &str, app_name: &str, tracing_target: TracingTarget) { let filter = match EnvFilter::try_from_env(env) { Ok(env_filter) => env_filter, @@ -29,7 +46,17 @@ pub fn initialize_logging(env: &str, app_name: &str, tracing_target: TracingTarg .expect("Failed to initialize default tracing level to INFO"), }; - let fmt = tracing_subscriber::fmt::layer(); + let file_appender_directory = std::env::var_os(format!("{env}_DIRECTORY")).map(PathBuf::from); + let file_appender = + OptionalMakeWriter::from(file_appender_directory.as_deref().map(|log_dir| { + RollingFileAppender::builder() + .filename_suffix(format!("{app_name}.log")) + .max_log_files(6) + .build(log_dir) + .expect("failed to initialize rolling file appender") + })); + + let fmt = tracing_subscriber::fmt::layer().with_writer(std::io::stdout.and(file_appender)); let registry = Registry::default().with(filter).with(fmt); match tracing_target { @@ -45,6 +72,50 @@ pub fn initialize_logging(env: &str, app_name: &str, tracing_target: TracingTarg registry.with(opentelemetry).init(); } } + + // need to delay logging until after tracing is initialized + match file_appender_directory { + Some(dir) => tracing::info!(log.file.directory = %dir.display(), "file logging enabled"), + None => tracing::debug!("file logging disabled, because no log directory set"), + } +} + +/// Like [`EitherWriter`] but implements [`MakeWriter`] instead of [`std::io::Write`]. +/// For selecting writers depending on dynamic configuration. +enum EitherMakeWriter { + A(A), + B(B), +} +impl<'a, A, B> MakeWriter<'a> for EitherMakeWriter +where + A: MakeWriter<'a>, + B: MakeWriter<'a>, +{ + type Writer = EitherWriter; + + fn make_writer(&'a self) -> Self::Writer { + match self { + Self::A(a) => EitherWriter::A(a.make_writer()), + Self::B(b) => EitherWriter::B(b.make_writer()), + } + } + + fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer { + match self { + Self::A(a) => EitherWriter::A(a.make_writer_for(meta)), + Self::B(b) => EitherWriter::B(b.make_writer_for(meta)), + } + } +} + +type OptionalMakeWriter = EitherMakeWriter Sink>; +impl From> for OptionalMakeWriter { + fn from(value: Option) -> Self { + match value { + Some(t) => Self::A(t), + None => Self::B(sink), + } + } } #[cfg(test)]