slog_dtrace/lib.rs
1//! Forward `slog` messages as DTrace USDT probes.
2//!
3//! Logging is invaluable in production applications. However, it presents a bit of a quandary.
4//! Most of the time, only informational or error messages are useful. But when an application
5//! crashes or is misbehaving, it can be extremely useful to retrieve more verbose logging
6//! information. Unfortunately, this can normally only be accomplished by restarting the process
7//! with a new log level.
8//!
9//! This crate allows applications to attach a [`slog::Drain`], the `Dtrace` drain, to their
10//! loggers that forwards all messages to DTrace. This is done with a
11//! [`usdt`](https://docs.rs/usdt/latest) probe function, with different probes indicating
12//! different log levels.
13//!
14//! Note that the [`Dtrace`] drain will _only_ send messages to DTrace, but in most situations, one
15//! is already sending log messages to some location (stdout, file, syslog, etc.). The
16//! [`with_drain`] constructor can be used to generate a [`Dtrace`] drain that will forward
17//! messages to an existing drain as well as to DTrace.
18//!
19//! The DTrace probe that emits log messages is efficient. In particular, when the probe is
20//! disabled, it incurs no cost beyond that of any other drain(s) in the hierarchy. However, when
21//! the probe is enabled, every message, regardless of log-level, can be viewed in DTrace.
22//!
23//! Example
24//! -------
25//!
26//! ```bash
27//! $ cargo +nightly run --example simple
28//!
29//! ```
30//!
31//! You can see that only warning messages are printed in the terminal. However, running a DTrace
32//! command in another shell should reveal more messages.
33//!
34//! ```bash
35//! ## dtrace -Z -n 'slog*::: { printf("%s\n", copyinstr(arg0)); }' -q
36//! {"ok": {"location":{"module":"simple","file":"examples/simple.rs","line":15},"level":"WARN","timestamp":"2021-10-19T17:55:55.260393409Z","message":"a warning message for everyone","kv":{"cool":true,"key":"value"}}}
37//! {"ok": {"location":{"module":"simple","file":"examples/simple.rs","line":16},"level":"INFO","timestamp":"2021-10-19T17:55:55.260531762Z","message":"info is just for dtrace","kv":{"cool":true,"hello":"from dtrace","key":"value"}}}
38//! {"ok": {"location":{"module":"simple","file":"examples/simple.rs","line":17},"level":"DEBUG","timestamp":"2021-10-19T17:55:55.260579423Z","message":"only dtrace gets debug messages","kv":{"cool":true,"hello":"from dtrace","key":"value"}}}
39//! ```
40//!
41//! We can see both the warning messages that the example's stdout prints, but also an info and
42//! debug message. There are specific probes for each logging level, allowing users to run DTrace
43//! actions in response to specific levels of messages. For example, this DTrace command receives
44//! just messages emitted via the `debug!` logging macro.
45//!
46//! ```bash
47//! ## dtrace -Z -n 'slog*:::debug { printf("%s\n", copyinstr(arg0)); }' -q
48//! {"ok": {"location":{"module":"simple","file":"examples/simple.rs","line":17},"level":"DEBUG","timestamp":"2021-10-19T17:57:30.578681933Z","message":"only dtrace gets debug messages","kv":{"cool":true,"hello":"from dtrace","key":"value"}}}
49//! ```
50//!
51//! Notes
52//! -----
53//!
54//! This crate inherits a reliance on a nightly toolchain from the `usdt` crate.
55
56// Copyright 2022 Oxide Computer Company
57//
58// Licensed under the Apache License, Version 2.0 (the "License");
59// you may not use this file except in compliance with the License.
60// You may obtain a copy of the License at
61//
62// http://www.apache.org/licenses/LICENSE-2.0
63//
64// Unless required by applicable law or agreed to in writing, software
65// distributed under the License is distributed on an "AS IS" BASIS,
66// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
67// See the License for the specific language governing permissions and
68// limitations under the License.
69
70#![cfg_attr(usdt_need_asm, feature(asm))]
71#![cfg_attr(all(target_os = "macos", usdt_need_asm_sym), feature(asm_sym))]
72
73use chrono::{DateTime, Utc};
74use serde::{Deserialize, Serialize};
75use slog::{Drain, KV};
76
77/// Type alias for a generic JSON map.
78pub type JsonMap = serde_json::Map<String, serde_json::Value>;
79
80#[usdt::provider(provider = "slog", probe_format = "{probe}_")]
81mod probes {
82 use crate::Message;
83 fn trace(msg: &Message) {}
84 fn debug(msg: &Message) {}
85 fn info(msg: &Message) {}
86 fn warn(msg: &Message) {}
87 fn error(msg: &Message) {}
88 fn critical(msg: &Message) {}
89}
90
91/// `Location` describes the location in the source from which a log message was issued.
92#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
93pub struct Location {
94 /// The Rust module from which the message was issued.
95 pub module: String,
96
97 /// The source file from which the message was issued.
98 pub file: String,
99
100 /// The line of the source file from which the message was issued.
101 pub line: u32,
102}
103
104/// A `Message` captures the all information about a single log message.
105#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
106pub struct Message {
107 /// The information about the [`Location`] of a message in the source.
108 pub location: Location,
109
110 /// The logging level, see [`slog::Level`].
111 pub level: String,
112
113 /// The timestamp at which the message was issued.
114 ///
115 /// As there may be latencies between a message's emission and consumption in DTrace, this can
116 /// be useful.
117 pub timestamp: DateTime<Utc>,
118
119 /// The string message emitted in the log entry.
120 pub message: String,
121
122 /// The key-value pairs in this log message, including those of parent loggers.
123 pub kv: JsonMap,
124}
125
126/// `ProbeRegistration` stores the result of registering probes with the DTrace kernel module.
127///
128/// Though unlikely, it's possible that probe registration fails. This may happen, for example, if
129/// the DTrace kernel module is extremely low on memory. One may want to abort the application in
130/// this case, or one might decide that a running but degraded application is better than nothing i
131/// such a situation. The `ProbeRegistration` enum contains information about whether probes were
132/// successfully registered. The caller may decide how to handle such a case.
133#[allow(clippy::large_enum_variant)]
134#[derive(Debug)]
135pub enum ProbeRegistration {
136 Success,
137 Failed(String),
138}
139
140impl ProbeRegistration {
141 /// Helper to check if the variant is `Success`.
142 pub fn is_success(&self) -> bool {
143 matches!(self, ProbeRegistration::Success)
144 }
145}
146
147/// A [`slog::Drain`] that forwards all log messages to DTrace.
148#[derive(Debug)]
149pub struct Dtrace<D> {
150 _phantom: std::marker::PhantomData<D>,
151}
152
153impl Dtrace<slog::Discard> {
154 /// Create a new DTrace logger, emitting messages only to DTrace.
155 ///
156 /// This method will create a `Dtrace` drain that sends messages _only_ to DTrace. If you wish
157 /// to emit messages to another location as well, you can use [`with_drain`] or call
158 /// [`slog::Duplicate`].
159 ///
160 /// Note that it's possible for probe registration to fail. The result of registering is
161 /// returned as the second tuple element. It may be inspected so that callers can decide how to
162 /// handle failure. See [`ProbeRegistration`] for more information.
163 pub fn new() -> (Self, ProbeRegistration) {
164 let registration = match usdt::register_probes() {
165 Ok(_) => ProbeRegistration::Success,
166 Err(e) => ProbeRegistration::Failed(e.to_string()),
167 };
168 (
169 Self {
170 _phantom: std::marker::PhantomData,
171 },
172 registration,
173 )
174 }
175}
176
177/// Combine the [`Dtrace`] drain with another drain.
178///
179/// This duplicates all log messages to `drain` and a new `Dtrace` drain.
180///
181/// Note that probe registration can fail, see [`ProbeRegistration`] and [`Dtrace::new`] for more
182/// information.
183pub fn with_drain<D>(drain: D) -> (slog::Duplicate<D, Dtrace<slog::Discard>>, ProbeRegistration)
184where
185 D: Drain,
186{
187 let (d, registration) = Dtrace::new();
188 (slog::Duplicate(drain, d), registration)
189}
190
191// Create a message to emit to DTrace
192fn create_dtrace_message(record: &slog::Record, values: &slog::OwnedKVList) -> Message {
193 let location = Location {
194 module: record.module().to_string(),
195 file: record.file().to_string(),
196 line: record.line(),
197 };
198 let mut serializer = Serializer::default();
199 let kv = match record
200 .kv()
201 .serialize(record, &mut serializer)
202 .and_then(|_| values.serialize(record, &mut serializer))
203 {
204 Ok(()) => serializer.map,
205 Err(e) => {
206 let mut map = JsonMap::default();
207 let _ = map.insert(
208 String::from("err"),
209 serde_json::Value::from(format!("{}", e)),
210 );
211 map
212 }
213 };
214 let msg = Message {
215 location,
216 timestamp: Utc::now(),
217 level: record.level().as_str().to_string(),
218 message: record.msg().to_string(),
219 kv,
220 };
221 msg
222}
223
224impl<D> Drain for Dtrace<D>
225where
226 D: Drain<Ok = (), Err = slog::Never>,
227{
228 type Ok = ();
229 type Err = slog::Never;
230
231 fn log(
232 &self,
233 record: &slog::Record<'_>,
234 values: &slog::OwnedKVList,
235 ) -> Result<Self::Ok, Self::Err> {
236 match record.level() {
237 slog::Level::Trace => probes::trace_!(|| create_dtrace_message(record, values)),
238 slog::Level::Debug => probes::debug_!(|| create_dtrace_message(record, values)),
239 slog::Level::Info => probes::info_!(|| create_dtrace_message(record, values)),
240 slog::Level::Warning => probes::warn_!(|| create_dtrace_message(record, values)),
241 slog::Level::Error => probes::error_!(|| create_dtrace_message(record, values)),
242 slog::Level::Critical => probes::critical_!(|| create_dtrace_message(record, values)),
243 }
244 Ok(())
245 }
246}
247
248// Type used to serialize slog's key-value pairs into JSON.
249#[derive(Debug, Clone, Default)]
250struct Serializer {
251 map: crate::JsonMap,
252}
253
254impl Serializer {
255 fn emit<T>(&mut self, key: slog::Key, value: T) -> slog::Result
256 where
257 T: Into<serde_json::Value>,
258 {
259 self.map.insert(key.to_string(), value.into());
260 Ok(())
261 }
262}
263
264macro_rules! impl_emit {
265 ($method:ident, $ty:ty) => {
266 fn $method(&mut self, key: slog::Key, value: $ty) -> slog::Result {
267 self.emit(key, value).unwrap();
268 Ok(())
269 }
270 };
271}
272
273impl slog::Serializer for Serializer {
274 fn emit_arguments(&mut self, key: slog::Key, values: &std::fmt::Arguments<'_>) -> slog::Result {
275 self.map
276 .insert(key.to_string(), format!("{}", values).into());
277 Ok(())
278 }
279
280 impl_emit!(emit_u8, u8);
281 impl_emit!(emit_u16, u16);
282 impl_emit!(emit_u32, u32);
283 impl_emit!(emit_u64, u64);
284 impl_emit!(emit_i8, i8);
285 impl_emit!(emit_i16, i16);
286 impl_emit!(emit_i32, i32);
287 impl_emit!(emit_i64, i64);
288 impl_emit!(emit_isize, isize);
289 impl_emit!(emit_usize, usize);
290 impl_emit!(emit_bool, bool);
291 impl_emit!(emit_f32, f32);
292 impl_emit!(emit_f64, f64);
293 impl_emit!(emit_str, &str);
294
295 fn emit_unit(&mut self, key: slog::Key) -> slog::Result {
296 self.emit(key, ())
297 }
298
299 fn emit_none(&mut self, key: slog::Key) -> slog::Result {
300 self.map.insert(key.to_string(), serde_json::Value::Null);
301 Ok(())
302 }
303}