fix(alerts): объяснить отключённые и неоднозначные цели оповещений
This commit is contained in:
1 parent
db7105119d
commit
d3f9a120e6
23 files changed
+1429
-16
No files matched your search
@@ -3,6 +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 type { ApiAlertRule } from "../alerts-service.js"
|
||||
|
||||
const database = process.env.MMAPP_OWN_SQL_DB
|
||||
@@ -41,12 +42,14 @@ else {
|
||||
let signals = await build()
|
||||
assert.equal(signals.servers.length, 2); assert.equal(signals.servers[0].prevStatus, "online")
|
||||
assert.equal(signals.trafficByServer[0].rxMbps, 0); assert.equal(signals.probes.length, 2); cases++
|
||||
assert.deepEqual((await getAlertsMeta()).targetIssues, [])
|
||||
const offlineRule = { id: "owned", name: "owned", type: "server", targets: [server().name], conditions: ["offline"], enabled: true } as ApiAlertRule
|
||||
assert.ok(evaluateRule(offlineRule, signals))
|
||||
|
||||
await pool.query("UPDATE uptime_settings SET resources_enabled=false,ping_enabled=false WHERE id=1")
|
||||
await pool.query("UPDATE traffic_settings SET enabled=false WHERE id=1")
|
||||
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.probes, []); assert.deepEqual(signals.trafficByServer, []); cases++
|
||||
assert((await getAlertsMeta()).targetIssues?.every(issue => issue.reason === "disabled-source"))
|
||||
await pool.query("UPDATE uptime_settings SET resources_enabled=true,ping_enabled=true WHERE id=1")
|
||||
await pool.query("UPDATE traffic_settings SET enabled=true WHERE id=1")
|
||||
await pool.query("UPDATE servers SET enabled=false WHERE id=$1", [ids[0]])
|
||||
@@ -54,6 +57,7 @@ else {
|
||||
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [ids[0]])
|
||||
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server().name, ids[1]])
|
||||
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.trafficByServer, []); cases++
|
||||
assert.deepEqual((await getAlertsMeta()).targetIssues?.filter(issue => issue.reason === "ambiguous").map(issue => issue.kind), ["server", "traffic"])
|
||||
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server(1).name, ids[1]])
|
||||
|
||||
for (const broken of [null, "{", { ...snapshot("uptime_resources", []), servers: {} }, snapshot("uptime_resources", [])]) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AlertTargetIssue } from "@mmapp/contracts/alerts"
|
||||
import type { CollectorSourceConfig } from "./collector-source-signals.js"
|
||||
|
||||
/** Configuration diagnostics only. This does not claim fresh samples or Telegram delivery. */
|
||||
export function collectAlertTargetIssues(config: CollectorSourceConfig): AlertTargetIssue[] {
|
||||
const out: AlertTargetIssue[] = []
|
||||
const servers = new Map<string, typeof config.servers>()
|
||||
for (const server of config.servers) {
|
||||
const key = (server.name || server.host).trim().toLowerCase()
|
||||
if (key) servers.set(key, [...(servers.get(key) ?? []), server])
|
||||
}
|
||||
const serverOn = (config.uptime?.resourcesEnabled ?? config.uptime?.enabled) === true || config.rest?.enabled === true
|
||||
for (const group of servers.values()) {
|
||||
const active = group.filter(s => s.enabled === true), target = (group[0].name || group[0].host).trim()
|
||||
for (const [kind, sourceOn] of [["server", serverOn], ["traffic", config.traffic?.enabled === true]] as const) {
|
||||
const reason = !active.length ? "disabled-object" : active.length > 1 ? "ambiguous" : !sourceOn ? "disabled-source" : null
|
||||
if (reason) out.push({ kind, target, reason })
|
||||
}
|
||||
}
|
||||
const probes = new Map<string, typeof config.probes>()
|
||||
for (const probe of config.probes) {
|
||||
if (!probe.name.trim() || !probe.target.trim()) continue
|
||||
const key = `${probe.name.trim()} → ${probe.target.trim()}`
|
||||
probes.set(key, [...(probes.get(key) ?? []), probe])
|
||||
}
|
||||
for (const [target, group] of probes) {
|
||||
const active = group.filter(p => p.enabled && config.servers.some(s => s.enabled === true && Number(s.id) === p.srcServerId))
|
||||
const sourceOn = (config.uptime?.pingEnabled ?? config.uptime?.enabled) === true
|
||||
const reason = !active.length ? "disabled-object" : active.length > 1 ? "ambiguous" : !sourceOn ? "disabled-source" : null
|
||||
if (reason) out.push({ kind: "probe", target, reason })
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import assert from "node:assert/strict"
|
||||
import {test} from "node:test"
|
||||
import type {AlertsResponse} from "@mmapp/contracts/alerts"
|
||||
import {readAlertsOverview,alertsOverviewTransaction,buildAlertsMeta} from "./alerts-overview.js"
|
||||
import {collectAlertTargetIssues} from "./alert-engine/target-issues.js"
|
||||
import type {CollectorSourceConfig} from "./alert-engine/collector-source-signals.js"
|
||||
const empty:AlertsResponse={revision:alertsConfigRevision({groups:[],rules:[]}),telegram:{chatId:"",tokenConfigured:false,connected:false,messageThreadId:null},groups:[],rules:[],history:[],meta:buildAlertsMeta([],[])}
|
||||
test("Alerts overview uses one context with repeatable read/read only and rejects partial snapshots",async()=>{
|
||||
const context={snapshot:structuredClone(empty)},calls:string[]=[];let transactions=0
|
||||
@@ -17,3 +19,21 @@ test("Alerts metadata exposes existing probe keys without synthesizing per-serve
|
||||
const withoutProbes=buildAlertsMeta(servers,[]);assert.deepEqual(withoutProbes.probeTargets,[]);assert.deepEqual(withoutProbes.rttLossTargets,[]);assert.equal(withoutProbes.serversDetail[0]?.type,"")
|
||||
const actual=buildAlertsMeta(servers,[{name:"probe-fixture",target:"1.1.1.1"}]);assert.deepEqual(actual.probeTargets,["probe-fixture → 1.1.1.1"]);assert.deepEqual(actual.rttLossTargets,actual.probeTargets);assert.equal(JSON.stringify(actual).includes("8.8.8.8"),false)
|
||||
})
|
||||
test("Target diagnostics explain ambiguous and disabled configuration without claiming live status",()=>{
|
||||
const config:CollectorSourceConfig={servers:[{id:1,name:"CHR",host:"one",enabled:true},{id:2,name:" chr ",host:"two",enabled:true}],probes:[],uptime:{enabled:true,resourcesEnabled:true,pingEnabled:true,intervalSec:15,probeIntervalSec:15},rest:{enabled:false,intervalSec:120},traffic:{enabled:true,intervalSec:30}}
|
||||
assert.deepEqual(collectAlertTargetIssues(config),[{kind:"server",target:"CHR",reason:"ambiguous"},{kind:"traffic",target:"CHR",reason:"ambiguous"}])
|
||||
const unique={...config,servers:config.servers.map((s,index)=>({...s,name:"server"+index}))}
|
||||
assert.deepEqual(collectAlertTargetIssues(unique),[])
|
||||
assert.deepEqual(collectAlertTargetIssues({...config,servers:[config.servers[0],{...config.servers[1],enabled:false}]}),[])
|
||||
const disabled=collectAlertTargetIssues({...unique,servers:unique.servers.map(s=>({...s,enabled:false}))});assert.equal(disabled.length,4);assert(disabled.every(i=>i.reason==="disabled-object"))
|
||||
const sourceOff={...unique,uptime:{...unique.uptime!,resourcesEnabled:false},traffic:{...unique.traffic!,enabled:false}}
|
||||
assert(collectAlertTargetIssues(sourceOff).every(i=>i.reason==="disabled-source"))
|
||||
assert.deepEqual(collectAlertTargetIssues({...sourceOff,rest:{...sourceOff.rest!,enabled:true}}).map(i=>i.kind),["traffic","traffic"])
|
||||
const probe={id:"probe",name:"ping",target:"target",srcServerId:1,srcInterface:"",enabled:true,intervalSec:15}
|
||||
const withProbe={...unique,probes:[probe,{...probe,id:"second"}]}
|
||||
assert.deepEqual(collectAlertTargetIssues(withProbe),[{kind:"probe",target:"ping → target",reason:"ambiguous"}])
|
||||
assert.deepEqual(collectAlertTargetIssues({...withProbe,probes:[probe,{...probe,id:"second",enabled:false}]}),[])
|
||||
assert.equal(collectAlertTargetIssues({...unique,probes:[probe],uptime:{...unique.uptime!,pingEnabled:false}})[0].reason,"disabled-source")
|
||||
const current=buildAlertsMeta(unique.servers.map(s=>({...s,site:null,country:null,type:null})),[probe],unique)
|
||||
assert.deepEqual(current.targetIssues,[]);assert.equal("password" in current,false)
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import {alertsConfigRevision} from "./alerts-revision.js"
|
||||
import {alertsResponseSchema,type AlertsResponse} from "@mmapp/contracts/alerts"
|
||||
import {collectAlertTargetIssues} from "./alert-engine/target-issues.js"
|
||||
import type {CollectorSourceConfig} from "./alert-engine/collector-source-signals.js"
|
||||
export const alertsOverviewTransaction={isolationLevel:"repeatable read",accessMode:"read only"} as const
|
||||
interface Dependencies<C>{transaction:(work:(context:C)=>Promise<AlertsResponse>,options:typeof alertsOverviewTransaction)=>Promise<AlertsResponse>;telegram:(context:C)=>Promise<unknown>;groups:(context:C)=>Promise<unknown>;rules:(context:C)=>Promise<unknown>;history:(context:C)=>Promise<unknown>;meta:(context:C)=>Promise<unknown>}
|
||||
export async function readAlertsOverview<C>(deps:Dependencies<C>):Promise<AlertsResponse>{
|
||||
@@ -14,8 +16,8 @@ export async function readAlertsOverview<C>(deps:Dependencies<C>):Promise<Alerts
|
||||
},alertsOverviewTransaction)
|
||||
}
|
||||
|
||||
export function buildAlertsMeta(servers:{name:string|null;host:string;site:string|null;country:string|null;type:string|null}[],probes:{name:string;target:string}[]){
|
||||
export function buildAlertsMeta(servers:{name:string|null;host:string;site:string|null;country:string|null;type:string|null}[],probes:{name:string;target:string}[],config?:CollectorSourceConfig){
|
||||
const labels=servers.map(server=>server.name?.trim()||server.host)
|
||||
const probeTargets=probes.map(probe=>probe.name+" → "+probe.target)
|
||||
return {servers:labels,greClients:labels,probeTargets,rttLossTargets:probeTargets,trafficServers:labels,serversDetail:servers.map(server=>({name:server.name?.trim()||"",host:server.host.trim(),site:server.site?.trim()||"",country:server.country?.trim()||"",type:server.type??""}))}
|
||||
return {servers:labels,greClients:labels,probeTargets,rttLossTargets:probeTargets,trafficServers:labels,serversDetail:servers.map(server=>({name:server.name?.trim()||"",host:server.host.trim(),site:server.site?.trim()||"",country:server.country?.trim()||"",type:server.type??""})),...(config?{targetIssues:collectAlertTargetIssues(config)}:{})}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
alertTelegramSettings,
|
||||
servers,
|
||||
uptimeProbes,
|
||||
uptimeSettings,
|
||||
serversApiPingSettings,
|
||||
trafficSettings,
|
||||
} from "../db/schema.js"
|
||||
|
||||
export type ApiAlertGroup = {
|
||||
@@ -495,6 +498,8 @@ export async function listMergedHistory(persistLimit = 40, liveCap = 25,context:
|
||||
export async function getAlertsMeta(context:AlertsReadContext=db) {
|
||||
const srvRows = await context
|
||||
.select({
|
||||
id: servers.id,
|
||||
enabled: servers.enabled,
|
||||
name: servers.name,
|
||||
host: servers.host,
|
||||
site: servers.site,
|
||||
@@ -502,8 +507,12 @@ export async function getAlertsMeta(context:AlertsReadContext=db) {
|
||||
type: servers.type,
|
||||
})
|
||||
.from(servers)
|
||||
const probes=await context.select({name:uptimeProbes.name,target:uptimeProbes.target}).from(uptimeProbes)
|
||||
return buildAlertsMeta(srvRows,probes)
|
||||
const probes=await context.select({id:uptimeProbes.id,name:uptimeProbes.name,target:uptimeProbes.target,enabled:uptimeProbes.enabled,
|
||||
srcServerId:uptimeProbes.srcServerId,srcInterface:uptimeProbes.srcInterface,intervalSec:uptimeProbes.intervalSec}).from(uptimeProbes)
|
||||
const uptime=(await context.select().from(uptimeSettings).where(eq(uptimeSettings.id,1)).limit(1))[0]
|
||||
const rest=(await context.select().from(serversApiPingSettings).where(eq(serversApiPingSettings.id,1)).limit(1))[0]
|
||||
const traffic=(await context.select().from(trafficSettings).where(eq(trafficSettings.id,1)).limit(1))[0]
|
||||
return buildAlertsMeta(srvRows,probes,{servers:srvRows,probes,uptime,rest,traffic})
|
||||
}
|
||||
|
||||
export async function getAlertsOverview(){
|
||||
|
||||
Reference in new issue
Block a user