feat(platform): expose capacity check findings
This commit is contained in:
@@ -285,6 +285,7 @@ type OpsHealth struct {
|
|||||||
LinkHealth []LinkHealth `json:"linkHealth"`
|
LinkHealth []LinkHealth `json:"linkHealth"`
|
||||||
KafkaLag *int `json:"kafkaLag"`
|
KafkaLag *int `json:"kafkaLag"`
|
||||||
ActiveConnections *int `json:"activeConnections"`
|
ActiveConnections *int `json:"activeConnections"`
|
||||||
|
CapacityFindings []string `json:"capacityFindings"`
|
||||||
RedisOnlineKeys *int `json:"redisOnlineKeys"`
|
RedisOnlineKeys *int `json:"redisOnlineKeys"`
|
||||||
TDengineWritable bool `json:"tdengineWritable"`
|
TDengineWritable bool `json:"tdengineWritable"`
|
||||||
MySQLWritable bool `json:"mysqlWritable"`
|
MySQLWritable bool `json:"mysqlWritable"`
|
||||||
|
|||||||
@@ -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, activeConnections := s.capacityCheckHealth(ctx)
|
capacityHealth, kafkaLag, activeConnections, capacityFindings := 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{
|
||||||
@@ -918,22 +918,23 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
|
|||||||
},
|
},
|
||||||
KafkaLag: kafkaLag,
|
KafkaLag: kafkaLag,
|
||||||
ActiveConnections: activeConnections,
|
ActiveConnections: activeConnections,
|
||||||
|
CapacityFindings: capacityFindings,
|
||||||
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, *int) {
|
func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth, *int, *int, []string) {
|
||||||
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, nil
|
return health, nil, nil, []string{}
|
||||||
}
|
}
|
||||||
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, nil
|
return health, nil, nil, []string{}
|
||||||
}
|
}
|
||||||
kafkaLag := int(result.KafkaLag)
|
kafkaLag := int(result.KafkaLag)
|
||||||
activeConnections := int(result.ActiveConnections)
|
activeConnections := int(result.ActiveConnections)
|
||||||
@@ -947,7 +948,7 @@ func (s *ProductionStore) capacityCheckHealth(ctx context.Context) (LinkHealth,
|
|||||||
health.Status = "warning"
|
health.Status = "warning"
|
||||||
}
|
}
|
||||||
health.Detail = detail
|
health.Detail = detail
|
||||||
return health, &kafkaLag, &activeConnections
|
return health, &kafkaLag, &activeConnections, append([]string{}, result.Findings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) {
|
func (s *ProductionStore) redisOnlineKeyHealth(ctx context.Context) (LinkHealth, *int) {
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) {
|
|||||||
if health.ActiveConnections == nil || *health.ActiveConnections != 120000 {
|
if health.ActiveConnections == nil || *health.ActiveConnections != 120000 {
|
||||||
t.Fatalf("OpsHealth should expose capacity-check active connections, got %+v", health.ActiveConnections)
|
t.Fatalf("OpsHealth should expose capacity-check active connections, got %+v", health.ActiveConnections)
|
||||||
}
|
}
|
||||||
|
if len(health.CapacityFindings) != 1 || health.CapacityFindings[0] != "kafka lag 42" {
|
||||||
|
t.Fatalf("OpsHealth should expose structured capacity findings, got %+v", health.CapacityFindings)
|
||||||
|
}
|
||||||
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" {
|
||||||
@@ -242,6 +245,30 @@ func TestOpsHealthUsesCapacityCheckProbeForKafkaLag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpsHealthReturnsEmptyCapacityFindingsArray(t *testing.T) {
|
||||||
|
db, err := sql.Open("ops_health_test", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open ops health db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
store := &ProductionStore{
|
||||||
|
db: db,
|
||||||
|
capacityCheck: fakeCapacityCheckProbe{result: CapacityCheckResult{
|
||||||
|
Status: "ok",
|
||||||
|
ActiveConnections: 254,
|
||||||
|
KafkaLag: 0,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
health, err := store.OpsHealth(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpsHealth returned error: %v", err)
|
||||||
|
}
|
||||||
|
if health.CapacityFindings == nil || len(health.CapacityFindings) != 0 {
|
||||||
|
t.Fatalf("OpsHealth should expose an empty capacity findings array, got %#v", health.CapacityFindings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVehicleServiceOverviewMatchesPartialKeyword(t *testing.T) {
|
func TestVehicleServiceOverviewMatchesPartialKeyword(t *testing.T) {
|
||||||
overview := VehicleServiceOverview{
|
overview := VehicleServiceOverview{
|
||||||
VIN: "LB9A32A24R0LS1426",
|
VIN: "LB9A32A24R0LS1426",
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export interface OpsHealth {
|
|||||||
linkHealth: LinkHealth[];
|
linkHealth: LinkHealth[];
|
||||||
kafkaLag: number | null;
|
kafkaLag: number | null;
|
||||||
activeConnections: number | null;
|
activeConnections: number | null;
|
||||||
|
capacityFindings: string[];
|
||||||
redisOnlineKeys: number | null;
|
redisOnlineKeys: number | null;
|
||||||
tdengineWritable: boolean;
|
tdengineWritable: boolean;
|
||||||
mysqlWritable: boolean;
|
mysqlWritable: boolean;
|
||||||
|
|||||||
@@ -198,6 +198,15 @@ export function Quality({
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{health?.capacityFindings?.length ? (
|
||||||
|
<Card bordered title="容量检查发现" style={{ marginTop: 16 }}>
|
||||||
|
<Space wrap>
|
||||||
|
{health.capacityFindings.map((item) => (
|
||||||
|
<Tag key={item} color="orange">{item}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
{filterSummary.length > 0 ? (
|
{filterSummary.length > 0 ? (
|
||||||
<Card bordered title="当前筛选" style={{ marginTop: 16 }}>
|
<Card bordered title="当前筛选" style={{ marginTop: 16 }}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
|||||||
@@ -4153,6 +4153,31 @@ test('quality health shows active connection capacity metric', async () => {
|
|||||||
expect(screen.getByText('120,000')).toBeInTheDocument();
|
expect(screen.getByText('120,000')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('quality health shows structured capacity findings', 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: 42, activeConnections: 120000, capacityFindings: ['kafka lag 42'], 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('kafka lag 42')).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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user