feat: M1-05 — Observability deepening, AI + Worker performance metrics
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 39s

- GET /admin-api/metrics/ai — AI调用耗时按provider/模型分组
- GET /admin-api/metrics/worker — Worker任务按队列统计成功率
- Admin page: AI performance + Worker performance tabs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 10:56:36 +08:00
parent a08fd4970a
commit 809f125107
2 changed files with 101 additions and 0 deletions

View File

@ -47,4 +47,70 @@ export class AdminMetricsController {
async recent(@Query('limit') limit = 30) { async recent(@Query('limit') limit = 30) {
return this.prisma.apiMetric.findMany({ orderBy: { createdAt: 'desc' }, take: parseInt(String(limit)) }); return this.prisma.apiMetric.findMany({ orderBy: { createdAt: 'desc' }, take: parseInt(String(limit)) });
} }
@Get('ai')
@AdminRoles('SUPER_ADMIN' as AdminRole)
@ApiOperation({ summary: 'AI 调用耗时统计' })
async aiMetrics(@Query('days') days = '7') {
const since = new Date(Date.now() - parseInt(days) * 86400000);
const logs = await this.prisma.aiUsageLog.findMany({
where: { createdAt: { gte: since } },
select: { provider: true, model: true, latencyMs: true, success: true, estimatedCost: true },
});
const byProvider: Record<string, { calls: number; totalLatency: number; totalCost: number; failures: number }> = {};
const byModel: Record<string, { calls: number; totalLatency: number; totalCost: number; failures: number }> = {};
for (const l of logs) {
const pk = l.provider;
if (!byProvider[pk]) byProvider[pk] = { calls: 0, totalLatency: 0, totalCost: 0, failures: 0 };
byProvider[pk].calls++;
byProvider[pk].totalLatency += l.latencyMs;
byProvider[pk].totalCost += l.estimatedCost;
if (!l.success) byProvider[pk].failures++;
const mk = `${l.provider}/${l.model}`;
if (!byModel[mk]) byModel[mk] = { calls: 0, totalLatency: 0, totalCost: 0, failures: 0 };
byModel[mk].calls++;
byModel[mk].totalLatency += l.latencyMs;
byModel[mk].totalCost += l.estimatedCost;
if (!l.success) byModel[mk].failures++;
}
const providers = Object.entries(byProvider).map(([name, d]) => ({
name, calls: d.calls, avgLatencyMs: Math.round(d.totalLatency / d.calls),
totalCost: d.totalCost.toFixed(4), failureRate: ((d.failures / d.calls) * 100).toFixed(1) + '%',
}));
const models = Object.entries(byModel).map(([name, d]) => ({
name, calls: d.calls, avgLatencyMs: Math.round(d.totalLatency / d.calls),
totalCost: d.totalCost.toFixed(4), failureRate: ((d.failures / d.calls) * 100).toFixed(1) + '%',
}));
return { totalCalls: logs.length, days: parseInt(days), providers, models };
}
@Get('worker')
@AdminRoles('SUPER_ADMIN' as AdminRole)
@ApiOperation({ summary: 'Worker 任务耗时统计' })
async workerMetrics(@Query('days') days = '7') {
const since = new Date(Date.now() - parseInt(days) * 86400000);
const logs = await this.prisma.taskLog.findMany({
where: { createdAt: { gte: since } },
select: { queueName: true, status: true },
});
const byQueue: Record<string, { total: number; completed: number; failed: number }> = {};
for (const l of logs) {
if (!byQueue[l.queueName]) byQueue[l.queueName] = { total: 0, completed: 0, failed: 0 };
byQueue[l.queueName].total++;
if (l.status === 'completed' || l.status === 'retried') byQueue[l.queueName].completed++;
if (l.status === 'failed' || l.status === 'error') byQueue[l.queueName].failed++;
}
const queues = Object.entries(byQueue).map(([name, d]) => ({
name, total: d.total, completed: d.completed, failed: d.failed,
successRate: d.total > 0 ? ((d.completed / d.total) * 100).toFixed(1) + '%' : '0%',
}));
return { totalTasks: logs.length, days: parseInt(days), queues };
}
} }

View File

@ -288,4 +288,39 @@ describe('M1 E2E Tests', () => {
expect(res.body.success).toBe(true); expect(res.body.success).toBe(true);
}); });
}); });
// ══════════════════════════════════════════════
// M1-05: Observability 深化
// ══════════════════════════════════════════════
describe('M1-05 Observability Deepening', () => {
let token: string;
beforeAll(async () => { token = await loginAdmin(); });
it('GET /admin-api/metrics/ai → 200 AI performance stats', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/metrics/ai?days=7')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toHaveProperty('totalCalls');
expect(res.body.data).toHaveProperty('providers');
expect(res.body.data).toHaveProperty('models');
});
it('GET /admin-api/metrics/ai → 401 without token', async () => {
await request(app.getHttpServer())
.get('/admin-api/metrics/ai')
.expect(401);
});
it('GET /admin-api/metrics/worker → 200 Worker performance stats', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/metrics/worker?days=7')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toHaveProperty('totalTasks');
expect(res.body.data).toHaveProperty('queues');
});
});
}); });