Skip to content

Commit fdbf8be

Browse files
authored
fix(logs-search): restored support for log search queries (#2417)
1 parent 6f4f4e2 commit fdbf8be

File tree

5 files changed

+156
-212
lines changed

5 files changed

+156
-212
lines changed

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

Lines changed: 82 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,22 @@ import {
66
workflowDeploymentVersion,
77
workflowExecutionLogs,
88
} from '@sim/db/schema'
9-
import { and, desc, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
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'
1025
import { type NextRequest, NextResponse } from 'next/server'
1126
import { z } from 'zod'
1227
import { getSession } from '@/lib/auth'
@@ -22,14 +37,19 @@ const QueryParamsSchema = z.object({
2237
limit: z.coerce.number().optional().default(100),
2338
offset: z.coerce.number().optional().default(0),
2439
level: z.string().optional(),
25-
workflowIds: z.string().optional(), // Comma-separated list of workflow IDs
26-
folderIds: z.string().optional(), // Comma-separated list of folder IDs
27-
triggers: z.string().optional(), // Comma-separated list of trigger types
40+
workflowIds: z.string().optional(),
41+
folderIds: z.string().optional(),
42+
triggers: z.string().optional(),
2843
startDate: z.string().optional(),
2944
endDate: z.string().optional(),
3045
search: z.string().optional(),
3146
workflowName: z.string().optional(),
3247
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(),
3353
workspaceId: z.string(),
3454
})
3555

@@ -49,7 +69,6 @@ export async function GET(request: NextRequest) {
4969
const { searchParams } = new URL(request.url)
5070
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
5171

52-
// Conditionally select columns based on detail level to optimize performance
5372
const selectColumns =
5473
params.details === 'full'
5574
? {
@@ -63,9 +82,9 @@ export async function GET(request: NextRequest) {
6382
startedAt: workflowExecutionLogs.startedAt,
6483
endedAt: workflowExecutionLogs.endedAt,
6584
totalDurationMs: workflowExecutionLogs.totalDurationMs,
66-
executionData: workflowExecutionLogs.executionData, // Large field - only in full mode
85+
executionData: workflowExecutionLogs.executionData,
6786
cost: workflowExecutionLogs.cost,
68-
files: workflowExecutionLogs.files, // Large field - only in full mode
87+
files: workflowExecutionLogs.files,
6988
createdAt: workflowExecutionLogs.createdAt,
7089
workflowName: workflow.name,
7190
workflowDescription: workflow.description,
@@ -82,7 +101,6 @@ export async function GET(request: NextRequest) {
82101
deploymentVersionName: workflowDeploymentVersion.name,
83102
}
84103
: {
85-
// Basic mode - exclude large fields for better performance
86104
id: workflowExecutionLogs.id,
87105
workflowId: workflowExecutionLogs.workflowId,
88106
executionId: workflowExecutionLogs.executionId,
@@ -93,9 +111,9 @@ export async function GET(request: NextRequest) {
93111
startedAt: workflowExecutionLogs.startedAt,
94112
endedAt: workflowExecutionLogs.endedAt,
95113
totalDurationMs: workflowExecutionLogs.totalDurationMs,
96-
executionData: sql<null>`NULL`, // Exclude large execution data in basic mode
114+
executionData: sql<null>`NULL`,
97115
cost: workflowExecutionLogs.cost,
98-
files: sql<null>`NULL`, // Exclude files in basic mode
116+
files: sql<null>`NULL`,
99117
createdAt: workflowExecutionLogs.createdAt,
100118
workflowName: workflow.name,
101119
workflowDescription: workflow.description,
@@ -109,7 +127,7 @@ export async function GET(request: NextRequest) {
109127
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
110128
pausedResumedCount: pausedExecutions.resumedCount,
111129
deploymentVersion: workflowDeploymentVersion.version,
112-
deploymentVersionName: sql<null>`NULL`, // Only needed in full mode for details panel
130+
deploymentVersionName: sql<null>`NULL`,
113131
}
114132

115133
const baseQuery = db
@@ -139,34 +157,28 @@ export async function GET(request: NextRequest) {
139157
)
140158
)
141159

142-
// Build additional conditions for the query
143160
let conditions: SQL | undefined
144161

145-
// Filter by level with support for derived statuses (running, pending)
146162
if (params.level && params.level !== 'all') {
147163
const levels = params.level.split(',').filter(Boolean)
148164
const levelConditions: SQL[] = []
149165

150166
for (const level of levels) {
151167
if (level === 'error') {
152-
// Direct database field
153168
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
154169
} else if (level === 'info') {
155-
// Completed info logs only (not running, not pending)
156170
const condition = and(
157171
eq(workflowExecutionLogs.level, 'info'),
158172
isNotNull(workflowExecutionLogs.endedAt)
159173
)
160174
if (condition) levelConditions.push(condition)
161175
} else if (level === 'running') {
162-
// Running logs: info level with no endedAt
163176
const condition = and(
164177
eq(workflowExecutionLogs.level, 'info'),
165178
isNull(workflowExecutionLogs.endedAt)
166179
)
167180
if (condition) levelConditions.push(condition)
168181
} else if (level === 'pending') {
169-
// Pending logs: info level with pause status indicators
170182
const condition = and(
171183
eq(workflowExecutionLogs.level, 'info'),
172184
or(
@@ -189,31 +201,27 @@ export async function GET(request: NextRequest) {
189201
}
190202
}
191203

192-
// Filter by specific workflow IDs
193204
if (params.workflowIds) {
194205
const workflowIds = params.workflowIds.split(',').filter(Boolean)
195206
if (workflowIds.length > 0) {
196207
conditions = and(conditions, inArray(workflow.id, workflowIds))
197208
}
198209
}
199210

200-
// Filter by folder IDs
201211
if (params.folderIds) {
202212
const folderIds = params.folderIds.split(',').filter(Boolean)
203213
if (folderIds.length > 0) {
204214
conditions = and(conditions, inArray(workflow.folderId, folderIds))
205215
}
206216
}
207217

208-
// Filter by triggers
209218
if (params.triggers) {
210219
const triggers = params.triggers.split(',').filter(Boolean)
211220
if (triggers.length > 0 && !triggers.includes('all')) {
212221
conditions = and(conditions, inArray(workflowExecutionLogs.trigger, triggers))
213222
}
214223
}
215224

216-
// Filter by date range
217225
if (params.startDate) {
218226
conditions = and(
219227
conditions,
@@ -224,33 +232,79 @@ export async function GET(request: NextRequest) {
224232
conditions = and(conditions, lte(workflowExecutionLogs.startedAt, new Date(params.endDate)))
225233
}
226234

227-
// Filter by search query
228235
if (params.search) {
229236
const searchTerm = `%${params.search}%`
230-
// With message removed, restrict search to executionId only
231237
conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}`)
232238
}
233239

234-
// Filter by workflow name (from advanced search input)
235240
if (params.workflowName) {
236241
const nameTerm = `%${params.workflowName}%`
237242
conditions = and(conditions, sql`${workflow.name} ILIKE ${nameTerm}`)
238243
}
239244

240-
// Filter by folder name (best-effort text match when present on workflows)
241245
if (params.folderName) {
242246
const folderTerm = `%${params.folderName}%`
243247
conditions = and(conditions, sql`${workflow.name} ILIKE ${folderTerm}`)
244248
}
245249

246-
// Execute the query using the optimized join
250+
if (params.executionId) {
251+
conditions = and(conditions, eq(workflowExecutionLogs.executionId, params.executionId))
252+
}
253+
254+
if (params.costOperator && params.costValue !== undefined) {
255+
const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric`
256+
switch (params.costOperator) {
257+
case '=':
258+
conditions = and(conditions, sql`${costField} = ${params.costValue}`)
259+
break
260+
case '>':
261+
conditions = and(conditions, sql`${costField} > ${params.costValue}`)
262+
break
263+
case '<':
264+
conditions = and(conditions, sql`${costField} < ${params.costValue}`)
265+
break
266+
case '>=':
267+
conditions = and(conditions, sql`${costField} >= ${params.costValue}`)
268+
break
269+
case '<=':
270+
conditions = and(conditions, sql`${costField} <= ${params.costValue}`)
271+
break
272+
case '!=':
273+
conditions = and(conditions, sql`${costField} != ${params.costValue}`)
274+
break
275+
}
276+
}
277+
278+
if (params.durationOperator && params.durationValue !== undefined) {
279+
const durationField = workflowExecutionLogs.totalDurationMs
280+
switch (params.durationOperator) {
281+
case '=':
282+
conditions = and(conditions, eq(durationField, params.durationValue))
283+
break
284+
case '>':
285+
conditions = and(conditions, gt(durationField, params.durationValue))
286+
break
287+
case '<':
288+
conditions = and(conditions, lt(durationField, params.durationValue))
289+
break
290+
case '>=':
291+
conditions = and(conditions, gte(durationField, params.durationValue))
292+
break
293+
case '<=':
294+
conditions = and(conditions, lte(durationField, params.durationValue))
295+
break
296+
case '!=':
297+
conditions = and(conditions, ne(durationField, params.durationValue))
298+
break
299+
}
300+
}
301+
247302
const logs = await baseQuery
248303
.where(conditions)
249304
.orderBy(desc(workflowExecutionLogs.startedAt))
250305
.limit(params.limit)
251306
.offset(params.offset)
252307

253-
// Get total count for pagination using the same join structure
254308
const countQuery = db
255309
.select({ count: sql<number>`count(*)` })
256310
.from(workflowExecutionLogs)
@@ -279,13 +333,10 @@ export async function GET(request: NextRequest) {
279333

280334
const count = countResult[0]?.count || 0
281335

282-
// Block executions are now extracted from trace spans instead of separate table
283336
const blockExecutionsByExecution: Record<string, any[]> = {}
284337

285-
// Create clean trace spans from block executions
286338
const createTraceSpans = (blockExecutions: any[]) => {
287339
return blockExecutions.map((block, index) => {
288-
// For error blocks, include error information in the output
289340
let output = block.outputData
290341
if (block.status === 'error' && block.errorMessage) {
291342
output = {
@@ -314,7 +365,6 @@ export async function GET(request: NextRequest) {
314365
})
315366
}
316367

317-
// Extract cost information from block executions
318368
const extractCostSummary = (blockExecutions: any[]) => {
319369
let totalCost = 0
320370
let totalInputCost = 0
@@ -333,7 +383,6 @@ export async function GET(request: NextRequest) {
333383
totalPromptTokens += block.cost.tokens?.prompt || 0
334384
totalCompletionTokens += block.cost.tokens?.completion || 0
335385

336-
// Track per-model costs
337386
if (block.cost.model) {
338387
if (!models.has(block.cost.model)) {
339388
models.set(block.cost.model, {
@@ -363,34 +412,29 @@ export async function GET(request: NextRequest) {
363412
prompt: totalPromptTokens,
364413
completion: totalCompletionTokens,
365414
},
366-
models: Object.fromEntries(models), // Convert Map to object for JSON serialization
415+
models: Object.fromEntries(models),
367416
}
368417
}
369418

370-
// Transform to clean log format with workflow data included
371419
const enhancedLogs = logs.map((log) => {
372420
const blockExecutions = blockExecutionsByExecution[log.executionId] || []
373421

374-
// Only process trace spans and detailed cost in full mode
375422
let traceSpans = []
376423
let finalOutput: any
377424
let costSummary = (log.cost as any) || { total: 0 }
378425

379426
if (params.details === 'full' && log.executionData) {
380-
// Use stored trace spans if available, otherwise create from block executions
381427
const storedTraceSpans = (log.executionData as any)?.traceSpans
382428
traceSpans =
383429
storedTraceSpans && Array.isArray(storedTraceSpans) && storedTraceSpans.length > 0
384430
? storedTraceSpans
385431
: createTraceSpans(blockExecutions)
386432

387-
// Prefer stored cost JSON; otherwise synthesize from blocks
388433
costSummary =
389434
log.cost && Object.keys(log.cost as any).length > 0
390435
? (log.cost as any)
391436
: extractCostSummary(blockExecutions)
392437

393-
// Include finalOutput if present on executionData
394438
try {
395439
const fo = (log.executionData as any)?.finalOutput
396440
if (fo !== undefined) finalOutput = fo

0 commit comments

Comments
 (0)