70 lines
4.1 KiB
TypeScript
70 lines
4.1 KiB
TypeScript
import {Activity, RefreshCw, Timer, TriangleAlert} from 'lucide-react'
|
|
import {useQuery} from '@tanstack/react-query'
|
|
import {queryLogs} from '@/api/cloudProxyClient'
|
|
import type {CloudProvider} from '@/types/cloud'
|
|
|
|
interface LambdaInvocationHistoryPanelProps {
|
|
cloud: CloudProvider
|
|
functionName: string
|
|
runtimeReachable: boolean
|
|
}
|
|
|
|
interface InvocationRecord {
|
|
requestId: string
|
|
timestamp: string
|
|
durationMs: number
|
|
errorType: string | null
|
|
}
|
|
|
|
const HISTORY_QUERY = 'fields @timestamp, @message\n| filter @message like /REPORT RequestId:/\n| sort @timestamp desc\n| limit 20'
|
|
|
|
export function LambdaInvocationHistoryPanel({cloud, functionName, runtimeReachable}: LambdaInvocationHistoryPanelProps) {
|
|
const history = useQuery({
|
|
queryKey: ['lambda-invocation-history', cloud, functionName],
|
|
queryFn: async () => {
|
|
const endTime = Math.floor(Date.now() / 1000)
|
|
try {
|
|
const result = await queryLogs(cloud, `/aws/lambda/${functionName}`, {
|
|
queryString: HISTORY_QUERY,
|
|
startTime: endTime - 24 * 60 * 60,
|
|
endTime,
|
|
})
|
|
return result.rows.map(toInvocationRecord).filter((record): record is InvocationRecord => record !== null)
|
|
} catch (error) {
|
|
if (isMissingLogGroup(error)) return []
|
|
throw error
|
|
}
|
|
},
|
|
enabled: cloud === 'aws' && runtimeReachable,
|
|
})
|
|
const records = history.data ?? []
|
|
const errors = records.filter((record) => record.errorType !== null).length
|
|
const averageDuration = records.length === 0 ? null : records.reduce((sum, record) => sum + record.durationMs, 0) / records.length
|
|
|
|
return <section className="table-panel">
|
|
<div className="dynamic-stage-header">
|
|
<div><p className="eyebrow">Observability</p><h3><Activity size={15}/> Invocation history</h3><p className="muted compact-text">Real Lambda platform records from the last 24 hours.</p></div>
|
|
<button className="button" type="button" disabled={history.isFetching} onClick={() => history.refetch()}><RefreshCw size={13}/>{history.isFetching ? 'Refreshing' : 'Refresh'}</button>
|
|
</div>
|
|
<div className="apigateway-request-flow">
|
|
<div className="apigateway-flow-step"><Timer size={16}/><div><strong>{averageDuration === null ? 'No duration' : `${averageDuration.toFixed(1)} ms`}</strong><small>Average duration</small></div></div>
|
|
<div className="apigateway-flow-step"><TriangleAlert size={16}/><div><strong>{errors}</strong><small>Function errors</small></div></div>
|
|
<div className="apigateway-flow-step"><Activity size={16}/><div><strong>{records.length}</strong><small>Recorded invocations</small></div></div>
|
|
</div>
|
|
{history.isError && <p className="error-text compact-text">{history.error instanceof Error ? history.error.message : 'Unable to load invocation history.'}</p>}
|
|
{!history.isLoading && records.length === 0 && <p className="muted compact-text">No Lambda platform records yet. Invoke the function or trigger it from S3, then refresh.</p>}
|
|
{records.length > 0 && <table className="table"><thead><tr><th>Time</th><th>Request ID</th><th>Duration</th><th>Outcome</th></tr></thead><tbody>{records.map((record) => <tr key={record.requestId}><td>{record.timestamp}</td><td><code>{record.requestId}</code></td><td>{record.durationMs.toFixed(2)} ms</td><td>{record.errorType ? <span className="error-text">{record.errorType}</span> : 'Success'}</td></tr>)}</tbody></table>}
|
|
</section>
|
|
}
|
|
|
|
function toInvocationRecord(row: Record<string, string>): InvocationRecord | null {
|
|
const message = row['@message'] ?? ''
|
|
const match = /REPORT RequestId: (\S+) Duration: ([\d.]+) ms.*?(?: Error Type: (.+))?$/.exec(message)
|
|
if (!match) return null
|
|
return {requestId: match[1], timestamp: row['@timestamp'] ?? '', durationMs: Number(match[2]), errorType: match[3] ?? null}
|
|
}
|
|
|
|
function isMissingLogGroup(error: unknown): boolean {
|
|
return error instanceof Error && /log group does not exist|resource_not_found/i.test(error.message)
|
|
}
|