use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use serde::Serialize;
use serde_json::{Map, Value};
use tracing::Level;
use super::config::ErrorLogDetail;
const MAX_ERROR_CHAIN: usize = 16;
const ERROR_DETAIL_TYPE_ONLY: u8 = 0;
const ERROR_DETAIL_MESSAGE_ONLY: u8 = 1;
const ERROR_DETAIL_FULL_CHAIN: u8 = 2;
static ERROR_LOG_DETAIL: AtomicU8 = AtomicU8::new(ERROR_DETAIL_TYPE_ONLY);
#[must_use = "a LogEvent does nothing until emit() is called"]
pub struct LogEvent {
pub(crate) level: Level,
pub(crate) context: Arc<str>,
pub(crate) message: String,
pub(crate) fields: Map<String, Value>,
pub(crate) error: Option<Value>,
}
impl LogEvent {
pub fn field<T: Serialize>(mut self, key: &'static str, value: T) -> Self {
if let Ok(value) = serde_json::to_value(value) {
self.fields.insert(key.to_owned(), value);
}
self
}
pub fn error<E: std::error::Error>(mut self, error: &E) -> Self {
let mut object = Map::new();
object.insert(
"type".to_owned(),
Value::String(std::any::type_name::<E>().to_owned()),
);
match error_log_detail() {
ErrorLogDetail::TypeOnly => {}
ErrorLogDetail::MessageOnly => {
object.insert("message".to_owned(), Value::String(error.to_string()));
}
ErrorLogDetail::FullChain => {
object.insert("message".to_owned(), Value::String(error.to_string()));
let mut chain = Vec::new();
let mut source = error.source();
while let Some(error) = source {
if chain.len() >= MAX_ERROR_CHAIN {
chain.push(Value::String("... (chain truncated)".to_owned()));
break;
}
chain.push(Value::String(error.to_string()));
source = error.source();
}
if !chain.is_empty() {
object.insert("chain".to_owned(), Value::Array(chain));
}
}
}
self.error = Some(Value::Object(object));
self
}
pub fn emit(self) {
let LogEvent {
level,
context,
message,
fields,
error,
} = self;
let fields =
serde_json::to_string(&Value::Object(fields)).unwrap_or_else(|_| "{}".to_owned());
let error = error.map(|value| serde_json::to_string(&value).unwrap_or_default());
let context = context.as_ref();
macro_rules! emit_level {
($level:ident) => {
match &error {
Some(error) => tracing::$level!(
tork.context = %context,
tork.fields = %fields,
tork.error = %error,
"{message}"
),
None => tracing::$level!(
tork.context = %context,
tork.fields = %fields,
"{message}"
),
}
};
}
match level {
Level::TRACE => emit_level!(trace),
Level::DEBUG => emit_level!(debug),
Level::INFO => emit_level!(info),
Level::WARN => emit_level!(warn),
Level::ERROR => emit_level!(error),
}
}
}
pub(crate) fn set_error_log_detail(detail: ErrorLogDetail) {
let raw = match detail {
ErrorLogDetail::TypeOnly => ERROR_DETAIL_TYPE_ONLY,
ErrorLogDetail::MessageOnly => ERROR_DETAIL_MESSAGE_ONLY,
ErrorLogDetail::FullChain => ERROR_DETAIL_FULL_CHAIN,
};
ERROR_LOG_DETAIL.store(raw, Ordering::Relaxed);
}
fn error_log_detail() -> ErrorLogDetail {
match ERROR_LOG_DETAIL.load(Ordering::Relaxed) {
ERROR_DETAIL_MESSAGE_ONLY => ErrorLogDetail::MessageOnly,
ERROR_DETAIL_FULL_CHAIN => ErrorLogDetail::FullChain,
_ => ErrorLogDetail::TypeOnly,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::LogRecorder;
use std::sync::Mutex;
use tracing_subscriber::layer::SubscriberExt;
static ERROR_DETAIL_TEST_LOCK: Mutex<()> = Mutex::new(());
#[derive(Debug)]
struct InnerError;
impl std::fmt::Display for InnerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "inner")
}
}
impl std::error::Error for InnerError {}
#[derive(Debug)]
struct OuterError;
impl std::fmt::Display for OuterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "outer")
}
}
impl std::error::Error for OuterError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&InnerError)
}
}
struct BrokenSerialize;
impl Serialize for BrokenSerialize {
fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(serde::ser::Error::custom("nope"))
}
}
#[test]
fn field_skips_unserializable_values_and_emit_records_error_chain() {
let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
set_error_log_detail(ErrorLogDetail::FullChain);
let recorder = LogRecorder::new();
let subscriber = tracing_subscriber::registry().with(recorder.clone());
let event = LogEvent {
level: Level::ERROR,
context: Arc::<str>::from("Orders"),
message: "failed".to_owned(),
fields: Map::new(),
error: None,
}
.field("ok", 1)
.field("skip", BrokenSerialize)
.error(&OuterError);
tracing::subscriber::with_default(subscriber, move || event.emit());
let records = recorder.records();
assert_eq!(records.len(), 1);
assert_eq!(records[0].context, "Orders");
assert_eq!(records[0].message, "failed");
assert_eq!(records[0].fields["ok"], Value::from(1));
set_error_log_detail(ErrorLogDetail::TypeOnly);
}
#[derive(Debug)]
struct Deep(usize, Option<Box<Deep>>);
impl std::fmt::Display for Deep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "level {}", self.0)
}
}
impl std::error::Error for Deep {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.1
.as_deref()
.map(|inner| inner as &(dyn std::error::Error + 'static))
}
}
fn deep_chain(depth: usize) -> Deep {
let mut error = Deep(0, None);
for level in 1..depth {
error = Deep(level, Some(Box::new(error)));
}
error
}
#[test]
fn error_chain_is_truncated_at_the_cap() {
let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
set_error_log_detail(ErrorLogDetail::FullChain);
let event = LogEvent {
level: Level::ERROR,
context: Arc::<str>::from("X"),
message: "boom".to_owned(),
fields: Map::new(),
error: None,
}
.error(&deep_chain(100));
let chain = event.error.as_ref().unwrap()["chain"].as_array().unwrap();
assert_eq!(chain.len(), MAX_ERROR_CHAIN + 1);
assert_eq!(
chain.last().unwrap(),
&Value::String("... (chain truncated)".to_owned())
);
set_error_log_detail(ErrorLogDetail::TypeOnly);
}
#[test]
fn default_error_detail_is_type_only() {
let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
set_error_log_detail(ErrorLogDetail::TypeOnly);
let event = LogEvent {
level: Level::ERROR,
context: Arc::<str>::from("X"),
message: "boom".to_owned(),
fields: Map::new(),
error: None,
}
.error(&OuterError);
let object = event.error.as_ref().unwrap().as_object().unwrap();
let ty = object.get("type").unwrap().as_str().unwrap();
assert!(ty.ends_with("OuterError"), "{ty}");
assert!(object.get("message").is_none());
assert!(object.get("chain").is_none());
}
#[test]
fn message_only_includes_the_top_level_message_without_the_chain() {
let _guard = ERROR_DETAIL_TEST_LOCK.lock().unwrap();
set_error_log_detail(ErrorLogDetail::MessageOnly);
let event = LogEvent {
level: Level::ERROR,
context: Arc::<str>::from("X"),
message: "boom".to_owned(),
fields: Map::new(),
error: None,
}
.error(&OuterError);
let object = event.error.as_ref().unwrap().as_object().unwrap();
let ty = object.get("type").unwrap().as_str().unwrap();
assert!(ty.ends_with("OuterError"), "{ty}");
assert_eq!(object.get("message").unwrap(), "outer");
assert!(object.get("chain").is_none());
set_error_log_detail(ErrorLogDetail::TypeOnly);
}
}