From e29f8768ca1eef398393d19f44447705116d52f0 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 08:52:57 +0800 Subject: [PATCH] feat(platform): expose active connection capacity --- .../apps/api/internal/platform/model.go | 13 +++++----- .../api/internal/platform/production_store.go | 22 ++++++++-------- .../platform/production_store_test.go | 3 +++ .../apps/web/src/api/types.ts | 1 + .../apps/web/src/pages/Quality.tsx | 2 +- .../apps/web/src/test/App.test.tsx | 25 +++++++++++++++++++ 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index af8e2c88..b1c9656d 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -282,12 +282,13 @@ type QualityBucketStat struct { } type OpsHealth struct { - LinkHealth []LinkHealth `json:"linkHealth"` - KafkaLag *int `json:"kafkaLag"` - RedisOnlineKeys *int `json:"redisOnlineKeys"` - TDengineWritable bool `json:"tdengineWritable"` - MySQLWritable bool `json:"mysqlWritable"` - Runtime RuntimeInfo `json:"runtime"` + LinkHealth []LinkHealth `json:"linkHealth"` + KafkaLag *int `json:"kafkaLag"` + ActiveConnections *int `json:"activeConnections"` + RedisOnlineKeys *int `json:"redisOnlineKeys"` + TDengineWritable bool `json:"tdengineWritable"` + MySQLWritable bool `json:"mysqlWritable"` + Runtime RuntimeInfo `json:"runtime"` } type RuntimeInfo struct { diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store.go b/vehicle-data-platform/apps/api/internal/platform/production_store.go index 84e73d22..566edf58 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go @@ -905,7 +905,7 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) { locationHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_location", "读取实时位置表正常") tdengineHealth := s.tdengineRawFrameHealth(ctx) redisHealth, redisOnlineKeys := s.redisOnlineKeyHealth(ctx) - capacityHealth, kafkaLag := s.capacityCheckHealth(ctx) + capacityHealth, kafkaLag, activeConnections := s.capacityCheckHealth(ctx) mysqlWritable := mysqlStatus == "ok" && snapshotHealth.Status == "ok" && locationHealth.Status == "ok" return OpsHealth{ LinkHealth: []LinkHealth{ @@ -916,26 +916,28 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) { capacityHealth, redisHealth, }, - KafkaLag: kafkaLag, - RedisOnlineKeys: redisOnlineKeys, - TDengineWritable: tdengineHealth.Status == "ok", - MySQLWritable: mysqlWritable, + KafkaLag: kafkaLag, + ActiveConnections: activeConnections, + RedisOnlineKeys: redisOnlineKeys, + TDengineWritable: tdengineHealth.Status == "ok", + MySQLWritable: mysqlWritable, }, nil } -func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int) { +func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int, *int) { health := LinkHealth{Name: "Capacity check", Status: "warning", Detail: "平台暂未接入 capacity-check"} if s.capacityCheck == nil { - return health, nil + return health, nil, nil } result, err := s.capacityCheck.CheckCapacity(ctx) if err != nil { health.Status = "error" health.Detail = err.Error() - return health, nil + return health, nil, nil } kafkaLag := int(result.KafkaLag) - detail := "active_connections=" + strconv.Itoa(int(result.ActiveConnections)) + ", kafka_lag=" + strconv.Itoa(kafkaLag) + activeConnections := int(result.ActiveConnections) + detail := "active_connections=" + strconv.Itoa(activeConnections) + ", kafka_lag=" + strconv.Itoa(kafkaLag) if len(result.Findings) > 0 { detail += ", findings=" + strings.Join(result.Findings, "; ") } @@ -945,7 +947,7 @@ func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, health.Status = "warning" } health.Detail = detail - return health, &kafkaLag + return health, &kafkaLag, &activeConnections } func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) { diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go index 8889dbde..5b45fec9 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go @@ -227,6 +227,9 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) { if health.KafkaLag == nil || *health.KafkaLag != 42 { t.Fatalf("OpsHealth should expose capacity-check kafka lag, got %+v", health.KafkaLag) } + if health.ActiveConnections == nil || *health.ActiveConnections != 120000 { + t.Fatalf("OpsHealth should expose capacity-check active connections, got %+v", health.ActiveConnections) + } var capacityHealth *LinkHealth for index := range health.LinkHealth { if health.LinkHealth[index].Name == "Capacity check" { diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index ec55110d..9ed4036a 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -274,6 +274,7 @@ export interface QualityBucketStat { export interface OpsHealth { linkHealth: LinkHealth[]; kafkaLag: number | null; + activeConnections: number | null; redisOnlineKeys: number | null; tdengineWritable: boolean; mysqlWritable: boolean; diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 01994295..813a295a 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -188,8 +188,8 @@ export function Quality({ {formatLag(health?.kafkaLag)} + {formatLag(health?.activeConnections)} {formatLag(health?.redisOnlineKeys)} - {formatRequestTimeout(health)} diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index ba2078c8..5f6a162c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4128,6 +4128,31 @@ test('quality health storage card stays pending before ops health loads', async expect(screen.queryByText('异常')).not.toBeInTheDocument(); }); +test('quality health shows active connection capacity metric', async () => { + window.history.replaceState(null, '', '/#/quality'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + return { + ok: true, + json: async () => ({ + data: path.includes('/api/ops/health') + ? { linkHealth: [], kafkaLag: 0, activeConnections: 120000, redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } + : path.includes('/api/quality/summary') + ? { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] } + : { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByRole('heading', { name: '质量治理' })).toBeInTheDocument(); + expect(await screen.findByText('活跃连接')).toBeInTheDocument(); + expect(screen.getByText('120,000')).toBeInTheDocument(); +}); + test('shows vehicle service evidence chain before source details', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {