Skip to content

Commit

Permalink
feat: automatic cleanup functionality for logs file
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanodecillis committed May 17, 2024
1 parent 2ec58ef commit ae6a2ab
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 24 deletions.
15 changes: 13 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
[package]
name = "speak-easy"
version = "0.1.0"
authors = ["Stefano De Cillis"]
version = "0.1.1"
authors = ["stefanodecillis"]
edition = "2021"
description = "Logging functionalities with different levels and rotation options built on top of tokio-rs tracing."
license = "MIT"
readme = "README.md"
repository = "https://github.com/stefanodecillis/speak-easy"
categories = [
"development-tools::debugging",
"development-tools::profiling",
"asynchronous",
"logging",
"no-std",
]
keywords = ["logging", "tracing", "async"]


[dependencies]
tokio = { version = "1.37.0", features = ["time", "rt"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.18"
tracing-appender = "0.2"
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
![Speak Easy — Logging functionalities][splash]

[splash]: https://raw.githubusercontent.com/stefanodecillis/speak-easy/main/assets/crab-contained.jpg

# Speak-Easy

Speak-Easy is a Rust library that provides logging functionalities with different levels and rotation options built on top of tokio-rs tracing.

## Features

- Different log levels
- Log rotation options
- Cleanup functionality

## Usage

First, add the following to your `Cargo.toml`:

```toml
[dependencies]
speak-easy = "0.1.0"
```

Then, use the library in your code like this:


```rust
use speak_easy::{SpeakEasy, SpeakConfig, Rotation};

let speak_config = SpeakConfig::new(Rotation::Minutely, "./logs".to_string(), "my_log".to_string())
.with_cleanup(24 * 60 * 60, 5);

SpeakEasy::init(Level::INFO, Some(config));

```

Please replace "/path/to/log/files" with the actual path where you want to store your log files.

## License
This project is licensed under the MIT License - see the LICENSE file for details.



Binary file added assets/crab-contained.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 91 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,20 @@
//!
//! Then add this to your crate root:
//!
//! ```rust
//! use speak_easy::{debug, error, info, trace, warn};
//! use speak_easy::{speak_easy::SpeakEasy, Level, Rotation, SpeakConfig};
//! ```
//!
//! You can then use the logging functionality as follows:
//!
//! ```rust
//! use speak-easy::{debug, error, info, trace, warn};
//! use speak_easy::{speak_easy::SpeakEasy, Level, Rotation, SpeakConfig};
//!
//! let config = SpeakConfig {
//! interval: Rotation::Hourly,
//! directory_path: "./logs".to_string(),
//! prefix: "my_log".to_string(),
//! };
//! # use speak_easy::{debug, error, info, trace, warn};
//! # use speak_easy::{speak_easy::SpeakEasy, Level, Rotation, SpeakConfig};
//! let speak_config = SpeakConfig::new(Rotation::Minutely, "./logs".to_string(), "my_log".to_string());
//! SpeakEasy::init(
//! Level::INFO,
//! Some(SpeakConfig {
//! interval: ::speak_easy::log::Rotation::Minutely,
//! directory_path: "./logs".to_string(),
//! prefix: "indid".to_string(),
//! }),
//! Some(speak_config),
//! );
//!
//! info!("This is an info log");
Expand All @@ -66,21 +61,49 @@
//! ```
//!
//! This will create logs with different levels and rotate them hourly.
//!
//!
//! ### With Cleanup
//!
//! You can also set up log cleanup with the `with_cleanup` method:
//!
//! ```rust
//! # use speak_easy::{debug, error, info, trace, warn};
//! # use speak_easy::{speak_easy::SpeakEasy, Level, Rotation, SpeakConfig};
//! let speak_config = SpeakConfig::new(Rotation::Minutely, "./logs".to_string(), "my_log".to_string())
//! .with_cleanup(24 * 60 * 60, 5);
//! SpeakEasy::init(
//! Level::INFO,
//! Some(speak_config),
//! );
//! ```
//!
//! This will create logs with different levels, rotate them minutely, and clean up the logs every 24 hours, keeping the last 5 logs.
//!
//! ## License
//!
//! This project is licensed under the MIT license.

mod formatter;
pub use tracing::{debug, error, info, trace, warn, Level};
pub mod speak_easy;

#[derive(Clone, Debug)]
pub enum Rotation {
Minutely,
Hourly,
Daily,
Never,
}

#[derive(Clone, Debug)]
pub struct SpeakConfig {
pub interval: Rotation,
pub directory_path: String,
pub prefix: String,
pub cleanup: bool,
pub cleanup_interval: u64,
pub keep_last: usize,
}

impl Default for SpeakConfig {
Expand All @@ -89,6 +112,61 @@ impl Default for SpeakConfig {
interval: Rotation::Never,
directory_path: "./logs".to_string(),
prefix: "log".to_string(),
cleanup: false,
cleanup_interval: 24 * 60 * 60, // 24 hours
keep_last: 5,
}
}
}

impl SpeakConfig {
/// Creates a new `SpeakConfig` with the specified rotation interval, directory path, and log file prefix.
///
/// # Arguments
///
/// * `interval` - The rotation interval for the logs. Options are `Minutely`, `Hourly`, `Daily`, and `Never`.
/// * `directory_path` - The directory path where the log files will be stored.
/// * `prefix` - The prefix for the log files.
///
/// # Example
///
/// ```rust
/// use speak_easy::{Rotation, SpeakConfig};
///
/// let speak_config = SpeakConfig::new(Rotation::Minutely, "./logs".to_string(), "my_log".to_string());
/// ```
pub fn new(interval: Rotation, directory_path: String, prefix: String) -> Self {
Self {
interval,
directory_path,
prefix,
cleanup: SpeakConfig::default().cleanup,
cleanup_interval: SpeakConfig::default().cleanup_interval,
keep_last: SpeakConfig::default().keep_last,
}
}

/// Sets the cleanup options for the log files.
///
/// # Arguments
///
/// * `cleanup_interval` - The interval (in seconds) at which the log files should be cleaned up.
/// * `keep_last` - The number of log files to keep.
///
/// # Example
///
/// ```rust
/// use speak_easy::{Rotation, SpeakConfig};
///
/// let speak_config = SpeakConfig::new(Rotation::Minutely, "./logs".to_string(), "my_log".to_string())
/// .with_cleanup(24 * 60 * 60, 5);
/// ```
pub fn with_cleanup(mut self, cleanup_interval: u64, keep_last: usize) -> Self {
self.cleanup = true;
self.cleanup_interval = cleanup_interval;
self.keep_last = keep_last;
self
}
}

impl SpeakConfig {}
65 changes: 56 additions & 9 deletions src/speak_easy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
//! ## Methods
//!
//! - `init`: This method initializes the SpeakEasy logging system. It takes a log level and an optional `SpeakConfig` struct. The `SpeakConfig` struct defines the rotation interval, the directory path for the logs, and the prefix for the log files.
//! - `keep_last_logs`: This method keeps the last `n` logs in the log directory and deletes the rest. It is used for log rotation.
//!

use crate::{
formatter::LogsFileFormatter,
Expand All @@ -22,6 +24,11 @@ use tracing_subscriber::{
reload,
};

use std::fs;
use std::io;
use std::sync::Arc;
use tokio::time::{sleep, Duration};

pub struct SpeakEasy {}

impl SpeakEasy {
Expand All @@ -31,22 +38,22 @@ impl SpeakEasy {

match speak_easy_config {
Some(speak_easy_config) => {
let logfile = match speak_easy_config.interval {
let logfile = match &speak_easy_config.interval {
Rotation::Minutely => tracing_appender::rolling::minutely(
speak_easy_config.directory_path,
speak_easy_config.prefix,
&speak_easy_config.directory_path,
&speak_easy_config.prefix,
),
Rotation::Hourly => tracing_appender::rolling::hourly(
speak_easy_config.directory_path,
speak_easy_config.prefix,
&speak_easy_config.directory_path,
&speak_easy_config.prefix,
),
Rotation::Daily => tracing_appender::rolling::daily(
speak_easy_config.directory_path,
speak_easy_config.prefix,
&speak_easy_config.directory_path,
&speak_easy_config.prefix,
),
Rotation::Never => tracing_appender::rolling::never(
speak_easy_config.directory_path,
speak_easy_config.prefix,
&speak_easy_config.directory_path,
&speak_easy_config.prefix,
),
};

Expand All @@ -64,6 +71,20 @@ impl SpeakEasy {
if tracing::subscriber::set_global_default(with_file_subscriber).is_err() {
tracing::warn!("A global default tracing subscriber has already been set.");
}

if speak_easy_config.clone().cleanup {
let directory_path = Arc::new(speak_easy_config.directory_path.clone());
let prefix = Arc::new(speak_easy_config.prefix.clone());
let cleanup_interval = Arc::new(speak_easy_config.cleanup_interval);
let holds_num = Arc::new(speak_easy_config.keep_last);

tokio::spawn(async move {
loop {
let _ = SpeakEasy::keep_last_logs(&directory_path, &prefix, &holds_num);
sleep(Duration::from_secs(*cleanup_interval)).await;
}
});
}
}
None => {
if tracing::subscriber::set_global_default(base_subscriber).is_err() {
Expand All @@ -72,4 +93,30 @@ impl SpeakEasy {
}
};
}

fn keep_last_logs(directory_path: &str, prefix: &str, holds_num: &usize) -> io::Result<()> {
// Read the directory
let mut entries = fs::read_dir(directory_path)?
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, io::Error>>()?;

// Filter the entries to only include files that start with the prefix
entries.retain(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.starts_with(prefix))
.unwrap_or(false)
});

// Sort the entries by modified date in descending order
entries.sort_by_key(|path| fs::metadata(path).and_then(|meta| meta.modified()).unwrap());
entries.reverse();

// Remove all but the last five entries
for path in entries.into_iter().skip(*holds_num) {
fs::remove_file(path)?;
}

Ok(())
}
}

0 comments on commit ae6a2ab

Please sign in to comment.