147 lines
3.5 KiB
TypeScript
147 lines
3.5 KiB
TypeScript
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)}`),
|
|
);
|
|
}
|
|
},
|
|
};
|