From fdd5c57773d04334fa0fc20316486c87b02a0d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Schn=C3=B6rch?= Date: Sat, 24 Oct 2026 21:35:45 +0200 Subject: [PATCH] feat(core): the log_*! macros forward to defmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `log_trace!` … `log_error!` gain a third destination: `defmt`, under the calling crate's `defmt` feature, next to `tracing` and `log`. On an Embassy device every core log line now reaches the probe log (the session engine had none, #352). - `aimdb-core/src/log.rs`: a `::defmt::…!` arm per macro; the module docs give the rule: a crate forwarding `defmt` needs `dep:defmt` (defmt's macros expand to absolute `defmt::` paths, so a re-export would not help), its arguments implement `defmt::Format`, and a `std`-only crate names the feature through `check-cfg`. - `LogDisplay` / `LogDebug`: wrap a foreign type without `defmt::Format` in a log call (Display2Format / Debug2Format under defmt; tracing/log output unchanged). Used at ten call sites in the zenoh and MQTT std backends, whose `rumqttc`/zenoh types would otherwise break an all-features build (docs.rs builds both crates with `std` and `defmt` together). - `aimdb-knx-connector`: `dep:defmt`, and `defmt/ip_in_core` for `SocketAddr`. - `aimdb-zenoh-connector`: a `defmt` feature for its embedded build. - `aimdb-sync`, `aimdb-uds-connector` (std only): `check-cfg` for the name. - Makefile: thumbv7em check and clippy for `aimdb-core` with `defmt`, and for the zenoh embedded build with `defmt`. Part of #352. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YPDjQ947yKjLLAq3Xzgu3e --- Cargo.lock | 2 + Makefile | 10 +++ aimdb-core/src/lib.rs | 1 + aimdb-core/src/log.rs | 97 +++++++++++++++++++++++------ aimdb-knx-connector/Cargo.toml | 3 +- aimdb-mqtt-connector/src/native.rs | 9 ++- aimdb-sync/Cargo.toml | 5 ++ aimdb-uds-connector/Cargo.toml | 5 ++ aimdb-zenoh-connector/Cargo.toml | 3 + aimdb-zenoh-connector/src/native.rs | 9 +-- aimdb-zenoh-connector/src/ros2.rs | 12 ++-- 11 files changed, 127 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47dcbac5..d1498291 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,7 @@ dependencies = [ "aimdb-core", "aimdb-knx-pico", "aimdb-tokio-adapter", + "defmt 1.1.1", "embassy-futures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "heapless 0.8.0", "tokio", @@ -449,6 +450,7 @@ dependencies = [ "aimdb-core", "aimdb-data-contracts", "aimdb-tokio-adapter", + "defmt 1.1.1", "serde", "tokio", "trybuild", diff --git a/Makefile b/Makefile index b3942b0f..bfd0b06f 100644 --- a/Makefile +++ b/Makefile @@ -415,6 +415,10 @@ clippy: cargo clippy --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,embassy-tls,defmt" -- -D warnings @printf "$(YELLOW) → Clippy on KNX connector (neutral + defmt)$(NC)\n" cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "connector,defmt" -- -D warnings + @printf "$(YELLOW) → Clippy on aimdb-core (no_std + defmt log destination)$(NC)\n" + cargo clippy --package aimdb-core --target thumbv7em-none-eabihf --no-default-features --features "alloc,connector-session,remote,defmt" -- -D warnings + @printf "$(YELLOW) → Clippy on Zenoh connector (embedded + defmt)$(NC)\n" + cargo clippy --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --no-default-features --features "embedded,defmt" -- -D warnings @printf "$(YELLOW) → Clippy on WebSocket connector$(NC)\n" cargo clippy --package aimdb-websocket-connector --features "tokio-runtime,client" --all-targets -- -D warnings @printf "$(YELLOW) → Clippy on UDS connector$(NC)\n" @@ -558,6 +562,10 @@ test-embedded: cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features alloc @printf "$(YELLOW) → Checking aimdb-core (no_std + log destination) on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,log" + @printf "$(YELLOW) → Checking aimdb-core (no_std + defmt log destination) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,defmt" + @printf "$(YELLOW) → Checking aimdb-core (full no_std AimX server + defmt log destination) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-core --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "alloc,connector-session,remote,defmt" @printf "$(YELLOW) → Checking aimdb-embassy-adapter on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime" @printf "$(YELLOW) → Checking aimdb-embassy-adapter with network support on thumbv7em-none-eabihf target$(NC)\n" @@ -597,6 +605,8 @@ test-embedded: @printf "$(BLUE)✓ mountain-mqtt is the codec alone$(NC)\n" @printf "$(YELLOW) → Checking aimdb-zenoh-connector (embedded backend) on thumbv7em-none-eabihf target$(NC)\n" cargo check --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embedded" + @printf "$(YELLOW) → Checking aimdb-zenoh-connector (embedded backend + defmt) on thumbv7em-none-eabihf target$(NC)\n" + cargo check --package aimdb-zenoh-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embedded,defmt" @printf "$(YELLOW) → Asserting no std-only Zenoh crates in the embedded Zenoh backend$(NC)\n" @err=$$(mktemp); \ out=$$(cargo tree -p aimdb-zenoh-connector --target thumbv7em-none-eabihf --no-default-features --features "embedded" -e features,no-dev 2>$$err) || { \ diff --git a/aimdb-core/src/lib.rs b/aimdb-core/src/lib.rs index b770c5fe..e36b5536 100644 --- a/aimdb-core/src/lib.rs +++ b/aimdb-core/src/lib.rs @@ -123,6 +123,7 @@ pub mod typed_record; pub use context::RuntimeContext; pub use error::{ConfigError, DbError, DbErrorKind, DbResult}; pub use extensions::Extensions; +pub use log::{LogDebug, LogDisplay}; // Runtime capability surface: the runtime travels as `Arc`, // the one trait an adapter implements. diff --git a/aimdb-core/src/log.rs b/aimdb-core/src/log.rs index fe2136f6..2fa76aba 100644 --- a/aimdb-core/src/log.rs +++ b/aimdb-core/src/log.rs @@ -2,18 +2,20 @@ //! //! `log_trace!`/`log_debug!`/`log_info!`/`log_warn!`/`log_error!` forward to every enabled //! *destination* — the `tracing` event macro under the `tracing` feature, the -//! `log` crate under the `log` feature — and otherwise expand to a no-op that -//! still borrows the arguments, so call sites compile identically (no -//! unused-variable warnings) under every feature combination. This replaces the -//! per-call-site `#[cfg(feature = "tracing")]` gates. +//! `log` crate under the `log` feature, `defmt` under the `defmt` feature — and +//! otherwise expand to a no-op that still borrows the arguments, so call sites +//! compile identically (no unused-variable warnings) under every feature +//! combination. This replaces the per-call-site `#[cfg(feature = "tracing")]` +//! gates. //! //! Notes: //! - The no-op branch *borrows* (and therefore evaluates) the arguments — keep //! them cheap (getters, lengths, references), never allocate in hot paths. -//! - `defmt` is intentionally not folded in: most call sites use `{:?}` with -//! types that do not implement `defmt::Format` (e.g. `DbError`, `String`, -//! `Vec`). The few sites that mirror events to defmt (router.rs) -//! keep explicit `#[cfg(feature = "defmt")]` gates next to these macros. +//! - Under `defmt`, every argument must implement `defmt::Format` (the format +//! strings are shared: `defmt` reads `{}` and `{:?}` alike). Core's own +//! types do (#352); a crate logging its own types derives or implements +//! `Format` for them under its `defmt` feature, and wraps a foreign type in +//! [`LogDisplay`] / [`LogDebug`]. //! //! # Mirroring the feature //! @@ -23,19 +25,30 @@ //! nowhere: //! //! ```toml -//! tracing = ["aimdb-core/tracing"] # no `dep:tracing` -//! log = ["aimdb-core/log"] # no `dep:log` +//! tracing = ["aimdb-core/tracing"] # no `dep:tracing` +//! log = ["aimdb-core/log"] # no `dep:log` +//! defmt = ["dep:defmt", "aimdb-core/defmt"] # `dep:defmt` required //! ``` //! -//! Neither arm needs the dependency declared: both go through `$crate::__private`. -//! The *feature* is what cannot be dropped — a `#[cfg]` in a `#[macro_export]`ed -//! macro is resolved where it expands, so a crate without it would emit nothing. +//! `tracing` and `log` need no dependency declared: both arms go through +//! `$crate::__private`. `defmt` does: its macros expand to absolute `defmt::` +//! paths, so the calling crate needs `defmt` itself. The *feature* is what +//! cannot be dropped — a `#[cfg]` in a `#[macro_export]`ed macro is resolved +//! where it expands, so a crate without it would emit nothing. //! //! That is not a silent failure any more: an undeclared feature name makes //! `unexpected_cfgs` fire at every call site, which is a warning in an ordinary //! build and an error under `-D warnings`, as `make clippy` runs it. A dropped //! mirror therefore breaks CI rather than quietly muting a crate. //! `aimdb-sync/tests/log_facade.rs` additionally proves delivery end to end. +//! +//! A crate that never builds with `defmt` (a `std`-only connector) declares the +//! name instead, so the `defmt` arm's `#[cfg]` is known to the lint: +//! +//! ```toml +//! [lints.rust] +//! unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } +//! ``` #[macro_export] macro_rules! log_trace { @@ -44,7 +57,9 @@ macro_rules! log_trace { $crate::__private::tracing::trace!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::trace!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -56,7 +71,9 @@ macro_rules! log_debug { $crate::__private::tracing::debug!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::debug!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -68,7 +85,9 @@ macro_rules! log_info { $crate::__private::tracing::info!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::info!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -80,7 +99,9 @@ macro_rules! log_warn { $crate::__private::tracing::warn!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::warn!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } @@ -92,7 +113,47 @@ macro_rules! log_error { $crate::__private::tracing::error!($s $(, $x)*); #[cfg(feature = "log")] $crate::__private::log::error!($s $(, $x)*); - #[cfg(not(any(feature = "tracing", feature = "log")))] + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "tracing", feature = "log", feature = "defmt")))] { let _ = ($( & $x ),*); } }}; } + +/// Logs `T` through its `Display` impl in every destination, `defmt` included +/// (via `defmt::Display2Format`). +/// +/// For a foreign type that does not implement `defmt::Format` in a `log_*!` +/// call: `log_warn!("send failed: {}", LogDisplay(&e))`. The `tracing` and +/// `log` output is unchanged; under `defmt` the text is formatted on the +/// device, not interned, so keep it to error paths. +pub struct LogDisplay<'a, T: ?Sized>(pub &'a T); + +impl core::fmt::Display for LogDisplay<'_, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for LogDisplay<'_, T> { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Display2Format(self)) + } +} + +/// Like [`LogDisplay`], through `Debug` (`{:?}`; `defmt::Debug2Format`). +pub struct LogDebug<'a, T: ?Sized>(pub &'a T); + +impl core::fmt::Debug for LogDebug<'_, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for LogDebug<'_, T> { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "{}", defmt::Debug2Format(self)) + } +} diff --git a/aimdb-knx-connector/Cargo.toml b/aimdb-knx-connector/Cargo.toml index 198c074f..35f7a500 100644 --- a/aimdb-knx-connector/Cargo.toml +++ b/aimdb-knx-connector/Cargo.toml @@ -47,9 +47,10 @@ critical-section-std-impl = [] # where it expands, so without them this crate would emit nothing. tracing = ["aimdb-core/tracing"] log = ["aimdb-core/log"] -defmt = ["aimdb-core/defmt", "knx-pico/defmt"] +defmt = ["dep:defmt", "defmt/ip_in_core", "aimdb-core/defmt", "knx-pico/defmt"] [dependencies] +defmt = { workspace = true, optional = true } aimdb-core = { version = "2.0.0", path = "../aimdb-core", default-features = false } # aimdb-dev fork: upstream 0.3.0 panics on an npdu_length=1 telegram. A patch diff --git a/aimdb-mqtt-connector/src/native.rs b/aimdb-mqtt-connector/src/native.rs index 3e66bcb1..72eb1dc7 100644 --- a/aimdb-mqtt-connector/src/native.rs +++ b/aimdb-mqtt-connector/src/native.rs @@ -8,6 +8,7 @@ use aimdb_core::connector::ConnectorUrl; use aimdb_core::{log_debug, log_error, log_info}; use aimdb_core::{InboundDispatch, OutboundRoutes}; +use aimdb_core::{LogDebug, LogDisplay}; use rumqttc::{AsyncClient, Event, EventLoop, MqttOptions, Packet}; use std::future::Future; use std::pin::Pin; @@ -212,7 +213,11 @@ async fn run_event_loop(mut event_loop: EventLoop, inbound: InboundDispatch, _br } Ok(_) => {} Err(_e) => { - log_error!("MQTT event loop error for {}: {:?}", _broker_key, _e); + log_error!( + "MQTT event loop error for {}: {:?}", + _broker_key, + LogDebug(&_e) + ); tokio::time::sleep(Duration::from_secs(5)).await; } } @@ -255,7 +260,7 @@ async fn run_publish_loop( log_error!( "MQTT publish on route '{}' failed: {}", outbound.routes()[id].default_topic, - _e + LogDisplay(&_e) ); outbound.reject(id); } diff --git a/aimdb-sync/Cargo.toml b/aimdb-sync/Cargo.toml index fb36c38c..18302528 100644 --- a/aimdb-sync/Cargo.toml +++ b/aimdb-sync/Cargo.toml @@ -66,3 +66,8 @@ libc = { version = "0.2", default-features = false, optional = true } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +# `aimdb-core`'s log macros carry a `#[cfg(feature = "defmt")]` arm that +# expands here; this crate never builds with defmt, so it only names it. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } diff --git a/aimdb-uds-connector/Cargo.toml b/aimdb-uds-connector/Cargo.toml index c2f85219..6dd49be7 100644 --- a/aimdb-uds-connector/Cargo.toml +++ b/aimdb-uds-connector/Cargo.toml @@ -36,3 +36,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } # For the crate-level doc example (a mirrored record needs a JSON shape) serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + +# `aimdb-core`'s log macros carry a `#[cfg(feature = "defmt")]` arm that +# expands here; this crate never builds with defmt, so it only names it. +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("defmt"))'] } diff --git a/aimdb-zenoh-connector/Cargo.toml b/aimdb-zenoh-connector/Cargo.toml index fbe8091d..2ca97474 100644 --- a/aimdb-zenoh-connector/Cargo.toml +++ b/aimdb-zenoh-connector/Cargo.toml @@ -35,8 +35,11 @@ log = ["aimdb-core/log"] # Reserved for the `zenoh-nostd` backend (`zenoh://` only), once zenoh-nostd is # on crates.io; today the `no_std` parts such as `ZenohGrammar`. embedded = ["aimdb-core/alloc"] +# The log lines reach defmt (core's `log_*!` macros), for the embedded build. +defmt = ["dep:defmt", "aimdb-core/defmt"] [dependencies] +defmt = { workspace = true, optional = true } aimdb-core = { version = "2.0.0", path = "../aimdb-core", default-features = false } zenoh = { version = "1.10.1", default-features = false, features = [ "transport_tcp", diff --git a/aimdb-zenoh-connector/src/native.rs b/aimdb-zenoh-connector/src/native.rs index 982604b1..e2203626 100644 --- a/aimdb-zenoh-connector/src/native.rs +++ b/aimdb-zenoh-connector/src/native.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use aimdb_core::LogDisplay; use aimdb_core::{ log_debug, log_error, log_info, log_warn, AimDb, DbError, DbResult, InboundDispatch, OutboundRoutes, RouteInfo, TopicFilter, TopicGrammar, TopicPattern, MAX_CAPTURES, @@ -171,7 +172,7 @@ impl ZenohParts { { Ok(publisher) => publishers.push(Some(publisher)), Err(_e) => { - log_error!("Zenoh: cannot declare a publisher: {}", _e); + log_error!("Zenoh: cannot declare a publisher: {}", LogDisplay(&_e)); publishers.push(None); } } @@ -200,7 +201,7 @@ impl ZenohParts { log_error!( "Zenoh: put on route '{}' failed: {}", outbound.routes()[id].default_topic, - _e + LogDisplay(&_e) ); outbound.reject(id); } @@ -238,7 +239,7 @@ pub(crate) async fn open(config: zenoh::Config) -> Session { } } }; - log_info!("Zenoh: session {} open", session.zid()); + log_info!("Zenoh: session {} open", LogDisplay(&session.zid())); session } @@ -323,7 +324,7 @@ async fn subscribe( subscribers.push(subscriber); earlier.push(Arc::from(compiled)); } - Err(_e) => log_error!("Zenoh: cannot declare a subscriber: {}", _e), + Err(_e) => log_error!("Zenoh: cannot declare a subscriber: {}", LogDisplay(&_e)), } } subscribers diff --git a/aimdb-zenoh-connector/src/ros2.rs b/aimdb-zenoh-connector/src/ros2.rs index 9ad4c187..7409e4e6 100644 --- a/aimdb-zenoh-connector/src/ros2.rs +++ b/aimdb-zenoh-connector/src/ros2.rs @@ -9,6 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::vec::Vec; use aimdb_core::connector::ConnectorBuilder; +use aimdb_core::LogDisplay; use aimdb_core::{ log_error, log_warn, AimDb, ExactGrammar, InboundDispatch, OutboundRoutes, RouteInfo, }; @@ -487,7 +488,10 @@ impl Ros2Parts { for token in tokens { match session.liveliness().declare_token(token).await { Ok(token) => live.push(token), - Err(_e) => log_error!("ROS 2: cannot declare a liveliness token: {}", _e), + Err(_e) => log_error!( + "ROS 2: cannot declare a liveliness token: {}", + LogDisplay(&_e) + ), } } @@ -506,7 +510,7 @@ impl Ros2Parts { .await; match declared { Ok(subscriber) => subscribers.push(subscriber), - Err(_e) => log_error!("ROS 2: cannot declare a subscriber: {}", _e), + Err(_e) => log_error!("ROS 2: cannot declare a subscriber: {}", LogDisplay(&_e)), } } @@ -523,7 +527,7 @@ impl Ros2Parts { log_error!( "ROS 2: cannot declare a publisher on '{}': {}", publisher.link.topic, - _e + LogDisplay(&_e) ); senders.push(None); } @@ -551,7 +555,7 @@ impl Ros2Parts { log_error!( "ROS 2: put on '{}' failed: {}", publishers[id].link.topic, - _e + LogDisplay(&_e) ); outbound.reject(id); }