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