fix(traffic): отличать измеренный ноль от отсутствия наблюдений
This commit is contained in:
1 parent
a28f871fd4
commit
9e0f695684
12 files changed
+111
-30
No files matched your search
@@ -1,12 +1,19 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {test} from "node:test"
|
||||
import {parseDashboardTraffic} from "./dashboard-traffic"
|
||||
const row={id:"1",rxNow:2,txNow:1,rxSeries:[1,2],txSeries:[0,1]}
|
||||
const row={id:"1",rxNow:2,txNow:1,rxSeries:[1,2],txSeries:[0,1],observations:{rxNow:true,txNow:true,rxSeries:[true,true],txSeries:[true,true],sampledAt:"2026-09-20T00:00:00Z"}}
|
||||
test("traffic sums aligned observed series and does not invent sixty samples",()=>{
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[row,{...row,id:"2"}]}),{rx:[2,4],tx:[0,2],rxNow:4,txNow:2})
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[]}),{rx:[],tx:[],rxNow:0,txNow:0})
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[{...row,rxSeries:[],txSeries:[]}]}),{rx:[],tx:[],rxNow:2,txNow:1})
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[row,{...row,id:"2"}]}),{rx:[2,4],tx:[0,2],rxNow:4,txNow:2,observationsKnown:true,sampledAt:new Date(row.observations.sampledAt).toISOString()})
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[]}),{rx:[],tx:[],rxNow:0,txNow:0,observationsKnown:true,sampledAt:null})
|
||||
assert.deepEqual(parseDashboardTraffic({servers:[{...row,rxSeries:[],txSeries:[],observations:{...row.observations,rxSeries:[],txSeries:[]}}]}),{rx:[],tx:[],rxNow:2,txNow:1,observationsKnown:true,sampledAt:new Date(row.observations.sampledAt).toISOString()})
|
||||
})
|
||||
test("missing, duplicate, null, invalid, misaligned and overflowing traffic cannot replace a snapshot",()=>{
|
||||
for(const raw of [{},{servers:[row,row]},{servers:[{...row,rxNow:null}]},{servers:[{...row,txNow:-1}]},{servers:[{...row,rxSeries:[null,1]}]},{servers:[{...row,txSeries:[1]}]},{servers:[row,{...row,id:"2",rxSeries:[],txSeries:[]}]},{servers:[{...row,rxNow:Number.MAX_VALUE},{...row,id:"2",rxNow:Number.MAX_VALUE}]}])assert.throws(()=>parseDashboardTraffic(raw))
|
||||
})
|
||||
|
||||
test("legacy rates and absent observations remain unknown while measured zero remains zero",()=>{
|
||||
const legacy={...row,observations:undefined};assert.equal(parseDashboardTraffic({servers:[legacy]}).rxNow,null);assert.deepEqual(parseDashboardTraffic({servers:[legacy]}).rx,[null,null]);assert.equal(parseDashboardTraffic({servers:[legacy]}).observationsKnown,false)
|
||||
const holes={...row,rxNow:0,txNow:0,rxSeries:[0,0],txSeries:[0,0],observations:{...row.observations,rxSeries:[false,true],txSeries:[false,true]}};const known=parseDashboardTraffic({servers:[holes]});assert.deepEqual(known.rx,[null,0]);assert.equal(known.rxNow,0)
|
||||
assert.equal(parseDashboardTraffic({servers:[row,{...legacy,id:"2"}]}).rxNow,null)
|
||||
for(const observations of [{...row.observations,rxSeries:[]},{...row.observations,sampledAt:null},{...row.observations,sampledAt:"invalid"}])assert.throws(()=>parseDashboardTraffic({servers:[{...row,observations}]}))
|
||||
})
|
||||
@@ -1,14 +1,18 @@
|
||||
import {z} from "zod"
|
||||
const rate=z.number().finite().nonnegative()
|
||||
const row=z.object({id:z.string().trim().min(1),rxNow:rate,txNow:rate,rxSeries:z.array(rate),txSeries:z.array(rate)})
|
||||
const observations=z.object({rxNow:z.boolean(),txNow:z.boolean(),rxSeries:z.array(z.boolean()),txSeries:z.array(z.boolean()),sampledAt:z.string().refine(v=>Number.isFinite(Date.parse(v))).nullable()})
|
||||
const row=z.object({id:z.string().trim().min(1),rxNow:rate,txNow:rate,rxSeries:z.array(rate),txSeries:z.array(rate),observations:observations.optional()}).refine(r=>!r.observations||(r.observations.rxSeries.length===r.rxSeries.length&&r.observations.txSeries.length===r.txSeries.length&&(!(r.observations.rxNow||r.observations.txNow||r.observations.rxSeries.some(Boolean)||r.observations.txSeries.some(Boolean))||r.observations.sampledAt!==null)))
|
||||
const payload=z.object({servers:z.array(row)}).refine(data=>new Set(data.servers.map(r=>r.id)).size===data.servers.length&&data.servers.every(r=>r.rxSeries.length===r.txSeries.length&&r.rxSeries.length===data.servers[0].rxSeries.length))
|
||||
export interface DashboardTraffic {rx:number[];tx:number[];rxNow:number;txNow:number}
|
||||
/** API series share an index axis; unequal series cannot be completed with assumed zero. */
|
||||
export interface DashboardTraffic {rx:(number|null)[];tx:(number|null)[];rxNow:number|null;txNow:number|null;observationsKnown:boolean;sampledAt:string|null}
|
||||
/** Numeric legacy zero alone cannot confirm that the collector observed an interval. */
|
||||
export function parseDashboardTraffic(raw:unknown):DashboardTraffic {
|
||||
const parsed=payload.safeParse(raw)
|
||||
if(!parsed.success)throw Error("Некорректные данные трафика: проверьте счётчики и согласованность серий")
|
||||
const rows=parsed.data.servers,length=rows[0]?.rxSeries.length??0
|
||||
const total={rx:Array.from({length},(_,i)=>rows.reduce((sum,r)=>sum+r.rxSeries[i],0)),tx:Array.from({length},(_,i)=>rows.reduce((sum,r)=>sum+r.txSeries[i],0)),rxNow:rows.reduce((sum,r)=>sum+r.rxNow,0),txNow:rows.reduce((sum,r)=>sum+r.txNow,0)}
|
||||
if(![total.rxNow,total.txNow,...total.rx,...total.tx].every(Number.isFinite))throw Error("Суммарный трафик превышает допустимый диапазон")
|
||||
const sum=(key:"rxNow"|"txNow")=>rows.every(r=>r.observations?.[key])?rows.reduce((sum,r)=>sum+r[key],0):null
|
||||
const series=(key:"rxSeries"|"txSeries")=>Array.from({length},(_,i)=>rows.every(r=>r.observations?.[key][i])?rows.reduce((sum,r)=>sum+r[key][i],0):null)
|
||||
const timestamps=rows.map(r=>r.observations?.sampledAt).filter((v):v is string=>!!v)
|
||||
const total={rx:series("rxSeries"),tx:series("txSeries"),rxNow:sum("rxNow"),txNow:sum("txNow"),observationsKnown:rows.every(r=>!!r.observations),sampledAt:timestamps.length===rows.length&×tamps.length?new Date(timestamps.reduce((oldest,v)=>Math.min(oldest,Date.parse(v)),Infinity)).toISOString():null}
|
||||
if(![total.rxNow,total.txNow,...total.rx,...total.tx].every(v=>v===null||Number.isFinite(v)))throw Error("Суммарный трафик превышает допустимый диапазон")
|
||||
return total
|
||||
}
|
||||
Reference in new issue
Block a user