improve logging ui

This commit is contained in:
Vikhyath Mondreti
2025-07-09 19:19:53 -07:00
parent 50595c5c49
commit e102b6cf17
9 changed files with 116 additions and 230 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,69 @@ 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,
@@ -160,14 +226,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 +241,17 @@ 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 +271,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 +304,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

View File

@@ -494,42 +494,7 @@ 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 && (
@@ -583,7 +548,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,11 +1,9 @@
'use client'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import {
ChevronDown,
ChevronDownSquare,
ChevronRight,
ChevronUpSquare,
Code,
Cpu,
ExternalLink,
@@ -203,39 +201,16 @@ 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) => {
@@ -269,48 +244,12 @@ 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 +308,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,8 +329,7 @@ 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 = () => {
@@ -605,17 +544,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 ml-8 mr-4 mb-4 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='overflow-hidden rounded-md bg-secondary/30 p-3 mb-2'>
<BlockDataDisplay data={span.input} blockType={span.type} isInput={true} />
</div>
</div>
@@ -627,7 +566,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='overflow-hidden rounded-md bg-secondary/30 p-3 mb-2'>
<BlockDataDisplay
data={span.output}
blockType={span.type}
@@ -639,12 +578,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,16 @@ 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 lg:block font-medium text-muted-foreground text-xs'>Trigger</div>
<div className='hidden xl:block font-medium text-muted-foreground text-xs'>Cost</div>
<div className='font-medium text-muted-foreground text-xs'>Duration</div>
</div>
</div>
</div>
</div>
@@ -344,7 +346,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 +362,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 +405,13 @@ export default function Logs() {
{/* Cost */}
<div className='hidden xl:block'>
<div className='text-xs'>
<div className='text-xs text-muted-foreground'>
{log.metadata?.enhanced && log.metadata?.cost?.total ? (
<span className='text-muted-foreground'>
<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
@@ -224,7 +218,6 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
executionId,
endedAt,
totalDurationMs,
blockStats,
costSummary,
finalOutput,
traceSpans,
@@ -232,11 +225,8 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
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`
const level = 'info'
const message = `Workflow execution completed`
const [updatedLog] = await db
.update(workflowExecutionLogs)
@@ -245,10 +235,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,50 +46,7 @@ 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

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