diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 697c781..3ebabf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ env: # - src/lib.rs # - Cargo.toml # - README.md - rust_minver: 1.56.0 + rust_minver: 1.60.0 defaults: run: @@ -49,7 +49,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] - fn_features: ['', 'log native libsystemd multi-thread runtime-pattern'] + fn_features: ['', 'log native libsystemd multi-thread runtime-pattern serde_json'] cfg_feature: ['', 'flexible-string', 'source-location'] runs-on: ${{ matrix.os }} steps: @@ -212,7 +212,7 @@ jobs: - name: Restore cargo caches uses: Swatinem/rust-cache@v2 - name: Run benchmark - run: cargo +nightly bench --features "multi-thread,runtime-pattern" --bench spdlog_rs --bench spdlog_rs_pattern | tee bench-results.txt + run: cargo +nightly bench --features "multi-thread,runtime-pattern,serde_json" --bench spdlog_rs --bench spdlog_rs_pattern | tee bench-results.txt - name: Discard irrelevant changes run: git checkout -- spdlog/Cargo.toml - name: Process results diff --git a/README.md b/README.md index 9c8e344..614999b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ When updating this, also update: - Cargo.toml --> -The current minimum supported Rust version is 1.56. +The current minimum supported Rust version is 1.60. `spdlog-rs` is built against the latest Rust stable release, it is not guaranteed to build on Rust versions earlier than the minimum supported version. diff --git a/spdlog-internal/Cargo.toml b/spdlog-internal/Cargo.toml index b688281..8269bdd 100644 --- a/spdlog-internal/Cargo.toml +++ b/spdlog-internal/Cargo.toml @@ -2,7 +2,7 @@ name = "spdlog-internal" version = "0.1.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.60" [dependencies] nom = "7.1.3" diff --git a/spdlog-macros/Cargo.toml b/spdlog-macros/Cargo.toml index 2c81167..a42a10a 100644 --- a/spdlog-macros/Cargo.toml +++ b/spdlog-macros/Cargo.toml @@ -2,7 +2,7 @@ name = "spdlog-macros" version = "0.1.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.60" description = "Macros implementation of crate \"spdlog-rs\"" repository = "https://github.com/SpriteOvO/spdlog-rs" license = "MIT OR Apache-2.0" diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 4b0f831..5901220 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -2,7 +2,7 @@ name = "spdlog-rs" version = "0.3.13" edition = "2021" -rust-version = "1.56" +rust-version = "1.60" description = "A fast and combinable Rust logging crate, inspired by the C++ logging library spdlog" repository = "https://github.com/SpriteOvO/spdlog-rs" license = "MIT OR Apache-2.0" @@ -37,6 +37,7 @@ native = [] libsystemd = ["libsystemd-sys"] multi-thread = ["crossbeam"] runtime-pattern = ["spdlog-internal"] +serde_json = ["serde", "dep:serde_json"] [dependencies] arc-swap = "1.5.1" @@ -50,6 +51,8 @@ if_chain = "1.0.2" is-terminal = "0.4" log = { version = "0.4.8", optional = true } once_cell = "1.16.0" +serde = { version = "1.0.163", optional = true, features = ["derive"] } +serde_json = { version = "1.0.120", optional = true } spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true } spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" @@ -106,6 +109,7 @@ harness = false [[bench]] name = "spdlog_rs_pattern" path = "benches/spdlog-rs/pattern.rs" +required-features = ["runtime-pattern", "serde_json"] [[bench]] name = "fast_log" path = "benches/fast_log/main.rs" diff --git a/spdlog/benches/spdlog-rs/pattern.rs b/spdlog/benches/spdlog-rs/pattern.rs index ea251e1..111dc4f 100644 --- a/spdlog/benches/spdlog-rs/pattern.rs +++ b/spdlog/benches/spdlog-rs/pattern.rs @@ -5,6 +5,8 @@ extern crate test; use std::{cell::RefCell, sync::Arc}; use paste::paste; +#[cfg(feature = "serde_json")] +use spdlog::formatter::JsonFormatter; use spdlog::{ formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter}, prelude::*, @@ -104,6 +106,12 @@ fn bench_1_full_formatter(bencher: &mut Bencher) { bench_formatter(bencher, FullFormatter::new()) } +#[cfg(feature = "serde_json")] +#[bench] +fn bench_1_json_formatter(bencher: &mut Bencher) { + bench_formatter(bencher, JsonFormatter::new()) +} + #[bench] fn bench_2_full_pattern_ct(bencher: &mut Bencher) { bench_full_pattern( diff --git a/spdlog/src/error.rs b/spdlog/src/error.rs index d0ef27f..4b06dde 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -98,6 +98,13 @@ pub enum Error { #[error("failed to build pattern at runtime: {0}")] BuildPattern(BuildPatternError), + /// Returned by [`Formatter`]s when an error occurs in serializing a log. + /// + /// [`Formatter`]: crate::formatter::Formatter + #[cfg(feature = "serde")] + #[error("failed to serialize log: {0}")] + SerializeRecord(io::Error), + /// Returned when multiple errors occurred. #[error("{0:?}")] Multiple(Vec), diff --git a/spdlog/src/formatter/json_formatter.rs b/spdlog/src/formatter/json_formatter.rs new file mode 100644 index 0000000..346caed --- /dev/null +++ b/spdlog/src/formatter/json_formatter.rs @@ -0,0 +1,258 @@ +use std::{ + fmt::{self, Write}, + marker::PhantomData, + time::SystemTime, +}; + +use cfg_if::cfg_if; +use serde::{ser::SerializeStruct, Serialize}; + +use crate::{ + formatter::{FmtExtraInfo, Formatter}, + Error, Record, StringBuf, __EOL, +}; + +struct JsonRecord<'a>(&'a Record<'a>); + +impl<'a> Serialize for JsonRecord<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let src_loc = self.0.source_location(); + + let mut record = + serializer.serialize_struct("JsonRecord", if src_loc.is_none() { 4 } else { 5 })?; + + record.serialize_field("level", &self.0.level())?; + record.serialize_field( + "timestamp", + &self + .0 + .time() + .duration_since(SystemTime::UNIX_EPOCH) + .ok() + // https://github.com/SpriteOvO/spdlog-rs/pull/69#discussion_r1694063293 + .and_then(|dur| u64::try_from(dur.as_millis()).ok()) + .expect("invalid timestamp"), + )?; + record.serialize_field("payload", self.0.payload())?; + 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) = src_loc { + record.serialize_field("source", src_loc)?; + } + + record.end() + } +} + +impl<'a> From<&'a Record<'a>> for JsonRecord<'a> { + fn from(value: &'a Record<'a>) -> Self { + JsonRecord(value) + } +} + +enum JsonFormatterError { + Fmt(fmt::Error), + Serialization(serde_json::Error), +} + +impl From for JsonFormatterError { + fn from(value: fmt::Error) -> Self { + JsonFormatterError::Fmt(value) + } +} + +impl From for JsonFormatterError { + fn from(value: serde_json::Error) -> Self { + JsonFormatterError::Serialization(value) + } +} + +impl From 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] +/// JSON logs formatter. +/// +/// Each log will be serialized into a single line of JSON object with the following schema. +/// +/// ## Schema +/// +/// | Field | Type | Description | +/// |-------------|--------------|--------------------------------------------------------------------------------------------------------------------------------| +/// | `level` | String | The level of the log. Same as the return of [`Level::as_str`]. | +/// | `timestamp` | Integer(u64) | The timestamp when the log was generated, in milliseconds since January 1, 1970 00:00:00 UTC. | +/// | `payload` | String | The contents of the log. | +/// | `logger` | String/Null | The name of the logger. Null if the logger has no name. | +/// | `tid` | Integer(u64) | The thread ID when the log was generated. | +/// | `source` | Object/Null | The source location of the log. See [`SourceLocation`] for its schema. Null if crate feature `source-location` is not enabled. | +/// +///
+/// +/// - If the type of a field is Null, the field will not be present or be `null`. +/// +/// - The order of the fields is not guaranteed. +/// +///
+/// +/// --- +/// +/// ## Examples +/// +/// - Default: +/// +/// ```json +/// {"level":"Info","timestamp":1722817424798,"payload":"hello, world!","tid":3472525} +/// {"level":"Error","timestamp":1722817424798,"payload":"something went wrong","tid":3472525} +/// ``` +/// +/// - If the logger has a name: +/// +/// ```json +/// {"level":"Info","timestamp":1722817541459,"payload":"hello, world!","logger":"app-component","tid":3478045} +/// {"level":"Error","timestamp":1722817541459,"payload":"something went wrong","logger":"app-component","tid":3478045} +/// ``` +/// +/// - If crate feature `source-location` is enabled: +/// +/// ```json +/// {"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}} +/// {"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}} +/// ``` +/// +/// [`Level::as_str`]: crate::Level::as_str +/// [`SourceLocation`]: crate::SourceLocation +#[derive(Clone)] +pub struct JsonFormatter(PhantomData<()>); + +impl JsonFormatter { + /// Constructs a `JsonFormatter`. + #[must_use] + pub fn new() -> JsonFormatter { + JsonFormatter(PhantomData) + } + + fn format_impl( + &self, + record: &Record, + dest: &mut StringBuf, + ) -> Result { + cfg_if! { + if #[cfg(not(feature = "flexible-string"))] { + dest.reserve(crate::string_buf::RESERVE_SIZE); + } + } + + let json_record: JsonRecord = record.into(); + + // TODO: https://github.com/serde-rs/json/issues/863 + // + // The performance can be significantly optimized here if the issue can be + // solved. + dest.write_str(&serde_json::to_string(&json_record)?)?; + + dest.write_str(__EOL)?; + + Ok(FmtExtraInfo { style_range: None }) + } +} + +impl Formatter for JsonFormatter { + fn format(&self, record: &Record, dest: &mut StringBuf) -> crate::Result { + self.format_impl(record, dest).map_err(Into::into) + } +} + +impl Default for JsonFormatter { + fn default() -> Self { + JsonFormatter::new() + } +} + +#[cfg(test)] +mod tests { + use chrono::prelude::*; + + use super::*; + use crate::{Level, SourceLocation, __EOL}; + + #[test] + fn should_format_json() { + let mut dest = StringBuf::new(); + let formatter = JsonFormatter::new(); + let record = Record::builder(Level::Info, "payload").build(); + let extra_info = formatter.format(&record, &mut dest).unwrap(); + + let local_time: DateTime = record.time().into(); + + assert_eq!(extra_info.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::builder(Level::Info, "payload") + .logger_name("my-component") + .build(); + let extra_info = formatter.format(&record, &mut dest).unwrap(); + + let local_time: DateTime = record.time().into(); + + assert_eq!(extra_info.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::builder(Level::Info, "payload") + .source_location(Some(SourceLocation::__new("module", "file.rs", 1, 2))) + .build(); + let extra_info = formatter.format(&record, &mut dest).unwrap(); + + let local_time: DateTime = record.time().into(); + + assert_eq!(extra_info.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 + ) + ); + } +} diff --git a/spdlog/src/formatter/mod.rs b/spdlog/src/formatter/mod.rs index 2c6623a..c5884c6 100644 --- a/spdlog/src/formatter/mod.rs +++ b/spdlog/src/formatter/mod.rs @@ -54,6 +54,8 @@ mod full_formatter; all(doc, not(doctest)) ))] mod journald_formatter; +#[cfg(feature = "serde_json")] +mod json_formatter; mod local_time_cacher; mod pattern_formatter; @@ -66,6 +68,8 @@ pub use full_formatter::*; all(doc, not(doctest)) ))] pub(crate) use journald_formatter::*; +#[cfg(feature = "serde_json")] +pub use json_formatter::*; pub(crate) use local_time_cacher::*; pub use pattern_formatter::*; diff --git a/spdlog/src/level.rs b/spdlog/src/level.rs index 26df4dd..1bc4bbf 100644 --- a/spdlog/src/level.rs +++ b/spdlog/src/level.rs @@ -67,6 +67,16 @@ pub enum Level { Trace, } +#[cfg(feature = "serde")] +impl serde::Serialize for Level { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + cfg_if! { if #[cfg(test)] { crate::utils::const_assert!(atomic::Atomic::::is_lock_free()); diff --git a/spdlog/src/lib.rs b/spdlog/src/lib.rs index d5afcc7..3bf3fdf 100644 --- a/spdlog/src/lib.rs +++ b/spdlog/src/lib.rs @@ -216,7 +216,7 @@ //! - README.md //! --> //! -//! The current minimum supported Rust version is 1.56. +//! The current minimum supported Rust version is 1.60. //! //! `spdlog-rs` is built against the latest Rust stable release, it is not //! guaranteed to build on Rust versions earlier than the minimum supported diff --git a/spdlog/src/sink/rotating_file_sink.rs b/spdlog/src/sink/rotating_file_sink.rs index 6e297dc..05379f3 100644 --- a/spdlog/src/sink/rotating_file_sink.rs +++ b/spdlog/src/sink/rotating_file_sink.rs @@ -648,7 +648,6 @@ impl TimePoint { #[must_use] fn delta_chrono(&self) -> chrono::Duration { - #[allow(deprecated)] // For keeping the current MSRV 1.56.0 match self { Self::Daily { .. } => chrono::Duration::days(1), Self::Hourly { .. } => chrono::Duration::hours(1), diff --git a/spdlog/src/source_location.rs b/spdlog/src/source_location.rs index fcead47..1db56aa 100644 --- a/spdlog/src/source_location.rs +++ b/spdlog/src/source_location.rs @@ -5,8 +5,21 @@ use std::path; /// Usually users don't need to construct it manually, but if you do, use macro /// [`source_location_current`]. /// +/// ## Schema +/// +/// This struct is implemented [`serde::Serialize`] if crate feature `serde` is +/// enabled. +/// +/// | Field | Type | +/// |---------------|--------| +/// | `module_path` | String | +/// | `file` | String | +/// | `line` | u32 | +/// | `column` | u32 | +/// /// [`source_location_current`]: crate::source_location_current #[derive(Clone, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct SourceLocation { module_path: &'static str, file: &'static str,