From 9b5efc3d9a0381d194cbb57f6beee12c6e3fb5f9 Mon Sep 17 00:00:00 2001 From: Dirreke Date: Fri, 2 Feb 2024 17:25:09 +0800 Subject: [PATCH] feat!(trigger): add support for CompositeTrigger --- .github/workflows/main.yml | 4 +- Cargo.toml | 2 +- README.md | 4 +- benches/rotation.rs | 2 +- docs/Configuration.md | 22 ++-- examples/log_to_file_with_rolling.rs | 22 +++- examples/sample_config.yml | 64 +++++----- src/append/rolling_file/mod.rs | 50 ++++---- .../rolling_file/policy/compound/mod.rs | 44 ++++--- .../policy/compound/trigger/mod.rs | 2 +- .../policy/compound/trigger/time.rs | 118 +++++++++--------- 11 files changed, 188 insertions(+), 146 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 342c785e..b8219b46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - rust_versions: ["stable", "1.67"] + rust_versions: ["stable", "1.69"] os: [ubuntu-latest, windows-latest] steps: - name: Checkout the source code @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - rust_versions: ["stable", "1.67"] + rust_versions: ["stable", "1.69"] steps: - name: Checkout the source code uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 705f12b0..81e6453e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/estk/log4rs" readme = "README.md" keywords = ["log", "logger", "logging", "log4"] edition = "2018" -rust-version = "1.67" +rust-version = "1.69" [features] default = ["all_components", "config_parsing", "yaml_format"] diff --git a/README.md b/README.md index 641ea09d..056d1e4d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/log4rs.svg)](https://crates.io/crates/log4rs) [![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/clippy.svg)](#license) ![CI](https://github.com/estk/log4rs/workflows/CI/badge.svg) -[![Minimum rustc version](https://img.shields.io/badge/rustc-1.67+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) +[![Minimum rustc version](https://img.shields.io/badge/rustc-1.69+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) log4rs is a highly configurable logging framework modeled after Java's Logback and log4j libraries. @@ -71,7 +71,7 @@ fn main() { ## Rust Version Requirements -1.67 +1.69 ## Building for Dev diff --git a/benches/rotation.rs b/benches/rotation.rs index 4167331f..799174f2 100644 --- a/benches/rotation.rs +++ b/benches/rotation.rs @@ -75,7 +75,7 @@ fn mk_config(file_size: u64, file_count: u32) -> log4rs::config::Config { let roller = policy::compound::roll::fixed_window::FixedWindowRoller::builder() .build(&roll_pattern, file_count) .unwrap(); - let policy = policy::compound::CompoundPolicy::new(Box::new(trigger), Box::new(roller)); + let policy = policy::compound::CompoundPolicy::new(vec![Box::new(trigger)], Box::new(roller)); let file = RollingFileAppender::builder() .encoder(Box::new(PatternEncoder::new( "{d(%Y-%m-%d %H:%M:%S.%3f %Z)} {l} [{t} - {T}] {m}{n}", diff --git a/docs/Configuration.md b/docs/Configuration.md index 5ec2ce65..34abc37c 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -158,9 +158,13 @@ my_rolling_appender: path: "logs/test.log" policy: kind: compound - trigger: - kind: size - limit: 1mb + triggers: + - kind: size + limit: 1mb + - kind: time + interval: 1 day + modulate: false + max_random_delay: 0 roller: kind: fixed_window base: 1 @@ -171,7 +175,7 @@ my_rolling_appender: The new component is the _policy_ field. A policy must have the _kind_ field like most other components, the default (and only supported) policy is `kind: compound`. -The _trigger_ field is used to dictate when the log file should be rolled. It +The _triggers_ field is used to dictate when the log file should be rolled. It supports two types: `size`, and `time`. For `size`, it require a _limit_ field. The _limit_ field is a string which defines the maximum file size @@ -187,9 +191,9 @@ units in bytes, case does not matter: i.e. ```yml -trigger: - kind: size - limit: 10 mb +triggers: + - kind: size + limit: 10 mb ``` For `time`, it has three field, _interval_, _modulate_ and _max_random_delay_. @@ -226,8 +230,8 @@ time. i.e. ```yml -trigger: - kind: time +triggers: + - kind: time interval: 1 day modulate: false max_random_delay: 0 diff --git a/examples/log_to_file_with_rolling.rs b/examples/log_to_file_with_rolling.rs index 97e8217c..7d2e16d5 100644 --- a/examples/log_to_file_with_rolling.rs +++ b/examples/log_to_file_with_rolling.rs @@ -7,7 +7,8 @@ /// This is the size at which a new file should be created. For the demo it is /// set to 2KB which is very small and only for demo purposes const TRIGGER_FILE_SIZE: u64 = 2 * 1024; - +const TRIGGER_TIME_CONFIG: (TimeTriggerInterval, bool, u64) = + (TimeTriggerInterval::Day(1), false, 0); /// Delay between log messages for demo purposes const TIME_BETWEEN_LOG_MESSAGES: Duration = Duration::from_millis(10); @@ -35,7 +36,13 @@ use log4rs::{ append::{ console::{ConsoleAppender, Target}, rolling_file::policy::compound::{ - roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, CompoundPolicy, + roll::fixed_window::FixedWindowRoller, + trigger::{ + size::SizeTrigger, + time::{TimeTrigger, TimeTriggerInterval}, + Trigger, + }, + CompoundPolicy, }, }, config::{Appender, Config, Root}, @@ -50,12 +57,19 @@ fn main() -> Result<(), SetLoggerError> { let stderr = ConsoleAppender::builder().target(Target::Stderr).build(); // Create a policy to use with the file logging - let trigger = SizeTrigger::new(TRIGGER_FILE_SIZE); + let trigger_size = SizeTrigger::new(TRIGGER_FILE_SIZE); + let trigger_time = TimeTrigger::new( + TRIGGER_TIME_CONFIG.0, + TRIGGER_TIME_CONFIG.1, + TRIGGER_TIME_CONFIG.2, + ); + let trigger: Vec> = vec![Box::new(trigger_size), Box::new(trigger_time)]; + let roller = FixedWindowRoller::builder() .base(0) // Default Value (line not needed unless you want to change from 0 (only here for demo purposes) .build(ARCHIVE_PATTERN, LOG_FILE_COUNT) // Roll based on pattern and max 3 archive files .unwrap(); - let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); + let policy = CompoundPolicy::new(trigger, Box::new(roller)); // Logging to log file. (with rolling) let logfile = log4rs::append::rolling_file::RollingFileAppender::builder() diff --git a/examples/sample_config.yml b/examples/sample_config.yml index 84bb9133..65669c1e 100644 --- a/examples/sample_config.yml +++ b/examples/sample_config.yml @@ -1,33 +1,35 @@ appenders: - stdout: - kind: console - encoder: - pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" - filters: - - kind: threshold - level: info - file: - kind: file - path: "log/file.log" - encoder: - pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" - rollingfile: - kind: rolling_file - path: "log/rolling_file.log" - encoder: - pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" - policy: - trigger: - kind: time - interval: 1 minute - roller: - kind: fixed_window - pattern: "log/old-rolling_file-{}.log" - base: 0 - count: 2 + stdout: + kind: console + encoder: + pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" + filters: + - kind: threshold + level: info + file: + kind: file + path: "log/file.log" + encoder: + pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" + rollingfile: + kind: rolling_file + path: "log/rolling_file.log" + encoder: + pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" + policy: + triggers: + - kind: time + interval: 1 day + - kind: size + limit: 2 mb + roller: + kind: fixed_window + pattern: "log/old-rolling_file-{}.log" + base: 0 + count: 2 root: - level: info - appenders: - - stdout - - file - - rollingfile + level: info + appenders: + - stdout + - file + - rollingfile diff --git a/src/append/rolling_file/mod.rs b/src/append/rolling_file/mod.rs index 9e6d35ee..35f9ece5 100644 --- a/src/append/rolling_file/mod.rs +++ b/src/append/rolling_file/mod.rs @@ -328,9 +328,9 @@ impl RollingFileAppenderBuilder { /// /// # The remainder of the configuration is passed along to the policy's /// # deserializer, and will vary based on the kind of policy. -/// trigger: -/// kind: size -/// limit: 10 mb +/// triggers: +/// - kind: size +/// limit: 10 mb /// /// roller: /// kind: delete @@ -385,28 +385,28 @@ mod test { let config = format!( " appenders: - foo: - kind: rolling_file - path: {0}/foo.log - policy: - trigger: - kind: time - interval: 2 minutes - roller: - kind: delete - bar: - kind: rolling_file - path: {0}/foo.log - policy: - kind: compound - trigger: - kind: size - limit: 5 mb - roller: - kind: fixed_window - pattern: '{0}/foo.log.{{}}' - base: 1 - count: 5 + foo: + kind: rolling_file + path: {0}/foo.log + policy: + triggers: + - kind: time + interval: 2 minutes + roller: + kind: delete + bar: + kind: rolling_file + path: {0}/foo.log + policy: + kind: compound + triggers: + - kind: size + limit: 5 mb + roller: + kind: fixed_window + pattern: '{0}/foo.log.{{}}' + base: 1 + count: 5 ", dir.path().display() ); diff --git a/src/append/rolling_file/policy/compound/mod.rs b/src/append/rolling_file/policy/compound/mod.rs index 484af19c..8b0475f1 100644 --- a/src/append/rolling_file/policy/compound/mod.rs +++ b/src/append/rolling_file/policy/compound/mod.rs @@ -23,7 +23,7 @@ pub mod trigger; #[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct CompoundPolicyConfig { - trigger: Trigger, + triggers: Vec, roller: Roller, } @@ -81,27 +81,33 @@ impl<'de> serde::Deserialize<'de> for Roller { } } -/// A rolling policy which delegates to a "trigger" and "roller". +/// A rolling policy which delegates to a "triggers" and "roller". /// /// The trigger determines if the log file should roll, for example, by checking /// the size of the file. The roller processes the old log file, for example, /// by compressing it and moving it to a different location. #[derive(Debug)] pub struct CompoundPolicy { - trigger: Box, + triggers: Vec>, roller: Box, } impl CompoundPolicy { /// Creates a new `CompoundPolicy`. - pub fn new(trigger: Box, roller: Box) -> CompoundPolicy { - CompoundPolicy { trigger, roller } + pub fn new(triggers: Vec>, roller: Box) -> CompoundPolicy { + CompoundPolicy { triggers, roller } } } impl Policy for CompoundPolicy { fn process(&self, log: &mut LogFile) -> anyhow::Result<()> { - if self.trigger.trigger(log)? { + let mut is_trigger = false; + for trigger in &self.triggers { + if trigger.trigger(log)? { + is_trigger = true; + } + } + if is_trigger { log.roll(); self.roller.roll(log.path())?; } @@ -109,7 +115,12 @@ impl Policy for CompoundPolicy { } fn is_pre_process(&self) -> bool { - self.trigger.is_pre_process() + for trigger in &self.triggers { + if trigger.is_pre_process() { + return true; + } + } + false } } @@ -121,14 +132,14 @@ impl Policy for CompoundPolicy { /// kind: compound /// /// # The trigger, which determines when the log will roll over. Required. -/// trigger: +/// triggers: /// /// # Identifies which trigger is to be used. Required. -/// kind: size +/// - kind: size /// -/// # The remainder of the configuration is passed to the trigger's -/// # deserializer, and will vary based on the kind of trigger. -/// limit: 10 mb +/// # The remainder of the configuration is passed to the trigger's +/// # deserializer, and will vary based on the kind of trigger. +/// limit: 10 mb /// /// # The roller, which processes the old log file. Required. /// roller: @@ -154,8 +165,13 @@ impl Deserialize for CompoundPolicyDeserializer { config: CompoundPolicyConfig, deserializers: &Deserializers, ) -> anyhow::Result> { - let trigger = deserializers.deserialize(&config.trigger.kind, config.trigger.config)?; + let mut triggers = Vec::new(); + for config_trigger in &config.triggers { + let trigger = + deserializers.deserialize(&config_trigger.kind, config_trigger.config.clone())?; + triggers.push(trigger); + } let roller = deserializers.deserialize(&config.roller.kind, config.roller.config)?; - Ok(Box::new(CompoundPolicy::new(trigger, roller))) + Ok(Box::new(CompoundPolicy::new(triggers, roller))) } } diff --git a/src/append/rolling_file/policy/compound/trigger/mod.rs b/src/append/rolling_file/policy/compound/trigger/mod.rs index 3d61e0da..377164e0 100644 --- a/src/append/rolling_file/policy/compound/trigger/mod.rs +++ b/src/append/rolling_file/policy/compound/trigger/mod.rs @@ -26,6 +26,6 @@ pub trait Trigger: fmt::Debug + Send + Sync + 'static { #[cfg(feature = "config_parsing")] impl Deserializable for dyn Trigger { fn name() -> &'static str { - "trigger" + "triggers" } } diff --git a/src/append/rolling_file/policy/compound/trigger/time.rs b/src/append/rolling_file/policy/compound/trigger/time.rs index 4568a524..e555dfa1 100644 --- a/src/append/rolling_file/policy/compound/trigger/time.rs +++ b/src/append/rolling_file/policy/compound/trigger/time.rs @@ -12,37 +12,38 @@ use rand::Rng; use serde::de; #[cfg(feature = "config_parsing")] use std::fmt; -use std::sync::RwLock; +use std::sync::{Once, RwLock}; use crate::append::rolling_file::{policy::compound::trigger::Trigger, LogFile}; #[cfg(feature = "config_parsing")] use crate::config::{Deserialize, Deserializers}; +static INITIAL: Once = Once::new(); + #[cfg(feature = "config_parsing")] /// Configuration for the time trigger. #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct TimeTriggerConfig { - interval: TimeTriggerInterval, + /// The date/time interval between log file rolls. + pub interval: TimeTriggerInterval, + /// Whether to modulate the interval. #[serde(default)] - modulate: bool, + pub modulate: bool, + /// The maximum random delay in seconds. #[serde(default)] - max_random_delay: u64, -} - -#[cfg(not(feature = "config_parsing"))] -/// Configuration for the time trigger. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] -pub struct TimeTriggerConfig { - interval: TimeTriggerInterval, - modulate: bool, - max_random_delay: u64, + pub max_random_delay: u64, } /// A trigger which rolls the log once it has passed a certain time. #[derive(Debug)] pub struct TimeTrigger { - config: TimeTriggerConfig, + /// The date/time interval between log file rolls. + pub interval: TimeTriggerInterval, + /// Whether to modulate the interval. + pub modulate: bool, + /// The maximum random delay in seconds. + pub max_random_delay: u64, next_roll_time: RwLock>, } @@ -175,31 +176,16 @@ impl<'de> serde::Deserialize<'de> for TimeTriggerInterval { impl TimeTrigger { /// Returns a new trigger which rolls the log once it has passed the /// specified time. - pub fn new(config: TimeTriggerConfig) -> TimeTrigger { - #[cfg(test)] - let current = { - let now: std::time::Duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before Unix epoch"); - NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()) - .unwrap() - .and_local_timezone(Local) - .unwrap() - }; - - #[cfg(not(test))] - let current = Local::now(); - let next_time = TimeTrigger::get_next_time(current, config.interval, config.modulate); - let next_roll_time = if config.max_random_delay > 0 { - let random_delay = rand::thread_rng().gen_range(0..config.max_random_delay); - next_time + Duration::seconds(random_delay as i64) - } else { - next_time - }; - + pub fn new( + interval: TimeTriggerInterval, + modulate: bool, + max_random_delay: u64, + ) -> TimeTrigger { TimeTrigger { - config, - next_roll_time: RwLock::new(next_roll_time), + interval, + modulate, + max_random_delay, + next_roll_time: RwLock::default(), } } @@ -274,10 +260,37 @@ impl TimeTrigger { } panic!("Should not reach here!"); } + + fn refresh_time(&self) { + #[cfg(test)] + let current = { + let now: std::time::Duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before Unix epoch"); + NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()) + .unwrap() + .and_local_timezone(Local) + .unwrap() + }; + + #[cfg(not(test))] + let current = Local::now(); + let next_time = TimeTrigger::get_next_time(current, self.interval, self.modulate); + let next_roll_time = if self.max_random_delay > 0 { + let random_delay = rand::thread_rng().gen_range(0..self.max_random_delay); + next_time + Duration::seconds(random_delay as i64) + } else { + next_time + }; + *self.next_roll_time.write().unwrap() = next_roll_time; + } } impl Trigger for TimeTrigger { fn trigger(&self, _file: &LogFile) -> anyhow::Result { + INITIAL.call_once(|| { + self.refresh_time(); + }); #[cfg(test)] let current = { let now = SystemTime::now() @@ -291,12 +304,11 @@ impl Trigger for TimeTrigger { #[cfg(not(test))] let current: DateTime = Local::now(); - let mut next_roll_time = self.next_roll_time.write().unwrap(); + let next_roll_time = self.next_roll_time.read().unwrap(); let is_trigger = current >= *next_roll_time; + drop(next_roll_time); if is_trigger { - let tmp = TimeTrigger::new(self.config); - let time_new = tmp.next_roll_time.read().unwrap(); - *next_roll_time = *time_new; + self.refresh_time(); } Ok(is_trigger) } @@ -333,7 +345,11 @@ impl Deserialize for TimeTriggerDeserializer { config: TimeTriggerConfig, _: &Deserializers, ) -> anyhow::Result> { - Ok(Box::new(TimeTrigger::new(config))) + Ok(Box::new(TimeTrigger::new( + config.interval, + config.modulate, + config.max_random_delay, + ))) } } @@ -355,13 +371,8 @@ mod test { len: 0, }; - let config = TimeTriggerConfig { - interval, - modulate, - max_random_delay: 0, - }; - - let trigger = TimeTrigger::new(config); + let trigger = TimeTrigger::new(interval, modulate, 0); + trigger.trigger(&logfile).unwrap(); MockClock::advance_system_time(Duration::from_millis(millis / 2)); let result1 = trigger.trigger(&logfile).unwrap(); @@ -485,12 +496,7 @@ mod test { #[test] fn pre_process() { - let config = TimeTriggerConfig { - interval: TimeTriggerInterval::Minute(2), - modulate: true, - max_random_delay: 0, - }; - let trigger = TimeTrigger::new(config); + let trigger = TimeTrigger::new(TimeTriggerInterval::Minute(2), true, 0); assert!(trigger.is_pre_process()); } }