Skip to content

Commit 0de5d37

Browse files
lsr911claudesteipete
authored
fix(logging): keep bounded log text UTF-16 safe (#102560)
* fix(logging): use truncateUtf16Safe in diagnostic log text clamp Replace naive .slice(0, maxChars) with truncateUtf16Safe() in clampDiagnosticLogText() — the core helper used by all diagnostic log output (sanitizeDiagnosticLogText, formatDiagnosticAttributes, etc.). This prevents surrogate pair splitting across the entire diagnostic logging subsystem. Co-Authored-By: Claude <[email protected]> * fix(logging): preserve UTF-16 in bounded log text --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent f7cc6eb commit 0de5d37

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

src/logging/diagnostic-log-events.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,23 @@ describe("diagnostic log events", () => {
143143
expect(Object.hasOwn(event, "argsJson")).toBe(false);
144144
});
145145

146+
it("keeps bounded diagnostic messages UTF-16 safe", async () => {
147+
const received: Array<Extract<DiagnosticEventPayload, { type: "log.record" }>> = [];
148+
const unsubscribe = onInternalDiagnosticEvent((event) => {
149+
if (event.type === "log.record") {
150+
received.push(event);
151+
}
152+
});
153+
const prefix = "x".repeat(4_095);
154+
155+
getChildLogger({ subsystem: "diagnostic" }).info(`${prefix}😀tail`);
156+
await flushDiagnosticEvents();
157+
unsubscribe();
158+
159+
// The post-redaction bound keeps the first dot from the initial truncation marker.
160+
expect(received.at(-1)?.message).toBe(`${prefix}....(truncated)`);
161+
});
162+
146163
it("drops sensitive, blocked, and excess log attribute keys without copying large objects", async () => {
147164
const received: Array<Extract<DiagnosticEventPayload, { type: "log.record" }>> = [];
148165
const unsubscribe = onInternalDiagnosticEvent((evt) => {

src/logging/logger-redaction-behavior.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ describe("file log redaction", () => {
192192
expect(record.message).toBe("request completed");
193193
});
194194

195+
it("keeps bounded file-log messages UTF-16 safe", () => {
196+
const logPath = logPathTracker.nextPath();
197+
setLoggerOverride({ level: "info", file: logPath });
198+
const prefix = "x".repeat(4_095);
199+
200+
getLogger().info(`${prefix}😀tail`);
201+
202+
const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n");
203+
const record = JSON.parse(line ?? "{}") as Record<string, unknown>;
204+
expect(record.message).toBe(`${prefix}...(truncated)`);
205+
});
206+
195207
it("retries hostname resolution after an empty value and caches the first real value", () => {
196208
const logPath = logPathTracker.nextPath();
197209
setLoggerOverride({ level: "info", file: logPath });

src/logging/logger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
5+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
56
import { Logger as TsLogger } from "tslog";
67
import type { OpenClawConfig } from "../config/types.js";
78
import {
@@ -90,7 +91,7 @@ let cachedHostname: string | null = null;
9091
type DiagnosticLogAttributes = Record<string, string | number | boolean>;
9192

9293
function clampDiagnosticLogText(value: string, maxChars: number): string {
93-
return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value;
94+
return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value;
9495
}
9596

9697
function sanitizeDiagnosticLogText(value: string, maxChars: number): string {
@@ -217,7 +218,7 @@ function getSortedNumericLogArgs(logObj: TsLogRecord): unknown[] {
217218
}
218219

219220
function clampFileLogText(value: string, maxChars: number): string {
220-
return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value;
221+
return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value;
221222
}
222223

223224
function normalizeFileLogContextValue(value: unknown): string | undefined {

0 commit comments

Comments
 (0)