fix(alerts): исключить неподтверждённые проблемы из журнала мониторинга
This commit is contained in:
1 parent
d3f9a120e6
commit
ca23284486
20 files changed
+1384
-98
No files matched your search
@@ -8,7 +8,7 @@
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "npm run build -w @mmapp/contracts && tsc",
|
||||
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { sql } from "drizzle-orm"
|
||||
import type { db } from "../../db/index.js"
|
||||
import type { CollectorRead } from "./signals-from-collectors.js"
|
||||
|
||||
/** Bind internal collector SELECTs to the caller's read-only transaction, including all values. */
|
||||
export function createCollectorContextReader(context: Pick<typeof db, "execute">): CollectorRead {
|
||||
return async <T extends object>(query: string, params: unknown[] = []): Promise<T[]> => {
|
||||
const parts = query.split("?")
|
||||
if (parts.length !== params.length + 1) throw Error("Неверные параметры запроса источников оповещений")
|
||||
const statement = sql.empty()
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
statement.append(sql.raw(parts[index]))
|
||||
if (index < params.length) statement.append(sql`${params[index]}`)
|
||||
}
|
||||
const result = await context.execute(statement)
|
||||
return result.rows as T[]
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { pool, closePool } from "../../db/index.js"
|
||||
import { initDatabase } from "../../db/bootstrap.js"
|
||||
import { buildSignalSnapshotFromCollectors } from "./signals-from-collectors.js"
|
||||
import { evaluateRule } from "./evaluate-rule.js"
|
||||
import { getAlertsMeta } from "../alerts-service.js"
|
||||
import { getAlertsMeta, getAlertsOverview } from "../alerts-service.js"
|
||||
import type { ApiAlertRule } from "../alerts-service.js"
|
||||
|
||||
const database = process.env.MMAPP_OWN_SQL_DB
|
||||
@@ -77,12 +77,58 @@ else {
|
||||
await reset(); await insert("uptime_ping", snapshot("uptime_ping", [probe()], -15_000), -15_000)
|
||||
await insert("uptime_ping", null, -1_000, "error")
|
||||
assert.deepEqual((await build()).probes, []); cases++
|
||||
// The journal must use the same confirmed facts, rather than reviving native sample tables.
|
||||
await pool.query("UPDATE uptime_probes SET updated_at=$1 WHERE id=$2", [iso(-900_000), probe().probeId])
|
||||
await pool.query("INSERT INTO uptime_resource_samples(server_id,sampled_at,status) VALUES($1,$2,'offline')", [ids[0], iso(-1_000)])
|
||||
await pool.query("INSERT INTO uptime_probe_samples(probe_id,sampled_at,rtt_ms,loss_pct,status) VALUES($1,$2,8,25,'warn')", [probe().probeId, iso(-1_000)])
|
||||
await pool.query("INSERT INTO alert_history(id,rule_name,severity,message,sent_ok,fired_at) VALUES($1,'owned receipt','info','confirmed delivery receipt',true,$2)", [prefix + "receipt", iso(-1_000)])
|
||||
const freshJournal = async () => {
|
||||
await reset()
|
||||
await insert("uptime_resources", snapshot("uptime_resources", [resource()]))
|
||||
await insert("uptime_ping", snapshot("uptime_ping", [{ ...probe(), status: "warn", lossPct: 25, rttMs: 8 }]))
|
||||
}
|
||||
const journal = async () => {
|
||||
const overview = await getAlertsOverview()
|
||||
assert.equal(overview.history.filter(h => h.source === "db" && h.id === prefix + "receipt").length, 1)
|
||||
assert.equal(overview.history.find(h => h.id === prefix + "receipt")?.sent, true)
|
||||
return overview.history.filter(h => h.source === "live")
|
||||
}
|
||||
await freshJournal()
|
||||
let live = await journal(); assert.equal(live.length, 2); assert(live.every(h => typeof h.firedAt === "string" && h.sent === false)); cases++
|
||||
await pool.query("UPDATE alert_history SET fired_at=$1 WHERE id=$2", [iso(), prefix + "receipt"])
|
||||
assert.equal((await getAlertsOverview()).history[0].id, prefix + "receipt"); cases++
|
||||
await pool.query("UPDATE servers SET enabled=false WHERE id=$1", [ids[0]])
|
||||
assert.deepEqual(await journal(), []); cases++
|
||||
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [ids[0]])
|
||||
await pool.query("UPDATE uptime_settings SET resources_enabled=false,ping_enabled=false WHERE id=1")
|
||||
assert.deepEqual(await journal(), []); cases++
|
||||
await pool.query("UPDATE uptime_settings SET resources_enabled=true,ping_enabled=true WHERE id=1")
|
||||
await reset()
|
||||
await insert("uptime_resources", snapshot("uptime_resources", [resource()], -3_600_000), -3_600_000)
|
||||
await insert("uptime_ping", snapshot("uptime_ping", [probe()], -3_600_000), -3_600_000)
|
||||
assert.deepEqual(await journal(), []); cases++
|
||||
await freshJournal()
|
||||
await insert("uptime_resources", null, 0, "error"); await insert("uptime_ping", null, 0, "error")
|
||||
assert.deepEqual(await journal(), []); cases++
|
||||
await reset(); await insert("uptime_resources", snapshot("uptime_resources", [])); await insert("uptime_ping", snapshot("uptime_ping", []))
|
||||
assert.deepEqual(await journal(), []); cases++
|
||||
await freshJournal()
|
||||
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server().name, ids[1]])
|
||||
live = await journal(); assert.equal(live.length, 1); assert.equal(live[0].ruleName, "Ping-проба"); cases++
|
||||
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server(1).name, ids[1]])
|
||||
await pool.query("UPDATE uptime_probes SET name=$1 WHERE id=$2", [prefix + "changed-probe", probe().probeId])
|
||||
live = await journal(); assert.equal(live.length, 1); assert.equal(live[0].ruleName, "Доступность сервера"); cases++
|
||||
await reset(); assert.deepEqual(await journal(), []); cases++
|
||||
await pool.query("UPDATE uptime_probes SET name=$1 WHERE id=$2", [probe().name, probe().probeId])
|
||||
await reset(); await insert("uptime_ping", snapshot("uptime_ping", [probe()], -15_000), -15_000)
|
||||
await pool.query("UPDATE uptime_probes SET updated_at=$1 WHERE id=$2", [iso(), probe().probeId])
|
||||
assert.deepEqual((await build()).probes, []); cases++
|
||||
console.log(`collector sources owned SQL: ${cases} cases; real settings/identity/error/time/due-only/unknown/reconfiguration/evaluation passed`)
|
||||
} finally {
|
||||
await reset()
|
||||
await pool.query("DELETE FROM alert_history WHERE id LIKE $1", [prefix + "%"])
|
||||
await pool.query("DELETE FROM uptime_resource_samples WHERE server_id=ANY($1::bigint[])", [ids])
|
||||
await pool.query("DELETE FROM uptime_probe_samples WHERE probe_id LIKE $1", [prefix + "%"])
|
||||
await pool.query("DELETE FROM uptime_probes WHERE id LIKE $1", [prefix + "%"])
|
||||
await pool.query("DELETE FROM servers WHERE id = ANY($1::bigint[])", [ids])
|
||||
assert.equal((await pool.query("SELECT count(*)::int AS n FROM scheduler_runs WHERE id LIKE $1", [prefix + "%"])).rows[0].n, 0)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { monitoringHistory } from "./monitoring-history.js"
|
||||
import { alertHistoryResponseSchema } from "@mmapp/contracts/alerts"
|
||||
import type { CollectorSourceSnapshot } from "./signals-from-collectors.js"
|
||||
const sampledAt = "2026-10-12T12:00:00+07:00"
|
||||
const snapshot: CollectorSourceSnapshot = { sampledAt, trafficByServer: [], servers: [
|
||||
{ name: "one", status: "offline", prevStatus: null, prev2Status: null, sampledAt },
|
||||
{ name: "two", status: "online", prevStatus: null, prev2Status: null, sampledAt },
|
||||
], probes: [{ key: "probe → target", status: "warn", rttMs: null, lossPct: 0, sampledAt }] }
|
||||
const result = monitoringHistory(snapshot)
|
||||
assert.equal(result.length, 2)
|
||||
assert(result.every(row => row.source === "live" && row.sent === false && row.firedAt === "2026-10-12T05:00:00.000Z"))
|
||||
assert(result.every(row => alertHistoryResponseSchema.safeParse(row).success))
|
||||
assert(result[1].message.includes("RTT —, потери 0%"))
|
||||
assert.deepEqual(monitoringHistory(snapshot), result)
|
||||
const collisions = monitoringHistory({ ...snapshot, probes: [], servers: ["?", "/"].map(name => ({ ...snapshot.servers[0], name })) })
|
||||
assert.notEqual(collisions[0].id, collisions[1].id)
|
||||
assert.deepEqual(monitoringHistory({ ...snapshot, servers: [], probes: [] }), [])
|
||||
assert.deepEqual(monitoringHistory({ ...snapshot, probes: [], servers: [{ ...snapshot.servers[0], sampledAt: "invalid" }] }), [])
|
||||
console.log("monitoring history: confirmed source only, canonical timestamps, unknown metrics, no delivery claim and unique ids passed")
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import type { CollectorSourceSnapshot } from "./signals-from-collectors.js"
|
||||
import type { ApiAlertHistoryEntry } from "../alerts-service.js"
|
||||
|
||||
function id(prefix: string, parts: string[]): string {
|
||||
return prefix + createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24)
|
||||
}
|
||||
/** These are current monitoring problems, not a receipt of notification delivery. */
|
||||
export function monitoringHistory(snapshot: CollectorSourceSnapshot): ApiAlertHistoryEntry[] {
|
||||
const out: ApiAlertHistoryEntry[] = []
|
||||
for (const server of snapshot.servers) {
|
||||
if (server.status !== "offline" && server.status !== "degraded") continue
|
||||
if (!Number.isFinite(Date.parse(server.sampledAt))) continue
|
||||
const firedAt = new Date(server.sampledAt).toISOString()
|
||||
out.push({ id: id("live-res-", [server.name, server.sampledAt]), ruleName: "Доступность сервера",
|
||||
severity: server.status === "offline" ? "critical" : "warning", message: `${server.name}: статус ${server.status} по данным мониторинга`,
|
||||
sent: false, firedAt, source: "live" })
|
||||
}
|
||||
for (const probe of snapshot.probes) {
|
||||
if (probe.status !== "down" && probe.status !== "warn") continue
|
||||
if (!Number.isFinite(Date.parse(probe.sampledAt))) continue
|
||||
const firedAt = new Date(probe.sampledAt).toISOString()
|
||||
out.push({ id: id("live-prb-", [probe.key, probe.sampledAt]), ruleName: "Ping-проба",
|
||||
severity: probe.status === "down" ? "critical" : "warning", message: `${probe.key}: ${probe.status} (RTT ${probe.rttMs == null ? "—" : probe.rttMs + " мс"}, потери ${probe.lossPct == null ? "—" : probe.lossPct + "%"})`,
|
||||
sent: false, firedAt, source: "live" })
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -5,37 +5,45 @@ import { readGreBgpRunSnapshots } from "../gre-bgp-snapshot-history.js"
|
||||
import { collectSourceSignals, probePeriodMs, type CollectorRunRow, type CollectorSourceConfig } from "./collector-source-signals.js"
|
||||
import type { SignalSnapshot } from "./types.js"
|
||||
|
||||
async function readRuns(job: string, limit: number, since?: string): Promise<CollectorRunRow[]> {
|
||||
return dbAll<CollectorRunRow>(`SELECT result_json AS "resultJson", finished_at AS "finishedAt"
|
||||
export type CollectorRead = <T extends object>(query: string, params?: unknown[]) => Promise<T[]>
|
||||
export type CollectorSourceSnapshot = Pick<SignalSnapshot, "sampledAt" | "servers" | "probes" | "trafficByServer">
|
||||
|
||||
async function readRuns(read: CollectorRead, job: string, limit: number, since?: string): Promise<CollectorRunRow[]> {
|
||||
return read<CollectorRunRow>(`SELECT result_json AS "resultJson", finished_at AS "finishedAt"
|
||||
FROM scheduler_runs WHERE job_key = ? AND status IN ('ok', 'error')
|
||||
${since ? "AND finished_at >= ?" : ""}
|
||||
ORDER BY finished_at DESC, id DESC LIMIT ?`, since ? [job, since, limit] : [job, limit])
|
||||
}
|
||||
|
||||
/** Read-only: do not seed defaults or load credentials while building alert facts. */
|
||||
export async function buildSignalSnapshotFromCollectors(clock: () => number = Date.now): Promise<SignalSnapshot> {
|
||||
export async function readCollectorSourceSnapshot(clock: () => number = Date.now, read: CollectorRead = dbAll): Promise<CollectorSourceSnapshot> {
|
||||
const now = clock()
|
||||
const [servers, probes, uptime, rest, traffic] = await Promise.all([
|
||||
dbAll<CollectorSourceConfig["servers"][number]>("SELECT id, name, host, enabled, updated_at AS \"updatedAt\" FROM servers"),
|
||||
dbAll<CollectorSourceConfig["probes"][number]>(`SELECT id, name, target, src_server_id AS "srcServerId", src_interface AS "srcInterface",
|
||||
read<CollectorSourceConfig["servers"][number]>("SELECT id, name, host, enabled, updated_at AS \"updatedAt\" FROM servers"),
|
||||
read<CollectorSourceConfig["probes"][number]>(`SELECT id, name, target, src_server_id AS "srcServerId", src_interface AS "srcInterface",
|
||||
enabled, interval_sec AS "intervalSec", updated_at AS "updatedAt" FROM uptime_probes`),
|
||||
dbAll<NonNullable<CollectorSourceConfig["uptime"]>>(`SELECT enabled, resources_enabled AS "resourcesEnabled", ping_enabled AS "pingEnabled",
|
||||
read<NonNullable<CollectorSourceConfig["uptime"]>>(`SELECT enabled, resources_enabled AS "resourcesEnabled", ping_enabled AS "pingEnabled",
|
||||
interval_sec AS "intervalSec", probe_interval_sec AS "probeIntervalSec" FROM uptime_settings WHERE id = 1`),
|
||||
dbAll<NonNullable<CollectorSourceConfig["rest"]>>('SELECT enabled, interval_sec AS "intervalSec" FROM servers_api_ping_settings WHERE id = 1'),
|
||||
dbAll<NonNullable<CollectorSourceConfig["traffic"]>>('SELECT enabled, interval_sec AS "intervalSec" FROM traffic_settings WHERE id = 1'),
|
||||
read<NonNullable<CollectorSourceConfig["rest"]>>('SELECT enabled, interval_sec AS "intervalSec" FROM servers_api_ping_settings WHERE id = 1'),
|
||||
read<NonNullable<CollectorSourceConfig["traffic"]>>('SELECT enabled, interval_sec AS "intervalSec" FROM traffic_settings WHERE id = 1'),
|
||||
])
|
||||
const config: CollectorSourceConfig = { servers, probes, uptime: uptime[0], rest: rest[0], traffic: traffic[0] }
|
||||
const periods = probes.filter(p => p.enabled).map(p => probePeriodMs(config, p)).filter(Number.isFinite)
|
||||
const probeWindowMs = 3 * Math.max(5_000, ...periods)
|
||||
const [resourceRuns, restRuns, pingRuns, trafficRuns, greBgpRuns] = await Promise.all([
|
||||
readRuns("uptime_resources", 16), readRuns("servers_rest_ping", 16),
|
||||
const [resourceRuns, restRuns, pingRuns, trafficRuns] = await Promise.all([
|
||||
readRuns(read, "uptime_resources", 16), readRuns(read, "servers_rest_ping", 16),
|
||||
// Due-only batches: cover the longest configured TTL rather than only the last 24 ticks.
|
||||
readRuns("uptime_ping", 4096, new Date(now - probeWindowMs).toISOString()),
|
||||
readRuns("traffic", 8), readGreBgpRunSnapshots(undefined, clock),
|
||||
readRuns(read, "uptime_ping", 4096, new Date(now - probeWindowMs).toISOString()),
|
||||
readRuns(read, "traffic", 8),
|
||||
])
|
||||
return {
|
||||
sampledAt: new Date(now).toISOString(),
|
||||
...collectSourceSignals(config, { resources: resourceRuns, rest: restRuns, ping: pingRuns, traffic: trafficRuns }, now),
|
||||
greTunnels: collectGreSignalsFromRuns(greBgpRuns), bgpSessions: collectBgpSignalsFromRuns(greBgpRuns),
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildSignalSnapshotFromCollectors(clock: () => number = Date.now): Promise<SignalSnapshot> {
|
||||
const now = clock()
|
||||
const [sources, greBgpRuns] = await Promise.all([readCollectorSourceSnapshot(() => now), readGreBgpRunSnapshots(undefined, () => now)])
|
||||
return { ...sources, greTunnels: collectGreSignalsFromRuns(greBgpRuns), bgpSessions: collectBgpSignalsFromRuns(greBgpRuns) }
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import {deliverTelegramAlert,type TelegramAlertInput} from "./alerts-telegram-delivery.js"
|
||||
import {readCollectorSourceSnapshot} from "./alert-engine/signals-from-collectors.js"
|
||||
import {createCollectorContextReader} from "./alert-engine/collector-context-reader.js"
|
||||
import {monitoringHistory} from "./alert-engine/monitoring-history.js"
|
||||
import {alertsConfigRevision} from "./alerts-revision.js"
|
||||
import {readAlertsOverview,buildAlertsMeta} from "./alerts-overview.js"
|
||||
import {executeAlertsConfigReplacement} from "./alerts-config-execution.js"
|
||||
@@ -390,77 +393,9 @@ export async function appendAlertHistory(entry: {
|
||||
})
|
||||
}
|
||||
|
||||
interface LiveResRow {
|
||||
serverName: string
|
||||
sampledAt: string
|
||||
}
|
||||
|
||||
interface LiveProbeRow {
|
||||
probeName: string
|
||||
target: string
|
||||
sampledAt: string
|
||||
status: string
|
||||
rttMs: number | null
|
||||
lossPct: number | null
|
||||
}
|
||||
|
||||
async function loadLiveResourceIssues(context:AlertsReadContext): Promise<LiveResRow[]> {
|
||||
const result=await context.execute(sql.raw(`SELECT s.name AS "serverName", urs.sampled_at AS "sampledAt"
|
||||
FROM uptime_resource_samples urs
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT ON (server_id) server_id, sampled_at, status
|
||||
FROM uptime_resource_samples
|
||||
ORDER BY server_id, sampled_at DESC
|
||||
) latest ON latest.server_id = urs.server_id AND latest.sampled_at = urs.sampled_at
|
||||
INNER JOIN servers s ON s.id = urs.server_id
|
||||
WHERE urs.status = 'offline'`))
|
||||
return result.rows as unknown as LiveResRow[]
|
||||
}
|
||||
|
||||
async function loadLiveProbeIssues(context:AlertsReadContext): Promise<LiveProbeRow[]> {
|
||||
const result=await context.execute(sql.raw(`SELECT p.name AS "probeName", p.target AS target, ups.sampled_at AS "sampledAt",
|
||||
ups.status AS status, ups.rtt_ms AS "rttMs", ups.loss_pct AS "lossPct"
|
||||
FROM uptime_probe_samples ups
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT ON (probe_id) probe_id, sampled_at, status, rtt_ms, loss_pct
|
||||
FROM uptime_probe_samples
|
||||
ORDER BY probe_id, sampled_at DESC
|
||||
) latest ON latest.probe_id = ups.probe_id AND latest.sampled_at = ups.sampled_at
|
||||
INNER JOIN uptime_probes p ON p.id = ups.probe_id
|
||||
WHERE ups.status IN ('down', 'warn')`))
|
||||
return result.rows as unknown as LiveProbeRow[]
|
||||
}
|
||||
|
||||
async function toLiveHistoryEntries(context:AlertsReadContext): Promise<ApiAlertHistoryEntry[]> {
|
||||
const out: ApiAlertHistoryEntry[] = []
|
||||
for (const r of await loadLiveResourceIssues(context)) {
|
||||
const safeId = `live-res-${r.serverName}-${r.sampledAt}`.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
||||
out.push({
|
||||
id: safeId,
|
||||
ruleName: "Сервер (ресурсы)",
|
||||
severity: "critical",
|
||||
message: `${r.serverName}: статус offline по данным мониторинга`,
|
||||
sent: false,
|
||||
firedAt: r.sampledAt,
|
||||
source: "live",
|
||||
})
|
||||
}
|
||||
for (const p of await loadLiveProbeIssues(context)) {
|
||||
const sev = p.status === "down" ? "critical" : "warning"
|
||||
const loss = p.lossPct != null ? `${p.lossPct}%` : "—"
|
||||
const rtt = p.rttMs != null ? `${p.rttMs} мс` : "—"
|
||||
const safeId = `live-prb-${p.probeName}-${p.target}-${p.sampledAt}`.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
||||
out.push({
|
||||
id: safeId,
|
||||
ruleName: "Ping-проба",
|
||||
severity: sev,
|
||||
message: `${p.probeName} → ${p.target}: ${p.status} (RTT ${rtt}, потери ${loss})`,
|
||||
sent: false,
|
||||
firedAt: p.sampledAt,
|
||||
source: "live",
|
||||
})
|
||||
}
|
||||
return out
|
||||
const snapshot = await readCollectorSourceSnapshot(Date.now, createCollectorContextReader(context))
|
||||
return monitoringHistory(snapshot)
|
||||
}
|
||||
|
||||
export async function listPersistedHistory(limit: number,context:AlertsReadContext=db): Promise<ApiAlertHistoryEntry[]> {
|
||||
@@ -483,7 +418,7 @@ export async function listPersistedHistory(limit: number,context:AlertsReadConte
|
||||
export async function listMergedHistory(persistLimit = 40, liveCap = 25,context:AlertsReadContext=db): Promise<ApiAlertHistoryEntry[]> {
|
||||
const persisted = await listPersistedHistory(persistLimit,context)
|
||||
const live = (await toLiveHistoryEntries(context)).slice(0, liveCap)
|
||||
const merged = [...persisted, ...live].sort((a, b) => (a.firedAt < b.firedAt ? 1 : a.firedAt > b.firedAt ? -1 : 0))
|
||||
const merged = [...persisted, ...live].sort((a, b) => Date.parse(b.firedAt) - Date.parse(a.firedAt))
|
||||
const seen = new Set<string>()
|
||||
const dedup: ApiAlertHistoryEntry[] = []
|
||||
for (const e of merged) {
|
||||
|
||||
Reference in new issue
Block a user