Skip to main content

hick_trace/
lib.rs

1//! Tracing-or-noop diagnostic macro shim and backend-agnostic stats/metrics
2//! primitives for the hick mDNS stack.
3//!
4//! # Macros
5//!
6//! The five macros `trace!`, `debug!`, `info!`, `warn!`, and `error!` are
7//! always available as `hick_trace::<name>!(...)`. When the `tracing` Cargo
8//! feature is enabled they delegate to the real [`tracing`] crate; otherwise
9//! they discard every argument without emitting code.
10//!
11//! # `stats` / `metrics` feature
12//!
13//! Enabling `stats` unlocks [`stats::Stats`] and [`stats::StatsSnapshot`]: a
14//! set of atomic counters and gauges that are `no_std`-safe. Enabling
15//! `metrics` additionally forwards every counter/gauge update to the
16//! [`metrics`] facade (requires `std`).
17
18#![cfg_attr(not(feature = "metrics"), no_std)]
19
20// ── Tracing shim ────────────────────────────────────────────────────────────
21
22#[cfg(feature = "tracing")]
23pub use tracing::{debug, debug_span, error, info, info_span, trace, trace_span, warn};
24
25/// Token-consuming no-op for all five diagnostic macros when `tracing` is
26/// disabled. Every argument expression is type-checked but **never executed**:
27/// each value is referenced inside an `if false { }` block, which the compiler
28/// eliminates entirely while still seeing the expression as "used" (no
29/// `unused_variables` warning, no side effects, no alloc, no panics).
30///
31/// # Supported forms
32///
33/// | Form | Example |
34/// |------|---------|
35/// | Positional format string | `debug!("n={}", n)` |
36/// | `key = value` | `debug!(x = val, "msg")` |
37/// | `key = %value` (Display) | `debug!(x = %val, "msg")` |
38/// | `key = ?value` (Debug) | `debug!(x = ?val, "msg")` |
39/// | Bare `%value` / `?value` | `debug!(%val)` |
40/// | Bare `ident` shorthand | `debug!(x, "msg")` |
41/// | `target: "t", ...` prefix | `debug!(target: "t", x = 1, "m")` |
42///
43/// # Unsupported forms
44///
45/// The following `tracing` forms are deliberately **not** supported:
46/// `name: "..."`, `parent: span`, dotted field names (`a.b`), and
47/// string-literal field keys (`"k" = v`). The codebase stays within the
48/// subset above; any violation is caught as a compile error in the default
49/// (no-tracing) build.
50///
51/// # Implementation note
52///
53/// Each value expression `$val` expands to `if false { let _ = &$val; }`.
54/// The compiler eliminates the dead branch during MIR building, so there is
55/// zero runtime cost. A plain `let _ = &$val;` (the previous approach)
56/// evaluated the expression to produce the reference even though it was
57/// discarded, causing side effects (allocs, panics, counter bumps) to run
58/// in disabled builds.
59#[doc(hidden)]
60#[cfg(not(feature = "tracing"))]
61#[macro_export]
62macro_rules! __hick_trace_noop {
63  (target: $tgt:expr, $($rest:tt)*) => {
64    { if false { let _ = &$tgt; } $crate::__hick_trace_noop!($($rest)*) }
65  };
66
67  ($key:ident = %$val:expr, $($rest:tt)*) => {
68    { if false { let _ = &$val; } $crate::__hick_trace_noop!($($rest)*) }
69  };
70  ($key:ident = %$val:expr) => {
71    { if false { let _ = &$val; } }
72  };
73
74  ($key:ident = ?$val:expr, $($rest:tt)*) => {
75    { if false { let _ = &$val; } $crate::__hick_trace_noop!($($rest)*) }
76  };
77  ($key:ident = ?$val:expr) => {
78    { if false { let _ = &$val; } }
79  };
80
81  ($key:ident = $val:expr, $($rest:tt)*) => {
82    { if false { let _ = &$val; } $crate::__hick_trace_noop!($($rest)*) }
83  };
84  ($key:ident = $val:expr) => {
85    { if false { let _ = &$val; } }
86  };
87
88  // Matches BEFORE the format-string literal arm so that a bare ident that
89  // is NOT a string literal is consumed correctly.
90  ($key:ident, $($rest:tt)*) => {
91    { if false { let _ = &$key; } $crate::__hick_trace_noop!($($rest)*) }
92  };
93  ($key:ident) => {
94    { if false { let _ = &$key; } }
95  };
96
97  // ── Bare `%value` ────────────────────────────────────────────────────────
98  (%$val:expr, $($rest:tt)*) => {
99    { if false { let _ = &$val; } $crate::__hick_trace_noop!($($rest)*) }
100  };
101  (%$val:expr) => {
102    { if false { let _ = &$val; } }
103  };
104
105  (?$val:expr, $($rest:tt)*) => {
106    { if false { let _ = &$val; } $crate::__hick_trace_noop!($($rest)*) }
107  };
108  (?$val:expr) => {
109    { if false { let _ = &$val; } }
110  };
111
112  ($fmt:literal $(, $arg:expr)* $(,)?) => {
113    { if false { let _ = ::core::format_args!($fmt $(, $arg)*); } }
114  };
115
116  () => {{}};
117}
118
119#[cfg(not(feature = "tracing"))]
120pub use __hick_trace_noop as trace;
121#[cfg(not(feature = "tracing"))]
122pub use __hick_trace_noop as debug;
123#[cfg(not(feature = "tracing"))]
124pub use __hick_trace_noop as info;
125#[cfg(not(feature = "tracing"))]
126pub use __hick_trace_noop as warn;
127#[cfg(not(feature = "tracing"))]
128pub use __hick_trace_noop as error;
129
130/// No-op span returned when the `tracing` feature is disabled.
131///
132/// Implements `.entered()` and `.enter()` so that
133/// `hick_trace::info_span!(...).entered()` compiles in both tracing and
134/// no-tracing builds.
135#[cfg(not(feature = "tracing"))]
136#[derive(Debug)]
137pub struct NoopSpan;
138
139#[cfg(not(feature = "tracing"))]
140impl NoopSpan {
141  /// Enters the span (no-op). Returns `self` so it acts as a drop-guard.
142  #[inline]
143  pub fn entered(self) -> Self {
144    self
145  }
146  /// Borrows the span and returns a new no-op guard (matches tracing's API).
147  #[inline]
148  pub fn enter(&self) -> Self {
149    NoopSpan
150  }
151}
152
153/// Token-consuming no-op for span macros when `tracing` is disabled.
154/// Returns a [`NoopSpan`] so callers may use `.entered()` / `.enter()`
155/// without compile errors. Uses the same field-consuming grammar as
156/// [`__hick_trace_noop`] so variables passed as span fields are not flagged
157/// as unused.
158#[doc(hidden)]
159#[cfg(not(feature = "tracing"))]
160#[macro_export]
161macro_rules! __hick_trace_noop_span {
162  // Strip target prefix.
163  (target: $tgt:expr, $($rest:tt)*) => {
164    { if false { let _ = &$tgt; } $crate::__hick_trace_noop_span!($($rest)*) }
165  };
166
167  // Span name only (the required first argument after an optional target).
168  // Any remaining tokens are field key=value pairs — consume them via the
169  // diagnostic no-op and return the NoopSpan.
170  ($name:literal, $($fields:tt)*) => {
171    { $crate::__hick_trace_noop!($($fields)*); $crate::NoopSpan }
172  };
173  ($name:literal) => {
174    $crate::NoopSpan
175  };
176
177  // Fallback: consume everything, return NoopSpan.
178  ($($tt:tt)*) => {
179    { $crate::__hick_trace_noop!($($tt)*); $crate::NoopSpan }
180  };
181}
182
183#[cfg(not(feature = "tracing"))]
184pub use __hick_trace_noop_span as trace_span;
185#[cfg(not(feature = "tracing"))]
186pub use __hick_trace_noop_span as debug_span;
187#[cfg(not(feature = "tracing"))]
188pub use __hick_trace_noop_span as info_span;
189
190#[cfg(feature = "stats")]
191pub mod stats {
192  //! Backend-agnostic atomic counters and gauges for the hick mDNS stack.
193  //!
194  //! [`Stats`] owns one atomic counter per counter and gauge. All loads and
195  //! stores use `Relaxed` ordering (sufficient for monotone counters where
196  //! precise cross-thread ordering is not required).
197  //!
198  //! On targets that have native 64-bit atomics (`target_has_atomic = "64"`)
199  //! [`core::sync::atomic::AtomicU64`] is used directly. On 32-bit embedded
200  //! targets (e.g. `thumbv7em-none-eabihf`) [`portable_atomic::AtomicU64`]
201  //! provides the same API via software emulation.
202  //!
203  //! When the `metrics` Cargo feature is also enabled, every counter increment
204  //! and gauge update additionally forwards the value to the [`metrics`] facade.
205
206  #[cfg(target_has_atomic = "64")]
207  use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
208  #[cfg(not(target_has_atomic = "64"))]
209  use portable_atomic::{AtomicU64, Ordering::Relaxed};
210
211  macro_rules! declare_counters {
212    ($($field:ident => $metric:literal),* $(,)?) => {
213      $(
214        #[inline]
215        pub fn $field(&self, by: u64) {
216          self.$field.fetch_add(by, Relaxed);
217          #[cfg(feature = "metrics")]
218          ::metrics::counter!($metric).increment(by);
219        }
220      )*
221    };
222  }
223
224  macro_rules! declare_gauges {
225    ($(
226      $field:ident => $metric:literal :
227        incr = $incr:ident,
228        decr = $decr:ident,
229        set  = $set:ident
230    ),* $(,)?) => {
231      $(
232        #[inline]
233        pub fn $incr(&self, by: u64) {
234          self.$field.fetch_add(by, Relaxed);
235          #[cfg(feature = "metrics")]
236          ::metrics::gauge!($metric).increment(by as f64);
237        }
238
239        #[inline]
240        pub fn $decr(&self, by: u64) {
241          self.$field.fetch_sub(by, Relaxed);
242          #[cfg(feature = "metrics")]
243          ::metrics::gauge!($metric).decrement(by as f64);
244        }
245
246        /// Store an absolute value into this gauge.
247        ///
248        /// Note: values above 2^53 lose precision when forwarded to the
249        /// `f64` metrics gauge.
250        #[inline]
251        pub fn $set(&self, v: u64) {
252          self.$field.store(v, Relaxed);
253          #[cfg(feature = "metrics")]
254          ::metrics::gauge!($metric).set(v as f64);
255        }
256      )*
257    };
258  }
259
260  /// Atomic counters and gauges for a single mDNS stack instance.
261  ///
262  /// Construct via [`Stats::default()`]; all fields start at zero.
263  #[derive(Default, Debug)]
264  pub struct Stats {
265    // ── Counters ──────────────────────────────────────────────────────────
266    packets_rx: AtomicU64,
267    packets_tx: AtomicU64,
268    bytes_rx: AtomicU64,
269    bytes_tx: AtomicU64,
270    packets_dropped: AtomicU64,
271    parse_errors: AtomicU64,
272    send_errors: AtomicU64,
273    questions_rx: AtomicU64,
274    answers_rx: AtomicU64,
275    answers_collected: AtomicU64,
276    answers_suppressed_kas: AtomicU64,
277    duplicate_questions_suppressed: AtomicU64,
278    responses_tx: AtomicU64,
279    probes_tx: AtomicU64,
280    announcements_tx: AtomicU64,
281    goodbyes_tx: AtomicU64,
282    conflicts: AtomicU64,
283    renames: AtomicU64,
284    cache_inserts: AtomicU64,
285    cache_refreshes: AtomicU64,
286    cache_evictions: AtomicU64,
287    cache_expirations: AtomicU64,
288    queries_started: AtomicU64,
289    queries_done: AtomicU64,
290    queries_timeout: AtomicU64,
291    services_registered: AtomicU64,
292    services_established: AtomicU64,
293    // ── Gauges ────────────────────────────────────────────────────────────
294    cache_size: AtomicU64,
295    queries_active: AtomicU64,
296    services_active: AtomicU64,
297  }
298
299  impl Stats {
300    declare_counters! {
301      packets_rx => "mdns_packets_rx",
302      packets_tx => "mdns_packets_tx",
303      bytes_rx => "mdns_bytes_rx",
304      bytes_tx => "mdns_bytes_tx",
305      packets_dropped => "mdns_packets_dropped",
306      parse_errors => "mdns_parse_errors",
307      send_errors => "mdns_send_errors",
308      questions_rx => "mdns_questions_rx",
309      answers_rx => "mdns_answers_rx",
310      answers_collected => "mdns_answers_collected",
311      answers_suppressed_kas => "mdns_answers_suppressed_kas",
312      duplicate_questions_suppressed => "mdns_duplicate_questions_suppressed",
313      responses_tx => "mdns_responses_tx",
314      probes_tx => "mdns_probes_tx",
315      announcements_tx => "mdns_announcements_tx",
316      goodbyes_tx => "mdns_goodbyes_tx",
317      conflicts => "mdns_conflicts",
318      renames => "mdns_renames",
319      cache_inserts => "mdns_cache_inserts",
320      cache_refreshes => "mdns_cache_refreshes",
321      cache_evictions => "mdns_cache_evictions",
322      cache_expirations => "mdns_cache_expirations",
323      queries_started => "mdns_queries_started",
324      queries_done => "mdns_queries_done",
325      queries_timeout => "mdns_queries_timeout",
326      services_registered => "mdns_services_registered",
327      services_established => "mdns_services_established",
328    }
329
330    declare_gauges! {
331      cache_size => "mdns_cache_size" :
332        incr = incr_cache_size,
333        decr = decr_cache_size,
334        set  = set_cache_size,
335      queries_active => "mdns_queries_active" :
336        incr = incr_queries_active,
337        decr = decr_queries_active,
338        set  = set_queries_active,
339      services_active => "mdns_services_active" :
340        incr = incr_services_active,
341        decr = decr_services_active,
342        set  = set_services_active,
343    }
344
345    /// Load a consistent snapshot of every counter and gauge.
346    ///
347    /// Each field is loaded independently with Relaxed ordering; the snapshot
348    /// is not guaranteed to reflect a single instant in time but is sufficient
349    /// for periodic reporting.
350    pub fn snapshot(&self) -> StatsSnapshot {
351      StatsSnapshot {
352        packets_rx: self.packets_rx.load(Relaxed),
353        packets_tx: self.packets_tx.load(Relaxed),
354        bytes_rx: self.bytes_rx.load(Relaxed),
355        bytes_tx: self.bytes_tx.load(Relaxed),
356        packets_dropped: self.packets_dropped.load(Relaxed),
357        parse_errors: self.parse_errors.load(Relaxed),
358        send_errors: self.send_errors.load(Relaxed),
359        questions_rx: self.questions_rx.load(Relaxed),
360        answers_rx: self.answers_rx.load(Relaxed),
361        answers_collected: self.answers_collected.load(Relaxed),
362        answers_suppressed_kas: self.answers_suppressed_kas.load(Relaxed),
363        duplicate_questions_suppressed: self.duplicate_questions_suppressed.load(Relaxed),
364        responses_tx: self.responses_tx.load(Relaxed),
365        probes_tx: self.probes_tx.load(Relaxed),
366        announcements_tx: self.announcements_tx.load(Relaxed),
367        goodbyes_tx: self.goodbyes_tx.load(Relaxed),
368        conflicts: self.conflicts.load(Relaxed),
369        renames: self.renames.load(Relaxed),
370        cache_inserts: self.cache_inserts.load(Relaxed),
371        cache_refreshes: self.cache_refreshes.load(Relaxed),
372        cache_evictions: self.cache_evictions.load(Relaxed),
373        cache_expirations: self.cache_expirations.load(Relaxed),
374        queries_started: self.queries_started.load(Relaxed),
375        queries_done: self.queries_done.load(Relaxed),
376        queries_timeout: self.queries_timeout.load(Relaxed),
377        services_registered: self.services_registered.load(Relaxed),
378        services_established: self.services_established.load(Relaxed),
379        cache_size: self.cache_size.load(Relaxed),
380        queries_active: self.queries_active.load(Relaxed),
381        services_active: self.services_active.load(Relaxed),
382      }
383    }
384  }
385
386  /// Point-in-time snapshot of every [`Stats`] counter and gauge.
387  #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
388  pub struct StatsSnapshot {
389    // Counters
390    pub packets_rx: u64,
391    pub packets_tx: u64,
392    pub bytes_rx: u64,
393    pub bytes_tx: u64,
394    pub packets_dropped: u64,
395    pub parse_errors: u64,
396    pub send_errors: u64,
397    pub questions_rx: u64,
398    pub answers_rx: u64,
399    pub answers_collected: u64,
400    pub answers_suppressed_kas: u64,
401    pub duplicate_questions_suppressed: u64,
402    pub responses_tx: u64,
403    pub probes_tx: u64,
404    pub announcements_tx: u64,
405    pub goodbyes_tx: u64,
406    pub conflicts: u64,
407    pub renames: u64,
408    pub cache_inserts: u64,
409    pub cache_refreshes: u64,
410    pub cache_evictions: u64,
411    pub cache_expirations: u64,
412    pub queries_started: u64,
413    pub queries_done: u64,
414    pub queries_timeout: u64,
415    pub services_registered: u64,
416    pub services_established: u64,
417    // Gauges
418    pub cache_size: u64,
419    pub queries_active: u64,
420    pub services_active: u64,
421  }
422}
423
424#[cfg(test)]
425mod tests;