Skip to main content

rlg/
tracing.rs

1// tracing.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6//! Integration with the `tracing` ecosystem.
7//!
8//! Provides both a standalone [`RlgSubscriber`] and, behind the
9//! `tracing-layer` feature, a composable [`RlgLayer`].
10
11use crate::log::Log;
12use crate::log_level::LogLevel;
13use std::sync::atomic::{AtomicU64, Ordering};
14use tracing_core::field::{Field, Visit};
15use tracing_core::{Event, Level, Metadata, Subscriber};
16
17/// Maps a [`tracing_core::Level`] to an RLG [`LogLevel`].
18fn map_tracing_level(level: Level) -> LogLevel {
19    if level == Level::ERROR {
20        LogLevel::ERROR
21    } else if level == Level::WARN {
22        LogLevel::WARN
23    } else if level == Level::INFO {
24        LogLevel::INFO
25    } else if level == Level::DEBUG {
26        LogLevel::DEBUG
27    } else {
28        LogLevel::TRACE
29    }
30}
31
32/// Monotonic span ID counter for unique span identification.
33static SPAN_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
34
35/// A `tracing::Subscriber` that routes events to the `RLG` engine.
36#[derive(Debug, Default, Clone, Copy)]
37pub struct RlgSubscriber;
38
39impl RlgSubscriber {
40    /// Create a new `RlgSubscriber`.
41    #[must_use]
42    pub const fn new() -> Self {
43        Self
44    }
45}
46
47impl Subscriber for RlgSubscriber {
48    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
49        map_tracing_level(*metadata.level()).to_numeric()
50            >= crate::engine::ENGINE.filter_level()
51    }
52
53    fn new_span(
54        &self,
55        _span: &tracing_core::span::Attributes<'_>,
56    ) -> tracing_core::span::Id {
57        tracing_core::span::Id::from_u64(
58            SPAN_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
59        )
60    }
61
62    fn record(
63        &self,
64        _span: &tracing_core::span::Id,
65        _values: &tracing_core::span::Record<'_>,
66    ) {
67    }
68
69    fn record_follows_from(
70        &self,
71        _span: &tracing_core::span::Id,
72        _follows: &tracing_core::span::Id,
73    ) {
74    }
75
76    fn event(&self, event: &Event<'_>) {
77        let metadata = event.metadata();
78        let level = map_tracing_level(*metadata.level());
79
80        let mut visitor = RlgVisitor::default();
81        event.record(&mut visitor);
82
83        let mut log = Log::build(level, &visitor.message);
84        log.component =
85            std::borrow::Cow::Owned(metadata.target().to_string());
86
87        for (key, value) in visitor.fields {
88            log = log.with(&key, value);
89        }
90
91        log.fire();
92    }
93
94    fn enter(&self, _span: &tracing_core::span::Id) {}
95
96    fn exit(&self, _span: &tracing_core::span::Id) {}
97}
98
99#[derive(Default)]
100struct RlgVisitor {
101    message: String,
102    fields: std::collections::BTreeMap<String, serde_json::Value>,
103}
104
105macro_rules! impl_record_field {
106    ($method:ident, $ty:ty) => {
107        fn $method(&mut self, field: &Field, value: $ty) {
108            self.fields.insert(
109                field.name().to_string(),
110                serde_json::json!(value),
111            );
112        }
113    };
114    (stringify $method:ident, $ty:ty) => {
115        fn $method(&mut self, field: &Field, value: $ty) {
116            self.fields.insert(
117                field.name().to_string(),
118                serde_json::json!(value.to_string()),
119            );
120        }
121    };
122}
123
124impl Visit for RlgVisitor {
125    fn record_debug(
126        &mut self,
127        field: &Field,
128        value: &dyn std::fmt::Debug,
129    ) {
130        if field.name() == "message" {
131            self.message = format!("{value:?}");
132        } else {
133            self.fields.insert(
134                field.name().to_string(),
135                serde_json::json!(format!("{value:?}")),
136            );
137        }
138    }
139
140    fn record_str(&mut self, field: &Field, value: &str) {
141        if field.name() == "message" {
142            self.message = value.to_string();
143        } else {
144            self.fields.insert(
145                field.name().to_string(),
146                serde_json::json!(value),
147            );
148        }
149    }
150
151    fn record_error(
152        &mut self,
153        field: &Field,
154        value: &(dyn std::error::Error + 'static),
155    ) {
156        self.fields.insert(
157            field.name().to_string(),
158            serde_json::json!(value.to_string()),
159        );
160    }
161
162    impl_record_field!(record_u64, u64);
163    impl_record_field!(record_i64, i64);
164    impl_record_field!(record_bool, bool);
165    impl_record_field!(record_f64, f64);
166    impl_record_field!(stringify record_u128, u128);
167    impl_record_field!(stringify record_i128, i128);
168}
169
170// ---------------------------------------------------------------------------
171// Composable tracing Layer (behind `tracing-layer` feature)
172// ---------------------------------------------------------------------------
173
174/// A composable [`tracing_subscriber::Layer`] that routes events into the RLG engine.
175///
176/// This allows RLG to be used alongside other tracing layers in a
177/// `tracing_subscriber::Registry` stack.
178///
179/// # Example
180///
181/// ```rust,ignore
182/// use tracing_subscriber::prelude::*;
183/// use rlg::tracing::RlgLayer;
184///
185/// tracing_subscriber::registry()
186///     .with(RlgLayer::new())
187///     .init();
188/// ```
189#[cfg(feature = "tracing-layer")]
190#[derive(Debug, Clone, Copy)]
191pub struct RlgLayer {
192    format: crate::log_format::LogFormat,
193}
194
195#[cfg(feature = "tracing-layer")]
196impl Default for RlgLayer {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202#[cfg(feature = "tracing-layer")]
203impl RlgLayer {
204    /// Creates a new `RlgLayer` with the default MCP format.
205    #[must_use]
206    pub const fn new() -> Self {
207        Self {
208            format: crate::log_format::LogFormat::MCP,
209        }
210    }
211
212    /// Sets the log output format for this layer.
213    #[must_use]
214    pub const fn with_format(
215        mut self,
216        format: crate::log_format::LogFormat,
217    ) -> Self {
218        self.format = format;
219        self
220    }
221}
222
223#[cfg(feature = "tracing-layer")]
224impl<S> tracing_subscriber::Layer<S> for RlgLayer
225where
226    S: Subscriber
227        + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
228{
229    fn enabled(
230        &self,
231        metadata: &Metadata<'_>,
232        _ctx: tracing_subscriber::layer::Context<'_, S>,
233    ) -> bool {
234        map_tracing_level(*metadata.level()).to_numeric()
235            >= crate::engine::ENGINE.filter_level()
236    }
237
238    fn on_event(
239        &self,
240        event: &Event<'_>,
241        _ctx: tracing_subscriber::layer::Context<'_, S>,
242    ) {
243        let metadata = event.metadata();
244        let level = map_tracing_level(*metadata.level());
245
246        let mut visitor = RlgVisitor::default();
247        event.record(&mut visitor);
248
249        let mut log = Log::build(level, &visitor.message);
250        log.component =
251            std::borrow::Cow::Owned(metadata.target().to_string());
252        log.format = self.format;
253
254        for (key, value) in visitor.fields {
255            log = log.with(&key, value);
256        }
257
258        log.fire();
259    }
260
261    fn on_new_span(
262        &self,
263        _attrs: &tracing_core::span::Attributes<'_>,
264        _id: &tracing_core::span::Id,
265        _ctx: tracing_subscriber::layer::Context<'_, S>,
266    ) {
267        crate::engine::ENGINE.inc_spans();
268    }
269
270    fn on_close(
271        &self,
272        _id: tracing_core::span::Id,
273        _ctx: tracing_subscriber::layer::Context<'_, S>,
274    ) {
275        crate::engine::ENGINE.dec_spans();
276    }
277}