mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-27 19:22:50 +00:00
Add caching to the hc and fix resource stuff
This commit is contained in:
@@ -1,4 +1,73 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { db, statusHistory } from "@server/db";
|
||||||
|
import { and, eq, gte, asc } from "drizzle-orm";
|
||||||
|
import cache from "@server/lib/cache";
|
||||||
|
|
||||||
|
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||||
|
|
||||||
|
function statusHistoryCacheKey(
|
||||||
|
entityType: string,
|
||||||
|
entityId: number,
|
||||||
|
days: number
|
||||||
|
): string {
|
||||||
|
return `statusHistory:${entityType}:${entityId}:${days}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCachedStatusHistory(
|
||||||
|
entityType: string,
|
||||||
|
entityId: number,
|
||||||
|
days: number
|
||||||
|
): Promise<StatusHistoryResponse> {
|
||||||
|
const cacheKey = statusHistoryCacheKey(entityType, entityId, days);
|
||||||
|
const cached = await cache.get<StatusHistoryResponse>(cacheKey);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
const startSec = nowSec - days * 86400;
|
||||||
|
|
||||||
|
const events = await db
|
||||||
|
.select()
|
||||||
|
.from(statusHistory)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(statusHistory.entityType, entityType),
|
||||||
|
eq(statusHistory.entityId, entityId),
|
||||||
|
gte(statusHistory.timestamp, startSec)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(statusHistory.timestamp));
|
||||||
|
|
||||||
|
const { buckets, totalDowntime } = computeBuckets(events, days);
|
||||||
|
const totalWindow = days * 86400;
|
||||||
|
const overallUptime =
|
||||||
|
totalWindow > 0
|
||||||
|
? Math.max(0, ((totalWindow - totalDowntime) / totalWindow) * 100)
|
||||||
|
: 100;
|
||||||
|
|
||||||
|
const result: StatusHistoryResponse = {
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days: buckets,
|
||||||
|
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
||||||
|
totalDowntimeSeconds: totalDowntime
|
||||||
|
};
|
||||||
|
|
||||||
|
await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidateStatusHistoryCache(
|
||||||
|
entityType: string,
|
||||||
|
entityId: number
|
||||||
|
): Promise<void> {
|
||||||
|
const prefix = `statusHistory:${entityType}:${entityId}:`;
|
||||||
|
const keys = cache.keys().filter((k) => k.startsWith(prefix));
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await cache.del(keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const statusHistoryQuerySchema = z
|
export const statusHistoryQuerySchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
Transaction
|
Transaction
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
|
||||||
import {
|
import {
|
||||||
fireResourceDegradedAlert,
|
fireResourceDegradedAlert,
|
||||||
fireResourceHealthyAlert,
|
fireResourceHealthyAlert,
|
||||||
@@ -61,8 +62,9 @@ export async function fireHealthCheckHealthyAlert(
|
|||||||
status: "healthy",
|
status: "healthy",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("health_check", healthCheckId);
|
||||||
|
|
||||||
await handleResource(orgId, healthCheckTargetId, trx);
|
await handleResource(orgId, healthCheckTargetId, send, trx);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
return;
|
return;
|
||||||
@@ -124,8 +126,9 @@ export async function fireHealthCheckUnhealthyAlert(
|
|||||||
status: "unhealthy",
|
status: "unhealthy",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("health_check", healthCheckId);
|
||||||
|
|
||||||
await handleResource(orgId, healthCheckTargetId, trx);
|
await handleResource(orgId, healthCheckTargetId, send, trx);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
return;
|
return;
|
||||||
@@ -176,8 +179,9 @@ export async function fireHealthCheckUnknownAlert(
|
|||||||
status: "unknown",
|
status: "unknown",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("health_check", healthCheckId);
|
||||||
|
|
||||||
await handleResource(orgId, healthCheckTargetId, trx);
|
await handleResource(orgId, healthCheckTargetId, send, trx);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
return;
|
return;
|
||||||
@@ -190,11 +194,11 @@ export async function fireHealthCheckUnknownAlert(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResource(orgId: string, healthCheckTargetId?: number | null, trx: Transaction | typeof db = db) {
|
async function handleResource(orgId: string, healthCheckTargetId?: number | null, send: boolean = true, trx: Transaction | typeof db = db) {
|
||||||
if (!healthCheckTargetId) {
|
if (!healthCheckTargetId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we have resources lets get them
|
// we have targets lets get them
|
||||||
const [target] = await trx
|
const [target] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(targets)
|
.from(targets)
|
||||||
@@ -204,6 +208,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [resource] = await trx
|
const [resource] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -213,6 +218,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherTargets = await trx
|
const otherTargets = await trx
|
||||||
.select({ hcHealth: targetHealthCheck.hcHealth })
|
.select({ hcHealth: targetHealthCheck.hcHealth })
|
||||||
.from(targets)
|
.from(targets)
|
||||||
@@ -256,6 +262,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
resource.name,
|
resource.name,
|
||||||
undefined,
|
undefined,
|
||||||
|
send,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
} else if (health === "unhealthy") {
|
} else if (health === "unhealthy") {
|
||||||
@@ -264,6 +271,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
resource.name,
|
resource.name,
|
||||||
undefined,
|
undefined,
|
||||||
|
send,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
} else if (health === "healthy") {
|
} else if (health === "healthy") {
|
||||||
@@ -272,6 +280,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
resource.name,
|
resource.name,
|
||||||
undefined,
|
undefined,
|
||||||
|
send,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
} else if (health === "degraded") {
|
} else if (health === "degraded") {
|
||||||
@@ -280,6 +289,7 @@ async function handleResource(orgId: string, healthCheckTargetId?: number | null
|
|||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
resource.name,
|
resource.name,
|
||||||
undefined,
|
undefined,
|
||||||
|
send,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { processAlerts } from "../processAlerts";
|
import { processAlerts } from "../processAlerts";
|
||||||
import { db, statusHistory, Transaction } from "@server/db";
|
import { db, statusHistory, Transaction } from "@server/db";
|
||||||
|
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API
|
// Public API
|
||||||
@@ -35,6 +36,7 @@ export async function fireResourceHealthyAlert(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
resourceName?: string | null,
|
resourceName?: string | null,
|
||||||
extra?: Record<string, unknown>,
|
extra?: Record<string, unknown>,
|
||||||
|
send: boolean = true,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -45,6 +47,11 @@ export async function fireResourceHealthyAlert(
|
|||||||
status: "healthy",
|
status: "healthy",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("resource", resourceId);
|
||||||
|
|
||||||
|
if (!send) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await processAlerts({
|
await processAlerts({
|
||||||
eventType: "resource_healthy",
|
eventType: "resource_healthy",
|
||||||
@@ -90,6 +97,7 @@ export async function fireResourceUnhealthyAlert(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
resourceName?: string | null,
|
resourceName?: string | null,
|
||||||
extra?: Record<string, unknown>,
|
extra?: Record<string, unknown>,
|
||||||
|
send: boolean = true,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -100,6 +108,11 @@ export async function fireResourceUnhealthyAlert(
|
|||||||
status: "unhealthy",
|
status: "unhealthy",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("resource", resourceId);
|
||||||
|
|
||||||
|
if (!send) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await processAlerts({
|
await processAlerts({
|
||||||
eventType: "resource_unhealthy",
|
eventType: "resource_unhealthy",
|
||||||
@@ -145,6 +158,7 @@ export async function fireResourceDegradedAlert(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
resourceName?: string | null,
|
resourceName?: string | null,
|
||||||
extra?: Record<string, unknown>,
|
extra?: Record<string, unknown>,
|
||||||
|
send: boolean = true,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -155,6 +169,11 @@ export async function fireResourceDegradedAlert(
|
|||||||
status: "degraded",
|
status: "degraded",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("resource", resourceId);
|
||||||
|
|
||||||
|
if (!send) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await processAlerts({
|
await processAlerts({
|
||||||
eventType: "resource_degraded",
|
eventType: "resource_degraded",
|
||||||
@@ -200,6 +219,7 @@ export async function fireResourceUnknownAlert(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
resourceName?: string | null,
|
resourceName?: string | null,
|
||||||
extra?: Record<string, unknown>,
|
extra?: Record<string, unknown>,
|
||||||
|
send: boolean = true,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -210,6 +230,11 @@ export async function fireResourceUnknownAlert(
|
|||||||
status: "unknown",
|
status: "unknown",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("resource", resourceId);
|
||||||
|
|
||||||
|
if (!send) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await processAlerts({
|
await processAlerts({
|
||||||
eventType: "resource_toggle",
|
eventType: "resource_toggle",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { processAlerts } from "../processAlerts";
|
import { processAlerts } from "../processAlerts";
|
||||||
import { db, sites, statusHistory, targetHealthCheck, Transaction } from "@server/db";
|
import { db, sites, statusHistory, targetHealthCheck, Transaction } from "@server/db";
|
||||||
|
import { invalidateStatusHistoryCache } from "@server/lib/statusHistory";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { fireHealthCheckUnhealthyAlert } from "./healthCheckEvents";
|
import { fireHealthCheckUnhealthyAlert } from "./healthCheckEvents";
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export async function fireSiteOnlineAlert(
|
|||||||
status: "online",
|
status: "online",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("site", siteId);
|
||||||
|
|
||||||
await processAlerts({
|
await processAlerts({
|
||||||
eventType: "site_online",
|
eventType: "site_online",
|
||||||
@@ -102,6 +104,7 @@ export async function fireSiteOfflineAlert(
|
|||||||
status: "offline",
|
status: "offline",
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
await invalidateStatusHistoryCache("site", siteId);
|
||||||
|
|
||||||
const unhealthyHealthChecks = await trx
|
const unhealthyHealthChecks = await trx
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
|
|||||||
@@ -13,15 +13,13 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, statusHistory } from "@server/db";
|
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
computeBuckets,
|
getCachedStatusHistory,
|
||||||
statusHistoryQuerySchema,
|
statusHistoryQuerySchema,
|
||||||
StatusHistoryResponse
|
StatusHistoryResponse
|
||||||
} from "@server/lib/statusHistory";
|
} from "@server/lib/statusHistory";
|
||||||
@@ -59,39 +57,10 @@ export async function getHealthCheckStatusHistory(
|
|||||||
const entityId = parsedParams.data.healthCheckId;
|
const entityId = parsedParams.data.healthCheckId;
|
||||||
const { days } = parsedQuery.data;
|
const { days } = parsedQuery.data;
|
||||||
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const data = await getCachedStatusHistory(entityType, entityId, days);
|
||||||
const startSec = nowSec - days * 86400;
|
|
||||||
|
|
||||||
const events = await db
|
|
||||||
.select()
|
|
||||||
.from(statusHistory)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(statusHistory.entityType, entityType),
|
|
||||||
eq(statusHistory.entityId, entityId),
|
|
||||||
gte(statusHistory.timestamp, startSec)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(asc(statusHistory.timestamp));
|
|
||||||
|
|
||||||
const { buckets, totalDowntime } = computeBuckets(events, days);
|
|
||||||
const totalWindow = days * 86400;
|
|
||||||
const overallUptime =
|
|
||||||
totalWindow > 0
|
|
||||||
? Math.max(
|
|
||||||
0,
|
|
||||||
((totalWindow - totalDowntime) / totalWindow) * 100
|
|
||||||
)
|
|
||||||
: 100;
|
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data: {
|
data,
|
||||||
entityType,
|
|
||||||
entityId,
|
|
||||||
days: buckets,
|
|
||||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
|
||||||
totalDowntimeSeconds: totalDowntime
|
|
||||||
},
|
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Status history retrieved successfully",
|
message: "Status history retrieved successfully",
|
||||||
@@ -103,4 +72,4 @@ export async function getHealthCheckStatusHistory(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, statusHistory } from "@server/db";
|
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
computeBuckets,
|
getCachedStatusHistory,
|
||||||
statusHistoryQuerySchema,
|
statusHistoryQuerySchema,
|
||||||
StatusHistoryResponse
|
StatusHistoryResponse
|
||||||
} from "@server/lib/statusHistory";
|
} from "@server/lib/statusHistory";
|
||||||
@@ -46,39 +44,10 @@ export async function getResourceStatusHistory(
|
|||||||
const entityId = parsedParams.data.resourceId;
|
const entityId = parsedParams.data.resourceId;
|
||||||
const { days } = parsedQuery.data;
|
const { days } = parsedQuery.data;
|
||||||
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const data = await getCachedStatusHistory(entityType, entityId, days);
|
||||||
const startSec = nowSec - days * 86400;
|
|
||||||
|
|
||||||
const events = await db
|
|
||||||
.select()
|
|
||||||
.from(statusHistory)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(statusHistory.entityType, entityType),
|
|
||||||
eq(statusHistory.entityId, entityId),
|
|
||||||
gte(statusHistory.timestamp, startSec)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(asc(statusHistory.timestamp));
|
|
||||||
|
|
||||||
const { buckets, totalDowntime } = computeBuckets(events, days);
|
|
||||||
const totalWindow = days * 86400;
|
|
||||||
const overallUptime =
|
|
||||||
totalWindow > 0
|
|
||||||
? Math.max(
|
|
||||||
0,
|
|
||||||
((totalWindow - totalDowntime) / totalWindow) * 100
|
|
||||||
)
|
|
||||||
: 100;
|
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data: {
|
data,
|
||||||
entityType,
|
|
||||||
entityId,
|
|
||||||
days: buckets,
|
|
||||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
|
||||||
totalDowntimeSeconds: totalDowntime
|
|
||||||
},
|
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Status history retrieved successfully",
|
message: "Status history retrieved successfully",
|
||||||
@@ -90,4 +59,4 @@ export async function getResourceStatusHistory(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, statusHistory } from "@server/db";
|
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
computeBuckets,
|
getCachedStatusHistory,
|
||||||
statusHistoryQuerySchema,
|
statusHistoryQuerySchema,
|
||||||
StatusHistoryResponse
|
StatusHistoryResponse
|
||||||
} from "@server/lib/statusHistory";
|
} from "@server/lib/statusHistory";
|
||||||
@@ -46,39 +44,10 @@ export async function getSiteStatusHistory(
|
|||||||
const entityId = parsedParams.data.siteId;
|
const entityId = parsedParams.data.siteId;
|
||||||
const { days } = parsedQuery.data;
|
const { days } = parsedQuery.data;
|
||||||
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const data = await getCachedStatusHistory(entityType, entityId, days);
|
||||||
const startSec = nowSec - days * 86400;
|
|
||||||
|
|
||||||
const events = await db
|
|
||||||
.select()
|
|
||||||
.from(statusHistory)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(statusHistory.entityType, entityType),
|
|
||||||
eq(statusHistory.entityId, entityId),
|
|
||||||
gte(statusHistory.timestamp, startSec)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(asc(statusHistory.timestamp));
|
|
||||||
|
|
||||||
const { buckets, totalDowntime } = computeBuckets(events, days);
|
|
||||||
const totalWindow = days * 86400;
|
|
||||||
const overallUptime =
|
|
||||||
totalWindow > 0
|
|
||||||
? Math.max(
|
|
||||||
0,
|
|
||||||
((totalWindow - totalDowntime) / totalWindow) * 100
|
|
||||||
)
|
|
||||||
: 100;
|
|
||||||
|
|
||||||
return response<StatusHistoryResponse>(res, {
|
return response<StatusHistoryResponse>(res, {
|
||||||
data: {
|
data,
|
||||||
entityType,
|
|
||||||
entityId,
|
|
||||||
days: buckets,
|
|
||||||
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
|
||||||
totalDowntimeSeconds: totalDowntime
|
|
||||||
},
|
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Status history retrieved successfully",
|
message: "Status history retrieved successfully",
|
||||||
@@ -90,4 +59,4 @@ export async function getSiteStatusHistory(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ export async function createTarget(
|
|||||||
healthCheck[0].orgId,
|
healthCheck[0].orgId,
|
||||||
healthCheck[0].targetHealthCheckId,
|
healthCheck[0].targetHealthCheckId,
|
||||||
healthCheck[0].name,
|
healthCheck[0].name,
|
||||||
undefined,
|
healthCheck[0].targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
@@ -278,7 +278,7 @@ export async function createTarget(
|
|||||||
healthCheck[0].orgId,
|
healthCheck[0].orgId,
|
||||||
healthCheck[0].targetHealthCheckId,
|
healthCheck[0].targetHealthCheckId,
|
||||||
healthCheck[0].name,
|
healthCheck[0].name,
|
||||||
undefined,
|
healthCheck[0].targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
@@ -288,7 +288,7 @@ export async function createTarget(
|
|||||||
healthCheck[0].orgId,
|
healthCheck[0].orgId,
|
||||||
healthCheck[0].targetHealthCheckId,
|
healthCheck[0].targetHealthCheckId,
|
||||||
healthCheck[0].name,
|
healthCheck[0].name,
|
||||||
undefined,
|
healthCheck[0].targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
|
|||||||
@@ -228,12 +228,7 @@ export async function updateTarget(
|
|||||||
hcHealthValue = undefined;
|
hcHealthValue = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDisablingHc =
|
[updatedHc] = await trx
|
||||||
(parsedBody.data.hcEnabled === false ||
|
|
||||||
parsedBody.data.hcEnabled === null) &&
|
|
||||||
existingHc.hcEnabled === true;
|
|
||||||
|
|
||||||
const [updatedHc] = await trx
|
|
||||||
.update(targetHealthCheck)
|
.update(targetHealthCheck)
|
||||||
.set({
|
.set({
|
||||||
siteId: parsedBody.data.siteId,
|
siteId: parsedBody.data.siteId,
|
||||||
@@ -259,32 +254,41 @@ export async function updateTarget(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (updatedHc.hcHealth === "unhealthy" && existingHc.hcHealth !== "unhealthy") {
|
if (updatedHc.hcHealth === "unhealthy" && existingHc.hcHealth !== "unhealthy") {
|
||||||
|
logger.debug(
|
||||||
|
`Health check ${updatedHc.targetHealthCheckId} for target ${targetId} is now unhealthy, firing alert`
|
||||||
|
);
|
||||||
await fireHealthCheckUnhealthyAlert(
|
await fireHealthCheckUnhealthyAlert(
|
||||||
updatedHc.orgId,
|
updatedHc.orgId,
|
||||||
updatedHc.targetHealthCheckId,
|
updatedHc.targetHealthCheckId,
|
||||||
updatedHc.name || "",
|
updatedHc.name || "",
|
||||||
undefined,
|
updatedHc.targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
} else if (updatedHc.hcHealth === "unknown" && existingHc.hcHealth !== "unknown") {
|
} else if (updatedHc.hcHealth === "unknown" && existingHc.hcHealth !== "unknown") {
|
||||||
|
logger.debug(
|
||||||
|
`Health check ${updatedHc.targetHealthCheckId} for target ${targetId} is now unknown, firing alert`
|
||||||
|
);
|
||||||
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
// if the health is unknown, we want to fire an alert to notify users to enable health checks
|
||||||
await fireHealthCheckUnknownAlert(
|
await fireHealthCheckUnknownAlert(
|
||||||
updatedHc.orgId,
|
updatedHc.orgId,
|
||||||
updatedHc.targetHealthCheckId,
|
updatedHc.targetHealthCheckId,
|
||||||
updatedHc.name,
|
updatedHc.name,
|
||||||
undefined,
|
updatedHc.targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
} else if (updatedHc.hcHealth === "healthy" && existingHc.hcHealth !== "healthy") {
|
} else if (updatedHc.hcHealth === "healthy" && existingHc.hcHealth !== "healthy") {
|
||||||
|
logger.debug(
|
||||||
|
`Health check ${updatedHc.targetHealthCheckId} for target ${targetId} is now healthy, firing alert`
|
||||||
|
);
|
||||||
await fireHealthCheckHealthyAlert(
|
await fireHealthCheckHealthyAlert(
|
||||||
updatedHc.orgId,
|
updatedHc.orgId,
|
||||||
updatedHc.targetHealthCheckId,
|
updatedHc.targetHealthCheckId,
|
||||||
updatedHc.name,
|
updatedHc.name,
|
||||||
undefined,
|
updatedHc.targetId,
|
||||||
undefined,
|
undefined,
|
||||||
false, // dont send the alert because we just want to create the alert, not notify users yet
|
false, // dont send the alert because we just want to create the alert, not notify users yet
|
||||||
trx
|
trx
|
||||||
|
|||||||
Reference in New Issue
Block a user