| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { NextRequest, NextResponse } from "next/server";
|
| import { readFileSync, existsSync } from "fs";
|
| import { getAppLogFilePath } from "@/lib/logEnv";
|
| import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
| import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
|
|
|
|
| const NUMERIC_LEVEL_MAP: Record<number, string> = {
|
| 10: "trace",
|
| 20: "debug",
|
| 30: "info",
|
| 40: "warn",
|
| 50: "error",
|
| 60: "fatal",
|
| };
|
|
|
| function parseLevel(raw: string | number): string {
|
| if (typeof raw === "number") {
|
| return NUMERIC_LEVEL_MAP[raw] || "info";
|
| }
|
| return String(raw).toLowerCase();
|
| }
|
|
|
| function stringifyLogValue(value: unknown): string {
|
| if (value === undefined || value === null) return "";
|
| if (typeof value === "string") return value;
|
| if (value instanceof Error) return value.message || value.name;
|
| if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
| return String(value);
|
| }
|
| try {
|
| const json = JSON.stringify(value);
|
| return typeof json === "string" ? json : String(value);
|
| } catch {
|
| return String(value);
|
| }
|
| }
|
|
|
| |
| |
|
|
| export async function GET(request: NextRequest) {
|
| const authError = await requireCliToolsAuth(request);
|
| if (authError) return authError;
|
|
|
| try {
|
| const { searchParams } = new URL(request.url);
|
|
|
|
|
|
|
| const _follow = searchParams.get("follow") === "true";
|
|
|
|
|
| const filterRaw = searchParams.get("filter") || "";
|
| const filterTokens = filterRaw
|
| .split(",")
|
| .map((t) => t.trim().toLowerCase())
|
| .filter(Boolean);
|
|
|
| const rawLimit = parseInt(searchParams.get("limit") || "500", 10);
|
| const limit = Math.min(Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 500, 2000);
|
|
|
| const logPath = getAppLogFilePath();
|
|
|
| if (!existsSync(logPath)) {
|
| return NextResponse.json([], { status: 200 });
|
| }
|
|
|
| const raw = readFileSync(logPath, "utf-8");
|
| const lines = raw.trim().split("\n").filter(Boolean);
|
|
|
| const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
| const entries: Record<string, unknown>[] = [];
|
|
|
| for (const line of lines) {
|
| try {
|
| const entry = JSON.parse(line) as Record<string, unknown>;
|
|
|
|
|
| const ts = entry.time || entry.timestamp;
|
| if (ts) {
|
| const entryTime = new Date(ts as string | number).getTime();
|
| if (entryTime < oneHourAgo) continue;
|
| }
|
|
|
|
|
| entry.level = parseLevel(entry.level as string | number);
|
| entry.msg = stringifyLogValue(entry.msg ?? entry.message ?? "");
|
| entry.message = stringifyLogValue(entry.message ?? entry.msg);
|
| if (entry.component !== undefined) entry.component = stringifyLogValue(entry.component);
|
| if (entry.module !== undefined) entry.module = stringifyLogValue(entry.module);
|
|
|
|
|
| if (entry.time && !entry.timestamp) {
|
| entry.timestamp = entry.time;
|
| }
|
|
|
|
|
|
|
| if (filterTokens.length > 0) {
|
| const haystack = [
|
| String(entry.component || ""),
|
| String(entry.module || ""),
|
| String(entry.msg || ""),
|
| ]
|
| .join(" ")
|
| .toLowerCase();
|
|
|
| const matches = filterTokens.some((token) => haystack.includes(token));
|
| if (!matches) continue;
|
| }
|
|
|
| entries.push(entry);
|
| } catch {
|
|
|
| }
|
| }
|
|
|
|
|
| const result = entries.slice(-limit);
|
|
|
| return NextResponse.json(result, {
|
| status: 200,
|
| headers: {
|
| "Cache-Control": "no-store, no-cache, must-revalidate",
|
| },
|
| });
|
| } catch (err: unknown) {
|
| const message = err instanceof Error ? err.message : String(err);
|
| return NextResponse.json(
|
| { error: sanitizeErrorMessage(message) || "Failed to read logs" },
|
| { status: 500 }
|
| );
|
| }
|
| }
|
|
|