|
| 1 | +use std::{ |
| 2 | + fmt::{self, Write}, |
| 3 | + marker::PhantomData, |
| 4 | + time::SystemTime, |
| 5 | +}; |
| 6 | + |
| 7 | +use cfg_if::cfg_if; |
| 8 | +use serde::{ser::SerializeStruct, Serialize}; |
| 9 | + |
| 10 | +use crate::{ |
| 11 | + formatter::{FmtExtraInfo, Formatter}, |
| 12 | + Error, Record, StringBuf, __EOL, |
| 13 | +}; |
| 14 | + |
| 15 | +struct JsonRecord<'a>(&'a Record<'a>); |
| 16 | + |
| 17 | +impl<'a> Serialize for JsonRecord<'a> { |
| 18 | + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| 19 | + where |
| 20 | + S: serde::Serializer, |
| 21 | + { |
| 22 | + let src_loc = self.0.source_location(); |
| 23 | + |
| 24 | + let mut record = |
| 25 | + serializer.serialize_struct("JsonRecord", if src_loc.is_none() { 4 } else { 5 })?; |
| 26 | + |
| 27 | + record.serialize_field("level", &self.0.level())?; |
| 28 | + record.serialize_field( |
| 29 | + "timestamp", |
| 30 | + &self |
| 31 | + .0 |
| 32 | + .time() |
| 33 | + .duration_since(SystemTime::UNIX_EPOCH) |
| 34 | + .ok() |
| 35 | + // https://github.com/SpriteOvO/spdlog-rs/pull/69#discussion_r1694063293 |
| 36 | + .and_then(|dur| u64::try_from(dur.as_millis()).ok()) |
| 37 | + .expect("invalid timestamp"), |
| 38 | + )?; |
| 39 | + record.serialize_field("payload", self.0.payload())?; |
| 40 | + if let Some(logger_name) = self.0.logger_name() { |
| 41 | + record.serialize_field("logger", logger_name)?; |
| 42 | + } |
| 43 | + record.serialize_field("tid", &self.0.tid())?; |
| 44 | + if let Some(src_loc) = src_loc { |
| 45 | + record.serialize_field("source", src_loc)?; |
| 46 | + } |
| 47 | + |
| 48 | + record.end() |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +impl<'a> From<&'a Record<'a>> for JsonRecord<'a> { |
| 53 | + fn from(value: &'a Record<'a>) -> Self { |
| 54 | + JsonRecord(value) |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +enum JsonFormatterError { |
| 59 | + Fmt(fmt::Error), |
| 60 | + Serialization(serde_json::Error), |
| 61 | +} |
| 62 | + |
| 63 | +impl From<fmt::Error> for JsonFormatterError { |
| 64 | + fn from(value: fmt::Error) -> Self { |
| 65 | + JsonFormatterError::Fmt(value) |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +impl From<serde_json::Error> for JsonFormatterError { |
| 70 | + fn from(value: serde_json::Error) -> Self { |
| 71 | + JsonFormatterError::Serialization(value) |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +impl From<JsonFormatterError> for crate::Error { |
| 76 | + fn from(value: JsonFormatterError) -> Self { |
| 77 | + match value { |
| 78 | + JsonFormatterError::Fmt(e) => Error::FormatRecord(e), |
| 79 | + JsonFormatterError::Serialization(e) => Error::SerializeRecord(e.into()), |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +#[rustfmt::skip] |
| 85 | +/// JSON logs formatter. |
| 86 | +/// |
| 87 | +/// Each log will be serialized into a single line of JSON object with the following schema. |
| 88 | +/// |
| 89 | +/// ## Schema |
| 90 | +/// |
| 91 | +/// | Field | Type | Description | |
| 92 | +/// |-------------|--------------|--------------------------------------------------------------------------------------------------------------------------------| |
| 93 | +/// | `level` | String | The level of the log. Same as the return of [`Level::as_str`]. | |
| 94 | +/// | `timestamp` | Integer(u64) | The timestamp when the log was generated, in milliseconds since January 1, 1970 00:00:00 UTC. | |
| 95 | +/// | `payload` | String | The contents of the log. | |
| 96 | +/// | `logger` | String/Null | The name of the logger. Null if the logger has no name. | |
| 97 | +/// | `tid` | Integer(u64) | The thread ID when the log was generated. | |
| 98 | +/// | `source` | Object/Null | The source location of the log. See [`SourceLocation`] for its schema. Null if crate feature `source-location` is not enabled. | |
| 99 | +/// |
| 100 | +/// <div class="warning"> |
| 101 | +/// |
| 102 | +/// - If the type of a field is Null, the field will not be present or be `null`. |
| 103 | +/// |
| 104 | +/// - The order of the fields is not guaranteed. |
| 105 | +/// |
| 106 | +/// </div> |
| 107 | +/// |
| 108 | +/// --- |
| 109 | +/// |
| 110 | +/// ## Examples |
| 111 | +/// |
| 112 | +/// - Default: |
| 113 | +/// |
| 114 | +/// ```json |
| 115 | +/// {"level":"Info","timestamp":1722817424798,"payload":"hello, world!","tid":3472525} |
| 116 | +/// {"level":"Error","timestamp":1722817424798,"payload":"something went wrong","tid":3472525} |
| 117 | +/// ``` |
| 118 | +/// |
| 119 | +/// - If the logger has a name: |
| 120 | +/// |
| 121 | +/// ```json |
| 122 | +/// {"level":"Info","timestamp":1722817541459,"payload":"hello, world!","logger":"app-component","tid":3478045} |
| 123 | +/// {"level":"Error","timestamp":1722817541459,"payload":"something went wrong","logger":"app-component","tid":3478045} |
| 124 | +/// ``` |
| 125 | +/// |
| 126 | +/// - If crate feature `source-location` is enabled: |
| 127 | +/// |
| 128 | +/// ```json |
| 129 | +/// {"level":"Info","timestamp":1722817572709,"payload":"hello, world!","tid":3479856,"source":{"module_path":"my_app::say_hi","file":"src/say_hi.rs","line":4,"column":5}} |
| 130 | +/// {"level":"Error","timestamp":1722817572709,"payload":"something went wrong","tid":3479856,"source":{"module_path":"my_app::say_hi","file":"src/say_hi.rs","line":5,"column":5}} |
| 131 | +/// ``` |
| 132 | +/// |
| 133 | +/// [`Level::as_str`]: crate::Level::as_str |
| 134 | +/// [`SourceLocation`]: crate::SourceLocation |
| 135 | +#[derive(Clone)] |
| 136 | +pub struct JsonFormatter(PhantomData<()>); |
| 137 | + |
| 138 | +impl JsonFormatter { |
| 139 | + /// Constructs a `JsonFormatter`. |
| 140 | + #[must_use] |
| 141 | + pub fn new() -> JsonFormatter { |
| 142 | + JsonFormatter(PhantomData) |
| 143 | + } |
| 144 | + |
| 145 | + fn format_impl( |
| 146 | + &self, |
| 147 | + record: &Record, |
| 148 | + dest: &mut StringBuf, |
| 149 | + ) -> Result<FmtExtraInfo, JsonFormatterError> { |
| 150 | + cfg_if! { |
| 151 | + if #[cfg(not(feature = "flexible-string"))] { |
| 152 | + dest.reserve(crate::string_buf::RESERVE_SIZE); |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + let json_record: JsonRecord = record.into(); |
| 157 | + |
| 158 | + // TODO: https://github.com/serde-rs/json/issues/863 |
| 159 | + // |
| 160 | + // The performance can be significantly optimized here if the issue can be |
| 161 | + // solved. |
| 162 | + dest.write_str(&serde_json::to_string(&json_record)?)?; |
| 163 | + |
| 164 | + dest.write_str(__EOL)?; |
| 165 | + |
| 166 | + Ok(FmtExtraInfo { style_range: None }) |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +impl Formatter for JsonFormatter { |
| 171 | + fn format(&self, record: &Record, dest: &mut StringBuf) -> crate::Result<FmtExtraInfo> { |
| 172 | + self.format_impl(record, dest).map_err(Into::into) |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +impl Default for JsonFormatter { |
| 177 | + fn default() -> Self { |
| 178 | + JsonFormatter::new() |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +#[cfg(test)] |
| 183 | +mod tests { |
| 184 | + use chrono::prelude::*; |
| 185 | + |
| 186 | + use super::*; |
| 187 | + use crate::{Level, SourceLocation, __EOL}; |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn should_format_json() { |
| 191 | + let mut dest = StringBuf::new(); |
| 192 | + let formatter = JsonFormatter::new(); |
| 193 | + let record = Record::builder(Level::Info, "payload").build(); |
| 194 | + let extra_info = formatter.format(&record, &mut dest).unwrap(); |
| 195 | + |
| 196 | + let local_time: DateTime<Local> = record.time().into(); |
| 197 | + |
| 198 | + assert_eq!(extra_info.style_range, None); |
| 199 | + assert_eq!( |
| 200 | + dest.to_string(), |
| 201 | + format!( |
| 202 | + r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{}}}{}"#, |
| 203 | + local_time.timestamp_millis(), |
| 204 | + "payload", |
| 205 | + record.tid(), |
| 206 | + __EOL |
| 207 | + ) |
| 208 | + ); |
| 209 | + } |
| 210 | + |
| 211 | + #[test] |
| 212 | + fn should_format_json_with_logger_name() { |
| 213 | + let mut dest = StringBuf::new(); |
| 214 | + let formatter = JsonFormatter::new(); |
| 215 | + let record = Record::builder(Level::Info, "payload") |
| 216 | + .logger_name("my-component") |
| 217 | + .build(); |
| 218 | + let extra_info = formatter.format(&record, &mut dest).unwrap(); |
| 219 | + |
| 220 | + let local_time: DateTime<Local> = record.time().into(); |
| 221 | + |
| 222 | + assert_eq!(extra_info.style_range, None); |
| 223 | + assert_eq!( |
| 224 | + dest.to_string(), |
| 225 | + format!( |
| 226 | + r#"{{"level":"info","timestamp":{},"payload":"{}","logger":"my-component","tid":{}}}{}"#, |
| 227 | + local_time.timestamp_millis(), |
| 228 | + "payload", |
| 229 | + record.tid(), |
| 230 | + __EOL |
| 231 | + ) |
| 232 | + ); |
| 233 | + } |
| 234 | + |
| 235 | + #[test] |
| 236 | + fn should_format_json_with_src_loc() { |
| 237 | + let mut dest = StringBuf::new(); |
| 238 | + let formatter = JsonFormatter::new(); |
| 239 | + let record = Record::builder(Level::Info, "payload") |
| 240 | + .source_location(Some(SourceLocation::__new("module", "file.rs", 1, 2))) |
| 241 | + .build(); |
| 242 | + let extra_info = formatter.format(&record, &mut dest).unwrap(); |
| 243 | + |
| 244 | + let local_time: DateTime<Local> = record.time().into(); |
| 245 | + |
| 246 | + assert_eq!(extra_info.style_range, None); |
| 247 | + assert_eq!( |
| 248 | + dest.to_string(), |
| 249 | + format!( |
| 250 | + r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{},"source":{{"module_path":"module","file":"file.rs","line":1,"column":2}}}}{}"#, |
| 251 | + local_time.timestamp_millis(), |
| 252 | + "payload", |
| 253 | + record.tid(), |
| 254 | + __EOL |
| 255 | + ) |
| 256 | + ); |
| 257 | + } |
| 258 | +} |
0 commit comments