feat: add gateway logs tab
This commit is contained in:
150
src/gateway/server-methods/logs.ts
Normal file
150
src/gateway/server-methods/logs.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateLogsTailParams,
|
||||
} from "../protocol/index.js";
|
||||
import { getResolvedLoggerSettings } from "../../logging.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);
|
||||
let 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)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user