all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels
@ 2026-09-21  9:51 Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 1/5] log: replace per layer filtering by one global filter Thomas Ellmenreich
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

The core of this change is a simple reinterpretation of the logging environment
variables that we are already using. Instead of parsing them as a single level
filter [0], they will now be interpreted as env filters [1]. Since the latter
can still parse the former, this change is backwards compatible and will not
break with any logging environment variables that have been set.

Implementation
--------------

As Gabriel Goller proposed in this [2] Bugzilla enhancement, by using
EnvFilters [2], we can have more granular control over the logging happening in
the different Proxmox products. The first two commits do exactly this and can
thus be applied by themselves. The last three add tests as well as utilities to
better communicate log levels to use cases that don't directly integrate with
tracing.

proxmox 1/5:
    A simple reordering of the logging layers. This had to be done since
    EnvFilter is currently not clonable and can thus be applied only once.
    Previously, the global filter was being applied unnecessarily often, on all
    layers and then on the tracing_log layer as well.

proxmox 2/5:
    Actual implementation of EnvFilters.

proxmox 3/5:
    Introduction of tests for our EnvFilter, parser, and constructor. This was
    initially created for me to better understand EnvFilters, but then evolved
    into tests to ensure consistent functionality with our 'default_log_level'.

proxmox 4/5:
    Make the Logger's 'init' function return the configuration used, so that
    it may be reused by other loggers or similar.

datacenter-manager 5/5:
    Use the configuration returned by the 'init' function of the Logger to
    determine whether to set the REST server debug mode or not.

Different Log Options
---------------------

Combining the different options of the EnvFilter as well as our own default
log level, we have the following cases for logging:

1. Env contains 'simple':
    Meaning that the environment variable contains a simple 'info'... This
    means that that level is taken as the expected one.

2. Env contains 'simple', and 'module':
    Means that the env variable contains a simple log level as well as zero or
    more module specific log levels. As expected the simple level is applied
    as a default and then for the specified modules the defined levels apply.

3. Env only contains 'module':
    If the env only contains a module (or multiple), that is interpreted by
    tracing as disabling the default logging and only enabling logging for the
    module/s

4. Env is empty:
    No logs are printed at all, everything is hidden.

5. Env is not set:
    This is the only time the default comes into play, by being set as the
    default log level.

Open Questions
--------------

- To me, the idea of storing a nested config and then returning it on 'init'
  seems a bit overengineered, but I could not find a better way. Especially
  when taking into consideration that, for this [3] follow up issue, relaying
  the exact log level might be necessary.

- I had the idea that instead of having all users of the proxmox-log crate
  provide their own 'default_log_level' we could define that inside of the
  proxmox-log crate. By doing so, we could have the default change between
  DEBUG and INFO depending on if we are in a normal or release build.

- Initially, I found case 3. of the different logging cases quite confusing and
  would have thought that our default should apply as default in that case.
  Unfortunately, when setting a default and then applying the EnvFilter string,
  tracing applies the modules as expected, but then also overrides the default
  as '', which means off.

Notes for the Maintainer
------------------------

- For all tracing backed logging in Proxmox products to support EnvFilters,
  only patches 1 and 2 have to be applied. That said, for addressing other log
  related issues like [3], some way of inspecting current logging config, as
  done by the 'init' function (patch 4), should be made available.

- Patch 4 contains a less desirable implementation of Clone for EnvFilters but
  since tracing_subscriber only includes a Clone implementation from 0.3.20
  onwards, I was not able to find a better solution. Either the less desirable
  implementation of Clone is applied, a newer version of tracing_subscriber is
  packaged, patch 4 and 5 are skipped or a different way to return the config
  is found.

[0]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.LevelFilter.html
[1]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
[2]: https://bugzilla.proxmox.com/show_bug.cgi?id=6081
[3]: https://bugzilla.proxmox.com/show_bug.cgi?id=4646


proxmox:

Thomas Ellmenreich (4):
  log: replace per layer filtering by one global filter
  log: replace simple level filter with env filter
  log: add tests to the logger
  log: return the logger configuration after initialisation

 proxmox-log/Cargo.toml                |   2 +-
 proxmox-log/src/builder.rs            | 491 +++++++++++++++++++++++---
 proxmox-log/src/lib.rs                |  22 +-
 proxmox-log/src/pve_task_formatter.rs |   2 +-
 4 files changed, 454 insertions(+), 63 deletions(-)


proxmox-datacenter-manager:

Thomas Ellmenreich (1):
  api: set REST server debug level based on actual log level

 server/src/bin/proxmox-datacenter-api/main.rs | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)


Summary over all repositories:
  5 files changed, 459 insertions(+), 66 deletions(-)

-- 
Generated by murpp 0.12.0




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH proxmox 1/5] log: replace per layer filtering by one global filter
  2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
