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. `defmt`'s proc-macros
49//!   expand to literal `defmt::…` paths resolved at the call site, so a crate
50//!   that enables this backend **must also depend on `defmt` directly** (the
51//!   facade cannot supply the name). The workspace's firmware crates already do.
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#![no_std]
59
60#[doc(hidden)]
61pub use consortium_log_macros::expand_captures as __expand_captures;
62
63// Re-export each backend macro *by name* under a hidden alias. Re-exporting the
64// individual macros (rather than the whole crate via `pub use defmt;`) is the
65// reliable way to make them resolvable through `$crate::…` from a downstream
66// crate: a bare crate re-export does not reliably expose a `#[macro_export]`
67// macro by path across crates.
68#[cfg(feature = "defmt")]
69#[doc(hidden)]
70pub use defmt::{
71    debug as __defmt_debug, error as __defmt_error, info as __defmt_info, trace as __defmt_trace,
72    warn as __defmt_warn,
73};
74#[cfg(feature = "tracing")]
75#[doc(hidden)]
76pub use tracing::{
77    debug as __tracing_debug, error as __tracing_error, info as __tracing_info,
78    trace as __tracing_trace, warn as __tracing_warn,
79};
80
81// Backend dispatch. `#[cfg]` *inside* an exported macro's body is resolved
82// against the *calling* crate's features, not this crate's, so the selection
83// must happen where it can see this crate's features: on the definitions of the
84// `__defmt`/`__tracing` dispatch macros, gated once each. When a backend is off,
85// its dispatch is the no-op arm. The public level macros carry no `#[cfg]`.
86
87/// Routes a level to the aliased `defmt` macro, or nothing when `defmt` is off.
88#[cfg(feature = "defmt")]
89#[doc(hidden)]
90#[macro_export]
91macro_rules! __defmt {
92    (trace, $($a:tt)*) => { $crate::__defmt_trace!($($a)*) };
93    (debug, $($a:tt)*) => { $crate::__defmt_debug!($($a)*) };
94    (info,  $($a:tt)*) => { $crate::__defmt_info!($($a)*) };
95    (warn,  $($a:tt)*) => { $crate::__defmt_warn!($($a)*) };
96    (error, $($a:tt)*) => { $crate::__defmt_error!($($a)*) };
97}
98#[cfg(not(feature = "defmt"))]
99#[doc(hidden)]
100#[macro_export]
101macro_rules! __defmt {
102    ($level:ident, $($a:tt)*) => {{}};
103}
104
105/// Routes a level to the aliased `tracing` macro, or nothing when it is off.
106#[cfg(feature = "tracing")]
107#[doc(hidden)]
108#[macro_export]
109macro_rules! __tracing {
110    (trace, $($a:tt)*) => { $crate::__tracing_trace!($($a)*) };
111    (debug, $($a:tt)*) => { $crate::__tracing_debug!($($a)*) };
112    (info,  $($a:tt)*) => { $crate::__tracing_info!($($a)*) };
113    (warn,  $($a:tt)*) => { $crate::__tracing_warn!($($a)*) };
114    (error, $($a:tt)*) => { $crate::__tracing_error!($($a)*) };
115}
116
117#[cfg(not(feature = "tracing"))]
118#[doc(hidden)]
119#[macro_export]
120macro_rules! __tracing {
121    ($level:ident, $($a:tt)*) => {{}};
122}
123
124// When both backends are disabled, still expand the portable format arguments
125// so variables referenced only by a log call count as used. The false branch
126// ensures argument expressions are never evaluated at runtime.
127#[cfg(not(any(feature = "defmt", feature = "tracing")))]
128#[doc(hidden)]
129#[macro_export]
130macro_rules! __disabled {
131    ($($a:tt)*) => {{
132        #[allow(clippy::all)]
133        if false {
134            let _ = core::format_args!($($a)*);
135        }
136    }};
137}
138#[cfg(any(feature = "defmt", feature = "tracing"))]
139#[doc(hidden)]
140#[macro_export]
141macro_rules! __disabled {
142    ($($a:tt)*) => {{}};
143}
144
145/// Logs at the `trace` level to every enabled backend. See the
146/// [crate docs](crate) for the portable format syntax.
147#[macro_export]
148macro_rules! trace {
149    ($($arg:tt)*) => {{
150        $crate::__expand_captures!(($crate::__log) (trace) ($($arg)*));
151    }};
152}
153
154/// Logs at the `debug` level to every enabled backend.
155#[macro_export]
156macro_rules! debug {
157    ($($arg:tt)*) => {{
158        $crate::__expand_captures!(($crate::__log) (debug) ($($arg)*));
159    }};
160}
161
162/// Logs at the `info` level to every enabled backend.
163#[macro_export]
164macro_rules! info {
165    ($($arg:tt)*) => {{
166        $crate::__expand_captures!(($crate::__log) (info) ($($arg)*));
167    }};
168}
169
170/// Logs at the `warn` level to every enabled backend.
171#[macro_export]
172macro_rules! warn {
173    ($($arg:tt)*) => {{
174        $crate::__expand_captures!(($crate::__log) (warn) ($($arg)*));
175    }};
176}
177
178/// Logs at the `error` level to every enabled backend.
179#[macro_export]
180macro_rules! error {
181    ($($arg:tt)*) => {{
182        $crate::__expand_captures!(($crate::__log) (error) ($($arg)*));
183    }};
184}
185
186#[doc(hidden)]
187#[macro_export]
188macro_rules! __log {
189    ($level:ident, $($arg:tt)*) => {{
190        $crate::__defmt!($level, $($arg)*);
191        $crate::__tracing!($level, $($arg)*);
192        $crate::__disabled!($($arg)*);
193    }};
194}
195
196// This function is never run: it keeps implicit-capture expansion type-checked
197// for the no-backend, tracing, defmt, and combined feature configurations
198// without requiring a linked defmt global logger in tests.
199#[cfg(test)]
200#[allow(dead_code)]
201fn implicit_capture_compiles_for_enabled_backends() {
202    let channel = 3u8;
203    crate::info!("captured channel {channel}");
204}
205
206// Exercised with the `tracing` backend only: a `defmt`-enabled test binary would
207// need a linked `#[defmt::global_logger]`, which a plain unit test cannot supply.
208#[cfg(all(test, feature = "tracing", not(feature = "defmt")))]
209mod tests {
210    // The macros forward the portable subset (format string + positional args)
211    // to the enabled backend; here that is `tracing`. This exercises each level
212    // and each supported arity so a broken expansion fails to compile.
213    #[test]
214    fn every_level_forwards() {
215        let name = "cm33";
216        let channel = 3u8;
217
218        crate::trace!("no args");
219        crate::debug!("one arg {}", name);
220        crate::info!("two args {} {}", name, channel);
221        crate::warn!("warn {}", channel);
222        crate::error!("error {}", name);
223        crate::error!("captured channel {channel} for {name}");
224        crate::error!("captured {name} then positional {}", channel);
225    }
226}