From 2502dd96260cbb8b70e3a8ef3fdd3dc09afc7cc3 Mon Sep 17 00:00:00 2001 From: Jeroen De Wilde <52921312+jeroendewilde@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:14:13 +0200 Subject: [PATCH] feat: add toggle for log timestamps (#2471) --- agent/docker.go | 7 +-- .../containers-table/containers-table.tsx | 20 ++++++-- internal/site/src/components/logs-display.tsx | 47 +++++++++++++++++-- .../systemd-table/systemd-table.tsx | 20 ++++++-- internal/site/src/lib/logs.ts | 13 +++++ internal/site/src/locales/en/en.po | 8 ++++ internal/site/src/locales/nl/nl.po | 7 +++ 7 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 internal/site/src/lib/logs.ts diff --git a/agent/docker.go b/agent/docker.go index 3d29272ae..ee7700497 100644 --- a/agent/docker.go +++ b/agent/docker.go @@ -866,9 +866,10 @@ func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID strin // getLogs fetches the logs for a container func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) { query := url.Values{ - "stdout": []string{"1"}, - "stderr": []string{"1"}, - "tail": []string{fmt.Sprintf("%d", dockerLogsTail)}, + "timestamps": []string{"1"}, + "stdout": []string{"1"}, + "stderr": []string{"1"}, + "tail": []string{fmt.Sprintf("%d", dockerLogsTail)}, } endpoint, err := buildDockerContainerEndpoint(containerID, "logs", query) if err != nil { diff --git a/internal/site/src/components/containers-table/containers-table.tsx b/internal/site/src/components/containers-table/containers-table.tsx index 68fe8cf88..8b3f10132 100644 --- a/internal/site/src/components/containers-table/containers-table.tsx +++ b/internal/site/src/components/containers-table/containers-table.tsx @@ -32,7 +32,8 @@ import { Separator } from "../ui/separator" import { $router, Link } from "../router" import { listenKeys } from "nanostores" import { getPagePath } from "@nanostores/router" -import { LogsDisplay, LogsFullscreenDialog } from "@/components/logs-display" +import { LogsDisplay, LogsFullscreenDialog, LogsTimestampToggle } from "@/components/logs-display" +import { getLogTimestampDecorations } from "@/lib/logs" const syntaxTheme = "github-dark-dimmed" @@ -282,7 +283,13 @@ async function getLogsHtml(container: ContainerRecord): Promise { container: container.id, }), ]) - return logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: "log", theme: syntaxTheme }) : t`No results.` + return logsHtml.logs + ? highlighter.codeToHtml(logsHtml.logs, { + lang: "log", + theme: syntaxTheme, + decorations: getLogTimestampDecorations(logsHtml.logs), + }) + : t`No results.` } catch (error) { console.error(error) return "" @@ -318,6 +325,8 @@ function ContainerSheet({ activeContainer: RefObject }) { const [logsDisplay, setLogsDisplay] = useState("") + const [showTimestamps, setShowTimestamps] = useState(false) + const toggleTimestamps = () => setShowTimestamps((shown) => !shown) const [infoDisplay, setInfoDisplay] = useState("") const [logsFullscreenOpen, setLogsFullscreenOpen] = useState(false) const [infoFullscreenOpen, setInfoFullscreenOpen] = useState(false) @@ -376,6 +385,8 @@ function ContainerSheet({ name={container.name} onRefresh={refreshLogs} isRefreshing={isRefreshingLogs} + showTimestamps={showTimestamps} + onToggleTimestamps={toggleTimestamps} />

{t`Logs`}

+
- +

{t`Detail`}

+ ) } // Shared by Docker and systemd service sheets so logs behave identically. -export function LogsDisplay({ logsDisplay, containerRef }: LogsDisplayProps) { +export function LogsDisplay({ logsDisplay, containerRef, showTimestamps = true }: LogsDisplayProps) { return (
@@ -32,6 +59,8 @@ export function LogsFullscreenDialog({ name, onRefresh, isRefreshing, + showTimestamps, + onToggleTimestamps, }: { open: boolean onOpenChange: (open: boolean) => void @@ -39,6 +68,8 @@ export function LogsFullscreenDialog({ name: string onRefresh: () => void | Promise isRefreshing: boolean + showTimestamps: boolean + onToggleTimestamps: () => void }) { const outerContainerRef = useRef(null) @@ -57,10 +88,20 @@ export function LogsFullscreenDialog({ {name} logs
-
+
+
{logs ? ( - + ) : logsStatus === "loading" ? ( <>
diff --git a/internal/site/src/lib/logs.ts b/internal/site/src/lib/logs.ts new file mode 100644 index 000000000..8e1e34bfc --- /dev/null +++ b/internal/site/src/lib/logs.ts @@ -0,0 +1,13 @@ +import type { DecorationItem } from "shiki/core" + +// Docker uses RFC3339Nano; journalctl short-iso uses an offset such as +0200. +// Only mark the first timestamp, leaving dates in the log message untouched. +export function getLogTimestampDecorations(logs: string): DecorationItem[] { + const timestamps = logs.matchAll(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2}) /gm) + return Array.from(timestamps, (match) => ({ + start: match.index, + end: match.index + match[0].length, + properties: { class: "log-timestamp" }, + alwaysWrap: true, + })) +} diff --git a/internal/site/src/locales/en/en.po b/internal/site/src/locales/en/en.po index d7038b61c..c19dec4ac 100644 --- a/internal/site/src/locales/en/en.po +++ b/internal/site/src/locales/en/en.po @@ -2354,3 +2354,11 @@ msgstr "Yes" #: src/components/routes/settings/layout.tsx msgid "Your user settings have been updated." msgstr "Your user settings have been updated." + +#: src/components/logs-display.tsx +msgid "Show timestamps" +msgstr "Show timestamps" + +#: src/components/logs-display.tsx +msgid "Hide timestamps" +msgstr "Hide timestamps" diff --git a/internal/site/src/locales/nl/nl.po b/internal/site/src/locales/nl/nl.po index 097bd68cd..dd6cecc2f 100644 --- a/internal/site/src/locales/nl/nl.po +++ b/internal/site/src/locales/nl/nl.po @@ -2360,3 +2360,10 @@ msgstr "Ja" msgid "Your user settings have been updated." msgstr "Je gebruikersinstellingen zijn bijgewerkt." +#: src/components/logs-display.tsx +msgid "Show timestamps" +msgstr "Tijdstempels tonen" + +#: src/components/logs-display.tsx +msgid "Hide timestamps" +msgstr "Tijdstempels verbergen"