bunyarrs/
lib.rs

1//! ## Bunyarrs
2//!
3//!
4//! `bunyarrs` is a very opinionated, low performance logging library,
5//! modelled on [node bunyan](https://www.npmjs.com/package/bunyan).
6//!
7//! ```rust
8//! # fn init_foo() {} fn init_bar() {}
9//!
10//! use bunyarrs::{Bunyarr, vars};
11//! use serde_json::json;
12//!
13//! let logger = Bunyarr::with_name("my-module");
14//!
15//! let foo = init_foo();
16//! let bar = init_bar();
17//!
18//! logger.info(vars! { foo, bar }, "initialisation complete");
19//! logger.debug(json!({ "version": 4, "hello": "world", }), "system stats");
20//! ```
21//!
22//! The levels are in the following order:
23//!
24//!  * debug
25//!  * info
26//!  * warn
27//!  * error
28//!  * fatal
29//!
30//! The default log level is `info`, which can be changed with the `LOG_LEVEL` environment variable,
31//! e.g. `LOG_LEVEL=error`. There is no special handling for `debug` or `fatal` (i.e. this are always
32//! available in debug and production builds, and never interfere with program flow).
33//!
34//! All of the log methods have the same signature, which accepts:
35//!
36//!  * the context object, for which you can use `vars!` or `json!`, like above, or `vars_dbg!`, or
37//!    you can specify `()` (nothing). This would ideally accept anything that can be turned into
38//!    `[(String, serde_json::Value)]`. Please raise issues if you have an interesting usecase.
39//!  * the "event name". This is *not* a log line, in the traditional sense. There is no templating
40//!    here, no placeholders, no support for dynamic strings at all. This should be a static string
41//!    literal in nearly every situation.
42
43use serde_json::{Value, json};
44use std::io;
45use std::io::Write;
46use std::sync::LazyLock;
47use time::OffsetDateTime;
48use time::format_description::well_known::Rfc3339;
49
50use crate::extras::Extras;
51
52mod extras;
53
54#[cfg(test)]
55mod tests;
56
57/// The main logger interface.
58///
59/// ```rust
60/// # use bunyarrs::Bunyarr;
61/// let logger = Bunyarr::with_name("client-factory-factory");
62/// logger.info((), "creating a factory to create a factory to create a factory");
63/// ```
64pub struct Bunyarr {
65    writer: WriteImpl,
66    name: String,
67    min_level_inclusive: u16,
68}
69
70enum WriteImpl {
71    StdOut,
72    #[cfg(test)]
73    Test(std::cell::RefCell<Vec<u8>>),
74}
75
76impl WriteImpl {
77    #[inline]
78    fn work(&self, f: impl FnOnce(&mut dyn Write) -> io::Result<()>) -> io::Result<()> {
79        match self {
80            WriteImpl::StdOut => {
81                let mut w = io::stdout().lock();
82                f(&mut w)
83            }
84            #[cfg(test)]
85            WriteImpl::Test(vec) => f(&mut *vec.borrow_mut()),
86        }
87    }
88}
89
90pub(crate) struct Options {
91    pub(crate) name: String,
92    pub(crate) writer: Option<WriteImpl>,
93    pub(crate) min_level_inclusive: u16,
94}
95
96static PROC_INFO: LazyLock<ProcInfo> = LazyLock::new(ProcInfo::new);
97
98impl Bunyarr {
99    /// Create a logger with a specified "module" name, called just "name" in the output.
100    ///
101    /// Inside myapp/src/clients/pg/mod.rs, you may want to use a name like "myapp-client-pg".
102    ///
103    /// This function is quite cheap, but it may still be better to call it outside of loops, or
104    /// functions, if possible.
105    pub fn with_name(name: impl ToString) -> Bunyarr {
106        Self::with_options(Options {
107            name: name.to_string(),
108            writer: None,
109            min_level_inclusive: PROC_INFO.min_level_inclusive,
110        })
111    }
112
113    pub(crate) fn with_options(options: Options) -> Bunyarr {
114        Bunyarr {
115            writer: options.writer.unwrap_or(WriteImpl::StdOut),
116            name: options.name,
117            min_level_inclusive: options.min_level_inclusive,
118        }
119    }
120
121    #[inline]
122    pub fn debug(&self, extras: impl Extras, event_type: &'static str) {
123        if self.min_level_inclusive > 20 {
124            return;
125        }
126        self.log(20, extras, event_type)
127    }
128
129    #[inline]
130    pub fn info(&self, extras: impl Extras, event_type: &'static str) {
131        if self.min_level_inclusive > 30 {
132            return;
133        }
134        self.log(30, extras, event_type)
135    }
136
137    #[inline]
138    pub fn warn(&self, extras: impl Extras, event_type: &'static str) {
139        if self.min_level_inclusive > 40 {
140            return;
141        }
142        self.log(40, extras, event_type)
143    }
144
145    #[inline]
146    pub fn error(&self, extras: impl Extras, event_type: &'static str) {
147        if self.min_level_inclusive > 50 {
148            return;
149        }
150        self.log(50, extras, event_type)
151    }
152
153    #[inline]
154    pub fn fatal(&self, extras: impl Extras, event_type: &'static str) {
155        if self.min_level_inclusive > 60 {
156            return;
157        }
158        self.log(60, extras, event_type)
159    }
160
161    pub(crate) fn log(&self, level: u16, extras: impl Extras, event_type: &'static str) {
162        // https://github.com/trentm/node-bunyan#core-fields
163        // allowing overwriting of things disallowed by bunyan, not particularly concerned, prefer the order
164        let capacity = 7 + extras.size_hint().unwrap_or(5);
165        let mut obj = serde_json::Map::<String, Value>::with_capacity(capacity);
166        obj.insert(
167            "time".to_string(),
168            Value::String(
169                OffsetDateTime::now_utc()
170                    .format(&Rfc3339)
171                    .expect("built-in time and formatter"),
172            ),
173        );
174        obj.insert("level".to_string(), json!(level));
175        obj.insert("msg".to_string(), json!(event_type));
176        obj.insert("name".to_string(), json!(self.name));
177        for (key, value) in extras.into_extras() {
178            obj.insert(key, value);
179        }
180        obj.insert("hostname".to_string(), json!(PROC_INFO.hostname));
181        obj.insert("pid".to_string(), json!(PROC_INFO.pid));
182        obj.insert("v".to_string(), json!(0));
183        let _ = self.writer.work(|mut w| {
184            serde_json::to_writer(&mut w, &obj)?;
185            w.write_all(b"\n")
186        });
187    }
188
189    #[cfg(test)]
190    pub(crate) fn into_inner(self) -> WriteImpl {
191        self.writer
192    }
193}
194
195struct ProcInfo {
196    hostname: String,
197    pid: u32,
198    min_level_inclusive: u16,
199}
200
201impl ProcInfo {
202    fn new() -> ProcInfo {
203        ProcInfo {
204            hostname: gethostname::gethostname().to_string_lossy().to_string(),
205            pid: std::process::id(),
206            min_level_inclusive: std::env::var("LOG_LEVEL")
207                .map(|s| match s.to_ascii_lowercase().as_ref() {
208                    "debug" => 20,
209                    "info" => 30,
210                    "warn" => 40,
211                    "error" => 50,
212                    "fatal" => 60,
213                    _ => 30,
214                })
215                .unwrap_or(30),
216        }
217    }
218}