From f1753aa3369a508b18d428e8c76c1f0e2d672a89 Mon Sep 17 00:00:00 2001 From: Tarun Sukhani Date: Fri, 6 Feb 2026 23:35:30 +0800 Subject: [PATCH] logging: use local time (with tz offset) everywhere instead of UTC --- src/cli/logs-cli.ts | 16 +++++------- src/hooks/bundled/command-logger/handler.ts | 3 ++- src/hooks/bundled/session-memory/handler.ts | 18 +++++++------ src/logging/console.ts | 6 ++--- src/logging/logger.ts | 3 ++- src/logging/subsystem.ts | 3 ++- src/logging/timestamp.ts | 29 +++++++++++++++++++++ 7 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 src/logging/timestamp.ts diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 2fa8345814a..14e3df96bd7 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -2,7 +2,7 @@ import type { Command } from "commander"; import { setTimeout as delay } from "node:timers/promises"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { parseLogLine } from "../logging/parse-log-line.js"; -import { formatLocalIsoWithOffset } from "../logging/timestamps.js"; +import { formatLocalIso } from "../logging/timestamp.js"; import { formatDocsLink } from "../terminal/links.js"; import { clearActiveProgressLine } from "../terminal/progress-line.js"; import { createSafeStreamWriter } from "../terminal/stream-writer.js"; @@ -73,17 +73,13 @@ export function formatLogTimestamp( if (Number.isNaN(parsed.getTime())) { return value; } - - let timeString: string; - if (localTime) { - timeString = formatLocalIsoWithOffset(parsed); - } else { - timeString = parsed.toISOString(); - } if (mode === "pretty") { - return timeString.slice(11, 19); + const h = String(parsed.getHours()).padStart(2, "0"); + const m = String(parsed.getMinutes()).padStart(2, "0"); + const s = String(parsed.getSeconds()).padStart(2, "0"); + return `${h}:${m}:${s}`; } - return timeString; + return formatLocalIso(parsed); } function formatLogLine( diff --git a/src/hooks/bundled/command-logger/handler.ts b/src/hooks/bundled/command-logger/handler.ts index 0731296b0ff..ff77eb32c39 100644 --- a/src/hooks/bundled/command-logger/handler.ts +++ b/src/hooks/bundled/command-logger/handler.ts @@ -28,6 +28,7 @@ import os from "node:os"; import path from "node:path"; import type { HookHandler } from "../../hooks.js"; import { resolveStateDir } from "../../../config/paths.js"; +import { formatLocalIso } from "../../../logging/timestamp.js"; /** * Log all command events to a file @@ -48,7 +49,7 @@ const logCommand: HookHandler = async (event) => { const logFile = path.join(logDir, "commands.log"); const logLine = JSON.stringify({ - timestamp: event.timestamp.toISOString(), + timestamp: formatLocalIso(event.timestamp), action: event.action, sessionKey: event.sessionKey, senderId: event.context.senderId ?? "unknown", diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 187640eed7f..5683c21a390 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -13,6 +13,7 @@ import type { HookHandler } from "../../hooks.js"; import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js"; import { resolveStateDir } from "../../../config/paths.js"; import { createSubsystemLogger } from "../../../logging/subsystem.js"; +import { localDateStr, localTimeStr, tzOffsetLabel } from "../../../logging/timestamp.js"; import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js"; import { hasInterSessionUserProvenance } from "../../../sessions/input-provenance.js"; import { resolveHookConfig } from "../../config.js"; @@ -88,14 +89,15 @@ async function saveToLanceDB(params: { } // Format memory text with metadata and truncated content - const dateStr = timestamp.toISOString().split("T")[0]; - const timeStr = timestamp.toISOString().split("T")[1].split(".")[0]; + const dateStr = localDateStr(timestamp); + const timeStr = localTimeStr(timestamp); + const tz = tzOffsetLabel(timestamp); const truncatedContent = sessionContent.slice(0, 2000); const wasTruncated = sessionContent.length > 2000; const memoryText = [ `Session: ${slug}`, - `Date: ${dateStr} ${timeStr} UTC`, + `Date: ${dateStr} ${timeStr} ${tz}`, `Session Key: ${sessionKey}`, "", truncatedContent, @@ -149,7 +151,7 @@ const saveSessionToMemory: HookHandler = async (event) => { // Get today's date for filename const now = new Date(event.timestamp); - const dateStr = now.toISOString().split("T")[0]; // YYYY-MM-DD + const dateStr = localDateStr(now); // Generate descriptive slug from session using LLM const sessionEntry = (context.previousSessionEntry || context.sessionEntry || {}) as Record< @@ -206,7 +208,7 @@ const saveSessionToMemory: HookHandler = async (event) => { // If no slug, use timestamp if (!slug) { - const timeSlug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, ""); + const timeSlug = localTimeStr(now).replace(/:/g, ""); slug = timeSlug.slice(0, 4); // HHMM log.debug("Using fallback timestamp slug", { slug }); } @@ -243,8 +245,8 @@ const saveSessionToMemory: HookHandler = async (event) => { path: memoryFilePath.replace(os.homedir(), "~"), }); - // Format time as HH:MM:SS UTC - const timeStr = now.toISOString().split("T")[1].split(".")[0]; + const timeStr = localTimeStr(now); + const tz = tzOffsetLabel(now); // Extract context details const sessionId = (sessionEntry.sessionId as string) || "unknown"; @@ -252,7 +254,7 @@ const saveSessionToMemory: HookHandler = async (event) => { // Build Markdown entry const entryParts = [ - `# Session: ${dateStr} ${timeStr} UTC`, + `# Session: ${dateStr} ${timeStr} ${tz}`, "", `- **Session Key**: ${event.sessionKey}`, `- **Session ID**: ${sessionId}`, diff --git a/src/logging/console.ts b/src/logging/console.ts index 3454bb604ec..21a05010421 100644 --- a/src/logging/console.ts +++ b/src/logging/console.ts @@ -7,7 +7,7 @@ import { readLoggingConfig } from "./config.js"; import { type LogLevel, normalizeLogLevel } from "./levels.js"; import { getLogger, type LoggerSettings } from "./logger.js"; import { loggingState } from "./state.js"; -import { formatLocalIsoWithOffset } from "./timestamps.js"; +import { formatLocalIso } from "./timestamp.js"; export type ConsoleStyle = "pretty" | "compact" | "json"; type ConsoleSettings = { @@ -151,14 +151,14 @@ function isEpipeError(err: unknown): boolean { } export function formatConsoleTimestamp(style: ConsoleStyle): string { - const now = new Date(); if (style === "pretty") { + const now = new Date(); 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 formatLocalIsoWithOffset(now); + return formatLocalIso(); } function hasTimestampPrefix(value: string): boolean { diff --git a/src/logging/logger.ts b/src/logging/logger.ts index 8c5a4526d4a..46ef6883919 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -8,6 +8,7 @@ import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { readLoggingConfig } from "./config.js"; import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js"; import { loggingState } from "./state.js"; +import { formatLocalIso } from "./timestamp.js"; // When running under vitest, isolate logs to avoid polluting production logs. function getDefaultLogDir(): string { @@ -113,7 +114,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger { logger.attachTransport((logObj: LogObj) => { try { - const time = logObj.date?.toISOString?.() ?? new Date().toISOString(); + const time = formatLocalIso(logObj.date ?? new Date()); const line = JSON.stringify({ ...logObj, time }); fs.appendFileSync(settings.file, `${line}\n`, { encoding: "utf8" }); } catch { diff --git a/src/logging/subsystem.ts b/src/logging/subsystem.ts index 76174281d9c..766f06b5678 100644 --- a/src/logging/subsystem.ts +++ b/src/logging/subsystem.ts @@ -8,6 +8,7 @@ import { getConsoleSettings, shouldLogSubsystemToConsole } from "./console.js"; import { type LogLevel, levelToMinLevel } from "./levels.js"; import { getChildLogger, isFileLogLevelEnabled } from "./logger.js"; import { loggingState } from "./state.js"; +import { formatLocalIso } from "./timestamp.js"; type LogObj = { date?: Date } & Record; @@ -155,7 +156,7 @@ function formatConsoleLine(opts: { opts.style === "json" ? opts.subsystem : formatSubsystemForConsole(opts.subsystem); if (opts.style === "json") { return JSON.stringify({ - time: new Date().toISOString(), + time: formatLocalIso(), level: opts.level, subsystem: displaySubsystem, message: opts.message, diff --git a/src/logging/timestamp.ts b/src/logging/timestamp.ts new file mode 100644 index 00000000000..1e0c82cd290 --- /dev/null +++ b/src/logging/timestamp.ts @@ -0,0 +1,29 @@ +/** Format a Date as local ISO-8601 with milliseconds and timezone offset. */ +export function formatLocalIso(date?: Date): string { + const d = date ?? new Date(); + const p = (n: number, len = 2) => String(n).padStart(len, "0"); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? "+" : "-"; + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}${sign}${p(Math.floor(Math.abs(off) / 60))}:${p(Math.abs(off) % 60)}`; +} + +/** Extract local date portion (YYYY-MM-DD) from a Date. */ +export function localDateStr(date: Date): string { + const p = (n: number) => String(n).padStart(2, "0"); + return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`; +} + +/** Extract local time portion (HH:MM:SS) from a Date. */ +export function localTimeStr(date: Date): string { + const p = (n: number) => String(n).padStart(2, "0"); + return `${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`; +} + +/** Get timezone offset label like "+08:00" from a Date. */ +export function tzOffsetLabel(date?: Date): string { + const d = date ?? new Date(); + const p = (n: number) => String(n).padStart(2, "0"); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? "+" : "-"; + return `${sign}${p(Math.floor(Math.abs(off) / 60))}:${p(Math.abs(off) % 60)}`; +}