mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
feat(logging): add file log correlation fields
This commit is contained in:
@@ -123,4 +123,37 @@ describe("file log redaction", () => {
|
||||
spanId: SPAN_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it("writes hostname and flattened message as top-level JSONL fields", () => {
|
||||
const logPath = logPathTracker.nextPath();
|
||||
setLoggerOverride({ level: "info", file: logPath });
|
||||
|
||||
getLogger().info({ route: "/api/health" }, "request completed");
|
||||
|
||||
const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n");
|
||||
const record = JSON.parse(line ?? "{}") as Record<string, unknown>;
|
||||
expect(record.hostname).toEqual(expect.any(String));
|
||||
expect(record.hostname).not.toBe("");
|
||||
expect(record.message).toBe("request completed");
|
||||
});
|
||||
|
||||
it("promotes agent, session, and channel context to top-level JSONL fields", () => {
|
||||
const logPath = logPathTracker.nextPath();
|
||||
setLoggerOverride({ level: "info", file: logPath });
|
||||
const logger = getChildLogger({
|
||||
agentId: "agent-main",
|
||||
messageProvider: "discord",
|
||||
});
|
||||
|
||||
logger.info({ sessionKey: "agent:main:discord:channel:c1" }, "session routed");
|
||||
|
||||
const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n");
|
||||
const record = JSON.parse(line ?? "{}") as Record<string, unknown>;
|
||||
expect(record).toMatchObject({
|
||||
agent_id: "agent-main",
|
||||
session_id: "agent:main:discord:channel:c1",
|
||||
channel: "discord",
|
||||
message: "session routed",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Logger as TsLogger } from "tslog";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
@@ -79,7 +80,10 @@ const MAX_DIAGNOSTIC_LOG_MESSAGE_CHARS = 4 * 1024;
|
||||
const MAX_DIAGNOSTIC_LOG_ATTRIBUTE_COUNT = 32;
|
||||
const MAX_DIAGNOSTIC_LOG_ATTRIBUTE_VALUE_CHARS = 2 * 1024;
|
||||
const MAX_DIAGNOSTIC_LOG_NAME_CHARS = 120;
|
||||
const MAX_FILE_LOG_MESSAGE_CHARS = 4 * 1024;
|
||||
const MAX_FILE_LOG_CONTEXT_VALUE_CHARS = 512;
|
||||
const DIAGNOSTIC_LOG_ATTRIBUTE_KEY_RE = /^[A-Za-z0-9_.:-]{1,64}$/u;
|
||||
const HOSTNAME = os.hostname() || "unknown";
|
||||
|
||||
type DiagnosticLogAttributes = Record<string, string | number | boolean>;
|
||||
|
||||
@@ -210,6 +214,75 @@ function getSortedNumericLogArgs(logObj: TsLogRecord): unknown[] {
|
||||
.map(([, value]) => value);
|
||||
}
|
||||
|
||||
function clampFileLogText(value: string, maxChars: number): string {
|
||||
return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value;
|
||||
}
|
||||
|
||||
function normalizeFileLogContextValue(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim();
|
||||
return normalized ? clampFileLogText(normalized, MAX_FILE_LOG_CONTEXT_VALUE_CHARS) : undefined;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readFirstContextString(
|
||||
sources: Array<Record<string, unknown> | undefined>,
|
||||
keys: readonly string[],
|
||||
): string | undefined {
|
||||
for (const source of sources) {
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
for (const key of keys) {
|
||||
const value = normalizeFileLogContextValue(source[key]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function stringifyFileLogMessagePart(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
return value.message || value.name;
|
||||
}
|
||||
if (isPlainLogRecordObject(value) && typeof value.message === "string") {
|
||||
return value.message;
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileLogMessage(numericArgs: readonly unknown[]): string | undefined {
|
||||
const parts = numericArgs
|
||||
.map(stringifyFileLogMessagePart)
|
||||
.filter((part): part is string => Boolean(part && part.trim()));
|
||||
if (parts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return clampFileLogText(parts.join(" "), MAX_FILE_LOG_MESSAGE_CHARS);
|
||||
}
|
||||
|
||||
function extractLogBindingPrefix(numericArgs: unknown[]): {
|
||||
bindings?: Record<string, unknown>;
|
||||
args: unknown[];
|
||||
@@ -265,6 +338,25 @@ function buildTraceFileLogFields(logObj: TsLogRecord): Record<string, string> |
|
||||
};
|
||||
}
|
||||
|
||||
function buildStructuredFileLogFields(logObj: TsLogRecord): Record<string, string> {
|
||||
const { bindings, args } = extractLogBindingPrefix(getSortedNumericLogArgs(logObj));
|
||||
const structuredArg = isPlainLogRecordObject(args[0]) ? args[0] : undefined;
|
||||
const sources = [structuredArg, bindings, logObj];
|
||||
const messageArgs =
|
||||
structuredArg && typeof structuredArg.message !== "string" ? args.slice(1) : args;
|
||||
const message = buildFileLogMessage(messageArgs);
|
||||
const agentId = readFirstContextString(sources, ["agent_id", "agentId"]);
|
||||
const sessionId = readFirstContextString(sources, ["session_id", "sessionId", "sessionKey"]);
|
||||
const channel = readFirstContextString(sources, ["channel", "messageProvider"]);
|
||||
return {
|
||||
hostname: HOSTNAME,
|
||||
...(message ? { message } : {}),
|
||||
...(agentId ? { agent_id: agentId } : {}),
|
||||
...(sessionId ? { session_id: sessionId } : {}),
|
||||
...(channel ? { channel } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDiagnosticLogRecord(logObj: TsLogRecord) {
|
||||
const meta = logObj._meta as
|
||||
| {
|
||||
@@ -447,7 +539,10 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
|
||||
}
|
||||
const time = formatTimestamp(logObj.date ?? new Date(), { style: "long" });
|
||||
const traceFields = buildTraceFileLogFields(logObj as TsLogRecord);
|
||||
const line = redactSensitiveText(JSON.stringify({ ...logObj, time, ...traceFields }));
|
||||
const structuredFields = buildStructuredFileLogFields(logObj as TsLogRecord);
|
||||
const line = redactSensitiveText(
|
||||
JSON.stringify({ ...logObj, time, ...structuredFields, ...traceFields }),
|
||||
);
|
||||
const payload = `${line}\n`;
|
||||
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
||||
const nextBytes = currentFileBytes + payloadBytes;
|
||||
|
||||
Reference in New Issue
Block a user