1use std::sync::atomic::{AtomicU8, Ordering};
4use std::sync::Arc;
5
6use serde::Serialize;
7use serde_json::{Map, Value};
8use tracing::Level;
9
10use super::config::ErrorLogDetail;
11
12const MAX_ERROR_CHAIN: usize = 16;
15const ERROR_DETAIL_TYPE_ONLY: u8 = 0;
16const ERROR_DETAIL_MESSAGE_ONLY: u8 = 1;
17const ERROR_DETAIL_FULL_CHAIN: u8 = 2;
18
19static ERROR_LOG_DETAIL: AtomicU8 = AtomicU8::new(ERROR_DETAIL_TYPE_ONLY);
20
21#[must_use = "a LogEvent does nothing until emit() is called"]
26pub struct LogEvent {
27 pub(crate) level: Level,
28 pub(crate) context: Arc<str>,
29 pub(crate) message: String,
30 pub(crate) fields: Map<String, Value>,
31 pub(crate) error: Option<Value>,
32}
33
34impl LogEvent {
35 pub fn field<T: Serialize>(mut self, key: &'static str, value: T) -> Self {
37 if let Ok(value) = serde_json::to_value(value) {
38 self.fields.insert(key.to_owned(), value);
39 }
40 self
41 }
42
43 pub fn error<E: std::error::Error>(mut self, error: &E) -> Self {
51 let mut object = Map::new();
52 object.insert(
53 "type".to_owned(),
54 Value::String(std::any::type_name::<E>().to_owned()),
55 );
56 match error_log_detail() {
57 ErrorLogDetail::TypeOnly => {}
58 ErrorLogDetail::MessageOnly => {
59 object.insert("message".to_owned(), Value::String(error.to_string()));
60 }
61 ErrorLogDetail::FullChain => {
62 object.insert("message".to_owned(), Value::String(error.to_string()));
63
64 let mut chain = Vec::new();
65 let mut source = error.source();
66 while let Some(error) = source {
67 if chain.len() >= MAX_ERROR_CHAIN {
68 chain.push(Value::String("... (chain truncated)".to_owned()));
69 break;
70 }
71 chain.push(Value::String(error.to_string()));
72 source = error.source();
73 }
74 if !chain.is_empty() {
75 object.insert("chain".to_owned(), Value::Array(chain));
76 }
77 }
78 }
79
80 self.error = Some(Value::Object(object));
81 self
82 }
83
84 pub fn emit(self) {
86 let LogEvent {
87 level,
88 context,
89 message,
90 fields,
91 error,
92 } = self;
93 let fields =
94 serde_json::to_string(&Value::Object(fields)).unwrap_or_else(|_| "{}".to_owned());
95 let error = error.map(|value| serde_json::to_string(&value).unwrap_or_default());
96 let context = context.as_ref();
97
98 macro_rules! emit_level {
102 ($level:ident) => {
103 match &error {
104 Some(error) => tracing::$level!(
105 tork.context = %context,
106 tork.fields = %fields,
107 tork.error = %error,
108 "{message}"
109 ),
110 None => tracing::$level!(
111 tork.context = %context,
112 tork.fields = %fields,
113 "{message}"
114 ),
115 }
116 };
117 }
118
119 match level {
120 Level::TRACE => emit_level!(trace),
121 Level::DEBUG => emit_level!(debug),
122 Level::INFO => emit_level!(info),
123 Level::WARN => emit_level!(warn),
124 Level::ERROR => emit_level!(error),
125 }
126 }
127}
128
129pub(crate) fn set_error_log_detail(detail: ErrorLogDetail) {
130 let raw = match detail {
131 ErrorLogDetail::TypeOnly => ERROR_DETAIL_TYPE_ONLY,
132 ErrorLogDetail::MessageOnly => ERROR_DETAIL_MESSAGE_ONLY,
133 ErrorLogDetail::FullChain => ERROR_DETAIL_FULL_CHAIN,
134 };
135 ERROR_LOG_DETAIL.store(raw, Ordering::Relaxed);
136}
137
138fn error_log_detail() -> ErrorLogDetail {
139 match ERROR_LOG_DETAIL.load(Ordering::Relaxed) {
140 ERROR_DETAIL_MESSAGE_ONLY => ErrorLogDetail::MessageOnly,
141 ERROR_DETAIL_FULL_CHAIN => ErrorLogDetail::FullChain,
142 _ => ErrorLogDetail::TypeOnly,
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::testing::LogRecorder;
150 use std::sync::Mutex;
151 use tracing_subscriber::layer::SubscriberExt;
152
153 static ERROR_DETAIL_TEST_LOCK: Mutex<()> = Mutex::new(());
154
155 #[derive(Debug)]
156 struct InnerError;
157
158 impl std::fmt::Display for InnerError {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 write!(f, "inner")
161 }
162 }
163
164 impl std::error::Error for InnerError {}
165
166 #[derive(Debug)]
167 struct OuterError;
168
169 impl std::fmt::Display for OuterError {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 write!(f, "outer")
172 }
173 }
174
175 impl std::error::Error for OuterError {
176 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
177 Some(&InnerError)
178 }
179 }
180
181 struct BrokenSerialize;
182
183 impl Serialize for BrokenSerialize {
184 fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
185 where
186 S: serde::Serializer,
187 {
188 Err(serde::ser::Error::custom("nope"))
189 }
190 }
191
192 #[test]
193 fn field_skips_unserializable_values_and_emit_records_error_chain() {
194 let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
195 set_error_log_detail(ErrorLogDetail::FullChain);
196 let recorder = LogRecorder::new();
197 let subscriber = tracing_subscriber::registry().with(recorder.clone());
198 let event = LogEvent {
199 level: Level::ERROR,
200 context: Arc::<str>::from("Orders"),
201 message: "failed".to_owned(),
202 fields: Map::new(),
203 error: None,
204 }
205 .field("ok", 1)
206 .field("skip", BrokenSerialize)
207 .error(&OuterError);
208
209 tracing::subscriber::with_default(subscriber, move || event.emit());
210
211 let records = recorder.records();
212 assert_eq!(records.len(), 1);
213 assert_eq!(records[0].context, "Orders");
214 assert_eq!(records[0].message, "failed");
215 assert_eq!(records[0].fields["ok"], Value::from(1));
216 set_error_log_detail(ErrorLogDetail::TypeOnly);
217 }
218
219 #[derive(Debug)]
221 struct Deep(usize, Option<Box<Deep>>);
222
223 impl std::fmt::Display for Deep {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 write!(f, "level {}", self.0)
226 }
227 }
228
229 impl std::error::Error for Deep {
230 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
231 self.1
232 .as_deref()
233 .map(|inner| inner as &(dyn std::error::Error + 'static))
234 }
235 }
236
237 fn deep_chain(depth: usize) -> Deep {
238 let mut error = Deep(0, None);
239 for level in 1..depth {
240 error = Deep(level, Some(Box::new(error)));
241 }
242 error
243 }
244
245 #[test]
246 fn error_chain_is_truncated_at_the_cap() {
247 let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
248 set_error_log_detail(ErrorLogDetail::FullChain);
249 let event = LogEvent {
250 level: Level::ERROR,
251 context: Arc::<str>::from("X"),
252 message: "boom".to_owned(),
253 fields: Map::new(),
254 error: None,
255 }
256 .error(&deep_chain(100));
257
258 let chain = event.error.as_ref().unwrap()["chain"].as_array().unwrap();
259 assert_eq!(chain.len(), MAX_ERROR_CHAIN + 1);
261 assert_eq!(
262 chain.last().unwrap(),
263 &Value::String("... (chain truncated)".to_owned())
264 );
265 set_error_log_detail(ErrorLogDetail::TypeOnly);
266 }
267
268 #[test]
269 fn default_error_detail_is_type_only() {
270 let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
271 set_error_log_detail(ErrorLogDetail::TypeOnly);
272 let event = LogEvent {
273 level: Level::ERROR,
274 context: Arc::<str>::from("X"),
275 message: "boom".to_owned(),
276 fields: Map::new(),
277 error: None,
278 }
279 .error(&OuterError);
280 let object = event.error.as_ref().unwrap().as_object().unwrap();
281 let ty = object.get("type").unwrap().as_str().unwrap();
282 assert!(ty.ends_with("OuterError"), "{ty}");
283 assert!(object.get("message").is_none());
284 assert!(object.get("chain").is_none());
285 }
286
287 #[test]
288 fn message_only_includes_the_top_level_message_without_the_chain() {
289 let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
290 set_error_log_detail(ErrorLogDetail::MessageOnly);
291 let event = LogEvent {
292 level: Level::ERROR,
293 context: Arc::<str>::from("X"),
294 message: "boom".to_owned(),
295 fields: Map::new(),
296 error: None,
297 }
298 .error(&OuterError);
299 let object = event.error.as_ref().unwrap().as_object().unwrap();
300 let ty = object.get("type").unwrap().as_str().unwrap();
301 assert!(ty.ends_with("OuterError"), "{ty}");
302 assert_eq!(object.get("message").unwrap(), "outer");
303 assert!(object.get("chain").is_none());
304 set_error_log_detail(ErrorLogDetail::TypeOnly);
305 }
306}