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 <vincentkoc@ieee.org>
This commit is contained in:
Liu Yuan
2026-03-02 23:57:03 +08:00
committed by GitHub
parent 82247f09a7
commit ade46d8ab7
3 changed files with 48 additions and 2 deletions

View File

@@ -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.

View File

@@ -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$/);
});
});

View File

@@ -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<LogObj> {
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<LogObj> {
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}`,