feat: set log timezone, closes #56
Implement a CLI arg, and config file setting, for changing the timezone of the Docker logs timestamp
This commit is contained in:
@@ -6,6 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use bollard::service::Port;
|
||||
use jiff::{tz::TimeZone, Timestamp};
|
||||
use ratatui::{
|
||||
style::Color,
|
||||
widgets::{ListItem, ListState},
|
||||
@@ -34,6 +35,7 @@ impl ContainerId {
|
||||
}
|
||||
|
||||
/// Only return first 8 chars of id, is usually more than enough for uniqueness
|
||||
/// TODO container id is a hex string, so can assume that 0..=8 will always return a 8 char ascii &str - need to update tests to use real ids, or atleast strings of the correct-ish length
|
||||
pub fn get_short(&self) -> String {
|
||||
self.0.chars().take(8).collect::<String>()
|
||||
}
|
||||
@@ -513,21 +515,34 @@ pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct LogsTz(String);
|
||||
|
||||
/// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet`
|
||||
/// So just split at the inclusive index of the first space, needs to be inclusive, hence the use of format to at the space, so that we can remove the whole thing when the `-t` flag is set
|
||||
/// Need to make sure that this isn't an empty string?!
|
||||
impl From<&str> for LogsTz {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.split_inclusive(' ').take(1).collect::<String>())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogsTz {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl LogsTz {
|
||||
/// With a given &str, split into a logtz and content, so that we only need to `use split_once()` once
|
||||
/// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet`
|
||||
pub fn splitter(input: &str) -> (Self, String) {
|
||||
let (tz, content) = input.split_once(' ').unwrap_or_default();
|
||||
(Self(tz.to_owned()), content.to_owned())
|
||||
}
|
||||
|
||||
/// Display the timestamp in a given format, and if provided, with a timezone offset
|
||||
pub fn display_with_formatter(&self, tz: Option<&TimeZone>, format: &str) -> Option<String> {
|
||||
self.0.parse::<Timestamp>().map_or(None, |t| {
|
||||
if let Some(tz) = tz.as_ref() {
|
||||
let tz = tz.iana_name()?;
|
||||
let z = t.in_tz(tz).ok()?;
|
||||
Some(z.strftime(format).to_string())
|
||||
} else {
|
||||
Some(t.strftime(format).to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
|
||||
/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the
|
||||
/// stateful list dependent on whethere the timestamp is in the HashSet or not
|
||||
@@ -745,15 +760,18 @@ impl Columns {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use jiff::tz::TimeZone;
|
||||
use ratatui::widgets::ListItem;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, Logs, RunningState},
|
||||
app_data::{ContainerImage, Logs, LogsTz, RunningState},
|
||||
ui::log_sanitizer,
|
||||
};
|
||||
|
||||
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
||||
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, State};
|
||||
|
||||
#[test]
|
||||
/// Display CpuStats as a string
|
||||
@@ -813,11 +831,76 @@ mod tests {
|
||||
assert_eq!(result, "name_01_name_01_name_01_name_01_");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// LogzTz correctly splits a line by timestamp
|
||||
fn test_container_state_logz_splitter() {
|
||||
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||
let log_tz = LogsTz::splitter(input);
|
||||
|
||||
assert_eq!(
|
||||
log_tz.0,
|
||||
super::LogsTz("2023-01-14T12:01:20.012345678Z".to_owned())
|
||||
);
|
||||
assert_eq!(log_tz.1, "Lorem ipsum dolor sit amet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// LogsTz display correctly formats with a given timestamp string
|
||||
fn test_container_state_logz_display() {
|
||||
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||
let log_tz = LogsTz::splitter(input);
|
||||
|
||||
let result = log_tz
|
||||
.0
|
||||
.display_with_formatter(None, "%Y-%m-%dT%H:%M:%S.%8f");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(result, "2023-01-14T12:01:20.01234567");
|
||||
|
||||
let result = log_tz.0.display_with_formatter(None, "%Y-%m-%d %H:%M:%S");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(result, "2023-01-14 12:01:20");
|
||||
|
||||
let result = log_tz.0.display_with_formatter(None, "%Y-%j");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result, "2023-014");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// LogsTz display correctly formats with a given timestamp string & timezone
|
||||
fn test_container_state_logz_display_with_timezone() {
|
||||
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||
let log_tz = LogsTz::splitter(input);
|
||||
|
||||
let timezone = Some(TimeZone::get("Asia/Tokyo").unwrap());
|
||||
let result = log_tz
|
||||
.0
|
||||
.display_with_formatter(timezone.as_ref(), "%Y-%m-%dT%H:%M:%S.%8f");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(result, "2023-01-14T21:01:20.01234567");
|
||||
|
||||
let result = log_tz
|
||||
.0
|
||||
.display_with_formatter(timezone.as_ref(), "%Y-%m-%d %H:%M:%S");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(result, "2023-01-14 21:01:20");
|
||||
|
||||
let result = log_tz.0.display_with_formatter(timezone.as_ref(), "%Y-%j");
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert_eq!(result, "2023-014");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Logs can only contain 1 entry per LogzTz
|
||||
fn test_container_state_logz() {
|
||||
let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||
let tz = LogsTz::from(input);
|
||||
let (tz, _) = LogsTz::splitter(input);
|
||||
let mut logs = Logs::default();
|
||||
let line = log_sanitizer::remove_ansi(input);
|
||||
|
||||
@@ -828,7 +911,7 @@ mod tests {
|
||||
assert_eq!(logs.logs.items.len(), 1);
|
||||
|
||||
let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||
let tz = LogsTz::from(input);
|
||||
let (tz, _) = LogsTz::splitter(input);
|
||||
let line = log_sanitizer::remove_ansi(input);
|
||||
|
||||
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||
|
||||
+18
-9
@@ -585,7 +585,7 @@ impl AppData {
|
||||
/// Get the title for log panel for selected container, will be either
|
||||
/// 1) "logs x/x - container_name - container_image"
|
||||
/// 2) "logs - container_name - container_image" when no logs found
|
||||
/// 3) "" no container currently selected - aka no containers on system
|
||||
/// 3) " " no container currently selected - aka no containers on system
|
||||
pub fn get_log_title(&self) -> String {
|
||||
self.get_selected_container()
|
||||
.map_or_else(String::new, |ci| {
|
||||
@@ -879,18 +879,27 @@ impl AppData {
|
||||
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
||||
let color = self.config.color_logs;
|
||||
let raw = self.config.raw_logs;
|
||||
let format = self.config.timestamp_format.clone();
|
||||
let config_tz = self.config.timezone.clone();
|
||||
|
||||
let timestamp = self.config.show_timestamp;
|
||||
let show_timestamp = self.config.show_timestamp;
|
||||
|
||||
if let Some(container) = self.get_any_container_by_id(id) {
|
||||
if !container.is_oxker {
|
||||
container.last_updated = Self::get_systemtime();
|
||||
let current_len = container.logs.len();
|
||||
|
||||
for mut i in logs {
|
||||
let tz = LogsTz::from(i.as_str());
|
||||
if !timestamp {
|
||||
i = i.replace(&tz.to_string(), "");
|
||||
let (log_tz, log_content) = LogsTz::splitter(i.as_str());
|
||||
if show_timestamp {
|
||||
i = format!(
|
||||
"{} {}",
|
||||
log_tz
|
||||
.display_with_formatter(config_tz.as_ref(), &format)
|
||||
.unwrap_or_else(|| log_tz.to_string()),
|
||||
log_content
|
||||
);
|
||||
} else {
|
||||
i = log_content;
|
||||
}
|
||||
let lines = if color {
|
||||
log_sanitizer::colorize_logs(&i)
|
||||
@@ -899,7 +908,7 @@ impl AppData {
|
||||
} else {
|
||||
log_sanitizer::remove_ansi(&i)
|
||||
};
|
||||
container.logs.insert(ListItem::new(lines), tz);
|
||||
container.logs.insert(ListItem::new(lines), log_tz);
|
||||
}
|
||||
|
||||
// Set the logs selected row for each container
|
||||
@@ -1819,7 +1828,7 @@ mod tests {
|
||||
assert_eq!(result, " - container_1 - image_1");
|
||||
|
||||
// On last line of logs
|
||||
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
|
||||
let logs = (1..=3).map(|i| format!("{i} {i}")).collect::<Vec<_>>();
|
||||
app_data.update_log_by_id(logs, &ids[0]);
|
||||
let result = app_data.get_log_title();
|
||||
assert_eq!(result, " 3/3 - container_1 - image_1");
|
||||
@@ -1851,7 +1860,7 @@ mod tests {
|
||||
assert_eq!(result, " - container_2 - image_2");
|
||||
|
||||
// On last line of logs
|
||||
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
|
||||
let logs = (1..=3).map(|i| format!("{i} {i}")).collect::<Vec<_>>();
|
||||
app_data.update_log_by_id(logs, &ids[1]);
|
||||
let result = app_data.get_log_title();
|
||||
assert_eq!(result, " 3/3 - container_2 - image_2");
|
||||
|
||||
Reference in New Issue
Block a user