Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
improvement(logs): object storage backed tracespans#4787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
afef76d3243feb3f19076a49e53575dbed5d957a8ed5d08af1e8995bb7ce4c3c5607885e0ab8d51cc9a30014218File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,7 +4,9 @@ import { createLogger } from '@sim/logger' | ||
| import { and, desc, eq, sql } from 'drizzle-orm' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { materializeExecutionData } from '@/lib/logs/execution/trace-store' | ||
| import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' | ||
| import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' | ||
| @@ -41,7 +43,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| startedAt: workflowExecutionLogs.startedAt, | ||
| endedAt: workflowExecutionLogs.endedAt, | ||
| totalDurationMs: workflowExecutionLogs.totalDurationMs, | ||
| cost: workflowExecutionLogs.cost, | ||
| costTotal: workflowExecutionLogs.costTotal, | ||
| executionData: workflowExecutionLogs.executionData, | ||
| workflowName: sql<string>`COALESCE(${workflow.name}, 'Deleted Workflow')`, | ||
| } | ||
| @@ -96,32 +98,55 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| if (!rows.length) break | ||
| for (const r of rows as any[]) { | ||
| // Heavy execution data may live in object storage; materialize per | ||
| // row with bounded concurrency so a 1000-row page doesn't fan out | ||
| // into 1000 simultaneous reads. | ||
| const materialized = await mapWithConcurrency( | ||
| rows as any[], | ||
| MATERIALIZE_CONCURRENCY, | ||
| (r) => | ||
| materializeExecutionData(r.executionData as Record<string, unknown> | null, { | ||
| workspaceId: params.workspaceId, | ||
| workflowId: r.workflowId, | ||
| executionId: r.executionId, | ||
| }) | ||
| ) | ||
| for (let j = 0; j < rows.length; j++) { | ||
| const r = rows[j] as any | ||
| const ed = materialized[j] as Record<string, any> | ||
| // A single malformed/unserializable row must not abort the whole CSV | ||
| // stream — derive the message/trace columns defensively and fall back | ||
| // to empty on error so the row's metadata still exports. | ||
| let message = '' | ||
| let traces: any = null | ||
| let tracesJson = '' | ||
| try { | ||
| const ed = (r as any).executionData | ||
| if (ed) { | ||
| if (ed.finalOutput) | ||
| message = | ||
| typeof ed.finalOutput === 'string' | ||
| ? ed.finalOutput | ||
| : JSON.stringify(ed.finalOutput) | ||
| if (ed.message) message = ed.message | ||
| if (ed.traceSpans) traces = ed.traceSpans | ||
| if (ed.traceSpans) tracesJson = JSON.stringify(ed.traceSpans) | ||
| } | ||
| } catch {} | ||
| } catch (rowError) { | ||
| logger.warn('Skipping unserializable execution data for export row', { | ||
| executionId: r.executionId, | ||
| error: rowError instanceof Error ? rowError.message : String(rowError), | ||
| }) | ||
| } | ||
icecrasher321 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const line = [ | ||
| escapeCsv(r.startedAt?.toISOString?.() || r.startedAt), | ||
| escapeCsv(r.level), | ||
| escapeCsv(r.workflowName), | ||
| escapeCsv(r.trigger), | ||
| escapeCsv(r.totalDurationMs ?? ''), | ||
| escapeCsv(r.cost?.total ?? r.cost?.value?.total ?? ''), | ||
| escapeCsv(r.costTotal ?? ''), | ||
| escapeCsv(r.workflowId ?? ''), | ||
| escapeCsv(r.executionId ?? ''), | ||
| escapeCsv(message), | ||
| escapeCsv(traces ? JSON.stringify(traces) : ''), | ||
| escapeCsv(tracesJson), | ||
| ].join(',') | ||
| controller.enqueue(encoder.encode(`${line}\n`)) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -31,6 +31,7 @@ import { listLogsContract, type WorkflowLogSummary } from '@/lib/api/contracts/l | ||
| import { parseRequest } from '@/lib/api/server' | ||
| import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { jobCostTotal } from '@/lib/logs/fetch-log-detail' | ||
| import { buildFilterConditions } from '@/lib/logs/filters' | ||
| import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' | ||
| @@ -81,7 +82,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| case 'duration': | ||
| return sql`${workflowExecutionLogs.totalDurationMs}` | ||
| case 'cost': | ||
| return sql`(${workflowExecutionLogs.cost}->>'total')::numeric` | ||
| // Indexed projection of the usage_log ledger (dollars); no live aggregation. | ||
| return sql`${workflowExecutionLogs.costTotal}` | ||
| case 'status': | ||
| return sql`${workflowExecutionLogs.status}` | ||
| default: | ||
| @@ -201,7 +203,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| startedAt: workflowExecutionLogs.startedAt, | ||
| endedAt: workflowExecutionLogs.endedAt, | ||
| totalDurationMs: workflowExecutionLogs.totalDurationMs, | ||
| cost: workflowExecutionLogs.cost, | ||
| costTotal: workflowExecutionLogs.costTotal, | ||
| createdAt: workflowExecutionLogs.createdAt, | ||
| workflowName: workflow.name, | ||
| workflowDescription: workflow.description, | ||
| @@ -379,7 +381,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| } | ||
| : null, | ||
| jobTitle: null, | ||
| cost: (log.cost as WorkflowLogSummary['cost']) ?? null, | ||
| // List cost is the cost_total projection (faithful ledger sum). Null until | ||
| // completion (running) or until the one-time legacy backfill populates it. | ||
| cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, | ||
icecrasher321 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| pauseSummary: { | ||
| status: log.pausedStatus ?? null, | ||
| total: totalPauseCount, | ||
| @@ -405,7 +409,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { | ||
| createdAt: log.startedAt.toISOString(), | ||
| workflow: null, | ||
| jobTitle: log.jobTitle ?? null, | ||
| cost: (log.cost as WorkflowLogSummary['cost']) ?? null, | ||
| cost: jobCostTotal(log.cost), | ||
| pauseSummary: { status: null, total: 0, resumed: 0 }, | ||
| hasPendingPause: false, | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.