diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 31000a2..d7a5d23 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,3 +20,5 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose + - name: Run tests with default_encoders feature + run: cargo test --verbose --features default_encoders diff --git a/README.md b/README.md index 5db379b..e043084 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ This `log::Log` implementation allows to log to a Redis server. It supports writ You can specify custom encoders for pub/sub and stream log messages. Using the `default_encoders` feature default implementations for the encoders are available. This feature is disabled by default. +If you enable the `shared_logger` feature you can use the `RedisLogger` inside a `simplelog::CombinedLogger`. + ## Usage Add the dependency to your `Cargo.toml`: @@ -16,13 +18,59 @@ Add the dependency to your `Cargo.toml`: [dependencies] log = "0.4" redis = "0.24" -redis_logger = "0.2" +redis_logger = "0.3" ``` How to use in your application: Build a `RedisLoggerConfig` using the `RedisLoggerConfigBuilder` methods. Specify a connection and at least one pub/sub or stream channel. Use this configuration to either instantiate a `RedisLogger` instance with `RedisLogger::new` if you wish to use this logger with other loggers (like the [parallel_logger](https://crates.io/crates/parallel_logger) crate or [CombinedLogger](https://crates.io/crates/simplelog) logger from the `simplelog` crate) or use the `RedisLogger::init` method to initialize the logger as the only logger for the application. +A simple example using the `default_encoders` feature and setting the `RedisLogger` as the only logger would look like this: +```rust +let redis_client = redis::Client::open(REDIS_URL).unwrap(); +let redis_connection = redis_client.get_connection().unwrap(); + +fn main() { + RedisLogger::init( + LevelFilter::Debug, + RedisLoggerConfigBuilder::build_with_pubsub_default(redis_connection, vec!["logging".into()]), + ); +} +``` + +This broader example uses `RedisLogger` inside a `ParallelLogger` and encodes messages for pub/sub using the `bincode` crate and a custom `PubSubEncoder`: +```rust +struct BincodeRedisEncoder; + +impl PubSubEncoder for BincodeRedisEncoder { + fn encode(&self, record: &log::Record) -> Vec { + let mut slice = [0u8; 2000]; + let message = SerializableLogRecord::from(record); + let size = bincode::encode_into_slice(message, &mut slice, BINCODE_CONFIG).unwrap(); + let slice = &slice[..size]; + slice.to_vec() + } +} + +fn main() { + let redis_client = redis::Client::open(REDIS_URL).unwrap(); + let redis_connection = redis_client.get_connection().unwrap(); + + ParallelLogger::init( + log::LevelFilter::Debug, + ParallelMode::Sequential, + vec![ + FileLogger::new(LevelFilter::Debug, "log_file.log"), + TerminalLogger::new(LevelFilter::Info), + RedisLogger::new( + LevelFilter::Debug, + RedisLoggerConfigBuilder::build_with_pubsub(redis_connection, vec!["logging".into()], BincodeRedisEncoder {}), + ), + ], + ); +} +``` + ## License Licensed under either of diff --git a/src/defaults.rs b/src/defaults.rs index 70e670b..e177ea6 100644 --- a/src/defaults.rs +++ b/src/defaults.rs @@ -12,28 +12,25 @@ //! //! `DefaultStreamEncoder` is a default implementation of the `StreamEncoder` trait. //! It encodes a `log::Record` into a vector of tuples, where each tuple contains a field name from the `Record` and the -//! corresponding value as a byte vector. If a field in the `Record` is `None`, it uses a default value. +//! corresponding value as a byte vector. If a field in the `Record` is `None`, the byte vector is empty. //! //! ## Usage //! //! You can use these default encoders when you don't need to customize the encoding process. //! If you need to customize the encoding, you can implement the `PubSubEncoder` and `StreamEncoder` traits yourself. -use std::marker::PhantomData; - use serializable_log_record::SerializableLogRecord; use super::{PubSubEncoder, Record, StreamEncoder}; /// Default implementation of the `PubSubEncoder` trait converting the incoming `log::Record` into a JSON object. #[derive(Debug)] -pub struct DefaultPubSubEncoder { - __private: PhantomData<()>, -} +#[non_exhaustive] +pub struct DefaultPubSubEncoder {} impl DefaultPubSubEncoder { pub const fn new() -> Self { - Self { __private: PhantomData } + Self {} } } @@ -44,28 +41,27 @@ impl PubSubEncoder for DefaultPubSubEncoder { } } -/// Default implementation of the `StreamEncoder` trait converting the incoming `log::Record` into a vector of tuples. +/// Default implementation of the `StreamEncoder` trait converting the incoming `log::Record` into a vector of tuples of field name and bytes. #[derive(Debug)] -pub struct DefaultStreamEncoder { - __private: PhantomData<()>, -} +#[non_exhaustive] +pub struct DefaultStreamEncoder {} impl DefaultStreamEncoder { pub const fn new() -> Self { - Self { __private: PhantomData } + Self {} } } impl StreamEncoder for DefaultStreamEncoder { - fn encode(&self, record: &Record) -> Vec<(&'static str, Vec)> { - vec![ - ("level", record.level().as_str().to_owned().into_bytes()), - ("args", record.args().to_string().into_bytes()), - ("target", record.target().to_owned().into_bytes()), - ("module_path", record.module_path().unwrap_or("null").to_owned().into_bytes()), - ("file", record.file().unwrap_or("null").to_owned().into_bytes()), - ("line", record.line().unwrap_or(0).to_string().into_bytes()), - ] + fn encode(&self, record: &Record) -> Vec<(String, Vec)> { + let ser_record = SerializableLogRecord::from(record); + serde_json::to_value(&ser_record) + .unwrap_or_else(|_| serde_json::json!({})) + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_owned().into_bytes())) + .collect() } } @@ -104,12 +100,12 @@ mod tests { .build(); let expected = vec![ - ("level", b"ERROR".to_vec()), - ("args", b"Error message".to_vec()), - ("target", b"my_target".to_vec()), - ("module_path", b"null".to_vec()), - ("file", b"my_file.rs".to_vec()), - ("line", b"0".to_vec()), + ("args".to_owned(), b"Error message".to_vec()), + ("file".to_owned(), b"my_file.rs".to_vec()), + ("level".to_owned(), b"ERROR".to_vec()), + ("line".to_owned(), b"".to_vec()), + ("module_path".to_owned(), b"".to_vec()), + ("target".to_owned(), b"my_target".to_vec()), ]; assert_eq!(encoder.encode(&record), expected); diff --git a/src/lib.rs b/src/lib.rs index 91f8054..ad3bd8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,12 +30,52 @@ //! To use this logger, you need to create a `RedisLoggerConfig` (using `RedisLoggerConfigBuilder`), create a `RedisLogger` with the config, //! either by calling `::new` or `::init`, the latter of which also sets the logger as the global logger. //! +//! We recommend using this logger with the `parallel_logger` crate to avoid blocking the main thread when logging to Redis. +//! +//! ## Example +//! This example shows how to implement a `PubSubEncoder` that encodes log messages as a byte vector using the `bincode` crate. It also +//! shows how to configure `RedisLogger` to use this encoder while being part of multiple loggers that run on a separate thread using `parallel_logger`. +//! ```rust,ignore +//! struct BincodeRedisEncoder; +//! +//! impl PubSubEncoder for BincodeRedisEncoder { +//! fn encode(&self, record: &log::Record) -> Vec { +//! let mut slice = [0u8; 2000]; +//! let message = SerializableLogRecord::from(record); +//! let size = bincode::encode_into_slice(message, &mut slice, BINCODE_CONFIG).unwrap(); +//! let slice = &slice[..size]; +//! slice.to_vec() +//! } +//! } +//! +//! fn main() { +//! let redis_client = redis::Client::open(REDIS_URL).unwrap(); +//! let redis_connection = redis_client.get_connection().unwrap(); +//! +//! ParallelLogger::init( +//! log::LevelFilter::Debug, +//! ParallelMode::Sequential, +//! vec![ +//! FileLogger::new(LevelFilter::Debug, "log_file.log"), +//! TerminalLogger::new(LevelFilter::Info), +//! RedisLogger::new( +//! LevelFilter::Debug, +//! RedisLoggerConfigBuilder::build_with_pubsub(redis_connection, vec!["logging".into()], BincodeRedisEncoder {}), +//! ), +//! ], +//! ); +//! } +//! ``` +//! Using `RedisLogger::init` insted of `RedisLogger::new` would allow the logger to be used as the only global logger. +//! //! ## Features //! //! This module has a feature flag `default_encoders` that, when enabled, provides default implementations //! of `PubSubEncoder` and `StreamEncoder` that encode the log messages as JSON or as a vector of tuples, respectively. +//! +//! Another feature flag `shared_logger` implements the `simplelog::SharedLogger` trait for `RedisLogger`. This enables use in a `simplelog::CombinedLogger`. -use std::{marker::PhantomData, sync::Mutex}; +use std::sync::Mutex; use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; use redis::ConnectionLike; @@ -57,15 +97,14 @@ pub trait PubSubEncoder: Send + Sync + Sized { /// Trait for encoding log messages to be added to a Redis stream. pub trait StreamEncoder: Send + Sync + Sized { /// Encodes the given `log::Record` into a vector of tuples of a field name and the corresponding value as a byte vector. - fn encode(&self, record: &Record) -> Vec<(&'static str, Vec)>; + fn encode(&self, record: &Record) -> Vec<(String, Vec)>; } /// Placeholder. Cannot be instantiated or used. Necessary as a placeholder when not specifing a pub/sub encoder. #[derive(Debug)] #[doc(hidden)] -pub struct DummyPubSubEncoder { - __private: PhantomData<()>, -} +#[non_exhaustive] +pub struct DummyPubSubEncoder {} #[doc(hidden)] impl PubSubEncoder for DummyPubSubEncoder { @@ -77,13 +116,12 @@ impl PubSubEncoder for DummyPubSubEncoder { /// Placeholder. Cannot be instantiated or used. Necessary as a placeholder when not specifing a stream encoder. #[derive(Debug)] #[doc(hidden)] -pub struct DummyStreamEncoder { - __private: PhantomData<()>, -} +#[non_exhaustive] +pub struct DummyStreamEncoder {} #[doc(hidden)] impl StreamEncoder for DummyStreamEncoder { - fn encode(&self, _record: &Record) -> Vec<(&'static str, Vec)> { + fn encode(&self, _record: &Record) -> Vec<(String, Vec)> { panic!() } } @@ -206,9 +244,8 @@ where /// /// Panics if the channels or streams vectors are empty when building the `RedisLoggerConfig`. #[derive(Debug)] -pub struct RedisLoggerConfigBuilder { - __private: PhantomData<()>, -} +#[non_exhaustive] +pub struct RedisLoggerConfigBuilder {} impl RedisLoggerConfigBuilder { /// Constructs a `RedisLoggerConfig` with a given connection, channels, and a Pub/Sub encoder. diff --git a/src/lib_tests.rs b/src/lib_tests.rs index 81c893b..e2522ec 100644 --- a/src/lib_tests.rs +++ b/src/lib_tests.rs @@ -15,8 +15,8 @@ mock! { unsafe impl Send for RedisConnection {} } -const DUMMY_PUBSUB_ENCODER: DummyPubSubEncoder = DummyPubSubEncoder { __private: PhantomData }; -const DUMMY_STREAM_ENCODER: DummyStreamEncoder = DummyStreamEncoder { __private: PhantomData }; +const DUMMY_PUBSUB_ENCODER: DummyPubSubEncoder = DummyPubSubEncoder {}; +const DUMMY_STREAM_ENCODER: DummyStreamEncoder = DummyStreamEncoder {}; #[test] fn test_build_only_streams() {