Skip to content

Commit

Permalink
feat: add timezone support for timestamp conversion
Browse files Browse the repository at this point in the history
Added support for specifying a timezone using the `--timezone` flag or the
`SNAZY_TIMEZONE` environment variable. This allows timestamps to be displayed
in the specified timezone instead of the server's default timezone (usually
UTC). Updated the `Config` struct and related functions to handle timezone
conversion. Added tests to verify the correct parsing and conversion of
timestamps with timezones.

Fixes #276

Signed-off-by: Chmouel Boudjnah <chmouel@chmouel.com>
  • Loading branch information
chmouel committed Feb 13, 2025
1 parent 3ae5fbb commit 4cb4dd9
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 18 deletions.
90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ tempfile = "3.16.0"
clap_complete = "4.5.44"
color-print = "0.3.7"
is-terminal = "0.4.15"
chrono-tz = "0.10.1"
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ are supported for now (ie: no rgb or fixed):
[`strftime`](https://man7.org/linux/man-pages/man3/strftime.3.html) format
strings.

- You can specify a timezone with the `--timezone` flag (or the environment variable
`SNAZY_TIMEZONE`). By default, the timestamps are displayed in the server's timezone
(usually UTC). The timezone should be specified in the IANA timezone database format
(e.g., "America/New_York", "Europe/Paris", "Asia/Tokyo").

```shell
kubectl logs deployment/controller | snazy --timezone America/New_York
```

- If you want to skip showing some lines you can specify the flag
`-S/--skip-line-regexp`. When it matches the word or regexp in
this value it will simply skipping printing the line. You can have multiple flags
Expand Down
5 changes: 5 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ struct Args {
/// A timeformat as documented by the strftime(3) manpage.
pub time_format: String,

#[arg(long, env = "SNAZY_TIMEZONE")]
/// Convert timestamps to specified timezone (e.g. `Europe/Paris`, `America/New_York`)
pub timezone: Option<String>,

#[arg(
long,
verbatim_doc_comment,
Expand Down Expand Up @@ -248,6 +252,7 @@ pub fn build_cli_config() -> Config {
kail_prefix_format: args.kail_prefix_format,
kail_no_prefix: args.kail_no_prefix,
time_format: args.time_format,
timezone: args.timezone,
skip_line_regexp: args.skip_line_regexp,
filter_levels: args.filter_levels,
action_command: args.action_command,
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Config {
pub regexp_colours: HashMap<String, Style>,
pub skip_line_regexp: Vec<String>,
pub time_format: String,
pub timezone: Option<String>,
}

impl Default for Config {
Expand All @@ -54,6 +55,7 @@ impl Default for Config {
kail_no_prefix: false,
kail_prefix_format: String::from("{namespace}/{pod}[{container}]"),
time_format: String::from("%H:%M:%S"),
timezone: None,
filter_levels: <Vec<LogLevel>>::new(),
regexp_colours: HashMap::new(),
json_keys: HashMap::new(),
Expand Down
18 changes: 12 additions & 6 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Info {

pub fn extract_info(rawline: &str, config: &Config) -> HashMap<String, String> {
let time_format = config.time_format.as_str();
let timezone = config.timezone.as_deref();
let mut msg = HashMap::new();
let mut kail_msg_prefix = String::new();
let mut line = rawline.to_string();
Expand All @@ -65,7 +66,11 @@ pub fn extract_info(rawline: &str, config: &Config) -> HashMap<String, String> {
// parse timestamp to a unix timestamp
msg.insert(
"ts".to_string(),
crate::utils::convert_str_to_ts(p.timestamp.as_str(), config.time_format.as_str()),
crate::utils::convert_str_to_ts(
p.timestamp.as_str(),
config.time_format.as_str(),
config.timezone.as_deref(),
),
);
let mut others = String::new();
if p.other.contains_key("provider") {
Expand All @@ -84,7 +89,7 @@ pub fn extract_info(rawline: &str, config: &Config) -> HashMap<String, String> {
if let Some(ts) = p.other.get("ts") {
msg.insert(
String::from("ts"),
crate::utils::convert_ts_float_or_str(ts, time_format),
crate::utils::convert_ts_float_or_str(ts, time_format, timezone),
);
};
}
Expand Down Expand Up @@ -123,18 +128,19 @@ fn custom_json_match(
if let Ok(p) = serde_json::from_str::<Value>(line) {
for (key, value) in &config.json_keys {
if p.pointer(value).is_some() {
// if value equal ts or timestamp or date then parse as timestamp
if key == "ts" || key == "timestamp" || key == "date" {
// make a serde json Value
let v = p.pointer(value).unwrap();
let ts = crate::utils::convert_ts_float_or_str(v, time_format);
let ts = crate::utils::convert_ts_float_or_str(
v,
time_format,
config.timezone.as_deref(),
);
dico.insert(key.to_string(), ts);
} else {
let mut v = p.pointer(value).unwrap().to_string();
if v.contains('"') {
v = v.replace('"', "");
}

dico.insert(key.to_string(), v);
}
}
Expand Down
49 changes: 37 additions & 12 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use chrono::{DateTime, NaiveDateTime};
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use chrono_tz::Tz;
use serde_json::Value;
use yansi::Paint;

Expand Down Expand Up @@ -35,24 +36,35 @@ pub fn convert_pac_provider_to_fa_icon(provider: &str) -> &str {
}
}

pub fn convert_str_to_ts(s: &str, time_format: &str) -> String {
// try to convert s to a nativdatetime if fail then return just the string
pub fn convert_str_to_ts(s: &str, time_format: &str, timezone: Option<&str>) -> String {
if let Ok(ts) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%fZ") {
return ts.format(time_format).to_string();
let utc_dt = Utc.from_utc_datetime(&ts);
if let Some(tz) = timezone {
if let Ok(tz) = tz.parse::<Tz>() {
return utc_dt.with_timezone(&tz).format(time_format).to_string();
}
}
return utc_dt.format(time_format).to_string();
}

s.to_string()
}

fn convert_unix_ts(value: i64, time_format: &str) -> String {
let ts = DateTime::from_timestamp(value, 0).unwrap();
ts.format(time_format).to_string()
fn convert_unix_ts(value: i64, time_format: &str, timezone: Option<&str>) -> String {
if let Some(ts) = DateTime::from_timestamp(value, 0) {
if let Some(tz) = timezone {
if let Ok(tz) = tz.parse::<Tz>() {
return ts.with_timezone(&tz).format(time_format).to_string();
}
}
return ts.format(time_format).to_string();
}
value.to_string()
}

pub fn convert_ts_float_or_str(value: &Value, time_format: &str) -> String {
pub fn convert_ts_float_or_str(value: &Value, time_format: &str, timezone: Option<&str>) -> String {
match value {
Value::String(s) => convert_str_to_ts(s.as_str(), time_format),
Value::Number(n) => convert_unix_ts(n.as_f64().unwrap() as i64, time_format),
Value::String(s) => convert_str_to_ts(s.as_str(), time_format, timezone),
Value::Number(n) => convert_unix_ts(n.as_f64().unwrap() as i64, time_format, timezone),
_ => String::new(),
}
}
Expand Down Expand Up @@ -88,9 +100,22 @@ mod tests {
assert_eq!(
convert_ts_float_or_str(
&Value::String("2020-01-01T00:00:00.000Z".to_string()),
"%Y-%m-%d %H:%M:%S"
"%Y-%m-%d %H:%M:%S",
None
),
"2020-01-01 00:00:00"
);
}

#[test]
fn test_convert_ts_float_or_str_with_timezone() {
assert_eq!(
convert_ts_float_or_str(
&Value::String("2020-01-01T00:00:00.000Z".to_string()),
"%Y-%m-%d %H:%M:%S",
Some("Europe/Paris")
),
"2020-01-01 01:00:00" // Paris is UTC+1
);
}
}
8 changes: 8 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ snazytest!(
false
);

snazytest!(
timezone_parsing,
["--timezone", "America/New_York"],
r#"{"level":"info","ts":1739447782.2690723,"msg":"timezone test"}"#,
"INFO 06:56:22 timezone test\n",
false
);

#[test]
#[should_panic]
fn all_json_keys_need_tobe_specified() {
Expand Down

0 comments on commit 4cb4dd9

Please sign in to comment.