Files
Aether/frontend/src/views/admin/AdminOperationsDashboard.vue
T

2224 lines
85 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="space-y-5 px-4 pb-8 sm:px-6 lg:px-0">
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div>
<h1 class="text-lg font-semibold">
运维总览
</h1>
<p class="text-xs text-muted-foreground">
统一查看流量、吞吐、延迟、错误、上游健康、缓存与审计风险
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-muted-foreground">
更新 {{ lastUpdatedLabel }}
</span>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 shrink-0"
:class="autoRefresh ? 'text-primary' : ''"
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
:aria-label="autoRefresh ? '关闭自动刷新' : '开启自动刷新'"
:aria-pressed="autoRefresh"
@click="toggleAutoRefresh"
>
<RefreshCw
class="h-3.5 w-3.5"
:class="autoRefresh || refreshing ? 'animate-spin' : ''"
/>
</Button>
<TimeRangePicker
v-model="timeRange"
:allow-hourly="true"
/>
</div>
</div>
<div
v-if="loadWarning"
class="rounded-lg border border-yellow-300/70 bg-yellow-50/80 px-3 py-2 text-xs text-yellow-900 dark:border-yellow-900/60 dark:bg-yellow-950/30 dark:text-yellow-100"
>
{{ loadWarning }}
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-8">
<Card
v-for="card in kpiCards"
:key="card.title"
class="min-h-[118px] p-4"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="truncate text-xs text-muted-foreground">
{{ card.title }}
</p>
<div
class="mt-2 truncate text-2xl font-semibold tabular-nums"
:class="card.valueClass"
>
{{ card.value }}
</div>
</div>
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-muted/30">
<component
:is="card.icon"
class="h-4 w-4"
:class="card.iconClass"
/>
</div>
</div>
<p class="mt-3 line-clamp-2 text-xs text-muted-foreground">
{{ card.hint }}
</p>
</Card>
</div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
<Card class="overflow-hidden xl:col-span-2">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-sm font-semibold">
流量与吞吐
</h2>
<p class="text-xs text-muted-foreground">
请求、Token 与费用趋势,按当前时间窗口聚合
</p>
</div>
<Badge variant="outline">
{{ timeRange.granularity || 'day' }}
</Badge>
</div>
</div>
<div class="p-4">
<div
v-if="trendLoading"
class="py-12"
>
<LoadingState message="加载流量趋势中" />
</div>
<div
v-else
class="h-[302px]"
>
<LineChart
:data="trafficChartData"
:options="trafficChartOptions"
/>
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
实时并发
</h2>
<p class="text-xs text-muted-foreground">
网关、全局锁与代理通道
</p>
</div>
<Badge :variant="distributedGateVariant">
{{ distributedGateText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3 text-sm">
<MetricCell
label="本机处理中"
:value="formatMetricNumber(gatewayMetrics?.local.inFlight)"
/>
<MetricCell
label="本机可接入"
:value="formatMetricNumber(gatewayMetrics?.local.availablePermits)"
/>
<MetricCell
label="全局处理中"
:value="formatMetricNumber(gatewayMetrics?.distributed.inFlight)"
/>
<MetricCell
label="全局可接入"
:value="formatMetricNumber(gatewayMetrics?.distributed.availablePermits)"
/>
<MetricCell
label="代理活跃流"
:value="formatMetricNumber(currentActiveStreams)"
/>
<MetricCell
label="代理连接"
:value="formatMetricNumber(currentProxyConnections)"
/>
</div>
<div class="rounded-lg border border-border/60 bg-background/45 px-3 py-3">
<div class="flex items-center justify-between gap-3 text-xs">
<span class="text-muted-foreground">队列利用率</span>
<span class="font-medium tabular-nums">{{ tunnelQueueUtilizationText }}</span>
</div>
<div class="mt-2 h-2 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary"
:style="{ width: tunnelQueueUtilizationWidth }"
/>
</div>
<div class="mt-3 grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<span>拒绝 {{ formatMetricNumber(tunnelQueueRejectedTotal) }}</span>
<span>选择压力 {{ formatMetricNumber(tunnelSelectionPressureTotal) }}</span>
</div>
</div>
</div>
</Card>
</div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-4 2xl:grid-cols-5">
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
Admission
</h2>
<p class="text-xs text-muted-foreground">
本机、分布式、候选与上游执行 Gate
</p>
</div>
<Badge :variant="admissionStatusVariant">
{{ admissionStatusText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="本机 Gate"
:value="localGateUtilizationText"
:value-class="capacityPercentClass(localGateUtilization)"
/>
<MetricCell
label="分布式 Gate"
:value="distributedGateUtilizationText"
:value-class="capacityPercentClass(distributedGateUtilization)"
/>
<MetricCell
label="候选规划"
:value="candidatePlanningGateUtilizationText"
:value-class="capacityPercentClass(candidatePlanningGateUtilization)"
/>
<MetricCell
label="上游执行"
:value="upstreamExecutionGateUtilizationText"
:value-class="capacityPercentClass(upstreamExecutionGateUtilization)"
/>
</div>
<div class="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<span>本机拒绝 {{ formatMetricNumber(gatewayMetrics?.local.rejectedTotal) }}</span>
<span>上游拒绝 {{ formatMetricNumber(gatewayMetrics?.upstreamExecution.rejectedTotal) }}</span>
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
DB Pool
</h2>
<p class="text-xs text-muted-foreground">
前台路径共享连接预算
</p>
</div>
<Badge :variant="databasePoolStatusVariant">
{{ databasePoolStatusText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="使用率"
:value="databasePoolUsageText"
:value-class="capacityPercentClass(databasePoolUsagePercent)"
/>
<MetricCell
label="占用 / 上限"
:value="databasePoolCheckedOutText"
/>
<MetricCell
label="空闲连接"
:value="formatMetricNumber(gatewayMetrics?.databasePool.idle)"
/>
<MetricCell
label="预留空闲"
:value="formatMetricNumber(gatewayMetrics?.databasePool.idleReserve)"
/>
<MetricCell
label="PG 等待"
:value="postgresWaitingText"
:value-class="counterRiskClass(gatewayMetrics?.postgres.waitingConnections)"
/>
<MetricCell
label="PG 锁等待"
:value="formatMetricNumber(gatewayMetrics?.postgres.lockWaitingConnections)"
:value-class="counterRiskClass(gatewayMetrics?.postgres.lockWaitingConnections)"
/>
<MetricCell
label="最长查询"
:value="postgresOldestActiveQueryText"
:value-class="latencyToneClass(gatewayMetrics?.postgres.oldestActiveQueryAgeMs)"
/>
<MetricCell
label="最长事务"
:value="postgresOldestTransactionText"
:value-class="latencyToneClass(gatewayMetrics?.postgres.oldestTransactionAgeMs)"
/>
<MetricCell
label="PG 命中"
:value="postgresCacheHitText"
:value-class="postgresCacheHitClass"
/>
<MetricCell
label="临时写"
:value="formatBytes(gatewayMetrics?.postgres.tempBytesTotal)"
/>
<MetricCell
label="WAL"
:value="postgresWalText"
:value-class="postgresWalClass"
/>
<MetricCell
label="Checkpoint"
:value="postgresCheckpointText"
:value-class="postgresCheckpointClass"
/>
<MetricCell
label="Top SQL"
:value="postgresTopStatementText"
:value-class="postgresStatementClass"
/>
<MetricCell
label="PG 回滚"
:value="formatMetricNumber(gatewayMetrics?.postgres.xactRollbackTotal)"
:value-class="counterRiskClass(gatewayMetrics?.postgres.xactRollbackTotal)"
/>
</div>
<div class="text-xs text-muted-foreground">
Driver {{ gatewayMetrics?.databasePool.driver || '-' }} · Pool size {{ formatMetricNumber(gatewayMetrics?.databasePool.size) }} · PG active {{ formatMetricNumber(gatewayMetrics?.postgres.activeConnections) }} · idle tx {{ formatMetricNumber(gatewayMetrics?.postgres.idleInTransactionConnections) }} · deadlocks {{ formatMetricNumber(gatewayMetrics?.postgres.deadlocksTotal) }} · top calls {{ formatMetricNumber(gatewayMetrics?.postgres.statementTopCallsTotal) }}
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
Gateway 进程
</h2>
<p class="text-xs text-muted-foreground">
CPU、RSS、FD 与 TCP 连接边界
</p>
</div>
<Badge :variant="gatewayProcessStatusVariant">
{{ gatewayProcessStatusText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="进程 CPU"
:value="formatBasisPointsPercent(gatewayMetrics?.process.processCpuUsageBasisPoints)"
:value-class="resourceToneClass(gatewayProcessCpuPercent, 70, 90)"
/>
<MetricCell
label="RSS"
:value="formatBytes(gatewayMetrics?.process.processMemoryBytes)"
:value-class="resourceToneClass(gatewayProcessMemoryPercent, 70, 90)"
/>
<MetricCell
label="Heap allocated"
:value="formatBytes(gatewayMetrics?.allocator.allocatedBytes)"
/>
<MetricCell
label="Heap resident"
:value="formatBytes(gatewayMetrics?.allocator.residentBytes)"
/>
<MetricCell
label="Heap active"
:value="formatBasisPointsPercent(gatewayAllocatorActivePercentBasisPoints)"
:value-class="resourceToneClass(gatewayAllocatorActivePercent, 125, 200)"
/>
<MetricCell
label="线程"
:value="formatMetricNumber(gatewayMetrics?.process.processThreads)"
/>
<MetricCell
label="后台任务"
:value="gatewayBackgroundTaskText"
/>
<MetricCell
label="Tokio 任务"
:value="formatMetricNumber(gatewayMetrics?.tokioRuntime.aliveTasks)"
/>
<MetricCell
label="Runtime 队列"
:value="formatMetricNumber(gatewayMetrics?.tokioRuntime.globalQueueDepth)"
/>
<MetricCell
label="任务退出"
:value="formatMetricNumber(gatewayBackgroundTaskUnexpectedExits)"
:value-class="counterRiskClass(gatewayBackgroundTaskUnexpectedExits)"
/>
<MetricCell
label="FD 使用"
:value="gatewayProcessFdText"
:value-class="resourceToneClass(gatewayProcessFdPercent, 60, 70)"
/>
<MetricCell
label="Socket FD"
:value="gatewayProcessSocketFdText"
/>
<MetricCell
label="TCP 连接"
:value="gatewayProcessTcpText"
/>
<MetricCell
label="CLOSE_WAIT"
:value="formatMetricNumber(gatewayMetrics?.process.processTcpCloseWaitConnections)"
:value-class="counterRiskClass(gatewayMetrics?.process.processTcpCloseWaitConnections)"
/>
<MetricCell
label="系统内存"
:value="formatBasisPointsPercent(gatewayMetrics?.process.systemMemoryUsageBasisPoints)"
:value-class="resourceToneClass(gatewaySystemMemoryPercent, 75, 90)"
/>
<MetricCell
label="网卡流量"
:value="gatewayNetworkTrafficText"
/>
</div>
<div class="text-xs text-muted-foreground">
运行 {{ gatewayProcessUptimeText }} · 虚拟内存 {{ formatBytes(gatewayMetrics?.process.processVirtualMemoryBytes) }} · TCP {{ gatewayTcpStateText }} · allocator {{ gatewayAllocatorStatusText }} · supervisor {{ formatMetricNumber(gatewayMetrics?.backgroundTasks.supervisedTotal) }} · Tokio workers {{ formatMetricNumber(gatewayMetrics?.tokioRuntime.workers) }}
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
Usage 与队列
</h2>
<p class="text-xs text-muted-foreground">
账务写入和 request candidate 削峰
</p>
</div>
<Badge :variant="queueStatusVariant">
{{ queueStatusText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="Usage Worker"
:value="usageWorkerText"
/>
<MetricCell
label="Usage ACK"
:value="formatMetricNumber(gatewayMetrics?.usageRuntime.workerAckedEntriesTotal)"
/>
<MetricCell
label="Usage Lag"
:value="formatMetricNumber(gatewayMetrics?.usageQueue.groupLag)"
:value-class="counterRiskClass(gatewayMetrics?.usageQueue.groupLag)"
/>
<MetricCell
label="Usage DLQ"
:value="formatMetricNumber(gatewayMetrics?.usageQueue.dlqLength)"
:value-class="counterRiskClass(gatewayMetrics?.usageQueue.dlqLength)"
/>
<MetricCell
label="Outbox 待处理"
:value="formatMetricNumber(gatewayMetrics?.usageCounter.pendingRows)"
:value-class="counterRiskClass(gatewayMetrics?.usageCounter.pendingRows)"
/>
<MetricCell
label="Outbox 已刷"
:value="formatMetricNumber(gatewayMetrics?.usageCounter.flushRowsClaimedTotal)"
/>
<MetricCell
label="最老待处理"
:value="usageCounterOldestPendingAgeText"
:value-class="usageCounterOldestPendingAgeClass"
/>
<MetricCell
label="Outbox 失败"
:value="formatMetricNumber(usageCounterOutboxFailures)"
:value-class="counterRiskClass(usageCounterOutboxFailures)"
/>
<MetricCell
label="Terminal 失败"
:value="formatMetricNumber(gatewayMetrics?.usageRuntime.terminalEnqueueFailedTotal)"
:value-class="counterRiskClass(gatewayMetrics?.usageRuntime.terminalEnqueueFailedTotal)"
/>
<MetricCell
label="Worker 异常"
:value="formatMetricNumber(usageRuntimeWorkerFaults)"
:value-class="counterRiskClass(usageRuntimeWorkerFaults)"
/>
<MetricCell
label="Candidate 水位"
:value="candidateQueueUtilizationText"
:value-class="capacityPercentClass(candidateQueueUtilization)"
/>
<MetricCell
label="Candidate 待刷"
:value="formatMetricNumber(gatewayMetrics?.requestCandidateQueue.pendingDepth)"
/>
<MetricCell
label="Candidate 已刷"
:value="formatMetricNumber(gatewayMetrics?.requestCandidateQueue.flushedTotal)"
/>
<MetricCell
label="Candidate 合并"
:value="formatMetricNumber(gatewayMetrics?.requestCandidateQueue.compactedTotal)"
/>
</div>
<div class="rounded-lg border border-border/60 bg-background/45 px-3 py-3">
<div class="flex items-center justify-between gap-3 text-xs">
<span class="text-muted-foreground">Candidate depth</span>
<span class="font-medium tabular-nums">{{ candidateQueueDepthText }}</span>
</div>
<div class="mt-2 h-2 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary"
:style="{ width: candidateQueueUtilizationWidth }"
/>
</div>
<div class="mt-3 grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<span>Outbox processed {{ formatMetricNumber(gatewayMetrics?.usageCounter.processedRows) }}</span>
<span>Outbox health {{ usageCounterHealthText }}</span>
<span>Flush batches {{ formatMetricNumber(gatewayMetrics?.usageCounter.flushBatchesTotal) }}</span>
<span>Flush targets {{ formatMetricNumber(gatewayMetrics?.usageCounter.flushTargetsTotal) }}</span>
<span>Worker read {{ formatMetricNumber(gatewayMetrics?.usageRuntime.workerReadEntriesTotal) }}</span>
<span>Worker reclaim {{ formatMetricNumber(gatewayMetrics?.usageRuntime.workerReclaimedEntriesTotal) }}</span>
<span>Usage pending {{ formatMetricNumber(gatewayMetrics?.usageQueue.groupPending) }}</span>
<span>Pending idle {{ usageQueueOldestPendingIdleText }}</span>
<span>Drop {{ formatMetricNumber(gatewayMetrics?.requestCandidateQueue.droppedTotal) }}</span>
<span>Fallback {{ formatMetricNumber(gatewayMetrics?.requestCandidateQueue.syncFallbackTotal) }}</span>
<span>Flush batches {{ formatMetricNumber(gatewayMetrics?.requestCandidateQueue.flushBatchesTotal) }}</span>
<span>SQL ops {{ formatMetricNumber(gatewayMetrics?.requestCandidateQueue.flushSqlOpsTotal) }}</span>
</div>
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
Upstream Target
</h2>
<p class="text-xs text-muted-foreground">
单 target 饱和隔离和选择压力
</p>
</div>
<Badge :variant="upstreamTargetStatusVariant">
{{ upstreamTargetStatusText }}
</Badge>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="活跃 Target"
:value="formatMetricNumber(gatewayMetrics?.upstreamTargets.activeTargets)"
/>
<MetricCell
label="单 Target 上限"
:value="formatMetricNumber(gatewayMetrics?.upstreamTargets.limit)"
/>
<MetricCell
label="饱和累计"
:value="formatMetricNumber(gatewayMetrics?.upstreamTargets.saturatedTotal)"
:value-class="counterRiskClass(gatewayMetrics?.upstreamTargets.saturatedTotal)"
/>
<MetricCell
label="拒绝累计"
:value="formatMetricNumber(gatewayMetrics?.upstreamTargets.rejectedTotal)"
:value-class="counterRiskClass(gatewayMetrics?.upstreamTargets.rejectedTotal)"
/>
</div>
<div class="space-y-2">
<div
v-for="target in upstreamTargetRows"
:key="target.target"
class="rounded-lg border border-border/50 bg-background/45 px-3 py-2 text-xs"
>
<div class="flex items-center justify-between gap-3">
<span class="min-w-0 truncate font-medium">{{ target.target }}</span>
<span class="shrink-0 tabular-nums">{{ formatMetricNumber(target.inFlight) }} in-flight</span>
</div>
<div class="mt-1 flex items-center justify-between gap-3 text-muted-foreground">
<span>available {{ formatMetricNumber(target.availablePermits) }}</span>
<span>saturated {{ formatMetricNumber(target.saturatedTotal) }}</span>
</div>
</div>
<div
v-if="upstreamTargetRows.length === 0"
class="rounded-lg border border-dashed border-border/60 px-3 py-6 text-center text-xs text-muted-foreground"
>
暂无 target 样本
</div>
</div>
</div>
</Card>
</div>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
关键路径 Stage
</h2>
<p class="text-xs text-muted-foreground">
入口排队、候选规划、上游 Gate 和流式总耗时
</p>
</div>
<Badge variant="outline">
Gateway latency
</Badge>
</div>
</div>
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Stage</TableHead>
<TableHead class="text-right">
样本
</TableHead>
<TableHead class="text-right">
平均
</TableHead>
<TableHead class="text-right">
最大
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="stage in stageLatencyRows"
:key="stage.stage"
>
<TableCell>
<div class="text-sm font-medium">
{{ stage.label }}
</div>
<div class="text-xs text-muted-foreground">
{{ stage.stage }}
</div>
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatMetricNumber(stage.count) }}
</TableCell>
<TableCell
class="text-right tabular-nums"
:class="latencyToneClass(stage.avgMs)"
>
{{ formatMs(stage.avgMs) }}
</TableCell>
<TableCell
class="text-right tabular-nums"
:class="latencyToneClass(stage.maxMs)"
>
{{ formatMs(stage.maxMs) }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</Card>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
<Card class="overflow-hidden xl:col-span-2">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-sm font-semibold">
延迟与首字
</h2>
<p class="text-xs text-muted-foreground">
P50/P90/P99 请求耗时与 TTFT 趋势
</p>
</div>
<RouterLink
to="/admin/performance-analysis"
class="text-xs font-medium text-primary hover:underline"
>
查看性能分析
</RouterLink>
</div>
</div>
<div class="p-4">
<div
v-if="percentileLoading"
class="py-12"
>
<LoadingState message="加载延迟百分位中" />
</div>
<div
v-else
class="h-[288px]"
>
<LineChart
:data="latencyChartData"
:options="latencyChartOptions"
/>
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<h2 class="text-sm font-semibold">
SLA 与错误
</h2>
<p class="text-xs text-muted-foreground">
请求错误、上游错误与熔断风险
</p>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="成功率"
:value="formatPercent(providerPerformance?.summary.success_rate)"
:value-class="slaValueClass"
/>
<MetricCell
label="错误率"
:value="formatErrorRate(providerPerformance?.summary.success_rate)"
:value-class="errorRateValueClass"
/>
<MetricCell
label="已分类错误"
:value="formatMetricNumber(classifiedErrorCount)"
/>
<MetricCell
label="熔断打开"
:value="formatMetricNumber(resilienceStatus?.error_statistics.open_circuit_breakers)"
/>
</div>
<div>
<div class="mb-2 flex items-center justify-between gap-3">
<span class="text-xs font-medium text-muted-foreground">错误分类</span>
<RouterLink
to="/admin/audit-logs"
class="text-xs font-medium text-primary hover:underline"
>
审计
</RouterLink>
</div>
<div class="h-[154px]">
<DoughnutChart
:data="errorDistributionChartData"
:options="errorDistributionOptions"
/>
</div>
</div>
</div>
</Card>
</div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
<Card class="overflow-hidden xl:col-span-2">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-sm font-semibold">
上游健康与吞吐
</h2>
<p class="text-xs text-muted-foreground">
Provider 维度的 TPS、TTFT、慢请求与错误样本
</p>
</div>
<RouterLink
to="/admin/health-monitor"
class="text-xs font-medium text-primary hover:underline"
>
健康监控
</RouterLink>
</div>
</div>
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>上游</TableHead>
<TableHead class="text-right">
请求
</TableHead>
<TableHead class="text-right">
成功率
</TableHead>
<TableHead class="text-right">
TPS
</TableHead>
<TableHead class="text-right">
TTFT
</TableHead>
<TableHead class="text-right">
P99
</TableHead>
<TableHead class="text-right">
慢请求
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="provider in providerRows"
:key="provider.provider_id"
>
<TableCell>
<div class="max-w-[220px] truncate text-sm font-medium">
{{ provider.provider }}
</div>
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatMetricNumber(provider.request_count) }}
</TableCell>
<TableCell
class="text-right tabular-nums"
:class="successRateClass(provider.success_rate)"
>
{{ formatPercent(provider.success_rate) }}
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatTps(provider.avg_output_tps) }}
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatMs(provider.avg_first_byte_time_ms) }}
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatMs(provider.p99_response_time_ms) }}
</TableCell>
<TableCell class="text-right tabular-nums">
{{ formatMetricNumber(provider.slow_request_count) }}
</TableCell>
</TableRow>
<TableRow v-if="providerRows.length === 0">
<TableCell
colspan="7"
class="py-8 text-center text-sm text-muted-foreground"
>
当前时间窗口暂无上游样本
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
资源、数据流与缓存
</h2>
<p class="text-xs text-muted-foreground">
代理节点资源、WebSocket 流量、Redis Key 分类与缓存命中
</p>
</div>
<RouterLink
to="/admin/cache-monitoring"
class="text-xs font-medium text-primary hover:underline"
>
缓存页
</RouterLink>
</div>
</div>
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-3">
<MetricCell
label="CPU"
:value="formatPercentResource(resourceSnapshot?.avgCpuPercent)"
:value-class="resourceToneClass(resourceSnapshot?.avgCpuPercent, 70, 90)"
/>
<MetricCell
label="内存"
:value="formatPercentResource(resourceSnapshot?.avgMemoryPercent)"
:value-class="resourceToneClass(resourceSnapshot?.avgMemoryPercent, 75, 90)"
/>
<MetricCell
label="WS 入站"
:value="formatBytes(resourceSnapshot?.wsInBytes)"
/>
<MetricCell
label="WS 出站"
:value="formatBytes(resourceSnapshot?.wsOutBytes)"
/>
<MetricCell
label="网关 TCP"
:value="gatewayProcessTcpText"
/>
<MetricCell
label="网络异常"
:value="formatMetricNumber(gatewayNetworkFaults)"
:value-class="counterRiskClass(gatewayNetworkFaults)"
/>
<MetricCell
label="Redis 状态"
:value="redisStatusText"
:value-class="redisStatusClass"
/>
<MetricCell
label="Redis 内存"
:value="redisRuntimeMemoryText"
:value-class="capacityPercentClass(redisRuntimeMemoryPercent)"
/>
<MetricCell
label="Redis OPS"
:value="redisRuntimeOpsText"
/>
<MetricCell
label="Redis 延迟"
:value="redisRuntimeLatencyText"
:value-class="latencyToneClass(gatewayMetrics?.redisRuntime.nonblockingCommandLatencyMaxMs)"
/>
<MetricCell
label="Redis 客户端"
:value="redisRuntimeClientsText"
/>
<MetricCell
label="Redis 命令异常"
:value="formatMetricNumber(redisRuntimeCommandFaults)"
:value-class="counterRiskClass(redisRuntimeCommandFaults)"
/>
<MetricCell
label="Redis Keys"
:value="formatMetricNumber(redisCategories?.total_keys)"
/>
<MetricCell
label="亲和缓存"
:value="formatMetricNumber(cacheStats?.affinity_stats.total_affinities)"
/>
<MetricCell
label="命中率"
:value="formatPercent(cacheHitRate)"
/>
</div>
<div class="space-y-2">
<div
v-for="item in redisCategoryRows"
:key="item.key"
class="flex items-center justify-between gap-3 rounded-lg border border-border/50 bg-background/45 px-3 py-2 text-xs"
>
<div class="min-w-0">
<div class="truncate font-medium">
{{ item.name }}
</div>
<div class="truncate text-muted-foreground">
{{ item.pattern }}
</div>
</div>
<span class="font-semibold tabular-nums">{{ formatMetricNumber(item.count) }}</span>
</div>
</div>
</div>
</Card>
</div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
<Card class="overflow-hidden xl:col-span-2">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<h2 class="text-sm font-semibold">
错误审计
</h2>
<p class="text-xs text-muted-foreground">
最近错误、熔断事件和可疑活动入口
</p>
</div>
<RouterLink
to="/admin/usage?status=error"
class="text-xs font-medium text-primary hover:underline"
>
使用记录
</RouterLink>
</div>
</div>
<div class="divide-y divide-border/50">
<div
v-for="error in recentErrors"
:key="error.error_id"
class="grid grid-cols-1 gap-2 px-4 py-3 text-sm lg:grid-cols-[180px_1fr_120px]"
>
<div class="font-medium">
{{ error.error_type }}
</div>
<div class="min-w-0 text-xs text-muted-foreground">
<div class="truncate">
{{ error.context.error_message || error.operation }}
</div>
<div class="mt-1 truncate">
{{ error.context.provider_name || error.context.provider_id || '-' }} / {{ error.context.model || '-' }}
</div>
</div>
<div class="text-xs text-muted-foreground lg:text-right">
{{ formatShortDate(error.timestamp) }}
</div>
</div>
<div
v-if="recentErrors.length === 0"
class="px-4 py-8 text-center text-sm text-muted-foreground"
>
当前没有近期错误
</div>
</div>
</Card>
<Card class="overflow-hidden">
<div class="border-b border-border/70 bg-muted/20 px-4 py-3">
<h2 class="text-sm font-semibold">
运维入口
</h2>
<p class="text-xs text-muted-foreground">
常用排障视角
</p>
</div>
<div class="grid grid-cols-1 gap-2 p-4">
<RouterLink
v-for="link in opsLinks"
:key="link.to"
:to="link.to"
class="flex items-center justify-between rounded-lg border border-border/60 bg-background/45 px-3 py-3 text-sm transition-colors hover:border-primary/50 hover:bg-primary/5"
>
<span class="font-medium">{{ link.label }}</span>
<component
:is="link.icon"
class="h-4 w-4 text-muted-foreground"
/>
</RouterLink>
</div>
</Card>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, defineComponent, h, onMounted, onUnmounted, ref, watch, type Component } from 'vue'
import { RouterLink } from 'vue-router'
import type { ChartData, ChartOptions } from 'chart.js'
import {
Activity,
AlertTriangle,
BarChart3,
CircleDollarSign,
Database,
Gauge,
ListChecks,
RefreshCw,
ShieldCheck,
Timer,
Zap,
} from 'lucide-vue-next'
import { Badge, Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
import { LoadingState, TimeRangePicker } from '@/components/common'
import LineChart from '@/components/charts/LineChart.vue'
import DoughnutChart from '@/components/charts/DoughnutChart.vue'
import { adminApi, type ErrorDistributionItem, type PercentileItem, type ProviderPerformanceResponse } from '@/api/admin'
import { cacheApi, redisCacheApi, type CacheStats, type RedisCacheCategoriesResponse } from '@/api/cache'
import { monitoringApi, type AdminMonitoringRecentError, type AdminMonitoringResilienceStatus, type GatewayMetricsSummary } from '@/api/monitoring'
import { proxyNodesApi, type ProxyNode, type ProxyNodeMetricsResponse } from '@/api/proxy-nodes'
import { getDateRangeFromPeriod } from '@/features/usage/composables'
import type { DateRangeParams } from '@/features/usage/types'
import { formatByteSize, formatCurrency, formatNumber, formatTokens } from '@/utils/format'
import { log } from '@/utils/logger'
interface MetricCellProps {
label: string
value: string
valueClass?: string
}
const MetricCell = defineComponent<MetricCellProps>({
name: 'MetricCell',
props: {
label: { type: String, required: true },
value: { type: String, required: true },
valueClass: { type: String, default: '' },
},
setup(props) {
return () => h('div', { class: 'rounded-lg border border-border/60 bg-background/45 px-3 py-3' }, [
h('div', { class: 'text-xs text-muted-foreground' }, props.label),
h('div', { class: ['mt-1 truncate text-lg font-semibold tabular-nums', props.valueClass] }, props.value),
])
},
})
const DEFAULT_SLOW_THRESHOLD_MS = 10_000
interface ResourceSnapshot {
totalNodes: number
onlineNodes: number
avgCpuPercent: number | null
avgMemoryPercent: number | null
wsInBytes: number | null
wsOutBytes: number | null
}
const timeRange = ref<DateRangeParams>({
...getDateRangeFromPeriod('today'),
granularity: 'hour',
})
const timeSeries = ref<Array<Record<string, unknown>>>([])
const percentiles = ref<PercentileItem[]>([])
const providerPerformance = ref<ProviderPerformanceResponse | null>(null)
const errorDistribution = ref<ErrorDistributionItem[]>([])
const errorDistributionLoaded = ref(false)
const resilienceStatus = ref<AdminMonitoringResilienceStatus | null>(null)
const gatewayMetrics = ref<GatewayMetricsSummary | null>(null)
const cacheStats = ref<CacheStats | null>(null)
const redisCategories = ref<RedisCacheCategoriesResponse | null>(null)
const resourceSnapshot = ref<ResourceSnapshot | null>(null)
const lastUpdatedAt = ref<string | null>(null)
const analyticsWarning = ref<string | null>(null)
const realtimeWarning = ref<string | null>(null)
const loadWarning = computed(() =>
[analyticsWarning.value, realtimeWarning.value].filter(Boolean).join(';') || null
)
const refreshing = ref(false)
const autoRefresh = ref(false)
const trendLoading = ref(false)
const percentileLoading = ref(false)
const AUTO_REFRESH_INTERVAL = 10_000
let requestId = 0
let refreshPromise: Promise<void> | null = null
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
let analyticsGeneration = 0
const timeRangeParams = computed(() => ({
start_date: timeRange.value.start_date,
end_date: timeRange.value.end_date,
preset: timeRange.value.preset,
timezone: timeRange.value.timezone,
tz_offset_minutes: timeRange.value.tz_offset_minutes,
granularity: timeRange.value.granularity || 'day',
}))
function numeric(value: unknown): number {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string') {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
return 0
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
return value as Record<string, unknown>
}
function numberField(record: Record<string, unknown> | null | undefined, key: string): number | null {
if (!record) return null
const value = record[key]
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string') {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
function averageFinite(values: Array<number | null | undefined>): number | null {
const numbers = values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value))
if (!numbers.length) return null
return numbers.reduce((total, value) => total + value, 0) / numbers.length
}
function memoryUsedPercent(node: ProxyNode): number | null {
const metadata = asRecord(node.proxy_metadata)
const resource = asRecord(metadata?.resource_usage)
const hardware = asRecord(node.hardware_info)
const explicitPercent = numberField(resource, 'memory_used_percent')
if (explicitPercent != null) return explicitPercent
const usedBytes = numberField(resource, 'memory_used_bytes')
const totalBytes = numberField(resource, 'memory_total_bytes')
?? numberField(hardware, 'memory_total_bytes')
?? numberField(hardware, 'total_memory_bytes')
if (usedBytes == null || totalBytes == null || totalBytes <= 0) return null
return usedBytes / totalBytes * 100
}
function cpuUsedPercent(node: ProxyNode): number | null {
const metadata = asRecord(node.proxy_metadata)
const resource = asRecord(metadata?.resource_usage)
return numberField(resource, 'system_cpu_usage_percent')
?? numberField(resource, 'process_cpu_usage_percent')
}
async function loadResourceSnapshot(): Promise<ResourceSnapshot | null> {
const now = Math.floor(Date.now() / 1000)
const from = now - 3600
const [nodesResult, fleetResult] = await Promise.allSettled([
proxyNodesApi.listProxyNodes({ limit: 200 }),
proxyNodesApi.listFleetMetrics({ from, to: now, step: '1m' }),
])
const nodes = nodesResult.status === 'fulfilled' ? nodesResult.value.items : []
const fleet: ProxyNodeMetricsResponse | null = fleetResult.status === 'fulfilled'
? fleetResult.value
: null
const onlineNodes = nodes.filter(node => node.status === 'online' || node.tunnel_connected)
return {
totalNodes: nodes.length,
onlineNodes: onlineNodes.length,
avgCpuPercent: averageFinite(onlineNodes.map(cpuUsedPercent)),
avgMemoryPercent: averageFinite(onlineNodes.map(memoryUsedPercent)),
wsInBytes: fleet?.summary.ws_in_bytes_delta ?? null,
wsOutBytes: fleet?.summary.ws_out_bytes_delta ?? null,
}
}
function formatMetricNumber(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
if (!Number.isInteger(value)) return value.toFixed(value < 10 ? 2 : 1)
return formatNumber(value)
}
function formatPercent(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
const ratio = value <= 1 ? value * 100 : value
return `${ratio.toFixed(2)}%`
}
function formatErrorRate(successRate: number | null | undefined): string {
if (successRate == null || Number.isNaN(successRate)) return '-'
const rate = successRate <= 1 ? successRate * 100 : successRate
return `${Math.max(0, 100 - rate).toFixed(2)}%`
}
function formatMs(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
if (value >= 1000) return `${(value / 1000).toFixed(value >= 10_000 ? 1 : 2)}s`
return `${Math.round(value)}ms`
}
function formatTps(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
return `${value.toFixed(value < 10 ? 2 : value < 100 ? 1 : 0)} tps`
}
function formatPercentResource(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
return `${value.toFixed(value < 10 ? 1 : 0)}%`
}
function basisPointsPercent(value: number | null | undefined): number | null {
if (value == null || Number.isNaN(value)) return null
return value / 100
}
function formatBasisPointsPercent(value: number | null | undefined): string {
return formatPercentResource(basisPointsPercent(value))
}
function formatBytes(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
return formatByteSize(value)
}
function formatDurationSeconds(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
if (value < 60) return `${Math.round(value)}s`
if (value < 3600) return `${Math.round(value / 60)}m`
if (value < 86_400) return `${(value / 3600).toFixed(value < 36_000 ? 1 : 0)}h`
return `${(value / 86_400).toFixed(1)}d`
}
function resourceToneClass(value: number | null | undefined, warning: number, critical: number): string {
if (value == null || Number.isNaN(value)) return ''
if (value >= critical) return 'text-red-600 dark:text-red-400'
if (value >= warning) return 'text-amber-600 dark:text-amber-400'
return 'text-green-600 dark:text-green-400'
}
function capacityPercentClass(value: number | null | undefined): string {
return resourceToneClass(value, 80, 90)
}
function counterRiskClass(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return ''
return value > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400'
}
function latencyToneClass(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return ''
if (value >= 5_000) return 'text-red-600 dark:text-red-400'
if (value >= 1_000) return 'text-amber-600 dark:text-amber-400'
return ''
}
function formatCapacityPercent(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return '-'
return `${value.toFixed(value < 10 ? 1 : 0)}%`
}
function formatMetricRatio(current: number | null | undefined, max: number | null | undefined): string {
return `${formatMetricNumber(current)} / ${formatMetricNumber(max)}`
}
function gateUtilization(gate: GatewayMetricsSummary['local'] | null | undefined): number | null {
const inFlight = gate?.inFlight
const available = gate?.availablePermits
if (inFlight == null || available == null) return null
const total = inFlight + available
if (total <= 0) return null
return Math.max(0, Math.min(100, inFlight / total * 100))
}
function hasCapacitySignal(...values: Array<number | null | undefined>): boolean {
return values.some(value => typeof value === 'number' && Number.isFinite(value))
}
function formatShortDate(value?: string | null): string {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
function successRateClass(value: number | null | undefined): string {
if (value == null || Number.isNaN(value)) return ''
const rate = value <= 1 ? value * 100 : value
if (rate >= 95) return 'text-green-600 dark:text-green-400'
if (rate >= 80) return 'text-amber-600 dark:text-amber-400'
return 'text-red-600 dark:text-red-400'
}
function average(values: Array<number | null | undefined>): number | null {
const numbers = values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value))
if (!numbers.length) return null
return numbers.reduce((total, value) => total + value, 0) / numbers.length
}
function sumSeries(field: string): number {
return timeSeries.value.reduce((total, item) => total + numeric(item[field]), 0)
}
function seriesTokenTotal(item: Record<string, unknown>): number {
return numeric(item.input_tokens)
+ numeric(item.output_tokens)
+ numeric(item.cache_creation_tokens)
+ numeric(item.cache_read_tokens)
}
async function performRefresh(refreshAnalytics: boolean) {
const currentRequestId = ++requestId
const currentAnalyticsGeneration = analyticsGeneration
refreshing.value = true
if (refreshAnalytics) {
trendLoading.value = timeSeries.value.length === 0
percentileLoading.value = percentiles.value.length === 0
analyticsWarning.value = null
}
realtimeWarning.value = null
const params = timeRangeParams.value
const canCommitRealtime = () => currentRequestId === requestId
const canCommitAnalytics = () => (
refreshAnalytics
&& canCommitRealtime()
&& currentAnalyticsGeneration === analyticsGeneration
)
const timeSeriesRequest = (refreshAnalytics
? adminApi.getTimeSeries(params, { skipCache: true })
: Promise.resolve(timeSeries.value)
).then((value) => {
if (canCommitAnalytics()) timeSeries.value = value
return value
}).finally(() => {
if (canCommitAnalytics()) trendLoading.value = false
})
const percentilesRequest = (refreshAnalytics
? adminApi.getPercentiles(params, { skipCache: true })
: Promise.resolve(percentiles.value)
).then((value) => {
if (canCommitAnalytics()) percentiles.value = value
return value
}).finally(() => {
if (canCommitAnalytics()) percentileLoading.value = false
})
const providerPerformanceRequest = (refreshAnalytics
? adminApi.getProviderPerformance({
...params,
granularity: params.granularity === 'hour' ? 'hour' : 'day',
limit: 8,
slow_threshold_ms: DEFAULT_SLOW_THRESHOLD_MS,
include_timeline: false,
}, { skipCache: true })
: Promise.resolve(providerPerformance.value)
).then((value) => {
if (canCommitAnalytics()) providerPerformance.value = value
return value
})
const errorDistributionRequest = (refreshAnalytics
? adminApi.getErrorDistribution(params, { skipCache: true })
: Promise.resolve({ distribution: errorDistribution.value })
).then((value) => {
if (canCommitAnalytics()) {
errorDistribution.value = value.distribution
errorDistributionLoaded.value = true
}
return value
})
const resilienceRequest = monitoringApi.getResilienceStatus().then((value) => {
if (canCommitRealtime()) resilienceStatus.value = value
return value
})
const gatewayRequest = monitoringApi.getGatewayMetricsSummary().then((value) => {
if (canCommitRealtime()) gatewayMetrics.value = value
return value
})
const cacheRequest = cacheApi.getStats().then((value) => {
if (canCommitRealtime()) cacheStats.value = value
return value
})
const redisRequest = redisCacheApi.getCategories().then((value) => {
if (canCommitRealtime()) redisCategories.value = value
return value
})
const resourceRequest = loadResourceSnapshot().then((value) => {
if (canCommitRealtime()) resourceSnapshot.value = value
return value
})
const results = await Promise.allSettled([
timeSeriesRequest,
percentilesRequest,
providerPerformanceRequest,
errorDistributionRequest,
resilienceRequest,
gatewayRequest,
cacheRequest,
redisRequest,
resourceRequest,
])
if (currentRequestId !== requestId) return
const analyticsFailed: string[] = []
const realtimeFailed: string[] = []
const [
timeSeriesResult,
percentilesResult,
providerPerformanceResult,
errorDistributionResult,
resilienceResult,
gatewayResult,
cacheResult,
redisResult,
] = results
if (refreshAnalytics && currentAnalyticsGeneration === analyticsGeneration) {
if (timeSeriesResult.status === 'rejected') analyticsFailed.push('流量趋势')
if (percentilesResult.status === 'rejected') analyticsFailed.push('延迟百分位')
if (providerPerformanceResult.status === 'rejected') analyticsFailed.push('上游性能')
if (errorDistributionResult.status === 'rejected') analyticsFailed.push('错误分类')
analyticsWarning.value = analyticsFailed.length
? `分析数据加载失败:${analyticsFailed.join('、')}`
: null
}
if (resilienceResult.status === 'rejected') realtimeFailed.push('韧性状态')
if (gatewayResult.status === 'rejected') realtimeFailed.push('网关指标')
if (cacheResult.status === 'rejected') realtimeFailed.push('缓存统计')
if (redisResult.status === 'rejected') realtimeFailed.push('Redis 分类')
results.forEach((result, index) => {
if (result.status === 'rejected') {
log.error(`运维总览加载失败 ${index}`, result.reason)
}
})
realtimeWarning.value = realtimeFailed.length
? `实时数据加载失败:${realtimeFailed.join('、')}`
: null
if (currentAnalyticsGeneration === analyticsGeneration) {
lastUpdatedAt.value = new Date().toISOString()
}
}
function refreshAll(): Promise<void> {
if (refreshPromise) {
return refreshPromise
}
const request = performRefresh(true).finally(() => {
if (refreshPromise === request) {
refreshPromise = null
}
refreshing.value = false
trendLoading.value = false
percentileLoading.value = false
})
refreshPromise = request
return request
}
function stopAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
function toggleAutoRefresh() {
autoRefresh.value = !autoRefresh.value
if (!autoRefresh.value) {
stopAutoRefresh()
return
}
void refreshAll()
autoRefreshTimer = setInterval(() => {
void refreshAll()
}, AUTO_REFRESH_INTERVAL)
}
const lastUpdatedLabel = computed(() => formatShortDate(lastUpdatedAt.value))
const totalRequests = computed(() => sumSeries('total_requests'))
const totalTokens = computed(() => timeSeries.value.reduce(
(total, item) => total + seriesTokenTotal(item),
0,
))
const totalCost = computed(() => sumSeries('total_cost'))
const classifiedErrorCount = computed(() => (
errorDistributionLoaded.value
? errorDistribution.value.reduce((total, item) => total + numeric(item.count), 0)
: null
))
const avgResponseMs = computed(() => providerPerformance.value?.summary.avg_response_time_ms ?? null)
const avgFirstByteMs = computed(() => providerPerformance.value?.summary.avg_first_byte_time_ms ?? average(percentiles.value.map(item => item.p50_first_byte_time_ms)))
const avgOutputTps = computed(() => providerPerformance.value?.summary.avg_output_tps ?? null)
const windowSeconds = computed(() => {
const start = timeRange.value.start_date ? new Date(timeRange.value.start_date).getTime() : NaN
const end = timeRange.value.end_date ? new Date(timeRange.value.end_date).getTime() : NaN
if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
return (end - start) / 1000
}
switch (timeRange.value.preset) {
case 'today': return Math.max(1, (Date.now() - new Date().setHours(0, 0, 0, 0)) / 1000)
case 'yesterday': return 86_400
case 'last7days': return 7 * 86_400
case 'last30days': return 30 * 86_400
case 'last90days': return 90 * 86_400
default: return Math.max(1, timeSeries.value.length * 86_400)
}
})
const qps = computed(() => totalRequests.value / windowSeconds.value)
const rpm = computed(() => qps.value * 60)
const tokensPerMinute = computed(() => totalTokens.value / Math.max(1, windowSeconds.value / 60))
const slaRate = computed(() => providerPerformance.value?.summary.success_rate ?? null)
const kpiCards = computed<Array<{
title: string
value: string
hint: string
icon: Component
iconClass: string
valueClass?: string
}>>(() => [
{
title: 'QPS',
value: qps.value.toFixed(qps.value < 10 ? 2 : 1),
hint: `RPM ${rpm.value.toFixed(rpm.value < 100 ? 1 : 0)} · 请求 ${formatMetricNumber(totalRequests.value)}`,
icon: Activity,
iconClass: 'text-sky-500',
},
{
title: '趋势 Tokens',
value: formatTokens(totalTokens.value),
hint: `趋势聚合 · ${formatMetricNumber(tokensPerMinute.value)} TPM`,
icon: Zap,
iconClass: 'text-amber-500',
},
{
title: 'SLA',
value: formatPercent(slaRate.value),
hint: `错误率 ${formatErrorRate(slaRate.value)}`,
icon: ShieldCheck,
iconClass: 'text-emerald-500',
valueClass: successRateClass(slaRate.value),
},
{
title: '请求时长',
value: formatMs(avgResponseMs.value),
hint: `P99 ${formatMs(providerPerformance.value?.summary.p99_response_time_ms)}`,
icon: Gauge,
iconClass: 'text-violet-500',
},
{
title: 'TTFT',
value: formatMs(avgFirstByteMs.value),
hint: `P99 首字 ${formatMs(providerPerformance.value?.summary.p99_first_byte_time_ms)}`,
icon: Timer,
iconClass: 'text-blue-500',
},
{
title: '输出 TPS',
value: formatTps(avgOutputTps.value),
hint: `慢请求 ${formatMetricNumber(providerPerformance.value?.summary.slow_request_count)}`,
icon: BarChart3,
iconClass: 'text-cyan-500',
},
{
title: '上游错误',
value: formatMetricNumber(resilienceStatus.value?.error_statistics.total_errors),
hint: `打开熔断 ${formatMetricNumber(resilienceStatus.value?.error_statistics.open_circuit_breakers)}`,
icon: AlertTriangle,
iconClass: 'text-red-500',
},
{
title: '费用',
value: formatCurrency(totalCost.value),
hint: `缓存读 ${formatTokens(cacheStats.value?.affinity_stats.cache_hits ?? 0)} 次`,
icon: CircleDollarSign,
iconClass: 'text-green-500',
},
])
const trafficChartData = computed<ChartData<'line'>>(() => ({
labels: timeSeries.value.map(item => String(item.date ?? item.bucket ?? item.time ?? '')),
datasets: [
{
label: '请求',
data: timeSeries.value.map(item => numeric(item.total_requests ?? item.requests)),
borderColor: 'rgb(14, 165, 233)',
backgroundColor: 'rgba(14, 165, 233, 0.12)',
tension: 0.25,
pointRadius: 2,
yAxisID: 'y',
},
{
label: 'Tokens',
data: timeSeries.value.map(seriesTokenTotal),
borderColor: 'rgb(245, 158, 11)',
backgroundColor: 'rgba(245, 158, 11, 0.12)',
tension: 0.25,
pointRadius: 2,
yAxisID: 'y1',
},
],
}))
const trafficChartOptions: ChartOptions<'line'> = {
interaction: { mode: 'index', intersect: false },
scales: {
y: { position: 'left' },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
},
},
}
const latencyChartData = computed<ChartData<'line'>>(() => ({
labels: percentiles.value.map(item => item.date),
datasets: [
{
label: 'P90 请求',
data: percentiles.value.map(item => item.p90_response_time_ms ?? null),
borderColor: 'rgb(124, 58, 237)',
tension: 0.25,
pointRadius: 2,
},
{
label: 'P99 请求',
data: percentiles.value.map(item => item.p99_response_time_ms ?? null),
borderColor: 'rgb(239, 68, 68)',
tension: 0.25,
pointRadius: 2,
},
{
label: 'P90 首字',
data: percentiles.value.map(item => item.p90_first_byte_time_ms ?? null),
borderColor: 'rgb(14, 165, 233)',
tension: 0.25,
pointRadius: 2,
},
],
}))
const latencyChartOptions: ChartOptions<'line'> = {
scales: {
y: {
ticks: {
callback: value => formatMs(Number(value)),
},
},
},
}
const errorDistributionChartData = computed<ChartData<'doughnut'>>(() => {
const rows = errorDistribution.value.length
? errorDistribution.value
: [{ category: '无错误', count: 1 }]
return {
labels: rows.map(item => item.category),
datasets: [
{
data: rows.map(item => item.count),
backgroundColor: [
'rgba(239, 68, 68, 0.82)',
'rgba(245, 158, 11, 0.82)',
'rgba(14, 165, 233, 0.82)',
'rgba(99, 102, 241, 0.82)',
'rgba(34, 197, 94, 0.82)',
],
},
],
}
})
const errorDistributionOptions: ChartOptions<'doughnut'> = {
plugins: {
legend: {
position: 'bottom',
},
tooltip: {
callbacks: {
label: context => `${context.label}: ${formatMetricNumber(context.raw as number)}`,
},
},
},
}
const distributedGateVariant = computed<'warning' | 'outline'>(() => (
gatewayMetrics.value?.distributed.unavailable ? 'warning' : 'outline'
))
const distributedGateText = computed(() => (
gatewayMetrics.value?.distributed.unavailable ? '全局不可用' : '全局在线'
))
const currentActiveStreams = computed(() => gatewayMetrics.value?.tunnel.activeStreams ?? null)
const currentProxyConnections = computed(() => gatewayMetrics.value?.tunnel.proxyConnections ?? null)
const localGateUtilization = computed(() => gateUtilization(gatewayMetrics.value?.local))
const distributedGateUtilization = computed(() => gateUtilization(gatewayMetrics.value?.distributed))
const candidatePlanningGateUtilization = computed(() => gateUtilization(gatewayMetrics.value?.candidatePlanning))
const upstreamExecutionGateUtilization = computed(() => gateUtilization(gatewayMetrics.value?.upstreamExecution))
const localGateUtilizationText = computed(() => formatCapacityPercent(localGateUtilization.value))
const distributedGateUtilizationText = computed(() => formatCapacityPercent(distributedGateUtilization.value))
const candidatePlanningGateUtilizationText = computed(() => formatCapacityPercent(candidatePlanningGateUtilization.value))
const upstreamExecutionGateUtilizationText = computed(() => formatCapacityPercent(upstreamExecutionGateUtilization.value))
const admissionStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
if (!gatewayMetrics.value) return 'outline'
if (gatewayMetrics.value.distributed.unavailable) return 'destructive'
if (
(localGateUtilization.value ?? 0) >= 90
|| (distributedGateUtilization.value ?? 0) >= 90
|| (candidatePlanningGateUtilization.value ?? 0) >= 90
|| (upstreamExecutionGateUtilization.value ?? 0) >= 90
) return 'warning'
return 'success'
})
const admissionStatusText = computed(() => {
if (!gatewayMetrics.value) return '未接入'
if (gatewayMetrics.value.distributed.unavailable) return '分布式异常'
if (admissionStatusVariant.value === 'warning') return '接近上限'
return '正常'
})
const databasePoolUsagePercent = computed(() => {
const basisPoints = gatewayMetrics.value?.databasePool.usageBasisPoints
return basisPoints == null ? null : basisPoints / 100
})
const databasePoolUsageText = computed(() => formatCapacityPercent(databasePoolUsagePercent.value))
const databasePoolCheckedOutText = computed(() => formatMetricRatio(
gatewayMetrics.value?.databasePool.checkedOut,
gatewayMetrics.value?.databasePool.max
))
const postgresWaitingText = computed(() => formatMetricRatio(
gatewayMetrics.value?.postgres.waitingConnections,
gatewayMetrics.value?.postgres.activeConnections
))
const postgresOldestActiveQueryText = computed(() => formatMs(
gatewayMetrics.value?.postgres.oldestActiveQueryAgeMs
))
const postgresOldestTransactionText = computed(() => formatMs(
gatewayMetrics.value?.postgres.oldestTransactionAgeMs
))
const postgresCacheHitPercent = computed(() => (
basisPointsPercent(gatewayMetrics.value?.postgres.blockCacheHitRateBasisPoints)
))
const postgresCacheHitText = computed(() => formatBasisPointsPercent(
gatewayMetrics.value?.postgres.blockCacheHitRateBasisPoints
))
const postgresCacheHitClass = computed(() => {
const value = postgresCacheHitPercent.value
if (value == null || Number.isNaN(value)) return ''
if (value < 90) return 'text-red-600 dark:text-red-400'
if (value < 95) return 'text-amber-600 dark:text-amber-400'
return 'text-green-600 dark:text-green-400'
})
function postgresOptionalText(
available: boolean | null | undefined,
unavailable: boolean | null | undefined,
value: string,
): string {
if (unavailable) return '异常'
if (available === false) return '未接入'
return value
}
const postgresWalText = computed(() => {
const postgres = gatewayMetrics.value?.postgres
return postgresOptionalText(
postgres?.walAvailable,
postgres?.walUnavailable,
formatBytes(postgres?.walBytesTotal),
)
})
const postgresWalClass = computed(() => (
gatewayMetrics.value?.postgres.walUnavailable ? 'text-red-600 dark:text-red-400' : ''
))
const postgresCheckpointText = computed(() => {
const postgres = gatewayMetrics.value?.postgres
const value = `${formatMs(postgres?.checkpointWriteTimeMsTotal)} / ${formatMs(postgres?.checkpointSyncTimeMsTotal)}`
return postgresOptionalText(postgres?.checkpointAvailable, postgres?.checkpointUnavailable, value)
})
const postgresCheckpointClass = computed(() => (
gatewayMetrics.value?.postgres.checkpointUnavailable ? 'text-red-600 dark:text-red-400' : ''
))
const postgresTopStatementText = computed(() => {
const postgres = gatewayMetrics.value?.postgres
return postgresOptionalText(
postgres?.statementAvailable,
postgres?.statementUnavailable,
formatMs(postgres?.statementTopMaxExecTimeMs),
)
})
const postgresStatementClass = computed(() => {
const postgres = gatewayMetrics.value?.postgres
if (postgres?.statementUnavailable) return 'text-red-600 dark:text-red-400'
return latencyToneClass(postgres?.statementTopMaxExecTimeMs)
})
const databasePoolStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
const pool = gatewayMetrics.value?.databasePool
const postgres = gatewayMetrics.value?.postgres
if (!pool || pool.max == null) return postgres?.unavailable ? 'destructive' : 'outline'
if (
pool.underMaintenancePressure
|| postgres?.unavailable
|| (postgres?.lockWaitingConnections ?? 0) > 0
|| (postgres?.idleInTransactionConnections ?? 0) > 0
|| (postgres?.oldestActiveQueryAgeMs ?? 0) >= 60_000
|| (postgres?.oldestTransactionAgeMs ?? 0) >= 60_000
|| (postgres?.walUnavailable ?? false)
|| (postgres?.checkpointUnavailable ?? false)
|| (postgres?.statementUnavailable ?? false)
|| (postgres?.statementTopMaxExecTimeMs ?? 0) >= 60_000
|| (databasePoolUsagePercent.value ?? 0) >= 90
) return 'destructive'
if ((postgres?.waitingConnections ?? 0) > 0 || (databasePoolUsagePercent.value ?? 0) >= 80) return 'warning'
return 'success'
})
const databasePoolStatusText = computed(() => {
const pool = gatewayMetrics.value?.databasePool
const postgres = gatewayMetrics.value?.postgres
if (!pool || pool.max == null) return postgres?.unavailable ? 'PG 异常' : '未接入'
if (postgres?.unavailable) return 'PG 异常'
if ((postgres?.lockWaitingConnections ?? 0) > 0) return '锁等待'
if ((postgres?.idleInTransactionConnections ?? 0) > 0) return '长事务'
if (postgres?.walUnavailable) return 'WAL 异常'
if (postgres?.checkpointUnavailable) return 'Checkpoint 异常'
if (postgres?.statementUnavailable) return 'Top SQL 异常'
if ((postgres?.statementTopMaxExecTimeMs ?? 0) >= 60_000) return '慢 SQL'
if (pool.underMaintenancePressure) return '维护压力'
if (databasePoolStatusVariant.value === 'destructive') return '高压'
if (databasePoolStatusVariant.value === 'warning') return '偏高'
return '正常'
})
const gatewayProcessCpuPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.process.processCpuUsageBasisPoints))
const gatewaySystemCpuPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.process.systemCpuUsageBasisPoints))
const gatewayProcessMemoryPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.process.processMemoryBasisPoints))
const gatewaySystemMemoryPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.process.systemMemoryUsageBasisPoints))
const gatewayProcessFdPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.process.fdUsageBasisPoints))
const gatewayAllocatorActivePercent = computed(() => basisPointsPercent(gatewayMetrics.value?.allocator.activeToAllocatedBasisPoints))
const gatewayAllocatorActivePercentBasisPoints = computed(() => gatewayMetrics.value?.allocator.activeToAllocatedBasisPoints ?? null)
const gatewayProcessFdText = computed(() => formatMetricRatio(
gatewayMetrics.value?.process.openFds,
gatewayMetrics.value?.process.fdLimit
))
const gatewayProcessSocketFdText = computed(() => formatMetricRatio(
gatewayMetrics.value?.process.socketFds,
gatewayMetrics.value?.process.openFds
))
const gatewayProcessTcpText = computed(() => formatMetricRatio(
gatewayMetrics.value?.process.processTcpEstablishedConnections,
gatewayMetrics.value?.process.processTcpConnections
))
const gatewayBackgroundTaskText = computed(() => formatMetricRatio(
gatewayMetrics.value?.backgroundTasks.active,
gatewayMetrics.value?.backgroundTasks.supervisedTotal
))
const gatewayBackgroundTaskUnexpectedExits = computed(() => {
const tasks = gatewayMetrics.value?.backgroundTasks
if (!tasks) return null
return tasks.unexpectedExitsTotal
})
const gatewayNetworkTrafficText = computed(() => {
const process = gatewayMetrics.value?.process
if (process?.networkReceivedBytesTotal == null && process?.networkTransmittedBytesTotal == null) return '-'
return `RX ${formatBytes(process?.networkReceivedBytesTotal)} / TX ${formatBytes(process?.networkTransmittedBytesTotal)}`
})
const gatewayNetworkFaults = computed(() => {
const process = gatewayMetrics.value?.process
if (!process) return null
return (process.networkReceiveErrorsTotal ?? 0)
+ (process.networkTransmitErrorsTotal ?? 0)
+ (process.networkReceiveDroppedTotal ?? 0)
+ (process.networkTransmitDroppedTotal ?? 0)
})
const gatewayTcpStateText = computed(() => {
const process = gatewayMetrics.value?.process
if (!process || process.tcpStateAvailable == null) return '-'
if (!process.tcpStateAvailable) return '未采集'
return `host ${formatMetricNumber(process.hostTcpEstablishedConnections)} established · ${formatMetricNumber(process.hostTcpTimeWaitConnections)} time-wait`
})
const gatewayProcessUptimeText = computed(() => formatDurationSeconds(gatewayMetrics.value?.process.processUptimeSeconds))
const gatewayAllocatorStatusText = computed(() => {
const allocator = gatewayMetrics.value?.allocator
if (!allocator || allocator.available == null) return '-'
if (!allocator.available) return 'unavailable'
return `allocated ${formatBytes(allocator.allocatedBytes)}`
})
const gatewayProcessStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
const process = gatewayMetrics.value?.process
const backgroundTasks = gatewayMetrics.value?.backgroundTasks
const tokioRuntime = gatewayMetrics.value?.tokioRuntime
if (!process || !hasCapacitySignal(
process.processCpuUsageBasisPoints,
process.processMemoryBytes,
process.fdUsageBasisPoints,
process.socketFds,
process.processTcpConnections,
backgroundTasks?.active,
backgroundTasks?.supervisedTotal,
tokioRuntime?.aliveTasks,
tokioRuntime?.globalQueueDepth,
)) return 'outline'
if (
(process.processTcpCloseWaitConnections ?? 0) > 0
|| tokioRuntime?.available === false
|| (backgroundTasks?.unexpectedExitsTotal ?? 0) > 0
|| (backgroundTasks?.panickedTotal ?? 0) > 0
|| (backgroundTasks?.abortedTotal ?? 0) > 0
|| (gatewayProcessFdPercent.value ?? 0) >= 70
|| (gatewaySystemCpuPercent.value ?? 0) >= 90
|| (gatewayProcessMemoryPercent.value ?? 0) >= 90
) return 'destructive'
if (
(gatewayProcessFdPercent.value ?? 0) >= 60
|| (gatewaySystemCpuPercent.value ?? 0) >= 70
|| (gatewayProcessMemoryPercent.value ?? 0) >= 70
) return 'warning'
return 'success'
})
const gatewayProcessStatusText = computed(() => {
if (gatewayProcessStatusVariant.value === 'outline') return '未接入'
if ((gatewayMetrics.value?.process.processTcpCloseWaitConnections ?? 0) > 0) return '连接泄漏'
if (gatewayMetrics.value?.tokioRuntime.available === false) return 'Runtime 异常'
if ((gatewayMetrics.value?.backgroundTasks.unexpectedExitsTotal ?? 0) > 0) return '任务退出'
if (gatewayProcessStatusVariant.value === 'destructive') return '超阈值'
if (gatewayProcessStatusVariant.value === 'warning') return '接近上限'
return '正常'
})
const usageWorkerText = computed(() => {
const usage = gatewayMetrics.value?.usageRuntime
return formatMetricRatio(
usage?.workerActiveCount ?? usage?.workerCount,
usage?.workerMaxCount
)
})
const usageRuntimeWorkerFaults = computed(() => {
const usage = gatewayMetrics.value?.usageRuntime
if (!usage) return null
const values = [
usage.workerDeadLetteredEntriesTotal,
usage.workerProcessFailuresTotal,
usage.workerReadFailuresTotal,
usage.workerReclaimFailuresTotal,
]
if (!values.some(value => value != null)) return null
return values.reduce((total, value) => total + (value ?? 0), 0)
})
const usageQueueOldestPendingIdleText = computed(() => (
formatDurationSeconds((gatewayMetrics.value?.usageQueue.oldestPendingIdleMs ?? null) == null
? null
: (gatewayMetrics.value?.usageQueue.oldestPendingIdleMs ?? 0) / 1000)
))
const usageCounterOldestPendingAgeText = computed(() => (
formatDurationSeconds(gatewayMetrics.value?.usageCounter.oldestPendingAgeSeconds)
))
const usageCounterOldestPendingAgeClass = computed(() => {
const age = gatewayMetrics.value?.usageCounter.oldestPendingAgeSeconds
if (age == null || Number.isNaN(age)) return ''
if (age >= 60) return 'text-red-600 dark:text-red-400'
if (age > 0) return 'text-amber-600 dark:text-amber-400'
return 'text-green-600 dark:text-green-400'
})
const usageCounterHealthText = computed(() => {
const counter = gatewayMetrics.value?.usageCounter
if (!counter || counter.unavailable == null) return '-'
if (counter.unavailable) return 'unavailable'
if ((counter.oldestPendingAgeSeconds ?? 0) >= 60) return 'backlogged'
if ((counter.pendingRows ?? 0) > 0) return 'catching-up'
return 'ok'
})
const usageCounterOutboxFailures = computed(() => {
const counter = gatewayMetrics.value?.usageCounter
if (!counter) return null
const values = [
counter.flushFailedBatchesTotal,
counter.cleanupFailedBatchesTotal,
]
if (!values.some(value => value != null)) return null
return values.reduce((total, value) => total + (value ?? 0), 0)
})
const candidateQueueUtilization = computed(() => {
const depth = gatewayMetrics.value?.requestCandidateQueue.depth
const capacity = gatewayMetrics.value?.requestCandidateQueue.capacity
if (depth == null || capacity == null || capacity <= 0) return null
return Math.max(0, Math.min(100, depth / capacity * 100))
})
const candidateQueueUtilizationText = computed(() => formatCapacityPercent(candidateQueueUtilization.value))
const candidateQueueUtilizationWidth = computed(() => (
candidateQueueUtilization.value == null ? '0%' : `${candidateQueueUtilization.value}%`
))
const candidateQueueDepthText = computed(() => formatMetricRatio(
gatewayMetrics.value?.requestCandidateQueue.depth,
gatewayMetrics.value?.requestCandidateQueue.capacity
))
const queueStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
const usage = gatewayMetrics.value?.usageRuntime
const queue = gatewayMetrics.value?.requestCandidateQueue
const usageQueue = gatewayMetrics.value?.usageQueue
const counter = gatewayMetrics.value?.usageCounter
const hasUsageQueueSignal = usageQueue?.unavailable != null || hasCapacitySignal(
usageQueue?.groupPending,
usageQueue?.groupLag,
usageQueue?.dlqLength,
)
const hasUsageCounterSignal = counter?.unavailable != null || hasCapacitySignal(
counter?.pendingRows,
counter?.processedRows,
counter?.oldestPendingAgeSeconds,
)
if (!gatewayMetrics.value || (!hasUsageQueueSignal && !hasUsageCounterSignal && !hasCapacitySignal(
usage?.workerActiveCount,
queue?.depth,
queue?.capacity,
))) return 'outline'
const hardFailures = (usage?.terminalEnqueueFailedTotal ?? 0)
+ (usage?.lifecycleEnqueueFailedTotal ?? 0)
+ (queue?.flushFailedTotal ?? 0)
+ (queue?.syncFallbackTotal ?? 0)
+ (usageRuntimeWorkerFaults.value ?? 0)
if (
hardFailures > 0
|| usageQueue?.unavailable
|| (usageQueue?.dlqLength ?? 0) > 0
|| (usageQueue?.oldestPendingIdleMs ?? 0) >= 60_000
|| counter?.unavailable
|| (usageCounterOutboxFailures.value ?? 0) > 0
|| (counter?.oldestPendingAgeSeconds ?? 0) >= 60
) return 'destructive'
if (
(candidateQueueUtilization.value ?? 0) >= 80
|| (usage?.lifecycleEnqueueDeferredDroppedTotal ?? 0) > 0
|| (usageQueue?.groupPending ?? 0) > 0
|| (usageQueue?.groupLag ?? 0) > 0
|| (counter?.pendingRows ?? 0) > 0
) return 'warning'
return 'success'
})
const queueStatusText = computed(() => {
if (queueStatusVariant.value === 'outline') return '未接入'
if (queueStatusVariant.value === 'destructive') return '异常'
if (queueStatusVariant.value === 'warning') return '积压'
return '正常'
})
const upstreamTargetRows = computed(() => gatewayMetrics.value?.upstreamTargets.rows ?? [])
const upstreamTargetStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
const targets = gatewayMetrics.value?.upstreamTargets
if (!targets || targets.activeTargets == null) return 'outline'
if (targets.rejectedTotal > 0) return 'destructive'
if (targets.saturatedTotal > 0) return 'warning'
return 'success'
})
const upstreamTargetStatusText = computed(() => {
if (upstreamTargetStatusVariant.value === 'outline') return '未接入'
if (upstreamTargetStatusVariant.value === 'destructive') return '有拒绝'
if (upstreamTargetStatusVariant.value === 'warning') return '有饱和'
return '正常'
})
const stageLatencyRows = computed(() => gatewayMetrics.value?.stageLatency.rows ?? [])
const tunnelQueueRejectedTotal = computed(() => (
(gatewayMetrics.value?.tunnel.outboundQueueRejectedFullTotal ?? 0)
+ (gatewayMetrics.value?.tunnel.outboundQueueRejectedClosedTotal ?? 0)
))
const tunnelSelectionPressureTotal = computed(() => (
(gatewayMetrics.value?.tunnel.proxyConnectionCongestedTotal ?? 0)
+ (gatewayMetrics.value?.tunnel.selectionRetryTotal ?? 0)
+ (gatewayMetrics.value?.tunnel.selectionUnavailableTotal ?? 0)
))
const tunnelQueueUtilization = computed(() => {
const depth = gatewayMetrics.value?.tunnel.outboundQueueDepthTotal
const capacity = gatewayMetrics.value?.tunnel.outboundQueueCapacityTotal
if (depth == null || capacity == null || capacity <= 0) return null
return Math.max(0, Math.min(100, depth / capacity * 100))
})
const tunnelQueueUtilizationText = computed(() => (
tunnelQueueUtilization.value == null ? '-' : `${Math.round(tunnelQueueUtilization.value)}%`
))
const tunnelQueueUtilizationWidth = computed(() => (
tunnelQueueUtilization.value == null ? '0%' : `${tunnelQueueUtilization.value}%`
))
const slaValueClass = computed(() => successRateClass(providerPerformance.value?.summary.success_rate))
const errorRateValueClass = computed(() => {
const rate = providerPerformance.value?.summary.success_rate
if (rate == null) return ''
const successPercent = rate <= 1 ? rate * 100 : rate
const errorPercent = Math.max(0, 100 - successPercent)
if (errorPercent <= 5) return 'text-green-600 dark:text-green-400'
if (errorPercent <= 20) return 'text-amber-600 dark:text-amber-400'
return 'text-red-600 dark:text-red-400'
})
const providerRows = computed(() => providerPerformance.value?.providers.slice(0, 8) ?? [])
const cacheHitRate = computed(() => cacheStats.value?.affinity_stats.cache_hit_rate ?? null)
const redisStatusText = computed(() => {
const runtime = gatewayMetrics.value?.redisRuntime
if (runtime?.unavailable) return '异常'
if (runtime?.enabled) return '在线'
if (!redisCategories.value) return '-'
return redisCategories.value.available ? '在线' : '未启用'
})
const redisStatusClass = computed(() => {
const runtime = gatewayMetrics.value?.redisRuntime
if (runtime?.unavailable) return 'text-red-600 dark:text-red-400'
if (
(redisRuntimeCommandFaults.value ?? 0) > 0
|| (redisRuntimeMemoryPercent.value ?? 0) >= 90
|| (gatewayMetrics.value?.redisRuntime.nonblockingCommandLatencyMaxMs ?? 0) >= 500
) {
return 'text-red-600 dark:text-red-400'
}
if (
(redisRuntimeMemoryPercent.value ?? 0) >= 80
|| (gatewayMetrics.value?.redisRuntime.nonblockingCommandLatencyMaxMs ?? 0) >= 100
) return 'text-amber-600 dark:text-amber-400'
if (runtime?.enabled) return 'text-green-600 dark:text-green-400'
if (!redisCategories.value) return ''
return redisCategories.value.available ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'
})
const redisRuntimeMemoryPercent = computed(() => basisPointsPercent(gatewayMetrics.value?.redisRuntime.memoryUsageBasisPoints))
const redisRuntimeMemoryText = computed(() => {
const runtime = gatewayMetrics.value?.redisRuntime
if (runtime?.usedMemoryBytes == null) return '-'
if ((runtime.maxmemoryBytes ?? 0) > 0) {
return `${formatBytes(runtime.usedMemoryBytes)} / ${formatBytes(runtime.maxmemoryBytes)}`
}
return formatBytes(runtime.usedMemoryBytes)
})
const redisRuntimeOpsText = computed(() => {
const ops = gatewayMetrics.value?.redisRuntime.instantaneousOpsPerSec
if (ops == null || Number.isNaN(ops)) return '-'
return `${formatMetricNumber(ops)}/s`
})
const redisRuntimeLatencyText = computed(() => formatMs(
gatewayMetrics.value?.redisRuntime.nonblockingCommandLatencyMaxMs
))
const redisRuntimeClientsText = computed(() => formatMetricRatio(
gatewayMetrics.value?.redisRuntime.connectedClients,
gatewayMetrics.value?.redisRuntime.blockedClients
))
const redisRuntimeCommandFaults = computed(() => {
const runtime = gatewayMetrics.value?.redisRuntime
if (!runtime) return null
return (runtime.laneCommandErrorsTotal ?? 0)
+ (runtime.laneCommandTimeoutsTotal ?? 0)
+ (runtime.totalErrorReplies ?? 0)
+ (runtime.rejectedConnectionsTotal ?? 0)
+ (runtime.evictedKeysTotal ?? 0)
})
const redisCategoryRows = computed(() => (
redisCategories.value?.categories
.slice()
.sort((left, right) => right.count - left.count)
.slice(0, 6) ?? []
))
const recentErrors = computed<AdminMonitoringRecentError[]>(() => resilienceStatus.value?.recent_errors.slice(0, 6) ?? [])
const opsLinks = [
{ label: '性能分析', to: '/admin/performance-analysis', icon: Gauge },
{ label: '健康监控', to: '/admin/health-monitor', icon: ShieldCheck },
{ label: '使用记录', to: '/admin/usage', icon: ListChecks },
{ label: '缓存监控', to: '/admin/cache-monitoring', icon: Database },
{ label: '审计日志', to: '/admin/audit-logs', icon: AlertTriangle },
{ label: '异步任务', to: '/admin/async-tasks', icon: RefreshCw },
]
watch(timeRange, () => {
analyticsGeneration += 1
timeSeries.value = []
percentiles.value = []
providerPerformance.value = null
errorDistribution.value = []
errorDistributionLoaded.value = false
analyticsWarning.value = null
lastUpdatedAt.value = null
trendLoading.value = false
percentileLoading.value = false
if (autoRefresh.value) {
void refreshAll()
}
}, { deep: true })
onMounted(() => {
void refreshAll()
})
onUnmounted(() => {
stopAutoRefresh()
requestId += 1
analyticsGeneration += 1
})
</script>