Files
openobserve/src/config/src/utils/time.rs
T
36c2ef4418 feat: anamoly detection p2 (#10940)
Design at: anomaly-detection

---------

Signed-off-by: loaki07 <rustyloaki@gmail.com>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Loakesh Indiran <undefined@users.noreply.github.com>
Co-authored-by: ByteBaker <42913098+ByteBaker@users.noreply.github.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-03-19 21:36:06 +05:30

584 lines
20 KiB
Rust

// Copyright 2025 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, TimeZone, Utc};
use once_cell::sync::Lazy;
use crate::utils::json;
// BASE_TIME is the time when the timestamp is 1 year, used to check a timestamp
// is in seconds or milliseconds or microseconds or nanoseconds
pub static BASE_TIME: Lazy<DateTime<Utc>> =
Lazy::new(|| Utc.with_ymd_and_hms(1971, 1, 1, 0, 0, 0).unwrap());
pub static DAY_MICRO_SECS: i64 = 24 * 3600 * 1_000_000;
pub static HOUR_MICRO_SECS: i64 = 3600 * 1_000_000;
// check format: 1s, 1m, 1h, 1d, 1w, 1y, 1h10m30s
static TIME_UNITS: [(char, u64); 7] = [
('!', 1), // ms
('s', 1000),
('m', 60 * 1000),
('h', 3600 * 1000),
('d', 24 * 3600 * 1000),
('w', 7 * 24 * 3600 * 1000),
('y', 365 * 24 * 3600 * 1000),
];
#[inline(always)]
pub fn now() -> DateTime<Utc> {
Utc::now()
}
#[inline(always)]
pub fn now_micros() -> i64 {
Utc::now().timestamp_micros()
}
#[inline(always)]
pub fn day_micros(n: i64) -> i64 {
Duration::try_days(n).unwrap().num_microseconds().unwrap()
}
#[inline(always)]
pub fn hour_micros(n: i64) -> i64 {
Duration::try_hours(n).unwrap().num_microseconds().unwrap()
}
#[inline(always)]
pub fn second_micros(n: i64) -> i64 {
Duration::try_seconds(n)
.unwrap()
.num_microseconds()
.unwrap()
}
#[inline(always)]
pub fn get_ymdh_from_micros(n: i64) -> String {
let n = if n > 0 {
n
} else {
Utc::now().timestamp_micros()
};
let t = Utc.timestamp_nanos(n * 1000);
t.format("%Y/%m/%d/%H").to_string()
}
#[inline(always)]
pub fn parse_i64_to_timestamp_micros(v: i64) -> i64 {
if v == 0 {
return Utc::now().timestamp_micros();
}
let mut duration = v;
if duration > BASE_TIME.timestamp_nanos_opt().unwrap_or_default() {
// nanoseconds
duration /= 1000;
} else if duration > BASE_TIME.timestamp_micros() {
// microseconds
// noop
} else if duration > BASE_TIME.timestamp_millis() {
// milliseconds
duration *= 1000;
} else {
// seconds
duration *= 1_000_000;
}
duration
}
#[inline(always)]
pub fn parse_str_to_timestamp_micros(v: &str) -> Result<i64, anyhow::Error> {
match v.parse() {
Ok(i) => Ok(parse_i64_to_timestamp_micros(i)),
Err(_) => match parse_str_to_time(v) {
Ok(v) => Ok(v.timestamp_micros()),
Err(_) => Err(anyhow::anyhow!("invalid time format [string]")),
},
}
}
#[inline(always)]
pub fn parse_str_to_time(s: &str) -> Result<DateTime<Utc>, anyhow::Error> {
if let Ok(v) = s.parse::<f64>() {
let v = parse_i64_to_timestamp_micros(v as i64);
return Ok(Utc.timestamp_nanos(v * 1000));
}
let s = s.trim_matches('"');
let ret = if s.contains(' ') && s.len() == 19 {
let fmt = "%Y-%m-%d %H:%M:%S";
NaiveDateTime::parse_from_str(s, fmt)?.and_utc()
} else if s.contains('T') && !s.contains(' ') {
if s.len() == 19 {
let fmt = "%Y-%m-%dT%H:%M:%S";
NaiveDateTime::parse_from_str(s, fmt)?.and_utc()
} else if s.contains('.') {
// Handle formats with decimal seconds: "2025-05-14T01:15:25.047"
// First check if it has milliseconds (3 decimal places)
if s.split('.').next_back().unwrap_or("").len() == 3 {
let fmt = "%Y-%m-%dT%H:%M:%S%.3f";
NaiveDateTime::parse_from_str(s, fmt)?.and_utc()
} else if s.split('.').next_back().unwrap_or("").len() == 6 {
// Handle microseconds (6 decimal places)
let fmt = "%Y-%m-%dT%H:%M:%S%.6f";
NaiveDateTime::parse_from_str(s, fmt)?.and_utc()
} else {
// Fall back to RFC3339 parsing for other decimal formats
let t = DateTime::parse_from_rfc3339(s)?;
t.into()
}
} else {
// Other formats with 'T' but no spaces or decimal points
let t = DateTime::parse_from_rfc3339(s)?;
t.into()
}
} else {
let t = DateTime::parse_from_rfc2822(s)?;
t.into()
};
Ok(ret)
}
// return timestamp and is_valid micros value
#[inline(always)]
pub fn parse_timestamp_micro_from_value(v: &json::Value) -> Result<(i64, bool), anyhow::Error> {
let (ts, is_i64) = match v {
json::Value::String(s) => (parse_str_to_timestamp_micros(s)?, false),
json::Value::Number(n) => {
if n.is_i64() {
(n.as_i64().unwrap(), true)
} else if n.is_u64() {
(n.as_u64().unwrap() as i64, true)
} else if n.is_f64() {
(n.as_f64().unwrap() as i64, false)
} else {
return Err(anyhow::anyhow!("Invalid time format [timestamp]"));
}
}
_ => return Err(anyhow::anyhow!("Invalid time format [type]")),
};
let new_ts = parse_i64_to_timestamp_micros(ts);
let is_valid = is_i64 && new_ts == ts;
Ok((new_ts, is_valid))
}
pub fn parse_milliseconds(s: &str) -> Result<u64, anyhow::Error> {
let chars = s.chars().collect::<Vec<char>>();
// without unit, default is second
if chars.iter().all(|c| c.is_ascii_digit()) {
return Ok(s.parse::<u64>().unwrap_or(0) * 1000);
}
let mut unit_pos = TIME_UNITS.len();
let mut start = 0;
let mut total = 0;
let chars_count = chars.len();
let mut i = 0;
while i < chars_count {
let c = chars.get(i).unwrap();
if c.is_ascii_digit() {
i += 1;
continue;
}
if i == 0 {
return Err(anyhow::anyhow!("Invalid time format: {c}"));
}
let step_value = chars[start..i]
.iter()
.collect::<String>()
.parse::<u64>()
.unwrap_or(0);
start = i + 1;
// check unit
let pos = TIME_UNITS[..unit_pos].iter().position(|&x| x.0 == *c);
if pos.is_none() && *c != 'm' {
return Err(anyhow::anyhow!("Invalid time format: {c}"));
}
// check unit: ms
let cur_unit = if *c == 'm' && i + 1 < chars_count && chars.get(i + 1).unwrap() == &'s' {
i += 1;
unit_pos = 0;
&TIME_UNITS[unit_pos]
} else {
unit_pos = pos.unwrap();
&TIME_UNITS[unit_pos]
};
// calc
total += step_value * cur_unit.1;
i += 1;
}
Ok(total)
}
pub fn parse_timezone_to_offset(offset: &str) -> i64 {
// let offset = "+08:00"; // or "-07:00" or "UTC"
let sign: i64;
let time: &str;
if let Some(stripped) = offset.strip_prefix('+') {
sign = 1;
time = stripped;
} else if let Some(stripped) = offset.strip_prefix('-') {
sign = -1;
time = stripped;
} else if offset.eq_ignore_ascii_case("cst") {
sign = 1;
time = "+08:00";
} else if offset.eq_ignore_ascii_case("utc") || offset.is_empty() {
sign = 0;
time = "00:00";
} else {
panic!("Invalid time zone offset");
}
// convert time to seconds
let seconds: i64 = time
.split(':')
.map(|val| val.parse::<i64>().unwrap())
.fold(0, |acc, val| acc * 60 + val * 60);
sign * seconds
}
#[inline(always)]
pub fn parse_str_to_timestamp_micros_as_option(v: &str) -> Option<i64> {
v.parse()
.ok()
.map(parse_i64_to_timestamp_micros)
.or_else(|| parse_str_to_time(v).map(|t| t.timestamp_micros()).ok())
}
/// Get the end of the day timestamp_micros
pub fn end_of_the_day(timestamp: i64) -> i64 {
let t = Utc.timestamp_nanos((timestamp + DAY_MICRO_SECS) * 1000);
let t_next_day_zero = Utc
.with_ymd_and_hms(t.year(), t.month(), t.day(), 0, 0, 0)
.unwrap()
.timestamp_micros();
t_next_day_zero - 1
}
pub fn format_duration(ms: u64) -> String {
if ms == 0 {
return "0s".to_string();
}
let seconds = ms / 1000;
let minutes = seconds / 60;
let hours = minutes / 60;
let days = hours / 24;
let remaining_seconds = seconds % 60;
let remaining_minutes = minutes % 60;
let remaining_hours = hours % 24;
let mut parts = Vec::new();
if days > 0 {
parts.push(format!("{days}d"));
}
if remaining_hours > 0 {
parts.push(format!("{remaining_hours}h"));
}
if remaining_minutes > 0 {
parts.push(format!("{remaining_minutes}m"));
}
if remaining_seconds > 0 {
parts.push(format!("{remaining_seconds}s"));
}
parts.join("")
}
/// Parse an interval string like "5m", "1h", "30s" into minutes.
/// Returns 0 for unrecognised formats.
pub fn parse_interval_to_minutes(s: &str) -> i64 {
if let Some(n) = s.strip_suffix('m') {
n.parse().unwrap_or(0)
} else if let Some(n) = s.strip_suffix('h') {
n.parse::<i64>().unwrap_or(0) * 60
} else if let Some(n) = s.strip_suffix('s') {
n.parse::<i64>().unwrap_or(0) / 60
} else if let Some(n) = s.strip_suffix('d') {
n.parse::<i64>().unwrap_or(0) * 1440
} else {
s.parse().unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_i64_to_timestamp_micros() {
let test_fn = parse_i64_to_timestamp_micros;
let v_exp_arr = [
(1609459200000000000, 1609459200000000000 / 1000),
(1609459200000000, 1609459200000000),
(1609459200000, 1609459200000 * 1000),
(1609459200, 1609459200 * 1_000_000),
];
for (input, expected) in v_exp_arr {
assert_eq!(test_fn(input), expected);
}
}
#[test]
fn test_parse_str_to_time() {
let test_fn = |v| parse_str_to_time(v).map(|v| v.timestamp_micros());
assert!(test_fn("2021-01-01T00:00:00").is_ok_and(|v| v == 1609459200000000));
assert!(test_fn("2021-01-01T00:00:00Z").is_ok_and(|v| v == 1609459200000000));
assert!(test_fn("2021-01-01T23:59:59Z").is_ok_and(|v| v == 1609545599000000));
assert!(test_fn("2021-01-01T00:00:00+08:00").is_ok_and(|v| v == 1609430400000000));
assert!(test_fn("2021-01-01T00:00:00-08:00").is_ok_and(|v| v == 1609488000000000));
assert!(test_fn("2021-01-01 00:00:00").is_ok_and(|v| v == 1609459200000000));
assert!(test_fn("2021-01-01T00:00:00.000000Z").is_ok_and(|v| v == 1609459200000000));
assert!(test_fn("2021-01-01T00:00:00.000000+08:00").is_ok_and(|v| v == 1609430400000000));
assert!(test_fn("Wed, 8 Mar 2023 16:46:51 CST").is_ok_and(|v| v == 1678315611000000));
assert!(test_fn("2021-01-01T00:00:00.123").is_ok_and(|v| v == 1609459200123000));
assert!(test_fn("2021-01-01T00:00:00.123456").is_ok_and(|v| v == 1609459200123456));
assert!(test_fn("2021-01-01T00:00:00.123456+08:00").is_ok_and(|v| v == 1609430400123456));
assert!(
test_fn("2021-01-01T00:00:00.123456789-08:00").is_ok_and(|v| v == 1609488000123456)
);
assert!(test_fn("2021-01-01T00:00:00.123Z").is_ok_and(|v| v == 1609459200123000));
assert!(test_fn("2021-01-01T00:00:00.123456789").is_err());
}
#[test]
fn test_parse_str_to_timestamp_micros() {
let test_fn = parse_str_to_timestamp_micros;
let v_exp_arr = [
(1609459200000000, "2021-01-01T00:00:00"),
(1609459200000000, "2021-01-01T00:00:00Z"),
(1609430400000000, "2021-01-01T00:00:00+08:00"),
(1609488000000000, "2021-01-01T00:00:00-08:00"),
(1609459200000000, "2021-01-01 00:00:00"),
(1609459200000000, "2021-01-01T00:00:00.000000Z"),
(1609430400000000, "2021-01-01T00:00:00.000000+08:00"),
(1678315611000000, "Wed, 8 Mar 2023 16:46:51 CST"),
(1609459200123000, "2021-01-01T00:00:00.123"),
(1609459200123456, "2021-01-01T00:00:00.123456"),
(1609430400123456, "2021-01-01T00:00:00.123456+08:00"),
(1609488000123456, "2021-01-01T00:00:00.123456789-08:00"),
];
for (v_exp, input) in v_exp_arr {
assert!(test_fn(input).is_ok_and(|v| v == v_exp));
}
}
#[test]
fn test_parse_timestamp_micro_from_value() {
let v_exp_arr = [
(1609459200000000, json::json!(1609459200000000i64)),
(1609459200000000, json::json!("2021-01-01T00:00:00")),
(1609459200000000, json::json!("2021-01-01T00:00:00Z")),
(1609430400000000, json::json!("2021-01-01T00:00:00+08:00")),
(1609488000000000, json::json!("2021-01-01T00:00:00-08:00")),
(1609459200000000, json::json!("2021-01-01 00:00:00")),
(1609459200000000, json::json!("2021-01-01T00:00:00.000000Z")),
(
1609430400000000,
json::json!("2021-01-01T00:00:00.000000+08:00"),
),
(
1678315611000000,
json::json!("Wed, 8 Mar 2023 16:46:51 CST"),
),
(1609459200123000, json::json!("2021-01-01T00:00:00.123")),
(1609459200000000, json::json!("2021-01-01T00:00:00Z")),
(1609430400000000, json::json!("2021-01-01T00:00:00+08:00")),
(1609488000000000, json::json!("2021-01-01T00:00:00-08:00")),
(1609459200000000, json::json!("2021-01-01 00:00:00")),
(1609459200000000, json::json!("2021-01-01T00:00:00.000000Z")),
(
1609430400000000,
json::json!("2021-01-01T00:00:00.000000+08:00"),
),
(
1678315611000000,
json::json!("Wed, 8 Mar 2023 16:46:51 CST"),
),
(1609459200123000, json::json!("2021-01-01T00:00:00.123")),
(1609459200123456, json::json!("2021-01-01T00:00:00.123456")),
];
for (v_exp, input) in v_exp_arr {
assert!(parse_timestamp_micro_from_value(&input).is_ok_and(|v| v.0 == v_exp));
}
}
#[test]
fn test_parse_milliseconds_without_unit() {
assert_eq!(parse_milliseconds("123").unwrap(), 123000);
assert_eq!(parse_milliseconds("0").unwrap(), 0);
assert_eq!(parse_milliseconds("").unwrap(), 0);
assert!(parse_milliseconds("abc").is_err());
}
#[test]
fn test_parse_milliseconds_with_unit() {
assert_eq!(parse_milliseconds("1s").unwrap(), 1000);
assert_eq!(parse_milliseconds("1m").unwrap(), 60 * 1000);
assert_eq!(parse_milliseconds("1h").unwrap(), 3600 * 1000);
assert_eq!(parse_milliseconds("1d").unwrap(), 24 * 3600 * 1000);
assert_eq!(parse_milliseconds("1w").unwrap(), 7 * 24 * 3600 * 1000);
assert_eq!(parse_milliseconds("1y").unwrap(), 365 * 24 * 3600 * 1000);
assert_eq!(parse_milliseconds("1h10m30s").unwrap(), 4230000);
assert_eq!(parse_milliseconds("1h10m30s10ms").unwrap(), 4230010);
assert!(parse_milliseconds("s").is_err());
assert!(parse_milliseconds("10z").is_err());
}
#[test]
fn test_parse_timezone_to_offset() {
assert_eq!(parse_timezone_to_offset(""), 0);
assert_eq!(parse_timezone_to_offset("UTC"), 0);
assert_eq!(parse_timezone_to_offset("CST"), 28800);
assert_eq!(parse_timezone_to_offset("+08:00"), 28800);
assert_eq!(parse_timezone_to_offset("-08:00"), -28800);
}
#[test]
fn test_end_of_the_day() {
let t_d_arr = [
(1609545599999999, 1609459200000000),
(1727827199999999, 1727740800000000),
];
for (d_exp, t) in t_d_arr {
assert_eq!(end_of_the_day(t), d_exp);
}
}
#[test]
fn test_get_ymdhms_from_micros() {
assert_eq!(get_ymdh_from_micros(1609459200000000), "2021/01/01/00");
assert_eq!(get_ymdh_from_micros(1744077663427000), "2025/04/08/02");
// Test with input 0 (uses current time, so we can't test the exact value)
let result = get_ymdh_from_micros(0);
assert!(!result.is_empty());
}
#[test]
fn test_format_duration() {
let v_exp_arr = [
("0s", 0), // Zero milliseconds
("1s", 1000), // 1 second
("5s", 5000), // 5 seconds
("59s", 59000), // 59 seconds
("1m", 60000), // 1 minute
("5m", 300000), // 5 minutes
("59m", 3540000), // 59 minutes
("1h", 3600000), // 1 hour
("2h", 7200000), // 2 hours
("23h", 82800000), // 23 hours
("1d", 86400000), // 1 day
("2d", 172800000), // 2 days
("30d", 2592000000), // 30 days
("1d1h1m1s", 90061000), // 1 day 1 hour 1 minute 1 second
("1d2h3m4s", 93784000), // 1 day 2 hours 3 minutes 4 seconds
("1h1m1s", 3661000),
("1m1s", 61000),
];
for (v_exp, input) in v_exp_arr {
assert_eq!(format_duration(input), v_exp);
}
}
#[test]
fn test_day_micros() {
assert_eq!(day_micros(1), 86400000000);
assert_eq!(day_micros(2), 172800000000);
assert_eq!(day_micros(0), 0);
}
#[test]
fn test_hour_micros() {
assert_eq!(hour_micros(1), 3600000000);
assert_eq!(hour_micros(2), 7200000000);
assert_eq!(hour_micros(0), 0);
}
#[test]
fn test_second_micros() {
assert_eq!(second_micros(1), 1000000);
assert_eq!(second_micros(60), 60000000);
assert_eq!(second_micros(0), 0);
}
#[test]
fn test_parse_str_to_timestamp_micros_as_option_valid() {
let result = parse_str_to_timestamp_micros_as_option("1609459200000000");
assert!(result.is_some());
assert_eq!(result.unwrap(), 1609459200000000);
}
#[test]
fn test_parse_str_to_timestamp_micros_as_option_invalid() {
let result = parse_str_to_timestamp_micros_as_option("invalid");
assert!(result.is_none());
}
#[test]
fn test_parse_str_to_timestamp_micros_as_option_date_string() {
let result = parse_str_to_timestamp_micros_as_option("2021-01-01T00:00:00Z");
assert!(result.is_some());
assert_eq!(result.unwrap(), 1609459200000000);
}
#[test]
fn test_parse_i64_to_timestamp_micros_zero() {
let now = now_micros();
let result = parse_i64_to_timestamp_micros(0);
// Result should be close to now (within a few seconds)
assert!(result > 0);
assert!((result - now).abs() < 5000000); // Within 5 seconds
}
#[test]
fn test_parse_i64_to_timestamp_micros_negative() {
// Negative timestamps should be treated as seconds
let result = parse_i64_to_timestamp_micros(-1);
assert_eq!(result, -1_000_000);
}
#[test]
fn test_parse_milliseconds_edge_cases() {
assert_eq!(parse_milliseconds("1ms").unwrap(), 1);
assert_eq!(parse_milliseconds("100ms").unwrap(), 100);
assert_eq!(parse_milliseconds("1s1ms").unwrap(), 1001);
assert_eq!(parse_milliseconds("1m1s1ms").unwrap(), 61001);
}
#[test]
fn test_parse_milliseconds_complex() {
assert_eq!(parse_milliseconds("2d3h4m5s").unwrap(), 183845000);
assert_eq!(parse_milliseconds("1w1d").unwrap(), 691200000);
assert_eq!(parse_milliseconds("1y1w1d").unwrap(), 32227200000);
}
#[test]
fn test_now_micros() {
let t1 = now_micros();
std::thread::sleep(std::time::Duration::from_millis(10));
let t2 = now_micros();
assert!(t2 > t1);
assert!(t2 - t1 >= 10000); // At least 10ms difference
}
}