Merge pull request #650 from simstudioai/improvement/logging-ui

improvement(logging-ui): improve logging UI to be less of information dump
This commit is contained in:
Vikhyath Mondreti
2025-07-09 20:41:16 -07:00
committed by GitHub
9 changed files with 288 additions and 336 deletions

View File

@@ -3,12 +3,15 @@
import { useEffect, useState } from 'react'
import {
AlertCircle,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
Clock,
DollarSign,
Hash,
Loader2,
Maximize2,
X,
Zap,
} from 'lucide-react'
@@ -21,6 +24,71 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('FrozenCanvas')
function ExpandableDataSection({ title, data }: { title: string; data: any }) {
const [isExpanded, setIsExpanded] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const jsonString = JSON.stringify(data, null, 2)
const isLargeData = jsonString.length > 500 || jsonString.split('\n').length > 10
return (
<>
<div>
<div className='mb-2 flex items-center justify-between'>
<h4 className='font-medium text-foreground text-sm'>{title}</h4>
<div className='flex items-center gap-1'>
{isLargeData && (
<button
onClick={() => setIsModalOpen(true)}
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
title='Expand in modal'
>
<Maximize2 className='h-3 w-3' />
</button>
)}
<button
onClick={() => setIsExpanded(!isExpanded)}
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
>
{isExpanded ? <ChevronUp className='h-3 w-3' /> : <ChevronDown className='h-3 w-3' />}
</button>
</div>
</div>
<div
className={cn(
'overflow-y-auto rounded bg-muted p-3 font-mono text-xs transition-all duration-200',
isExpanded ? 'max-h-96' : 'max-h-32'
)}
>
<pre className='whitespace-pre-wrap break-words text-foreground'>{jsonString}</pre>
</div>
</div>
{/* Modal for large data */}
{isModalOpen && (
<div className='fixed inset-0 z-[200] flex items-center justify-center bg-black/50'>
<div className='mx-4 h-[80vh] w-full max-w-4xl rounded-lg border bg-background shadow-lg'>
<div className='flex items-center justify-between border-b p-4'>
<h3 className='font-medium text-foreground text-lg'>{title}</h3>
<button
onClick={() => setIsModalOpen(false)}
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
>
<X className='h-4 w-4' />
</button>
</div>
<div className='h-[calc(80vh-4rem)] overflow-auto p-4'>
<pre className='whitespace-pre-wrap break-words font-mono text-foreground text-sm'>
{jsonString}
</pre>
</div>
</div>
</div>
)}
</>
)
}
function formatExecutionData(executionData: any) {
const {
inputData,
@@ -80,16 +148,79 @@ function getCurrentIterationData(blockExecutionData: any) {
}
}
function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: () => void }) {
function PinnedLogs({
executionData,
blockId,
workflowState,
onClose,
}: {
executionData: any | null
blockId: string
workflowState: any
onClose: () => void
}) {
// ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS
const [currentIterationIndex, setCurrentIterationIndex] = useState(0)
// Reset iteration index when execution data changes
useEffect(() => {
setCurrentIterationIndex(0)
}, [executionData])
// Handle case where block has no execution data (e.g., failed workflow)
if (!executionData) {
const blockInfo = workflowState?.blocks?.[blockId]
const formatted = {
blockName: blockInfo?.name || 'Unknown Block',
blockType: blockInfo?.type || 'unknown',
status: 'not_executed',
duration: 'N/A',
input: null,
output: null,
errorMessage: null,
errorStackTrace: null,
cost: null,
tokens: null,
}
return (
<Card className='fixed top-4 right-4 z-[100] max-h-[calc(100vh-8rem)] w-96 overflow-y-auto border-border bg-background shadow-lg'>
<CardHeader className='pb-3'>
<div className='flex items-center justify-between'>
<CardTitle className='flex items-center gap-2 text-foreground text-lg'>
<Zap className='h-5 w-5' />
{formatted.blockName}
</CardTitle>
<button onClick={onClose} className='rounded-sm p-1 text-foreground hover:bg-muted'>
<X className='h-4 w-4' />
</button>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Badge variant='secondary'>{formatted.blockType}</Badge>
<Badge variant='outline'>not executed</Badge>
</div>
</div>
</CardHeader>
<CardContent className='space-y-4'>
<div className='rounded-md bg-muted/50 p-4 text-center'>
<div className='text-muted-foreground text-sm'>
This block was not executed because the workflow failed before reaching it.
</div>
</div>
</CardContent>
</Card>
)
}
// Now we can safely use the execution data
const iterationInfo = getCurrentIterationData({
...executionData,
currentIteration: currentIterationIndex,
})
const formatted = formatExecutionData(iterationInfo.executionData)
const totalIterations = executionData.iterations?.length || 1
const goToPreviousIteration = () => {
@@ -104,10 +235,6 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
}
}
useEffect(() => {
setCurrentIterationIndex(0)
}, [executionData])
return (
<Card className='fixed top-4 right-4 z-[100] max-h-[calc(100vh-8rem)] w-96 overflow-y-auto border-border bg-background shadow-lg'>
<CardHeader className='pb-3'>
@@ -160,14 +287,14 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
<span className='text-foreground text-sm'>{formatted.duration}</span>
</div>
{formatted.cost && (
{formatted.cost && formatted.cost.total > 0 && (
<div className='flex items-center gap-2'>
<DollarSign className='h-4 w-4 text-muted-foreground' />
<span className='text-foreground text-sm'>${formatted.cost.total.toFixed(5)}</span>
</div>
)}
{formatted.tokens && (
{formatted.tokens && formatted.tokens.total > 0 && (
<div className='flex items-center gap-2'>
<Hash className='h-4 w-4 text-muted-foreground' />
<span className='text-foreground text-sm'>{formatted.tokens.total} tokens</span>
@@ -175,21 +302,11 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
)}
</div>
<div>
<h4 className='mb-2 font-medium text-foreground text-sm'>Input</h4>
<div className='max-h-32 overflow-y-auto rounded bg-muted p-3 font-mono text-xs'>
<pre className='text-foreground'>{JSON.stringify(formatted.input, null, 2)}</pre>
</div>
</div>
<ExpandableDataSection title='Input' data={formatted.input} />
<div>
<h4 className='mb-2 font-medium text-foreground text-sm'>Output</h4>
<div className='max-h-32 overflow-y-auto rounded bg-muted p-3 font-mono text-xs'>
<pre className='text-foreground'>{JSON.stringify(formatted.output, null, 2)}</pre>
</div>
</div>
<ExpandableDataSection title='Output' data={formatted.output} />
{formatted.cost && (
{formatted.cost && formatted.cost.total > 0 && (
<div>
<h4 className='mb-2 font-medium text-foreground text-sm'>Cost Breakdown</h4>
<div className='space-y-1 text-sm'>
@@ -209,7 +326,7 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
</div>
)}
{formatted.tokens && (
{formatted.tokens && formatted.tokens.total > 0 && (
<div>
<h4 className='mb-2 font-medium text-foreground text-sm'>Token Usage</h4>
<div className='space-y-1 text-sm'>
@@ -242,12 +359,7 @@ interface FrozenCanvasData {
startedAt: string
endedAt?: string
totalDurationMs?: number
blockStats: {
total: number
success: number
error: number
skipped: number
}
cost: {
total: number | null
input: number | null
@@ -284,76 +396,100 @@ export function FrozenCanvas({
if (traceSpans && Array.isArray(traceSpans)) {
const blockExecutionMap: Record<string, any> = {}
const workflowSpan = traceSpans[0]
if (workflowSpan?.children && Array.isArray(workflowSpan.children)) {
const traceSpansByBlockId = workflowSpan.children.reduce((acc: any, span: any) => {
logger.debug('Processing trace spans for frozen canvas:', { traceSpans })
// Recursively collect all spans with blockId from the trace spans tree
const collectBlockSpans = (spans: any[]): any[] => {
const blockSpans: any[] = []
for (const span of spans) {
// If this span has a blockId, it's a block execution
if (span.blockId) {
if (!acc[span.blockId]) {
acc[span.blockId] = []
}
acc[span.blockId].push(span)
blockSpans.push(span)
}
return acc
}, {})
for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) {
const spanArray = spans as any[]
const iterations = spanArray.map((span: any) => {
// Extract error information from span output if status is error
let errorMessage = null
let errorStackTrace = null
if (span.status === 'error' && span.output) {
// Error information can be in different formats in the output
if (typeof span.output === 'string') {
errorMessage = span.output
} else if (span.output.error) {
errorMessage = span.output.error
errorStackTrace = span.output.stackTrace || span.output.stack
} else if (span.output.message) {
errorMessage = span.output.message
errorStackTrace = span.output.stackTrace || span.output.stack
} else {
// Fallback: stringify the entire output for error cases
errorMessage = JSON.stringify(span.output)
}
}
return {
id: span.id,
blockId: span.blockId,
blockName: span.name,
blockType: span.type,
status: span.status,
startedAt: span.startTime,
endedAt: span.endTime,
durationMs: span.duration,
inputData: span.input,
outputData: span.output,
errorMessage,
errorStackTrace,
cost: span.cost || {
input: null,
output: null,
total: null,
},
tokens: span.tokens || {
prompt: null,
completion: null,
total: null,
},
modelUsed: span.model || null,
metadata: {},
}
})
blockExecutionMap[blockId] = {
iterations,
currentIteration: 0,
totalIterations: iterations.length,
// Recursively check children
if (span.children && Array.isArray(span.children)) {
blockSpans.push(...collectBlockSpans(span.children))
}
}
return blockSpans
}
const allBlockSpans = collectBlockSpans(traceSpans)
logger.debug('Collected all block spans:', allBlockSpans)
// Group spans by blockId
const traceSpansByBlockId = allBlockSpans.reduce((acc: any, span: any) => {
if (span.blockId) {
if (!acc[span.blockId]) {
acc[span.blockId] = []
}
acc[span.blockId].push(span)
}
return acc
}, {})
logger.debug('Grouped trace spans by blockId:', traceSpansByBlockId)
for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) {
const spanArray = spans as any[]
const iterations = spanArray.map((span: any) => {
// Extract error information from span output if status is error
let errorMessage = null
let errorStackTrace = null
if (span.status === 'error' && span.output) {
// Error information can be in different formats in the output
if (typeof span.output === 'string') {
errorMessage = span.output
} else if (span.output.error) {
errorMessage = span.output.error
errorStackTrace = span.output.stackTrace || span.output.stack
} else if (span.output.message) {
errorMessage = span.output.message
errorStackTrace = span.output.stackTrace || span.output.stack
} else {
// Fallback: stringify the entire output for error cases
errorMessage = JSON.stringify(span.output)
}
}
return {
id: span.id,
blockId: span.blockId,
blockName: span.name,
blockType: span.type,
status: span.status,
startedAt: span.startTime,
endedAt: span.endTime,
durationMs: span.duration,
inputData: span.input,
outputData: span.output,
errorMessage,
errorStackTrace,
cost: span.cost || {
input: null,
output: null,
total: null,
},
tokens: span.tokens || {
prompt: null,
completion: null,
total: null,
},
modelUsed: span.model || null,
metadata: {},
}
})
blockExecutionMap[blockId] = {
iterations,
currentIteration: 0,
totalIterations: iterations.length,
}
}
setBlockExecutions(blockExecutionMap)
@@ -386,8 +522,6 @@ export function FrozenCanvas({
fetchData()
}, [executionId])
// No need to create a temporary workflow - just use the workflowState directly
if (loading) {
return (
<div className={cn('flex items-center justify-center', className)} style={{ height, width }}>
@@ -449,16 +583,18 @@ export function FrozenCanvas({
showSubBlocks={true}
isPannable={true}
onNodeClick={(blockId) => {
if (blockExecutions[blockId]) {
setPinnedBlockId(blockId)
}
// Always allow clicking blocks, even if they don't have execution data
// This is important for failed workflows where some blocks never executed
setPinnedBlockId(blockId)
}}
/>
</div>
{pinnedBlockId && blockExecutions[pinnedBlockId] && (
{pinnedBlockId && (
<PinnedLogs
executionData={blockExecutions[pinnedBlockId]}
executionData={blockExecutions[pinnedBlockId] || null}
blockId={pinnedBlockId}
workflowState={data.workflowState}
onClose={() => setPinnedBlockId(null)}
/>
)}

View File

@@ -494,43 +494,6 @@ export function Sidebar({
</div>
)}
{/* Enhanced Stats - only show for enhanced logs */}
{log.metadata?.enhanced && log.metadata?.blockStats && (
<div>
<h3 className='mb-1 font-medium text-muted-foreground text-xs'>
Block Execution Stats
</h3>
<div className='space-y-1 text-sm'>
<div className='flex justify-between'>
<span>Total Blocks:</span>
<span className='font-medium'>{log.metadata.blockStats.total}</span>
</div>
<div className='flex justify-between'>
<span>Successful:</span>
<span className='font-medium text-green-600'>
{log.metadata.blockStats.success}
</span>
</div>
{log.metadata.blockStats.error > 0 && (
<div className='flex justify-between'>
<span>Failed:</span>
<span className='font-medium text-red-600'>
{log.metadata.blockStats.error}
</span>
</div>
)}
{log.metadata.blockStats.skipped > 0 && (
<div className='flex justify-between'>
<span>Skipped:</span>
<span className='font-medium text-yellow-600'>
{log.metadata.blockStats.skipped}
</span>
</div>
)}
</div>
</div>
)}
{/* Enhanced Cost - only show for enhanced logs with actual cost data */}
{log.metadata?.enhanced && hasCostInfo && (
<div>
@@ -583,7 +546,7 @@ export function Sidebar({
className='w-full justify-start gap-2'
>
<Eye className='h-4 w-4' />
View Frozen Canvas
View Snapshot
</Button>
<p className='mt-1 text-muted-foreground text-xs'>
See the exact workflow state and block inputs/outputs at execution time

View File

@@ -1,15 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
import {
ChevronDown,
ChevronDownSquare,
ChevronRight,
ChevronUpSquare,
Code,
Cpu,
ExternalLink,
} from 'lucide-react'
import { useState } from 'react'
import { ChevronDown, ChevronRight, Code, Cpu, ExternalLink } from 'lucide-react'
import {
AgentIcon,
ApiIcon,
@@ -203,40 +195,11 @@ export function TraceSpansDisplay({
// Keep track of expanded spans
const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set())
// Function to collect all span IDs recursively (for expand all functionality)
const collectAllSpanIds = (spans: TraceSpan[]): string[] => {
const ids: string[] = []
const collectIds = (span: TraceSpan) => {
const spanId = span.id || `span-${span.name}-${span.startTime}`
ids.push(spanId)
// Process children
if (span.children && span.children.length > 0) {
span.children.forEach(collectIds)
}
}
spans.forEach(collectIds)
return ids
}
const allSpanIds = useMemo(() => {
if (!traceSpans || traceSpans.length === 0) return []
return collectAllSpanIds(traceSpans)
}, [traceSpans])
// Early return after all hooks
if (!traceSpans || traceSpans.length === 0) {
return <div className='text-muted-foreground text-sm'>No trace data available</div>
}
// Format total duration for better readability
const _formatTotalDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(2)}s (${ms}ms)`
}
// Find the earliest start time among all spans to be the workflow start time
const workflowStartTime = traceSpans.reduce((earliest, span) => {
const startTime = new Date(span.startTime).getTime()
@@ -269,48 +232,10 @@ export function TraceSpansDisplay({
}
}
// Handle expand all / collapse all
const handleExpandAll = () => {
const newExpandedSpans = new Set(allSpanIds)
setExpandedSpans(newExpandedSpans)
if (onExpansionChange) {
onExpansionChange(true)
}
}
const handleCollapseAll = () => {
setExpandedSpans(new Set())
if (onExpansionChange) {
onExpansionChange(false)
}
}
// Determine if all spans are currently expanded
const allExpanded = allSpanIds.length > 0 && allSpanIds.every((id) => expandedSpans.has(id))
return (
<div className='w-full'>
<div className='mb-2 flex items-center justify-between'>
<div className='font-medium text-muted-foreground text-xs'>Trace Spans</div>
<button
onClick={allExpanded ? handleCollapseAll : handleExpandAll}
className='flex items-center gap-1 text-muted-foreground text-xs transition-colors hover:text-foreground'
title={allExpanded ? 'Collapse all spans' : 'Expand all spans'}
>
{allExpanded ? (
<>
<ChevronUpSquare className='h-3.5 w-3.5' />
<span>Collapse</span>
</>
) : (
<>
<ChevronDownSquare className='h-3.5 w-3.5' />
<span>Expand</span>
</>
)}
</button>
<div className='font-medium text-muted-foreground text-xs'>Workflow Execution</div>
</div>
<div className='w-full overflow-hidden rounded-md border shadow-sm'>
{traceSpans.map((span, index) => {
@@ -369,7 +294,8 @@ function TraceSpanItem({
const expanded = expandedSpans.has(spanId)
const hasChildren = span.children && span.children.length > 0
const hasToolCalls = span.toolCalls && span.toolCalls.length > 0
const hasNestedItems = hasChildren || hasToolCalls
const hasInputOutput = Boolean(span.input || span.output)
const hasNestedItems = hasChildren || hasToolCalls || hasInputOutput
// Calculate timing information
const spanStartTime = new Date(span.startTime).getTime()
@@ -389,9 +315,6 @@ function TraceSpanItem({
const safeStartPercent = Math.min(100, Math.max(0, relativeStartPercent))
const safeWidthPercent = Math.max(2, Math.min(100 - safeStartPercent, actualDurationPercent))
// For parent-relative timing display
const _startOffsetPercentage = totalDuration > 0 ? (startOffset / totalDuration) * 100 : 0
// Handle click to expand/collapse this span
const handleSpanClick = () => {
if (hasNestedItems) {
@@ -605,17 +528,17 @@ function TraceSpanItem({
</div>
</div>
{/* Children and tool calls */}
{/* Expanded content */}
{expanded && (
<div>
{/* Block Input/Output Data */}
{(span.input || span.output) && (
<div className='mt-2 ml-8 space-y-3 overflow-hidden'>
<div className='mt-2 mr-4 mb-4 ml-8 space-y-3 overflow-hidden'>
{/* Input Data */}
{span.input && (
<div>
<h4 className='mb-2 font-medium text-muted-foreground text-xs'>Input</h4>
<div className='overflow-hidden rounded-md bg-secondary/30 p-3'>
<div className='mb-2 overflow-hidden rounded-md bg-secondary/30 p-3'>
<BlockDataDisplay data={span.input} blockType={span.type} isInput={true} />
</div>
</div>
@@ -627,7 +550,7 @@ function TraceSpanItem({
<h4 className='mb-2 font-medium text-muted-foreground text-xs'>
{span.status === 'error' ? 'Error Details' : 'Output'}
</h4>
<div className='overflow-hidden rounded-md bg-secondary/30 p-3'>
<div className='mb-2 overflow-hidden rounded-md bg-secondary/30 p-3'>
<BlockDataDisplay
data={span.output}
blockType={span.type}
@@ -639,12 +562,8 @@ function TraceSpanItem({
)}
</div>
)}
</div>
)}
{/* Children and tool calls */}
{expanded && (
<div>
{/* Children and tool calls */}
{/* Render child spans */}
{hasChildren && (
<div>

View File

@@ -308,14 +308,20 @@ export default function Logs() {
{/* Table with fixed layout */}
<div className='w-full min-w-[800px]'>
{/* Header */}
<div className='border-border/50 border-b'>
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-3 font-medium text-muted-foreground text-xs'>
<div>Time</div>
<div>Status</div>
<div>Workflow</div>
<div className='hidden lg:block'>Trigger</div>
<div className='hidden xl:block'>Cost</div>
<div>Duration</div>
<div className='px-4 py-4'>
<div className='rounded-lg border border-border/30 bg-muted/30'>
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-3'>
<div className='font-medium text-muted-foreground text-xs'>Time</div>
<div className='font-medium text-muted-foreground text-xs'>Status</div>
<div className='font-medium text-muted-foreground text-xs'>Workflow</div>
<div className='hidden font-medium text-muted-foreground text-xs lg:block'>
Trigger
</div>
<div className='hidden font-medium text-muted-foreground text-xs xl:block'>
Cost
</div>
<div className='font-medium text-muted-foreground text-xs'>Duration</div>
</div>
</div>
</div>
</div>
@@ -344,7 +350,7 @@ export default function Logs() {
</div>
</div>
) : (
<div className='space-y-1 p-4'>
<div className='space-y-1 px-4 pb-4'>
{logs.map((log) => {
const formattedDate = formatDate(log.createdAt)
const isSelected = selectedLog?.id === log.id
@@ -360,7 +366,7 @@ export default function Logs() {
}`}
onClick={() => handleLogClick(log)}
>
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 p-4'>
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-4'>
{/* Time */}
<div>
<div className='font-medium text-sm'>{formattedDate.formatted}</div>
@@ -403,13 +409,11 @@ export default function Logs() {
{/* Cost */}
<div className='hidden xl:block'>
<div className='text-xs'>
<div className='text-muted-foreground text-xs'>
{log.metadata?.enhanced && log.metadata?.cost?.total ? (
<span className='text-muted-foreground'>
${log.metadata.cost.total.toFixed(4)}
</span>
<span>${log.metadata.cost.total.toFixed(4)}</span>
) : (
<span className='text-muted-foreground'></span>
<span className='pl-0.5'></span>
)}
</div>
</div>

View File

@@ -84,12 +84,7 @@ export interface WorkflowLog {
cost?: CostMetadata
blockInput?: Record<string, any>
enhanced?: boolean
blockStats?: {
total: number
success: number
error: number
skipped: number
}
blockExecutions?: Array<{
id: string
blockId: string

View File

@@ -194,12 +194,6 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
executionId: string
endedAt: string
totalDurationMs: number
blockStats: {
total: number
success: number
error: number
skipped: number
}
costSummary: {
totalCost: number
totalInputCost: number
@@ -220,23 +214,24 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
finalOutput: BlockOutputData
traceSpans?: TraceSpan[]
}): Promise<WorkflowExecutionLog> {
const {
executionId,
endedAt,
totalDurationMs,
blockStats,
costSummary,
finalOutput,
traceSpans,
} = params
const { executionId, endedAt, totalDurationMs, costSummary, finalOutput, traceSpans } = params
logger.debug(`Completing workflow execution ${executionId}`)
const level = blockStats.error > 0 ? 'error' : 'info'
const message =
blockStats.error > 0
? `Workflow execution failed: ${blockStats.error} error(s), ${blockStats.success} success(es)`
: `Workflow execution completed: ${blockStats.success} block(s) executed successfully`
// Determine if workflow failed by checking trace spans for errors
const hasErrors = traceSpans?.some((span: any) => {
const checkSpanForErrors = (s: any): boolean => {
if (s.status === 'error') return true
if (s.children && Array.isArray(s.children)) {
return s.children.some(checkSpanForErrors)
}
return false
}
return checkSpanForErrors(span)
})
const level = hasErrors ? 'error' : 'info'
const message = hasErrors ? 'Workflow execution failed' : 'Workflow execution completed'
const [updatedLog] = await db
.update(workflowExecutionLogs)
@@ -245,10 +240,10 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
message,
endedAt: new Date(endedAt),
totalDurationMs,
blockCount: blockStats.total,
successCount: blockStats.success,
errorCount: blockStats.error,
skippedCount: blockStats.skipped,
blockCount: 0,
successCount: 0,
errorCount: 0,
skippedCount: 0,
totalCost: costSummary.totalCost.toString(),
totalInputCost: costSummary.totalInputCost.toString(),
totalOutputCost: costSummary.totalOutputCost.toString(),

View File

@@ -46,51 +46,6 @@ export async function loadWorkflowStateForExecution(workflowId: string): Promise
}
}
export function calculateBlockStats(traceSpans: any[]): {
total: number
success: number
error: number
skipped: number
} {
if (!traceSpans || traceSpans.length === 0) {
return { total: 0, success: 0, error: 0, skipped: 0 }
}
// Recursively collect all block spans from the trace span tree
const collectBlockSpans = (spans: any[]): any[] => {
const blocks: any[] = []
for (const span of spans) {
// Check if this span is an actual workflow block
if (
span.type &&
span.type !== 'workflow' &&
span.type !== 'provider' &&
span.type !== 'model' &&
span.blockId
) {
blocks.push(span)
}
// Recursively check children
if (span.children && Array.isArray(span.children)) {
blocks.push(...collectBlockSpans(span.children))
}
}
return blocks
}
const blockSpans = collectBlockSpans(traceSpans)
const total = blockSpans.length
const success = blockSpans.filter((span) => span.status === 'success').length
const error = blockSpans.filter((span) => span.status === 'error').length
const skipped = blockSpans.filter((span) => span.status === 'skipped').length
return { total, success, error, skipped }
}
export function calculateCostSummary(traceSpans: any[]): {
totalCost: number
totalInputCost: number

View File

@@ -1,7 +1,6 @@
import { createLogger } from '@/lib/logs/console-logger'
import { enhancedExecutionLogger } from './enhanced-execution-logger'
import {
calculateBlockStats,
calculateCostSummary,
createEnvironmentObject,
createTriggerObject,
@@ -99,14 +98,12 @@ export class EnhancedLoggingSession {
const { endedAt, totalDurationMs, finalOutput, traceSpans } = params
try {
const blockStats = calculateBlockStats(traceSpans || [])
const costSummary = calculateCostSummary(traceSpans || [])
await enhancedExecutionLogger.completeWorkflowExecution({
executionId: this.executionId,
endedAt: endedAt || new Date().toISOString(),
totalDurationMs: totalDurationMs || 0,
blockStats,
costSummary,
finalOutput: finalOutput || {},
traceSpans: traceSpans || [],
@@ -126,7 +123,6 @@ export class EnhancedLoggingSession {
async completeWithError(error?: any): Promise<void> {
try {
const blockStats = { total: 0, success: 0, error: 1, skipped: 0 }
const costSummary = {
totalCost: 0,
totalInputCost: 0,
@@ -141,7 +137,6 @@ export class EnhancedLoggingSession {
executionId: this.executionId,
endedAt: new Date().toISOString(),
totalDurationMs: 0,
blockStats,
costSummary,
finalOutput: null,
traceSpans: [],

View File

@@ -169,12 +169,7 @@ export interface WorkflowExecutionSummary {
startedAt: string
endedAt: string
durationMs: number
blockStats: {
total: number
success: number
error: number
skipped: number
}
costSummary: {
total: number
inputCost: number
@@ -360,12 +355,7 @@ export interface ExecutionLoggerService {
executionId: string
endedAt: string
totalDurationMs: number
blockStats: {
total: number
success: number
error: number
skipped: number
}
costSummary: {
totalCost: number
totalInputCost: number