fix(scheduler): читать снимки JSONB и показывать недоступные результаты
This commit is contained in:
1 parent
63b2f75012
commit
92b7399ddb
8 files changed
+102
-9
No files matched your search
@@ -0,0 +1,22 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { test } from "node:test"
|
||||
import { parseSchedulerRunSnapshot } from "./scheduler-run-snapshot"
|
||||
const snapshot = { v: 1, job: "backups", sampledAt: "2026-10-01T00:00:00Z", due: true, created: 2, failures: 0, pruned: 1, uploadFailures: 1, pruneFailures: 1, metadataError: "metadata unavailable", eventErrors: ["event unavailable"], errors: ["S3 unavailable"] }
|
||||
test("JSONB object and legacy JSON preserve the same snapshot and partial diagnostics", () => {
|
||||
assert.deepEqual(parseSchedulerRunSnapshot(snapshot), snapshot)
|
||||
assert.deepEqual(parseSchedulerRunSnapshot(JSON.stringify(snapshot)), snapshot)
|
||||
})
|
||||
test("missing, primitive, broken and future snapshots never throw or become valid empty data", () => {
|
||||
for (const raw of [undefined, null, "", " ", "{", 1, false, [], "null", { ...snapshot, v: 2 }, { ...snapshot, job: "unknown" }, { ...snapshot, sampledAt: "bad" }]) assert.equal(parseSchedulerRunSnapshot(raw), null)
|
||||
})
|
||||
test("invalid backup counts, flags and diagnostic fields are rejected without zero substitution", () => {
|
||||
for (const patch of [{ created: -1 }, { failures: "0" }, { pruned: 1.5 }, { created: undefined }, { due: undefined }, { skipped: "true" }, { errors: [{}] }, { eventErrors: [null] }, { metadataError: {} }, { uploadFailures: -1 }]) assert.equal(parseSchedulerRunSnapshot({ ...snapshot, ...patch }), null)
|
||||
})
|
||||
test("other collector envelopes support both representations and keep missing arrays unknown", () => {
|
||||
for (const job of ["traffic", "uptime_resources", "servers_rest_ping", "uptime_ping", "uptime_speed"]) {
|
||||
const key = job === "uptime_ping" ? "probes" : job === "uptime_speed" ? "runs" : "servers"
|
||||
const value = { v: 1, job, sampledAt: snapshot.sampledAt, [key]: [] }
|
||||
assert.deepEqual(parseSchedulerRunSnapshot(value), value); assert.deepEqual(parseSchedulerRunSnapshot(JSON.stringify(value)), value)
|
||||
assert.equal(parseSchedulerRunSnapshot({ ...value, [key]: undefined }), null)
|
||||
}
|
||||
})
|
||||
@@ -93,6 +93,10 @@ export interface BackupsRunSnapshot {
|
||||
created: number
|
||||
failures: number
|
||||
pruned: number
|
||||
uploadFailures?: number
|
||||
pruneFailures?: number
|
||||
metadataError?: string
|
||||
eventErrors?: string[]
|
||||
errors?: string[]
|
||||
fatalError?: string
|
||||
}
|
||||
@@ -220,12 +224,32 @@ export interface SpeedScheduledRunSnapshot {
|
||||
runs: SpeedRunSnapshot[]
|
||||
}
|
||||
|
||||
export function parseSchedulerRunSnapshot(raw: string | null | undefined): SchedulerRunSnapshot | null {
|
||||
if (raw == null || raw.trim() === "") return null
|
||||
/** PostgreSQL JSONB returns an object; older records may still contain JSON text. */
|
||||
export function parseSchedulerRunSnapshot(raw: unknown): SchedulerRunSnapshot | null {
|
||||
try {
|
||||
const o = JSON.parse(raw) as { v?: unknown; job?: string }
|
||||
if (o?.v !== 1 || typeof o.job !== "string") return null
|
||||
return o as SchedulerRunSnapshot
|
||||
const value: unknown = typeof raw === "string" ? (raw.trim() ? JSON.parse(raw) : null) : raw
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null
|
||||
const o = value as Record<string, unknown>
|
||||
if (o.v !== 1 || typeof o.job !== "string" || typeof o.sampledAt !== "string" || !Number.isFinite(Date.parse(o.sampledAt))) return null
|
||||
if (o.skipped !== undefined && typeof o.skipped !== "boolean") return null
|
||||
if (o.fatalError !== undefined && typeof o.fatalError !== "string") return null
|
||||
for (const field of ["errors", "eventErrors"]) if (o[field] !== undefined && (!Array.isArray(o[field]) || !(o[field] as unknown[]).every(error => typeof error === "string"))) return null
|
||||
const arrays: Record<string, string> = { traffic: "servers", uptime_resources: "servers", servers_rest_ping: "servers", uptime_ping: "probes", uptime_speed: "runs" }
|
||||
if (Object.hasOwn(arrays, o.job)) {
|
||||
if (!Array.isArray(o[arrays[o.job]])) return null
|
||||
} else {
|
||||
const fields: Record<string, string[]> = { gre_bgp: ["greWritten", "bgpWritten"], internet_path: ["homes"], certificates_renew: ["checked", "renewed", "skippedTargets"], backups: ["created", "failures", "pruned"], geoip_update: ["checked", "downloaded", "skippedUnchanged", "bytes"], alert_engine: ["rulesChecked", "standaloneFires", "groupFires"] }
|
||||
if (!Object.hasOwn(fields, o.job) || !fields[o.job].every(field => typeof o[field] === "number" && Number.isSafeInteger(o[field]) && (o[field] as number) >= 0)) return null
|
||||
if (["certificates_renew", "geoip_update"].includes(o.job) && !Array.isArray(o.errors)) return null
|
||||
if (o.job === "backups") {
|
||||
if (typeof o.due !== "boolean") return null
|
||||
for (const field of ["uploadFailures", "pruneFailures"]) if (o[field] !== undefined && (typeof o[field] !== "number" || !Number.isSafeInteger(o[field]) || (o[field] as number) < 0)) return null
|
||||
if (o.metadataError !== undefined && typeof o.metadataError !== "string") return null
|
||||
}
|
||||
if (o.job === "internet_path" && typeof o.snapshotSaved !== "boolean") return null
|
||||
if (o.job === "alert_engine" && typeof o.skippedNoTelegram !== "boolean") return null
|
||||
}
|
||||
return o as unknown as SchedulerRunSnapshot
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -94,5 +94,5 @@ export interface SchedulerRunRowDto {
|
||||
error: string | null
|
||||
durationMs: number
|
||||
/** JSON снимка результатов прогона (серверы, пробы, speed и т.д.) */
|
||||
resultJson?: string | null
|
||||
resultJson?: unknown
|
||||
}
|
||||
Reference in new issue
Block a user