use std::{
fmt::{self, Write as _},
marker::PhantomData,
time::SystemTime,
};
use serde::{
ser::{SerializeMap as _, SerializeStruct as _},
Serialize, Serializer,
};
use crate::{
formatter::{Formatter, FormatterContext},
Error, Record, StringBuf, __EOL,
};
fn opt_to_num<T>(opt: Option<T>) -> usize {
opt.map_or(0, |_| 1)
}
struct JsonRecord<'a, 'b>(&'a Record<'b>);
impl Serialize for JsonRecord<'_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let fields_len =
4 + opt_to_num(self.0.logger_name()) + opt_to_num(self.0.source_location());
let mut record = serializer.serialize_struct("JsonRecord", fields_len)?;
record.serialize_field("level", &self.0.level())?;
record.serialize_field(
"timestamp",
&self
.0
.time()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.and_then(|dur| u64::try_from(dur.as_millis()).ok())
.expect("invalid timestamp"),
)?;
record.serialize_field("payload", self.0.payload())?;
if !self.0.key_values().is_empty() {
struct JsonKV<'a, 'b>(&'a Record<'b>);
impl Serialize for JsonKV<'_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let kv = self.0.key_values();
let mut map = serializer.serialize_map(Some(kv.len()))?;
for (key, value) in kv {
map.serialize_entry(key.as_str(), &value)?;
}
map.end()
}
}
record.serialize_field("kv", &JsonKV(self.0))?;
}
if let Some(logger_name) = self.0.logger_name() {
record.serialize_field("logger", logger_name)?;
}
record.serialize_field("tid", &self.0.tid())?;
if let Some(src_loc) = self.0.source_location() {
record.serialize_field("source", src_loc)?;
}
record.end()
}
}
enum JsonFormatterError {
Fmt(fmt::Error),
Serialization(serde_json::Error),
}
impl From<fmt::Error> for JsonFormatterError {
fn from(value: fmt::Error) -> Self {
JsonFormatterError::Fmt(value)
}
}
impl From<serde_json::Error> for JsonFormatterError {
fn from(value: serde_json::Error) -> Self {
JsonFormatterError::Serialization(value)
}
}
impl From<JsonFormatterError> for crate::Error {
fn from(value: JsonFormatterError) -> Self {
match value {
JsonFormatterError::Fmt(e) => Error::FormatRecord(e),
JsonFormatterError::Serialization(e) => Error::SerializeRecord(e.into()),
}
}
}
#[rustfmt::skip]
#[derive(Clone)]
pub struct JsonFormatter(PhantomData<()>);
impl JsonFormatter {
#[must_use]
pub fn new() -> JsonFormatter {
JsonFormatter(PhantomData)
}
fn format_impl(
&self,
record: &Record,
dest: &mut StringBuf,
_ctx: &mut FormatterContext,
) -> Result<(), JsonFormatterError> {
#[cfg(not(feature = "flexible-string"))]
dest.reserve(crate::string_buf::RESERVE_SIZE);
dest.write_str(&serde_json::to_string(&JsonRecord(record))?)?;
dest.write_str(__EOL)?;
Ok(())
}
}
impl Formatter for JsonFormatter {
fn format(
&self,
record: &Record,
dest: &mut StringBuf,
ctx: &mut FormatterContext,
) -> crate::Result<()> {
self.format_impl(record, dest, ctx).map_err(Into::into)
}
}
impl Default for JsonFormatter {
fn default() -> Self {
JsonFormatter::new()
}
}
#[cfg(test)]
mod tests {
use chrono::prelude::*;
use super::*;
use crate::{kv, Level, SourceLocation, __EOL};
#[test]
fn should_format_json() {
let mut dest = StringBuf::new();
let formatter = JsonFormatter::new();
let record = Record::new(Level::Info, "payload", None, None, &[]);
let mut ctx = FormatterContext::new();
formatter.format(&record, &mut dest, &mut ctx).unwrap();
let local_time: DateTime<Local> = record.time().into();
assert_eq!(ctx.style_range(), None);
assert_eq!(
dest.to_string(),
format!(
r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{}}}{}"#,
local_time.timestamp_millis(),
"payload",
record.tid(),
__EOL
)
);
}
#[test]
fn should_format_json_with_logger_name() {
let mut dest = StringBuf::new();
let formatter = JsonFormatter::new();
let record = Record::new(Level::Info, "payload", None, Some("my-component"), &[]);
let mut ctx = FormatterContext::new();
formatter.format(&record, &mut dest, &mut ctx).unwrap();
let local_time: DateTime<Local> = record.time().into();
assert_eq!(ctx.style_range(), None);
assert_eq!(
dest.to_string(),
format!(
r#"{{"level":"info","timestamp":{},"payload":"{}","logger":"my-component","tid":{}}}{}"#,
local_time.timestamp_millis(),
"payload",
record.tid(),
__EOL
)
);
}
#[test]
fn should_format_json_with_src_loc() {
let mut dest = StringBuf::new();
let formatter = JsonFormatter::new();
let record = Record::new(
Level::Info,
"payload",
Some(SourceLocation::__new("module", "file.rs", 1, 2)),
None,
&[],
);
let mut ctx = FormatterContext::new();
formatter.format(&record, &mut dest, &mut ctx).unwrap();
let local_time: DateTime<Local> = record.time().into();
assert_eq!(ctx.style_range(), None);
assert_eq!(
dest.to_string(),
format!(
r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{},"source":{{"module_path":"module","file":"file.rs","line":1,"column":2}}}}{}"#,
local_time.timestamp_millis(),
"payload",
record.tid(),
__EOL
)
);
}
#[test]
fn should_format_json_with_kv() {
let mut dest = StringBuf::new();
let formatter = JsonFormatter::new();
let kvs = [
(kv::Key::__from_static_str("k1"), kv::Value::from(114)),
(kv::Key::__from_static_str("k2"), kv::Value::from("514")),
];
let record = Record::new(Level::Info, "payload", None, None, &kvs);
let mut ctx = FormatterContext::new();
formatter.format(&record, &mut dest, &mut ctx).unwrap();
let local_time: DateTime<Local> = record.time().into();
assert_eq!(ctx.style_range(), None);
assert_eq!(
dest.to_string(),
format!(
r#"{{"level":"info","timestamp":{},"payload":"{}","kv":{{"k1":114,"k2":"514"}},"tid":{}}}{}"#,
local_time.timestamp_millis(),
"payload",
record.tid(),
__EOL
)
);
}
}