import { dbReplica } from '@sim/db' import { workflow, workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { type DashboardStatsResponse, type SegmentStats, statsQueryParamsSchema, type WorkflowStats, } from '@/lib/api/contracts/logs' import { isZodError } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildFilterConditions } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('LogsStatsAPI') export const revalidate = 0 export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const session = await getSession() if (!session?.user?.id) { logger.warn(`[${requestId}] Unauthorized logs stats access attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id try { const { searchParams } = new URL(request.url) const params = statsQueryParamsSchema.parse(Object.fromEntries(searchParams.entries())) const access = await checkWorkspaceAccess(params.workspaceId, userId) if (!access.hasAccess) { return NextResponse.json( { workflows: [], aggregateSegments: [], totalRuns: 0, totalErrors: 0, avgLatency: 0, timeBounds: { start: new Date().toISOString(), end: new Date().toISOString() }, segmentMs: 0, } satisfies DashboardStatsResponse, { status: 200 } ) } const workspaceFilter = eq(workflowExecutionLogs.workspaceId, params.workspaceId) if (params.folderIds) { params.folderIds = await expandFolderIdsWithDescendants( params.workspaceId, params.folderIds ) } const commonFilters = buildFilterConditions(params, { useSimpleLevelFilter: true }) const whereCondition = commonFilters ? and(workspaceFilter, commonFilters) : workspaceFilter const boundsQuery = await dbReplica .select({ minTime: sql`MIN(${workflowExecutionLogs.startedAt})`, maxTime: sql`MAX(${workflowExecutionLogs.startedAt})`, }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) .where(whereCondition) const bounds = boundsQuery[0] const now = new Date() let startTime: Date let endTime: Date if (!bounds?.minTime || !bounds?.maxTime) { endTime = now startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) } else { startTime = new Date(bounds.minTime) endTime = new Date(Math.max(new Date(bounds.maxTime).getTime(), now.getTime())) } const totalMs = Math.max(1, endTime.getTime() - startTime.getTime()) const segmentMs = Math.max(60000, Math.floor(totalMs / params.segmentCount)) const startTimeIso = startTime.toISOString() const statsQuery = await dbReplica .select({ workflowId: sql`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`, workflowName: sql`COALESCE(${workflow.name}, 'Deleted Workflow')`, segmentIndex: sql`FLOOR(EXTRACT(EPOCH FROM (${workflowExecutionLogs.startedAt} - ${startTimeIso}::timestamp)) * 1000 / ${segmentMs})`.as( 'segment_index' ), totalExecutions: sql`COUNT(*)`.as('total_executions'), successfulExecutions: sql`COUNT(*) FILTER (WHERE ${workflowExecutionLogs.level} != 'error')`.as( 'successful_executions' ), avgDurationMs: sql`COALESCE(AVG(${workflowExecutionLogs.totalDurationMs}) FILTER (WHERE ${workflowExecutionLogs.totalDurationMs} > 0), 0)`.as( 'avg_duration_ms' ), }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) .where(whereCondition) .groupBy( sql`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`, sql`COALESCE(${workflow.name}, 'Deleted Workflow')`, sql`segment_index` ) const workflowMap = new Map< string, { workflowId: string workflowName: string segments: Map totalExecutions: number totalSuccessful: number } >() for (const row of statsQuery) { const segmentIndex = Math.min( params.segmentCount - 1, Math.max(0, Math.floor(Number(row.segmentIndex))) ) if (!workflowMap.has(row.workflowId)) { workflowMap.set(row.workflowId, { workflowId: row.workflowId, workflowName: row.workflowName, segments: new Map(), totalExecutions: 0, totalSuccessful: 0, }) } const wf = workflowMap.get(row.workflowId)! wf.totalExecutions += Number(row.totalExecutions) wf.totalSuccessful += Number(row.successfulExecutions) const existing = wf.segments.get(segmentIndex) if (existing) { const oldTotal = existing.totalExecutions const newTotal = oldTotal + Number(row.totalExecutions) existing.totalExecutions = newTotal existing.successfulExecutions += Number(row.successfulExecutions) existing.avgDurationMs = newTotal > 0 ? (existing.avgDurationMs * oldTotal + Number(row.avgDurationMs || 0) * Number(row.totalExecutions)) / newTotal : 0 } else { wf.segments.set(segmentIndex, { timestamp: new Date(startTime.getTime() + segmentIndex * segmentMs).toISOString(), totalExecutions: Number(row.totalExecutions), successfulExecutions: Number(row.successfulExecutions), avgDurationMs: Number(row.avgDurationMs || 0), }) } } const workflows: WorkflowStats[] = [] for (const wf of workflowMap.values()) { const segments: SegmentStats[] = [] for (let i = 0; i < params.segmentCount; i++) { const existing = wf.segments.get(i) if (existing) { segments.push(existing) } else { segments.push({ timestamp: new Date(startTime.getTime() + i * segmentMs).toISOString(), totalExecutions: 0, successfulExecutions: 0, avgDurationMs: 0, }) } } workflows.push({ workflowId: wf.workflowId, workflowName: wf.workflowName, segments, totalExecutions: wf.totalExecutions, totalSuccessful: wf.totalSuccessful, overallSuccessRate: wf.totalExecutions > 0 ? (wf.totalSuccessful / wf.totalExecutions) * 100 : 100, }) } workflows.sort((a, b) => { const errA = a.overallSuccessRate < 100 ? 1 - a.overallSuccessRate / 100 : 0 const errB = b.overallSuccessRate < 100 ? 1 - b.overallSuccessRate / 100 : 0 if (errA !== errB) return errB - errA return a.workflowName.localeCompare(b.workflowName) }) const aggregateSegments: SegmentStats[] = [] let totalRuns = 0 let totalErrors = 0 let weightedLatencySum = 0 let latencyCount = 0 for (let i = 0; i < params.segmentCount; i++) { let segTotal = 0 let segSuccess = 0 let segWeightedLatency = 0 let segLatencyCount = 0 for (const wf of workflows) { const seg = wf.segments[i] segTotal += seg.totalExecutions segSuccess += seg.successfulExecutions if (seg.avgDurationMs > 0 && seg.totalExecutions > 0) { segWeightedLatency += seg.avgDurationMs * seg.totalExecutions segLatencyCount += seg.totalExecutions } } totalRuns += segTotal totalErrors += segTotal - segSuccess weightedLatencySum += segWeightedLatency latencyCount += segLatencyCount aggregateSegments.push({ timestamp: new Date(startTime.getTime() + i * segmentMs).toISOString(), totalExecutions: segTotal, successfulExecutions: segSuccess, avgDurationMs: segLatencyCount > 0 ? segWeightedLatency / segLatencyCount : 0, }) } const avgLatency = latencyCount > 0 ? weightedLatencySum / latencyCount : 0 const response: DashboardStatsResponse = { workflows, aggregateSegments, totalRuns, totalErrors, avgLatency, timeBounds: { start: startTime.toISOString(), end: endTime.toISOString(), }, segmentMs, } return NextResponse.json(response, { status: 200 }) } catch (validationError) { if (isZodError(validationError)) { logger.warn(`[${requestId}] Invalid logs stats request parameters`, { errors: validationError.issues, }) return NextResponse.json( { error: 'Invalid request parameters', details: validationError.issues, }, { status: 400 } ) } throw validationError } } catch (error: any) { logger.error(`[${requestId}] logs stats fetch error`, error) return NextResponse.json({ error: error.message }, { status: 500 }) } })