2026-02-04 02:04:54 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="space-y-6 px-4 sm:px-6 lg:px-0">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<div>
|
2026-02-04 23:59:45 +08:00
|
|
|
|
<h1 class="text-lg font-semibold">
|
|
|
|
|
|
性能分析
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
实时性能监控与历史延迟趋势
|
2026-02-04 23:59:45 +08:00
|
|
|
|
</p>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
</div>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
实时 10s 刷新
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">
|
|
|
|
|
|
上次更新 {{ liveLastUpdatedLabel }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="isRefreshing"
|
|
|
|
|
|
title="刷新实时与历史性能数据"
|
|
|
|
|
|
@click="handleManualRefresh"
|
|
|
|
|
|
/>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<TimeRangePicker
|
|
|
|
|
|
v-model="timeRange"
|
|
|
|
|
|
:show-granularity="false"
|
|
|
|
|
|
/>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<Card class="overflow-hidden">
|
|
|
|
|
|
<div class="border-b border-border/70 bg-muted/20 px-4 py-3 sm:px-5">
|
|
|
|
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
实时运行状态
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</h2>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
聚合系统健康、并发保护、代理通道与降级切换
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Badge :variant="healthStatusVariant">
|
|
|
|
|
|
{{ healthStatusText }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
{{ metricsAvailabilityText }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="p-4 sm:p-5">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="liveLoading && !liveReady"
|
|
|
|
|
|
class="py-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LoadingState message="加载实时性能数据中" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="!liveReady"
|
|
|
|
|
|
class="rounded-xl border border-dashed border-border/70 bg-muted/15 px-4 py-6 text-sm text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
实时性能数据暂不可用,请稍后重试。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="liveLoadError"
|
|
|
|
|
|
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"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ liveLoadError }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="card in liveSummaryCards"
|
|
|
|
|
|
:key="card.title"
|
|
|
|
|
|
class="rounded-xl border border-border/70 bg-card/70 px-4 py-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">{{ card.title }}</span>
|
|
|
|
|
|
<component
|
|
|
|
|
|
:is="card.icon"
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
:class="card.iconClass"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-3 text-2xl font-semibold tracking-tight">
|
|
|
|
|
|
{{ card.value }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ card.hint }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:col-span-2">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4 lg:col-span-2">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
并发保护
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<Badge variant="outline">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
全局 {{ distributedGateText }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
|
|
|
|
<div class="rounded-lg border border-border/60 bg-background/50 px-3 py-3">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div class="text-sm font-medium">
|
|
|
|
|
|
当前节点
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
本机
|
|
|
|
|
|
</Badge>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
处理中
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.local.inFlight) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
可接入
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.local.availablePermits) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
峰值并发
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.local.highWatermark) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
被限流
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.local.rejectedTotal) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="rounded-lg border border-border/60 bg-background/50 px-3 py-3">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div class="text-sm font-medium">
|
|
|
|
|
|
全局保护
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Badge :variant="distributedGateVariant">
|
|
|
|
|
|
{{ distributedGateText }}
|
|
|
|
|
|
</Badge>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
处理中
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.distributed.inFlight) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
可接入
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.distributed.availablePermits) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
峰值并发
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.distributed.highWatermark) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
被限流
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.distributed.rejectedTotal) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="gatewayMetrics?.distributed.unavailable"
|
|
|
|
|
|
class="mt-3 text-xs text-yellow-700 dark:text-yellow-300"
|
|
|
|
|
|
>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
全局并发保护暂不可用,请检查 Redis 连接。
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
代理通道
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
实时连接
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
节点数
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(currentTunnelNodes) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
可用连接
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.availableProxyConnections) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
活跃流
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
|
|
|
|
|
{{ formatMetricNumber(currentActiveStreams) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
避让连接
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.softAvoidProxyConnections) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
代理通道压力
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<Badge variant="outline">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
排队 {{ tunnelQueueUtilizationText }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
排队中
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.outboundQueueDepthTotal) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
峰值排队
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.outboundQueueDepthMax) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
队列满拒绝
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(tunnelQueueRejectedTotal) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
无可用通道
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-lg font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatMetricNumber(tunnelSelectionPressureTotal) }}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
降级切换统计
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">
|
|
|
|
|
|
总计 {{ formatMetricNumber(gatewayMetrics?.fallbackTotal) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!fallbackRows.length"
|
|
|
|
|
|
class="mt-4 rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
|
|
|
|
|
>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
当前没有记录到降级切换。
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="mt-4 space-y-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in fallbackRows"
|
|
|
|
|
|
:key="item.name"
|
|
|
|
|
|
class="rounded-lg border border-border/60 bg-background/50 px-3 py-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<span class="text-sm font-medium">{{ item.label }}</span>
|
|
|
|
|
|
<span class="text-sm font-semibold">{{ formatMetricNumber(item.total) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2 h-2 overflow-hidden rounded-full bg-muted">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="h-full rounded-full bg-primary/80"
|
|
|
|
|
|
:style="{ width: `${Math.max(item.ratio, 8)}%` }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<div class="grid grid-cols-1 items-stretch gap-4 xl:grid-cols-2">
|
|
|
|
|
|
<section class="flex h-full flex-col rounded-xl border border-border/70 bg-card/60 p-4">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
|
|
|
|
|
最近错误
|
|
|
|
|
|
</h3>
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="hasMoreRecentErrors"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-7 gap-1 px-2 text-xs"
|
|
|
|
|
|
:title="recentErrorsExpanded ? '收起最近错误' : '展开最近错误'"
|
|
|
|
|
|
@click="recentErrorsExpanded = !recentErrorsExpanded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component
|
|
|
|
|
|
:is="recentErrorsExpanded ? ChevronUp : ChevronDown"
|
|
|
|
|
|
class="h-3.5 w-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{{ recentErrorsExpanded ? '收起' : `展开 ${recentErrors.length}` }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ formatMetricNumber(resilienceStatus?.error_statistics.total_errors) }} / 24h
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!recentErrors.length"
|
2026-05-06 09:09:55 +08:00
|
|
|
|
class="mt-4 flex-1 rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
2026-04-13 14:53:46 +08:00
|
|
|
|
>
|
|
|
|
|
|
当前没有最近错误。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
2026-05-06 09:09:55 +08:00
|
|
|
|
class="mt-4 min-h-0 flex-1"
|
2026-04-13 14:53:46 +08:00
|
|
|
|
>
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<div :class="recentErrorsListClass">
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="item in visibleRecentErrors"
|
|
|
|
|
|
:key="item.error_id"
|
|
|
|
|
|
class="rounded-lg border border-border/60 bg-background/50 px-3 py-3"
|
2026-04-13 14:53:46 +08:00
|
|
|
|
>
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-sm font-medium">
|
|
|
|
|
|
{{ item.error_type }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ item.operation }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="shrink-0 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ formatDate(item.timestamp) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
HTTP {{ item.context.status_code ?? '-' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
{{ item.context.provider_name || item.context.provider_id || '未知上游' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
{{ item.context.api_format || item.context.model || '未知格式' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="item.context.error_message"
|
|
|
|
|
|
class="mt-2 line-clamp-2 break-words text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.context.error_message }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<section class="flex h-full flex-col rounded-xl border border-border/70 bg-card/60 p-4">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
|
|
|
|
|
熔断历史与建议
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">
|
|
|
|
|
|
开路 {{ formatMetricNumber(resilienceStatus?.error_statistics.open_circuit_breakers) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="!circuitHistory.length"
|
|
|
|
|
|
class="mt-4 rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
当前没有熔断事件。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="mt-4 space-y-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<article
|
|
|
|
|
|
v-for="item in circuitHistory"
|
|
|
|
|
|
:key="`${item.key_id}-${item.api_format}-${item.timestamp}`"
|
|
|
|
|
|
class="rounded-lg border border-border/60 bg-background/50 px-3 py-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<span class="text-sm font-medium">
|
|
|
|
|
|
{{ item.provider_name || item.provider_id }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Badge :variant="item.event === 'opened' ? 'destructive' : 'warning'">
|
|
|
|
|
|
{{ item.event === 'opened' ? '已打开' : '半开' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ item.key_name || item.key_id }} · {{ item.api_format || '未知格式' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="shrink-0 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ formatDate(item.timestamp) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-2 text-xs text-muted-foreground">
|
|
|
|
|
|
原因:{{ item.reason || '未提供' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
恢复窗口:{{ item.recovery_seconds != null ? `${item.recovery_seconds}s` : '-' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-4 border-t border-border/70 pt-4">
|
|
|
|
|
|
<h4 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
建议
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<ul
|
|
|
|
|
|
v-if="resilienceRecommendations.length"
|
|
|
|
|
|
class="mt-3 space-y-2 text-sm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="item in resilienceRecommendations"
|
|
|
|
|
|
:key="item"
|
|
|
|
|
|
class="rounded-lg border border-border/60 bg-background/50 px-3 py-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item }}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="mt-3 text-sm text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
当前没有额外运维建议。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-05-05 11:24:16 +08:00
|
|
|
|
<Card class="space-y-4 p-4">
|
|
|
|
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
上游服务性能
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ providerPerformanceSubtitle }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
Top {{ providerPerformanceRows.length || 0 }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="hasProviderPerformanceFilters"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
class="h-8 gap-1 px-2 text-xs"
|
|
|
|
|
|
title="清除上游服务性能筛选"
|
|
|
|
|
|
@click="resetProviderPerformanceFilters"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FilterX class="h-3.5 w-3.5" />
|
|
|
|
|
|
清除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-2 pt-1 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="providerPerformanceProviderId"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
placeholder="上游 ID"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="providerPerformanceModel"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
placeholder="模型"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="providerPerformanceApiFormat"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
placeholder="API 格式"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="providerPerformanceEndpointKind"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
placeholder="端点类型"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select v-model="providerPerformanceIsStream">
|
|
|
|
|
|
<SelectTrigger class="h-8 text-xs border-border/60">
|
|
|
|
|
|
<SelectValue placeholder="流式" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部流式
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="true">
|
|
|
|
|
|
仅流式
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="false">
|
|
|
|
|
|
非流式
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select v-model="providerPerformanceHasFormatConversion">
|
|
|
|
|
|
<SelectTrigger class="h-8 text-xs border-border/60">
|
|
|
|
|
|
<SelectValue placeholder="格式转换" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部转换
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="true">
|
|
|
|
|
|
仅转换
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="false">
|
|
|
|
|
|
不转换
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="providerPerformanceSlowThresholdMs"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
max="600000"
|
|
|
|
|
|
placeholder="慢请求阈值 ms"
|
|
|
|
|
|
/>
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="providerPerformanceLoading"
|
|
|
|
|
|
class="p-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LoadingState />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="card in providerPerformanceSummaryCards"
|
|
|
|
|
|
:key="card.title"
|
|
|
|
|
|
class="rounded-xl border border-border/70 bg-card/70 px-4 py-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">{{ card.title }}</span>
|
|
|
|
|
|
<component
|
|
|
|
|
|
:is="card.icon"
|
|
|
|
|
|
class="h-4 w-4"
|
|
|
|
|
|
:class="card.iconClass"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-3 text-2xl font-semibold tracking-tight">
|
|
|
|
|
|
{{ card.value }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ card.hint }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="providerPerformanceRows.length"
|
|
|
|
|
|
class="overflow-x-auto rounded-lg border border-border/70"
|
|
|
|
|
|
>
|
|
|
|
|
|
<table class="min-w-full divide-y divide-border/70 text-sm">
|
|
|
|
|
|
<thead class="bg-muted/30 text-xs text-muted-foreground">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th class="px-3 py-2 text-left font-medium">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
上游服务
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
请求
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
成功率
|
|
|
|
|
|
</th>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
错误率
|
|
|
|
|
|
</th>
|
2026-05-05 11:24:16 +08:00
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
输出 TPS
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
平均首字
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
平均响应
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
P90/P99 响应
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
P90/P99 首字
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
慢请求
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th class="px-3 py-2 text-right font-medium">
|
|
|
|
|
|
样本覆盖
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody class="divide-y divide-border/60">
|
|
|
|
|
|
<tr
|
|
|
|
|
|
v-for="provider in providerPerformanceRows"
|
|
|
|
|
|
:key="provider.provider_id"
|
|
|
|
|
|
class="bg-background/40"
|
|
|
|
|
|
>
|
|
|
|
|
|
<td class="max-w-[220px] px-3 py-2">
|
|
|
|
|
|
<div class="truncate font-medium">
|
|
|
|
|
|
{{ provider.provider }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="truncate text-xs text-muted-foreground">
|
|
|
|
|
|
{{ provider.provider_id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatMetricNumber(provider.request_count) }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatProviderPerformanceMetric(provider.success_rate, '%') }}
|
|
|
|
|
|
</td>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatErrorRate(provider.success_rate) }}
|
|
|
|
|
|
</td>
|
2026-05-05 11:24:16 +08:00
|
|
|
|
<td class="px-3 py-2 text-right">
|
2026-05-05 17:34:12 +08:00
|
|
|
|
{{ formatProviderPerformanceMetric(provider.avg_output_tps, ' tps') }}
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatProviderPerformanceMetric(provider.avg_first_byte_time_ms, 'ms') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatProviderPerformanceMetric(provider.avg_response_time_ms, 'ms') }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatProviderPerformanceMetric(provider.p90_response_time_ms, 'ms', 0) }}
|
|
|
|
|
|
/
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ formatProviderPerformanceMetric(provider.p99_response_time_ms, 'ms', 0) }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
2026-05-05 11:24:16 +08:00
|
|
|
|
{{ formatProviderPerformanceMetric(provider.p90_first_byte_time_ms, 'ms', 0) }}
|
2026-05-06 02:01:32 +08:00
|
|
|
|
/
|
|
|
|
|
|
{{ formatProviderPerformanceMetric(provider.p99_first_byte_time_ms, 'ms', 0) }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right">
|
|
|
|
|
|
{{ formatMetricNumber(provider.slow_request_count) }}
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-3 py-2 text-right text-xs text-muted-foreground">
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{{ providerSampleCoverageText(provider) }}
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
|
|
|
|
|
>
|
2026-05-06 02:01:32 +08:00
|
|
|
|
当前没有上游服务性能数据。
|
2026-05-05 11:24:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
|
|
|
|
<Card class="space-y-3 p-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
|
|
|
|
|
输出 TPS 趋势
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="providerPerformanceLoading"
|
|
|
|
|
|
class="p-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LoadingState />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-[260px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LineChart
|
|
|
|
|
|
:data="providerTpsChartData"
|
|
|
|
|
|
:options="providerTpsChartOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
<Card class="space-y-3 p-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold">
|
|
|
|
|
|
平均首字趋势
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="providerPerformanceLoading"
|
|
|
|
|
|
class="p-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LoadingState />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-[260px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<LineChart
|
|
|
|
|
|
:data="providerFirstByteChartData"
|
|
|
|
|
|
:options="providerLatencyChartOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-06 09:09:55 +08:00
|
|
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
|
|
|
|
<Card class="p-4">
|
|
|
|
|
|
<PercentileChart
|
|
|
|
|
|
title="响应延迟百分位"
|
|
|
|
|
|
:series="percentiles"
|
|
|
|
|
|
mode="response"
|
|
|
|
|
|
:loading="percentileLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
<Card class="p-4">
|
|
|
|
|
|
<PercentileChart
|
|
|
|
|
|
title="首字节延迟百分位"
|
|
|
|
|
|
:series="percentiles"
|
|
|
|
|
|
mode="ttfb"
|
|
|
|
|
|
:loading="percentileLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<Card class="p-4">
|
|
|
|
|
|
<ErrorDistributionChart
|
|
|
|
|
|
title="错误分布"
|
|
|
|
|
|
:distribution="errorDistribution"
|
|
|
|
|
|
:loading="errorLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
2026-04-13 14:53:46 +08:00
|
|
|
|
<Card class="space-y-3 p-4">
|
2026-02-04 23:59:45 +08:00
|
|
|
|
<h3 class="text-sm font-semibold">
|
|
|
|
|
|
错误趋势
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="errorLoading"
|
|
|
|
|
|
class="p-6"
|
|
|
|
|
|
>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<LoadingState />
|
|
|
|
|
|
</div>
|
2026-02-04 23:59:45 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="h-[260px]"
|
|
|
|
|
|
>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
<LineChart :data="errorTrendChartData" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Activity,
|
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
|
Cable,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
CheckCircle2,
|
2026-05-06 09:09:55 +08:00
|
|
|
|
ChevronDown,
|
|
|
|
|
|
ChevronUp,
|
2026-05-06 02:01:32 +08:00
|
|
|
|
FilterX,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
GitBranch,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
Gauge,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
ShieldCheck,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
Timer,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
Workflow,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
Zap,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
} from 'lucide-vue-next'
|
2026-05-05 11:24:16 +08:00
|
|
|
|
import {
|
|
|
|
|
|
adminApi,
|
|
|
|
|
|
type ErrorDistributionResponse,
|
|
|
|
|
|
type PercentileItem,
|
2026-05-06 02:01:32 +08:00
|
|
|
|
type ProviderPerformanceItem,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
type ProviderPerformanceResponse,
|
|
|
|
|
|
} from '@/api/admin'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import {
|
|
|
|
|
|
monitoringApi,
|
|
|
|
|
|
type AdminMonitoringCircuitHistoryItem,
|
|
|
|
|
|
type AdminMonitoringResilienceStatus,
|
|
|
|
|
|
type AdminMonitoringSystemStatus,
|
|
|
|
|
|
type GatewayMetricsSummary,
|
|
|
|
|
|
} from '@/api/monitoring'
|
|
|
|
|
|
import LineChart from '@/components/charts/LineChart.vue'
|
|
|
|
|
|
import { LoadingState, TimeRangePicker } from '@/components/common'
|
|
|
|
|
|
import { ErrorDistributionChart, PercentileChart } from '@/components/stats'
|
|
|
|
|
|
import Badge from '@/components/ui/badge.vue'
|
2026-05-06 02:01:32 +08:00
|
|
|
|
import Button from '@/components/ui/button.vue'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import Card from '@/components/ui/card.vue'
|
2026-05-06 02:01:32 +08:00
|
|
|
|
import Input from '@/components/ui/input.vue'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import RefreshButton from '@/components/ui/refresh-button.vue'
|
2026-05-06 02:01:32 +08:00
|
|
|
|
import Select from '@/components/ui/select.vue'
|
|
|
|
|
|
import SelectContent from '@/components/ui/select-content.vue'
|
|
|
|
|
|
import SelectItem from '@/components/ui/select-item.vue'
|
|
|
|
|
|
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
|
|
|
|
|
import SelectValue from '@/components/ui/select-value.vue'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-02-04 02:04:54 +08:00
|
|
|
|
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
|
|
|
|
|
import type { DateRangeParams } from '@/features/usage/types'
|
2026-05-06 02:01:32 +08:00
|
|
|
|
import { formatDate, formatNumber } from '@/utils/format'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
import { log } from '@/utils/logger'
|
2026-05-05 11:24:16 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildProviderPerformanceChartData,
|
2026-05-06 09:09:55 +08:00
|
|
|
|
formatDurationMs,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
formatProviderPerformanceMetric,
|
|
|
|
|
|
} from './performanceAnalysisHelpers'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
|
|
|
|
|
const LIVE_REFRESH_INTERVAL_MS = 10_000
|
2026-05-06 09:09:55 +08:00
|
|
|
|
const RECENT_ERRORS_COLLAPSED_LIMIT = 3
|
2026-05-06 02:01:32 +08:00
|
|
|
|
const DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS = 10_000
|
|
|
|
|
|
|
|
|
|
|
|
type ProviderPerformanceBooleanFilter = 'all' | 'true' | 'false'
|
|
|
|
|
|
type ProviderPerformanceParams = NonNullable<Parameters<typeof adminApi.getProviderPerformance>[0]>
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
2026-05-06 09:09:55 +08:00
|
|
|
|
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last7days'))
|
2026-04-13 14:53:46 +08:00
|
|
|
|
const { error: showError } = useToast()
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
|
|
|
|
|
const percentiles = ref<PercentileItem[]>([])
|
|
|
|
|
|
const percentileLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const errorDistribution = ref<ErrorDistributionResponse['distribution']>([])
|
|
|
|
|
|
const errorTrend = ref<ErrorDistributionResponse['trend']>([])
|
|
|
|
|
|
const errorLoading = ref(false)
|
|
|
|
|
|
|
2026-05-05 11:24:16 +08:00
|
|
|
|
const providerPerformance = ref<ProviderPerformanceResponse | null>(null)
|
|
|
|
|
|
const providerPerformanceLoading = ref(false)
|
2026-05-06 02:01:32 +08:00
|
|
|
|
const providerPerformanceProviderId = ref('')
|
|
|
|
|
|
const providerPerformanceModel = ref('')
|
|
|
|
|
|
const providerPerformanceApiFormat = ref('')
|
|
|
|
|
|
const providerPerformanceEndpointKind = ref('')
|
|
|
|
|
|
const providerPerformanceIsStream = ref<ProviderPerformanceBooleanFilter>('all')
|
|
|
|
|
|
const providerPerformanceHasFormatConversion = ref<ProviderPerformanceBooleanFilter>('all')
|
|
|
|
|
|
const providerPerformanceSlowThresholdMs = ref(String(DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS))
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
|
|
|
|
|
const systemStatus = ref<AdminMonitoringSystemStatus | null>(null)
|
|
|
|
|
|
const resilienceStatus = ref<AdminMonitoringResilienceStatus | null>(null)
|
|
|
|
|
|
const circuitHistory = ref<AdminMonitoringCircuitHistoryItem[]>([])
|
|
|
|
|
|
const gatewayMetrics = ref<GatewayMetricsSummary | null>(null)
|
|
|
|
|
|
const liveLoading = ref(false)
|
|
|
|
|
|
const liveRefreshing = ref(false)
|
|
|
|
|
|
const liveReady = ref(false)
|
|
|
|
|
|
const liveLoadError = ref<string | null>(null)
|
|
|
|
|
|
const liveLastUpdatedAt = ref<string | null>(null)
|
2026-05-06 09:09:55 +08:00
|
|
|
|
const recentErrorsExpanded = ref(false)
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
let percentilesRequestId = 0
|
|
|
|
|
|
let errorsRequestId = 0
|
2026-05-05 11:24:16 +08:00
|
|
|
|
let providerPerformanceRequestId = 0
|
2026-04-13 14:53:46 +08:00
|
|
|
|
let liveRequestId = 0
|
2026-03-03 22:04:40 +08:00
|
|
|
|
let loadAllPromise: Promise<void> | null = null
|
|
|
|
|
|
let hasPendingLoadAll = false
|
|
|
|
|
|
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
2026-05-06 02:01:32 +08:00
|
|
|
|
let providerPerformanceDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
2026-04-13 14:53:46 +08:00
|
|
|
|
let liveRefreshTimer: ReturnType<typeof setInterval> | null = null
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
|
|
|
|
|
function buildTimeRangeParams() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
function normalizeProviderPerformanceFilter(value: string): string | undefined {
|
|
|
|
|
|
const trimmed = value.trim()
|
|
|
|
|
|
return trimmed ? trimmed : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseProviderPerformanceBooleanFilter(
|
|
|
|
|
|
value: ProviderPerformanceBooleanFilter
|
|
|
|
|
|
): boolean | undefined {
|
|
|
|
|
|
if (value === 'true') return true
|
|
|
|
|
|
if (value === 'false') return false
|
|
|
|
|
|
return undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clampProviderPerformanceSlowThreshold(value: string): number {
|
|
|
|
|
|
const parsed = Number(value)
|
|
|
|
|
|
if (!Number.isFinite(parsed)) {
|
|
|
|
|
|
return DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS
|
|
|
|
|
|
}
|
|
|
|
|
|
return Math.min(600_000, Math.max(1, Math.round(parsed)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveProviderPerformanceGranularity(): 'day' | 'hour' {
|
|
|
|
|
|
if (timeRange.value.preset === 'today' || timeRange.value.preset === 'yesterday') {
|
|
|
|
|
|
return 'hour'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!timeRange.value.preset && timeRange.value.start_date && timeRange.value.end_date) {
|
|
|
|
|
|
return timeRange.value.start_date === timeRange.value.end_date ? 'hour' : 'day'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 'day'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const providerPerformanceSlowThresholdValue = computed(() => (
|
|
|
|
|
|
clampProviderPerformanceSlowThreshold(providerPerformanceSlowThresholdMs.value)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const providerPerformanceSlowThresholdLabel = computed(() => {
|
|
|
|
|
|
const value = providerPerformanceSlowThresholdValue.value
|
|
|
|
|
|
if (value >= 1000) {
|
|
|
|
|
|
const seconds = value / 1000
|
|
|
|
|
|
return `${Number.isInteger(seconds) ? seconds.toFixed(0) : seconds.toFixed(1)}s`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${value}ms`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerPerformanceActiveFilterCount = computed(() => {
|
|
|
|
|
|
const textFilterCount = [
|
|
|
|
|
|
providerPerformanceProviderId.value,
|
|
|
|
|
|
providerPerformanceModel.value,
|
|
|
|
|
|
providerPerformanceApiFormat.value,
|
|
|
|
|
|
providerPerformanceEndpointKind.value,
|
|
|
|
|
|
].filter(value => normalizeProviderPerformanceFilter(value)).length
|
|
|
|
|
|
|
|
|
|
|
|
const booleanFilterCount = [
|
|
|
|
|
|
providerPerformanceIsStream.value,
|
|
|
|
|
|
providerPerformanceHasFormatConversion.value,
|
|
|
|
|
|
].filter(value => value !== 'all').length
|
|
|
|
|
|
|
|
|
|
|
|
const thresholdFilterCount = providerPerformanceSlowThresholdValue.value === DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS
|
|
|
|
|
|
? 0
|
|
|
|
|
|
: 1
|
|
|
|
|
|
|
|
|
|
|
|
return textFilterCount + booleanFilterCount + thresholdFilterCount
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const hasProviderPerformanceFilters = computed(() => providerPerformanceActiveFilterCount.value > 0)
|
|
|
|
|
|
|
|
|
|
|
|
function buildProviderPerformanceParams(): ProviderPerformanceParams {
|
|
|
|
|
|
const params: ProviderPerformanceParams = {
|
|
|
|
|
|
...buildTimeRangeParams(),
|
|
|
|
|
|
granularity: resolveProviderPerformanceGranularity(),
|
|
|
|
|
|
limit: 8,
|
|
|
|
|
|
slow_threshold_ms: providerPerformanceSlowThresholdValue.value,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const providerId = normalizeProviderPerformanceFilter(providerPerformanceProviderId.value)
|
|
|
|
|
|
if (providerId) params.provider_id = providerId
|
|
|
|
|
|
|
|
|
|
|
|
const model = normalizeProviderPerformanceFilter(providerPerformanceModel.value)
|
|
|
|
|
|
if (model) params.model = model
|
|
|
|
|
|
|
|
|
|
|
|
const apiFormat = normalizeProviderPerformanceFilter(providerPerformanceApiFormat.value)
|
|
|
|
|
|
if (apiFormat) params.api_format = apiFormat
|
|
|
|
|
|
|
|
|
|
|
|
const endpointKind = normalizeProviderPerformanceFilter(providerPerformanceEndpointKind.value)
|
|
|
|
|
|
if (endpointKind) params.endpoint_kind = endpointKind
|
|
|
|
|
|
|
|
|
|
|
|
const isStream = parseProviderPerformanceBooleanFilter(providerPerformanceIsStream.value)
|
|
|
|
|
|
if (isStream !== undefined) params.is_stream = isStream
|
|
|
|
|
|
|
|
|
|
|
|
const hasFormatConversion = parseProviderPerformanceBooleanFilter(
|
|
|
|
|
|
providerPerformanceHasFormatConversion.value
|
|
|
|
|
|
)
|
|
|
|
|
|
if (hasFormatConversion !== undefined) {
|
|
|
|
|
|
params.has_format_conversion = hasFormatConversion
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return params
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetProviderPerformanceFilters() {
|
|
|
|
|
|
providerPerformanceProviderId.value = ''
|
|
|
|
|
|
providerPerformanceModel.value = ''
|
|
|
|
|
|
providerPerformanceApiFormat.value = ''
|
|
|
|
|
|
providerPerformanceEndpointKind.value = ''
|
|
|
|
|
|
providerPerformanceIsStream.value = 'all'
|
|
|
|
|
|
providerPerformanceHasFormatConversion.value = 'all'
|
|
|
|
|
|
providerPerformanceSlowThresholdMs.value = String(DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
function formatMetricNumber(value: number | null | undefined): string {
|
|
|
|
|
|
if (value == null || Number.isNaN(value)) {
|
|
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!Number.isInteger(value)) {
|
|
|
|
|
|
return value.toFixed(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return formatNumber(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
function formatErrorRate(successRate: number | null | undefined): string {
|
|
|
|
|
|
if (successRate == null || Number.isNaN(successRate)) {
|
|
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${Math.max(0, 100 - successRate).toFixed(2)}%`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function providerSampleCoverageText(provider: ProviderPerformanceItem): string {
|
|
|
|
|
|
if (!provider.request_count) {
|
|
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
|
|
|
|
|
const responseSamples = provider.response_time_sample_count ?? 0
|
|
|
|
|
|
const firstByteSamples = provider.first_byte_sample_count ?? 0
|
|
|
|
|
|
const responseCoverage = Math.round(responseSamples / provider.request_count * 100)
|
|
|
|
|
|
const firstByteCoverage = Math.round(firstByteSamples / provider.request_count * 100)
|
|
|
|
|
|
return `${responseCoverage}% / ${firstByteCoverage}%`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 02:04:54 +08:00
|
|
|
|
async function loadPercentiles() {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
const requestId = ++percentilesRequestId
|
2026-02-04 02:04:54 +08:00
|
|
|
|
percentileLoading.value = true
|
|
|
|
|
|
try {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
const data = await adminApi.getPercentiles(buildTimeRangeParams())
|
|
|
|
|
|
if (requestId !== percentilesRequestId) return
|
|
|
|
|
|
percentiles.value = data
|
2026-04-13 14:53:46 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestId !== percentilesRequestId) return
|
|
|
|
|
|
percentiles.value = []
|
|
|
|
|
|
log.error('加载延迟百分位失败', error)
|
2026-02-04 02:04:54 +08:00
|
|
|
|
} finally {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId === percentilesRequestId) {
|
|
|
|
|
|
percentileLoading.value = false
|
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadErrors() {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
const requestId = ++errorsRequestId
|
2026-02-04 02:04:54 +08:00
|
|
|
|
errorLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await adminApi.getErrorDistribution(buildTimeRangeParams())
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId !== errorsRequestId) return
|
2026-02-04 02:04:54 +08:00
|
|
|
|
errorDistribution.value = response.distribution
|
|
|
|
|
|
errorTrend.value = response.trend
|
2026-04-13 14:53:46 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestId !== errorsRequestId) return
|
|
|
|
|
|
errorDistribution.value = []
|
|
|
|
|
|
errorTrend.value = []
|
|
|
|
|
|
log.error('加载错误分布失败', error)
|
2026-02-04 02:04:54 +08:00
|
|
|
|
} finally {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId === errorsRequestId) {
|
|
|
|
|
|
errorLoading.value = false
|
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 11:24:16 +08:00
|
|
|
|
async function loadProviderPerformance() {
|
|
|
|
|
|
const requestId = ++providerPerformanceRequestId
|
|
|
|
|
|
providerPerformanceLoading.value = true
|
|
|
|
|
|
try {
|
2026-05-06 02:01:32 +08:00
|
|
|
|
const data = await adminApi.getProviderPerformance(buildProviderPerformanceParams())
|
2026-05-05 11:24:16 +08:00
|
|
|
|
if (requestId !== providerPerformanceRequestId) return
|
|
|
|
|
|
providerPerformance.value = data
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestId !== providerPerformanceRequestId) return
|
|
|
|
|
|
providerPerformance.value = null
|
2026-05-06 02:01:32 +08:00
|
|
|
|
log.error('加载上游服务性能统计失败', error)
|
2026-05-05 11:24:16 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
if (requestId === providerPerformanceRequestId) {
|
|
|
|
|
|
providerPerformanceLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
async function loadLiveData(options: { silent?: boolean } = {}) {
|
|
|
|
|
|
const requestId = ++liveRequestId
|
|
|
|
|
|
const initialLoad = !liveReady.value
|
|
|
|
|
|
|
|
|
|
|
|
if (initialLoad) {
|
|
|
|
|
|
liveLoading.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
liveRefreshing.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const results = await Promise.allSettled([
|
|
|
|
|
|
monitoringApi.getSystemStatus(),
|
|
|
|
|
|
monitoringApi.getResilienceStatus(),
|
|
|
|
|
|
monitoringApi.getCircuitHistory(8),
|
|
|
|
|
|
monitoringApi.getGatewayMetricsSummary(),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
if (requestId !== liveRequestId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const failedScopes: string[] = []
|
|
|
|
|
|
let successCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
const [systemResult, resilienceResult, circuitResult, metricsResult] = results
|
|
|
|
|
|
|
|
|
|
|
|
if (systemResult.status === 'fulfilled') {
|
|
|
|
|
|
systemStatus.value = systemResult.value
|
|
|
|
|
|
successCount += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedScopes.push('系统状态')
|
|
|
|
|
|
log.error('加载系统状态失败', systemResult.reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (resilienceResult.status === 'fulfilled') {
|
|
|
|
|
|
resilienceStatus.value = resilienceResult.value
|
|
|
|
|
|
successCount += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedScopes.push('韧性状态')
|
|
|
|
|
|
log.error('加载韧性状态失败', resilienceResult.reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (circuitResult.status === 'fulfilled') {
|
|
|
|
|
|
circuitHistory.value = circuitResult.value.items
|
|
|
|
|
|
successCount += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedScopes.push('熔断历史')
|
|
|
|
|
|
log.error('加载熔断历史失败', circuitResult.reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (metricsResult.status === 'fulfilled') {
|
|
|
|
|
|
gatewayMetrics.value = metricsResult.value
|
|
|
|
|
|
successCount += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedScopes.push('网关指标')
|
|
|
|
|
|
log.error('加载网关指标失败', metricsResult.reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
liveReady.value = successCount > 0
|
|
|
|
|
|
if (successCount > 0) {
|
|
|
|
|
|
liveLastUpdatedAt.value = new Date().toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
liveLoadError.value = failedScopes.length
|
|
|
|
|
|
? `部分实时数据加载失败:${failedScopes.join('、')}`
|
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
|
|
if (failedScopes.length && !options.silent) {
|
|
|
|
|
|
showError(liveLoadError.value ?? '实时性能数据加载失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (requestId === liveRequestId) {
|
|
|
|
|
|
liveLoading.value = false
|
|
|
|
|
|
liveRefreshing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 02:04:54 +08:00
|
|
|
|
const errorTrendChartData = computed(() => ({
|
|
|
|
|
|
labels: errorTrend.value.map(item => item.date),
|
|
|
|
|
|
datasets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '错误数',
|
|
|
|
|
|
data: errorTrend.value.map(item => item.total),
|
|
|
|
|
|
borderColor: 'rgb(239, 68, 68)',
|
|
|
|
|
|
tension: 0.25,
|
|
|
|
|
|
pointRadius: 2
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
const recentErrors = computed(() => resilienceStatus.value?.recent_errors ?? [])
|
2026-05-06 09:09:55 +08:00
|
|
|
|
const hasMoreRecentErrors = computed(() => recentErrors.value.length > RECENT_ERRORS_COLLAPSED_LIMIT)
|
|
|
|
|
|
const visibleRecentErrors = computed(() => (
|
|
|
|
|
|
recentErrorsExpanded.value
|
|
|
|
|
|
? recentErrors.value
|
|
|
|
|
|
: recentErrors.value.slice(0, RECENT_ERRORS_COLLAPSED_LIMIT)
|
|
|
|
|
|
))
|
|
|
|
|
|
const recentErrorsListClass = computed(() => [
|
|
|
|
|
|
'space-y-3',
|
|
|
|
|
|
recentErrorsExpanded.value ? 'max-h-[360px] overflow-y-auto pr-1' : '',
|
|
|
|
|
|
])
|
2026-04-13 14:53:46 +08:00
|
|
|
|
const resilienceRecommendations = computed(() => resilienceStatus.value?.recommendations ?? [])
|
|
|
|
|
|
|
|
|
|
|
|
const healthStatusVariant = computed<'success' | 'warning' | 'destructive' | 'outline'>(() => {
|
|
|
|
|
|
switch (resilienceStatus.value?.status) {
|
|
|
|
|
|
case 'healthy':
|
|
|
|
|
|
return 'success'
|
|
|
|
|
|
case 'degraded':
|
|
|
|
|
|
return 'warning'
|
|
|
|
|
|
case 'critical':
|
|
|
|
|
|
return 'destructive'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'outline'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const healthStatusText = computed(() => {
|
|
|
|
|
|
if (!resilienceStatus.value) {
|
|
|
|
|
|
return '健康状态未知'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const statusMap: Record<string, string> = {
|
|
|
|
|
|
healthy: '系统健康',
|
|
|
|
|
|
degraded: '系统降级',
|
|
|
|
|
|
critical: '系统告警',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${statusMap[resilienceStatus.value.status] ?? resilienceStatus.value.status} · ${resilienceStatus.value.health_score}/100`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const metricsAvailabilityText = computed(() => (
|
2026-05-06 02:01:32 +08:00
|
|
|
|
gatewayMetrics.value ? '网关指标在线' : '网关指标暂不可达'
|
2026-04-13 14:53:46 +08:00
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const distributedGateVariant = computed<'warning' | 'outline'>(() => (
|
|
|
|
|
|
gatewayMetrics.value?.distributed.unavailable ? 'warning' : 'outline'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const distributedGateText = computed(() => (
|
|
|
|
|
|
gatewayMetrics.value?.distributed.unavailable ? '不可用' : '在线'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const liveLastUpdatedLabel = computed(() => (
|
|
|
|
|
|
liveLastUpdatedAt.value ? formatDate(liveLastUpdatedAt.value) : '尚未刷新'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const currentActiveStreams = computed(() => (
|
|
|
|
|
|
gatewayMetrics.value?.tunnel.activeStreams ?? systemStatus.value?.tunnel.active_streams ?? null
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const currentProxyConnections = computed(() => (
|
|
|
|
|
|
gatewayMetrics.value?.tunnel.proxyConnections ?? systemStatus.value?.tunnel.proxy_connections ?? null
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const currentTunnelNodes = computed(() => (
|
|
|
|
|
|
gatewayMetrics.value?.tunnel.nodes ?? systemStatus.value?.tunnel.nodes ?? null
|
|
|
|
|
|
))
|
|
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
const tunnelQueueRejectedTotal = computed(() => {
|
|
|
|
|
|
const full = gatewayMetrics.value?.tunnel.outboundQueueRejectedFullTotal ?? 0
|
|
|
|
|
|
const closed = gatewayMetrics.value?.tunnel.outboundQueueRejectedClosedTotal ?? 0
|
|
|
|
|
|
return full + closed
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tunnelSelectionPressureTotal = computed(() => {
|
|
|
|
|
|
const congested = gatewayMetrics.value?.tunnel.proxyConnectionCongestedTotal ?? 0
|
|
|
|
|
|
const retry = gatewayMetrics.value?.tunnel.selectionRetryTotal ?? 0
|
|
|
|
|
|
const unavailable = gatewayMetrics.value?.tunnel.selectionUnavailableTotal ?? 0
|
|
|
|
|
|
return congested + retry + unavailable
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tunnelQueueUtilizationText = computed(() => {
|
|
|
|
|
|
const depth = gatewayMetrics.value?.tunnel.outboundQueueDepthTotal
|
|
|
|
|
|
const capacity = gatewayMetrics.value?.tunnel.outboundQueueCapacityTotal
|
|
|
|
|
|
if (depth == null || capacity == null || capacity <= 0) {
|
2026-04-13 14:53:46 +08:00
|
|
|
|
return '-'
|
|
|
|
|
|
}
|
2026-05-06 02:01:32 +08:00
|
|
|
|
return `${Math.round(depth / capacity * 100)}%`
|
2026-04-13 14:53:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const fallbackRows = computed(() => {
|
|
|
|
|
|
const items = gatewayMetrics.value?.fallbacks ?? []
|
|
|
|
|
|
const maxValue = Math.max(...items.map(item => item.total), 0)
|
|
|
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
.filter(item => item.total > 0)
|
|
|
|
|
|
.sort((left, right) => right.total - left.total)
|
|
|
|
|
|
.map(item => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
ratio: maxValue > 0 ? item.total / maxValue * 100 : 0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-05 11:24:16 +08:00
|
|
|
|
const providerPerformanceRows = computed(() => providerPerformance.value?.providers ?? [])
|
|
|
|
|
|
|
|
|
|
|
|
const providerPerformanceSubtitle = computed(() => {
|
|
|
|
|
|
const requests = providerPerformance.value?.summary.request_count ?? 0
|
2026-05-06 02:01:32 +08:00
|
|
|
|
const filters = providerPerformanceActiveFilterCount.value
|
|
|
|
|
|
const filterText = filters > 0 ? ` · ${filters} 个筛选条件` : ''
|
|
|
|
|
|
return `完成窗口内 ${formatMetricNumber(requests)} 个上游请求样本${filterText}`
|
2026-05-05 11:24:16 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerPerformanceSummaryCards = computed(() => {
|
|
|
|
|
|
const summary = providerPerformance.value?.summary
|
|
|
|
|
|
return [
|
2026-05-06 02:01:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '请求样本',
|
|
|
|
|
|
value: formatMetricNumber(summary?.request_count),
|
|
|
|
|
|
hint: `${formatMetricNumber(providerPerformanceRows.value.length)} 个上游服务`,
|
|
|
|
|
|
icon: Activity,
|
|
|
|
|
|
iconClass: 'text-blue-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '成功率',
|
|
|
|
|
|
value: formatProviderPerformanceMetric(summary?.success_rate, '%'),
|
|
|
|
|
|
hint: `错误率 ${formatErrorRate(summary?.success_rate)}`,
|
|
|
|
|
|
icon: CheckCircle2,
|
|
|
|
|
|
iconClass: 'text-emerald-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'P99 响应',
|
|
|
|
|
|
value: formatProviderPerformanceMetric(summary?.p99_response_time_ms, 'ms', 0),
|
|
|
|
|
|
hint: `P90 ${formatProviderPerformanceMetric(summary?.p90_response_time_ms, 'ms', 0)}`,
|
|
|
|
|
|
icon: Gauge,
|
|
|
|
|
|
iconClass: 'text-violet-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'P99 首字',
|
|
|
|
|
|
value: formatProviderPerformanceMetric(summary?.p99_first_byte_time_ms, 'ms', 0),
|
|
|
|
|
|
hint: `P90 ${formatProviderPerformanceMetric(summary?.p90_first_byte_time_ms, 'ms', 0)}`,
|
|
|
|
|
|
icon: Timer,
|
|
|
|
|
|
iconClass: 'text-sky-500',
|
|
|
|
|
|
},
|
2026-05-05 11:24:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '输出 TPS',
|
2026-05-05 17:34:12 +08:00
|
|
|
|
value: formatProviderPerformanceMetric(summary?.avg_output_tps, ' tps'),
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: `TPS 样本 ${formatMetricNumber(summary?.tps_sample_count)}`,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
icon: Zap,
|
|
|
|
|
|
iconClass: 'text-amber-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '平均首字',
|
|
|
|
|
|
value: formatProviderPerformanceMetric(summary?.avg_first_byte_time_ms, 'ms'),
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: `首字样本 ${formatMetricNumber(summary?.first_byte_sample_count)}`,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
icon: Timer,
|
|
|
|
|
|
iconClass: 'text-sky-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '平均响应',
|
|
|
|
|
|
value: formatProviderPerformanceMetric(summary?.avg_response_time_ms, 'ms'),
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: `响应样本 ${formatMetricNumber(summary?.response_time_sample_count)}`,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
icon: Gauge,
|
|
|
|
|
|
iconClass: 'text-violet-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-06 02:01:32 +08:00
|
|
|
|
title: '慢请求',
|
|
|
|
|
|
value: formatMetricNumber(summary?.slow_request_count),
|
|
|
|
|
|
hint: `响应耗时 >= ${providerPerformanceSlowThresholdLabel.value}`,
|
|
|
|
|
|
icon: AlertTriangle,
|
|
|
|
|
|
iconClass: 'text-yellow-500',
|
2026-05-05 11:24:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerTpsChartData = computed(() => (
|
|
|
|
|
|
buildProviderPerformanceChartData(
|
|
|
|
|
|
providerPerformance.value?.timeline ?? [],
|
|
|
|
|
|
'avg_output_tps',
|
|
|
|
|
|
providerPerformanceRows.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const providerFirstByteChartData = computed(() => (
|
|
|
|
|
|
buildProviderPerformanceChartData(
|
|
|
|
|
|
providerPerformance.value?.timeline ?? [],
|
|
|
|
|
|
'avg_first_byte_time_ms',
|
|
|
|
|
|
providerPerformanceRows.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const providerTpsChartOptions = computed(() => ({
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
ticks: {
|
2026-05-05 17:34:12 +08:00
|
|
|
|
callback: (value: string | number) => `${value} tps`,
|
2026-05-05 11:24:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const providerLatencyChartOptions = computed(() => ({
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
ticks: {
|
2026-05-06 09:09:55 +08:00
|
|
|
|
callback: (value: string | number) => formatDurationMs(Number(value), 0),
|
2026-05-05 11:24:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
const liveSummaryCards = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '系统健康',
|
|
|
|
|
|
value: resilienceStatus.value ? `${resilienceStatus.value.health_score}/100` : '-',
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: `${healthStatusText.value} · 熔断打开 ${formatMetricNumber(resilienceStatus.value?.error_statistics.open_circuit_breakers)}`,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
icon: ShieldCheck,
|
|
|
|
|
|
iconClass: 'text-emerald-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '最近 1 小时错误',
|
|
|
|
|
|
value: formatMetricNumber(systemStatus.value?.recent_errors),
|
|
|
|
|
|
hint: `24h 总错误 ${formatMetricNumber(resilienceStatus.value?.error_statistics.total_errors)}`,
|
|
|
|
|
|
icon: AlertTriangle,
|
|
|
|
|
|
iconClass: 'text-yellow-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-06 02:01:32 +08:00
|
|
|
|
title: '代理活跃流',
|
2026-04-13 14:53:46 +08:00
|
|
|
|
value: formatMetricNumber(currentActiveStreams.value),
|
|
|
|
|
|
hint: `代理连接 ${formatMetricNumber(currentProxyConnections.value)}`,
|
|
|
|
|
|
icon: Cable,
|
|
|
|
|
|
iconClass: 'text-sky-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-06 02:01:32 +08:00
|
|
|
|
title: '当前节点处理中',
|
2026-04-13 14:53:46 +08:00
|
|
|
|
value: formatMetricNumber(gatewayMetrics.value?.local.inFlight),
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: `可接入 ${formatMetricNumber(gatewayMetrics.value?.local.availablePermits)}`,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
icon: Activity,
|
|
|
|
|
|
iconClass: 'text-blue-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-06 02:01:32 +08:00
|
|
|
|
title: '全局处理中',
|
2026-04-13 14:53:46 +08:00
|
|
|
|
value: gatewayMetrics.value?.distributed.unavailable
|
|
|
|
|
|
? '不可用'
|
|
|
|
|
|
: formatMetricNumber(gatewayMetrics.value?.distributed.inFlight),
|
|
|
|
|
|
hint: gatewayMetrics.value?.distributed.unavailable
|
2026-05-06 02:01:32 +08:00
|
|
|
|
? '检查 Redis 连接'
|
|
|
|
|
|
: `可接入 ${formatMetricNumber(gatewayMetrics.value?.distributed.availablePermits)}`,
|
2026-04-13 14:53:46 +08:00
|
|
|
|
icon: Workflow,
|
|
|
|
|
|
iconClass: 'text-violet-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-06 02:01:32 +08:00
|
|
|
|
title: '降级切换',
|
2026-04-13 14:53:46 +08:00
|
|
|
|
value: formatMetricNumber(gatewayMetrics.value?.fallbackTotal),
|
2026-05-06 02:01:32 +08:00
|
|
|
|
hint: '当前进程累计',
|
2026-04-13 14:53:46 +08:00
|
|
|
|
icon: GitBranch,
|
|
|
|
|
|
iconClass: 'text-rose-500',
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const isRefreshing = computed(() => (
|
|
|
|
|
|
liveLoading.value ||
|
|
|
|
|
|
liveRefreshing.value ||
|
|
|
|
|
|
percentileLoading.value ||
|
|
|
|
|
|
errorLoading.value ||
|
2026-05-05 11:24:16 +08:00
|
|
|
|
providerPerformanceLoading.value
|
2026-04-13 14:53:46 +08:00
|
|
|
|
))
|
|
|
|
|
|
|
2026-02-04 02:04:54 +08:00
|
|
|
|
async function loadAll() {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (loadAllPromise) {
|
|
|
|
|
|
hasPendingLoadAll = true
|
|
|
|
|
|
return loadAllPromise
|
|
|
|
|
|
}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
2026-05-05 11:24:16 +08:00
|
|
|
|
loadAllPromise = Promise.all([
|
|
|
|
|
|
loadPercentiles(),
|
|
|
|
|
|
loadErrors(),
|
|
|
|
|
|
loadProviderPerformance(),
|
|
|
|
|
|
])
|
2026-03-03 22:04:40 +08:00
|
|
|
|
.then(() => undefined)
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
loadAllPromise = null
|
|
|
|
|
|
if (hasPendingLoadAll) {
|
|
|
|
|
|
hasPendingLoadAll = false
|
|
|
|
|
|
void loadAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
return loadAllPromise
|
2026-02-04 02:04:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
async function handleManualRefresh() {
|
|
|
|
|
|
await Promise.allSettled([loadLiveData(), loadAll()])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
function scheduleLoadAll() {
|
|
|
|
|
|
if (loadAllDebounceTimer) {
|
|
|
|
|
|
clearTimeout(loadAllDebounceTimer)
|
|
|
|
|
|
}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
loadAllDebounceTimer = setTimeout(() => {
|
|
|
|
|
|
loadAllDebounceTimer = null
|
|
|
|
|
|
void loadAll()
|
|
|
|
|
|
}, 120)
|
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
function scheduleProviderPerformanceLoad() {
|
|
|
|
|
|
if (providerPerformanceDebounceTimer) {
|
|
|
|
|
|
clearTimeout(providerPerformanceDebounceTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
providerPerformanceDebounceTimer = setTimeout(() => {
|
|
|
|
|
|
providerPerformanceDebounceTimer = null
|
|
|
|
|
|
void loadProviderPerformance()
|
|
|
|
|
|
}, 180)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
watch(timeRange, scheduleLoadAll, { deep: true })
|
2026-05-06 02:01:32 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
[
|
|
|
|
|
|
providerPerformanceProviderId,
|
|
|
|
|
|
providerPerformanceModel,
|
|
|
|
|
|
providerPerformanceApiFormat,
|
|
|
|
|
|
providerPerformanceEndpointKind,
|
|
|
|
|
|
providerPerformanceIsStream,
|
|
|
|
|
|
providerPerformanceHasFormatConversion,
|
|
|
|
|
|
providerPerformanceSlowThresholdMs,
|
|
|
|
|
|
],
|
|
|
|
|
|
scheduleProviderPerformanceLoad
|
|
|
|
|
|
)
|
2026-03-03 22:04:40 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-04-13 14:53:46 +08:00
|
|
|
|
void loadLiveData()
|
2026-03-03 22:04:40 +08:00
|
|
|
|
void loadAll()
|
2026-04-13 14:53:46 +08:00
|
|
|
|
liveRefreshTimer = setInterval(() => {
|
|
|
|
|
|
void loadLiveData({ silent: true })
|
|
|
|
|
|
}, LIVE_REFRESH_INTERVAL_MS)
|
2026-03-03 22:04:40 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (loadAllDebounceTimer) {
|
|
|
|
|
|
clearTimeout(loadAllDebounceTimer)
|
|
|
|
|
|
loadAllDebounceTimer = null
|
|
|
|
|
|
}
|
2026-04-13 14:53:46 +08:00
|
|
|
|
|
2026-05-06 02:01:32 +08:00
|
|
|
|
if (providerPerformanceDebounceTimer) {
|
|
|
|
|
|
clearTimeout(providerPerformanceDebounceTimer)
|
|
|
|
|
|
providerPerformanceDebounceTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:53:46 +08:00
|
|
|
|
if (liveRefreshTimer) {
|
|
|
|
|
|
clearInterval(liveRefreshTimer)
|
|
|
|
|
|
liveRefreshTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
|
hasPendingLoadAll = false
|
|
|
|
|
|
loadAllPromise = null
|
|
|
|
|
|
percentilesRequestId += 1
|
|
|
|
|
|
errorsRequestId += 1
|
2026-05-05 11:24:16 +08:00
|
|
|
|
providerPerformanceRequestId += 1
|
2026-04-13 14:53:46 +08:00
|
|
|
|
liveRequestId += 1
|
2026-03-03 22:04:40 +08:00
|
|
|
|
})
|
2026-02-04 02:04:54 +08:00
|
|
|
|
</script>
|