1use 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
57pub 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 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 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}