183 lines
5.6 KiB
TypeScript
183 lines
5.6 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class DashboardService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async summary() {
|
|
const totalContacts = await this.prisma.contact.count();
|
|
const totalWebhookEvents = await this.prisma.webhookEvent.count();
|
|
const failedWebhookEvents = await this.prisma.webhookEvent.count({
|
|
where: { processingStatus: 'failed' },
|
|
});
|
|
|
|
return {
|
|
totalContacts,
|
|
totalWebhookEvents,
|
|
deliveredRate: 0,
|
|
webhookHealth: failedWebhookEvents > 0 ? 'degraded' : 'healthy',
|
|
};
|
|
}
|
|
|
|
async analyticsSummary() {
|
|
const now = new Date();
|
|
const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
const lastHour = new Date(now.getTime() - 60 * 60 * 1000);
|
|
|
|
const [
|
|
totalJobs,
|
|
pendingJobs,
|
|
processingJobs,
|
|
failedJobs24h,
|
|
totalWebhookEvents,
|
|
verifiedWebhookEvents,
|
|
pendingWebhookEvents,
|
|
auditTrailTotal,
|
|
recentJobs,
|
|
recentWebhookEvents,
|
|
] = await this.prisma.$transaction([
|
|
this.prisma.job.count(),
|
|
this.prisma.job.count({ where: { status: 'queued' } }),
|
|
this.prisma.job.count({ where: { status: 'processing' } }),
|
|
this.prisma.job.count({
|
|
where: {
|
|
status: 'failed',
|
|
updatedAt: { gte: last24Hours },
|
|
},
|
|
}),
|
|
this.prisma.webhookEvent.count(),
|
|
this.prisma.webhookEvent.count({ where: { verified: true } }),
|
|
this.prisma.webhookEvent.count({
|
|
where: {
|
|
processingStatus: { not: 'processed' },
|
|
},
|
|
}),
|
|
this.prisma.auditLog.count(),
|
|
this.prisma.job.findMany({
|
|
where: {
|
|
createdAt: { gte: lastHour },
|
|
},
|
|
select: {
|
|
queueName: true,
|
|
status: true,
|
|
attempts: true,
|
|
maxAttempts: true,
|
|
createdAt: true,
|
|
processedAt: true,
|
|
},
|
|
}),
|
|
this.prisma.webhookEvent.findMany({
|
|
where: {
|
|
createdAt: { gte: lastHour },
|
|
},
|
|
select: {
|
|
createdAt: true,
|
|
verified: true,
|
|
processingStatus: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const workerGroups = recentJobs.reduce<Record<string, number>>((acc, job) => {
|
|
const key = `${job.queueName}-worker`;
|
|
acc[key] = (acc[key] || 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
|
|
const workerHealth = Object.entries(workerGroups).map(([name, count]) => {
|
|
const load = Math.max(4, Math.min(100, count * 12));
|
|
return {
|
|
name,
|
|
load,
|
|
tone: load >= 70 ? 'success' : 'warning',
|
|
};
|
|
});
|
|
|
|
const latencySamples = recentJobs
|
|
.filter((job) => job.processedAt)
|
|
.map((job) => job.processedAt!.getTime() - job.createdAt.getTime())
|
|
.filter((value) => Number.isFinite(value) && value >= 0);
|
|
|
|
const averageLatencyMs =
|
|
latencySamples.length > 0
|
|
? Math.round(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length)
|
|
: 120;
|
|
|
|
const processedLastHour = recentJobs.filter((job) => job.status === 'processed').length;
|
|
const webhookLastHour = recentWebhookEvents.length;
|
|
const verifiedWebhookRate =
|
|
totalWebhookEvents > 0 ? Math.round((verifiedWebhookEvents / totalWebhookEvents) * 100) : 0;
|
|
const throughputPerMinute = Math.max(1, Math.round((processedLastHour + webhookLastHour) / 60));
|
|
const averageAttempts =
|
|
recentJobs.length > 0
|
|
? Math.round(
|
|
recentJobs.reduce((sum, job) => sum + Math.min(job.attempts, job.maxAttempts), 0) /
|
|
recentJobs.length,
|
|
)
|
|
: 0;
|
|
|
|
const databaseConnectionsEstimate = Math.max(
|
|
1,
|
|
Math.min(200, recentJobs.length + recentWebhookEvents.length + Math.min(auditTrailTotal, 120)),
|
|
);
|
|
const memoryUsageGbEstimate = Number(
|
|
(1.8 + (recentJobs.length + recentWebhookEvents.length + pendingWebhookEvents) * 0.07).toFixed(1),
|
|
);
|
|
|
|
const apiLatencyBars = [
|
|
Math.max(4, Math.min(100, 32 + pendingJobs * 2)),
|
|
Math.max(4, Math.min(100, 42 + processingJobs * 8)),
|
|
Math.max(4, Math.min(100, 36 + failedJobs24h * 6)),
|
|
Math.max(4, Math.min(100, 56 + pendingWebhookEvents * 8)),
|
|
Math.max(4, Math.min(100, 28 + averageAttempts * 9)),
|
|
Math.max(4, Math.min(100, 22 + processingJobs * 7)),
|
|
Math.max(4, Math.min(100, 30 + verifiedWebhookRate)),
|
|
];
|
|
|
|
const memoryBars = [
|
|
Math.max(4, Math.min(100, 48 + pendingJobs * 3)),
|
|
Math.max(4, Math.min(100, 54 + processingJobs * 5)),
|
|
Math.max(4, Math.min(100, 58 + failedJobs24h * 4)),
|
|
Math.max(4, Math.min(100, 50 + pendingWebhookEvents * 7)),
|
|
Math.max(4, Math.min(100, 55 + verifiedWebhookRate)),
|
|
Math.max(4, Math.min(100, 62 + processingJobs * 4)),
|
|
Math.max(4, Math.min(100, 64 + averageAttempts * 5)),
|
|
];
|
|
|
|
return {
|
|
generatedAt: now.toISOString(),
|
|
queue: {
|
|
pendingJobs,
|
|
processingJobs,
|
|
failedJobs24h,
|
|
},
|
|
workers: workerHealth,
|
|
throughput: {
|
|
perMinute: throughputPerMinute,
|
|
verifiedWebhookRate,
|
|
jobsLastHour: recentJobs.length,
|
|
webhooksLastHour: webhookLastHour,
|
|
},
|
|
metrics: {
|
|
apiLatencyMs: averageLatencyMs,
|
|
apiLatencyBars,
|
|
databaseConnectionsEstimate,
|
|
databaseUsagePercent: Math.max(4, Math.min(100, Math.round((databaseConnectionsEstimate / 200) * 100))),
|
|
memoryUsageGbEstimate,
|
|
memoryBars,
|
|
},
|
|
totals: {
|
|
totalJobs,
|
|
totalWebhookEvents,
|
|
pendingWebhookEvents,
|
|
auditTrailTotal,
|
|
},
|
|
health: {
|
|
status: 'ok',
|
|
database: 'ok',
|
|
},
|
|
};
|
|
}
|
|
}
|