mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-26 15:19:57 +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
|
// Shift by the client's offset before formatting so the label reflects
|
||||||
// their local calendar date rather than the UTC date of dayStartSec
|
// their local calendar date rather than the UTC date of dayStartSec
|
||||||
const dateStr = new Date(
|
const dateStr = new Date((dayStartSec + tzOffsetMinutes * 60) * 1000)
|
||||||
(dayStartSec + tzOffsetMinutes * 60) * 1000
|
|
||||||
)
|
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
@@ -305,13 +303,10 @@ export function computeBuckets(
|
|||||||
return { buckets, totalDowntime };
|
return { buckets, totalDowntime };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchedStatusHistoryResponse {
|
export type BatchedStatusHistoryResponse = Record<
|
||||||
entityType: string;
|
string,
|
||||||
entityIds: number[];
|
StatusHistoryResponse
|
||||||
days: StatusHistoryDayBucket[];
|
>;
|
||||||
overallUptimePercent: number;
|
|
||||||
totalDowntimeSeconds: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBatchedStatusHistory(
|
export async function getBatchedStatusHistory(
|
||||||
entityType: string,
|
entityType: string,
|
||||||
@@ -345,8 +340,8 @@ export async function getBatchedStatusHistory(
|
|||||||
// Fetch the last known state before the window so that entities that
|
// Fetch the last known state before the window so that entities that
|
||||||
// haven't changed status recently still show the correct status rather
|
// haven't changed status recently still show the correct status rather
|
||||||
// than appearing as "no_data".
|
// than appearing as "no_data".
|
||||||
const [lastKnownEvent] = await logsDb
|
const lastKnownEvents = await logsDb
|
||||||
.select()
|
.selectDistinctOn([statusHistory.entityId, statusHistory.timestamp])
|
||||||
.from(statusHistory)
|
.from(statusHistory)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -355,29 +350,57 @@ export async function getBatchedStatusHistory(
|
|||||||
lt(statusHistory.timestamp, startSec)
|
lt(statusHistory.timestamp, startSec)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(desc(statusHistory.timestamp))
|
.orderBy(desc(statusHistory.timestamp));
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const priorStatus = lastKnownEvent?.status ?? null;
|
const eventStatusMap: Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
events: typeof events;
|
||||||
|
lastKnownEvent: (typeof lastKnownEvents)[number] | null;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
const { buckets, totalDowntime } = computeBuckets(
|
for (const event of events) {
|
||||||
events,
|
if (!eventStatusMap[event.entityId]) {
|
||||||
days,
|
eventStatusMap[event.entityId] = {
|
||||||
priorStatus
|
events: [],
|
||||||
);
|
lastKnownEvent:
|
||||||
const totalWindow = days * 86400;
|
lastKnownEvents.find(
|
||||||
const overallUptime =
|
(ev) => ev.entityId === event.entityId
|
||||||
totalWindow > 0
|
) ?? null
|
||||||
? Math.max(0, ((totalWindow - totalDowntime) / totalWindow) * 100)
|
};
|
||||||
: 100;
|
}
|
||||||
|
eventStatusMap[event.entityId].events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
const result: BatchedStatusHistoryResponse = {
|
const result: BatchedStatusHistoryResponse = {};
|
||||||
entityType,
|
|
||||||
entityIds,
|
for (const entityId in eventStatusMap) {
|
||||||
days: buckets,
|
const event = eventStatusMap[Number(entityId)];
|
||||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
const priorStatus = event.lastKnownEvent?.status ?? null;
|
||||||
totalDowntimeSeconds: totalDowntime
|
|
||||||
};
|
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);
|
// await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -11,8 +11,35 @@ import {
|
|||||||
StatusHistoryResponse
|
StatusHistoryResponse
|
||||||
} from "@server/lib/statusHistory";
|
} from "@server/lib/statusHistory";
|
||||||
|
|
||||||
const siteParamsSchema = z.object({
|
const siteIdParamsSchema = z.object({
|
||||||
siteId: z.string().transform((v) => parseInt(v, 10))
|
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(
|
export async function getBatchedSiteStatusHistory(
|
||||||
@@ -21,16 +48,7 @@ export async function getBatchedSiteStatusHistory(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedParams = siteParamsSchema.safeParse(req.params);
|
const parsedQuery = siteIdParamsSchema.safeParse(req.query);
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const parsedQuery = statusHistoryQuerySchema.safeParse(req.query);
|
|
||||||
if (!parsedQuery.success) {
|
if (!parsedQuery.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
Reference in New Issue
Block a user