Skip to content

Commit 40e30a1

Browse files
authored
improvement(logs): update logs export route to respect filters (#2550)
1 parent d1ebad9 commit 40e30a1

File tree

5 files changed

+283
-226
lines changed

5 files changed

+283
-226
lines changed

apps/sim/app/api/logs/export/route.ts

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import { db } from '@sim/db'
22
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
3-
import { and, desc, eq, gte, inArray, lte, type SQL, sql } from 'drizzle-orm'
3+
import { and, desc, eq } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
5-
import { z } from 'zod'
65
import { getSession } from '@/lib/auth'
76
import { createLogger } from '@/lib/logs/console/logger'
7+
import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters'
88

99
const logger = createLogger('LogsExportAPI')
1010

1111
export const revalidate = 0
1212

13-
const ExportParamsSchema = z.object({
14-
level: z.string().optional(),
15-
workflowIds: z.string().optional(),
16-
folderIds: z.string().optional(),
17-
triggers: z.string().optional(),
18-
startDate: z.string().optional(),
19-
endDate: z.string().optional(),
20-
search: z.string().optional(),
21-
workflowName: z.string().optional(),
22-
folderName: z.string().optional(),
23-
workspaceId: z.string(),
24-
})
25-
2613
function escapeCsv(value: any): string {
2714
if (value === null || value === undefined) return ''
2815
const str = String(value)
@@ -41,7 +28,7 @@ export async function GET(request: NextRequest) {
4128

4229
const userId = session.user.id
4330
const { searchParams } = new URL(request.url)
44-
const params = ExportParamsSchema.parse(Object.fromEntries(searchParams.entries()))
31+
const params = LogFilterParamsSchema.parse(Object.fromEntries(searchParams.entries()))
4532

4633
const selectColumns = {
4734
id: workflowExecutionLogs.id,
@@ -57,53 +44,11 @@ export async function GET(request: NextRequest) {
5744
workflowName: workflow.name,
5845
}
5946

60-
let conditions: SQL | undefined = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
61-
62-
if (params.level && params.level !== 'all') {
63-
const levels = params.level.split(',').filter(Boolean)
64-
if (levels.length === 1) {
65-
conditions = and(conditions, eq(workflowExecutionLogs.level, levels[0]))
66-
} else if (levels.length > 1) {
67-
conditions = and(conditions, inArray(workflowExecutionLogs.level, levels))
68-
}
69-
}
70-
71-
if (params.workflowIds) {
72-
const workflowIds = params.workflowIds.split(',').filter(Boolean)
73-
if (workflowIds.length > 0) conditions = and(conditions, inArray(workflow.id, workflowIds))
74-
}
75-
76-
if (params.folderIds) {
77-
const folderIds = params.folderIds.split(',').filter(Boolean)
78-
if (folderIds.length > 0) conditions = and(conditions, inArray(workflow.folderId, folderIds))
79-
}
80-
81-
if (params.triggers) {
82-
const triggers = params.triggers.split(',').filter(Boolean)
83-
if (triggers.length > 0 && !triggers.includes('all')) {
84-
conditions = and(conditions, inArray(workflowExecutionLogs.trigger, triggers))
85-
}
86-
}
87-
88-
if (params.startDate) {
89-
conditions = and(conditions, gte(workflowExecutionLogs.startedAt, new Date(params.startDate)))
90-
}
91-
if (params.endDate) {
92-
conditions = and(conditions, lte(workflowExecutionLogs.startedAt, new Date(params.endDate)))
93-
}
94-
95-
if (params.search) {
96-
const term = `%${params.search}%`
97-
conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${term}`)
98-
}
99-
if (params.workflowName) {
100-
const nameTerm = `%${params.workflowName}%`
101-
conditions = and(conditions, sql`${workflow.name} ILIKE ${nameTerm}`)
102-
}
103-
if (params.folderName) {
104-
const folderTerm = `%${params.folderName}%`
105-
conditions = and(conditions, sql`${workflow.name} ILIKE ${folderTerm}`)
106-
}
47+
const workspaceCondition = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
48+
const filterConditions = buildFilterConditions(params)
49+
const conditions = filterConditions
50+
? and(workspaceCondition, filterConditions)
51+
: workspaceCondition
10752

10853
const header = [
10954
'startedAt',

apps/sim/app/api/logs/route.ts

Lines changed: 8 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,22 @@ import {
66
workflowDeploymentVersion,
77
workflowExecutionLogs,
88
} from '@sim/db/schema'
9-
import {
10-
and,
11-
desc,
12-
eq,
13-
gt,
14-
gte,
15-
inArray,
16-
isNotNull,
17-
isNull,
18-
lt,
19-
lte,
20-
ne,
21-
or,
22-
type SQL,
23-
sql,
24-
} from 'drizzle-orm'
9+
import { and, desc, eq, isNotNull, isNull, or, type SQL, sql } from 'drizzle-orm'
2510
import { type NextRequest, NextResponse } from 'next/server'
2611
import { z } from 'zod'
2712
import { getSession } from '@/lib/auth'
2813
import { generateRequestId } from '@/lib/core/utils/request'
2914
import { createLogger } from '@/lib/logs/console/logger'
15+
import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters'
3016

3117
const logger = createLogger('LogsAPI')
3218

3319
export const revalidate = 0
3420

35-
const QueryParamsSchema = z.object({
21+
const QueryParamsSchema = LogFilterParamsSchema.extend({
3622
details: z.enum(['basic', 'full']).optional().default('basic'),
3723
limit: z.coerce.number().optional().default(100),
3824
offset: z.coerce.number().optional().default(0),
39-
level: z.string().optional(),
40-
workflowIds: z.string().optional(),
41-
folderIds: z.string().optional(),
42-
triggers: z.string().optional(),
43-
startDate: z.string().optional(),
44-
endDate: z.string().optional(),
45-
search: z.string().optional(),
46-
workflowName: z.string().optional(),
47-
folderName: z.string().optional(),
48-
executionId: z.string().optional(),
49-
costOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
50-
costValue: z.coerce.number().optional(),
51-
durationOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
52-
durationValue: z.coerce.number().optional(),
53-
workspaceId: z.string(),
5425
})
5526

5627
export async function GET(request: NextRequest) {
@@ -197,102 +168,11 @@ export async function GET(request: NextRequest) {
197168
}
198169
}
199170

200-
if (params.workflowIds) {
201-
const workflowIds = params.workflowIds.split(',').filter(Boolean)
202-
if (workflowIds.length > 0) {
203-
conditions = and(conditions, inArray(workflow.id, workflowIds))
204-
}
205-
}
206-
207-
if (params.folderIds) {
208-
const folderIds = params.folderIds.split(',').filter(Boolean)
209-
if (folderIds.length > 0) {
210-
conditions = and(conditions, inArray(workflow.folderId, folderIds))
211-
}
212-
}
213-
214-
if (params.triggers) {
215-
const triggers = params.triggers.split(',').filter(Boolean)
216-
if (triggers.length > 0 && !triggers.includes('all')) {
217-
conditions = and(conditions, inArray(workflowExecutionLogs.trigger, triggers))
218-
}
219-
}
220-
221-
if (params.startDate) {
222-
conditions = and(
223-
conditions,
224-
gte(workflowExecutionLogs.startedAt, new Date(params.startDate))
225-
)
226-
}
227-
if (params.endDate) {
228-
conditions = and(conditions, lte(workflowExecutionLogs.startedAt, new Date(params.endDate)))
229-
}
230-
231-
if (params.search) {
232-
const searchTerm = `%${params.search}%`
233-
conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}`)
234-
}
235-
236-
if (params.workflowName) {
237-
const nameTerm = `%${params.workflowName}%`
238-
conditions = and(conditions, sql`${workflow.name} ILIKE ${nameTerm}`)
239-
}
240-
241-
if (params.folderName) {
242-
const folderTerm = `%${params.folderName}%`
243-
conditions = and(conditions, sql`${workflow.name} ILIKE ${folderTerm}`)
244-
}
245-
246-
if (params.executionId) {
247-
conditions = and(conditions, eq(workflowExecutionLogs.executionId, params.executionId))
248-
}
249-
250-
if (params.costOperator && params.costValue !== undefined) {
251-
const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric`
252-
switch (params.costOperator) {
253-
case '=':
254-
conditions = and(conditions, sql`${costField} = ${params.costValue}`)
255-
break
256-
case '>':
257-
conditions = and(conditions, sql`${costField} > ${params.costValue}`)
258-
break
259-
case '<':
260-
conditions = and(conditions, sql`${costField} < ${params.costValue}`)
261-
break
262-
case '>=':
263-
conditions = and(conditions, sql`${costField} >= ${params.costValue}`)
264-
break
265-
case '<=':
266-
conditions = and(conditions, sql`${costField} <= ${params.costValue}`)
267-
break
268-
case '!=':
269-
conditions = and(conditions, sql`${costField} != ${params.costValue}`)
270-
break
271-
}
272-
}
273-
274-
if (params.durationOperator && params.durationValue !== undefined) {
275-
const durationField = workflowExecutionLogs.totalDurationMs
276-
switch (params.durationOperator) {
277-
case '=':
278-
conditions = and(conditions, eq(durationField, params.durationValue))
279-
break
280-
case '>':
281-
conditions = and(conditions, gt(durationField, params.durationValue))
282-
break
283-
case '<':
284-
conditions = and(conditions, lt(durationField, params.durationValue))
285-
break
286-
case '>=':
287-
conditions = and(conditions, gte(durationField, params.durationValue))
288-
break
289-
case '<=':
290-
conditions = and(conditions, lte(durationField, params.durationValue))
291-
break
292-
case '!=':
293-
conditions = and(conditions, ne(durationField, params.durationValue))
294-
break
295-
}
171+
// Apply common filters (workflowIds, folderIds, triggers, dates, search, cost, duration)
172+
// Level filtering is handled above with advanced running/pending state logic
173+
const commonFilters = buildFilterConditions(params, { useSimpleLevelFilter: false })
174+
if (commonFilters) {
175+
conditions = and(conditions, commonFilters)
296176
}
297177

298178
const logs = await baseQuery

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { AlertCircle, Loader2 } from 'lucide-react'
55
import { useParams } from 'next/navigation'
66
import { cn } from '@/lib/core/utils/cn'
7+
import { getStartDateFromTimeRange } from '@/lib/logs/filters'
78
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
89
import { useFolders } from '@/hooks/queries/folders'
910
import { useDashboardLogs, useLogDetail, useLogsList } from '@/hooks/queries/logs'
@@ -262,6 +263,11 @@ export default function Logs() {
262263
if (workflowIds.length > 0) params.set('workflowIds', workflowIds.join(','))
263264
if (folderIds.length > 0) params.set('folderIds', folderIds.join(','))
264265

266+
const startDate = getStartDateFromTimeRange(timeRange)
267+
if (startDate) {
268+
params.set('startDate', startDate.toISOString())
269+
}
270+
265271
const parsed = parseQuery(debouncedSearchQuery)
266272
const extra = queryToApiParams(parsed)
267273
Object.entries(extra).forEach(([k, v]) => params.set(k, v))

apps/sim/hooks/queries/logs.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
2+
import { getStartDateFromTimeRange } from '@/lib/logs/filters'
23
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
3-
import type { LogsResponse, WorkflowLog } from '@/stores/logs/filters/types'
4+
import type { LogsResponse, TimeRange, WorkflowLog } from '@/stores/logs/filters/types'
45

56
export const logKeys = {
67
all: ['logs'] as const,
@@ -14,7 +15,7 @@ export const logKeys = {
1415
}
1516

1617
interface LogFilters {
17-
timeRange: string
18+
timeRange: TimeRange
1819
level: string
1920
workflowIds: string[]
2021
folderIds: string[]
@@ -23,39 +24,6 @@ interface LogFilters {
2324
limit: number
2425
}
2526

26-
/**
27-
* Calculates start date from a time range string.
28-
* Returns null for 'All time' to indicate no date filtering.
29-
*/
30-
function getStartDateFromTimeRange(timeRange: string): Date | null {
31-
if (timeRange === 'All time') return null
32-
33-
const now = new Date()
34-
35-
switch (timeRange) {
36-
case 'Past 30 minutes':
37-
return new Date(now.getTime() - 30 * 60 * 1000)
38-
case 'Past hour':
39-
return new Date(now.getTime() - 60 * 60 * 1000)
40-
case 'Past 6 hours':
41-
return new Date(now.getTime() - 6 * 60 * 60 * 1000)
42-
case 'Past 12 hours':
43-
return new Date(now.getTime() - 12 * 60 * 60 * 1000)
44-
case 'Past 24 hours':
45-
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
46-
case 'Past 3 days':
47-
return new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
48-
case 'Past 7 days':
49-
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
50-
case 'Past 14 days':
51-
return new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
52-
case 'Past 30 days':
53-
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
54-
default:
55-
return new Date(0)
56-
}
57-
}
58-
5927
/**
6028
* Applies common filter parameters to a URLSearchParams object.
6129
* Shared between paginated and non-paginated log fetches.

0 commit comments

Comments
 (0)