Skip to content

Commit

Permalink
feat(triger): Add "time" triger
Browse files Browse the repository at this point in the history
  • Loading branch information
Dirreke committed Nov 28, 2023
1 parent 8f78cc4 commit 1e256bc
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 6 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ edition = "2018"
rust = "1.60"

[features]
default = ["all_components", "config_parsing", "yaml_format"]
default = ["all_components", "config_parsing", "yaml_format", "time_trigger"]

config_parsing = ["humantime", "serde", "serde-value", "typemap-ors", "log/serde"]
yaml_format = ["serde_yaml"]
Expand All @@ -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 = []
Expand All @@ -41,6 +42,7 @@ all_components = [
"delete_roller",
"fixed_window_roller",
"size_trigger",
"time_trigger",
"json_encoder",
"pattern_encoder",
"threshold_filter"
Expand Down
21 changes: 21 additions & 0 deletions examples/sample_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,28 @@ appenders:
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
- file
- rollingfile
17 changes: 12 additions & 5 deletions src/append/rolling_file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,8 @@ impl Append for RollingFileAppender {
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 writer_file = self.get_writer(&mut writer)?;
writer_file.len
};

let mut file = LogFile {
Expand All @@ -182,7 +180,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) {}
Expand Down
3 changes: 3 additions & 0 deletions src/append/rolling_file/policy/compound/trigger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
277 changes: 277 additions & 0 deletions src/append/rolling_file/policy/compound/trigger/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
//! The time trigger.
//!
//! Requires the `time_trigger` feature.

use chrono::{TimeZone, DateTime, Local, Duration, Timelike, Datelike};
#[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:Limit,
}

#[cfg(feature = "config_parsing")]
fn deserialize_limit<'de, D>(d: D) -> Result<Limit, D::Error>
where
D: de::Deserializer<'de>,
{
struct V;

impl<'de2> de::Visitor<'de2> for V {
type Value = Limit;

fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str("a time")
}

fn visit_u64<E>(self, v: u64) -> Result<Limit, E>
where
E: de::Error,
{
Ok(Limit::Second(v))
}

fn visit_i64<E>(self, v: i64) -> Result<Limit, E>
where
E: de::Error,
{
if v < 0 {
return Err(E::invalid_value(
de::Unexpected::Signed(v),
&"a non-negative number",
));
}

Ok(Limit::Second(v as u64))
}

fn visit_str<E>(self, v: &str) -> Result<Limit, E>
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::<u64>() {
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(Limit::Second(number)),
};

let result = if unit.eq_ignore_ascii_case("second") || unit.eq_ignore_ascii_case("seconds") {
Some(Limit::Second(number))
} else if unit.eq_ignore_ascii_case("minute") || unit.eq_ignore_ascii_case("minutes") {
Some(Limit::Minute(number))
} else if unit.eq_ignore_ascii_case("hour") || unit.eq_ignore_ascii_case("hours") {
Some(Limit::Hour(number))
} else if unit.eq_ignore_ascii_case("day") || unit.eq_ignore_ascii_case("days") {
Some(Limit::Day(number))
} else if unit.eq_ignore_ascii_case("week") || unit.eq_ignore_ascii_case("weeks") {
Some(Limit::Week(number))
} else if unit.eq_ignore_ascii_case("month") || unit.eq_ignore_ascii_case("month") {
Some(Limit::Month(number))
} else if unit.eq_ignore_ascii_case("year") || unit.eq_ignore_ascii_case("years") {
Some(Limit::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(crate) struct TimeTrigger {
limit: Limit,
time_start: RwLock<DateTime<Local>>,
}

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) enum Limit {
Second(u64),
Minute(u64),
Hour(u64),
Day(u64),
Week(u64),
Month(u64),
Year(u64),
}

impl Default for Limit {
fn default() -> Self {
Limit::Second(1)
}
}

impl TimeTrigger {
/// Returns a new trigger which rolls the log once it has passed the
/// specified time.
pub(crate) fn new(limit: Limit) -> 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 sec = time.second();

let time_new = match limit {
Limit::Second(_) => {
Local.with_ymd_and_hms(year, month, day, hour, min, sec).unwrap()
}
Limit::Minute(_) => {
Local.with_ymd_and_hms(year, month, day, hour, min, 0).unwrap()
}
Limit::Hour(_) => {
Local.with_ymd_and_hms(year, month, day, hour, 0, 0).unwrap()
}
Limit::Day(_) => {
Local.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap()
}
Limit::Week(_) => {
Local.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap() - Duration::days(weekday.num_days_from_monday() as i64)
}
Limit::Month(_) => {
Local.with_ymd_and_hms(year, month, 1, 0, 0, 0).unwrap()
}
Limit::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<bool> {
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 {
Limit::Second(num) => {
duration.num_seconds() as u64 >= num
}
Limit::Minute(num) => {
duration.num_minutes() as u64 >= num
}
Limit::Hour(num) => {
duration.num_hours() as u64 >= num
}
Limit::Day(num) => {
duration.num_days() as u64 >= num
}
Limit::Week(num) => {
duration.num_weeks() as u64 >= num
}
Limit::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
}
Limit::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<Box<dyn Trigger>> {
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(Limit::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);
}

}
8 changes: 8 additions & 0 deletions src/config/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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()
}
Expand Down

0 comments on commit 1e256bc

Please sign in to comment.