From ade46d8ab70c26325b0f49b468fdb63205bc9be8 Mon Sep 17 00:00:00 2001 From: Liu Yuan Date: Mon, 2 Mar 2026 23:57:03 +0800 Subject: [PATCH] fix(logging): log timestamps use local time instead of UTC (#28434) * fix(logging): log timestamps use local time instead of UTC Problem: Log timestamps used UTC, but docs say they should use host local timezone * test(logging): add test for logger timestamp format Verify logger uses local time (not UTC) in file logs * changelog: note logger timestamp local-time fix --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + src/logging/logger-timestamp.test.ts | 44 ++++++++++++++++++++++++++++ src/logging/logger.ts | 5 ++-- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/logging/logger-timestamp.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6d7b2d9..218634862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3. +- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy. - Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (`monitor.account-scope.test.ts`) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3. - Security/Web tools SSRF guard: keep DNS pinning for untrusted `web_fetch` and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting. - Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded `/api/channels/*` variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting. diff --git a/src/logging/logger-timestamp.test.ts b/src/logging/logger-timestamp.test.ts new file mode 100644 index 000000000..3634a9a08 --- /dev/null +++ b/src/logging/logger-timestamp.test.ts @@ -0,0 +1,44 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getLogger, resetLogger, setLoggerOverride } from "../logging.js"; + +describe("logger timestamp format", () => { + let logPath = ""; + + beforeEach(() => { + logPath = path.join(os.tmpdir(), `openclaw-log-ts-${crypto.randomUUID()}.log`); + resetLogger(); + setLoggerOverride(null); + }); + + afterEach(() => { + resetLogger(); + setLoggerOverride(null); + try { + fs.rmSync(logPath, { force: true }); + } catch { + // ignore cleanup errors + } + }); + + it("uses local time format in file logs (not UTC)", () => { + setLoggerOverride({ level: "info", file: logPath }); + const logger = getLogger(); + + // Write a log entry + logger.info("test-timestamp-format"); + + // Read the log file + const content = fs.readFileSync(logPath, "utf8"); + const lines = content.trim().split("\n"); + const lastLine = JSON.parse(lines[lines.length - 1]); + + // Should use local time format like "2026-02-27T15:04:00.000+08:00" + // NOT UTC format like "2026-02-27T07:04:00.000Z" + expect(lastLine.time).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/); + expect(lastLine.time).not.toMatch(/Z$/); + }); +}); diff --git a/src/logging/logger.ts b/src/logging/logger.ts index ebe552a66..074058051 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -9,6 +9,7 @@ import { resolveEnvLogLevelOverride } from "./env-log-level.js"; import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js"; import { resolveNodeRequireFromMeta } from "./node-require.js"; import { loggingState } from "./state.js"; +import { formatLocalIsoWithOffset } from "./timestamps.js"; export const DEFAULT_LOG_DIR = resolvePreferredOpenClawTmpDir(); export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // legacy single-file path @@ -113,7 +114,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger { logger.attachTransport((logObj: LogObj) => { try { - const time = logObj.date?.toISOString?.() ?? new Date().toISOString(); + const time = formatLocalIsoWithOffset(logObj.date ?? new Date()); const line = JSON.stringify({ ...logObj, time }); const payload = `${line}\n`; const payloadBytes = Buffer.byteLength(payload, "utf8"); @@ -122,7 +123,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger { if (!warnedAboutSizeCap) { warnedAboutSizeCap = true; const warningLine = JSON.stringify({ - time: new Date().toISOString(), + time: formatLocalIsoWithOffset(new Date()), level: "warn", subsystem: "logging", message: `log file size cap reached; suppressing writes file=${settings.file} maxFileBytes=${settings.maxFileBytes}`,