From 0ba8c8b242d3dcc412eddccc5f49ae3f362dcfa1 Mon Sep 17 00:00:00 2001 From: Churi12 Date: Wed, 29 Jul 2026 03:25:30 +0100 Subject: [PATCH] feat: Add an optional task appender to AutomaticContainerLogConfig Airflow renders a separate task handler which is what feeds the log view in its web UI. Its level was not configurable, because console and file are named fields on this struct while loggers is a map, so there was no place in the CRD to express it. Add a task appender next to console and file. It defaults to unset, so the configuration rendered by every other product is unchanged. The hand written Merge impl needs the field too, otherwise a level set on a role group is silently dropped, so a test covers that. --- crates/stackable-operator/CHANGELOG.md | 9 ++ .../src/product_logging/framework.rs | 2 + .../src/product_logging/spec.rs | 95 ++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 30a1c5051..2c33c08cf 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add an optional `task` appender to `AutomaticContainerLogConfig`, next to `console` and + `file`. It is only used by products which have a separate task log destination, currently + Airflow, where it sets the log level shown in the web UI. It defaults to unset, so the + rendered configuration of every other product is unchanged ([#1255]). + +[#1255]: https://github.com/stackabletech/operator-rs/pull/1255 + ## [0.114.0] - 2026-07-22 ### Added diff --git a/crates/stackable-operator/src/product_logging/framework.rs b/crates/stackable-operator/src/product_logging/framework.rs index 35c7bab02..b7f37486e 100644 --- a/crates/stackable-operator/src/product_logging/framework.rs +++ b/crates/stackable-operator/src/product_logging/framework.rs @@ -1553,6 +1553,7 @@ mod tests { file: Some(AppenderConfig { level: Some(LogLevel::ERROR), }), + task: None, }; let log4j2_properties = create_log4j2_config( @@ -1601,6 +1602,7 @@ mod tests { file: Some(AppenderConfig { level: Some(LogLevel::ERROR), }), + task: None, }; let log4j2_properties = create_log4j2_config( diff --git a/crates/stackable-operator/src/product_logging/spec.rs b/crates/stackable-operator/src/product_logging/spec.rs index 0d6f9aca2..1ce72c64d 100644 --- a/crates/stackable-operator/src/product_logging/spec.rs +++ b/crates/stackable-operator/src/product_logging/spec.rs @@ -230,6 +230,12 @@ pub struct AutomaticContainerLogConfig { pub console: Option, /// Configuration for the file appender pub file: Option, + /// Configuration for the task appender + /// + /// Only used by products which have a separate task log destination, currently Airflow, + /// where it controls the log level shown in the web UI. Left unset by every other product, + /// in which case it has no effect. + pub task: Option, } impl Merge for AutomaticContainerLogConfigFragment { @@ -252,6 +258,13 @@ impl Merge for AutomaticContainerLogConfigFragment { } else { self.file.clone_from(&defaults.file); } + if let Some(task) = &mut self.task { + if let Some(defaults_task) = &defaults.task { + task.merge(defaults_task); + } + } else { + self.task.clone_from(&defaults.task); + } } } @@ -449,6 +462,7 @@ pub fn default_container_log_config() -> ContainerLogConfigFragment { file: Some(AppenderConfigFragment { level: Some(LogLevel::INFO), }), + task: None, }, )), } @@ -471,7 +485,7 @@ mod tests { fn serialize_container_log_config() { // automatic configuration assert_eq!( - "{\"loggers\":{},\"console\":{\"level\":\"INFO\"},\"file\":{\"level\":\"WARN\"}}" + "{\"loggers\":{},\"console\":{\"level\":\"INFO\"},\"file\":{\"level\":\"WARN\"},\"task\":null}" .to_string(), serde_json::to_string(&ContainerLogConfigFragment { choice: Some(ContainerLogConfigChoiceFragment::Automatic( @@ -483,6 +497,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }) @@ -519,6 +534,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }, @@ -553,6 +569,7 @@ mod tests { loggers: BTreeMap::new(), console: None, file: None, + task: None, }, )), }, @@ -586,17 +603,20 @@ mod tests { loggers: BTreeMap::new(), console: None, file: None, + task: None, }, merge::merge( AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), console: None, file: None, + task: None, }, &AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), console: None, file: None, + task: None, } ) ); @@ -611,6 +631,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, merge::merge( AutomaticContainerLogConfigFragment { @@ -621,11 +642,13 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, &AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), console: None, file: None, + task: None, } ) ); @@ -640,12 +663,14 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, merge::merge( AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), console: None, file: None, + task: None, }, &AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), @@ -655,6 +680,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, } ) ); @@ -669,6 +695,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::ERROR), }), + task: None, }, merge::merge( AutomaticContainerLogConfigFragment { @@ -677,6 +704,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::ERROR), }), + task: None, }, &AutomaticContainerLogConfigFragment { loggers: BTreeMap::new(), @@ -686,8 +714,66 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, + } + ) + ); + } + + #[test] + fn merge_task_appender_config_fragment() { + // The task appender is merged like the console and file appenders. It is handled + // separately here because the Merge implementation is written by hand, so a missing + // block would silently drop the level instead of failing to compile. + + // overriding task level + default task level -> overriding task level + assert_eq!( + Some(AppenderConfigFragment { + level: Some(LogLevel::DEBUG), + }), + merge::merge( + AutomaticContainerLogConfigFragment { + loggers: BTreeMap::new(), + console: None, + file: None, + task: Some(AppenderConfigFragment { + level: Some(LogLevel::DEBUG), + }), + }, + &AutomaticContainerLogConfigFragment { + loggers: BTreeMap::new(), + console: None, + file: None, + task: Some(AppenderConfigFragment { + level: Some(LogLevel::WARN), + }), + } + ) + .task + ); + + // no overriding task level + default task level -> default task level + assert_eq!( + Some(AppenderConfigFragment { + level: Some(LogLevel::WARN), + }), + merge::merge( + AutomaticContainerLogConfigFragment { + loggers: BTreeMap::new(), + console: None, + file: None, + task: None, + }, + &AutomaticContainerLogConfigFragment { + loggers: BTreeMap::new(), + console: None, + file: None, + task: Some(AppenderConfigFragment { + level: Some(LogLevel::WARN), + }), } ) + .task ); } @@ -705,6 +791,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }, @@ -719,6 +806,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }, @@ -746,6 +834,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }, @@ -758,6 +847,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), }, @@ -769,6 +859,7 @@ mod tests { level: Some(LogLevel::INFO), }), file: Some(AppenderConfigFragment { level: None }), + task: None, }, )), } @@ -789,6 +880,7 @@ mod tests { file: Some(AppenderConfig { level: Some(LogLevel::WARN) }), + task: None, } )) }, @@ -802,6 +894,7 @@ mod tests { file: Some(AppenderConfigFragment { level: Some(LogLevel::WARN), }), + task: None, }, )), })