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}