diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d21a52f4f1..4a762f30553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ - [#4179](https://github.com/firecracker-microvm/firecracker/pull/4179): Fixed a bug reporting a non-zero exit code on successful shutdown when starting Firecracker with `--no-api`. +- [#4270](https://github.com/firecracker-microvm/firecracker/pull/4270): + Fixed a bug introduced in #4047 that limited the `--level` option of logger + to Pascal-cased values (e.g. accepting "Info", but not "info"). It now + ignores case again. ## [1.5.0] diff --git a/Cargo.lock b/Cargo.lock index fa359a44c32..373f886b335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1409,6 +1409,7 @@ dependencies = [ "device_tree", "displaydoc", "event-manager", + "itertools 0.12.0", "kvm-bindings", "kvm-ioctls", "lazy_static", diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index 65b0f07d1c6..94162116b3e 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -46,6 +46,7 @@ virtio_gen = { path = "../virtio_gen" } criterion = { version = "0.5.0", default-features = false } device_tree = "1.1.0" proptest = { version = "1.0.0", default-features = false, features = ["std"] } +itertools = "0.12.0" [[bench]] name = "cpu_templates" diff --git a/src/vmm/src/logger/logging.rs b/src/vmm/src/logger/logging.rs index fb9a27eacc1..057d6ea2b23 100644 --- a/src/vmm/src/logger/logging.rs +++ b/src/vmm/src/logger/logging.rs @@ -10,7 +10,7 @@ use std::sync::{Mutex, OnceLock}; use std::thread; use log::{Log, Metadata, Record}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use utils::time::LocalTime; use super::metrics::{IncMetric, METRICS}; @@ -188,25 +188,19 @@ pub struct LoggerConfig { /// the log level filter. It would be a breaking change to no longer support this. In the next /// breaking release this should be removed (replaced with `log::LevelFilter` and only supporting /// its default deserialization). -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] pub enum LevelFilter { /// [`log::LevelFilter:Off`] - #[serde(alias = "OFF")] Off, /// [`log::LevelFilter:Trace`] - #[serde(alias = "TRACE")] Trace, /// [`log::LevelFilter:Debug`] - #[serde(alias = "DEBUG")] Debug, /// [`log::LevelFilter:Info`] - #[serde(alias = "INFO")] Info, /// [`log::LevelFilter:Warn`] - #[serde(alias = "WARN", alias = "WARNING", alias = "Warning")] Warn, /// [`log::LevelFilter:Error`] - #[serde(alias = "ERROR")] Error, } impl From for log::LevelFilter { @@ -221,6 +215,25 @@ impl From for log::LevelFilter { } } } +impl<'de> Deserialize<'de> for LevelFilter { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + let key = String::deserialize(deserializer)?; + let level = match key.to_lowercase().as_str() { + "off" => Ok(LevelFilter::Off), + "trace" => Ok(LevelFilter::Trace), + "debug" => Ok(LevelFilter::Debug), + "info" => Ok(LevelFilter::Info), + "warn" | "warning" => Ok(LevelFilter::Warn), + "error" => Ok(LevelFilter::Error), + _ => Err(D::Error::custom("Invalid LevelFilter")), + }; + level + } +} /// Error type for [`::from_str`]. #[derive(Debug, PartialEq, Eq, thiserror::Error)] @@ -230,13 +243,13 @@ pub struct LevelFilterFromStrError(String); impl FromStr for LevelFilter { type Err = LevelFilterFromStrError; fn from_str(s: &str) -> Result { - match s { - "Off" | "OFF" => Ok(Self::Off), - "Trace" | "TRACE" => Ok(Self::Trace), - "Debug" | "DEBUG" => Ok(Self::Debug), - "Info" | "INFO" => Ok(Self::Info), - "Warn" | "WARN" | "Warning" | "WARNING" => Ok(Self::Warn), - "Error" | "ERROR" => Ok(Self::Error), + match s.to_ascii_lowercase().as_str() { + "off" => Ok(Self::Off), + "trace" => Ok(Self::Trace), + "debug" => Ok(Self::Debug), + "info" => Ok(Self::Info), + "warn" | "warning" => Ok(Self::Warn), + "error" => Ok(Self::Error), _ => Err(LevelFilterFromStrError(String::from(s))), } } @@ -275,29 +288,52 @@ mod tests { log::LevelFilter::Error ); } + #[test] - fn levelfilter_from_str() { + fn levelfilter_from_str_all_variants() { + use itertools::Itertools; + + #[derive(Deserialize)] + struct Foo { + #[allow(dead_code)] + level: LevelFilter, + } + + for (level, level_enum) in [ + ("off", LevelFilter::Off), + ("trace", LevelFilter::Trace), + ("debug", LevelFilter::Debug), + ("info", LevelFilter::Info), + ("warn", LevelFilter::Warn), + ("warning", LevelFilter::Warn), + ("error", LevelFilter::Error), + ] { + let multi = level.chars().map(|_| 0..=1).multi_cartesian_product(); + for combination in multi { + let variant = level + .chars() + .zip_eq(combination) + .map(|(c, v)| match v { + 0 => c.to_ascii_lowercase(), + 1 => c.to_ascii_uppercase(), + _ => unreachable!(), + }) + .collect::(); + + let ex = format!("{{ \"level\": \"{}\" }}", variant); + assert_eq!(LevelFilter::from_str(&variant), Ok(level_enum)); + assert!(serde_json::from_str::(&ex).is_ok(), "{ex}"); + } + } + let ex = "{{ \"level\": \"blah\" }}".to_string(); + assert!( + serde_json::from_str::(&ex).is_err(), + "expected error got {ex:#?}" + ); assert_eq!( LevelFilter::from_str("bad"), Err(LevelFilterFromStrError(String::from("bad"))) ); - - assert_eq!(LevelFilter::from_str("Off"), Ok(LevelFilter::Off)); - assert_eq!(LevelFilter::from_str("Trace"), Ok(LevelFilter::Trace)); - assert_eq!(LevelFilter::from_str("Debug"), Ok(LevelFilter::Debug)); - assert_eq!(LevelFilter::from_str("Info"), Ok(LevelFilter::Info)); - assert_eq!(LevelFilter::from_str("Warn"), Ok(LevelFilter::Warn)); - assert_eq!(LevelFilter::from_str("Error"), Ok(LevelFilter::Error)); - - assert_eq!(LevelFilter::from_str("OFF"), Ok(LevelFilter::Off)); - assert_eq!(LevelFilter::from_str("TRACE"), Ok(LevelFilter::Trace)); - assert_eq!(LevelFilter::from_str("DEBUG"), Ok(LevelFilter::Debug)); - assert_eq!(LevelFilter::from_str("INFO"), Ok(LevelFilter::Info)); - assert_eq!(LevelFilter::from_str("WARN"), Ok(LevelFilter::Warn)); - assert_eq!(LevelFilter::from_str("ERROR"), Ok(LevelFilter::Error)); - - assert_eq!(LevelFilter::from_str("Warning"), Ok(LevelFilter::Warn)); - assert_eq!(LevelFilter::from_str("WARNING"), Ok(LevelFilter::Warn)); } #[test]