chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import { db } from '@sim/db'
|
||||
import {
|
||||
jobExecutionLogs,
|
||||
pausedExecutions,
|
||||
usageLog,
|
||||
workflow,
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import { and, eq, type SQL } from 'drizzle-orm'
|
||||
import type { CostLedger } from '@/lib/api/contracts/logs'
|
||||
import {
|
||||
type ExecutionProgressMarkers,
|
||||
getProgressMarkers,
|
||||
pickLatestCompletedMarker,
|
||||
pickLatestStartedMarker,
|
||||
} from '@/lib/logs/execution/progress-markers'
|
||||
import { materializeExecutionData } from '@/lib/logs/execution/trace-store'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
type LookupColumn = 'id' | 'executionId'
|
||||
|
||||
async function buildCostLedger(executionId: string): Promise<CostLedger | null> {
|
||||
const rows = await db
|
||||
.select({
|
||||
category: usageLog.category,
|
||||
description: usageLog.description,
|
||||
cost: usageLog.cost,
|
||||
metadata: usageLog.metadata,
|
||||
})
|
||||
.from(usageLog)
|
||||
.where(and(eq(usageLog.executionId, executionId), eq(usageLog.source, 'workflow')))
|
||||
|
||||
if (rows.length === 0) return null
|
||||
|
||||
type LedgerItem = CostLedger['items'][number]
|
||||
const byKey = new Map<string, LedgerItem>()
|
||||
for (const row of rows) {
|
||||
const metadata = (row.metadata ?? {}) as { inputTokens?: number; outputTokens?: number }
|
||||
const category = row.category as LedgerItem['category']
|
||||
const key = `${category}::${row.description}`
|
||||
const existing = byKey.get(key)
|
||||
if (existing) {
|
||||
existing.cost += Number(row.cost)
|
||||
if (typeof metadata.inputTokens === 'number') {
|
||||
existing.inputTokens = Math.max(existing.inputTokens ?? 0, metadata.inputTokens)
|
||||
}
|
||||
if (typeof metadata.outputTokens === 'number') {
|
||||
existing.outputTokens = Math.max(existing.outputTokens ?? 0, metadata.outputTokens)
|
||||
}
|
||||
} else {
|
||||
byKey.set(key, {
|
||||
category,
|
||||
description: row.description,
|
||||
cost: Number(row.cost),
|
||||
...(typeof metadata.inputTokens === 'number' ? { inputTokens: metadata.inputTokens } : {}),
|
||||
...(typeof metadata.outputTokens === 'number'
|
||||
? { outputTokens: metadata.outputTokens }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const items = [...byKey.values()]
|
||||
const total = items.reduce((sum, item) => sum + item.cost, 0)
|
||||
return { total, items }
|
||||
}
|
||||
|
||||
export function jobCostTotal(raw: unknown): { total: number } | null {
|
||||
const total = (raw as { total?: unknown } | null | undefined)?.total
|
||||
const n = total == null ? Number.NaN : Number(total)
|
||||
return Number.isFinite(n) ? { total: n } : null
|
||||
}
|
||||
|
||||
interface FetchLogDetailArgs {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
lookupColumn: LookupColumn
|
||||
lookupValue: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared loader for the workflow-log detail shape returned by the by-id and
|
||||
* by-execution routes. Returns `null` when no matching row exists in either
|
||||
* the workflow-execution or job-execution tables for this user + workspace.
|
||||
*
|
||||
* For in-flight (running/pending) executions, live progress markers are merged
|
||||
* from Redis, since they are only folded into the row at a terminal/pause
|
||||
* boundary.
|
||||
*/
|
||||
export async function fetchLogDetail({
|
||||
userId,
|
||||
workspaceId,
|
||||
lookupColumn,
|
||||
lookupValue,
|
||||
}: FetchLogDetailArgs) {
|
||||
const access = await checkWorkspaceAccess(workspaceId, userId)
|
||||
if (!access.hasAccess) return null
|
||||
|
||||
const workflowMatch: SQL =
|
||||
lookupColumn === 'id'
|
||||
? eq(workflowExecutionLogs.id, lookupValue)
|
||||
: eq(workflowExecutionLogs.executionId, lookupValue)
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: workflowExecutionLogs.id,
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
|
||||
level: workflowExecutionLogs.level,
|
||||
status: workflowExecutionLogs.status,
|
||||
trigger: workflowExecutionLogs.trigger,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
endedAt: workflowExecutionLogs.endedAt,
|
||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||
executionData: workflowExecutionLogs.executionData,
|
||||
costTotal: workflowExecutionLogs.costTotal,
|
||||
files: workflowExecutionLogs.files,
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
workflowFolderId: workflow.folderId,
|
||||
workflowUserId: workflow.userId,
|
||||
workflowWorkspaceId: workflow.workspaceId,
|
||||
workflowCreatedAt: workflow.createdAt,
|
||||
workflowUpdatedAt: workflow.updatedAt,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: workflowDeploymentVersion.name,
|
||||
pausedStatus: pausedExecutions.status,
|
||||
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
|
||||
pausedResumedCount: pausedExecutions.resumedCount,
|
||||
})
|
||||
.from(workflowExecutionLogs)
|
||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||
.leftJoin(
|
||||
workflowDeploymentVersion,
|
||||
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
||||
)
|
||||
.leftJoin(pausedExecutions, eq(pausedExecutions.executionId, workflowExecutionLogs.executionId))
|
||||
.where(and(workflowMatch, eq(workflowExecutionLogs.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
const log = rows[0]
|
||||
|
||||
if (log) {
|
||||
const workflowSummary = log.workflowId
|
||||
? {
|
||||
id: log.workflowId,
|
||||
name: log.workflowName,
|
||||
description: log.workflowDescription,
|
||||
folderId: log.workflowFolderId,
|
||||
userId: log.workflowUserId,
|
||||
workspaceId: log.workflowWorkspaceId,
|
||||
createdAt: log.workflowCreatedAt?.toISOString() ?? null,
|
||||
updatedAt: log.workflowUpdatedAt?.toISOString() ?? null,
|
||||
}
|
||||
: null
|
||||
|
||||
const totalPauseCount = Number(log.pausedTotalPauseCount ?? 0)
|
||||
const resumedCount = Number(log.pausedResumedCount ?? 0)
|
||||
const hasPendingPause =
|
||||
(totalPauseCount > 0 && resumedCount < totalPauseCount) ||
|
||||
(log.pausedStatus !== null && log.pausedStatus !== 'fully_resumed')
|
||||
|
||||
// Cost is sourced exclusively from the usage_log ledger (itemized breakdown)
|
||||
// and its cost_total projection (run total). The cost jsonb is never read.
|
||||
const costLedger = await buildCostLedger(log.executionId)
|
||||
const totalDollars = costLedger?.total ?? (log.costTotal != null ? Number(log.costTotal) : null)
|
||||
|
||||
// Trace spans / heavy execution data may live in object storage; resolve the
|
||||
// pointer here (no-op for inline / pre-externalization rows).
|
||||
const executionData = await materializeExecutionData(
|
||||
log.executionData as Record<string, unknown> | null,
|
||||
{ workspaceId, workflowId: log.workflowId, executionId: log.executionId }
|
||||
)
|
||||
|
||||
const liveMarkers =
|
||||
log.status === 'running' || log.status === 'pending'
|
||||
? ((await getProgressMarkers(log.executionId)) ?? {})
|
||||
: {}
|
||||
const rowMarkers = (executionData ?? {}) as ExecutionProgressMarkers
|
||||
const mergedStartedBlock = pickLatestStartedMarker(
|
||||
liveMarkers.lastStartedBlock,
|
||||
rowMarkers.lastStartedBlock
|
||||
)
|
||||
const mergedCompletedBlock = pickLatestCompletedMarker(
|
||||
liveMarkers.lastCompletedBlock,
|
||||
rowMarkers.lastCompletedBlock
|
||||
)
|
||||
|
||||
return {
|
||||
id: log.id,
|
||||
workflowId: log.workflowId,
|
||||
executionId: log.executionId,
|
||||
deploymentVersionId: log.deploymentVersionId,
|
||||
deploymentVersion: log.deploymentVersion ?? null,
|
||||
deploymentVersionName: log.deploymentVersionName ?? null,
|
||||
level: log.level,
|
||||
status: log.status,
|
||||
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
|
||||
trigger: log.trigger,
|
||||
createdAt: log.startedAt.toISOString(),
|
||||
workflow: workflowSummary,
|
||||
jobTitle: null,
|
||||
cost: totalDollars != null ? { total: totalDollars } : null,
|
||||
costLedger,
|
||||
pauseSummary: {
|
||||
status: log.pausedStatus ?? null,
|
||||
total: totalPauseCount,
|
||||
resumed: resumedCount,
|
||||
},
|
||||
hasPendingPause,
|
||||
executionData: {
|
||||
totalDuration: log.totalDurationMs,
|
||||
...executionData,
|
||||
...(mergedStartedBlock ? { lastStartedBlock: mergedStartedBlock } : {}),
|
||||
...(mergedCompletedBlock ? { lastCompletedBlock: mergedCompletedBlock } : {}),
|
||||
enhanced: true as const,
|
||||
},
|
||||
files: log.files ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
const jobMatch: SQL =
|
||||
lookupColumn === 'id'
|
||||
? eq(jobExecutionLogs.id, lookupValue)
|
||||
: eq(jobExecutionLogs.executionId, lookupValue)
|
||||
|
||||
const jobRows = await db
|
||||
.select({
|
||||
id: jobExecutionLogs.id,
|
||||
executionId: jobExecutionLogs.executionId,
|
||||
level: jobExecutionLogs.level,
|
||||
status: jobExecutionLogs.status,
|
||||
trigger: jobExecutionLogs.trigger,
|
||||
startedAt: jobExecutionLogs.startedAt,
|
||||
endedAt: jobExecutionLogs.endedAt,
|
||||
totalDurationMs: jobExecutionLogs.totalDurationMs,
|
||||
executionData: jobExecutionLogs.executionData,
|
||||
cost: jobExecutionLogs.cost,
|
||||
createdAt: jobExecutionLogs.createdAt,
|
||||
})
|
||||
.from(jobExecutionLogs)
|
||||
.where(and(jobMatch, eq(jobExecutionLogs.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
const jobLog = jobRows[0]
|
||||
if (!jobLog) return null
|
||||
|
||||
const execData = (jobLog.executionData as Record<string, unknown> | null) ?? {}
|
||||
return {
|
||||
id: jobLog.id,
|
||||
workflowId: null,
|
||||
executionId: jobLog.executionId,
|
||||
deploymentVersionId: null,
|
||||
deploymentVersion: null,
|
||||
deploymentVersionName: null,
|
||||
level: jobLog.level,
|
||||
status: jobLog.status,
|
||||
duration: jobLog.totalDurationMs ? `${jobLog.totalDurationMs}ms` : null,
|
||||
trigger: jobLog.trigger,
|
||||
createdAt: jobLog.startedAt.toISOString(),
|
||||
workflow: null,
|
||||
jobTitle: ((execData.trigger as Record<string, unknown> | undefined)?.source as string) ?? null,
|
||||
cost: jobCostTotal(jobLog.cost),
|
||||
pauseSummary: { status: null, total: 0, resumed: 0 },
|
||||
hasPendingPause: false,
|
||||
executionData: {
|
||||
totalDuration: jobLog.totalDurationMs,
|
||||
...execData,
|
||||
enhanced: true as const,
|
||||
},
|
||||
files: null,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user