From 5b80116af67b4d21db2253a0440895da41ca1903 Mon Sep 17 00:00:00 2001 From: Dirreke Date: Sat, 2 Dec 2023 21:57:47 +0800 Subject: [PATCH] feat(triger): Add "time" triger --- Cargo.toml | 2 + docs/Configuration.md | 36 ++- examples/sample_config.yml | 41 ++- src/append/rolling_file/mod.rs | 19 +- .../policy/compound/trigger/mod.rs | 3 + .../policy/compound/trigger/time.rs | 270 ++++++++++++++++++ src/config/raw.rs | 8 + src/lib.rs | 1 + 8 files changed, 359 insertions(+), 21 deletions(-) create mode 100644 src/append/rolling_file/policy/compound/trigger/time.rs diff --git a/Cargo.toml b/Cargo.toml index 629999aa..7c81b4d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ compound_policy = [] delete_roller = [] fixed_window_roller = [] size_trigger = [] +time_trigger = [] json_encoder = ["serde", "serde_json", "chrono", "log-mdc", "log/serde", "thread-id"] pattern_encoder = ["chrono", "log-mdc", "thread-id"] ansi_writer = [] @@ -41,6 +42,7 @@ all_components = [ "delete_roller", "fixed_window_roller", "size_trigger", + "time_trigger", "json_encoder", "pattern_encoder", "threshold_filter" diff --git a/docs/Configuration.md b/docs/Configuration.md index d4120e5a..4e47e7ab 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -171,10 +171,12 @@ my_rolling_appender: The new component is the _policy_ field. A policy must have `kind` 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. The -only supported trigger is `kind: size`. There is a required field `limit` -which defines the maximum file size prior to a rolling of the file. The limit -field requires one of the following units in bytes, case does not matter: +The _trigger_ field is used to dictate when the log file should be rolled. It +supports two types: `size`, and `time`. They both require a `limit` field. + +For `size`, the `limit` field is a string which defines the maximum file size +prior to a rolling of the file. The limit field requires one of the following +units in bytes, case does not matter: - b - kb/kib @@ -190,6 +192,32 @@ trigger: limit: 10 mb ``` +For `time`, the `limit` field is a string which defines the time to roll the +file. The limit field supports the following units(second will be used if the +unit is not specified), case does not matter: + +- second[s] +- minute[s] +- hour[s] +- day[s] +- week[s] +- month[s] +- year[s] + +> note: The log file will be rolled at the integer time. For example, if the +`limit` is set to `2 day`, the log file will be rolled at 0:00 every other a +day, regardless of the time `log4rs` was started or the log file was created. +This means that the initial log file will be likely rolled before the limit +is reached. + +i.e. + +```yml +trigger: + kind: time + limit: 7 day +``` + The _roller_ field supports two types: delete, and fixed_window. The delete roller does not take any other configuration fields. The fixed_window roller supports three fields: pattern, base, and count. The most current log file will diff --git a/examples/sample_config.yml b/examples/sample_config.yml index 4a0d69cd..65d96e96 100644 --- a/examples/sample_config.yml +++ b/examples/sample_config.yml @@ -1,12 +1,33 @@ appenders: - stdout: - kind: console - encoder: - pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" - filters: - - kind: threshold - level: info + stdout: + kind: console + encoder: + pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" + filters: + - kind: threshold + level: info + file: + kind: file + path: "log/log.log" + encoder: + pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" + rollingfile: + kind: rolling_file + path: "log/log2.log" + encoder: + pattern: "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}" + policy: + trigger: + kind: time + limit: 1 minute + roller: + kind: fixed_window + pattern: "log/old-log-{}.log" + base: 0 + count: 2 root: - level: info - appenders: - - stdout + level: info + appenders: + - stdout + - file + - rollingfile diff --git a/src/append/rolling_file/mod.rs b/src/append/rolling_file/mod.rs index e14e4ad8..9b9355dd 100644 --- a/src/append/rolling_file/mod.rs +++ b/src/append/rolling_file/mod.rs @@ -167,12 +167,8 @@ impl Append for RollingFileAppender { // TODO(eas): Perhaps this is better as a concurrent queue? let mut writer = self.writer.lock(); - let len = { - let writer = self.get_writer(&mut writer)?; - self.encoder.encode(writer, record)?; - writer.flush()?; - writer.len - }; + let log_writer = self.get_writer(&mut writer)?; + let len = log_writer.len; let mut file = LogFile { writer: &mut writer, @@ -182,7 +178,16 @@ impl Append for RollingFileAppender { // TODO(eas): Idea: make this optionally return a future, and if so, we initialize a queue for // data that comes in while we are processing the file rotation. - self.policy.process(&mut file) + + //first, rotate + self.policy.process(&mut file)?; + + //second, write + let writer_file = self.get_writer(&mut writer)?; + self.encoder.encode(writer_file, record)?; + writer_file.flush()?; + + Ok(()) } fn flush(&self) {} diff --git a/src/append/rolling_file/policy/compound/trigger/mod.rs b/src/append/rolling_file/policy/compound/trigger/mod.rs index 76e67e74..5de7570f 100644 --- a/src/append/rolling_file/policy/compound/trigger/mod.rs +++ b/src/append/rolling_file/policy/compound/trigger/mod.rs @@ -9,6 +9,9 @@ use crate::config::Deserializable; #[cfg(feature = "size_trigger")] pub mod size; +#[cfg(feature = "time_trigger")] +pub mod time; + /// A trait which identifies if the active log file should be rolled over. pub trait Trigger: fmt::Debug + Send + Sync + 'static { /// Determines if the active log file should be rolled over. diff --git a/src/append/rolling_file/policy/compound/trigger/time.rs b/src/append/rolling_file/policy/compound/trigger/time.rs new file mode 100644 index 00000000..ac3a6b24 --- /dev/null +++ b/src/append/rolling_file/policy/compound/trigger/time.rs @@ -0,0 +1,270 @@ +//! The time trigger. +//! +//! Requires the `time_trigger` feature. + +use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike}; +#[cfg(feature = "config_parsing")] +use serde::de; +#[cfg(feature = "config_parsing")] +use std::fmt; +use std::sync::RwLock; + +use crate::append::rolling_file::{policy::compound::trigger::Trigger, LogFile}; + +#[cfg(feature = "config_parsing")] +use crate::config::{Deserialize, Deserializers}; + +/// Configuration for the time trigger. +#[cfg(feature = "config_parsing")] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct TimeTriggerConfig { + #[serde(deserialize_with = "deserialize_limit")] + limit: TimeTriggerLimit, +} + +#[cfg(feature = "config_parsing")] +fn deserialize_limit<'de, D>(d: D) -> Result +where + D: de::Deserializer<'de>, +{ + struct V; + + impl<'de2> de::Visitor<'de2> for V { + type Value = TimeTriggerLimit; + + fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str("a time") + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(TimeTriggerLimit::Second(v)) + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + if v < 0 { + return Err(E::invalid_value( + de::Unexpected::Signed(v), + &"a non-negative number", + )); + } + + Ok(TimeTriggerLimit::Second(v as u64)) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let (number, unit) = match v.find(|c: char| !c.is_ascii_digit()) { + Some(n) => (v[..n].trim(), Some(v[n..].trim())), + None => (v.trim(), None), + }; + + let number = match number.parse::() { + Ok(n) => n, + Err(_) => return Err(E::invalid_value(de::Unexpected::Str(number), &"a number")), + }; + + let unit = match unit { + Some(u) => u, + None => return Ok(TimeTriggerLimit::Second(number)), + }; + + let result = if unit.eq_ignore_ascii_case("second") + || unit.eq_ignore_ascii_case("seconds") + { + Some(TimeTriggerLimit::Second(number)) + } else if unit.eq_ignore_ascii_case("minute") || unit.eq_ignore_ascii_case("minutes") { + Some(TimeTriggerLimit::Minute(number)) + } else if unit.eq_ignore_ascii_case("hour") || unit.eq_ignore_ascii_case("hours") { + Some(TimeTriggerLimit::Hour(number)) + } else if unit.eq_ignore_ascii_case("day") || unit.eq_ignore_ascii_case("days") { + Some(TimeTriggerLimit::Day(number)) + } else if unit.eq_ignore_ascii_case("week") || unit.eq_ignore_ascii_case("weeks") { + Some(TimeTriggerLimit::Week(number)) + } else if unit.eq_ignore_ascii_case("month") || unit.eq_ignore_ascii_case("months") { + Some(TimeTriggerLimit::Month(number)) + } else if unit.eq_ignore_ascii_case("year") || unit.eq_ignore_ascii_case("years") { + Some(TimeTriggerLimit::Year(number)) + } else { + return Err(E::invalid_value(de::Unexpected::Str(unit), &"a valid unit")); + }; + + match result { + Some(n) => Ok(n), + None => Err(E::invalid_value(de::Unexpected::Str(v), &"a time")), + } + } + } + + d.deserialize_any(V) +} + +/// A trigger which rolls the log once it has passed a certain time. +#[derive(Debug)] +pub struct TimeTrigger { + limit: TimeTriggerLimit, + time_start: RwLock>, +} + +/// The TimeTriger have the following units are supported (case insensitive): +/// "second", "seconds", "minute", "minutes", "hour", "hours", "day", "days", "week", "weeks", "month", "months", "year", "years". The unit defaults to +/// week if not specified. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum TimeTriggerLimit { + /// TimeTriger in second(s). + Second(u64), + /// TimeTriger in minute(s). + Minute(u64), + /// TimeTriger in hour(s). + Hour(u64), + /// TimeTriger in day(s). + Day(u64), + /// TimeTriger in week(s). + Week(u64), + /// TimeTriger in month(s). + Month(u64), + /// TimeTriger in year(s). + Year(u64), +} + +impl Default for TimeTriggerLimit { + fn default() -> Self { + TimeTriggerLimit::Week(1) + } +} + +impl TimeTrigger { + /// Returns a new trigger which rolls the log once it has passed the + /// specified time. + pub fn new(limit: TimeTriggerLimit) -> TimeTrigger { + let time = Local::now(); + let year = time.year(); + let month = time.month(); + let day = time.day(); + let weekday = time.weekday(); + let hour = time.hour(); + let min = time.minute(); + + let time_new = match limit { + TimeTriggerLimit::Second(_) => time, + TimeTriggerLimit::Minute(_) => Local + .with_ymd_and_hms(year, month, day, hour, min, 0) + .unwrap(), + TimeTriggerLimit::Hour(_) => Local + .with_ymd_and_hms(year, month, day, hour, 0, 0) + .unwrap(), + TimeTriggerLimit::Day(_) => Local.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap(), + TimeTriggerLimit::Week(_) => { + Local.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap() + - Duration::days(weekday.num_days_from_monday() as i64) + } + TimeTriggerLimit::Month(_) => Local.with_ymd_and_hms(year, month, 1, 0, 0, 0).unwrap(), + TimeTriggerLimit::Year(_) => Local.with_ymd_and_hms(year, 1, 1, 0, 0, 0).unwrap(), + }; + + TimeTrigger { + limit, + time_start: RwLock::new(time_new), + } + } +} + +impl Trigger for TimeTrigger { + fn trigger(&self, _file: &LogFile) -> anyhow::Result { + let time_now = Local::now(); + let mut time_start = self.time_start.write().unwrap(); + let duration = time_now.signed_duration_since(*time_start); + let is_triger = match self.limit { + TimeTriggerLimit::Second(num) => duration.num_seconds() as u64 >= num, + TimeTriggerLimit::Minute(num) => duration.num_minutes() as u64 >= num, + TimeTriggerLimit::Hour(num) => duration.num_hours() as u64 >= num, + TimeTriggerLimit::Day(num) => duration.num_days() as u64 >= num, + TimeTriggerLimit::Week(num) => duration.num_weeks() as u64 >= num, + TimeTriggerLimit::Month(num) => { + let num_years_start = time_start.year() as u64; + let num_months_start = num_years_start * 12 + time_start.month() as u64; + let num_years_now = time_now.year() as u64; + let num_months_now = num_years_now * 12 + time_now.month() as u64; + + num_months_now - num_months_start >= num + } + TimeTriggerLimit::Year(num) => { + let num_years_start = time_start.year() as u64; + let num_years_now = time_now.year() as u64; + + num_years_now - num_years_start >= num + } + }; + if is_triger { + let tmp = TimeTrigger::new(self.limit); + let time_new = tmp.time_start.read().unwrap(); + *time_start = *time_new; + } + Ok(is_triger) + } +} + +/// A deserializer for the `TimeTrigger`. +/// +/// # Configuration +/// +/// ```yaml +/// kind: time +/// +/// # The time limit in second. The following units are supported (case insensitive): +/// # "second", "seconds", "minute", "minutes", "hour", "hours", "day", "days", "week", "weeks", "month", "months", "year", "years". The unit defaults to +/// # second if not specified. Required. +/// limit: 7 day +/// ``` +#[cfg(feature = "config_parsing")] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] +pub(crate) struct TimeTriggerDeserializer; + +#[cfg(feature = "config_parsing")] +impl Deserialize for TimeTriggerDeserializer { + type Trait = dyn Trigger; + + type Config = TimeTriggerConfig; + + fn deserialize( + &self, + config: TimeTriggerConfig, + _: &Deserializers, + ) -> anyhow::Result> { + Ok(Box::new(TimeTrigger::new(config.limit))) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn trigger() { + let file = tempfile::tempdir().unwrap(); + let logfile = LogFile { + writer: &mut None, + path: file.path(), + len: 0, + }; + let trigger = TimeTrigger::new(TimeTriggerLimit::Second(10)); + let result = trigger.trigger(&logfile).unwrap(); + assert_eq!(false, result); + std::thread::sleep(std::time::Duration::from_secs(12)); + let result = trigger.trigger(&logfile).unwrap(); + assert_eq!(true, result); + let result = trigger.trigger(&logfile).unwrap(); + assert_eq!(false, result); + std::thread::sleep(std::time::Duration::from_secs(12)); + let result = trigger.trigger(&logfile).unwrap(); + assert_eq!(true, result); + } +} diff --git a/src/config/raw.rs b/src/config/raw.rs index 38645888..d35920b9 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -216,6 +216,12 @@ impl Default for Deserializers { append::rolling_file::policy::compound::trigger::size::SizeTriggerDeserializer, ); + #[cfg(feature = "time_trigger")] + d.insert( + "time", + append::rolling_file::policy::compound::trigger::time::TimeTriggerDeserializer, + ); + #[cfg(feature = "json_encoder")] d.insert("json", encode::json::JsonEncoderDeserializer); @@ -260,6 +266,8 @@ impl Deserializers { /// * Triggers /// * "size" -> `SizeTriggerDeserializer` /// * Requires the `size_trigger` feature. + /// * "time" -> `TimeTriggerDeserializer` + /// * Requires the `time_trigger` feature. pub fn new() -> Deserializers { Deserializers::default() } diff --git a/src/lib.rs b/src/lib.rs index ecd7e354..5c7bf278 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ //! - [fixed_window](append/rolling_file/policy/compound/roll/fixed_window/struct.FixedWindowRollerDeserializer.html#configuration): requires the `fixed_window_roller` feature //! - Triggers //! - [size](append/rolling_file/policy/compound/trigger/size/struct.SizeTriggerDeserializer.html#configuration): requires the `size_trigger` feature +//! - [time](append/rolling_file/policy/compound/trigger/tine/struct.TimeTriggerDeserializer.html#configuration): requires the `time_trigger` feature //! //! ## Encoders //!