feat(platform): expose amap runtime health
This commit is contained in:
@@ -52,7 +52,11 @@ func NewServer(cfg config.Config) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
api := platform.NewHandler(platform.NewServiceWithRuntime(store, platform.RuntimeInfo{
|
api := platform.NewHandler(platform.NewServiceWithRuntime(store, platform.RuntimeInfo{
|
||||||
RequestTimeoutMs: int(cfg.RequestTimeout / time.Millisecond),
|
RequestTimeoutMs: int(cfg.RequestTimeout / time.Millisecond),
|
||||||
|
AMapWebJSConfigured: strings.TrimSpace(cfg.AMapWebJSKey) != "",
|
||||||
|
AMapSecurityProxyEnabled: strings.TrimSpace(cfg.AMapSecurityCode) != "" && strings.TrimSpace(cfg.AMapServiceHost) != "",
|
||||||
|
AMapSecurityCodeExposed: exposedAMapSecurityCode(cfg) != "",
|
||||||
|
AMapSecurityServiceHost: strings.TrimSpace(cfg.AMapServiceHost),
|
||||||
}))
|
}))
|
||||||
handler := static.Handler(cfg.StaticDir, api)
|
handler := static.Handler(cfg.StaticDir, api)
|
||||||
handler = withAppConfig(handler, cfg)
|
handler = withAppConfig(handler, cfg)
|
||||||
|
|||||||
@@ -889,7 +889,13 @@ func TestHandlerQualityNotificationPlan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerOpsHealthIncludesVehicleServiceRuntime(t *testing.T) {
|
func TestHandlerOpsHealthIncludesVehicleServiceRuntime(t *testing.T) {
|
||||||
handler := NewHandler(NewServiceWithRuntime(NewMockStore(), RuntimeInfo{RequestTimeoutMs: 1500}))
|
handler := NewHandler(NewServiceWithRuntime(NewMockStore(), RuntimeInfo{
|
||||||
|
RequestTimeoutMs: 1500,
|
||||||
|
AMapWebJSConfigured: true,
|
||||||
|
AMapSecurityProxyEnabled: true,
|
||||||
|
AMapSecurityCodeExposed: false,
|
||||||
|
AMapSecurityServiceHost: "/_AMapService",
|
||||||
|
}))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/ops/health", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/ops/health", nil)
|
||||||
handler.ServeHTTP(rec, req)
|
handler.ServeHTTP(rec, req)
|
||||||
@@ -905,6 +911,15 @@ func TestHandlerOpsHealthIncludesVehicleServiceRuntime(t *testing.T) {
|
|||||||
if body.Data.Runtime.RequestTimeoutMs != 1500 {
|
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())
|
t.Fatalf("ops health should include request timeout runtime, got %+v body=%s", body.Data.Runtime, rec.Body.String())
|
||||||
}
|
}
|
||||||
|
if !body.Data.Runtime.AMapWebJSConfigured || !body.Data.Runtime.AMapSecurityProxyEnabled {
|
||||||
|
t.Fatalf("ops health should expose AMap runtime readiness, got %+v body=%s", body.Data.Runtime, rec.Body.String())
|
||||||
|
}
|
||||||
|
if body.Data.Runtime.AMapSecurityCodeExposed {
|
||||||
|
t.Fatalf("ops health should show that AMap security code is not exposed when proxy is enabled: %+v", body.Data.Runtime)
|
||||||
|
}
|
||||||
|
if body.Data.Runtime.AMapSecurityServiceHost != "/_AMapService" {
|
||||||
|
t.Fatalf("ops health should include AMap security service host, got %+v", body.Data.Runtime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerVehicleDataAPIsResolveVehicleKeyword(t *testing.T) {
|
func TestHandlerVehicleDataAPIsResolveVehicleKeyword(t *testing.T) {
|
||||||
|
|||||||
@@ -344,5 +344,9 @@ type OpsHealth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeInfo struct {
|
type RuntimeInfo struct {
|
||||||
RequestTimeoutMs int `json:"requestTimeoutMs"`
|
RequestTimeoutMs int `json:"requestTimeoutMs"`
|
||||||
|
AMapWebJSConfigured bool `json:"amapWebJsConfigured"`
|
||||||
|
AMapSecurityProxyEnabled bool `json:"amapSecurityProxyEnabled"`
|
||||||
|
AMapSecurityCodeExposed bool `json:"amapSecurityCodeExposed"`
|
||||||
|
AMapSecurityServiceHost string `json:"amapSecurityServiceHost"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,10 @@ export interface OpsHealth {
|
|||||||
|
|
||||||
export interface RuntimeInfo {
|
export interface RuntimeInfo {
|
||||||
requestTimeoutMs: number;
|
requestTimeoutMs: number;
|
||||||
|
amapWebJsConfigured?: boolean;
|
||||||
|
amapSecurityProxyEnabled?: boolean;
|
||||||
|
amapSecurityCodeExposed?: boolean;
|
||||||
|
amapSecurityServiceHost?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Page<T> {
|
export interface Page<T> {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography
|
|||||||
import { IconCopy } from '@douyinfe/semi-icons';
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { VehicleRealtimeRow } from '../api/types';
|
import type { OpsHealth, VehicleRealtimeRow } from '../api/types';
|
||||||
import { DataEmpty } from '../components/DataEmpty';
|
import { DataEmpty } from '../components/DataEmpty';
|
||||||
import { PageHeader } from '../components/PageHeader';
|
import { PageHeader } from '../components/PageHeader';
|
||||||
import { SourceStatusTags } from '../components/SourceStatusTags';
|
import { SourceStatusTags } from '../components/SourceStatusTags';
|
||||||
@@ -190,8 +190,13 @@ export function Realtime({
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
|
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
|
||||||
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
|
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
|
||||||
|
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
|
||||||
const amapConfig = getAMapConfig();
|
const amapConfig = getAMapConfig();
|
||||||
const amapConfigured = isAMapConfigured(amapConfig);
|
const runtime = opsHealth?.runtime;
|
||||||
|
const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig);
|
||||||
|
const amapSecurityServiceHost = runtime?.amapSecurityServiceHost || amapConfig.securityServiceHost;
|
||||||
|
const amapSecurityProxyEnabled = runtime?.amapSecurityProxyEnabled ?? Boolean(amapSecurityServiceHost);
|
||||||
|
const amapSecurityCodeExposed = runtime?.amapSecurityCodeExposed ?? Boolean(amapConfig.securityJsCode && !amapSecurityServiceHost);
|
||||||
|
|
||||||
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -212,6 +217,9 @@ export function Realtime({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilters(initialFilters);
|
setFilters(initialFilters);
|
||||||
load(initialFilters, 1, pagination.pageSize);
|
load(initialFilters, 1, pagination.pageSize);
|
||||||
|
api.opsHealth()
|
||||||
|
.then(setOpsHealth)
|
||||||
|
.catch(() => setOpsHealth(null));
|
||||||
}, [JSON.stringify(initialFilters)]);
|
}, [JSON.stringify(initialFilters)]);
|
||||||
|
|
||||||
const applyFilters = (nextFilters: Record<string, string>) => {
|
const applyFilters = (nextFilters: Record<string, string>) => {
|
||||||
@@ -276,9 +284,15 @@ export function Realtime({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '安全代理',
|
label: '安全代理',
|
||||||
value: amapConfig.securityServiceHost || '未启用',
|
value: amapSecurityServiceHost || '未启用',
|
||||||
color: amapConfig.securityServiceHost ? 'green' as const : 'grey' as const,
|
color: amapSecurityProxyEnabled ? 'green' as const : 'grey' as const,
|
||||||
detail: amapConfig.securityServiceHost ? '安全密钥由服务端追加,不下发到浏览器。' : '未配置代理时会退回前端安全码模式。'
|
detail: amapSecurityProxyEnabled ? '安全密钥由服务端追加,不下发到浏览器。' : '未配置代理时会退回前端安全码模式。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '安全码',
|
||||||
|
value: amapSecurityCodeExposed ? '已暴露' : '未暴露',
|
||||||
|
color: amapSecurityCodeExposed ? 'red' as const : 'green' as const,
|
||||||
|
detail: amapSecurityCodeExposed ? '当前安全码会下发到浏览器,只建议调试使用。' : '生产安全码保持在服务端环境变量中。'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '定位覆盖',
|
label: '定位覆盖',
|
||||||
|
|||||||
@@ -7457,7 +7457,7 @@ test('shows production AMap integration status on realtime page', async () => {
|
|||||||
window.history.replaceState(null, '', '/#/realtime');
|
window.history.replaceState(null, '', '/#/realtime');
|
||||||
Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', {
|
Object.defineProperty(window, '__LINGNIU_APP_CONFIG__', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: { amapWebJsKey: 'amap-key', amapSecurityServiceHost: '/_AMapService' }
|
value: {}
|
||||||
});
|
});
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
@@ -7465,7 +7465,20 @@ test('shows production AMap integration status on realtime page', async () => {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } },
|
data: {
|
||||||
|
linkHealth: [],
|
||||||
|
kafkaLag: 0,
|
||||||
|
redisOnlineKeys: 0,
|
||||||
|
tdengineWritable: true,
|
||||||
|
mysqlWritable: true,
|
||||||
|
runtime: {
|
||||||
|
requestTimeoutMs: 5000,
|
||||||
|
amapWebJsConfigured: true,
|
||||||
|
amapSecurityProxyEnabled: true,
|
||||||
|
amapSecurityCodeExposed: false,
|
||||||
|
amapSecurityServiceHost: '/_AMapService'
|
||||||
|
}
|
||||||
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user