Skip to main content

consortium_log/
lib.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! A tiny logging facade that forwards to `defmt`, `tracing`, both, or
18//! neither, chosen by Cargo features.
19//!
20//! Consortium spans `no_std` firmware (which logs with `defmt`) and `std`
21//! host-side tooling such as the config codegen (which logs with `tracing`).
22//! This crate lets a single call site — `consortium_log::trace!(...)` — compile
23//! against whichever backend the consuming crate turns on:
24//!
25//! - `defmt` routes each macro to the matching `defmt` macro.
26//! - `tracing` routes each macro to the matching `tracing` macro. `tracing`
27//!   also enables [`alloc`](#features) because the `tracing` dependency needs
28//!   it.
29//! - With both enabled, every event is emitted to both backends.
30//! - With neither enabled, the macros expand to nothing.
31//!
32//! # Format syntax
33//!
34//! The portable subset is a format string plus positional arguments —
35//! `trace!("channel {} lowered", name)` — or Rust-style implicit captures —
36//! `trace!("channel {name} lowered")`. Implicit captures are normalized to
37//! indexed arguments before being forwarded, because `defmt` does not parse
38//! them itself. Each argument must implement the traits both backends require
39//! (`defmt::Format` for `defmt`, `core::fmt::Display`/`Debug` for `tracing`).
40//! Backend-specific extensions (`defmt`'s `{=u8}` type hints, `tracing`'s
41//! structured fields) are only valid when that backend is the sole one enabled.
42//!
43//! Log arguments must be side-effect free: when no backend is enabled the
44//! macros expand to nothing and the arguments are never evaluated.
45//!
46//! # Features
47//!
48//! - `defmt` — forward to the `defmt` logging macros. No direct `defmt`
49//!   dependency is needed at the call site: this crate re-exports `defmt` (the
50//!   re-export exists only when the feature is on, so it is not linked here)
51//!   and the dispatch macro binds it into scope for the expansion.
52//! - `tracing` — forward to the `tracing` logging macros (implies `alloc`).
53//!   `tracing`'s macros are `$crate`-hygienic, so no direct dependency is
54//!   needed beyond this crate.
55//! - `alloc` — marker capability required by `tracing`; enabling it alone pulls
56//!   no backend.
57//!
58//! Neither backend leaks its *name* into the call site, so enabling one on this
59//! crate alone resolves. That matters under Cargo feature unification: one
60//! workspace member turning on `consortium-log/defmt` switches the facade for
61//! *every* member sharing the build, including ones whose own `defmt` feature
62//! is off and which therefore have no `defmt` in their extern prelude.
63//!
64//! Names are only half of it, though. With the `defmt` backend live, every
65//! logged argument needs a `defmt::Format` impl, and a crate that gates its
66//! impls on its *own* `defmt` feature will not have emitted them — foreign
67//! types such as `anyhow::Error` cannot have them at all. Gate those impls with
68//! [`when_defmt!`](when_defmt) so they track the backend, and, for the types
69//! that can never implement it, keep `consortium-log/defmt` out of any `default`
70//! feature so a host build never switches the backend on by accident.
71
72#![no_std]
73
74#[doc(hidden)]
75pub use consortium_log_macros::expand_captures as __expand_captures;
76
77// Each backend is re-exported twice, because the two re-exports do different
78// jobs and neither substitutes for the other.
79//
80// The macros are aliased *by name* (`__defmt_info`, …) because that is the
81// reliable way to make them resolvable through `$crate::…` from a downstream
82// crate: a bare crate re-export does not reliably expose a `#[macro_export]`
83// macro by path across crates.
84//
85// The crate itself is re-exported because naming the macro is only half of it —
86// the macro's *expansion* also has to resolve. See [`defmt`].
87
88/// Re-export of the `defmt` crate, so the facade can supply the name itself.
89///
90/// Aliasing the level macros is not sufficient on its own. `defmt`'s
91/// proc-macros emit *bare* `defmt::export::…` paths (not `::defmt::…`, and not
92/// `$crate`-relative — a proc-macro cannot synthesize `$crate`), and those
93/// tokens carry call-site hygiene, so they resolve in the module that wrote the
94/// log call. A crate with `consortium-log/defmt` enabled but no `defmt` of its
95/// own has no such name, which is the feature-unification failure this
96/// re-export exists to prevent. [`__defmt`] binds it as `defmt` in the block it
97/// expands into; that `use` shares call-site hygiene with the emitted paths, so
98/// they resolve to it.
99///
100/// Downstream crates should reach `defmt` items through this re-export rather
101/// than through a direct dependency. A crate that writes its own
102/// `defmt::Format` impls still needs a direct `defmt` dependency for the trait
103/// path, gated on its own `defmt` feature, so the impls track the same feature
104/// that selects the backend.
105#[cfg(feature = "defmt")]
106pub use defmt;
107#[cfg(feature = "defmt")]
108#[doc(hidden)]
109pub use defmt::{
110    debug as __defmt_debug, error as __defmt_error, info as __defmt_info, trace as __defmt_trace,
111    warn as __defmt_warn,
112};
113#[cfg(feature = "tracing")]
114#[doc(hidden)]
115pub use tracing::{
116    debug as __tracing_debug, error as __tracing_error, info as __tracing_info,
117    trace as __tracing_trace, warn as __tracing_warn,
118};
119
120// Backend dispatch. `#[cfg]` *inside* an exported macro's body is resolved
121// against the *calling* crate's features, not this crate's, so the selection
122// must happen where it can see this crate's features: on the definitions of the
123// `__defmt`/`__tracing` dispatch macros, gated once each. When a backend is off,
124// its dispatch is the no-op arm. The public level macros carry no `#[cfg]`.
125
126/// Routes a level to the aliased `defmt` macro, or nothing when `defmt` is off.
127///
128/// Each arm expands to a block that first binds the crate's [`defmt`]
129/// re-export as `defmt`,
130/// which is what lets the bare `defmt::export::…` paths in the expansion
131/// resolve without a `defmt` dependency at the call site. The binding is scoped
132/// to the block, so it neither collides with nor shadows a `defmt` the calling
133/// crate already has in scope.
134#[cfg(feature = "defmt")]
135#[doc(hidden)]
136#[macro_export]
137macro_rules! __defmt {
138    (trace, $($a:tt)*) => {{ use $crate::defmt; $crate::__defmt_trace!($($a)*) }};
139    (debug, $($a:tt)*) => {{ use $crate::defmt; $crate::__defmt_debug!($($a)*) }};
140    (info,  $($a:tt)*) => {{ use $crate::defmt; $crate::__defmt_info!($($a)*) }};
141    (warn,  $($a:tt)*) => {{ use $crate::defmt; $crate::__defmt_warn!($($a)*) }};
142    (error, $($a:tt)*) => {{ use $crate::defmt; $crate::__defmt_error!($($a)*) }};
143}
144#[cfg(not(feature = "defmt"))]
145#[doc(hidden)]
146#[macro_export]
147macro_rules! __defmt {
148    ($level:ident, $($a:tt)*) => {{}};
149}
150
151/// Emits the items it wraps only when this crate's `defmt` backend is enabled.
152///
153/// The item-position counterpart to the level macros, and gated the same way:
154/// on the *macro definition*, so the items track `consortium-log`'s `defmt`
155/// feature rather than the calling crate's.
156///
157/// That distinction is the point. A `defmt::Format` impl gated on the calling
158/// crate's own `defmt` feature goes missing exactly when feature unification
159/// turns the backend on from elsewhere — the log call sites then expand to
160/// `defmt` code that demands `Format` impls which were never emitted. Gating
161/// the impls on the backend instead keeps the two in step no matter which
162/// member enabled it.
163///
164/// Name `defmt` items through the [`defmt`] re-export, so the block needs no
165/// direct dependency:
166///
167/// ```ignore
168/// consortium_log::when_defmt! {
169///     impl consortium_log::defmt::Format for Chan {
170///         fn format(&self, f: consortium_log::defmt::Formatter<'_>) {
171///             // `defmt::write!` expands to bare `defmt::…` paths, so bind the
172///             // re-export in the body they expand into.
173///             use consortium_log::defmt;
174///             defmt::write!(f, "Chan({=u8})", self.0)
175///         }
176///     }
177/// }
178/// ```
179#[cfg(feature = "defmt")]
180#[macro_export]
181macro_rules! when_defmt {
182    ($($item:tt)*) => { $($item)* };
183}
184
185/// Drops the items it wraps, because this crate's `defmt` backend is disabled.
186#[cfg(not(feature = "defmt"))]
187#[macro_export]
188macro_rules! when_defmt {
189    ($($item:tt)*) => {};
190}
191
192/// Routes a level to the aliased `tracing` macro, or nothing when it is off.
193#[cfg(feature = "tracing")]
194#[doc(hidden)]
195#[macro_export]
196macro_rules! __tracing {
197    (trace, $($a:tt)*) => { $crate::__tracing_trace!($($a)*) };
198    (debug, $($a:tt)*) => { $crate::__tracing_debug!($($a)*) };
199    (info,  $($a:tt)*) => { $crate::__tracing_info!($($a)*) };
200    (warn,  $($a:tt)*) => { $crate::__tracing_warn!($($a)*) };
201    (error, $($a:tt)*) => { $crate::__tracing_error!($($a)*) };
202}
203
204#[cfg(not(feature = "tracing"))]
205#[doc(hidden)]
206#[macro_export]
207macro_rules! __tracing {
208    ($level:ident, $($a:tt)*) => {{}};
209}
210
211// When both backends are disabled, still expand the portable format arguments
212// so variables referenced only by a log call count as used. The false branch
213// ensures argument expressions are never evaluated at runtime.
214#[cfg(not(any(feature = "defmt", feature = "tracing")))]
215#[doc(hidden)]
216#[macro_export]
217macro_rules! __disabled {
218    ($($a:tt)*) => {{
219        #[allow(clippy::all)]
220        if false {
221            let _ = core::format_args!($($a)*);
222        }
223    }};
224}
225#[cfg(any(feature = "defmt", feature = "tracing"))]
226#[doc(hidden)]
227#[macro_export]
228macro_rules! __disabled {
229    ($($a:tt)*) => {{}};
230}
231
232/// Logs at the `trace` level to every enabled backend. See the
233/// [crate docs](crate) for the portable format syntax.
234#[macro_export]
235macro_rules! trace {
236    ($($arg:tt)*) => {{
237        $crate::__expand_captures!(($crate::__log) (trace) ($($arg)*));
238    }};
239}
240
241/// Logs at the `debug` level to every enabled backend.
242#[macro_export]
243macro_rules! debug {
244    ($($arg:tt)*) => {{
245        $crate::__expand_captures!(($crate::__log) (debug) ($($arg)*));
246    }};
247}
248
249/// Logs at the `info` level to every enabled backend.
250#[macro_export]
251macro_rules! info {
252    ($($arg:tt)*) => {{
253        $crate::__expand_captures!(($crate::__log) (info) ($($arg)*));
254    }};
255}
256
257/// Logs at the `warn` level to every enabled backend.
258#[macro_export]
259macro_rules! warn {
260    ($($arg:tt)*) => {{
261        $crate::__expand_captures!(($crate::__log) (warn) ($($arg)*));
262    }};
263}
264
265/// Logs at the `error` level to every enabled backend.
266#[macro_export]
267macro_rules! error {
268    ($($arg:tt)*) => {{
269        $crate::__expand_captures!(($crate::__log) (error) ($($arg)*));
270    }};
271}
272
273#[doc(hidden)]
274#[macro_export]
275macro_rules! __log {
276    ($level:ident, $($arg:tt)*) => {{
277        $crate::__defmt!($level, $($arg)*);
278        $crate::__tracing!($level, $($arg)*);
279        $crate::__disabled!($($arg)*);
280    }};
281}
282
283// This function is never run: it keeps implicit-capture expansion type-checked
284// for the no-backend, tracing, defmt, and combined feature configurations
285// without requiring a linked defmt global logger in tests.
286#[cfg(test)]
287#[allow(dead_code)]
288fn implicit_capture_compiles_for_enabled_backends() {
289    let channel = 3u8;
290    crate::info!("captured channel {channel}");
291}
292
293// Exercised with the `tracing` backend only: a `defmt`-enabled test binary would
294// need a linked `#[defmt::global_logger]`, which a plain unit test cannot supply.
295#[cfg(all(test, feature = "tracing", not(feature = "defmt")))]
296mod tests {
297    // The macros forward the portable subset (format string + positional args)
298    // to the enabled backend; here that is `tracing`. This exercises each level
299    // and each supported arity so a broken expansion fails to compile.
300    #[test]
301    fn every_level_forwards() {
302        let name = "cm33";
303        let channel = 3u8;
304
305        crate::trace!("no args");
306        crate::debug!("one arg {}", name);
307        crate::info!("two args {} {}", name, channel);
308        crate::warn!("warn {}", channel);
309        crate::error!("error {}", name);
310        crate::error!("captured channel {channel} for {name}");
311        crate::error!("captured {name} then positional {}", channel);
312    }
313}