@ 2026-09-21  9:51 ` Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 2/5] log: replace simple level filter with env filter Thomas Ellmenreich
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

Instead of filtering with the global filter on every layer, provide one
global filter once, as described here: [0]

This is also in preparation for filters that either have to be cloned or
cannot be cloned at all, like EnvFilters* [1].

* EnvFilter can be cloned from tracing_subscriber version 0.3.20 onwards.

[0]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#global-filtering
[1]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 proxmox-log/src/builder.rs            | 48 +++++++++------------------
 proxmox-log/src/pve_task_formatter.rs |  2 +-
 2 files changed, 17 insertions(+), 33 deletions(-)

diff --git a/proxmox-log/src/builder.rs b/proxmox-log/src/builder.rs
index 6fcf1cc9..3108ea29 100644
--- a/proxmox-log/src/builder.rs
+++ b/proxmox-log/src/builder.rs
@@ -1,8 +1,9 @@
 use tracing::Level;
 use tracing::Metadata;
 use tracing::level_filters::LevelFilter;
-use tracing_log::{AsLog, LogTracer};
+use tracing_log::LogTracer;
 use tracing_subscriber::Layer;
+use tracing_subscriber::Registry;
 use tracing_subscriber::layer::Context;
 use tracing_subscriber::layer::Filter;
 use tracing_subscriber::layer::SubscriberExt;
@@ -55,9 +56,7 @@ impl<S> Filter<S> for NoWorkerTask {
 /// ```
 pub struct Logger {
     global_log_level: LevelFilter,
-    layer: Vec<
-        Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
-    >,
+    layer: Vec<Box<dyn Layer<Registry> + Send + Sync + 'static>>,
 }
 
 impl Logger {
@@ -76,11 +75,7 @@ impl Logger {
     ///
     /// If the journal cannot be opened, print to stderr instead.
     pub fn journald(mut self) -> Logger {
-        self.layer.push(
-            journald_or_stderr_layer()
-                .with_filter(self.global_log_level)
-                .boxed(),
-        );
+        self.layer.push(journald_or_stderr_layer().boxed());
         self
     }
 
@@ -90,12 +85,8 @@ impl Logger {
     /// no LogContext exists – which means we are not in a PBS workertask – or the level of the
     /// log message is 'ERROR'.
     pub fn journald_on_no_workertask(mut self) -> Logger {
-        self.layer.push(
-            journald_or_stderr_layer()
-                .with_filter(NoWorkerTask)
-                .with_filter(self.global_log_level)
-                .boxed(),
-        );
+        self.layer
+            .push(journald_or_stderr_layer().with_filter(NoWorkerTask).boxed());
         self
     }
 
@@ -103,8 +94,7 @@ impl Logger {
     ///
     /// Check if a LogContext exists and if it does, print to the corresponding task log file.
     pub fn tasklog_pbs(mut self) -> Logger {
-        self.layer
-            .push(TasklogLayer {}.with_filter(self.global_log_level).boxed());
+        self.layer.push(TasklogLayer.boxed());
         self
     }
 
@@ -112,11 +102,7 @@ impl Logger {
     ///
     /// Prints all the events to stderr with the compact format (no level, no timestamp).
     pub fn stderr(mut self) -> Logger {
-        self.layer.push(
-            plain_stderr_layer()
-                .with_filter(self.global_log_level)
-                .boxed(),
-        );
+        self.layer.push(plain_stderr_layer().boxed());
         self
     }
 
@@ -126,12 +112,8 @@ impl Logger {
     /// triggered if no workertask could be found (no LogContext exists) or the event level is
     /// `ERROR`.
     pub fn stderr_on_no_workertask(mut self) -> Logger {
-        self.layer.push(
-            plain_stderr_layer()
-                .with_filter(NoWorkerTask)
-                .with_filter(self.global_log_level)
-                .boxed(),
-        );
+        self.layer
+            .push(plain_stderr_layer().with_filter(NoWorkerTask).boxed());
         self
     }
 
@@ -141,9 +123,8 @@ impl Logger {
     /// e.g.: `DEBUG: event message`.
     pub fn stderr_pve(mut self) -> Logger {
         let layer = tracing_subscriber::fmt::layer()
-            .event_format(PveTaskFormatter {})
+            .event_format(PveTaskFormatter)
             .with_writer(std::io::stderr)
-            .with_filter(self.global_log_level)
             .boxed();
         self.layer.push(layer);
         self
@@ -153,10 +134,13 @@ impl Logger {
     ///
     /// Also configures the `LogTracer` which will convert all `log` events to tracing events.
     pub fn init(self) -> Result<(), anyhow::Error> {
-        let registry = tracing_subscriber::registry().with(self.layer);
+        let registry = tracing_subscriber::registry()
+            .with(self.layer)
+            .with(self.global_log_level);
+
         tracing::subscriber::set_global_default(registry)?;
 
-        LogTracer::init_with_filter(self.global_log_level.as_log())?;
+        LogTracer::init()?;
         Ok(())
     }
 }
diff --git a/proxmox-log/src/pve_task_formatter.rs b/proxmox-log/src/pve_task_formatter.rs
index e9866a4b..12bc33c8 100644
--- a/proxmox-log/src/pve_task_formatter.rs
+++ b/proxmox-log/src/pve_task_formatter.rs
@@ -8,7 +8,7 @@ use tracing_subscriber::registry::LookupSpan;
 /// This custom formatter outputs logs as they are visible in the PVE task log.
 ///
 /// e.g.: "DEBUG: sample message"
-pub struct PveTaskFormatter {}
+pub struct PveTaskFormatter;
 
 impl<C, N> FormatEvent<C, N> for PveTaskFormatter
 where
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH proxmox 2/5] log: replace simple level filter with env filter
  2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 1/5] log: replace per layer filtering by one global filter Thomas Ellmenreich
@ 2026-09-21  9:51 ` Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 3/5] log: add tests to the logger Thomas Ellmenreich
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

Instead of parsing the contents of the environment variable passed to the
Logger as a simple LevelFilter, parse them as an EnvFilter [0]. This is
backwards compatible, as a simple LevelFilter [1] string is parsable as
an EnvFilter.

[0]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
[1]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.LevelFilter.html

Suggested-by: Gabriel Goller <g.goller@proxmox.com>
Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=6081
Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 proxmox-log/Cargo.toml     |  2 +-
 proxmox-log/src/builder.rs | 38 ++++++++++++++++++++++++++++++++------
 proxmox-log/src/lib.rs     | 16 ----------------
 3 files changed, 33 insertions(+), 23 deletions(-)

diff --git a/proxmox-log/Cargo.toml b/proxmox-log/Cargo.toml
index 25c36fc4..4abe1b11 100644
--- a/proxmox-log/Cargo.toml
+++ b/proxmox-log/Cargo.toml
@@ -16,7 +16,7 @@ anyhow.workspace = true
 nix.workspace = true
 tracing.workspace = true
 tracing-journald.workspace = true
-tracing-subscriber.workspace = true
+tracing-subscriber = { workspace = true, features = ["env-filter"] }
 tracing-log = { workspace = true, features = ["std"] }
 tokio = { workspace = true, features = ["rt-multi-thread"] }
 
diff --git a/proxmox-log/src/builder.rs b/proxmox-log/src/builder.rs
index 3108ea29..9b545603 100644
--- a/proxmox-log/src/builder.rs
+++ b/proxmox-log/src/builder.rs
@@ -2,6 +2,7 @@ use tracing::Level;
 use tracing::Metadata;
 use tracing::level_filters::LevelFilter;
 use tracing_log::LogTracer;
+use tracing_subscriber::EnvFilter;
 use tracing_subscriber::Layer;
 use tracing_subscriber::Registry;
 use tracing_subscriber::layer::Context;
@@ -9,7 +10,7 @@ use tracing_subscriber::layer::Filter;
 use tracing_subscriber::layer::SubscriberExt;
 
 use crate::{
-    LogContext, get_env_variable, journald_or_stderr_layer, plain_stderr_layer,
+    LogContext, journald_or_stderr_layer, plain_stderr_layer,
     pve_task_formatter::PveTaskFormatter, tasklog_layer::TasklogLayer,
 };
 ///
@@ -55,16 +56,17 @@ impl<S> Filter<S> for NoWorkerTask {
 /// # func().expect("failed to init logger");
 /// ```
 pub struct Logger {
-    global_log_level: LevelFilter,
+    global_log_level: EnvFilter,
     layer: Vec<Box<dyn Layer<Registry> + Send + Sync + 'static>>,
 }
 
 impl Logger {
-    /// Create a new LogBuilder with no layers and a default loglevel retrieved from an env
-    /// variable. If the env variable cannot be retrieved or the content is not parsable, fallback
-    /// to the default_log_level passed.
+    /// Create a new Logger with no layers and a default loglevel retrieved from an env
+    /// variable. If the env variable cannot be retrieved or the content is not parsable,
+    /// fallback to the default_log_level passed.
     pub fn from_env(env_var: &str, default_log_level: LevelFilter) -> Logger {
-        let log_level = get_env_variable(env_var, default_log_level);
+        let var_content = std::env::var(env_var).ok();
+        let log_level = Self::apply_default_log_level(var_content, default_log_level);
         Logger {
             global_log_level: log_level,
             layer: vec![],
@@ -143,4 +145,28 @@ impl Logger {
         LogTracer::init()?;
         Ok(())
     }
+
+    /// If present, tries to parse the `env_filter_str` as a [`EnvFilter`],
+    /// otherwiese falls back to the provided default log level.
+    ///
+    /// The provided default log level is a application default defined by us.
+    /// The user can define their own default as part of the [`EnvFilter`]
+    ///
+    /// * `env_filter_str` - The log settings that would usually be written into
+    ///                      a environment variable and then extracted by [`get_env_variable`]
+    /// * `default_log_level` - Default log level used by the [`Logger`] in
+    ///                         case the log settings are empty.
+    fn apply_default_log_level(
+        env_filter_str: Option<String>,
+        default_log_level: LevelFilter,
+    ) -> EnvFilter {
+        if let Some(env_filter_str) = env_filter_str {
+            match EnvFilter::try_new(env_filter_str) {
+                Ok(filter) => return filter,
+                Err(e) => eprintln!("unable to parse the log env variable: {e:#}"),
+            }
+        }
+        EnvFilter::default().add_directive(default_log_level.into())
+    }
 }
+
diff --git a/proxmox-log/src/lib.rs b/proxmox-log/src/lib.rs
index 2d321f20..32c10e27 100644
--- a/proxmox-log/src/lib.rs
+++ b/proxmox-log/src/lib.rs
@@ -1,7 +1,6 @@
 #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
 #![deny(unsafe_op_in_unsafe_fn)]
 
-use std::env;
 use std::future::Future;
 use std::sync::{Arc, Mutex};
 
@@ -147,21 +146,6 @@ where
         .with_writer(std::io::stderr)
 }
 
-fn get_env_variable(env_var: &str, default_log_level: LevelFilter) -> LevelFilter {
-    let mut log_level = default_log_level;
-    if let Ok(v) = env::var(env_var) {
-        match v.parse::<LevelFilter>() {
-            Ok(l) => {
-                log_level = l;
-            }
-            Err(e) => {
-                eprintln!("env variable {env_var} found, but parsing failed: {e:?}");
-            }
-        }
-    }
-    log_level
-}
-
 /// Initialize tracing logger that prints to journald or stderr depending on if we are in a pbs
 /// task.
 ///
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH proxmox 3/5] log: add tests to the logger
  2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 1/5] log: replace per layer filtering by one global filter Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 2/5] log: replace simple level filter with env filter Thomas Ellmenreich
@ 2026-09-21  9:51 ` Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH proxmox 4/5] log: return the logger configuration after initialisation Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH datacenter-manager 5/5] api: set REST server debug level based on actual log level Thomas Ellmenreich
  4 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

Add tests to ensure consistent functionality when setting the logging
environment variable, as well as global filtering of logs.

Although the EnvFilter itself is not something we should be testing, we are
adding some additional machinery, on top of the extensive functionality
provided by tracing/tracing_subscriber, which does need testing.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 proxmox-log/src/builder.rs | 296 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 291 insertions(+), 5 deletions(-)

diff --git a/proxmox-log/src/builder.rs b/proxmox-log/src/builder.rs
index 9b545603..05da3643 100644
--- a/proxmox-log/src/builder.rs
+++ b/proxmox-log/src/builder.rs
@@ -1,5 +1,6 @@
 use tracing::Level;
 use tracing::Metadata;
+use tracing::Subscriber;
 use tracing::level_filters::LevelFilter;
 use tracing_log::LogTracer;
 use tracing_subscriber::EnvFilter;
@@ -10,8 +11,8 @@ use tracing_subscriber::layer::Filter;
 use tracing_subscriber::layer::SubscriberExt;
 
 use crate::{
-    LogContext, journald_or_stderr_layer, plain_stderr_layer,
-    pve_task_formatter::PveTaskFormatter, tasklog_layer::TasklogLayer,
+    LogContext, journald_or_stderr_layer, plain_stderr_layer, pve_task_formatter::PveTaskFormatter,
+    tasklog_layer::TasklogLayer,
 };
 ///
 /// Filter yielding `true` *outside* of worker tasks, *unless* the level is `ERROR`.
@@ -136,9 +137,7 @@ impl Logger {
     ///
     /// Also configures the `LogTracer` which will convert all `log` events to tracing events.
     pub fn init(self) -> Result<(), anyhow::Error> {
-        let registry = tracing_subscriber::registry()
-            .with(self.layer)
-            .with(self.global_log_level);
+        let registry = self.create_subscriber();
 
         tracing::subscriber::set_global_default(registry)?;
 
@@ -146,6 +145,15 @@ impl Logger {
         Ok(())
     }
 
+    /// Creates the subscriber to be used for the Logger
+    ///
+    /// Placed in its own method to allow easier testing of the subscriber
+    fn create_subscriber(self) -> impl Subscriber {
+        tracing_subscriber::registry()
+            .with(self.layer)
+            .with(self.global_log_level)
+    }
+
     /// If present, tries to parse the `env_filter_str` as a [`EnvFilter`],
     /// otherwiese falls back to the provided default log level.
     ///
@@ -170,3 +178,281 @@ impl Logger {
     }
 }
 
+#[cfg(test)]
+mod tests {
+    use std::sync::{Arc, Mutex};
+
+    use tracing::level_filters::LevelFilter;
+    use tracing_log::log;
+    use tracing_subscriber::{Layer, util::SubscriberInitExt};
+
+    use crate::Logger;
+
+    /// Modules created for testing purposes. Specifically, to test filtering
+    /// of logs in different modules.
+    mod test_module {
+        pub mod nested_module {
+            pub fn info(message: &'static str) {
+                tracing_log::log::info!("{message}");
+            }
+        }
+        pub fn info(message: &'static str) {
+            tracing_log::log::info!("{message}");
+        }
+    }
+
+    macro_rules! assert_logs {
+        ($events:expr, [$($expected:expr),* $(,)?]) => {{
+            let events = $events.lock().unwrap();
+
+            assert_eq!(events.as_slice(), [$($expected),*]);
+        }};
+    }
+
+    #[test]
+    fn logger_builder_correctly_applies_filter() {
+        // Arrange
+        let (events, builder) = create_logger(Some("WARN"), LevelFilter::INFO);
+        let _guard = builder.create_subscriber().set_default();
+
+        // Act
+        log::info!("log1");
+        log::warn!("log2");
+
+        // Assert
+        assert_logs!(events, ["log2"]);
+    }
+
+    #[test]
+    fn simple_log_is_registered() {
+        // Arrange
+        let (events, _guard) = setup_logging(None, LevelFilter::TRACE);
+
+        // Act
+        log::info!("log1");
+
+        // Assert
+        assert_logs!(events, ["log1"]);
+    }
+
+    #[test]
+    fn env_filter_is_backwards_compatible() {
+        // Arrange
+        let (events, _guard) = setup_logging(Some("ERROR"), LevelFilter::INFO);
+
+        // Act
+        test_module::info("log1");
+        test_module::nested_module::info("log2");
+        log::error!("log3");
+
+        // Assert
+        assert_logs!(events, ["log3"]);
+    }
+
+    #[test]
+    fn only_module_filter_disables_other_logging() {
+        // Arrange
+        let (events, _guard) = setup_logging(
+            Some("proxmox_log::builder::tests::test_module=INFO"),
+            LevelFilter::INFO,
+        );
+
+        // Act
+        test_module::info("log1");
+        test_module::nested_module::info("log2");
+        log::info!("log3");
+
+        // Assert
+        assert_logs!(events, ["log1", "log2"]);
+    }
+
+    #[test]
+    fn nested_module_filter_correctly_removes_logs_from_nested_module() {
+        // Arrange
+        let (events, _guard) = setup_logging(
+            Some(
+                "proxmox_log::builder::tests::test_module=INFO,proxmox_log::builder::tests::test_module::nested_module=ERROR",
+            ),
+            LevelFilter::INFO,
+        );
+
+        // Act
+        test_module::info("log1");
+        test_module::nested_module::info("log2");
+
+        // Assert
+        assert_logs!(events, ["log1",]);
+    }
+
+    #[test]
+    fn empty_variable_turns_off_all_logging() {
+        // Arrange
+        let (events, _guard) = setup_logging(Some(""), LevelFilter::TRACE);
+
+        // Act
+        log::trace!("log1");
+        log::debug!("log2");
+        log::info!("log3");
+        log::warn!("log4");
+        log::error!("log5");
+
+        // Assert
+        let list = events.lock().unwrap();
+        assert!(list.is_empty());
+    }
+
+    #[test]
+    fn gibberish_filter_allows_use_of_fallback() {
+        // Arrange
+        let (events, _guard) = setup_logging(
+            Some("SNERROR,::_interesting*Üpath::we;HAVE=QUARNING"),
+            LevelFilter::ERROR,
+        );
+
+        // Act
+        log::info!("log1");
+        log::error!("log2");
+
+        // Assert
+        assert_logs!(events, ["log2"]);
+    }
+
+    #[test]
+    fn setting_default_in_variable_is_adopted_as_default() {
+        // Arrange
+        let (events, _guard) = setup_logging(
+            Some("ERROR,proxmox_log::builder::tests::test_module::nested_module=INFO"),
+            LevelFilter::INFO,
+        );
+
+        // Act
+        test_module::info("log1");
+        test_module::nested_module::info("log2");
+        log::error!("log3");
+
+        assert_logs!(events, ["log2", "log3"]);
+    }
+
+    /// Creates a [`Logger`] with a [`crate::builder::tests::TestLayer`]
+    /// inserted so that any logs received can be monitored through the
+    /// returned Vec of strings.
+    /// * `var_filter` - The log settings that would usually be written into
+    ///                  a environment variable and then extracted by the [`Logger`]
+    /// * `default_log_level` - Default log level used by the [`Logger`] in
+    ///                         case the log settings are empty.
+    fn create_logger(
+        var_filter: Option<&'static str>,
+        default_log_level: LevelFilter,
+    ) -> (Arc<Mutex<Vec<String>>>, Logger) {
+        let (events, test_layer) = TestLayer::no_filter();
+        (
+            events,
+            Logger {
+                global_log_level: Logger::apply_default_log_level(
+                    var_filter.map(str::to_string),
+                    default_log_level,
+                ),
+                layer: vec![test_layer.boxed()],
+            },
+        )
+    }
+
+    /// Sets up logging in a way that allows the filter to act, and then
+    /// records every log that is let through in a Vec of strings. Returns
+    /// said Vec of strings as well as a guard that allows tracing to capture
+    /// logs while it's not dropped.
+    /// * `var_filter` - The log settings that would usually be written into
+    ///                  a environment variable and then extracted by the [`Logger`]
+    /// * `default_log_level` - Default log level used by the [`Logger`] in
+    ///                         case the log settings are empty.
+    fn setup_logging(
+        var_filter: Option<&'static str>,
+        default_log_level: LevelFilter,
+    ) -> (Arc<Mutex<Vec<String>>>, tracing::subscriber::DefaultGuard) {
+        use tracing_subscriber::layer::SubscriberExt;
+
+        let filter =
+            Logger::apply_default_log_level(var_filter.map(str::to_string), default_log_level);
+
+        let (events, filter) = TestLayer::new(filter);
+        let guard = tracing_subscriber::Registry::default()
+            .with(filter)
+            .set_default();
+        (events, guard)
+    }
+
+    pub(crate) struct TestLayer {
+        events: Arc<Mutex<Vec<String>>>,
+        filter: Option<tracing_subscriber::EnvFilter>,
+    }
+
+    impl TestLayer {
+        pub(crate) fn no_filter() -> (Arc<Mutex<Vec<String>>>, Self) {
+            let events = Arc::new(Mutex::new(vec![]));
+            (
+                Arc::clone(&events),
+                Self {
+                    events,
+                    filter: None,
+                },
+            )
+        }
+
+        pub(crate) fn new(
+            filter: tracing_subscriber::EnvFilter,
+        ) -> (Arc<Mutex<Vec<String>>>, Self) {
+            let events = Arc::new(Mutex::new(vec![]));
+            (
+                Arc::clone(&events),
+                Self {
+                    events,
+                    filter: Some(filter),
+                },
+            )
+        }
+    }
+
+    impl<S> tracing_subscriber::Layer<S> for TestLayer
+    where
+        S: tracing::Subscriber,
+    {
+        /// adds events to the Vec of Strings
+        fn on_event(
+            &self,
+            event: &tracing::Event<'_>,
+            _: tracing_subscriber::layer::Context<'_, S>,
+        ) {
+            struct Visitor(Option<String>);
+
+            impl tracing::field::Visit for Visitor {
+                fn record_debug(
+                    &mut self,
+                    field: &tracing::field::Field,
+                    value: &dyn std::fmt::Debug,
+                ) {
+                    if field.name() == "message" {
+                        self.0 = Some(format!("{value:?}"));
+                    }
+                }
+            }
+
+            let mut visitor = Visitor(None);
+            event.record(&mut visitor);
+            let message = visitor.0.unwrap_or_default();
+
+            self.events.lock().unwrap().push(message);
+        }
+
+        /// applies the filter to the logs
+        fn enabled(
+            &self,
+            metadata: &tracing::Metadata<'_>,
+            ctx: tracing_subscriber::layer::Context<'_, S>,
+        ) -> bool {
+            self.filter
+                .as_ref()
+                .map(|filter| filter.enabled(metadata, ctx))
+                .unwrap_or(true)
+        }
+    }
+}
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH proxmox 4/5] log: return the logger configuration after initialisation
  2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
                   ` (2 preceding siblings ...)
  2026-09-21  9:51 ` [PATCH proxmox 3/5] log: add tests to the logger Thomas Ellmenreich
@ 2026-09-21  9:51 ` Thomas Ellmenreich
  2026-09-21  9:51 ` [PATCH datacenter-manager 5/5] api: set REST server debug level based on actual log level Thomas Ellmenreich
  4 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

The logger configuration is returned when the logger is initialised, so
that it can be referenced for other configurations or loggers.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 proxmox-log/src/builder.rs | 135 +++++++++++++++++++++++++++++++++----
 proxmox-log/src/lib.rs     |   6 +-
 2 files changed, 126 insertions(+), 15 deletions(-)

diff --git a/proxmox-log/src/builder.rs b/proxmox-log/src/builder.rs
index 05da3643..e591fecc 100644
--- a/proxmox-log/src/builder.rs
+++ b/proxmox-log/src/builder.rs
@@ -57,7 +57,7 @@ impl<S> Filter<S> for NoWorkerTask {
 /// # func().expect("failed to init logger");
 /// ```
 pub struct Logger {
-    global_log_level: EnvFilter,
+    config: LoggerConfig,
     layer: Vec<Box<dyn Layer<Registry> + Send + Sync + 'static>>,
 }
 
@@ -66,10 +66,8 @@ impl Logger {
     /// variable. If the env variable cannot be retrieved or the content is not parsable,
     /// fallback to the default_log_level passed.
     pub fn from_env(env_var: &str, default_log_level: LevelFilter) -> Logger {
-        let var_content = std::env::var(env_var).ok();
-        let log_level = Self::apply_default_log_level(var_content, default_log_level);
         Logger {
-            global_log_level: log_level,
+            config: LoggerConfig::from_env(env_var, default_log_level),
             layer: vec![],
         }
     }
@@ -136,13 +134,14 @@ impl Logger {
     /// Inits the tracing logger with the previously configured layers.
     ///
     /// Also configures the `LogTracer` which will convert all `log` events to tracing events.
-    pub fn init(self) -> Result<(), anyhow::Error> {
+    pub fn init(self) -> Result<LoggerConfig, anyhow::Error> {
+        let config = self.config.clone();
         let registry = self.create_subscriber();
 
         tracing::subscriber::set_global_default(registry)?;
 
         LogTracer::init()?;
-        Ok(())
+        Ok(config)
     }
 
     /// Creates the subscriber to be used for the Logger
@@ -151,7 +150,76 @@ impl Logger {
     fn create_subscriber(self) -> impl Subscriber {
         tracing_subscriber::registry()
             .with(self.layer)
-            .with(self.global_log_level)
+            .with(self.config.global_log_level)
+    }
+}
+
+/// Struct containing the configuration of the logger
+pub struct LoggerConfig {
+    global_log_level: EnvFilter,
+}
+
+/// TODO: With version 0.3.20 of tracing_subscriber this [`Clone`]
+/// implementation can be replaced with a simple derive
+impl Clone for LoggerConfig {
+    fn clone(&self) -> Self {
+        let global_log_level = self.clone_global_filter();
+        Self { global_log_level }
+    }
+}
+
+impl LoggerConfig {
+    /// Tries to read the environemnt variable with the provided name and
+    /// parse its contents as a [`EnvFilter`]
+    pub fn from_env(env_var: &str, default_log_level: LevelFilter) -> LoggerConfig {
+        let var_content = std::env::var(env_var).ok();
+        Self::from_string(var_content, default_log_level)
+    }
+
+    /// Tries to parse the provided string as a [`EnvFilter`] if present. Falls
+    /// back to the default in all other cases
+    pub fn from_string(
+        env_filter_str: Option<String>,
+        default_log_level: LevelFilter,
+    ) -> LoggerConfig {
+        let global_log_level = Self::apply_default_log_level(env_filter_str, default_log_level);
+        Self { global_log_level }
+    }
+
+    pub fn global_filter(&self) -> EnvFilter {
+        self.clone_global_filter()
+    }
+
+    /// Returns an hint of the highest [verbosity level](https://docs.rs/tracing-core/0.1.32/tracing_core/metadata/struct.Level.html)
+    /// that this `EnvFilter` will enable.
+    ///
+    /// # Panics
+    ///
+    /// Panics if no max can be found, although that should never be the case
+    /// as we force a default log level and thus always have a max.
+    pub fn global_filter_max_level_hint(&self) -> LevelFilter {
+        self.global_log_level
+            .max_level_hint()
+            .expect("because we force a default log level there should always be a hint")
+    }
+
+    /// Clones the global_log_filter and returns said clone. Since the clone
+    /// implementation is not yet available, serializes the filter back into
+    /// a string and then parses it again.
+    ///
+    /// TODO: the current version of EnvFilter provided by the debian package
+    /// does not implement clone although the following [0] version already
+    /// does. So once the version is bumped, replace this hack with a `.clone()`
+    ///
+    /// [0]: https://docs.rs/tracing-subscriber/0.3.20/src/tracing_subscriber/filter/env/mod.rs.html#211-223
+    ///
+    /// # Panics
+    ///
+    /// Panics if, while serializing into a string and deserializing back into
+    /// a [`EnvFilter`], the parsing fails (which should never happen).
+    fn clone_global_filter(&self) -> EnvFilter {
+        EnvFilter::try_new(format!("{}", self.global_log_level))
+            .expect("creating a new envfilter from a existing filter should always be possible")
     }
 
     /// If present, tries to parse the `env_filter_str` as a [`EnvFilter`],
@@ -184,9 +252,9 @@ mod tests {
 
     use tracing::level_filters::LevelFilter;
     use tracing_log::log;
-    use tracing_subscriber::{Layer, util::SubscriberInitExt};
+    use tracing_subscriber::{EnvFilter, Layer, util::SubscriberInitExt};
 
-    use crate::Logger;
+    use crate::{Logger, builder::LoggerConfig};
 
     /// Modules created for testing purposes. Specifically, to test filtering
     /// of logs in different modules.
@@ -209,6 +277,47 @@ mod tests {
         }};
     }
 
+    // TODO: delete once [`EnvFilter`] implements [`Clone`]
+    #[test]
+    fn check_env_filter_display_contains_expected_modules() {
+        // Arrange
+        let filter_str = "warn,proxmox_log::builder::tests::test_module=info,proxmox_log::builder::tests::test_module::nested_module=error";
+
+        // Act
+        let formatted_filter = EnvFilter::try_new(filter_str).unwrap().to_string();
+
+        // Assert
+        let sort_parts = |filter: &str| -> String {
+            let mut parts = filter.split(',').collect::<Vec<_>>();
+            parts.sort();
+            parts.join(",")
+        };
+        assert_eq!(sort_parts(formatted_filter.as_str()), sort_parts(filter_str));
+    }
+
+    // TODO: delete once [`EnvFilter`] implements [`Clone`]
+    #[test]
+    fn check_env_filter_default_formats_as_expected() {
+        // Arrange
+        let default = LevelFilter::WARN;
+
+        // Act
+        let formatted_filter = EnvFilter::default().add_directive(default.into()).to_string();
+
+        // Assert
+        assert_eq!(formatted_filter.as_str(), "warn");
+    }
+
+    // TODO: delete once [`EnvFilter`] implements [`Clone`]
+    #[test]
+    fn check_empty_env_filter_formats_as_expected() {
+        // Act
+        let formatted_filter = EnvFilter::try_new("").unwrap().to_string();
+
+        // Assert
+        assert_eq!(formatted_filter.as_str(), "");
+    }
+
     #[test]
     fn logger_builder_correctly_applies_filter() {
         // Arrange
@@ -348,7 +457,7 @@ mod tests {
         (
             events,
             Logger {
-                global_log_level: Logger::apply_default_log_level(
+                config: LoggerConfig::from_string(
                     var_filter.map(str::to_string),
                     default_log_level,
                 ),
@@ -371,8 +480,10 @@ mod tests {
     ) -> (Arc<Mutex<Vec<String>>>, tracing::subscriber::DefaultGuard) {
         use tracing_subscriber::layer::SubscriberExt;
 
-        let filter =
-            Logger::apply_default_log_level(var_filter.map(str::to_string), default_log_level);
+        let filter = LoggerConfig::apply_default_log_level(
+            var_filter.map(str::to_string),
+            default_log_level,
+        );
 
         let (events, filter) = TestLayer::new(filter);
         let guard = tracing_subscriber::Registry::default()
diff --git a/proxmox-log/src/lib.rs b/proxmox-log/src/lib.rs
index 32c10e27..3c2e4fdd 100644
--- a/proxmox-log/src/lib.rs
+++ b/proxmox-log/src/lib.rs
@@ -13,7 +13,7 @@ mod pve_task_formatter;
 mod tasklog_layer;
 
 pub mod builder;
-pub use builder::Logger;
+pub use builder::{Logger, LoggerConfig};
 pub use file_logger::{FileLogOptions, FileLogger};
 
 pub use tracing::Level;
@@ -154,7 +154,7 @@ where
 pub fn init_logger(
     env_var_name: &str,
     default_log_level: LevelFilter,
-) -> Result<(), anyhow::Error> {
+) -> Result<LoggerConfig, anyhow::Error> {
     Logger::from_env(env_var_name, default_log_level)
         .journald_on_no_workertask()
         .tasklog_pbs()
@@ -168,7 +168,7 @@ pub fn init_logger(
 pub fn init_cli_logger(
     env_var_name: &str,
     default_log_level: LevelFilter,
-) -> Result<(), anyhow::Error> {
+) -> Result<LoggerConfig, anyhow::Error> {
     Logger::from_env(env_var_name, default_log_level)
         .stderr_on_no_workertask()
         .tasklog_pbs()
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH datacenter-manager 5/5] api: set REST server debug level based on actual log level
  2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
                   ` (3 preceding siblings ...)
  2026-09-21  9:51 ` [PATCH proxmox 4/5] log: return the logger configuration after initialisation Thomas Ellmenreich
@ 2026-09-21  9:51 ` Thomas Ellmenreich
  4 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-09-21  9:51 UTC (permalink / raw)
  To: pdm-devel; +Cc: Thomas Ellmenreich

Use the configuration returned by the initialised logger to decide whether to
activate debugging for the REST server.

Previously, whether the REST server was in debug mode would only be determined
by the existence of the PROXMOX_DEBUG variable, regardless of its value. Now,
if the environment variable is set to a less verbose level than 'debug', the
REST server will not activate its debug level.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 server/src/bin/proxmox-datacenter-api/main.rs | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/server/src/bin/proxmox-datacenter-api/main.rs b/server/src/bin/proxmox-datacenter-api/main.rs
index 57aa6e22..7b2d85cb 100644
--- a/server/src/bin/proxmox-datacenter-api/main.rs
+++ b/server/src/bin/proxmox-datacenter-api/main.rs
@@ -46,13 +46,15 @@ fn main() -> Result<(), Error> {
 
     server::env::sanitize_environment_vars();
 
-    let debug = std::env::var("PROXMOX_DEBUG").is_ok();
-
-    proxmox_log::Logger::from_env("PROXMOX_DEBUG", LevelFilter::INFO)
+    let log_config = proxmox_log::Logger::from_env("PROXMOX_DEBUG", LevelFilter::INFO)
         .journald_on_no_workertask()
         .tasklog_pbs()
         .init()?;
 
+    // NOTE: this will now also activate if a very specific module is set
+    // to debug but the default level is still on info
+    let debug = log_config.global_filter_max_level_hint() >= LevelFilter::DEBUG;
+
     if std::env::args().nth(1).is_some() {
         bail!("unexpected command line parameters");
     }
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-21  9:53 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-21  9:51 [RFC datacenter-manager/proxmox 0/5] log: allow finegrained control logging levels Thomas Ellmenreich
2026-09-21  9:51 ` [PATCH proxmox 1/5] log: replace per layer filtering by one global filter Thomas Ellmenreich
2026-09-21  9:51 ` [PATCH proxmox 2/5] log: replace simple level filter with env filter Thomas Ellmenreich
2026-09-21  9:51 ` [PATCH proxmox 3/5] log: add tests to the logger Thomas Ellmenreich
2026-09-21  9:51 ` [PATCH proxmox 4/5] log: return the logger configuration after initialisation Thomas Ellmenreich
2026-09-21  9:51 ` [PATCH datacenter-manager 5/5] api: set REST server debug level based on actual log level Thomas Ellmenreich

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal