feat(devops): diagnose slow HK requests and dependency timings
CI / check (push) Successful in 2m0s
CI / deploy (push) Successful in 1m35s
CI / publish-container (push) Successful in 46s

This commit is contained in:
Simo committed 2026-09-11 09:48:19 +02:00
1 parent 3b76d063a6
commit 00e33623f7
42 files changed
+1335 -132

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
import { z } from "zod";
import type { PerformanceMetrics } from "@/lib/performance-diagnostics";
import { redis } from "@/lib/redis";
const KEY = "cms:performance:v1";
const LIMIT = 200;
const RETENTION_MS = 60 * 60 * 1000;
const measurement = z.object({
count: z.number().nonnegative(),
durationMs: z.number().nonnegative(),
errors: z.number().nonnegative(),
});
const schema = z.object({
operationId: z.string().max(100),
route: z.string().max(300),
method: z.string().max(10),
status: z.number().int(),
at: z.number(),
streaming: z.boolean(),
metrics: z.object({
durationMs: z.number().nonnegative(),
database: measurement,
external: measurement,
}),
});
export interface PerformanceSample {
operationId: string;
route: string;
method: string;
status: number;
at: number;
streaming: boolean;
metrics: PerformanceMetrics;
}
const state = globalThis as typeof globalThis & {
cmsPerformance?: { samples: PerformanceSample[]; pending: number };
};
const buffer = state.cmsPerformance ?? { samples: [], pending: 0 };
state.cmsPerformance = buffer;
/** Best effort: no connection wait, no unbounded offline queue, no request failure. */
export function recordPerformance(sample: PerformanceSample): void {
buffer.samples.unshift(sample);
buffer.samples.length = Math.min(buffer.samples.length, LIMIT);
if (redis?.status !== "ready" || buffer.pending >= 4) return;
buffer.pending++;
try {
void redis
.multi()
.lpush(KEY, JSON.stringify(sample))
.ltrim(KEY, 0, LIMIT - 1)
.expire(KEY, 3600)
.exec()
.catch(() => undefined)
.finally(() => {
buffer.pending--;
});
} catch {
buffer.pending--;
}
}
export async function readPerformance(): Promise<{
samples: PerformanceSample[];
shared: boolean;
}> {
const cutoff = Date.now() - RETENTION_MS;
if (redis?.status === "ready") {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const rows = await Promise.race([
redis.lrange(KEY, 0, LIMIT - 1),
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new Error("Diagnostic storage timeout")),
1000,
);
}),
]);
const samples = rows.flatMap((row) => {
try {
const sample = schema.safeParse(JSON.parse(row));
return sample.success && sample.data.at >= cutoff
? [sample.data]
: [];
} catch {
return [];
}
});
return { samples, shared: true };
} catch {
/* Keep local diagnostics available during a Redis outage. */
} finally {
clearTimeout(timer);
}
}
return {
samples: buffer.samples.filter((sample) => sample.at >= cutoff),
shared: false,
};
}