Files
lislgosms/api/src/infrastructure-monitoring/persistent-alerts.ts
T
2026-09-16 18:27:29 +08:00

144 lines
5.5 KiB
TypeScript

import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { createHash } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';
import type { InfrastructureAlert } from './infrastructure-monitoring.contracts';
export async function retainAlerts(prisma: PrismaService, alerts: InfrastructureAlert[], observedAt: Date) {
await prisma.$transaction(async (tx) => {
// Serialize snapshots across API processes; timestamps reject late HTTP results.
await tx.$executeRaw`SELECT pg_advisory_xact_lock(160916, 1)`;
const previous = await tx.infrastructureAlertCollection.findUnique({ where: { id: 'prometheus' } });
if (previous && previous.observedAt >= observedAt) return;
// Read durable work directly: application releases do not install Prometheus rules.
// Keep one occurrence identity until the condition really recovers, even after manual clear.
const reviewCount = await tx.smsAttemptCompletionWork.count({ where: { state: 'needs_review' } });
const oldest = await tx.smsCompletionEvent.findFirst({
where: { processedAt: null, work: { state: { in: ['pending', 'processing', 'retry_wait'] } } },
orderBy: { createdAt: 'asc' },
select: { createdAt: true },
});
const age = oldest ? Math.max(0, (observedAt.getTime() - oldest.createdAt.getTime()) / 1000) : 0;
alerts = [...alerts];
for (const condition of [
{
name: 'SmsCompletionNeedsReview',
active: reviewCount > 0,
severity: 'critical' as const,
summary: '短信收尾工作需要人工排查',
value: String(reviewCount),
threshold: '0',
},
{
name: 'SmsCompletionBacklog',
active: age > 300,
severity: 'warning' as const,
summary: '短信收尾工作等待超过5分钟',
value: `${Math.floor(age)}秒`,
threshold: '300秒',
},
]) {
if (!condition.active) continue;
const fingerprint = createHash('sha256').update(`durable:${condition.name}`).digest('hex').slice(0, 24);
const occurrence = await tx.infrastructureAlertEvent.findFirst({
where: { fingerprint, recoveredAt: null },
orderBy: { activeAt: 'desc' },
});
alerts.push({
fingerprint,
name: condition.name,
severity: condition.severity,
status: 'firing',
startedAt: (occurrence?.activeAt ?? observedAt).toISOString(),
summary: condition.summary,
currentValue: condition.value,
threshold: condition.threshold,
service: '短信收尾',
acknowledged: false,
});
}
await tx.infrastructureAlertCollection.upsert({
where: { id: 'prometheus' },
create: { id: 'prometheus', observedAt },
update: { observedAt },
});
for (const alert of alerts) {
const activeAt = new Date(alert.startedAt);
const payload = JSON.parse(JSON.stringify(alert)) as Prisma.InputJsonValue;
await tx.infrastructureAlertEvent.createMany({
data: [{ fingerprint: alert.fingerprint, activeAt, payload, lastObservedAt: observedAt }],
skipDuplicates: true,
});
await tx.infrastructureAlertEvent.updateMany({
where: { fingerprint: alert.fingerprint, activeAt, lastObservedAt: { lte: observedAt } },
data: { payload, lastObservedAt: observedAt, recoveredAt: null },
});
}
await tx.infrastructureAlertEvent.updateMany({
where: {
recoveredAt: null,
lastObservedAt: { lt: observedAt },
...(alerts.length
? {
NOT: {
OR: alerts.map((alert) => ({ fingerprint: alert.fingerprint, activeAt: new Date(alert.startedAt) })),
},
}
: {}),
},
data: { recoveredAt: observedAt },
});
});
return retainedAlerts(prisma);
}
export async function retainedAlerts(prisma: PrismaService): Promise<InfrastructureAlert[]> {
const records = await prisma.infrastructureAlertEvent.findMany({
where: { clearedAt: null },
orderBy: [{ activeAt: 'desc' }, { id: 'asc' }],
});
return records.map((record) => ({
...(record.payload as unknown as InfrastructureAlert),
...(record.recoveredAt ? { status: 'resolved' } : {}),
acknowledged: false,
}));
}
export async function clearRetainedAlert(
prisma: PrismaService,
fingerprint: string,
rawActiveAt: unknown,
userId: string,
) {
const activeAt = new Date(String(rawActiveAt ?? ''));
if (!/^[a-f0-9]{24}$/.test(fingerprint) || !Number.isFinite(activeAt.getTime()))
throw new BadRequestException('告警标识无效');
return prisma.$transaction(async (tx) => {
const record = await tx.infrastructureAlertEvent.findUnique({
where: { fingerprint_activeAt: { fingerprint, activeAt } },
});
if (!record) throw new NotFoundException('告警记录不存在');
const clearedAt = new Date();
const result = await tx.infrastructureAlertEvent.updateMany({
where: { id: record.id, clearedAt: null },
data: { clearedAt, clearedBy: userId },
});
if (result.count)
await tx.operationLog.create({
data: {
userId,
action: 'monitoring.alert_cleared',
resource: 'infrastructure_alert',
resourceId: record.id,
detail: { fingerprint, activeAt: activeAt.toISOString() },
},
});
return {
fingerprint,
activeAt: activeAt.toISOString(),
cleared: true,
clearedAt: (record.clearedAt ?? clearedAt).toISOString(),
};
});
}