feat(platform): expose active connection capacity

This commit is contained in:
lingniu
2026-07-04 08:52:57 +08:00
parent b26a37f6f7
commit e29f8768ca
6 changed files with 49 additions and 17 deletions

View File

@@ -284,6 +284,7 @@ type QualityBucketStat struct {
type OpsHealth struct { type OpsHealth struct {
LinkHealth []LinkHealth `json:"linkHealth"` LinkHealth []LinkHealth `json:"linkHealth"`
KafkaLag *int `json:"kafkaLag"` KafkaLag *int `json:"kafkaLag"`
ActiveConnections *int `json:"activeConnections"`
RedisOnlineKeys *int `json:"redisOnlineKeys"` RedisOnlineKeys *int `json:"redisOnlineKeys"`
TDengineWritable bool `json:"tdengineWritable"` TDengineWritable bool `json:"tdengineWritable"`
MySQLWritable bool `json:"mysqlWritable"` MySQLWritable bool `json:"mysqlWritable"`

View File

@@ -905,7 +905,7 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
locationHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_location", "读取实时位置表正常") locationHealth := s.mysqlTableReadHealth(ctx, "vehicle_realtime_location", "读取实时位置表正常")
tdengineHealth := s.tdengineRawFrameHealth(ctx) tdengineHealth := s.tdengineRawFrameHealth(ctx)
redisHealth, redisOnlineKeys := s.redisOnlineKeyHealth(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" mysqlWritable := mysqlStatus == "ok" && snapshotHealth.Status == "ok" && locationHealth.Status == "ok"
return OpsHealth{ return OpsHealth{
LinkHealth: []LinkHealth{ LinkHealth: []LinkHealth{
@@ -917,25 +917,27 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
redisHealth, redisHealth,
}, },
KafkaLag: kafkaLag, KafkaLag: kafkaLag,
ActiveConnections: activeConnections,
RedisOnlineKeys: redisOnlineKeys, RedisOnlineKeys: redisOnlineKeys,
TDengineWritable: tdengineHealth.Status == "ok", TDengineWritable: tdengineHealth.Status == "ok",
MySQLWritable: mysqlWritable, MySQLWritable: mysqlWritable,
}, nil }, 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"} health := LinkHealth{Name: "Capacity check", Status: "warning", Detail: "平台暂未接入 capacity-check"}
if s.capacityCheck == nil { if s.capacityCheck == nil {
return health, nil return health, nil, nil
} }
result, err := s.capacityCheck.CheckCapacity(ctx) result, err := s.capacityCheck.CheckCapacity(ctx)
if err != nil { if err != nil {
health.Status = "error" health.Status = "error"
health.Detail = err.Error() health.Detail = err.Error()
return health, nil return health, nil, nil
} }
kafkaLag := int(result.KafkaLag) 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 { if len(result.Findings) > 0 {
detail += ", findings=" + strings.Join(result.Findings, "; ") detail += ", findings=" + strings.Join(result.Findings, "; ")
} }
@@ -945,7 +947,7 @@ func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth,
health.Status = "warning" health.Status = "warning"
} }
health.Detail = detail health.Detail = detail
return health, &kafkaLag return health, &kafkaLag, &activeConnections
} }
func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) { func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) {

View File

@@ -227,6 +227,9 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) {
if health.KafkaLag == nil || *health.KafkaLag != 42 { if health.KafkaLag == nil || *health.KafkaLag != 42 {
t.Fatalf("OpsHealth should expose capacity-check kafka lag, got %+v", health.KafkaLag) 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 var capacityHealth *LinkHealth
for index := range health.LinkHealth { for index := range health.LinkHealth {
if health.LinkHealth[index].Name == "Capacity check" { if health.LinkHealth[index].Name == "Capacity check" {

View File

@@ -274,6 +274,7 @@ export interface QualityBucketStat {
export interface OpsHealth { export interface OpsHealth {
linkHealth: LinkHealth[]; linkHealth: LinkHealth[];
kafkaLag: number | null; kafkaLag: number | null;
activeConnections: number | null;
redisOnlineKeys: number | null; redisOnlineKeys: number | null;
tdengineWritable: boolean; tdengineWritable: boolean;
mysqlWritable: boolean; mysqlWritable: boolean;

View File

@@ -188,8 +188,8 @@ export function Quality({
</div> </div>
<Row gutter={16}> <Row gutter={16}>
<Col span={6}><Card bordered title="Kafka Lag">{formatLag(health?.kafkaLag)}</Card></Col> <Col span={6}><Card bordered title="Kafka Lag">{formatLag(health?.kafkaLag)}</Card></Col>
<Col span={6}><Card bordered title="活跃连接">{formatLag(health?.activeConnections)}</Card></Col>
<Col span={6}><Card bordered title="Redis 在线 Key">{formatLag(health?.redisOnlineKeys)}</Card></Col> <Col span={6}><Card bordered title="Redis 在线 Key">{formatLag(health?.redisOnlineKeys)}</Card></Col>
<Col span={6}><Card bordered title="BFF 请求超时">{formatRequestTimeout(health)}</Card></Col>
<Col span={6}> <Col span={6}>
<Card bordered title="存储读取"> <Card bordered title="存储读取">
<Tag color={statusColor[storageReadStatus(health)] ?? 'grey'}> <Tag color={statusColor[storageReadStatus(health)] ?? 'grey'}>

View File

@@ -4128,6 +4128,31 @@ test('quality health storage card stays pending before ops health loads', async
expect(screen.queryByText('异常')).not.toBeInTheDocument(); 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(<App />);
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 () => { test('shows vehicle service evidence chain before source details', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {