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}