tracing_slog/
lib.rs

1//! Adapters for connecting structured log records from the [slog] crate into the [tracing](https://github.com/tokio-rs/tracing) ecosystem.
2//!
3//! Use when a library uses `slog` but your application uses `tracing`.
4//!
5//! Heavily inspired by [tracing-log](https://github.com/tokio-rs/tracing/tree/5fdbcbf61da27ec3e600678121d8c00d2b9b5cb1/tracing-log).
6
7use once_cell::sync::Lazy;
8
9#[cfg(feature = "kv")]
10use slog::KV;
11use tracing_core::{
12    callsite, dispatcher, field, identify_callsite,
13    metadata::{Kind, Level},
14    subscriber, Event, Metadata,
15};
16
17#[cfg(feature = "kv")]
18/// An allocating serializer to use for serializing key-value pairs in a [`slog::Record`].
19#[derive(Default)]
20struct TracingKvSerializer {
21    storage: String,
22}
23
24#[cfg(feature = "kv")]
25impl TracingKvSerializer {
26    /// Returns the serialized fields as a string. If empty, returns `None`.
27    fn as_str(&self) -> Option<&str> {
28        self.storage
29            .get(..self.storage.len().saturating_sub(1))
30            .filter(|kv_serialization| !kv_serialization.is_empty())
31    }
32}
33
34#[cfg(feature = "kv")]
35impl slog::Serializer for TracingKvSerializer {
36    fn emit_arguments(&mut self, key: slog::Key, val: &core::fmt::Arguments) -> slog::Result {
37        self.storage.push_str(&format!("{key}={val},"));
38        Ok(())
39    }
40}
41
42/// A [slog Drain](slog::Drain) that converts [records](slog::Record) into [tracing events](Event).
43///
44/// To use, create a [slog logger](slog::Logger) using an instance of [TracingSlogDrain] as its drain:
45///
46/// ```rust
47/// # use slog::*;
48/// # use tracing_slog::TracingSlogDrain;
49/// let drain = TracingSlogDrain;
50/// let root = Logger::root(drain, o!());
51///
52/// info!(root, "logged using slogger");
53/// ```
54#[derive(Debug)]
55pub struct TracingSlogDrain;
56
57impl slog::Drain for TracingSlogDrain {
58    type Ok = ();
59    type Err = slog::Never;
60
61    /// Converts a [slog record](slog::Record) into a [tracing event](Event)
62    /// and dispatches it to any registered tracing subscribers
63    /// using the [default dispatcher](dispatcher::get_default).
64    /// Currently, the key-value pairs are ignored.
65    fn log(
66        &self,
67        record: &slog::Record<'_>,
68        _values: &slog::OwnedKVList,
69    ) -> Result<Self::Ok, Self::Err> {
70        dispatcher::get_default(|dispatch| {
71            let filter_meta = slogrecord_to_trace(record);
72            if !dispatch.enabled(&filter_meta) {
73                return;
74            }
75
76            #[cfg(feature = "kv")]
77            let kv_serializer = {
78                let mut ser = TracingKvSerializer::default();
79                let _ = record.kv().serialize(record, &mut ser);
80                ser
81            };
82
83            let (_, keys, meta) = sloglevel_to_cs(record.level());
84
85            let target = get_target(record);
86
87            dispatch.event(&Event::new(
88                meta,
89                &meta.fields().value_set(&[
90                    (&keys.message, Some(record.msg() as &dyn field::Value)),
91                    (&keys.target, Some(&target)),
92                    (&keys.module, Some(&record.module())),
93                    (&keys.file, Some(&record.file())),
94                    (&keys.line, Some(&record.line())),
95                    (&keys.column, Some(&record.column())),
96                    #[cfg(feature = "kv")]
97                    (
98                        &keys.kv,
99                        kv_serializer
100                            .as_str()
101                            .as_ref()
102                            .map(|x| x as &dyn tracing_core::field::Value),
103                    ),
104                ]),
105            ));
106        });
107
108        Ok(())
109    }
110}
111
112fn get_target<'a>(record: &'a slog::Record<'a>) -> &'a str {
113    let target = record.tag();
114    if target.is_empty() {
115        record.module()
116    } else {
117        target
118    }
119}
120
121struct Fields {
122    message: field::Field,
123    target: field::Field,
124    module: field::Field,
125    file: field::Field,
126    line: field::Field,
127    column: field::Field,
128    #[cfg(feature = "kv")]
129    kv: field::Field,
130}
131
132static FIELD_NAMES: &[&str] = &[
133    "message",
134    "slog.target",
135    "slog.module_path",
136    "slog.file",
137    "slog.line",
138    "slog.column",
139    #[cfg(feature = "kv")]
140    "slog.kv",
141];
142
143impl Fields {
144    fn new(cs: &'static dyn callsite::Callsite) -> Self {
145        let fieldset = cs.metadata().fields();
146        let message = fieldset.field("message").unwrap();
147        let target = fieldset.field("slog.target").unwrap();
148        let module = fieldset.field("slog.module_path").unwrap();
149        let file = fieldset.field("slog.file").unwrap();
150        let line = fieldset.field("slog.line").unwrap();
151        let column = fieldset.field("slog.column").unwrap();
152        #[cfg(feature = "kv")]
153        let kv = fieldset.field("slog.kv").unwrap();
154        Fields {
155            message,
156            target,
157            module,
158            file,
159            line,
160            column,
161            #[cfg(feature = "kv")]
162            kv,
163        }
164    }
165}
166
167macro_rules! slog_cs {
168    ($level:expr, $cs:ident, $meta:ident, $ty:ident) => {
169        struct $ty;
170        static $cs: $ty = $ty;
171        static $meta: Metadata<'static> = Metadata::new(
172            "slog event",
173            "slog",
174            $level,
175            None,
176            None,
177            None,
178            field::FieldSet::new(FIELD_NAMES, identify_callsite!(&$cs)),
179            Kind::EVENT,
180        );
181
182        impl callsite::Callsite for $ty {
183            fn set_interest(&self, _: subscriber::Interest) {}
184            fn metadata(&self) -> &'static Metadata<'static> {
185                &$meta
186            }
187        }
188    };
189}
190
191slog_cs!(
192    tracing_core::Level::TRACE,
193    TRACE_CS,
194    TRACE_META,
195    TraceCallsite
196);
197
198slog_cs!(
199    tracing_core::Level::DEBUG,
200    DEBUG_CS,
201    DEBUG_META,
202    DebugCallsite
203);
204
205slog_cs!(tracing_core::Level::INFO, INFO_CS, INFO_META, InfoCallsite);
206
207slog_cs!(tracing_core::Level::WARN, WARN_CS, WARN_META, WarnCallsite);
208
209slog_cs!(
210    tracing_core::Level::ERROR,
211    ERROR_CS,
212    ERROR_META,
213    ErrorCallsite
214);
215
216static TRACE_FIELDS: Lazy<Fields> = Lazy::new(|| Fields::new(&TRACE_CS));
217static DEBUG_FIELDS: Lazy<Fields> = Lazy::new(|| Fields::new(&DEBUG_CS));
218static INFO_FIELDS: Lazy<Fields> = Lazy::new(|| Fields::new(&INFO_CS));
219static WARN_FIELDS: Lazy<Fields> = Lazy::new(|| Fields::new(&WARN_CS));
220static ERROR_FIELDS: Lazy<Fields> = Lazy::new(|| Fields::new(&ERROR_CS));
221
222fn sloglevel_to_cs(
223    level: slog::Level,
224) -> (
225    &'static dyn callsite::Callsite,
226    &'static Fields,
227    &'static Metadata<'static>,
228) {
229    match level {
230        slog::Level::Trace => (&TRACE_CS, &*TRACE_FIELDS, &TRACE_META),
231        slog::Level::Debug => (&DEBUG_CS, &*DEBUG_FIELDS, &DEBUG_META),
232        slog::Level::Info => (&INFO_CS, &*INFO_FIELDS, &INFO_META),
233        slog::Level::Warning => (&WARN_CS, &*WARN_FIELDS, &WARN_META),
234        slog::Level::Error | slog::Level::Critical => (&ERROR_CS, &*ERROR_FIELDS, &ERROR_META),
235    }
236}
237
238fn sloglevel_to_trace(level: slog::Level) -> Level {
239    match level {
240        slog::Level::Trace => Level::TRACE,
241        slog::Level::Debug => Level::DEBUG,
242        slog::Level::Info => Level::INFO,
243        slog::Level::Warning => Level::WARN,
244        slog::Level::Error | slog::Level::Critical => Level::ERROR,
245    }
246}
247
248fn slogrecord_to_trace<'a>(record: &'a slog::Record<'a>) -> Metadata<'a> {
249    let cs_id = identify_callsite!(sloglevel_to_cs(record.level()).0);
250    let target = get_target(record);
251
252    Metadata::new(
253        "slog record",
254        target,
255        sloglevel_to_trace(record.level()),
256        Some(record.file()),
257        Some(record.line()),
258        Some(record.module()),
259        field::FieldSet::new(FIELD_NAMES, cs_id),
260        Kind::EVENT,
261    )
262}
263
264#[cfg(test)]
265mod tests {
266    use super::TracingSlogDrain;
267    use slog::*;
268    use tracing_test::traced_test;
269
270    #[test]
271    #[traced_test]
272    fn basic() {
273        let drain = TracingSlogDrain;
274        let root = Logger::root(drain, o!());
275
276        info!(root, "slog test"; "arg1" => "val1");
277        assert!(logs_contain("slog test"));
278    }
279
280    #[cfg(feature = "kv")]
281    #[test]
282    #[traced_test]
283    fn key_value_pairs() {
284        let drain = TracingSlogDrain;
285        let root = Logger::root(drain, o!());
286
287        info!(root, "slog test"; "arg1"=>"val1", "arg2"=>"val2");
288        assert!(logs_contain("slog test"));
289        assert!(
290            logs_contain("arg1=val1"),
291            "first kv pair should be included"
292        );
293        assert!(
294            logs_contain("arg2=val2"),
295            "second kv pair should be included"
296        );
297        assert!(
298            logs_contain("arg2=val2,arg1=val1"),
299            "comma-separated kv pairs should be included"
300        );
301        assert!(
302            !logs_contain("arg1=val1,arg1=val1,"),
303            "trailing comma should not be included"
304        );
305    }
306
307    #[cfg(feature = "kv")]
308    #[test]
309    #[traced_test]
310    fn non_string_key_value_pairs() {
311        let drain = TracingSlogDrain;
312        let root = Logger::root(drain, o!());
313
314        info!(root, "slog test"; "log-key" => true);
315        assert!(
316            logs_contain("log-key=true"),
317            "first kv pair should be included"
318        );
319
320        #[allow(unused)]
321        #[derive(Debug)]
322        struct Wrapper(u8);
323
324        let w = Wrapper(100);
325
326        info!(root, "slog test"; "debug-struct" =>?w);
327        assert!(
328            logs_contain("debug-struct=Wrapper(100)"),
329            "Debug-formatted struct should be included"
330        );
331    }
332
333    #[cfg(feature = "kv")]
334    #[test]
335    #[traced_test]
336    fn log_without_kv_pair_doesnt_contain_kv_field() {
337        let drain = TracingSlogDrain;
338        let root = Logger::root(drain, o!());
339
340        info!(root, "slog test");
341        assert!(
342            !logs_contain("slog.kv"),
343            "log without key-value pair should not contain `slog.kv`"
344        );
345    }
346
347    mod nested_mod {
348        pub fn log_as_info(slogger: &slog::Logger) {
349            slog::info!(slogger, "slog test");
350        }
351    }
352
353    #[test]
354    #[traced_test]
355    fn nested() {
356        let drain = TracingSlogDrain;
357        let root = Logger::root(drain, o!());
358
359        nested_mod::log_as_info(&root);
360        assert!(logs_contain("nested_mod"));
361    }
362}