feat: M1-05 — Observability deepening, AI + Worker performance metrics
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 39s
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:
parent
a08fd4970a
commit
809f125107
@ -47,4 +47,70 @@ export class AdminMetricsController {
|
||||
async recent(@Query('limit') limit = 30) {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,4 +288,39 @@ describe('M1 E2E Tests', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user