logging: use local time (with tz offset) everywhere instead of UTC

This commit is contained in:
Tarun Sukhani
2026-02-06 23:35:30 +08:00
parent 516459395c
commit f1753aa336
7 changed files with 54 additions and 24 deletions

View File

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

View File

@@ -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",

View File

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

View File

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

View File

@@ -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<LogObj> {
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 {

View File

@@ -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<string, unknown>;
@@ -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,

29
src/logging/timestamp.ts Normal file
View File

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