import { json } from "@remix-run/node"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { $replica } from "~/db.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { LogsListOptionsSchema, LogsListPresenter, type LogLevel, } from "~/presenters/v3/LogsListPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { getCurrentPlan } from "~/services/platform.v3.server"; import { requireUser } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; // Valid log levels for filtering const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; function parseLevelsFromUrl(url: URL): LogLevel[] | undefined { const levelParams = url.searchParams.getAll("levels").filter((v) => v.length > 0); if (levelParams.length === 0) return undefined; return levelParams.filter((l): l is LogLevel => validLevels.includes(l as LogLevel)); } export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const userId = user.id; const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response("Project not found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response("Environment not found", { status: 404 }); } // Get the user's plan to determine log retention limit const plan = await getCurrentPlan(project.organizationId); const retentionLimitDays = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30; // Get filters from query params const url = new URL(request.url); const tasks = url.searchParams.getAll("tasks").filter((t) => t.length > 0); const runId = url.searchParams.get("runId") ?? undefined; const search = url.searchParams.get("search") ?? undefined; const cursor = url.searchParams.get("cursor") ?? undefined; const levels = parseLevelsFromUrl(url); const period = url.searchParams.get("period") ?? undefined; const fromStr = url.searchParams.get("from"); const toStr = url.searchParams.get("to"); let from = fromStr ? parseInt(fromStr, 10) : undefined; let to = toStr ? parseInt(toStr, 10) : undefined; if (Number.isNaN(from)) from = undefined; if (Number.isNaN(to)) to = undefined; const options = LogsListOptionsSchema.parse({ userId, projectId: project.id, tasks: tasks.length > 0 ? tasks : undefined, runId, search, cursor, period, from, to, levels, defaultPeriod: "1h", retentionLimitDays, }) as any; // Validated by LogsListOptionsSchema at runtime const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, "logs" ); const presenter = new LogsListPresenter($replica, logsClickhouse); const result = await presenter.call(project.organizationId, environment.id, options); return json({ logs: result.logs, pagination: result.pagination, }); };