Skip to content

Commit

Permalink
Added more examples in the documentation, changed signature of encode…
Browse files Browse the repository at this point in the history
…r traits.
  • Loading branch information
8192K committed Feb 19, 2024
1 parent 6e0b01b commit 7b9bf99
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand All @@ -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<u8> {
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
Expand Down
50 changes: 23 additions & 27 deletions src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
}

Expand All @@ -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<u8>)> {
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<u8>)> {
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()
}
}

Expand Down Expand Up @@ -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);
Expand Down
61 changes: 49 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
//! 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;
Expand All @@ -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<u8>)>;
fn encode(&self, record: &Record) -> Vec<(String, Vec<u8>)>;
}

/// 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 {
Expand All @@ -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<u8>)> {
fn encode(&self, _record: &Record) -> Vec<(String, Vec<u8>)> {
panic!()
}
}
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/lib_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 7b9bf99

Please sign in to comment.