mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-21 21:16:30 +02:00
🚧 WIP: batched status
This commit is contained in:
+55
-32
@@ -264,9 +264,7 @@ export function computeBuckets(
|
||||
|
||||
// Shift by the client's offset before formatting so the label reflects
|
||||
// their local calendar date rather than the UTC date of dayStartSec
|
||||
const dateStr = new Date(
|
||||
(dayStartSec + tzOffsetMinutes * 60) * 1000
|
||||
)
|
||||
const dateStr = new Date((dayStartSec + tzOffsetMinutes * 60) * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
|
||||
@@ -305,13 +303,10 @@ export function computeBuckets(
|
||||
return { buckets, totalDowntime };
|
||||
}
|
||||
|
||||
export interface BatchedStatusHistoryResponse {
|
||||
entityType: string;
|
||||
entityIds: number[];
|
||||
days: StatusHistoryDayBucket[];
|
||||
overallUptimePercent: number;
|
||||
totalDowntimeSeconds: number;
|
||||
}
|
||||
export type BatchedStatusHistoryResponse = Record<
|
||||
string,
|
||||
StatusHistoryResponse
|
||||
>;
|
||||
|
||||
export async function getBatchedStatusHistory(
|
||||
entityType: string,
|
||||
@@ -345,8 +340,8 @@ export async function getBatchedStatusHistory(
|
||||
// Fetch the last known state before the window so that entities that
|
||||
// haven't changed status recently still show the correct status rather
|
||||
// than appearing as "no_data".
|
||||
const [lastKnownEvent] = await logsDb
|
||||
.select()
|
||||
const lastKnownEvents = await logsDb
|
||||
.selectDistinctOn([statusHistory.entityId, statusHistory.timestamp])
|
||||
.from(statusHistory)
|
||||
.where(
|
||||
and(
|
||||
@@ -355,29 +350,57 @@ export async function getBatchedStatusHistory(
|
||||
lt(statusHistory.timestamp, startSec)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(statusHistory.timestamp))
|
||||
.limit(1);
|
||||
.orderBy(desc(statusHistory.timestamp));
|
||||
|
||||
const priorStatus = lastKnownEvent?.status ?? null;
|
||||
const eventStatusMap: Record<
|
||||
number,
|
||||
{
|
||||
events: typeof events;
|
||||
lastKnownEvent: (typeof lastKnownEvents)[number] | null;
|
||||
}
|
||||
> = {};
|
||||
|
||||
const { buckets, totalDowntime } = computeBuckets(
|
||||
events,
|
||||
days,
|
||||
priorStatus
|
||||
);
|
||||
const totalWindow = days * 86400;
|
||||
const overallUptime =
|
||||
totalWindow > 0
|
||||
? Math.max(0, ((totalWindow - totalDowntime) / totalWindow) * 100)
|
||||
: 100;
|
||||
for (const event of events) {
|
||||
if (!eventStatusMap[event.entityId]) {
|
||||
eventStatusMap[event.entityId] = {
|
||||
events: [],
|
||||
lastKnownEvent:
|
||||
lastKnownEvents.find(
|
||||
(ev) => ev.entityId === event.entityId
|
||||
) ?? null
|
||||
};
|
||||
}
|
||||
eventStatusMap[event.entityId].events.push(event);
|
||||
}
|
||||
|
||||
const result: BatchedStatusHistoryResponse = {
|
||||
entityType,
|
||||
entityIds,
|
||||
days: buckets,
|
||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
||||
totalDowntimeSeconds: totalDowntime
|
||||
};
|
||||
const result: BatchedStatusHistoryResponse = {};
|
||||
|
||||
for (const entityId in eventStatusMap) {
|
||||
const event = eventStatusMap[Number(entityId)];
|
||||
const priorStatus = event.lastKnownEvent?.status ?? null;
|
||||
|
||||
const { buckets, totalDowntime } = computeBuckets(
|
||||
event.events,
|
||||
days,
|
||||
priorStatus
|
||||
);
|
||||
const totalWindow = days * 86400;
|
||||
const overallUptime =
|
||||
totalWindow > 0
|
||||
? Math.max(
|
||||
0,
|
||||
((totalWindow - totalDowntime) / totalWindow) * 100
|
||||
)
|
||||
: 100;
|
||||
|
||||
result[entityId] = {
|
||||
entityType,
|
||||
entityId: Number(entityId),
|
||||
days: buckets,
|
||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
||||
totalDowntimeSeconds: totalDowntime
|
||||
};
|
||||
}
|
||||
|
||||
// await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
|
||||
return result;
|
||||
|
||||
@@ -11,8 +11,35 @@ import {
|
||||
StatusHistoryResponse
|
||||
} from "@server/lib/statusHistory";
|
||||
|
||||
const siteParamsSchema = z.object({
|
||||
siteId: z.string().transform((v) => parseInt(v, 10))
|
||||
const siteIdParamsSchema = z.object({
|
||||
days: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? parseInt(v, 10) : 90)),
|
||||
// Minutes to add to UTC to get the requesting client's local time
|
||||
// (e.g. Australia/Sydney standard time is 600). Optional and
|
||||
// defaults to 0 (UTC) so older clients keep the prior behavior.
|
||||
tzOffsetMinutes: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? parseInt(v, 10) : 0)),
|
||||
siteIds: z
|
||||
.preprocess((val) => {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
return undefined;
|
||||
}
|
||||
const raw = Array.isArray(val) ? val : [val];
|
||||
const nums = raw
|
||||
.map((v) =>
|
||||
typeof v === "string" ? parseInt(v, 10) : Number(v)
|
||||
)
|
||||
.filter((n) => Number.isInteger(n) && n > 0);
|
||||
const unique = [...new Set(nums)];
|
||||
return unique.length ? unique : undefined;
|
||||
}, z.array(z.number().int().positive()))
|
||||
.openapi({
|
||||
description: "Filter by siteIds (repeat query param)"
|
||||
})
|
||||
});
|
||||
|
||||
export async function getBatchedSiteStatusHistory(
|
||||
@@ -21,16 +48,7 @@ export async function getBatchedSiteStatusHistory(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = siteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const parsedQuery = statusHistoryQuerySchema.safeParse(req.query);
|
||||
const parsedQuery = siteIdParamsSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
Reference in New Issue
Block a user