import fs from "node:fs/promises"; import { getResolvedLoggerSettings } from "../../logging.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateLogsTailParams, } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; const DEFAULT_LIMIT = 500; const DEFAULT_MAX_BYTES = 250_000; const MAX_LIMIT = 5000; const MAX_BYTES = 1_000_000; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } async function readLogSlice(params: { file: string; cursor?: number; limit: number; maxBytes: number; }) { const stat = await fs.stat(params.file).catch(() => null); if (!stat) { return { cursor: 0, size: 0, lines: [] as string[], 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)) : undefined; let reset = false; let truncated = false; let start = 0; 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; start = Math.max(0, size - maxBytes); } } } else { start = Math.max(0, size - maxBytes); truncated = start > 0; } if (size === 0 || size <= start) { return { cursor: size, size, lines: [] as string[], truncated, reset, }; } 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 readResult = await handle.read(buffer, 0, length, start); const text = buffer.toString("utf8", 0, readResult.bytesRead); let lines = text.split("\n"); if (start > 0 && prefix !== "\n") { lines = lines.slice(1); } if (lines.length > 0 && lines[lines.length - 1] === "") { lines = lines.slice(0, -1); } if (lines.length > limit) { lines = lines.slice(lines.length - limit); } cursor = size; return { cursor, size, lines, truncated, reset, }; } finally { await handle.close(); } } export const logsHandlers: GatewayRequestHandlers = { "logs.tail": async ({ params, respond }) => { if (!validateLogsTailParams(params)) { respond( false, undefined, errorShape( ErrorCodes.INVALID_REQUEST, `invalid logs.tail params: ${formatValidationErrors(validateLogsTailParams.errors)}`, ), ); return; } const p = params as { cursor?: number; limit?: number; maxBytes?: number }; const file = getResolvedLoggerSettings().file; try { const result = await readLogSlice({ file, cursor: p.cursor, limit: p.limit ?? DEFAULT_LIMIT, maxBytes: p.maxBytes ?? DEFAULT_MAX_BYTES, }); respond(true, { file, ...result }, undefined); } catch (err) { respond( false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `log read failed: ${String(err)}`), ); } }, };