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:
Jack Wills
2025-02-21 11:03:19 +00:00
parent 8305e6fda6
commit 17a5e7a258
27 changed files with 1122 additions and 163 deletions
+96 -13
View File
@@ -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
View File
@@ -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");