1use 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#[derive(Default)]
20struct TracingKvSerializer {
21 storage: String,
22}
23
24#[cfg(feature = "kv")]
25impl TracingKvSerializer {
26 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#[derive(Debug)]
55pub struct TracingSlogDrain;
56
57impl slog::Drain for TracingSlogDrain {
58 type Ok = ();
59 type Err = slog::Never;
60
61 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}