From 468414cac400ed61c90de6f7aadb68b7c8281348 Mon Sep 17 00:00:00 2001 From: Elonito <0xRaini@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:08:43 +0800 Subject: [PATCH] fix: use local timezone in console log timestamps formatConsoleTimestamp previously used Date.toISOString() which always returns UTC time (suffixed with Z). This confused users whose local timezone differs from UTC. Now uses local time methods (getHours, getMinutes, etc.) and appends the local UTC offset (e.g. +08:00) instead of Z. The pretty style returns local HH:MM:SS. The hasTimestampPrefix regex is updated to accept both Z and +/-HH:MM offset suffixes. Closes #14699 --- src/logging/console-capture.test.ts | 5 +++- src/logging/console-timestamp.test.ts | 39 +++++++++++++++++++++++++++ src/logging/console.ts | 26 ++++++++++++++---- 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/logging/console-timestamp.test.ts diff --git a/src/logging/console-capture.test.ts b/src/logging/console-capture.test.ts index 39acaf108ef..a16c51581a7 100644 --- a/src/logging/console-capture.test.ts +++ b/src/logging/console-capture.test.ts @@ -86,7 +86,10 @@ describe("enableConsoleCapture", () => { console.warn("[EventQueue] Slow listener detected"); expect(warn).toHaveBeenCalledTimes(1); const firstArg = String(warn.mock.calls[0]?.[0] ?? ""); - expect(firstArg.startsWith("2026-01-17T18:01:02.000Z [EventQueue]")).toBe(true); + // Timestamp uses local time with timezone offset instead of UTC "Z" suffix + expect(firstArg).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2} \[EventQueue\]/, + ); vi.useRealTimers(); }); diff --git a/src/logging/console-timestamp.test.ts b/src/logging/console-timestamp.test.ts new file mode 100644 index 00000000000..5bc5aaf0083 --- /dev/null +++ b/src/logging/console-timestamp.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { formatConsoleTimestamp } from "./console.js"; + +describe("formatConsoleTimestamp", () => { + it("pretty style returns local HH:MM:SS", () => { + const result = formatConsoleTimestamp("pretty"); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + // Verify it uses local time, not UTC + const now = new Date(); + const expectedHour = String(now.getHours()).padStart(2, "0"); + expect(result.slice(0, 2)).toBe(expectedHour); + }); + + it("compact style returns local ISO-like timestamp with timezone offset", () => { + const result = formatConsoleTimestamp("compact"); + // Should match: YYYY-MM-DDTHH:MM:SS.mmm+HH:MM or -HH:MM + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/); + // Should NOT end with Z (UTC indicator) + expect(result).not.toMatch(/Z$/); + }); + + it("json style returns local ISO-like timestamp with timezone offset", () => { + const result = formatConsoleTimestamp("json"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/); + expect(result).not.toMatch(/Z$/); + }); + + it("timestamp contains the correct local date components", () => { + const before = new Date(); + const result = formatConsoleTimestamp("compact"); + const after = new Date(); + // The date portion should match the local date + const datePart = result.slice(0, 10); + const beforeDate = `${before.getFullYear()}-${String(before.getMonth() + 1).padStart(2, "0")}-${String(before.getDate()).padStart(2, "0")}`; + const afterDate = `${after.getFullYear()}-${String(after.getMonth() + 1).padStart(2, "0")}-${String(after.getDate()).padStart(2, "0")}`; + // Allow for date boundary crossing during test + expect([beforeDate, afterDate]).toContain(datePart); + }); +}); diff --git a/src/logging/console.ts b/src/logging/console.ts index dbff864ba2f..1e28391145c 100644 --- a/src/logging/console.ts +++ b/src/logging/console.ts @@ -135,16 +135,32 @@ function isEpipeError(err: unknown): boolean { return code === "EPIPE" || code === "EIO"; } -function formatConsoleTimestamp(style: ConsoleStyle): string { - const now = new Date().toISOString(); +export function formatConsoleTimestamp(style: ConsoleStyle): string { + const now = new Date(); if (style === "pretty") { - return now.slice(11, 19); + const h = String(now.getHours()).padStart(2, "0"); + const m = String(now.getMinutes()).padStart(2, "0"); + const s = String(now.getSeconds()).padStart(2, "0"); + return `${h}:${m}:${s}`; } - return now; + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const h = String(now.getHours()).padStart(2, "0"); + const m = String(now.getMinutes()).padStart(2, "0"); + const s = String(now.getSeconds()).padStart(2, "0"); + const ms = String(now.getMilliseconds()).padStart(3, "0"); + const tzOffset = now.getTimezoneOffset(); + const tzSign = tzOffset <= 0 ? "+" : "-"; + const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0"); + const tzMinutes = String(Math.abs(tzOffset) % 60).padStart(2, "0"); + return `${year}-${month}-${day}T${h}:${m}:${s}.${ms}${tzSign}${tzHours}:${tzMinutes}`; } function hasTimestampPrefix(value: string): boolean { - return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/.test(value); + return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)/.test( + value, + ); } function isJsonPayload(value: string): boolean {