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