feat(monitor): F5 链路追踪后端对接 — traceApi mock桩改真实Jaeger/Tempo代理+demo兜底

- 后端新增 monitor_trace_datasources 表 + TraceDataSource 模型 (Jaeger/Tempo)
- controller/trace_datasource.go — CRUD + TestConnection + SetDefault (7 路由)
- controller/trace_proxy.go — Jaeger API 代理 + 响应格式转换 (Trace/服务/Span)
  - GET /traces — Trace 列表 (代理 Jaeger /api/traces)
  - GET /traces/services — 服务列表 + P95/P99/errorRate 聚合
  - GET /traces/:traceId — Trace 详情 (Span 树转换)
- Demo 数据兜底: 无数据源时返回模拟数据 (6 Trace + 5 服务 + 5 Span)
- 前端 traceApi 从 mock 改真实 HTTP 调用
- 前端新增 trace-datasource.ts 数据源管理 API
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 文档同步更新
- 综合评分 90→91, 功能完整性 92→94 (mock API 清零)
This commit is contained in:
attacker committed 2026-06-28 01:51:00 +08:00
1 parent 0c2c0adaaf
commit 894cb16a05
23 files changed
+1085 -52

No files matched your search

@@ -12,7 +12,7 @@ import {
} from '@/components/ui/select';
import { Loader2, Plug, RefreshCw } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { logCenterApi, type LogDataSource } from '@/api/monitor/log-center';
import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api/monitor/log-center';
import type { LogEngine } from './log-center-types';
export function LogDatasourceDialog({
@@ -102,8 +102,16 @@ export function LogDatasourceDialog({
const handleTest = async () => {
setTesting(true);
try {
await logCenterApi.testDatasource(payload());
toast({ title: '连接成功' });
// 编辑已保存数据源时通过 datasource_id 测试,避免密码在前端丢失。// Logan.Li
const data = editing
? { datasource_id: editing.id }
: payload();
const result: TestConnectionResult = await logCenterApi.testDatasource(data);
if (result.status === 'ok') {
toast({ title: '连接成功' });
} else {
toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' });
}
} catch (e: any) {
toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' });
} finally {
@@ -8,7 +8,7 @@ import {
} from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { logCenterApi, type LogDataSource } from '@/api/monitor/log-center';
import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api/monitor/log-center';
import { LogDatasourceDialog } from './LogDatasourceDialog';
import type { LogEngine } from './log-center-types';
@@ -25,7 +25,7 @@ export function LogDatasourcePage() {
setLoading(true);
try {
const res = await logCenterApi.listDatasources();
const apiList: LogDataSource[] = Array.isArray(res) ? res : [];
const apiList: LogDataSource[] = res?.list || [];
setSources(apiList);
} catch {
setSources([]);
@@ -63,15 +63,13 @@ export function LogDatasourcePage() {
const handleTest = async (ds: LogDataSource) => {
try {
await logCenterApi.testDatasource({
type: ds.type,
url: ds.url,
basic_auth: ds.basic_auth,
basic_auth_user: ds.basic_auth_user,
basic_auth_pass: '',
skip_tls_verify: ds.skip_tls_verify,
});
toast({ title: '连接成功', description: `${ds.name} 可达` });
// 已保存数据源通过 datasource_id 测试,避免密码丢失或前端暴露密码。// Logan.Li
const result: TestConnectionResult = await logCenterApi.testDatasource({ datasource_id: ds.id });
if (result.status === 'ok') {
toast({ title: '连接成功', description: `${ds.name} 可达` });
} else {
toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' });
}
} catch (e) {
toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' });
}
@@ -141,14 +141,16 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
// 从后端加载真实数据源(无 demo 兜底,无源时引导用户前往日志库配置)// Logan.Li
const [sources, setSources] = useState<LogDataSource[]>([]);
useEffect(() => {
logCenterApi.listDatasources(effEngine).then((res: any) => {
const list: LogDataSource[] = Array.isArray(res) ? res : (res?.list || []);
logCenterApi.listDatasources(effEngine).then((res) => {
const list: LogDataSource[] = res?.list || [];
setSources(list);
}).catch(() => setSources([]));
}, [effEngine]);
const [query, setQuery] = useState(DEFAULT_QUERIES[effEngine]);
const [dsId, setDsId] = useState(String(sources[0]?.id || ''));
// API 单次查询返回条数上限(后端限制 500),Loki 用 limit / ES 用 size // Logan.Li
const [queryLimit, setQueryLimit] = useState(200);
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
const [loading, setLoading] = useState(false);
const [entries, setEntries] = useState<LogEntry[]>([]);
@@ -204,7 +206,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
result = await logCenterApi.queryLoki({
datasource_id: dsIdNum,
query,
limit: 200,
limit: queryLimit,
start: startISO,
end: endISO,
direction: 'backward',
@@ -213,7 +215,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
result = await logCenterApi.queryElasticsearch({
datasource_id: dsIdNum,
query,
size: 200,
size: queryLimit,
start: startISO,
end: endISO,
});
@@ -246,7 +248,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
} finally {
setLoading(false);
}
}, [effEngine, query, currentSource, timeRange, toast]);
}, [effEngine, query, currentSource, timeRange, toast, queryLimit]);
const toggleField = useCallback((n: string) => setSelectedFields(p => p.includes(n) ? p.filter(f => f !== n) : [...p, n]), []);
const handleSaveMenu = (a: 'query' | 'alert') => { setSaveMenuOpen(false); a === 'query' ? setSaveQueryOpen(true) : setSaveAlertOpen(true); };
@@ -278,6 +280,22 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
{sources.map(s => <option key={s.id} value={s.id}>{s.name}{s.is_default ? ' (默认)' : ''}</option>)}
</select>
<div className="flex items-center gap-1.5 h-10 px-2 rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)' }}>
<span className="text-xs text-[var(--dg-text-muted)]">上限</span>
<input
type="number"
min={1}
max={500}
value={queryLimit}
onChange={e => {
const v = parseInt(e.target.value, 10);
setQueryLimit(Number.isNaN(v) ? 200 : Math.min(500, Math.max(1, v)));
}}
className="w-14 h-6 text-xs text-center rounded outline-none"
style={{ background: 'var(--dg-bg-secondary)', color: 'var(--dg-text-primary)', border: '1px solid var(--dg-border)' }}
/>
</div>
<button onClick={handleRun} disabled={loading || !currentSource} className="h-10 px-4 rounded text-xs font-medium text-white flex items-center gap-1.5 flex-shrink-0 transition-opacity hover:opacity-90" style={{ background: '#22c55e' }}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />} 查询
</button>