UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

202 lines (201 loc) 6.86 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { c as normalizeOptionalLowercaseString } from "./string-coerce-CIXf7egm.js"; import { r as isMissingPathError } from "./errno-CkbDOfLk.js"; import { _ as resolveRedactOptions, f as redactSensitiveLines } from "./redact-BtvPPfTi.js"; import { n as clamp } from "./utils-P__uGsPB.js"; import { g as isSameRollingLogFileFamily, h as isRollingLogFilePath, m as getResolvedLoggerFileTarget } from "./logger-DK-iouVT.js"; import { t as readFileWindowFully } from "./file-read-DtMn74uz.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/logging/parse-log-line.ts function extractMessage(value) { const parts = []; for (const key of Object.keys(value)) { if (!/^\d+$/.test(key)) continue; const item = value[key]; if (typeof item === "string") parts.push(item); else if (item != null) parts.push(JSON.stringify(item)); } return parts.join(" "); } function parseMetaName(raw) { if (typeof raw !== "string" || !raw.trimStart().startsWith("{")) return {}; try { const parsed = JSON.parse(raw); return { subsystem: typeof parsed.subsystem === "string" ? parsed.subsystem : void 0, module: typeof parsed.module === "string" ? parsed.module : void 0, plugin: typeof parsed.plugin === "string" ? parsed.plugin : void 0 }; } catch { return {}; } } function resolveContext(value, meta) { const metadataContext = parseMetaName(meta?.name); const positionalContext = parseMetaName(value["0"]); return { subsystem: metadataContext.subsystem ?? positionalContext.subsystem, module: metadataContext.module ?? positionalContext.module, plugin: metadataContext.plugin ?? positionalContext.plugin }; } /** Parses a raw log line into compact metadata and message text, or null for non-JSON lines. */ function parseLogLine(raw) { try { const parsed = JSON.parse(raw); if (!isRecord(parsed)) return null; const meta = isRecord(parsed["_meta"]) ? parsed["_meta"] : void 0; const context = resolveContext(parsed, meta); const levelRaw = typeof meta?.logLevelName === "string" ? meta.logLevelName : void 0; return { time: typeof parsed.time === "string" ? parsed.time : typeof meta?.date === "string" ? meta.date : void 0, level: normalizeOptionalLowercaseString(levelRaw), subsystem: context.subsystem, module: context.module, plugin: context.plugin, message: typeof parsed.message === "string" ? parsed.message : extractMessage(parsed), raw }; } catch { return null; } } //#endregion //#region src/logging/log-tail.ts const DEFAULT_LIMIT = 500; const DEFAULT_MAX_BYTES = 25e4; const MAX_LIMIT = 5e3; const MAX_BYTES = 1e6; function missingPathToNull(error) { if (!isMissingPathError(error)) throw error; return null; } /** Resolves a rolling daily log path to the newest existing rolling log when needed. */ async function resolveLogFile(file, options) { if (await fs.stat(file).catch(missingPathToNull)) return file; if (!(options?.rolling ?? isRollingLogFilePath(file))) return file; const dir = path.dirname(file); const entries = await fs.readdir(dir, { withFileTypes: true }).catch(missingPathToNull); if (!entries) return file; return (await Promise.all(entries.filter((entry) => entry.isFile() && isSameRollingLogFileFamily(file, entry.name)).map(async (entry) => { const fullPath = path.join(dir, entry.name); const fileStat = await fs.stat(fullPath).catch(missingPathToNull); return fileStat ? { path: fullPath, mtimeMs: fileStat.mtimeMs } : null; }))).filter((entry) => Boolean(entry)).toSorted((a, b) => b.mtimeMs - a.mtimeMs)[0]?.path ?? file; } async function readLogSlice(params) { const stat = await fs.stat(params.file).catch(missingPathToNull); if (!stat) return { cursor: 0, size: 0, lines: [], truncated: false, reset: false }; const size = stat.size; const maxBytes = clamp(params.maxBytes, 1, MAX_BYTES); const limit = clamp(params.limit, 1, MAX_LIMIT); let cursor = typeof params.cursor === "number" && Number.isFinite(params.cursor) ? Math.max(0, Math.floor(params.cursor)) : void 0; let reset = false; let skippedBytes; let truncated = false; let start; if (cursor != null) { if (cursor > size) { reset = true; start = Math.max(0, size - maxBytes); truncated = start > 0; } else { start = cursor; if (size - start > maxBytes) { reset = true; truncated = true; const boundedStart = Math.max(0, size - maxBytes); skippedBytes = boundedStart - start; start = boundedStart; } } } else { start = Math.max(0, size - maxBytes); truncated = start > 0; } if (size === 0 || size <= start) return { cursor: size, size, lines: [], truncated, reset, skippedBytes }; const handle = await fs.open(params.file, "r"); try { let prefix = ""; if (start > 0) { const prefixBuf = Buffer.alloc(1); const prefixRead = await handle.read(prefixBuf, 0, 1, start - 1); prefix = prefixBuf.toString("utf8", 0, prefixRead.bytesRead); } const length = Math.max(0, size - start); const buffer = Buffer.alloc(length); const bytesRead = await readFileWindowFully(handle, buffer, start); const text = buffer.toString("utf8", 0, bytesRead); let lines = text.split("\n"); lines.pop(); if (start > 0 && prefix !== "\n") lines.shift(); if (params.filter) lines = lines.filter(params.filter); if (lines.length > limit) { truncated = true; lines = lines.slice(lines.length - limit); } const lastNewline = buffer.subarray(0, bytesRead).lastIndexOf(10); cursor = text.endsWith("\n") ? size : start + lastNewline + 1; return { cursor, size, lines, truncated, reset, skippedBytes }; } finally { await handle.close(); } } /** Reads and redacts the configured log tail with bounded bytes and line count. */ async function readConfiguredLogTail(params, filter) { const target = getResolvedLoggerFileTarget(); const file = await resolveLogFile(target.file, { rolling: target.rolling }); const result = await readLogSlice({ file, cursor: params?.cursor, limit: params?.limit ?? DEFAULT_LIMIT, maxBytes: params?.maxBytes ?? DEFAULT_MAX_BYTES, filter }); const redaction = resolveRedactOptions(); return { file, ...result, lines: redactSensitiveLines(result.lines, redaction) }; } /** Reads the canonical configured tail and parses its already-redacted lines. */ async function readConfiguredParsedLogTail(params) { const tail = await readConfiguredLogTail(params, (raw) => { const parsed = parseLogLine(raw); return parsed !== null && (params?.filter?.(parsed) ?? true); }); return { ...tail, lines: tail.lines.flatMap((line) => { const parsed = parseLogLine(line); return parsed ? [parsed] : []; }) }; } //#endregion export { readConfiguredParsedLogTail as n, parseLogLine as r, readConfiguredLogTail as t };