diff --git a/vehicle-data-platform/apps/api/internal/app/server.go b/vehicle-data-platform/apps/api/internal/app/server.go index 20a166c7..752c45e1 100644 --- a/vehicle-data-platform/apps/api/internal/app/server.go +++ b/vehicle-data-platform/apps/api/internal/app/server.go @@ -37,7 +37,9 @@ func NewServer(cfg config.Config) http.Handler { log.Printf("production mysql store enabled") } } - api := platform.NewHandler(platform.NewService(store)) + api := platform.NewHandler(platform.NewServiceWithRuntime(store, platform.RuntimeInfo{ + RequestTimeoutMs: int(cfg.RequestTimeout / time.Millisecond), + })) return withRequestTimeout(static.Handler(cfg.StaticDir, api), cfg.RequestTimeout) } diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 2e086c8f..8ccbf783 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -195,6 +195,25 @@ func TestHandlerHistoryMileageQualityOps(t *testing.T) { } } +func TestHandlerOpsHealthIncludesVehicleServiceRuntime(t *testing.T) { + handler := NewHandler(NewServiceWithRuntime(NewMockStore(), RuntimeInfo{RequestTimeoutMs: 1500})) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/ops/health", nil) + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) + } + var body struct { + Data OpsHealth `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String()) + } + if body.Data.Runtime.RequestTimeoutMs != 1500 { + t.Fatalf("ops health should include request timeout runtime, got %+v body=%s", body.Data.Runtime, rec.Body.String()) + } +} + func TestHandlerVehicleDataAPIsResolveVehicleKeyword(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) cases := []struct { diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index 0106eef2..c6d7339c 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -184,4 +184,9 @@ type OpsHealth struct { RedisOnlineKeys *int `json:"redisOnlineKeys"` TDengineWritable bool `json:"tdengineWritable"` MySQLWritable bool `json:"mysqlWritable"` + Runtime RuntimeInfo `json:"runtime"` +} + +type RuntimeInfo struct { + RequestTimeoutMs int `json:"requestTimeoutMs"` } diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index d5dc168c..7749e427 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -36,13 +36,18 @@ type RawFrameQuery struct { } type Service struct { - store Store + store Store + runtime RuntimeInfo } func NewService(store Store) *Service { return &Service{store: store} } +func NewServiceWithRuntime(store Store, runtime RuntimeInfo) *Service { + return &Service{store: store, runtime: runtime} +} + func (s *Service) DashboardSummary(ctx context.Context) (DashboardSummary, error) { return s.store.DashboardSummary(ctx) } @@ -282,7 +287,12 @@ func (s *Service) QualityIssues(ctx context.Context, query url.Values) (Page[Qua } func (s *Service) OpsHealth(ctx context.Context) (OpsHealth, error) { - return s.store.OpsHealth(ctx) + health, err := s.store.OpsHealth(ctx) + if err != nil { + return OpsHealth{}, err + } + health.Runtime = s.runtime + return health, nil } func (s *Service) resolveVehicleQuery(ctx context.Context, query url.Values) (url.Values, error) { diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index f256387b..e45bd1a7 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -174,6 +174,11 @@ export interface OpsHealth { redisOnlineKeys: number | null; tdengineWritable: boolean; mysqlWritable: boolean; + runtime: RuntimeInfo; +} + +export interface RuntimeInfo { + requestTimeoutMs: number; } export interface Page { diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 32863c4b..44383c84 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -20,6 +20,11 @@ function storageReadStatus(health: OpsHealth | null) { return health?.tdengineWritable && health.mysqlWritable ? 'ok' : 'error'; } +function formatRequestTimeout(health: OpsHealth | null) { + const value = health?.runtime?.requestTimeoutMs; + return value == null || value <= 0 ? '未限制' : `${value.toLocaleString()} ms`; +} + const emptySummary: QualitySummary = { issueVehicleCount: 0, issueRecordCount: 0, @@ -125,9 +130,10 @@ export function Quality({ ))} - {formatLag(health?.kafkaLag)} - {formatLag(health?.redisOnlineKeys)} - + {formatLag(health?.kafkaLag)} + {formatLag(health?.redisOnlineKeys)} + {formatRequestTimeout(health)} + {storageReadStatus(health) === 'ok' ? '正常' : '异常'}