#![allow(dead_code)]
use chrono::{DateTime, Local, TimeZone, Utc};
pub struct AdaptiveTsParser {
formats: Vec<String>,
}
impl AdaptiveTsParser {
pub fn new() -> Self {
Self {
formats: get_initial_timestamp_formats(),
}
}
pub fn parse_ts(&mut self, ts_str: &str) -> Option<DateTime<Utc>> {
self.parse_ts_with_config(ts_str, None, None)
}
pub fn parse_ts_with_config(
&mut self,
ts_str: &str,
custom_format: Option<&str>,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
let ts_str = ts_str.trim();
if let Some(format) = custom_format {
if let Some(parsed) = try_parse_with_format(ts_str, format, default_timezone) {
return Some(parsed);
}
}
match ts_str {
"now" => return Some(Utc::now()),
"today" => {
let local_today = Local::now().date_naive();
if let Some(naive_datetime) = local_today.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
"yesterday" => {
let local_yesterday = Local::now().date_naive() - chrono::Duration::days(1);
if let Some(naive_datetime) = local_yesterday.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
"tomorrow" => {
let local_tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
if let Some(naive_datetime) = local_tomorrow.and_hms_opt(0, 0, 0) {
return Some(naive_datetime.and_utc());
}
}
_ => {}
}
if ts_str.starts_with('-') || ts_str.starts_with('+') || looks_like_relative_time(ts_str) {
if let Ok(dt) = parse_relative_time(ts_str) {
return Some(dt);
}
}
if ts_str.chars().all(|c| c.is_ascii_digit()) {
if let Some(parsed) = try_parse_unix_timestamp(ts_str) {
return Some(parsed);
}
}
if let Ok(dt) = DateTime::parse_from_rfc3339(ts_str) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = DateTime::parse_from_rfc2822(ts_str) {
return Some(dt.with_timezone(&Utc));
}
if let Some(dt) = parse_date_only(ts_str) {
return Some(dt);
}
if let Some(dt) = parse_time_only(ts_str) {
return Some(dt);
}
self.try_formats_with_reordering(ts_str, default_timezone)
}
fn try_formats_with_reordering(
&mut self,
ts_str: &str,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
for (index, format) in self.formats.iter().enumerate() {
if let Some(parsed) = try_parse_with_format(ts_str, format, default_timezone) {
if index > 0 {
let successful_format = self.formats.remove(index);
self.formats.insert(0, successful_format);
}
return Some(parsed);
}
}
None
}
#[allow(dead_code)]
pub fn reset_ordering(&mut self) {
self.formats = get_initial_timestamp_formats();
}
#[allow(dead_code)]
pub fn get_format_ordering(&self) -> Vec<String> {
self.formats.clone()
}
}
impl Default for AdaptiveTsParser {
fn default() -> Self {
Self::new()
}
}
fn try_parse_with_format(
ts_str: &str,
format: &str,
default_timezone: Option<&str>,
) -> Option<DateTime<Utc>> {
use chrono_tz::Tz;
if let Ok(dt) = DateTime::parse_from_str(ts_str, format) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, format) {
match default_timezone {
Some("UTC") => {
return Some(naive_dt.and_utc());
}
Some(tz_str) => {
if let Ok(tz) = tz_str.parse::<Tz>() {
if let Some(dt) = tz.from_local_datetime(&naive_dt).single() {
return Some(dt.with_timezone(&Utc));
}
}
if let Some(local_dt) = chrono::Local.from_local_datetime(&naive_dt).single() {
return Some(local_dt.with_timezone(&Utc));
}
}
None => {
if let Some(local_dt) = chrono::Local.from_local_datetime(&naive_dt).single() {
return Some(local_dt.with_timezone(&Utc));
}
}
}
}
None
}
fn try_parse_unix_timestamp(ts_str: &str) -> Option<DateTime<Utc>> {
let timestamp_int = ts_str.parse::<i64>().ok()?;
let dt = match ts_str.len() {
10 => {
DateTime::from_timestamp(timestamp_int, 0)
}
13 => {
DateTime::from_timestamp(
timestamp_int / 1000,
(timestamp_int % 1000) as u32 * 1_000_000,
)
}
16 => {
DateTime::from_timestamp(
timestamp_int / 1_000_000,
(timestamp_int % 1_000_000) as u32 * 1_000,
)
}
19 => {
DateTime::from_timestamp(
timestamp_int / 1_000_000_000,
(timestamp_int % 1_000_000_000) as u32,
)
}
_ => None,
};
dt.map(|dt| dt.with_timezone(&Utc))
}
fn get_initial_timestamp_formats() -> Vec<String> {
vec![
"%Y-%m-%dT%H:%M:%S%.fZ".to_string(), "%Y-%m-%dT%H:%M:%SZ".to_string(), "%Y-%m-%dT%H:%M:%S%.f%:z".to_string(), "%Y-%m-%dT%H:%M:%S%:z".to_string(), "%Y-%m-%dT%H:%M:%S%.f".to_string(), "%Y-%m-%dT%H:%M:%S".to_string(), "%Y-%m-%d %H:%M:%S%.f".to_string(), "%Y-%m-%d %H:%M:%S".to_string(), "%Y-%m-%d %H:%M:%S%.fZ".to_string(), "%Y-%m-%d %H:%M:%SZ".to_string(), "%Y-%m-%d %H:%M:%S%z".to_string(), "%Y-%m-%d %H:%M:%S%.f%z".to_string(), "%b %d %H:%M:%S".to_string(), "%b %d %Y %H:%M:%S".to_string(), "%d/%b/%Y:%H:%M:%S %z".to_string(), "%Y-%m-%d %H:%M:%S,%f".to_string(), "%Y/%m/%d %H:%M:%S".to_string(), "%d.%m.%Y %H:%M:%S".to_string(), "%y%m%d %H:%M:%S".to_string(), "%a %b %d %H:%M:%S %Y".to_string(), "%d-%b-%y %I:%M:%S.%f %p".to_string(), "%b %d, %Y %I:%M:%S %p".to_string(), ]
}
#[derive(Debug, Clone)]
pub struct TsConfig {
pub custom_field: Option<String>,
pub custom_format: Option<String>,
pub default_timezone: Option<String>,
#[allow(dead_code)]
pub auto_parse: bool,
}
impl Default for TsConfig {
fn default() -> Self {
Self {
custom_field: None,
custom_format: None,
default_timezone: None,
auto_parse: true,
}
}
}
pub fn identify_timestamp_field(
fields: &indexmap::IndexMap<String, rhai::Dynamic>,
config: &TsConfig,
) -> Option<(String, String)> {
if let Some(ref custom_field) = config.custom_field {
if let Some(value) = fields.get(custom_field) {
if let Ok(ts_str) = value.clone().into_string() {
return Some((custom_field.clone(), ts_str));
}
}
}
for ts_key in crate::event::TIMESTAMP_FIELD_NAMES {
if let Some(value) = fields.get(*ts_key) {
if let Ok(ts_str) = value.clone().into_string() {
return Some((ts_key.to_string(), ts_str));
}
}
}
None
}
pub fn parse_timestamp_arg_with_timezone(
arg: &str,
default_timezone: Option<&str>,
) -> Result<DateTime<Utc>, String> {
let mut parser = AdaptiveTsParser::new();
parser
.parse_ts_with_config(arg, None, default_timezone)
.ok_or_else(|| format!("Could not parse timestamp: {}", arg))
}
fn looks_like_relative_time(arg: &str) -> bool {
if !arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return false;
}
let num_end = arg.find(|c: char| !c.is_ascii_digit()).unwrap_or(arg.len());
let remainder = &arg[num_end..].trim_start();
if remainder.is_empty() {
return false;
}
matches!(
*remainder,
"s" | "sec"
| "secs"
| "second"
| "seconds"
| "m"
| "min"
| "mins"
| "minute"
| "minutes"
| "h"
| "hour"
| "hours"
| "d"
| "day"
| "days"
| "w"
| "week"
| "weeks"
)
}
fn parse_relative_time(arg: &str) -> Result<DateTime<Utc>, String> {
let (sign, rest) = if let Some(stripped) = arg.strip_prefix('-') {
(-1, stripped)
} else if let Some(stripped) = arg.strip_prefix('+') {
(1, stripped)
} else {
(-1, arg)
};
if rest.is_empty() {
return Err("Empty relative time".to_string());
}
let (num_str, unit) = if let Some(pos) = rest.find(|c: char| !c.is_ascii_digit()) {
let num_part = &rest[..pos];
let unit_part = rest[pos..].trim_start();
if unit_part.is_empty() || !unit_part.chars().next().unwrap().is_alphabetic() {
return Err("Relative time must have a valid unit (h, m, d, etc.)".to_string());
}
(num_part, unit_part)
} else {
return Err("Relative time must have a unit (h, m, d, etc.)".to_string());
};
let num: i64 = num_str
.parse()
.map_err(|_| "Invalid number in relative time")?;
let signed_num = sign * num;
let duration = match unit {
"s" | "sec" | "secs" | "second" | "seconds" => chrono::Duration::seconds(signed_num),
"m" | "min" | "mins" | "minute" | "minutes" => chrono::Duration::minutes(signed_num),
"h" | "hour" | "hours" => chrono::Duration::hours(signed_num),
"d" | "day" | "days" => chrono::Duration::days(signed_num),
"w" | "week" | "weeks" => chrono::Duration::weeks(signed_num),
_ => return Err(format!("Unknown time unit: {}", unit)),
};
Ok(Utc::now() + duration)
}
fn parse_date_only(arg: &str) -> Option<DateTime<Utc>> {
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%Y-%m-%d") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%m/%d/%Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(arg, "%d.%m.%Y") {
if let Some(naive_dt) = date.and_hms_opt(0, 0, 0) {
return Some(naive_dt.and_utc());
}
}
None
}
fn parse_time_only(arg: &str) -> Option<DateTime<Utc>> {
let today = Local::now().date_naive();
if let Ok(time) = chrono::NaiveTime::parse_from_str(arg, "%H:%M:%S") {
let naive_dt = today.and_time(time);
return Some(naive_dt.and_utc());
}
if let Ok(time) = chrono::NaiveTime::parse_from_str(arg, "%H:%M") {
let naive_dt = today.and_time(time);
return Some(naive_dt.and_utc());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
#[test]
fn test_adaptive_parser_basic() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("2023-07-04T12:34:56Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2023);
assert_eq!(dt.month(), 7);
assert_eq!(dt.day(), 4);
}
#[test]
fn test_format_reordering() {
let mut parser = AdaptiveTsParser::new();
let result1 = parser.parse_ts("2023-07-04 12:34:56");
assert!(result1.is_some());
let formats = parser.get_format_ordering();
assert!(!formats.is_empty());
assert_eq!(formats[0], "%Y-%m-%d %H:%M:%S%.f");
let result2 = parser.parse_ts("2023-07-05 13:45:07");
assert!(result2.is_some());
}
#[test]
fn test_unix_timestamp_parsing() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
let result = parser.parse_ts("1735566123000");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.year(), 2024);
}
#[test]
fn test_timestamp_field_identification() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert("ts".to_string(), Dynamic::from("2023-07-04T12:34:56Z"));
fields.insert("message".to_string(), Dynamic::from("test message"));
let config = TsConfig::default();
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "ts");
assert_eq!(value, "2023-07-04T12:34:56Z");
}
#[test]
fn test_custom_timestamp_field() {
use indexmap::IndexMap;
use rhai::Dynamic;
let mut fields = IndexMap::new();
fields.insert(
"custom_time".to_string(),
Dynamic::from("2023-07-04T12:34:56Z"),
);
fields.insert("ts".to_string(), Dynamic::from("other_timestamp"));
let config = TsConfig {
custom_field: Some("custom_time".to_string()),
custom_format: None,
default_timezone: None,
auto_parse: true,
};
let result = identify_timestamp_field(&fields, &config);
assert!(result.is_some());
let (field_name, value) = result.unwrap();
assert_eq!(field_name, "custom_time");
assert_eq!(value, "2023-07-04T12:34:56Z");
}
#[test]
fn test_ordering_reset() {
let mut parser = AdaptiveTsParser::new();
parser.parse_ts("2023-07-04 12:34:56");
let formats_after = parser.get_format_ordering();
assert_eq!(formats_after[0], "%Y-%m-%d %H:%M:%S%.f");
parser.reset_ordering();
let formats_reset = parser.get_format_ordering();
assert_eq!(formats_reset[0], "%Y-%m-%dT%H:%M:%S%.fZ");
}
#[test]
fn test_invalid_timestamp() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("not-a-timestamp");
assert!(result.is_none());
let result = parser.parse_ts("2023-13-45T25:70:70Z");
assert!(result.is_none());
}
#[test]
fn test_multiple_format_reordering() {
let mut parser = AdaptiveTsParser::new();
parser.parse_ts("2023-07-04 12:34:56"); parser.parse_ts("2023-07-05T13:45:07Z"); parser.parse_ts("2023-07-06 14:56:08");
let formats = parser.get_format_ordering();
assert_eq!(formats[0], "%Y-%m-%d %H:%M:%S%.f");
}
#[test]
fn test_journalctl_compatible_formats() {
let mut parser = AdaptiveTsParser::new();
let now = parser.parse_ts("now");
assert!(now.is_some());
let today = parser.parse_ts("today");
assert!(today.is_some());
let yesterday = parser.parse_ts("yesterday");
assert!(yesterday.is_some());
let tomorrow = parser.parse_ts("tomorrow");
assert!(tomorrow.is_some());
let one_hour_ago = parser.parse_ts("-1h");
assert!(one_hour_ago.is_some());
let thirty_min_from_now = parser.parse_ts("+30m");
assert!(thirty_min_from_now.is_some());
let date_only = parser.parse_ts("2023-07-04");
assert!(date_only.is_some());
let time_only = parser.parse_ts("15:30:45");
assert!(time_only.is_some());
}
#[test]
fn test_parse_timestamp_arg_with_timezone() {
let result = parse_timestamp_arg_with_timezone("2023-07-04T12:34:56Z", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("now", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-1h", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("invalid-timestamp", None);
assert!(result.is_err());
}
#[test]
fn test_relative_time_parsing() {
let result = parse_timestamp_arg_with_timezone("-30m", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("+2h", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-1d", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-2w", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("-", None);
assert!(result.is_err());
let result = parse_timestamp_arg_with_timezone("-1x", None);
assert!(result.is_err());
let result = parse_timestamp_arg_with_timezone("1h", None); assert!(result.is_ok());
}
#[test]
fn test_date_only_parsing() {
let result = parse_timestamp_arg_with_timezone("2023-07-04", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("07/04/2023", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("04.07.2023", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("2023-13-45", None);
assert!(result.is_err());
}
#[test]
fn test_time_only_parsing() {
let result = parse_timestamp_arg_with_timezone("15:30:45", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("09:15", None);
assert!(result.is_ok());
let result = parse_timestamp_arg_with_timezone("25:70:80", None);
assert!(result.is_err());
}
#[test]
fn test_unix_timestamp_parsing_enhanced() {
let mut parser = AdaptiveTsParser::new();
let result = parser.parse_ts("1735566123"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000000"); assert!(result.is_some());
let result = parser.parse_ts("1735566123000000000"); assert!(result.is_some());
let result = parser.parse_ts("123"); assert!(result.is_none());
let result = parser.parse_ts("12345678901234567890123"); assert!(result.is_none());
}
}