69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { createHash } from 'node:crypto';
|
|
|
|
export function alertHistoryRange(from?: string, to?: string, now = new Date()) {
|
|
const dateKey = (date: Date) => new Date(date.getTime() + 8 * 3600_000).toISOString().slice(0, 10);
|
|
const endDate = to || dateKey(now);
|
|
const startDate = from || dateKey(new Date(now.getTime() - 6 * 86400_000));
|
|
const parse = (value: string) => {
|
|
const result = new Date(`${value}T00:00:00+08:00`);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(result.getTime()) || dateKey(result) !== value) {
|
|
throw new BadRequestException('告警日期无效');
|
|
}
|
|
return result.getTime() / 1000;
|
|
};
|
|
const start = parse(startDate);
|
|
const end = parse(endDate) + 86400;
|
|
if (end <= start || end - start > 31 * 86400) throw new BadRequestException('告警日期范围须为1至31天');
|
|
return { startDate, endDate, start, end: Math.min(end, now.getTime() / 1000) };
|
|
}
|
|
|
|
export type AlertHistoryItem = {
|
|
id: string;
|
|
name: string;
|
|
severity: string;
|
|
service: string;
|
|
instance: string;
|
|
startedAt: string;
|
|
firstObservedAt: string;
|
|
lastObservedAt: string;
|
|
};
|
|
|
|
// ALERTS_FOR_STATE stores activeAt as the sample value, separating repeated trigger cycles.
|
|
// Observation boundaries are not claimed as exact recovery times.
|
|
export function mergeAlertHistory(
|
|
target: Map<string, AlertHistoryItem>,
|
|
series: Array<{ metric: Record<string, string>; values?: [number, string][] }>,
|
|
start: number,
|
|
end: number,
|
|
) {
|
|
for (const { metric, values } of series) {
|
|
const labels = Object.entries(metric)
|
|
.filter(([key]) => key !== '__name__')
|
|
.sort(([a], [b]) => a.localeCompare(b));
|
|
const fingerprint = createHash('sha256').update(JSON.stringify(labels)).digest('hex');
|
|
for (const [time, rawActiveAt] of values ?? []) {
|
|
const activeAt = Number(rawActiveAt);
|
|
if (time < start || time >= end || !Number.isFinite(activeAt) || activeAt <= 0 || activeAt > time) continue;
|
|
const id = `${fingerprint}:${activeAt}`;
|
|
const observed = new Date(time * 1000).toISOString();
|
|
const item = target.get(id);
|
|
if (item) {
|
|
if (observed < item.firstObservedAt) item.firstObservedAt = observed;
|
|
if (observed > item.lastObservedAt) item.lastObservedAt = observed;
|
|
} else {
|
|
target.set(id, {
|
|
id,
|
|
name: metric.alertname || '未命名告警',
|
|
severity: metric.severity || 'info',
|
|
service: metric.service || '',
|
|
instance: metric.instance || '',
|
|
startedAt: new Date(activeAt * 1000).toISOString(),
|
|
firstObservedAt: observed,
|
|
lastObservedAt: observed,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|