Compare commits

..
Author SHA1 Message Date
gorkem-bwl 0b09895098 docs: align dev setup docs with current server config
## Changes
- Replace stale QUEUE_TYPE env var (superSimpleQueue/lessSimpleQueue) with QUEUE_MODE (primary/worker) to match server/src/config/envValidation.ts
- Mark ORIGIN, TOKEN_TTL and LOG_LEVEL as optional with their defaults; note that only CLIENT_HOST, JWT_SECRET and DB_CONNECTION_STRING are strictly required
- Note that CLIENT_HOST must be a full URL (enforced by env validation)
- Remove Redis port 6379 from the port-conflict troubleshooting list; the server has no Redis dependency
- Point CLAUDE.md at CONTRIBUTING.md as the setup source of truth

## Why
A contributor followed the setup docs and hit references to files and env vars that no longer exist after the repo reorg. This brings the contributor docs in line with the actual dev workflow.
2026-07-21 19:44:22 +03:00
132 changed files with 1503 additions and 985 deletions

No files matched your search

+1
View File
@@ -51,6 +51,7 @@ TOKEN_TTL="99d"
ORIGIN="localhost" ORIGIN="localhost"
LOG_LEVEL="debug" LOG_LEVEL="debug"
``` ```
Only `CLIENT_HOST` (must be a full URL), `JWT_SECRET`, and `DB_CONNECTION_STRING` are strictly required; the rest have defaults (see `server/src/config/envValidation.ts`). Setup source of truth: CONTRIBUTING.md.
### Client `.env` ### Client `.env`
```env ```env
+7 -5
View File
@@ -124,10 +124,12 @@ LOG_LEVEL="debug"
- `CLIENT_HOST`: Frontend URL (default: http://localhost:5173) - `CLIENT_HOST`: Frontend URL (default: http://localhost:5173)
- `JWT_SECRET`: Secret key for JWT tokens (change to something secure) - `JWT_SECRET`: Secret key for JWT tokens (change to something secure)
- `DB_CONNECTION_STRING`: MongoDB connection URL - `DB_CONNECTION_STRING`: MongoDB connection URL
- `ORIGIN`: Origin for CORS purposes - `ORIGIN`: Origin for CORS purposes (optional)
- `TOKEN_TTL`: Token time to live (in vercel/ms format) - `TOKEN_TTL`: Token time to live in vercel/ms format (optional, default: `99d`)
- `LOG_LEVEL`: Debug level (debug, info, warn, error) - `LOG_LEVEL`: Debug level: debug, info, warn, error (optional, default: `debug`)
- `QUEUE_TYPE`: Job queue implementation (`superSimpleQueue` or `lessSimpleQueue`, optional, default: `superSimpleQueue`) - `QUEUE_MODE`: Queue mode, `primary` or `worker` (optional, default: `primary`)
Only `CLIENT_HOST`, `JWT_SECRET`, and `DB_CONNECTION_STRING` are strictly required. Note that `CLIENT_HOST` must be a full URL.
Start the backend server: Start the backend server:
@@ -203,7 +205,7 @@ docker rm uptime_database_mongo
**Port already in use:** **Port already in use:**
- Check if another service is using ports 5173, 52345, 27017, or 6379 - Check if another service is using ports 5173, 52345, or 27017
- Stop the conflicting service or change the port in `.env` files - Stop the conflicting service or change the port in `.env` files
**MongoDB connection issues:** **MongoDB connection issues:**
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "client", "name": "client",
"version": "3.10.0", "version": "3.9.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "client", "name": "client",
"version": "3.10.0", "version": "3.9.2",
"dependencies": { "dependencies": {
"@csstools/css-tokenizer": "^3.0.4", "@csstools/css-tokenizer": "^3.0.4",
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "3.10.0", "version": "3.9.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -40,7 +40,6 @@ export const BaseChart = ({
<Typography <Typography
variant="eyebrow" variant="eyebrow"
color="text.secondary" color="text.secondary"
sx={{ overflowWrap: "anywhere" }}
> >
{title} {title}
</Typography> </Typography>
@@ -25,11 +25,10 @@ export const InfraDetailsGauges = ({
<Stack <Stack
direction={isSmall ? "column" : "row"} direction={isSmall ? "column" : "row"}
spacing={theme.spacing(8)} spacing={theme.spacing(8)}
alignItems={"stretch"} alignItems={"center"}
> >
<DetailGauge <DetailGauge
title={t("pages.infrastructure.gauges.memory.title")} title={t("pages.infrastructure.gauges.memory.title")}
maxWidth={260}
progress={(snapshot?.memory?.usage_percent || 0) * 100} progress={(snapshot?.memory?.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")} upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")}
upperValue={prettyBytes(snapshot?.memory?.used_bytes || 0)} upperValue={prettyBytes(snapshot?.memory?.used_bytes || 0)}
@@ -38,7 +37,6 @@ export const InfraDetailsGauges = ({
/> />
<DetailGauge <DetailGauge
title={t("pages.infrastructure.gauges.cpu.title")} title={t("pages.infrastructure.gauges.cpu.title")}
maxWidth={260}
progress={(snapshot?.cpu?.usage_percent || 0) * 100} progress={(snapshot?.cpu?.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.cpu.upperLabel")} upperLabel={t("pages.infrastructure.gauges.cpu.upperLabel")}
upperValue={getFrequency(snapshot?.cpu?.current_frequency || 0)} upperValue={getFrequency(snapshot?.cpu?.current_frequency || 0)}
@@ -51,7 +49,6 @@ export const InfraDetailsGauges = ({
key={disk?.device || 0 + idx} key={disk?.device || 0 + idx}
// title={`Disk ${idx} usage`} // title={`Disk ${idx} usage`}
title={t("pages.infrastructure.gauges.disk.title", { idx })} title={t("pages.infrastructure.gauges.disk.title", { idx })}
maxWidth={260}
progress={(disk.usage_percent || 0) * 100} progress={(disk.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.disk.upperLabel")} upperLabel={t("pages.infrastructure.gauges.disk.upperLabel")}
upperValue={prettyBytes(disk?.used_bytes || 0)} upperValue={prettyBytes(disk?.used_bytes || 0)}
+4 -4
View File
@@ -825,7 +825,7 @@
"netBytesRecv": "{{name}} - Empfangene Bytes", "netBytesRecv": "{{name}} - Empfangene Bytes",
"netBytesSent": "{{name}} - Gesendete Bytes", "netBytesSent": "{{name}} - Gesendete Bytes",
"netNoTraffic": "Capture meldet null Datenverkehr auf allen Schnittstellen. Das ist normal, wenn Capture in einem Container läuft, der nur Loopback-Schnittstellen sieht — führe Capture direkt auf dem Host aus, um echten Netzwerkverkehr zu sehen.", "netNoTraffic": "Capture meldet null Datenverkehr auf allen Schnittstellen. Das ist normal, wenn Capture in einem Container läuft, der nur Loopback-Schnittstellen sieht — führe Capture direkt auf dem Host aus, um echten Netzwerkverkehr zu sehen.",
"temp": "CPU-Temperatur" "temp": "Temp."
} }
}, },
"gauges": { "gauges": {
@@ -841,13 +841,13 @@
}, },
"memory": { "memory": {
"lowerLabel": "Frei", "lowerLabel": "Frei",
"title": "Speicherauslastung", "title": "Arbeitsspeicherauslastung",
"upperLabel": "Belegt" "upperLabel": "Belegt"
} }
}, },
"statBoxes": { "statBoxes": {
"avgCpuTemperature": "Durchschn. CPU-Temperatur", "avgCpuTemperature": "Durchschn. CPU Termperatur",
"cpuFrequency": "CPU-Frequenz", "cpuFrequency": "CPU Frequenz",
"cpuLogical": "CPU (logisch)", "cpuLogical": "CPU (logisch)",
"cpuPhysical": "CPU (physisch)", "cpuPhysical": "CPU (physisch)",
"disk": "Festplatte", "disk": "Festplatte",
+1 -1
View File
@@ -845,7 +845,7 @@
"netBytesRecv": "{{name}} - Bytes Received", "netBytesRecv": "{{name}} - Bytes Received",
"netBytesSent": "{{name}} - Bytes Sent", "netBytesSent": "{{name}} - Bytes Sent",
"loopbackOnlyNotice": "Capture is only reporting loopback or container-bridge interfaces (lo, lo0, docker0). This is normal when Capture runs inside a container that can only see its own network namespace — run Capture directly on the host to see real network activity.", "loopbackOnlyNotice": "Capture is only reporting loopback or container-bridge interfaces (lo, lo0, docker0). This is normal when Capture runs inside a container that can only see its own network namespace — run Capture directly on the host to see real network activity.",
"temp": "CPU temperature" "temp": "Temp"
} }
}, },
"gauges": { "gauges": {
-1
View File
@@ -15,7 +15,6 @@
"worker:dev": "QUEUE_MODE=worker nodemon --exec tsx src/index.js", "worker:dev": "QUEUE_MODE=worker nodemon --exec tsx src/index.js",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates", "build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates",
"typeCheck": "tsc --noEmit",
"build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts", "build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts",
"lint": "eslint .", "lint": "eslint .",
"lint-fix": "eslint --fix .", "lint-fix": "eslint --fix .",
@@ -22,6 +22,8 @@ import {
} from "@/api/validation/userValidation.js"; } from "@/api/validation/userValidation.js";
import { IUserService } from "@/domain/users/user.service.js"; import { IUserService } from "@/domain/users/user.service.js";
const SERVICE_NAME = "authController";
export interface IAuthController { export interface IAuthController {
registerUser: RequestHandler; registerUser: RequestHandler;
createUser: RequestHandler; createUser: RequestHandler;
@@ -40,12 +42,18 @@ export interface IAuthController {
} }
class AuthController implements IAuthController { class AuthController implements IAuthController {
static SERVICE_NAME = SERVICE_NAME;
private userService: IUserService; private userService: IUserService;
constructor(userService: IUserService) { constructor(userService: IUserService) {
this.userService = userService; this.userService = userService;
} }
get serviceName() {
return AuthController.SERVICE_NAME;
}
registerUser = catchAsync(async (req: Request, res: Response) => { registerUser = catchAsync(async (req: Request, res: Response) => {
const newUser = req.body.user; const newUser = req.body.user;
const newUserToken = req.body.token; const newUserToken = req.body.token;
@@ -11,7 +11,10 @@ import {
import { ICheckService } from "@/domain/checks/check.service.js"; import { ICheckService } from "@/domain/checks/check.service.js";
import { requireTeamId } from "@/api/controllers/controllerUtils.js"; import { requireTeamId } from "@/api/controllers/controllerUtils.js";
const SERVICE_NAME = "checkController";
export interface ICheckController { export interface ICheckController {
serviceName: string;
getChecksByMonitor: RequestHandler; getChecksByMonitor: RequestHandler;
getChecksByTeam: RequestHandler; getChecksByTeam: RequestHandler;
getChecksSummaryByTeamId: RequestHandler; getChecksSummaryByTeamId: RequestHandler;
@@ -20,11 +23,17 @@ export interface ICheckController {
} }
class CheckController implements ICheckController { class CheckController implements ICheckController {
static SERVICE_NAME = SERVICE_NAME;
private checkService: ICheckService; private checkService: ICheckService;
constructor(checkService: ICheckService) { constructor(checkService: ICheckService) {
this.checkService = checkService; this.checkService = checkService;
} }
get serviceName() {
return CheckController.SERVICE_NAME;
}
getChecksByMonitor = catchAsync(async (req: Request, res: Response) => { getChecksByMonitor = catchAsync(async (req: Request, res: Response) => {
const validatedParams = getChecksParamValidation.parse(req.params); const validatedParams = getChecksParamValidation.parse(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); const validatedQuery = getChecksQueryValidation.parse(req.query);
+19 -1
View File
@@ -1,5 +1,5 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Monitor, MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js";
import { UserRole } from "@/domain/users/user.type.js"; import { UserRole } from "@/domain/users/user.type.js";
import sslChecker, { SSLDetails } from "ssl-checker"; import sslChecker, { SSLDetails } from "ssl-checker";
type SSLCheckerType = typeof sslChecker; type SSLCheckerType = typeof sslChecker;
@@ -54,6 +54,24 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
}; };
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
if (value === undefined) {
return undefined;
}
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
}
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
};
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
const parseSingle = (input: unknown): MonitorType => { const parseSingle = (input: unknown): MonitorType => {
if (typeof input !== "string") { if (typeof input !== "string") {
@@ -2,17 +2,25 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js";
import { Request, Response, RequestHandler } from "express"; import { Request, Response, RequestHandler } from "express";
import { catchAsync } from "@/utils/catchAsync.js"; import { catchAsync } from "@/utils/catchAsync.js";
const SERVICE_NAME = "diagnosticController";
export interface IDiagnosticController { export interface IDiagnosticController {
getSystemStats: RequestHandler; getSystemStats: RequestHandler;
} }
class DiagnosticController implements IDiagnosticController { class DiagnosticController implements IDiagnosticController {
static SERVICE_NAME = SERVICE_NAME;
private diagnosticService: IDiagnosticService; private diagnosticService: IDiagnosticService;
constructor(diagnosticService: IDiagnosticService) { constructor(diagnosticService: IDiagnosticService) {
this.diagnosticService = diagnosticService; this.diagnosticService = diagnosticService;
} }
get serviceName() {
return DiagnosticController.SERVICE_NAME;
}
getSystemStats = catchAsync(async (req: Request, res: Response) => { getSystemStats = catchAsync(async (req: Request, res: Response) => {
const diagnostics = await this.diagnosticService.getSystemStats(); const diagnostics = await this.diagnosticService.getSystemStats();
return res.status(200).json({ return res.status(200).json({
@@ -4,15 +4,23 @@ import { getChecksParamValidation, getChecksQueryValidation } from "@/api/valida
import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js";
import { requireTeamId } from "./controllerUtils.js"; import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "geoCheckController";
export interface IGeoCheckController { export interface IGeoCheckController {
getGeoChecksByMonitor: RequestHandler; getGeoChecksByMonitor: RequestHandler;
} }
class GeoCheckController implements IGeoCheckController { class GeoCheckController implements IGeoCheckController {
static SERVICE_NAME = SERVICE_NAME;
private geoChecksService: IGeoChecksService; private geoChecksService: IGeoChecksService;
constructor(geoChecksService: IGeoChecksService) { constructor(geoChecksService: IGeoChecksService) {
this.geoChecksService = geoChecksService; this.geoChecksService = geoChecksService;
} }
get serviceName() {
return GeoCheckController.SERVICE_NAME;
}
getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => { getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => {
const validatedParams = getChecksParamValidation.parse(req.params); const validatedParams = getChecksParamValidation.parse(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); const validatedQuery = getChecksQueryValidation.parse(req.query);
@@ -3,6 +3,7 @@ import { catchAsync } from "@/utils/catchAsync.js";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js";
import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js"; import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js";
import { IInviteService } from "@/domain/invites/invite.service.js"; import { IInviteService } from "@/domain/invites/invite.service.js";
const SERVICE_NAME = "inviteController";
export interface IInviteController { export interface IInviteController {
getInviteToken: RequestHandler; getInviteToken: RequestHandler;
@@ -11,11 +12,16 @@ export interface IInviteController {
} }
class InviteController implements IInviteController { class InviteController implements IInviteController {
static SERVICE_NAME = SERVICE_NAME;
private inviteService: IInviteService; private inviteService: IInviteService;
constructor(inviteService: IInviteService) { constructor(inviteService: IInviteService) {
this.inviteService = inviteService; this.inviteService = inviteService;
} }
get serviceName() {
return InviteController.SERVICE_NAME;
}
getInviteToken = catchAsync(async (req: Request, res: Response) => { getInviteToken = catchAsync(async (req: Request, res: Response) => {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const userRoles = requireUserRoles(req.user?.role); const userRoles = requireUserRoles(req.user?.role);
@@ -2,15 +2,21 @@ import { ILogger } from "@/utils/logger.js";
import { Request, Response, RequestHandler } from "express"; import { Request, Response, RequestHandler } from "express";
import { catchAsync } from "@/utils/catchAsync.js"; import { catchAsync } from "@/utils/catchAsync.js";
const SERVICE_NAME = "LogController";
export interface ILogController { export interface ILogController {
getLogs: RequestHandler; getLogs: RequestHandler;
} }
class LogController { class LogController {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: ILogger;
constructor(logger: ILogger) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
get serviceName() {
return LogController.SERVICE_NAME;
}
getLogs = catchAsync(async (req: Request, res: Response) => { getLogs = catchAsync(async (req: Request, res: Response) => {
const logs = this.logger.getLogs(); const logs = this.logger.getLogs();
@@ -23,6 +23,8 @@ import { AppError } from "@/utils/AppError.js";
import { IMonitorService } from "@/domain/monitors/monitor.service.js"; import { IMonitorService } from "@/domain/monitors/monitor.service.js";
import { INotificationsService } from "@/domain/notifications/notification.service.js"; import { INotificationsService } from "@/domain/notifications/notification.service.js";
const SERVICE_NAME = "monitorController";
export interface IMonitorController { export interface IMonitorController {
getMonitorCertificate: RequestHandler; getMonitorCertificate: RequestHandler;
getUptimeDetailsById: RequestHandler; getUptimeDetailsById: RequestHandler;
@@ -45,6 +47,8 @@ export interface IMonitorController {
updateNotifications: RequestHandler; updateNotifications: RequestHandler;
} }
class MonitorController implements IMonitorController { class MonitorController implements IMonitorController {
static SERVICE_NAME = SERVICE_NAME;
private monitorService: IMonitorService; private monitorService: IMonitorService;
private notificationsService: INotificationsService; private notificationsService: INotificationsService;
@@ -53,6 +57,10 @@ class MonitorController implements IMonitorController {
this.notificationsService = notificationsService; this.notificationsService = notificationsService;
} }
get serviceName() {
return MonitorController.SERVICE_NAME;
}
getMonitorCertificate = catchAsync(async (req: Request, res: Response) => { getMonitorCertificate = catchAsync(async (req: Request, res: Response) => {
const validatedParams = getCertificateParamValidation.parse(req.params); const validatedParams = getCertificateParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
@@ -3,6 +3,8 @@ import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js
import { Request, Response, RequestHandler } from "express"; import { Request, Response, RequestHandler } from "express";
import { catchAsync } from "@/utils/catchAsync.js"; import { catchAsync } from "@/utils/catchAsync.js";
const SERVICE_NAME = "JobQueueController";
export interface IJobQueueController { export interface IJobQueueController {
getMetrics: RequestHandler; getMetrics: RequestHandler;
getJobs: RequestHandler; getJobs: RequestHandler;
@@ -11,8 +13,13 @@ export interface IJobQueueController {
} }
class JobQueueController implements IJobQueueController { class JobQueueController implements IJobQueueController {
static SERVICE_NAME = SERVICE_NAME;
constructor(private scheduler: IJobScheduler) {} constructor(private scheduler: IJobScheduler) {}
get serviceName() {
return JobQueueController.SERVICE_NAME;
}
getMetrics = catchAsync(async (req: Request, res: Response) => { getMetrics = catchAsync(async (req: Request, res: Response) => {
const metrics = await this.scheduler.getMetrics(); const metrics = await this.scheduler.getMetrics();
res.status(200).json({ res.status(200).json({
@@ -7,13 +7,17 @@ import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"
import { IEmailService } from "@/service/emailService.js"; import { IEmailService } from "@/service/emailService.js";
import { Settings } from "@/domain/app-settings/app-settings.type.js"; import { Settings } from "@/domain/app-settings/app-settings.type.js";
const SERVICE_NAME = "SettingsController";
export interface ISettingsController { export interface ISettingsController {
serviceName: string;
getAppSettings: RequestHandler; getAppSettings: RequestHandler;
updateAppSettings: RequestHandler; updateAppSettings: RequestHandler;
sendTestEmail: RequestHandler; sendTestEmail: RequestHandler;
} }
class SettingsController implements ISettingsController { class SettingsController implements ISettingsController {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: ISettingsService; private settingsService: ISettingsService;
private emailService: IEmailService; private emailService: IEmailService;
constructor(settingsService: ISettingsService, emailService: IEmailService) { constructor(settingsService: ISettingsService, emailService: IEmailService) {
@@ -21,6 +25,10 @@ class SettingsController implements ISettingsController {
this.emailService = emailService; this.emailService = emailService;
} }
get serviceName() {
return SettingsController.SERVICE_NAME;
}
buildAppSettings = (dbSettings: Settings) => { buildAppSettings = (dbSettings: Settings) => {
const sanitizedSettings: Record<string, unknown> = { ...dbSettings }; const sanitizedSettings: Record<string, unknown> = { ...dbSettings };
delete sanitizedSettings.version; delete sanitizedSettings.version;
@@ -13,7 +13,10 @@ import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.
import { IStatusPageService } from "@/domain/status-pages/status-page.service.js"; import { IStatusPageService } from "@/domain/status-pages/status-page.service.js";
import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js"; import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js";
const SERVICE_NAME = "statusPageController";
export interface IStatusPageController { export interface IStatusPageController {
readonly serviceName: string;
createStatusPage: RequestHandler; createStatusPage: RequestHandler;
updateStatusPage: RequestHandler; updateStatusPage: RequestHandler;
getStatusPageByUrl: RequestHandler; getStatusPageByUrl: RequestHandler;
@@ -23,11 +26,16 @@ export interface IStatusPageController {
} }
class StatusPageController implements IStatusPageController { class StatusPageController implements IStatusPageController {
static SERVICE_NAME = SERVICE_NAME;
private statusPageService: IStatusPageService; private statusPageService: IStatusPageService;
constructor(statusPageService: IStatusPageService) { constructor(statusPageService: IStatusPageService) {
this.statusPageService = statusPageService; this.statusPageService = statusPageService;
} }
get serviceName() {
return StatusPageController.SERVICE_NAME;
}
createStatusPage = catchAsync(async (req: Request, res: Response) => { createStatusPage = catchAsync(async (req: Request, res: Response) => {
createStatusPageBodyValidation.parse(req.body); createStatusPageBodyValidation.parse(req.body);
if (req.file) { if (req.file) {
+8 -9
View File
@@ -1,8 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js"; import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
import { MonitorTypes } from "@/domain/monitors/monitor.type.js"; import { MonitorTypes } from "@/domain/monitors/monitor.types.js";
import { CheckFilters, DateRanges, SortOrders } from "@/types/query.js";
//**************************************** //****************************************
// Check Validations // Check Validations
@@ -14,10 +13,10 @@ export const getChecksParamValidation = z.object({
export const getChecksQueryValidation = z.object({ export const getChecksQueryValidation = z.object({
type: z.enum(MonitorTypes).optional(), type: z.enum(MonitorTypes).optional(),
sortOrder: z.enum(SortOrders), sortOrder: z.enum(["asc", "desc"]),
limit: z.coerce.number().optional(), limit: z.coerce.number().optional(),
dateRange: z.enum(DateRanges), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
filter: z.enum(CheckFilters).optional(), filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: booleanCoercion.optional(), ack: booleanCoercion.optional(),
page: z.coerce.number(), page: z.coerce.number(),
rowsPerPage: z.coerce.number(), rowsPerPage: z.coerce.number(),
@@ -26,10 +25,10 @@ export const getChecksQueryValidation = z.object({
}); });
export const getTeamChecksQueryValidation = z.object({ export const getTeamChecksQueryValidation = z.object({
sortOrder: z.enum(SortOrders), sortOrder: z.enum(["asc", "desc"]),
limit: z.coerce.number().optional(), limit: z.coerce.number().optional(),
dateRange: z.enum(DateRanges), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
filter: z.enum(CheckFilters).optional(), filter: z.enum(["all", "up", "down", "resolve"]).optional(),
ack: booleanCoercion.optional(), ack: booleanCoercion.optional(),
page: z.coerce.number(), page: z.coerce.number(),
rowsPerPage: z.coerce.number(), rowsPerPage: z.coerce.number(),
@@ -40,5 +39,5 @@ export const deleteChecksParamValidation = z.object({
}); });
export const getChecksSummaryByTeamIdQueryValidation = z.object({ export const getChecksSummaryByTeamIdQueryValidation = z.object({
dateRange: z.enum(DateRanges).optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
}); });
@@ -1,20 +1,18 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { DateRanges, SortOrders } from "@/types/query.js";
import { IncidentResolutionTypes } from "@/domain/incidents/incident.type.js";
//**************************************** //****************************************
// Incident Validations // Incident Validations
//**************************************** //****************************************
export const getIncidentsByTeamQueryValidation = z.object({ export const getIncidentsByTeamQueryValidation = z.object({
sortOrder: z.enum(SortOrders), sortOrder: z.enum(["asc", "desc"]),
dateRange: z.enum(DateRanges).default("all"), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).default("all"),
page: z.coerce.number().int().min(0), page: z.coerce.number().int().min(0),
rowsPerPage: z.coerce.number().int().min(1), rowsPerPage: z.coerce.number().int().min(1),
status: booleanCoercion.optional(), status: booleanCoercion.optional(),
monitorId: z.string().optional(), monitorId: z.string().optional(),
resolutionType: z.enum(IncidentResolutionTypes).optional(), resolutionType: z.enum(["manual", "automatic"]).optional(),
}); });
export const getIncidentSummaryQueryValidation = z.object({ export const getIncidentSummaryQueryValidation = z.object({
@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { booleanCoercion } from "./shared.js"; import { booleanCoercion } from "./shared.js";
import { DurationUnits } from "@/domain/maintenance-windows/maintenance-window.type.js"; import { DurationUnits } from "@/domain/maintenance-windows/maintenance-window.type.js";
import { SortOrders } from "@/types/query.js";
const dateToString = z.coerce.date().transform((d) => d.toISOString()); const dateToString = z.coerce.date().transform((d) => d.toISOString());
@@ -50,7 +49,7 @@ export const getMaintenanceWindowsByTeamIdQueryValidation = z.object({
page: z.coerce.number().optional(), page: z.coerce.number().optional(),
rowsPerPage: z.coerce.number().optional(), rowsPerPage: z.coerce.number().optional(),
field: z.string().optional(), field: z.string().optional(),
order: z.enum(SortOrders).optional(), order: z.enum(["asc", "desc"]).optional(),
}); });
export const getMaintenanceWindowsByMonitorIdParamValidation = z.object({ export const getMaintenanceWindowsByMonitorIdParamValidation = z.object({
@@ -10,8 +10,7 @@ import {
MonitorStatuses, MonitorStatuses,
MonitorTypes, MonitorTypes,
PageSpeedStrategies, PageSpeedStrategies,
} from "@/domain/monitors/monitor.type.js"; } from "@/domain/monitors/monitor.types.js";
import { DateRanges, SortOrders } from "@/types/query.js";
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" }); const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
@@ -21,9 +20,9 @@ export const getMonitorByIdParamValidation = z.object({
export const getMonitorByIdQueryValidation = z.object({ export const getMonitorByIdQueryValidation = z.object({
status: booleanCoercion.optional(), status: booleanCoercion.optional(),
sortOrder: z.enum(SortOrders).optional(), sortOrder: z.enum(["asc", "desc"]).optional(),
limit: z.coerce.number().optional(), limit: z.coerce.number().optional(),
dateRange: z.enum(DateRanges).optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
numToDisplay: z.coerce.number().optional(), numToDisplay: z.coerce.number().optional(),
normalize: booleanCoercion.optional(), normalize: booleanCoercion.optional(),
continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(),
@@ -43,7 +42,7 @@ export const getMonitorsWithChecksQueryValidation = z.object({
rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), rowsPerPage: z.coerce.number().int().min(1).max(100).optional(),
filter: z.union([z.string(), z.literal("")]).optional(), filter: z.union([z.string(), z.literal("")]).optional(),
field: z.string().optional(), field: z.string().optional(),
order: z.enum(SortOrders).optional(), order: z.enum(["asc", "desc"]).optional(),
type: z.union([z.enum(MonitorTypes), z.array(z.enum(MonitorTypes))]).optional(), type: z.union([z.enum(MonitorTypes), z.array(z.enum(MonitorTypes))]).optional(),
tags: z.union([z.string(), z.array(z.string())]).optional(), tags: z.union([z.string(), z.array(z.string())]).optional(),
explain: booleanCoercion.optional(), explain: booleanCoercion.optional(),
@@ -196,7 +195,7 @@ export const getUptimeDetailsByIdParamValidation = z.object({
}); });
export const getUptimeDetailsByIdQueryValidation = z.object({ export const getUptimeDetailsByIdQueryValidation = z.object({
dateRange: z.enum(DateRanges), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]),
normalize: booleanCoercion.optional(), normalize: booleanCoercion.optional(),
}); });
@@ -207,7 +206,7 @@ const importedMonitorSchema = z
teamId: z.string().optional(), teamId: z.string().optional(),
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.union([z.string(), z.literal("")]).optional(), description: z.union([z.string(), z.literal("")]).optional(),
status: z.enum(MonitorStatuses).default("initializing"), status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"),
statusWindow: z.array(z.boolean()).default([]), statusWindow: z.array(z.boolean()).default([]),
statusWindowSize: z.number().min(1).max(20).default(5), statusWindowSize: z.number().min(1).max(20).default(5),
statusWindowThreshold: z.number().min(1).max(100).default(60), statusWindowThreshold: z.number().min(1).max(100).default(60),
@@ -264,7 +263,7 @@ export const getHardwareDetailsByIdParamValidation = z.object({
}); });
export const getHardwareDetailsByIdQueryValidation = z.object({ export const getHardwareDetailsByIdQueryValidation = z.object({
dateRange: z.enum(DateRanges).optional(), dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
}); });
// Canonical monitor shape returned by /monitors endpoints. Keep aligned with // Canonical monitor shape returned by /monitors endpoints. Keep aligned with
@@ -3,7 +3,6 @@ import { booleanCoercion, dnsHostnameRegex } from "./shared.js";
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js"; import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { cssReferencesExternalResource } from "@/utils/customCss.js"; import { cssReferencesExternalResource } from "@/utils/customCss.js";
import { ImageMimeTypes } from "@/types/upload.js";
//**************************************** //****************************************
// Status Page Validations // Status Page Validations
@@ -68,7 +67,7 @@ export const imageValidation = z
fieldname: z.string().min(1, "Field name is required"), fieldname: z.string().min(1, "Field name is required"),
originalname: z.string().min(1, "Original name is required"), originalname: z.string().min(1, "Original name is required"),
encoding: z.string().min(1, "Encoding is required"), encoding: z.string().min(1, "Encoding is required"),
mimetype: z.enum(ImageMimeTypes, { mimetype: z.enum(["image/jpeg", "image/png", "image/jpg"], {
message: "File must be a valid image (jpeg, jpg, or png)", message: "File must be a valid image (jpeg, jpg, or png)",
}), }),
size: z.number().max(3145728, "File size must be less than 3MB"), size: z.number().max(3145728, "File size must be less than 3MB"),
+1 -2
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { UserRoles } from "@/domain/users/user.type.js"; import { UserRoles } from "@/domain/users/user.type.js";
import { nameValidation, lowercaseEmailValidation, passwordValidation, booleanCoercion } from "@/api/validation/shared.js"; import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js";
//**************************************** //****************************************
// User Validations // User Validations
@@ -46,7 +46,6 @@ export const editUserBodyValidation = z
profilePicture: z.string().optional(), profilePicture: z.string().optional(),
password: passwordValidation.optional(), password: passwordValidation.optional(),
newPassword: passwordValidation.optional(), newPassword: passwordValidation.optional(),
deleteProfileImage: booleanCoercion.optional(),
}) })
// Changing a password requires both the current password and the new one. // Changing a password requires both the current password and the new one.
// The service only runs the change-password flow when both are present, so // The service only runs the change-password flow when both are present, so
+15 -20
View File
@@ -21,7 +21,6 @@ import { GlobalPingService } from "@/service/globalPingService.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
// Notification providers // Notification providers
import type { NotificationProviderRegistry } from "@/domain/notifications/notification.service.js";
import { WebhookProvider } from "@/domain/notifications/providers/webhook.js"; import { WebhookProvider } from "@/domain/notifications/providers/webhook.js";
import { SlackProvider } from "@/domain/notifications/providers/slack.js"; import { SlackProvider } from "@/domain/notifications/providers/slack.js";
import { EmailProvider } from "@/domain/notifications/providers/email.js"; import { EmailProvider } from "@/domain/notifications/providers/email.js";
@@ -126,7 +125,7 @@ export const buildShared = async ({
const checksRepository = new MongoChecksRepository(logger); const checksRepository = new MongoChecksRepository(logger);
const incidentsRepository = new MongoIncidentsRepository(); const incidentsRepository = new MongoIncidentsRepository();
const usersRepository = new MongoUsersRepository(); const usersRepository = new MongoUsersRepository();
const geoChecksRepository = new MongoGeoChecksRepository(); const geoChecksRepository = new MongoGeoChecksRepository(logger);
const notificationsRepository = new MongoNotificationsRepository(); const notificationsRepository = new MongoNotificationsRepository();
const jobsRepository = new MongoJobsRepository(workerId); const jobsRepository = new MongoJobsRepository(workerId);
const queueWorkersRepository = new MongoQueueWorkersRepository(); const queueWorkersRepository = new MongoQueueWorkersRepository();
@@ -168,28 +167,24 @@ export const buildShared = async ({
const twilioProvider = new TwilioProvider(logger); const twilioProvider = new TwilioProvider(logger);
const ntfyProvider = new NtfyProvider(logger); const ntfyProvider = new NtfyProvider(logger);
const notificationProviders: NotificationProviderRegistry = { const notificationsService = new NotificationsService(
webhook: webhookProvider,
email: emailProvider,
slack: slackProvider,
discord: discordProvider,
pager_duty: pagerDutyProvider,
matrix: matrixProvider,
teams: teamsProvider,
telegram: telegramProvider,
pushover: pushoverProvider,
twilio: twilioProvider,
ntfy: ntfyProvider,
};
const notificationsService = new NotificationsService({
notificationsRepository, notificationsRepository,
monitorsRepository, monitorsRepository,
providers: notificationProviders, webhookProvider,
emailProvider,
slackProvider,
discordProvider,
pagerDutyProvider,
matrixProvider,
teamsProvider,
telegramProvider,
pushoverProvider,
twilioProvider,
ntfyProvider,
settingsService, settingsService,
logger, logger,
notificationMessageBuilder, notificationMessageBuilder
}); );
const sharedServices: SharedServices = { const sharedServices: SharedServices = {
logger, logger,
+8 -8
View File
@@ -131,9 +131,9 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig
geoChecksRepository geoChecksRepository
); );
const worker = await DBQueueWorker.create({ const worker = await DBQueueWorker.create(
logger, logger,
isDbConnected: () => mongoose.connection.readyState === 1, () => mongoose.connection.readyState === 1,
jobsRepository, jobsRepository,
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
@@ -142,13 +142,13 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig
checkProducer, checkProducer,
checkEvaluator, checkEvaluator,
geoCheckPipeline, geoCheckPipeline,
dispatcher: reactorDispatcher, reactorDispatcher,
helper: workerHelper, workerHelper,
queueWorkersRepository, queueWorkersRepository,
queueMode: envSettings.queueMode, envSettings.queueMode,
queuePrimaryProcesses: envSettings.queuePrimaryProcesses, envSettings.queuePrimaryProcesses,
workerId, workerId
}); );
return { worker, networkService, bufferService, statusService }; return { worker, networkService, bufferService, statusService };
}; };
+4
View File
@@ -21,6 +21,10 @@ class MongoDB implements IDb<Mongoose> {
this.db = null; this.db = null;
} }
get serviceName() {
return MongoDB.SERVICE_NAME;
}
connect = async () => { connect = async () => {
try { try {
const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
@@ -20,6 +20,8 @@ export type EnvConfig = {
}; };
export interface ISettingsService { export interface ISettingsService {
readonly serviceName: string;
loadSettings(): EnvConfig;
getSettings(): EnvConfig; getSettings(): EnvConfig;
areStatusPageThemesEnabled(): boolean; areStatusPageThemesEnabled(): boolean;
getDBSettings(): Promise<Settings>; getDBSettings(): Promise<Settings>;
@@ -55,6 +57,14 @@ export class SettingsService implements ISettingsService {
this.settingsRepository = settingsRepository; this.settingsRepository = settingsRepository;
} }
get serviceName() {
return SettingsService.SERVICE_NAME;
}
loadSettings() {
return this.settings;
}
getSettings() { getSettings() {
return this.settings; return this.settings;
} }
@@ -1,22 +1,9 @@
import { CheckModel } from "@/domain/checks/check.model.js"; import { CheckModel } from "@/domain/checks/check.model.js";
import mongoose from "mongoose"; import mongoose from "mongoose";
import type { HardwareDiskStats, HardwareNetStats } from "@/domain/checks/check.type.js";
type DateRange = { start: Date; end: Date }; type DateRange = { start: Date; end: Date };
type HardwareUpChecks = { totalChecks: number }; type HardwareUpChecks = { totalChecks: number };
// $avg yields null when no values are present, so metrics are nullable until the repository applies fallbacks
type NullableValues<T> = { [K in keyof T]: T[K] | null };
export type HardwareStatsBucket = {
_id: string;
avgCpuUsage: number | null;
avgMemoryUsage: number | null;
avgTemperature: number[];
disks: NullableValues<HardwareDiskStats>[];
net: NullableValues<HardwareNetStats>[];
};
const NET_RATE_FIELDS = [ const NET_RATE_FIELDS = [
["bytesSentPerSecond", "bytes_sent"], ["bytesSentPerSecond", "bytes_sent"],
["deltaBytesRecv", "bytes_recv"], ["deltaBytesRecv", "bytes_recv"],
@@ -83,13 +70,13 @@ export const getHardwareUpChecks = async (monitorId: string, dates: DateRange):
return { totalChecks: count }; return { totalChecks: count };
}; };
export const getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string): Promise<HardwareStatsBucket[]> => { export const getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => {
const netProjection = { const netProjection = {
name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] },
...Object.fromEntries(NET_RATE_FIELDS.map(([outputKey, sourceField]) => [outputKey, netRatePerSecond(sourceField)])), ...Object.fromEntries(NET_RATE_FIELDS.map(([outputKey, sourceField]) => [outputKey, netRatePerSecond(sourceField)])),
}; };
return await CheckModel.aggregate<HardwareStatsBucket>([ return await CheckModel.aggregate([
{ {
$match: { $match: {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
+1 -1
View File
@@ -1,5 +1,5 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types } from "mongoose";
import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js";
import type { import type {
Check, Check,
CheckAudits, CheckAudits,
@@ -6,9 +6,8 @@ import type {
PageSpeedChecksResult, PageSpeedChecksResult,
UptimeChecksResult, UptimeChecksResult,
} from "@/domain/checks/check.type.js"; } from "@/domain/checks/check.type.js";
import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js";
import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js"; import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js";
import { CheckFilter, DateRange } from "@/types/query.js";
export interface IChecksRepository { export interface IChecksRepository {
// create // create
@@ -20,27 +19,29 @@ export interface IChecksRepository {
findByMonitorId( findByMonitorId(
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
filter: string | undefined,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined, status: boolean | undefined
filter?: CheckFilter
): Promise<ChecksQueryResult>; ): Promise<ChecksQueryResult>;
findByTeamId( findByTeamId(
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
filter: string | undefined,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
teamId: string, teamId: string
filter?: CheckFilter
): Promise<ChecksQueryResult>; ): Promise<ChecksQueryResult>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId( findByDateRangeAndMonitorId(
monitorId: string, monitorId: string,
dateRange: DateRange, startDate: Date,
endDate: Date,
dateString: string,
options?: { type?: MonitorType } options?: { type?: MonitorType }
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>; ): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise<ChecksSummary>; findSummaryByTeamId(teamId: string, dateRange: string): Promise<ChecksSummary>;
findUnevaluatedByMonitorId(monitorId: string, since: number): Promise<Check[]>; findUnevaluatedByMonitorId(monitorId: string, since: number): Promise<Check[]>;
// update // update
//delete //delete
@@ -11,19 +11,15 @@ import type {
CheckMetadata, CheckMetadata,
CheckNetworkInterfaceInfo, CheckNetworkInterfaceInfo,
GotTimings, GotTimings,
HardwareCheckStats,
} from "@/domain/checks/check.type.js"; } from "@/domain/checks/check.type.js";
import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js";
import { CheckModel, type CheckDocument } from "@/domain/checks/check.model.js"; import { CheckModel, type CheckDocument } from "@/domain/checks/check.model.js";
import mongoose from "mongoose"; import mongoose from "mongoose";
import { getDateFormat, getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js";
import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js"; import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js";
import { CheckFilter, DateRange } from "@/types/query.js";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/types/network.js";
const SERVICE_NAME = "StatusService"; const SERVICE_NAME = "StatusService";
@@ -184,12 +180,7 @@ class MongoChecksRepository implements IChecksRepository {
// Map id to _id for MongoDB storage // Map id to _id for MongoDB storage
const { id, metadata, ...rest } = check; const { id, metadata, ...rest } = check;
if (!metadata || !metadata.monitorId || !metadata.teamId) { if (!metadata || !metadata.monitorId || !metadata.teamId) {
throw new AppError({ throw new Error(`Check must have valid metadata with monitorId and teamId. Got: ${JSON.stringify({ id, metadata })}`);
message: `Check must have valid metadata with monitorId and teamId. Got: ${JSON.stringify({ id, metadata })}`,
status: 500,
service: SERVICE_NAME,
method: "toDocument",
});
} }
return { return {
_id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(),
@@ -213,41 +204,50 @@ class MongoChecksRepository implements IChecksRepository {
return this.mapDocuments(inserted as unknown as CheckDocument[]); return this.mapDocuments(inserted as unknown as CheckDocument[]);
}; };
private filterToMatch = (filter: CheckFilter | undefined): Record<string, unknown> => {
switch (filter) {
case "up":
return { status: true };
case "down":
return { status: false };
case "resolve":
return { status: false, statusCode: NETWORK_ERROR };
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "filterToMatch",
});
return {};
}
};
findByMonitorId = async ( findByMonitorId = async (
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
filter: string | undefined,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined, status: boolean | undefined
filter?: CheckFilter
) => { ) => {
// Match // Match
const matchStage: Record<string, unknown> = { const matchStage: Record<string, unknown> = {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
...(typeof status !== "undefined" && { status }), ...(typeof status !== "undefined" && { status }),
createdAt: { $gte: getDateForRange(dateRange) }, ...(getDateForRange(dateRange) && {
...this.filterToMatch(filter), createdAt: {
$gte: getDateForRange(dateRange),
},
}),
}; };
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "up":
matchStage.status = true;
break;
case "down":
matchStage.status = false;
break;
case "resolve":
matchStage.status = false;
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecks",
});
break;
}
}
//Sort //Sort
const convertedSortOrder = sortOrder === "asc" ? 1 : -1; const convertedSortOrder = sortOrder === "asc" ? 1 : -1;
@@ -265,12 +265,39 @@ class MongoChecksRepository implements IChecksRepository {
return { checksCount, checks: this.mapDocuments(checks) }; return { checksCount, checks: this.mapDocuments(checks) };
}; };
findByTeamId = async (sortOrder: string, dateRange: DateRange, page: number, rowsPerPage: number, teamId: string, filter?: CheckFilter) => { findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, unknown> = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), "metadata.teamId": new mongoose.Types.ObjectId(teamId),
createdAt: { $gte: getDateForRange(dateRange) }, ...(getDateForRange(dateRange) && {
...this.filterToMatch(filter), createdAt: {
$gte: getDateForRange(dateRange),
},
}),
}; };
// Add filter to match stage
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "up":
matchStage.status = true;
break;
case "down":
matchStage.status = false;
break;
case "resolve":
matchStage.status = false;
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksByTeam",
});
break;
}
}
const parsedSortOrder = sortOrder === "asc" ? 1 : -1; const parsedSortOrder = sortOrder === "asc" ? 1 : -1;
@@ -314,25 +341,25 @@ class MongoChecksRepository implements IChecksRepository {
return mapped; return mapped;
}; };
findByDateRangeAndMonitorId = async (monitorId: string, dateRange: DateRange, options?: { type?: MonitorType }) => { findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
const monitorObjectId = new mongoose.Types.ObjectId(monitorId); const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
const start = getDateForRange(dateRange);
const dateString = getDateFormat(dateRange);
const end = new Date();
if (options?.type === "hardware") { if (options?.type === "hardware") {
return this.findHardwareDateRangeChecks(monitorObjectId, start, end, dateString); return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
} }
if (options?.type === "pagespeed") { if (options?.type === "pagespeed") {
return this.findPageSpeedDateRangeChecks(monitorObjectId, start, end, dateString); return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
} }
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString); return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
}; };
findSummaryByTeamId = async (teamId: string, dateRange: DateRange) => { findSummaryByTeamId = async (teamId: string, dateRange: string) => {
const baseMatch = { const baseMatch = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), "metadata.teamId": new mongoose.Types.ObjectId(teamId),
createdAt: { $gte: getDateForRange(dateRange) }, ...(getDateForRange(dateRange) && {
createdAt: {
$gte: getDateForRange(dateRange),
},
}),
}; };
const [totalResult, downResult] = await Promise.all([ const [totalResult, downResult] = await Promise.all([
@@ -513,35 +540,33 @@ class MongoChecksRepository implements IChecksRepository {
totalChecks: upChecksDoc?.totalChecks ?? 0, totalChecks: upChecksDoc?.totalChecks ?? 0,
}; };
const checks = (hardwareMetrics ?? []).map( const checks = (hardwareMetrics ?? []).map((metric) => ({
(metric): HardwareCheckStats => ({ bucketDate: metric._id,
bucketDate: metric._id, avgCpuUsage: metric.avgCpuUsage ?? 0,
avgCpuUsage: metric.avgCpuUsage ?? 0, avgMemoryUsage: metric.avgMemoryUsage ?? 0,
avgMemoryUsage: metric.avgMemoryUsage ?? 0, avgTemperature: metric.avgTemperature ?? [],
avgTemperature: metric.avgTemperature ?? [], disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({
disks: (metric.disks ?? []).map((disk) => ({ name: disk?.name ?? "",
name: disk?.name ?? "", readSpeed: disk?.readSpeed ?? 0,
readSpeed: disk?.readSpeed ?? 0, writeSpeed: disk?.writeSpeed ?? 0,
writeSpeed: disk?.writeSpeed ?? 0, totalBytes: disk?.totalBytes ?? 0,
totalBytes: disk?.totalBytes ?? 0, freeBytes: disk?.freeBytes ?? 0,
freeBytes: disk?.freeBytes ?? 0, usagePercent: disk?.usagePercent ?? 0,
usagePercent: disk?.usagePercent ?? 0, })),
})), net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({
net: (metric.net ?? []).map((iface) => ({ name: iface?.name ?? "",
name: iface?.name ?? "", bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0,
bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0, deltaBytesRecv: iface?.deltaBytesRecv ?? 0,
deltaBytesRecv: iface?.deltaBytesRecv ?? 0, deltaPacketsSent: iface?.deltaPacketsSent ?? 0,
deltaPacketsSent: iface?.deltaPacketsSent ?? 0, deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0,
deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0, deltaErrIn: iface?.deltaErrIn ?? 0,
deltaErrIn: iface?.deltaErrIn ?? 0, deltaErrOut: iface?.deltaErrOut ?? 0,
deltaErrOut: iface?.deltaErrOut ?? 0, deltaDropIn: iface?.deltaDropIn ?? 0,
deltaDropIn: iface?.deltaDropIn ?? 0, deltaDropOut: iface?.deltaDropOut ?? 0,
deltaDropOut: iface?.deltaDropOut ?? 0, deltaFifoIn: iface?.deltaFifoIn ?? 0,
deltaFifoIn: iface?.deltaFifoIn ?? 0, deltaFifoOut: iface?.deltaFifoOut ?? 0,
deltaFifoOut: iface?.deltaFifoOut ?? 0, })),
})), }));
})
);
return { return {
monitorType: "hardware" as const, monitorType: "hardware" as const,
+17 -14
View File
@@ -6,7 +6,6 @@ import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.j
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { CheckFilter, DateRange } from "@/types/query.js";
const SERVICE_NAME = "checkService"; const SERVICE_NAME = "checkService";
@@ -19,21 +18,21 @@ export interface ICheckService {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
filter?: CheckFilter; filter?: string;
status?: boolean; status?: boolean;
}): Promise<ChecksQueryResult>; }): Promise<ChecksQueryResult>;
getChecksByTeam(params: { getChecksByTeam(params: {
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
filter?: CheckFilter; filter?: string;
}): Promise<ChecksQueryResult>; }): Promise<ChecksQueryResult>;
getChecksSummaryByTeamId(params: { teamId: string; dateRange: DateRange }): Promise<ChecksSummary>; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>; deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
deleteChecksByTeamId(params: { teamId: string }): Promise<number>; deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
deleteOlderThan(date: Date): Promise<number>; deleteOlderThan(date: Date): Promise<number>;
@@ -51,6 +50,10 @@ export class CheckService implements ICheckService {
this.checksRepository = checksRepository; this.checksRepository = checksRepository;
} }
get serviceName() {
return CheckService.SERVICE_NAME;
}
createChecks = async (checks: Check[]) => { createChecks = async (checks: Check[]) => {
return this.checksRepository.createChecks(checks); return this.checksRepository.createChecks(checks);
}; };
@@ -165,11 +168,11 @@ export class CheckService implements ICheckService {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
status?: boolean; status?: boolean;
filter: CheckFilter; filter?: string;
}) => { }) => {
if (!monitorId) { if (!monitorId) {
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
@@ -184,7 +187,7 @@ export class CheckService implements ICheckService {
const parsedPage = page ?? 0; const parsedPage = page ?? 0;
const parsedRowsPerPage = rowsPerPage ?? 5; const parsedRowsPerPage = rowsPerPage ?? 5;
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, status, filter); const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status);
return result; return result;
}; };
@@ -193,25 +196,25 @@ export class CheckService implements ICheckService {
teamId, teamId,
sortOrder, sortOrder,
dateRange, dateRange,
filter,
page, page,
rowsPerPage, rowsPerPage,
filter,
}: { }: {
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
filter?: CheckFilter; filter?: string;
}) => { }) => {
const parsedPage = page ?? 0; const parsedPage = page ?? 0;
const parsedRowsPerPage = rowsPerPage ?? 5; const parsedRowsPerPage = rowsPerPage ?? 5;
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, parsedPage, parsedRowsPerPage, teamId, filter); const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
return checkData; return checkData;
}; };
getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: DateRange }) => { getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => {
const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange);
return summary; return summary;
}; };
+38 -47
View File
@@ -1,4 +1,4 @@
import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js";
import type { Response } from "got"; import type { Response } from "got";
export const CHECK_TTL_SENTINEL = 366; export const CHECK_TTL_SENTINEL = 366;
@@ -126,6 +126,43 @@ export interface PageSpeedChecksResult {
groupedChecks: PageSpeedGroupedCheck[]; groupedChecks: PageSpeedGroupedCheck[];
} }
export interface HardwareChecksResult {
monitorType: "hardware";
aggregateData: {
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: Array<{
bucketDate: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: Array<{
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}>;
net: Array<{
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}>;
}>;
}
export interface GroupedCheck { export interface GroupedCheck {
bucketDate: string; bucketDate: string;
avgResponseTime: number; avgResponseTime: number;
@@ -168,49 +205,3 @@ export type NormalizedUptimeCheck<T extends GroupedCheck = GroupedCheck> = T & {
}; };
export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">; export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">;
export interface HardwareDiskStats {
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}
export interface HardwareNetStats {
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}
export interface HardwareCheckStats {
bucketDate: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: HardwareDiskStats[];
net: HardwareNetStats[];
}
export interface HardwareStats {
aggregateData: {
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: HardwareCheckStats[];
}
export interface HardwareChecksResult extends HardwareStats {
monitorType: "hardware";
}
@@ -79,6 +79,10 @@ export class DiagnosticService implements IDiagnosticService {
obs.observe({ entryTypes: ["measure"] }); obs.observe({ entryTypes: ["measure"] });
} }
get serviceName() {
return DiagnosticService.SERVICE_NAME;
}
private getCPUUsagePercentage = async (): Promise<number> => { private getCPUUsagePercentage = async (): Promise<number> => {
const startUsage = process.cpuUsage(); const startUsage = process.cpuUsage();
const timingPeriod = 1000; // measured in ms const timingPeriod = 1000; // measured in ms
@@ -1,5 +1,5 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types } from "mongoose";
import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js";
import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/domain/geo-checks/geo-check.type.js";
type GeoCheckMetadataDocument = Omit<GeoCheckMetadata, "monitorId" | "teamId"> & { type GeoCheckMetadataDocument = Omit<GeoCheckMetadata, "monitorId" | "teamId"> & {
@@ -1,6 +1,5 @@
import type { GeoCheck, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoCheck, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js";
import type { GeoContinent, FlatGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent, FlatGeoCheck } from "@/domain/geo-checks/geo-check.type.js";
import { DateRange } from "@/types/query.js";
export interface GeoChecksQueryResult { export interface GeoChecksQueryResult {
geoChecksCount: number; geoChecksCount: number;
@@ -17,12 +16,19 @@ export interface IGeoChecksRepository {
findByMonitorId( findByMonitorId(
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<FlatGeoChecksQueryResult>; ): Promise<FlatGeoChecksQueryResult>;
findGroupedByMonitorIdAndDateRange(monitorId: string, DateRange: DateRange, continents?: GeoContinent[]): Promise<GroupedGeoCheck[]>; findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]>;
findGroupedByMonitorIdAndDateRange(
monitorId: string,
startDate: Date,
endDate: Date,
dateFormat: string,
continents?: GeoContinent[]
): Promise<GroupedGeoCheck[]>;
deleteByMonitorId(monitorId: string): Promise<number>; deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>; deleteByTeamId(teamId: string): Promise<number>;
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
@@ -3,15 +3,20 @@ import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoCo
import type { FlatGeoChecksQueryResult } from "./geo-check.repository.interface.js"; import type { FlatGeoChecksQueryResult } from "./geo-check.repository.interface.js";
import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/domain/geo-checks/geo-check.model.js"; import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/domain/geo-checks/geo-check.model.js";
import mongoose, { PipelineStage } from "mongoose"; import mongoose, { PipelineStage } from "mongoose";
import { getDateForRange, getDateFormat } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js";
import { DateRange } from "@/types/query.js";
const SERVICE_NAME = "GeoChecksRepository"; const SERVICE_NAME = "GeoChecksRepository";
class MongoGeoChecksRepository implements IGeoChecksRepository { class MongoGeoChecksRepository implements IGeoChecksRepository {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
private toEntity = (doc: GeoCheckDocument): GeoCheck => { private toEntity = (doc: GeoCheckDocument): GeoCheck => {
const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({ const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({
monitorId: toStringId(metadata.monitorId), monitorId: toStringId(metadata.monitorId),
@@ -58,214 +63,304 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
}; };
createGeoChecks = async (geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]> => { createGeoChecks = async (geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]> => {
const docs = await GeoCheckModel.insertMany( try {
geoChecks.map((geoCheck) => ({ const docs = await GeoCheckModel.insertMany(
metadata: { geoChecks.map((geoCheck) => ({
monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId), metadata: {
teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId), monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId),
type: geoCheck.metadata.type, teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId),
}, type: geoCheck.metadata.type,
results: geoCheck.results, },
expiry: new Date(geoCheck.expiry), results: geoCheck.results,
})) expiry: new Date(geoCheck.expiry),
); }))
return docs.map((doc) => this.toEntity(doc)); );
return docs.map((doc) => this.toEntity(doc));
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks",
service: SERVICE_NAME,
method: "createGeoChecks",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
findByMonitorId = async ( findByMonitorId = async (
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<FlatGeoChecksQueryResult> => { ): Promise<FlatGeoChecksQueryResult> => {
const matchStage: Record<string, unknown> = { try {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), const matchStage: Record<string, unknown> = {
createdAt: { $gte: getDateForRange(dateRange) }, "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
}; ...(getDateForRange(dateRange) && {
createdAt: {
$gte: getDateForRange(dateRange),
},
}),
};
const convertedSortOrder = sortOrder === "asc" ? 1 : -1; const convertedSortOrder = sortOrder === "asc" ? 1 : -1;
let skip = 0; let skip = 0;
if (page && rowsPerPage) { if (page && rowsPerPage) {
skip = page * rowsPerPage; skip = page * rowsPerPage;
} }
// Common pipeline stages for both paths // Common pipeline stages for both paths
const pipeline: PipelineStage[] = [ const pipeline: PipelineStage[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
// Filter by continent if specified // Filter by continent if specified
...(continents && continents.length > 0 ...(continents && continents.length > 0
? [ ? [
{ {
$match: { $match: {
"results.location.continent": { $in: continents }, "results.location.continent": { $in: continents },
},
}, },
}, ]
] : []),
: []), // Project to flat structure
// Project to flat structure {
{ $project: {
$project: { _id: 0,
_id: 0, monitorId: "$metadata.monitorId",
monitorId: "$metadata.monitorId", teamId: "$metadata.teamId",
teamId: "$metadata.teamId", type: "$metadata.type",
type: "$metadata.type", location: "$results.location",
location: "$results.location", status: "$results.status",
status: "$results.status", statusCode: "$results.statusCode",
statusCode: "$results.statusCode", timings: "$results.timings",
timings: "$results.timings", createdAt: 1,
createdAt: 1, updatedAt: 1,
updatedAt: 1, },
}, },
}, { $sort: { createdAt: convertedSortOrder } },
{ $sort: { createdAt: convertedSortOrder } }, { $skip: skip },
{ $skip: skip }, { $limit: rowsPerPage },
{ $limit: rowsPerPage }, ];
];
// Count pipeline // Count pipeline
const countPipeline: PipelineStage[] = [ const countPipeline: PipelineStage[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
...(continents && continents.length > 0 ...(continents && continents.length > 0
? [ ? [
{ {
$match: { $match: {
"results.location.continent": { $in: continents }, "results.location.continent": { $in: continents },
},
}, },
}, ]
] : []),
: []), { $count: "count" },
{ $count: "count" }, ];
];
const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]); const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]);
const geoChecksCount = countResult[0]?.count || 0; const geoChecksCount = countResult[0]?.count || 0;
const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({ const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({
id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`, id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`,
monitorId: doc.monitorId.toString(), monitorId: doc.monitorId.toString(),
teamId: doc.teamId.toString(), teamId: doc.teamId.toString(),
type: doc.type, type: doc.type,
location: doc.location, location: doc.location,
status: doc.status, status: doc.status,
statusCode: doc.statusCode, statusCode: doc.statusCode,
timings: doc.timings, timings: doc.timings,
createdAt: new Date(doc.createdAt).toISOString(), createdAt: new Date(doc.createdAt).toISOString(),
updatedAt: new Date(doc.updatedAt).toISOString(), updatedAt: new Date(doc.updatedAt).toISOString(),
})); }));
return { geoChecksCount, geoChecks }; return { geoChecksCount, geoChecks };
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID",
service: SERVICE_NAME,
method: "findByMonitorId",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
findGroupedByMonitorIdAndDateRange = async (monitorId: string, dateRange: DateRange, continents?: GeoContinent[]): Promise<GroupedGeoCheck[]> => { findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]> => {
const start = getDateForRange(dateRange); try {
const dateString = getDateFormat(dateRange); const docs = await GeoCheckModel.find({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
createdAt: {
$gte: startDate,
$lte: endDate,
},
}).sort({ createdAt: -1 });
return docs.map(this.toEntity);
} catch (error: unknown) {
this.logger.error({
message:
error instanceof Error
? `Error finding geo checks by monitor ID and date range: ${error.message}`
: "Error finding geo checks by monitor ID and date range",
service: SERVICE_NAME,
method: "findByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
};
const end = new Date(); findGroupedByMonitorIdAndDateRange = async (
const pipeline: PipelineStage[] = [ monitorId: string,
// Match geo checks for this monitor in date range startDate: Date,
{ endDate: Date,
$match: { dateFormat: string,
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), continents?: GeoContinent[]
createdAt: { ): Promise<GroupedGeoCheck[]> => {
$gte: start, try {
$lte: end, const pipeline: PipelineStage[] = [
}, // Match geo checks for this monitor in date range
}, {
}, $match: {
// Unwind the results array to process each location separately "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
{ createdAt: {
$unwind: "$results", $gte: startDate,
}, $lte: endDate,
// Filter by continent if specified
...(continents && continents.length > 0
? [
{
$match: {
"results.location.continent": { $in: continents },
},
},
]
: []),
// Group by date bucket and continent
{
$group: {
_id: {
bucketDate: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
continent: "$results.location.continent",
},
avgResponseTime: { $avg: "$results.timings.total" },
totalChecks: { $sum: 1 },
upChecks: {
$sum: {
$cond: ["$results.status", 1, 0],
}, },
}, },
}, },
}, // Unwind the results array to process each location separately
// Calculate uptime percentage {
{ $unwind: "$results",
$project: { },
_id: 0, // Filter by continent if specified
bucketDate: "$_id.bucketDate", ...(continents && continents.length > 0
continent: "$_id.continent", ? [
avgResponseTime: { $round: ["$avgResponseTime", 2] },
totalChecks: 1,
uptimePercentage: {
$round: [
{ {
$multiply: [ $match: {
{ "results.location.continent": { $in: continents },
$divide: ["$upChecks", "$totalChecks"], },
},
100,
],
}, },
2, ]
], : []),
// Group by date bucket and continent
{
$group: {
_id: {
bucketDate: {
$dateToString: {
format: dateFormat,
date: "$createdAt",
},
},
continent: "$results.location.continent",
},
avgResponseTime: { $avg: "$results.timings.total" },
totalChecks: { $sum: 1 },
upChecks: {
$sum: {
$cond: ["$results.status", 1, 0],
},
},
}, },
}, },
}, // Calculate uptime percentage
// Sort by date and continent {
{ $project: {
$sort: { _id: 0,
bucketDate: 1, bucketDate: "$_id.bucketDate",
continent: 1, continent: "$_id.continent",
avgResponseTime: { $round: ["$avgResponseTime", 2] },
totalChecks: 1,
uptimePercentage: {
$round: [
{
$multiply: [
{
$divide: ["$upChecks", "$totalChecks"],
},
100,
],
},
2,
],
},
},
}, },
}, // Sort by date and continent
]; {
$sort: {
bucketDate: 1,
continent: 1,
},
},
];
const results = await GeoCheckModel.aggregate(pipeline); const results = await GeoCheckModel.aggregate(pipeline);
return results as GroupedGeoCheck[]; return results as GroupedGeoCheck[];
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks",
service: SERVICE_NAME,
method: "findGroupedByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
deleteByMonitorId = async (monitorId: string): Promise<number> => { deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await GeoCheckModel.deleteMany({ try {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), const result = await GeoCheckModel.deleteMany({
}); "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
return result.deletedCount || 0; });
return result.deletedCount || 0;
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID",
service: SERVICE_NAME,
method: "deleteByMonitorId",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
deleteByTeamId = async (teamId: string): Promise<number> => { deleteByTeamId = async (teamId: string): Promise<number> => {
const result = await GeoCheckModel.deleteMany({ try {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), const result = await GeoCheckModel.deleteMany({
}); "metadata.teamId": new mongoose.Types.ObjectId(teamId),
return result.deletedCount || 0; });
return result.deletedCount || 0;
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID",
service: SERVICE_NAME,
method: "deleteByTeamId",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => { deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); try {
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
return result.deletedCount || 0; const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
return result.deletedCount || 0;
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks",
service: SERVICE_NAME,
method: "deleteByMonitorIdsNotIn",
stack: error instanceof Error ? error.stack : undefined,
});
throw error;
}
}; };
} }
@@ -1,5 +1,5 @@
import type { GeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoCheck } from "@/domain/geo-checks/geo-check.type.js";
import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { GeoCheckResult, GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoCheckResult, GeoContinent } from "@/domain/geo-checks/geo-check.type.js";
import { Types } from "mongoose"; import { Types } from "mongoose";
import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/domain/geo-checks/geo-check.repository.interface.js"; import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/domain/geo-checks/geo-check.repository.interface.js";
@@ -7,18 +7,18 @@ import type { IMonitorsRepository } from "@/domain/monitors/monitor.repository.i
import type { IGlobalPingService } from "@/service/globalPingService.js"; import type { IGlobalPingService } from "@/service/globalPingService.js";
import type { ILogger } from "@/utils/logger.js"; import type { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { DateRange } from "@/types/query.js";
const SERVICE_NAME = "GeoChecksService"; const SERVICE_NAME = "GeoChecksService";
export interface IGeoChecksService { export interface IGeoChecksService {
readonly serviceName: string;
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>; buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>; createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
getGeoChecksByMonitor(args: { getGeoChecksByMonitor(args: {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page?: number; page?: number;
rowsPerPage?: number; rowsPerPage?: number;
continent?: GeoContinent | GeoContinent[]; continent?: GeoContinent | GeoContinent[];
@@ -50,6 +50,10 @@ export class GeoChecksService implements IGeoChecksService {
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
} }
get serviceName() {
return GeoChecksService.SERVICE_NAME;
}
async buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null> { async buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null> {
try { try {
if (!monitor.url) { if (!monitor.url) {
@@ -156,7 +160,7 @@ export class GeoChecksService implements IGeoChecksService {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
sortOrder: string; sortOrder: string;
dateRange: DateRange; dateRange: string;
page?: number; page?: number;
rowsPerPage?: number; rowsPerPage?: number;
continent: GeoContinent | GeoContinent[]; continent: GeoContinent | GeoContinent[];
@@ -1,4 +1,4 @@
import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js";
export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const; export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
export type GeoContinent = (typeof GeoContinents)[number]; export type GeoContinent = (typeof GeoContinents)[number];
@@ -1,5 +1,5 @@
const SERVICE_NAME = "incidentService"; const SERVICE_NAME = "incidentService";
import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { MonitorStatusResponse } from "@/types/network.js"; import type { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
@@ -11,7 +11,6 @@ import type { User } from "@/domain/users/user.type.js";
import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { MonitorActionDecision } from "@/worker/worker.helper.js";
import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js";
import type { ILogger } from "@/utils/logger.js"; import type { ILogger } from "@/utils/logger.js";
import { DateRange } from "@/types/query.js";
export interface IIncidentService { export interface IIncidentService {
handleIncident( handleIncident(
@@ -24,7 +23,7 @@ export interface IIncidentService {
getIncidentsByTeam( getIncidentsByTeam(
teamId: string, teamId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined, status: boolean | undefined,
@@ -58,6 +57,10 @@ export class IncidentService implements IIncidentService {
this.notificationMessageBuilder = notificationMessageBuilder; this.notificationMessageBuilder = notificationMessageBuilder;
} }
get serviceName() {
return IncidentService.SERVICE_NAME;
}
handleIncident = async ( handleIncident = async (
monitor: Monitor, monitor: Monitor,
code: number, code: number,
@@ -175,7 +178,7 @@ export class IncidentService implements IIncidentService {
getIncidentsByTeam = async ( getIncidentsByTeam = async (
teamId: string, teamId: string,
sortOrder: string, sortOrder: string,
dateRange: DateRange, dateRange: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined, status: boolean | undefined,
+2 -2
View File
@@ -1,7 +1,7 @@
// export type IncidentResolutionType = "automatic" | "manual" | null; // export type IncidentResolutionType = "automatic" | "manual" | null;
export const IncidentResolutionTypes = ["automatic", "manual"] as const; export const IncidentResolutionTypes = ["automatic", "manual", null] as const;
export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number] | null; export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number];
export interface Incident { export interface Incident {
id: string; id: string;
@@ -35,6 +35,10 @@ export class InviteService implements IInviteService {
this.emailService = emailService; this.emailService = emailService;
} }
get serviceName() {
return InviteService.SERVICE_NAME;
}
getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }) => { getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }) => {
invite.teamId = teamId; invite.teamId = teamId;
@@ -62,6 +62,10 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
this.scheduler = scheduler; this.scheduler = scheduler;
} }
get serviceName() {
return MaintenanceWindowService.SERVICE_NAME;
}
private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise<void> => { private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise<void> => {
if (monitorIds.length === 0) return; if (monitorIds.length === 0) return;
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monitors/monitor.type.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monitors/monitor.types.js";
import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.type.js"; import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.types.js";
import type { import type {
CheckAudits, CheckAudits,
CheckCaptureInfo, CheckCaptureInfo,
@@ -1,4 +1,4 @@
import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.type.js"; import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.types.js";
export interface TeamQueryConfig { export interface TeamQueryConfig {
limit?: number; limit?: number;
@@ -26,9 +26,9 @@ export interface IMonitorsRepository {
findByIdLean(monitorId: string): Promise<Monitor | null>; findByIdLean(monitorId: string): Promise<Monitor | null>;
// collection fetch // collection fetch
findAll(): Promise<Monitor[]>; findAll(): Promise<Monitor[] | null>;
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[]>; findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise<Monitor[]>; findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
findByIds(monitorIds: string[]): Promise<Monitor[]>; findByIds(monitorIds: string[]): Promise<Monitor[]>;
findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<Monitor[]>; findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<Monitor[]>;
@@ -1,7 +1,7 @@
import { MonitorModel } from "@/domain/monitors/monitor.model.js"; import { MonitorModel } from "@/domain/monitors/monitor.model.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/domain/monitors/monitor.model.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/domain/monitors/monitor.model.js";
import type { CheckSnapshot } from "@/domain/checks/check.type.js"; import type { CheckSnapshot } from "@/domain/checks/check.type.js";
import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js"; import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.types.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import { MongoBulkWriteError } from "mongodb"; import { MongoBulkWriteError } from "mongodb";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
@@ -91,26 +91,27 @@ class MongoMonitorsRepository implements IMonitorsRepository {
{ $project: { stats: 0 } }, { $project: { stats: 0 } },
]; ];
private pageOptions = (config: TeamQueryConfig) => { findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {};
return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage };
};
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[]> => {
const query = this.queryBuilder(config, teamId); const query = this.queryBuilder(config, teamId);
const { sort, skip, limit } = this.pageOptions(config); const sort = { [field]: order === "asc" ? 1 : -1 } as const;
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limit); const skip = Math.max(page, 0) * rowsPerPage;
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage);
return this.mapDocuments(documents); return this.mapDocuments(documents);
}; };
findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[]> => { findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { sort, skip, limit } = this.pageOptions(config); const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {};
const query = this.queryBuilder(config, teamId); const query = this.queryBuilder(config, teamId);
const sort = { [field]: order === "asc" ? 1 : -1 } as const;
const skip = Math.max(page, 0) * rowsPerPage;
const pipeline: PipelineStage[] = [ const pipeline: PipelineStage[] = [
{ $match: query }, { $match: query },
{ $sort: sort }, { $sort: sort },
...(limit ? [{ $skip: skip }, { $limit: limit }] : []), ...(rowsPerPage ? [{ $skip: skip }, { $limit: rowsPerPage }] : []),
...this.uptimeStatsLookupStages, ...this.uptimeStatsLookupStages,
]; ];
+79 -34
View File
@@ -1,5 +1,5 @@
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
import { type Monitor } from "@/domain/monitors/monitor.type.js"; import { type Monitor } from "@/domain/monitors/monitor.types.js";
import type { import type {
MonitorType, MonitorType,
MonitorsWithChecksByTeamIdResult, MonitorsWithChecksByTeamIdResult,
@@ -8,8 +8,8 @@ import type {
PageSpeedDetailsResult, PageSpeedDetailsResult,
GamesMap, GamesMap,
GroupedGeoCheckResult, GroupedGeoCheckResult,
} from "@/domain/monitors/monitor.type.js"; } from "@/domain/monitors/monitor.types.js";
import { supportsGeoCheck, supportsUptimeDetails } from "@/domain/monitors/monitor.type.js"; import { supportsGeoCheck, supportsUptimeDetails } from "@/domain/monitors/monitor.types.js";
import type { UptimeChecksResult, HardwareChecksResult, PageSpeedChecksResult } from "@/domain/checks/check.type.js"; import type { UptimeChecksResult, HardwareChecksResult, PageSpeedChecksResult } from "@/domain/checks/check.type.js";
import type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js";
import type { IChecksRepository } from "@/domain/checks/check.repository.interface.js"; import type { IChecksRepository } from "@/domain/checks/check.repository.interface.js";
@@ -23,27 +23,29 @@ import { AppError } from "@/utils/AppError.js";
import type { ImportedMonitor } from "@/api/validation/monitorValidation.js"; import type { ImportedMonitor } from "@/api/validation/monitorValidation.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { IJobScheduler } from "@/worker/worker.interface.js"; import { IJobScheduler } from "@/worker/worker.interface.js";
import { DateRange } from "@/types/query.js";
const SERVICE_NAME = "MonitorService"; const SERVICE_NAME = "MonitorService";
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult): result is UptimeChecksResult => const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult): result is UptimeChecksResult =>
supportsUptimeDetails(result.monitorType); supportsUptimeDetails(result.monitorType);
export interface IMonitorService { export interface IMonitorService {
readonly serviceName: string;
// create // create
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>; createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>; createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>; addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read // read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<UptimeDetailsResult>; getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<UptimeDetailsResult>;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<HardwareDetailsResult>; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<PageSpeedDetailsResult>; getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
getGeoChecksByMonitorId(args: { getGeoChecksByMonitorId(args: {
teamId: string; teamId: string;
monitorId: string; monitorId: string;
dateRange: DateRange; dateRange: string;
continents?: GeoContinent[]; continents?: GeoContinent[];
}): Promise<GroupedGeoCheckResult>; }): Promise<GroupedGeoCheckResult>;
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>; getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
@@ -133,6 +135,35 @@ export class MonitorService implements IMonitorService {
this.incidentsRepository = incidentsRepository; this.incidentsRepository = incidentsRepository;
} }
get serviceName(): string {
return MonitorService.SERVICE_NAME;
}
private getDateRange = (dateRange: DateRangeKey) => {
const startDates = {
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: new Date(0),
};
return {
start: startDates[dateRange],
end: new Date(),
};
};
private getDateFormat = (dateRange: DateRangeKey): string => {
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
all: "%Y-%m-%dT00:00:00Z",
};
return formatLookup[dateRange];
};
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => { createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
const monitor = await this.monitorsRepository.create(body, teamId, userId); const monitor = await this.monitorsRepository.create(body, teamId, userId);
if (!monitor) { if (!monitor) {
@@ -175,13 +206,15 @@ export class MonitorService implements IMonitorService {
}: { }: {
teamId: string; teamId: string;
monitorId: string; monitorId: string;
dateRange: DateRange; dateRange: string;
}): Promise<UptimeDetailsResult> => { }): Promise<UptimeDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) { if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
} }
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { const rangeKey = dateRange as DateRangeKey;
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type, type: monitor.type,
}); });
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
@@ -210,7 +243,7 @@ export class MonitorService implements IMonitorService {
}: { }: {
teamId: string; teamId: string;
monitorId: string; monitorId: string;
dateRange: DateRange; dateRange: string;
}): Promise<HardwareDetailsResult> => { }): Promise<HardwareDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) { if (!monitor) {
@@ -220,7 +253,9 @@ export class MonitorService implements IMonitorService {
throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 });
} }
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { const rangeKey = dateRange as DateRangeKey;
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type, type: monitor.type,
}); });
@@ -250,7 +285,7 @@ export class MonitorService implements IMonitorService {
}: { }: {
teamId: string; teamId: string;
monitorId: string; monitorId: string;
dateRange: DateRange; dateRange: string;
}): Promise<PageSpeedDetailsResult> => { }): Promise<PageSpeedDetailsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) { if (!monitor) {
@@ -260,7 +295,9 @@ export class MonitorService implements IMonitorService {
throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
} }
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { const rangeKey = dateRange as DateRangeKey;
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type, type: monitor.type,
}); });
@@ -287,7 +324,7 @@ export class MonitorService implements IMonitorService {
}: { }: {
teamId: string; teamId: string;
monitorId: string; monitorId: string;
dateRange: DateRange; dateRange: string;
continents?: GeoContinent[]; continents?: GeoContinent[];
}): Promise<GroupedGeoCheckResult> => { }): Promise<GroupedGeoCheckResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
@@ -299,7 +336,15 @@ export class MonitorService implements IMonitorService {
return { groupedGeoChecks: [] }; return { groupedGeoChecks: [] };
} }
const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(monitor.id, dateRange, continents); const rangeKey = dateRange as DateRangeKey;
const { start, end } = this.getDateRange(rangeKey);
const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(
monitor.id,
start,
end,
this.getDateFormat(rangeKey),
continents
);
return { groupedGeoChecks }; return { groupedGeoChecks };
}; };
@@ -318,7 +363,7 @@ export class MonitorService implements IMonitorService {
type?: MonitorType | MonitorType[]; type?: MonitorType | MonitorType[];
tags?: string | string[]; tags?: string | string[];
filter?: string; filter?: string;
}): Promise<Monitor[]> => { }): Promise<Monitor[] | null> => {
return await this.monitorsRepository.findByTeamId(teamId, { type, tags, filter }); return await this.monitorsRepository.findByTeamId(teamId, { type, tags, filter });
}; };
@@ -356,12 +401,13 @@ export class MonitorService implements IMonitorService {
order, order,
}); });
const monitorsList = (monitors ?? []) as Monitor[];
const snapshotTypes: MonitorType[] = ["hardware"]; const snapshotTypes: MonitorType[] = ["hardware"];
const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; const requestedTypes = Array.isArray(type) ? type : type ? [type] : [];
const snapshotOnlyRequest = const snapshotOnlyRequest =
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
const monitorsWithChecks = monitors.map((monitor: Monitor) => { const monitorsWithChecks = monitorsList.map((monitor: Monitor) => {
const rawChecks = monitor.recentChecks ?? []; const rawChecks = monitor.recentChecks ?? [];
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100);
@@ -450,7 +496,15 @@ export class MonitorService implements IMonitorService {
return { monitors, failedCount }; return { monitors, failedCount };
}; };
private deleteMonitorChildren = async (monitor: Monitor, teamId: string) => { deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err instanceof Error ? err.stack : undefined,
});
});
await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({ this.logger.warn({
message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`,
@@ -482,19 +536,6 @@ export class MonitorService implements IMonitorService {
}); });
}); });
await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => {
this.logger.warn({
message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`,
service: SERVICE_NAME,
stack: err instanceof Error ? err.stack : undefined,
});
});
};
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
await this.deleteMonitorChildren(monitor, teamId);
await this.scheduler.deleteJob(monitor); await this.scheduler.deleteJob(monitor);
return monitor; return monitor;
}; };
@@ -504,8 +545,12 @@ export class MonitorService implements IMonitorService {
await Promise.all( await Promise.all(
monitors.map(async (monitor) => { monitors.map(async (monitor) => {
try { try {
await this.deleteMonitorChildren(monitor, teamId);
await this.scheduler.deleteJob(monitor); await this.scheduler.deleteJob(monitor);
await this.checksRepository.deleteByMonitorId(monitor.id);
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId);
} catch (error: unknown) { } catch (error: unknown) {
this.logger.warn({ this.logger.warn({
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
@@ -522,7 +567,7 @@ export class MonitorService implements IMonitorService {
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => { exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => {
const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
if (monitors.length === 0) { if (!monitors || monitors.length === 0) {
throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 }); throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 });
} }
@@ -3,7 +3,6 @@ export type { CheckSnapshot } from "@/domain/checks/check.type.js";
import type { GeoContinent, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js";
export type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; export type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js";
import http from "node:http"; import http from "node:http";
import { HardwareStats } from "@/domain/checks/check.type.js";
export const HttpStatusCodes = [ export const HttpStatusCodes = [
...Object.keys(http.STATUS_CODES).map(Number), ...Object.keys(http.STATUS_CODES).map(Number),
@@ -150,6 +149,48 @@ export interface UptimeDetailsResult {
monitorStats: import("../monitor-stats/monitor-stats.type.js").MonitorStats | null; monitorStats: import("../monitor-stats/monitor-stats.type.js").MonitorStats | null;
} }
export interface HardwareDiskStats {
name: string;
readSpeed: number;
writeSpeed: number;
totalBytes: number;
freeBytes: number;
usagePercent: number;
}
export interface HardwareNetStats {
name: string;
bytesSentPerSecond: number;
deltaBytesRecv: number;
deltaPacketsSent: number;
deltaPacketsRecv: number;
deltaErrIn: number;
deltaErrOut: number;
deltaDropIn: number;
deltaDropOut: number;
deltaFifoIn: number;
deltaFifoOut: number;
}
export interface HardwareCheckStats {
bucketDate: string;
avgCpuUsage: number;
avgMemoryUsage: number;
avgTemperature: number[];
disks: HardwareDiskStats[];
net: HardwareNetStats[];
}
export interface HardwareStats {
aggregateData: {
totalChecks: number;
};
upChecks: {
totalChecks: number;
};
checks: HardwareCheckStats[];
}
export interface HardwareDetailsResult { export interface HardwareDetailsResult {
monitor: Monitor; monitor: Monitor;
stats: HardwareStats; stats: HardwareStats;
@@ -1,4 +1,4 @@
import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import type { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { MonitorActionDecision } from "@/worker/worker.helper.js";
import type { import type {
@@ -1,4 +1,4 @@
import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { Notification } from "@/domain/notifications/notification.type.js"; import type { Notification } from "@/domain/notifications/notification.type.js";
import type { MonitorStatusResponse } from "@/types/network.js"; import type { MonitorStatusResponse } from "@/types/network.js";
import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js";
@@ -9,9 +9,6 @@ import type { MonitorActionDecision } from "@/worker/worker.helper.js";
import type { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import type { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js";
import type { NotificationChannel } from "@/domain/notifications/notification.type.js";
export type NotificationProviderRegistry = Record<NotificationChannel, INotificationProvider>;
export interface INotificationsService { export interface INotificationsService {
createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>; createNotification: (notificationData: Partial<Notification>, userId: string, teamId: string) => Promise<Notification>;
@@ -32,29 +29,52 @@ export class NotificationsService implements INotificationsService {
private notificationsRepository: INotificationsRepository; private notificationsRepository: INotificationsRepository;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
private providers: NotificationProviderRegistry; private webhookProvider: INotificationProvider;
private settingsService: ISettingsService; private emailProvider: INotificationProvider;
private slackProvider: INotificationProvider;
private discordProvider: INotificationProvider;
private pagerDutyProvider: INotificationProvider;
private matrixProvider: INotificationProvider;
private teamsProvider: INotificationProvider;
private telegramProvider: INotificationProvider;
private pushoverProvider: INotificationProvider;
private twilioProvider: INotificationProvider;
private ntfyProvider: INotificationProvider;
private logger: ILogger; private logger: ILogger;
private settingsService: ISettingsService;
private notificationMessageBuilder: INotificationMessageBuilder; private notificationMessageBuilder: INotificationMessageBuilder;
constructor({ constructor(
notificationsRepository, notificationsRepository: INotificationsRepository,
monitorsRepository, monitorsRepository: IMonitorsRepository,
providers, webhookProvider: INotificationProvider,
settingsService, emailProvider: INotificationProvider,
logger, slackProvider: INotificationProvider,
notificationMessageBuilder, discordProvider: INotificationProvider,
}: { pagerDutyProvider: INotificationProvider,
notificationsRepository: INotificationsRepository; matrixProvider: INotificationProvider,
monitorsRepository: IMonitorsRepository; teamsProvider: INotificationProvider,
providers: NotificationProviderRegistry; telegramProvider: INotificationProvider,
settingsService: ISettingsService; pushoverProvider: INotificationProvider,
logger: ILogger; twilioProvider: INotificationProvider,
notificationMessageBuilder: INotificationMessageBuilder; ntfyProvider: INotificationProvider,
}) { settingsService: ISettingsService,
logger: ILogger,
notificationMessageBuilder: INotificationMessageBuilder
) {
this.notificationsRepository = notificationsRepository; this.notificationsRepository = notificationsRepository;
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
this.providers = providers; this.webhookProvider = webhookProvider;
this.emailProvider = emailProvider;
this.slackProvider = slackProvider;
this.discordProvider = discordProvider;
this.pagerDutyProvider = pagerDutyProvider;
this.matrixProvider = matrixProvider;
this.teamsProvider = teamsProvider;
this.telegramProvider = telegramProvider;
this.pushoverProvider = pushoverProvider;
this.twilioProvider = twilioProvider;
this.ntfyProvider = ntfyProvider;
this.settingsService = settingsService; this.settingsService = settingsService;
this.logger = logger; this.logger = logger;
this.notificationMessageBuilder = notificationMessageBuilder; this.notificationMessageBuilder = notificationMessageBuilder;
@@ -77,16 +97,37 @@ export class NotificationsService implements INotificationsService {
} }
// Route to provider based on notification type // Route to provider based on notification type
const provider = this.providers[notification.type]; switch (notification.type) {
if (!provider) { case "webhook":
this.logger.warn({ return await this.webhookProvider.sendMessage!(notification, notificationMessage);
message: `Unknown notification type: ${notification.type}`, case "slack":
service: SERVICE_NAME, return await this.slackProvider.sendMessage!(notification, notificationMessage);
method: "send", case "matrix":
}); return await this.matrixProvider.sendMessage!(notification, notificationMessage);
return false; case "pager_duty":
return await this.pagerDutyProvider.sendMessage!(notification, notificationMessage);
case "discord":
return await this.discordProvider.sendMessage!(notification, notificationMessage);
case "email":
return await this.emailProvider.sendMessage!(notification, notificationMessage);
case "teams":
return await this.teamsProvider.sendMessage!(notification, notificationMessage);
case "telegram":
return await this.telegramProvider.sendMessage!(notification, notificationMessage);
case "pushover":
return await this.pushoverProvider.sendMessage!(notification, notificationMessage);
case "twilio":
return await this.twilioProvider.sendMessage!(notification, notificationMessage);
case "ntfy":
return await this.ntfyProvider.sendMessage!(notification, notificationMessage);
default:
this.logger.warn({
message: `Unknown notification type: ${notification.type}`,
service: SERVICE_NAME,
method: "send",
});
return false;
} }
return await provider.sendMessage(notification, notificationMessage);
}; };
private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
@@ -124,26 +165,32 @@ export class NotificationsService implements INotificationsService {
}; };
sendTestNotification = async (notification: Partial<Notification>) => { sendTestNotification = async (notification: Partial<Notification>) => {
const type = notification.type; switch (notification.type) {
if (!type) { case "email":
this.logger.warn({ return await this.emailProvider.sendTestAlert(notification);
message: "Notification type not provided", case "slack":
service: SERVICE_NAME, return await this.slackProvider.sendTestAlert(notification);
method: "sendTestNotification", case "discord":
}); return await this.discordProvider.sendTestAlert(notification);
return false; case "pager_duty":
return await this.pagerDutyProvider.sendTestAlert(notification);
case "matrix":
return await this.matrixProvider.sendTestAlert(notification);
case "webhook":
return await this.webhookProvider.sendTestAlert(notification);
case "teams":
return await this.teamsProvider.sendTestAlert(notification);
case "telegram":
return await this.telegramProvider.sendTestAlert(notification);
case "pushover":
return await this.pushoverProvider.sendTestAlert(notification);
case "twilio":
return await this.twilioProvider.sendTestAlert(notification);
case "ntfy":
return await this.ntfyProvider.sendTestAlert(notification);
default:
return false;
} }
const provider = this.providers[type];
if (!provider) {
this.logger.warn({
message: `Unknown notification type: ${notification.type}`,
service: SERVICE_NAME,
method: "sendTestNotification",
});
return false;
}
return await provider.sendTestAlert(notification);
}; };
testAllNotifications = async (notificationIds: string[]) => { testAllNotifications = async (notificationIds: string[]) => {
@@ -4,8 +4,7 @@ import { QueueWorkerDocument } from "@/domain/queue-workers/queue-worker.type.js
// Single source of truth for worker liveness: the TTL that GCs stale records and // Single source of truth for worker liveness: the TTL that GCs stale records and
// the window readers use to count a worker as alive. Heartbeat cadence must stay well below it. // the window readers use to count a worker as alive. Heartbeat cadence must stay well below it.
const WORKER_TTL_SECONDS = 180; export const WORKER_TTL_SECONDS = 180;
export const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000;
const QueueWorkerSchema = new Schema<QueueWorkerDocument>( const QueueWorkerSchema = new Schema<QueueWorkerDocument>(
{ {
@@ -9,7 +9,7 @@ import {
} from "@/domain/status-pages/status-page.type.js"; } from "@/domain/status-pages/status-page.type.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import { NormalizeData } from "@/utils/dataUtils.js"; import { NormalizeData } from "@/utils/dataUtils.js";
export interface IStatusPageService { export interface IStatusPageService {
@@ -1,4 +1,4 @@
import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Monitor } from "@/domain/monitors/monitor.types.js";
import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js"; import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js";
export const StatusPageTypes = ["uptime", "infrastructure"] as const; export const StatusPageTypes = ["uptime", "infrastructure"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number]; export type StatusPageType = (typeof StatusPageTypes)[number];
@@ -2,6 +2,7 @@ import { IUsersRepository } from "@/domain/users/user.repository.interface.js";
import { UserModel, type UserDocument } from "@/domain/users/user.model.js"; import { UserModel, type UserDocument } from "@/domain/users/user.model.js";
import type { User, UserProfileImage } from "@/domain/users/user.type.js"; import type { User, UserProfileImage } from "@/domain/users/user.type.js";
import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; import { GenerateAvatarImage } from "@/utils/imageProcessing.js";
import { ParseBoolean } from "@/utils/utils.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js";
const SERVICE_NAME = "MongoUsersRepository"; const SERVICE_NAME = "MongoUsersRepository";
@@ -71,7 +72,7 @@ class MongoUsersRepository implements IUsersRepository {
findById = async (id: string) => { findById = async (id: string) => {
const user = await UserModel.findById(id).select("-password").select("-profileImage"); const user = await UserModel.findById(id).select("-password").select("-profileImage");
if (!user) { if (!user) {
throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); throw new Error("User not found");
} }
return this.toEntity(user); return this.toEntity(user);
@@ -86,8 +87,9 @@ class MongoUsersRepository implements IUsersRepository {
const candidateUser = { ...patch }; const candidateUser = { ...patch };
let unsetFields: Record<string, 1> | undefined; let unsetFields: Record<string, 1> | undefined;
if (candidateUser.deleteProfileImage === true) { if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
unsetFields = { profileImage: 1, avatarImage: 1 }; unsetFields = { profileImage: 1, avatarImage: 1 };
delete candidateUser.deleteProfileImage;
} else if (file) { } else if (file) {
// 1. Save the full size image // 1. Save the full size image
candidateUser.profileImage = { candidateUser.profileImage = {
+6 -2
View File
@@ -24,7 +24,7 @@ export interface IUserService {
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>; createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise<User>;
loginUser(email: string, password: string): Promise<{ user: User; token: string }>; loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
editUser( editUser(
updates: Partial<User & { newPassword?: string; deleteProfileImage?: boolean }>, updates: Partial<User & { newPassword?: string }>,
file: Express.Multer.File | null, file: Express.Multer.File | null,
currentUserId: string, currentUserId: string,
currentUserEmail: string currentUserEmail: string
@@ -103,6 +103,10 @@ export class UserService implements IUserService {
this.teamsRepository = teamsRepository; this.teamsRepository = teamsRepository;
} }
get serviceName() {
return UserService.SERVICE_NAME;
}
issueToken = (payload: Partial<User>, appSettings: EnvConfig) => { issueToken = (payload: Partial<User>, appSettings: EnvConfig) => {
return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL }); return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL });
}; };
@@ -238,7 +242,7 @@ export class UserService implements IUserService {
}; };
editUser = async ( editUser = async (
updates: Partial<User & { newPassword?: string; deleteProfileImage?: boolean }>, updates: Partial<User & { newPassword?: string }>,
file: Express.Multer.File | null, file: Express.Multer.File | null,
currentUserId: string, currentUserId: string,
currentUserEmail: string currentUserEmail: string
+1 -1
View File
@@ -27,7 +27,7 @@ const startApp = async () => {
const env = validateEnv(logger); const env = validateEnv(logger);
logger.setLogLevel(env.LOG_LEVEL); logger.setLogLevel(env.LOG_LEVEL);
const settingsService = new SettingsService(env); const settingsService = new SettingsService(env);
const envSettings = settingsService.getSettings(); const envSettings = settingsService.loadSettings();
const { queueMode, queuePrimaryProcesses } = envSettings; const { queueMode, queuePrimaryProcesses } = envSettings;
logger.info({ logger.info({
+4
View File
@@ -51,6 +51,10 @@ export class BufferService implements IBufferService {
}); });
} }
get serviceName() {
return BufferService.SERVICE_NAME;
}
addToBuffer(check: Check) { addToBuffer(check: Check) {
try { try {
this.buffer.push(check); this.buffer.push(check);
+5
View File
@@ -56,6 +56,10 @@ export class EmailService implements IEmailService {
this.init(); this.init();
} }
get serviceName() {
return EmailService.SERVICE_NAME;
}
init = () => { init = () => {
const loadTemplate = (templateName: string) => { const loadTemplate = (templateName: string) => {
try { try {
@@ -75,6 +79,7 @@ export class EmailService implements IEmailService {
this.templateLookup = { this.templateLookup = {
welcomeEmailTemplate: loadTemplate("welcomeEmail"), welcomeEmailTemplate: loadTemplate("welcomeEmail"),
employeeActivationTemplate: loadTemplate("employeeActivation"), employeeActivationTemplate: loadTemplate("employeeActivation"),
noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"),
passwordResetTemplate: loadTemplate("passwordReset"), passwordResetTemplate: loadTemplate("passwordReset"),
testEmailTemplate: loadTemplate("testEmailTemplate"), testEmailTemplate: loadTemplate("testEmailTemplate"),
unifiedNotificationTemplate: loadTemplate("unifiedNotification"), unifiedNotificationTemplate: loadTemplate("unifiedNotification"),
+7 -2
View File
@@ -1,6 +1,6 @@
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/domain/geo-checks/geo-check.type.js";
import { supportsGeoCheck, type HttpStatusCode } from "@/domain/monitors/monitor.type.js"; import { supportsGeoCheck, type HttpStatusCode } from "@/domain/monitors/monitor.types.js";
import { MonitorType } from "@/domain/monitors/monitor.type.js"; import { MonitorType } from "@/domain/monitors/monitor.types.js";
import type { ILogger } from "@/utils/logger.js"; import type { ILogger } from "@/utils/logger.js";
import got from "got"; import got from "got";
import { isStatusUp } from "@/service/network/utils.js"; import { isStatusUp } from "@/service/network/utils.js";
@@ -61,6 +61,7 @@ interface GlobalPingProbeResult {
} }
export interface IGlobalPingService { export interface IGlobalPingService {
readonly serviceName: string;
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>; createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise<GeoCheckResult[]>; pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise<GeoCheckResult[]>;
} }
@@ -74,6 +75,10 @@ export class GlobalPingService implements IGlobalPingService {
this.logger = logger; this.logger = logger;
} }
get serviceName() {
return GlobalPingService.SERVICE_NAME;
}
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> { async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
try { try {
if (!supportsGeoCheck(monitorType)) { if (!supportsGeoCheck(monitorType)) {
@@ -1,4 +1,4 @@
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import jmespath from "jmespath"; import jmespath from "jmespath";
import RE2 from "re2"; import RE2 from "re2";
type JmesPath = typeof jmespath; type JmesPath = typeof jmespath;
+2 -3
View File
@@ -1,10 +1,9 @@
import { DNSStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { DNSStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import type { Resolver } from "dns/promises"; import type { Resolver } from "dns/promises";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js";
import { NETWORK_ERROR } from "@/types/network.js";
const SERVICE_NAME = "DNSProvider"; const SERVICE_NAME = "DNSProvider";
+3 -3
View File
@@ -1,11 +1,11 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import Dockerode from "dockerode"; import Dockerode from "dockerode";
import { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js";
import { NETWORK_ERROR } from "@/types/network.js";
type DockerodeType = typeof Dockerode; type DockerodeType = typeof Dockerode;
const SERVICE_NAME = "DockerProvider"; const SERVICE_NAME = "DockerProvider";
+2 -2
View File
@@ -1,9 +1,9 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { GameDig } from "gamedig"; import { GameDig } from "gamedig";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/types/network.js"; import { NETWORK_ERROR } from "@/service/network/utils.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
type GameDigType = typeof GameDig; type GameDigType = typeof GameDig;
+1 -1
View File
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/network/utils.js"; import { timeRequest } from "@/service/network/utils.js";
import * as grpc from "@grpc/grpc-js"; import * as grpc from "@grpc/grpc-js";
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { HttpProvider } from "@/service/network/HttpProvider.js"; import { HttpProvider } from "@/service/network/HttpProvider.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
+2 -3
View File
@@ -5,9 +5,8 @@ import { HttpStatusPayload } from "@/types/network.js";
import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js";
import { Agent as HttpsAgent } from "https"; import { Agent as HttpsAgent } from "https";
import { Agent as HttpAgent } from "http"; import { Agent as HttpAgent } from "http";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { isStatusUp } from "@/service/network/utils.js"; import { NETWORK_ERROR, isStatusUp } from "@/service/network/utils.js";
import { NETWORK_ERROR } from "@/types/network.js";
import CacheableLookup from "cacheable-lookup"; import CacheableLookup from "cacheable-lookup";
export class HttpProvider implements IStatusProvider<HttpStatusPayload> { export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
@@ -1,4 +1,4 @@
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js";
export interface IStatusProvider<T> { export interface IStatusProvider<T> {
@@ -1,6 +1,6 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/domain/monitors/monitor.types.js";
import { HttpProvider } from "@/service/network/HttpProvider.js"; import { HttpProvider } from "@/service/network/HttpProvider.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
+1 -1
View File
@@ -1,6 +1,6 @@
import { PingStatusPayload } from "@/types/network.js"; import { PingStatusPayload } from "@/types/network.js";
import { IStatusProvider } from "./IStatusProvider.js"; import { IStatusProvider } from "./IStatusProvider.js";
import { MonitorType, Monitor } from "@/domain/monitors/monitor.type.js"; import { MonitorType, Monitor } from "@/domain/monitors/monitor.types.js";
import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import ping from "ping"; import ping from "ping";
+2 -3
View File
@@ -1,9 +1,8 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js";
import { NETWORK_ERROR } from "@/types/network.js";
import * as net from "net"; import * as net from "net";
type NetType = typeof net; type NetType = typeof net;
@@ -1,9 +1,8 @@
import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js";
import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js";
import { NETWORK_ERROR } from "@/types/network.js";
import type WebSocket from "ws"; import type WebSocket from "ws";
type WebSocketConstructor = typeof WebSocket; type WebSocketConstructor = typeof WebSocket;
+4 -1
View File
@@ -1,4 +1,4 @@
import type { HttpStatusCode } from "@/domain/monitors/monitor.type.js"; import type { HttpStatusCode } from "@/domain/monitors/monitor.types.js";
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => { export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
const start = process.hrtime.bigint(); const start = process.hrtime.bigint();
@@ -12,6 +12,9 @@ export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ res
} }
}; };
export const NETWORK_ERROR = 5000;
export const PING_ERROR = 5001;
export const isStatusUp = (statusCode: number | undefined, customUpCodes: HttpStatusCode[] = []): boolean => { export const isStatusUp = (statusCode: number | undefined, customUpCodes: HttpStatusCode[] = []): boolean => {
if (statusCode === undefined) return false; if (statusCode === undefined) return false;
return (statusCode >= 200 && statusCode < 300) || customUpCodes.includes(statusCode); return (statusCode >= 200 && statusCode < 300) || customUpCodes.includes(statusCode);
+7 -2
View File
@@ -1,13 +1,14 @@
import type { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import type { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js";
import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js"; import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js";
import type { AxiosStatic } from "axios"; import type { AxiosStatic } from "axios";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/types/network.js"; import { NETWORK_ERROR } from "@/service/network/utils.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { IStatusProvider } from "./network/IStatusProvider.js"; import { IStatusProvider } from "./network/IStatusProvider.js";
const SERVICE_NAME = "NetworkService"; const SERVICE_NAME = "NetworkService";
export interface INetworkService { export interface INetworkService {
readonly serviceName: string;
requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>; requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>>;
requestWebhook( requestWebhook(
type: string, type: string,
@@ -38,6 +39,10 @@ export class NetworkService implements INetworkService {
this.logger = logger; this.logger = logger;
} }
get serviceName(): string {
return NetworkService.SERVICE_NAME;
}
// Main entry point // Main entry point
async requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>> { async requestStatus<T extends MonitorType>(monitor: Monitor & { type: T }): Promise<MonitorStatusResponse<MonitorPayloadMap[T]>> {
const provider = this.providers.find((p) => p.supports(monitor.type)); const provider = this.providers.find((p) => p.supports(monitor.type));
+6 -2
View File
@@ -1,7 +1,7 @@
import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js"; import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js"; import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js";
import type { Monitor, MonitorStatus } from "@/domain/monitors/monitor.type.js"; import type { Monitor, MonitorStatus } from "@/domain/monitors/monitor.types.js";
import type { import type {
DockerStatusPayload, DockerStatusPayload,
GameStatusPayload, GameStatusPayload,
@@ -17,7 +17,7 @@ import type {
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import type { HardwareStatusMetrics } from "@/types/network.js"; import type { HardwareStatusMetrics } from "@/types/network.js";
import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.type.js"; import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.types.js";
const SERVICE_NAME = "StatusService"; const SERVICE_NAME = "StatusService";
const HARDWARE_ALERT_COUNTER_START = 5; const HARDWARE_ALERT_COUNTER_START = 5;
@@ -57,6 +57,10 @@ export class StatusService implements IStatusService {
this.monitorStatsRepository = monitorStatsRepository; this.monitorStatsRepository = monitorStatsRepository;
} }
get serviceName() {
return StatusService.SERVICE_NAME;
}
async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) { async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) {
try { try {
await this.monitorStatsRepository.updateByMonitorId(monitor.id, { await this.monitorStatsRepository.updateByMonitorId(monitor.id, {
+58
View File
@@ -0,0 +1,58 @@
<!-- name -->
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>
We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we
<b>truly appreciate</b> having you as part of our community.
</p>
<p>
If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and
supports our ongoing improvements.
</p>
G2 Link: TBD
<p>Thank you for taking the time to share your thoughts - we greatly appreciate it!</p>
Checkmate Team
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+66
View File
@@ -0,0 +1,66 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>Message from Checkmate Service</mj-text
>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="green"
>No incidents this week!</mj-text
>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>There were no incidents this week. Good job!</p>
<p><b>Current monitors:</b></p>
<p><b>Google:</b> 100% availability</p>
<p><b>Canada.ca:</b>100% availability</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+1 -3
View File
@@ -9,12 +9,10 @@ import type {
GotTimings, GotTimings,
ILighthouseAudit, ILighthouseAudit,
} from "@/domain/checks/check.type.js"; } from "@/domain/checks/check.type.js";
import type { DnsRecordType, Monitor, MonitorMatchMethod, MonitorStatus, MonitorType } from "@/domain/monitors/monitor.type.js"; import type { DnsRecordType, Monitor, MonitorMatchMethod, MonitorStatus, MonitorType } from "@/domain/monitors/monitor.types.js";
import type { QueryResult } from "gamedig"; import type { QueryResult } from "gamedig";
export const NETWORK_ERROR = 5000;
export interface MonitorStatusResponse< export interface MonitorStatusResponse<
T = T =
| HttpStatusPayload | HttpStatusPayload
-8
View File
@@ -1,8 +0,0 @@
export const DateRanges = ["recent", "hour", "day", "week", "month", "all"] as const;
export type DateRange = (typeof DateRanges)[number];
export const SortOrders = ["asc", "desc"] as const;
export type SortOrder = (typeof SortOrders)[number];
export const CheckFilters = ["all", "up", "down", "resolve"] as const;
export type CheckFilter = (typeof CheckFilters)[number];
+57 -43
View File
@@ -1,7 +1,6 @@
import type { GroupedCheck, HasResponseTime, NormalizedCheck, NormalizedUptimeCheck } from "@/domain/checks/check.type.js"; import type { GroupedCheck, HasResponseTime, NormalizedCheck, NormalizedUptimeCheck } from "@/domain/checks/check.type.js";
import { type DateRange } from "@/types/query.js";
export const getDateForRange = (dateRange: DateRange): Date => { export const getDateForRange = (dateRange: string): Date | undefined => {
const now = Date.now(); const now = Date.now();
switch (dateRange) { switch (dateRange) {
case "recent": case "recent":
@@ -15,63 +14,78 @@ export const getDateForRange = (dateRange: DateRange): Date => {
case "month": case "month":
return new Date(now - 30 * 24 * 60 * 60 * 1000); // 30 days return new Date(now - 30 * 24 * 60 * 60 * 1000); // 30 days
case "all": case "all":
return new Date(0); return undefined;
default: default:
return new Date(0); return undefined;
} }
}; };
export const getDateFormat = (dateRange: DateRange): string => { const calculatePercentile = <T extends HasResponseTime>(arr: T[], percentile: number): number => {
const formatLookup = { const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
hour: "%Y-%m-%dT%H:%M:00Z",
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
all: "%Y-%m-%dT00:00:00Z",
};
return formatLookup[dateRange];
};
const percentileBy = <T>(arr: T[], percentile: number, value: (item: T) => number): number => {
const sorted = arr.slice().sort((a, b) => value(a) - value(b));
const index = (percentile / 100) * (sorted.length - 1); const index = (percentile / 100) * (sorted.length - 1);
const lower = Math.floor(index); const lower = Math.floor(index);
const upper = lower + 1; const upper = lower + 1;
const weight = index % 1; const weight = index % 1;
if (upper >= sorted.length) return value(sorted[lower]!); if (upper >= sorted.length) return sorted[lower]!.responseTime;
return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight; return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight;
}; };
const rescale = (raw: number, min: number, max: number, rangeMin: number, rangeMax: number): number => { const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => {
const scaled = rangeMin + ((raw - min) * (rangeMax - rangeMin)) / (max - min); const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime);
return Math.max(rangeMin, Math.min(rangeMax, scaled)); const index = (percentile / 100) * (sorted.length - 1);
const lower = Math.floor(index);
const upper = lower + 1;
const weight = index % 1;
if (upper >= sorted.length) return sorted[lower]!.avgResponseTime;
return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight;
}; };
export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => { export const NormalizeData = <T extends HasResponseTime>(checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck<T>[] => {
const original = (check: T) => (typeof check.responseTime === "number" ? check.responseTime : 0); if (checks.length > 1) {
if (checks.length <= 1) { const min = calculatePercentile(checks, 0);
return checks.map((check) => ({ ...check, originalResponseTime: original(check) })); const max = calculatePercentile(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min);
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
responseTime: normalizedResponseTime,
originalResponseTime: originalResponseTime,
};
});
return normalizedChecks;
} else {
return checks.map((check) => {
const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0;
return { ...check, originalResponseTime: originalResponseTime };
});
} }
const min = percentileBy(checks, 0, (c) => c.responseTime);
const max = percentileBy(checks, 95, (c) => c.responseTime);
return checks.map((check) => ({
...check,
responseTime: rescale(check.responseTime, min, max, rangeMin, rangeMax),
originalResponseTime: original(check),
}));
}; };
export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => { export const NormalizeDataUptimeDetails = <T extends GroupedCheck>(checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck<T>[] => {
const original = (check: T) => (typeof check.avgResponseTime === "number" ? check.avgResponseTime : 0); if (checks.length > 1) {
if (checks.length <= 1) { const min = calculatePercentileUptimeDetails(checks, 0);
return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) })); const max = calculatePercentileUptimeDetails(checks, 95);
const normalizedChecks = checks.map((check) => {
const originalResponseTime = check.avgResponseTime;
let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min);
normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime));
return {
...check,
avgResponseTime: normalizedResponseTime,
originalAvgResponseTime: originalResponseTime,
};
});
return normalizedChecks;
} else {
return checks.map((check) => {
return { ...check, originalAvgResponseTime: check.avgResponseTime };
});
} }
const min = percentileBy(checks, 0, (c) => c.avgResponseTime);
const max = percentileBy(checks, 95, (c) => c.avgResponseTime);
return checks.map((check) => ({
...check,
avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax),
originalAvgResponseTime: original(check),
}));
}; };
+5
View File
@@ -19,6 +19,7 @@ interface LogEntry extends LogConfig {
} }
export interface ILogger { export interface ILogger {
readonly serviceName: string;
info(config: LogConfig): void; info(config: LogConfig): void;
warn(config: LogConfig): void; warn(config: LogConfig): void;
error(config: LogConfig): void; error(config: LogConfig): void;
@@ -96,6 +97,10 @@ class Logger implements ILogger {
}); });
} }
get serviceName() {
return Logger.SERVICE_NAME;
}
info(config: LogConfig) { info(config: LogConfig) {
const logEntry = this.buildLogEntry("info", config); const logEntry = this.buildLogEntry("info", config);
this.cacheLog(logEntry); this.cacheLog(logEntry);
+17
View File
@@ -0,0 +1,17 @@
export const ParseBoolean = (value: boolean | string | null | undefined) => {
if (value === true || value === "true") {
return true;
} else if (value === false || value === "false" || value === null || value === undefined) {
return false;
}
};
export const getTokenFromHeaders = (headers: Record<string, string>) => {
const authorizationHeader = headers.authorization;
if (!authorizationHeader) throw new Error("No auth headers");
const parts = authorizationHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers");
return parts[1];
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js";
import { MonitorEvaluation } from "@/worker/worker.interface.js"; import { MonitorEvaluation } from "@/worker/worker.interface.js";
import { Check } from "@/domain/checks/check.type.js"; import { Check } from "@/domain/checks/check.type.js";
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import { IMonitorStatusPolicy } from "@/worker/worker.monitor-status-policy.js"; import { IMonitorStatusPolicy } from "@/worker/worker.monitor-status-policy.js";
import { IStatusService } from "@/service/statusService.js"; import { IStatusService } from "@/service/statusService.js";
+2 -2
View File
@@ -1,11 +1,11 @@
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import { MonitorEvaluation } from "@/worker/worker.interface.js"; import { MonitorEvaluation } from "@/worker/worker.interface.js";
import { IMaintenanceWindowsRepository } from "@/domain/maintenance-windows/maintenance-window.repository.interface.js"; import { IMaintenanceWindowsRepository } from "@/domain/maintenance-windows/maintenance-window.repository.interface.js";
import { isWindowActive } from "@/utils/maintenanceWindow.js"; import { isWindowActive } from "@/utils/maintenanceWindow.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { IBufferService } from "@/service/bufferService.js"; import { IBufferService } from "@/service/bufferService.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { supportsGeoCheck } from "@/domain/monitors/monitor.type.js"; import { supportsGeoCheck } from "@/domain/monitors/monitor.types.js";
import { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js";
const SERVICE_NAME = "CheckPipeline"; const SERVICE_NAME = "CheckPipeline";
+1 -1
View File
@@ -1,4 +1,4 @@
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import { Check } from "@/domain/checks/check.type.js"; import { Check } from "@/domain/checks/check.type.js";
import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
+63 -52
View File
@@ -11,13 +11,14 @@ import { ICheckPipeline } from "@/worker/worker.check-pipeline.js";
import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js"; import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js";
import { IWorkerHelper } from "@/worker/worker.helper.js"; import { IWorkerHelper } from "@/worker/worker.helper.js";
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js";
import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js";
import { JobScheduler } from "@/worker/worker.job-scheduler.js"; import { JobScheduler } from "@/worker/worker.job-scheduler.js";
import { IBufferService } from "@/service/bufferService.js"; import { IBufferService } from "@/service/bufferService.js";
import { POLL_MS } from "@/worker/worker.job-scheduler.js";
const SERVICE_NAME = "JobQueue"; const SERVICE_NAME = "JobQueue";
const POLL_MS = 250; // base poll interval while a loop is actively claiming work
const POLL_MAX_MS = 5000; // back off poll interval to 5s if no jobs const POLL_MAX_MS = 5000; // back off poll interval to 5s if no jobs
const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window
const LOCK_RENEW_MS = LOCK_MS / 3; // renew an in-flight job's lock at 1/3 the lease, so it survives two missed renewals const LOCK_RENEW_MS = LOCK_MS / 3; // renew an in-flight job's lock at 1/3 the lease, so it survives two missed renewals
const DRAIN_TIMEOUT_MS = 25_000; const DRAIN_TIMEOUT_MS = 25_000;
const DRAIN_POLL_MS = 200; const DRAIN_POLL_MS = 200;
@@ -30,25 +31,6 @@ const CONCURRENCY: Record<JobType, number> = {
"cleanup-retention": 1, "cleanup-retention": 1,
}; };
export interface DBQueueWorkerDependencies {
logger: ILogger; // forwarded to super
isDbConnected: () => boolean;
jobsRepository: IJobsRepository; // forwarded
monitorsRepository: IMonitorsRepository; // forwarded
checksRepository: IChecksRepository;
checkService: ICheckService;
bufferService: IBufferService;
checkProducer: ICheckProducer;
checkEvaluator: ICheckEvaluator;
geoCheckPipeline: ICheckPipeline;
dispatcher: IReactorDispatcher;
helper: IWorkerHelper;
queueWorkersRepository: IQueueWorkersRepository; // forwarded
queueMode: QueueMode; // forwarded
queuePrimaryProcesses: boolean; // consumed immediately
workerId: string;
}
export class DBQueueWorker extends JobScheduler implements IQueueWorker { export class DBQueueWorker extends JobScheduler implements IQueueWorker {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
@@ -63,39 +45,68 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
"cleanup-retention": 0, "cleanup-retention": 0,
}; };
private checksRepository: IChecksRepository; constructor(
private checkProducer: ICheckProducer; logger: ILogger, // forwarded to super
private checkService: ICheckService; private isDbConnected: () => boolean,
private checkEvaluator: ICheckEvaluator; jobsRepository: IJobsRepository, // forwarded
private dispatcher: IReactorDispatcher; monitorsRepository: IMonitorsRepository, // forwarded
private geoCheckPipeline: ICheckPipeline; private checksRepository: IChecksRepository,
private helper: IWorkerHelper; private checkService: ICheckService,
private bufferService: IBufferService; private bufferService: IBufferService,
private isDbConnected: () => boolean; private checkProducer: ICheckProducer,
private checkEvaluator: ICheckEvaluator,
constructor(dependencies: DBQueueWorkerDependencies) { private geoCheckPipeline: ICheckPipeline,
super( private dispatcher: IReactorDispatcher,
dependencies.jobsRepository, private helper: IWorkerHelper,
dependencies.queueWorkersRepository, queueWorkersRepository: IQueueWorkersRepository, // forwarded
dependencies.monitorsRepository, queueMode: QueueMode, // forwarded
dependencies.queueMode, queuePrimaryProcesses: boolean, // consumed immediately
dependencies.logger, workerId: string
dependencies.workerId ) {
); super(jobsRepository, queueWorkersRepository, monitorsRepository, queueMode, logger, workerId);
this.processesJobs = dependencies.queuePrimaryProcesses === true || dependencies.queueMode === "worker"; this.processesJobs = queuePrimaryProcesses === true || queueMode === "worker";
this.checksRepository = dependencies.checksRepository;
this.checkProducer = dependencies.checkProducer;
this.checkService = dependencies.checkService;
this.checkEvaluator = dependencies.checkEvaluator;
this.dispatcher = dependencies.dispatcher;
this.geoCheckPipeline = dependencies.geoCheckPipeline;
this.helper = dependencies.helper;
this.bufferService = dependencies.bufferService;
this.isDbConnected = dependencies.isDbConnected;
} }
static async create(dependencies: DBQueueWorkerDependencies): Promise<DBQueueWorker> { get serviceName() {
const instance = new DBQueueWorker(dependencies); return DBQueueWorker.SERVICE_NAME;
}
static async create(
logger: ILogger,
isDbConnected: () => boolean,
jobsRepository: IJobsRepository,
monitorsRepository: IMonitorsRepository,
checksRepository: IChecksRepository,
checkService: ICheckService,
bufferService: IBufferService,
checkProducer: ICheckProducer,
checkEvaluator: ICheckEvaluator,
geoCheckPipeline: ICheckPipeline,
dispatcher: IReactorDispatcher,
helper: IWorkerHelper,
queueWorkersRepository: IQueueWorkersRepository,
queueMode: QueueMode,
queuePrimaryProcesses: boolean,
workerId: string
): Promise<DBQueueWorker> {
const instance = new DBQueueWorker(
logger,
isDbConnected,
jobsRepository,
monitorsRepository,
checksRepository,
checkService,
bufferService,
checkProducer,
checkEvaluator,
geoCheckPipeline,
dispatcher,
helper,
queueWorkersRepository,
queueMode,
queuePrimaryProcesses,
workerId
);
await instance.init(); await instance.init();
return instance; return instance;
} }
+5
View File
@@ -12,6 +12,7 @@ import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
export interface IWorkerHelper { export interface IWorkerHelper {
readonly serviceName: string;
getCleanupOrphanedJob(): () => Promise<void>; getCleanupOrphanedJob(): () => Promise<void>;
getCleanupRetentionJob(): () => Promise<void>; getCleanupRetentionJob(): () => Promise<void>;
} }
@@ -68,6 +69,10 @@ export class WorkerHelper implements IWorkerHelper {
this.geoChecksRepository = geoChecksRepository; this.geoChecksRepository = geoChecksRepository;
} }
get serviceName() {
return WorkerHelper.SERVICE_NAME;
}
getCleanupOrphanedJob = () => { getCleanupOrphanedJob = () => {
return async () => { return async () => {
try { try {
+3 -1
View File
@@ -1,4 +1,4 @@
import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Monitor } from "@/domain/monitors/monitor.types.js";
import { Check } from "@/domain/checks/check.type.js"; import { Check } from "@/domain/checks/check.type.js";
import { JobType } from "@/domain/jobs/job.type.js"; import { JobType } from "@/domain/jobs/job.type.js";
import { MonitorPayloadMap, MonitorStatusResponse, StatusChangeResult } from "@/types/network.js"; import { MonitorPayloadMap, MonitorStatusResponse, StatusChangeResult } from "@/types/network.js";
@@ -67,6 +67,7 @@ export type WorkerHealth = {
inFlight: number; // total in-flight jobs across types inFlight: number; // total in-flight jobs across types
}; };
export interface IJobScheduler { export interface IJobScheduler {
readonly serviceName: string;
addJob(monitorId: string, monitor: Monitor): Promise<void>; addJob(monitorId: string, monitor: Monitor): Promise<void>;
deleteJob(monitor: Monitor): Promise<void>; deleteJob(monitor: Monitor): Promise<void>;
pauseJob(monitor: Monitor): Promise<void>; pauseJob(monitor: Monitor): Promise<void>;
@@ -82,6 +83,7 @@ export interface IJobScheduler {
} }
export interface IQueueWorker extends IJobScheduler { export interface IQueueWorker extends IJobScheduler {
readonly serviceName: string;
getHealth(): WorkerHealth; getHealth(): WorkerHealth;
countDueBacklog(): Promise<number>; countDueBacklog(): Promise<number>;
countAliveWorkers(): Promise<number>; countAliveWorkers(): Promise<number>;
+8 -4
View File
@@ -1,16 +1,17 @@
import { IJobScheduler } from "@/worker/worker.interface.js"; import { IJobScheduler } from "@/worker/worker.interface.js";
import { IJobsRepository } from "@/domain/jobs/job.repository.interface.js"; import { IJobsRepository } from "@/domain/jobs/job.repository.interface.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.type.js"; import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.types.js";
import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js"; import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js";
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js"; import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js";
import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js";
import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
export const POLL_MS = 250; // base poll interval while a loop is actively claiming work const POLL_MS = 250; // base poll interval while a loop is actively claiming work
const SERVICE_NAME = "JobScheduler"; const SERVICE_NAME = "JobScheduler";
const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window
const HEARTBEAT_MS = WORKER_STALE_MS / 3; // Worker can miss two beats without being considered stale const HEARTBEAT_MS = WORKER_STALE_MS / 3; // Worker can miss two beats without being considered stale
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -25,6 +26,9 @@ export class JobScheduler implements IJobScheduler {
protected readonly workerId: string // same id MongoJobsRepository stamps into lockedBy protected readonly workerId: string // same id MongoJobsRepository stamps into lockedBy
) {} ) {}
get serviceName() {
return JobScheduler.SERVICE_NAME;
}
private heartbeatInFlight: Promise<void> = Promise.resolve(); private heartbeatInFlight: Promise<void> = Promise.resolve();
protected processesJobs = false; // scheduler-only by default; DBQueueWorker flips this on when it runs job loops protected processesJobs = false; // scheduler-only by default; DBQueueWorker flips this on when it runs job loops
@@ -187,7 +191,7 @@ export class JobScheduler implements IJobScheduler {
// Seed queue // Seed queue
protected reconcile = async () => { protected reconcile = async () => {
const now = Date.now(); const now = Date.now();
const monitors = await this.monitorsRepository.findAll(); const monitors = (await this.monitorsRepository.findAll()) ?? [];
for (const monitor of monitors) { for (const monitor of monitors) {
await this.jobsRepository.upsertJob(this.toCheckJob(monitor, now)); await this.jobsRepository.upsertJob(this.toCheckJob(monitor, now));
if (supportsGeoCheck(monitor.type) && monitor.geoCheckEnabled) { if (supportsGeoCheck(monitor.type) && monitor.geoCheckEnabled) {
@@ -1,6 +1,6 @@
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/domain/monitors/monitor.repository.interface.ts"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/domain/monitors/monitor.repository.interface.ts";
import type { CheckSnapshot } from "../../src/domain/checks/check.type.ts"; import type { CheckSnapshot } from "../../src/domain/checks/check.type.ts";
import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts"; import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.types.ts";
export class InMemoryMonitorsRepository implements IMonitorsRepository { export class InMemoryMonitorsRepository implements IMonitorsRepository {
private monitors: Monitor[] = []; private monitors: Monitor[] = [];
+1 -1
View File
@@ -10,7 +10,7 @@ import { IncidentService } from "../../src/domain/incidents/incident.service.ts"
import { InMemoryMonitorsRepository } from "./InMemoryMonitorsRepository.ts"; import { InMemoryMonitorsRepository } from "./InMemoryMonitorsRepository.ts";
import { InMemoryIncidentsRepository } from "./InMemoryIncidentsRepository.ts"; import { InMemoryIncidentsRepository } from "./InMemoryIncidentsRepository.ts";
import { createMockLogger } from "./createMockLogger.ts"; import { createMockLogger } from "./createMockLogger.ts";
import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.types.ts";
import type { MonitorStatusResponse } from "../../src/types/network.ts"; import type { MonitorStatusResponse } from "../../src/types/network.ts";
import type { Check } from "../../src/domain/checks/check.type.ts"; import type { Check } from "../../src/domain/checks/check.type.ts";
import type { MaintenanceWindow } from "../../src/domain/maintenance-windows/maintenance-window.type.ts"; import type { MaintenanceWindow } from "../../src/domain/maintenance-windows/maintenance-window.type.ts";
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import type { IStatusProvider } from "../../src/service/network/IStatusProvider.ts"; import type { IStatusProvider } from "../../src/service/network/IStatusProvider.ts";
import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.types.ts";
/** /**
* Shared contract tests for all IStatusProvider implementations. * Shared contract tests for all IStatusProvider implementations.
@@ -5,7 +5,7 @@ import { createMockLogger } from "../helpers/createMockLogger.ts";
import type { IMonitorsRepository } from "../../src/domain/monitors/monitor.repository.interface.ts"; import type { IMonitorsRepository } from "../../src/domain/monitors/monitor.repository.interface.ts";
import type { IUsersRepository } from "../../src/domain/users/user.repository.interface.ts"; import type { IUsersRepository } from "../../src/domain/users/user.repository.interface.ts";
import type { INotificationMessageBuilder } from "../../src/domain/notifications/notification.message-builder.ts"; import type { INotificationMessageBuilder } from "../../src/domain/notifications/notification.message-builder.ts";
import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.types.ts";
import type { MonitorActionDecision } from "../../src/worker/worker.helper.ts"; import type { MonitorActionDecision } from "../../src/worker/worker.helper.ts";
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
import { describe, expect, it, jest } from "@jest/globals"; import { describe, expect, it, jest } from "@jest/globals";
import { AdvancedMatcher } from "../../../../src/service/network/AdvancedMatcher.ts"; import { AdvancedMatcher } from "../../../../src/service/network/AdvancedMatcher.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../../src/types/network.ts"; import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
import { DNSProvider } from "../../../../src/service/network/DNSProvider.ts"; import { DNSProvider } from "../../../../src/service/network/DNSProvider.ts";
// ── Mocks ──────────────────────────────────────────────────────────────────── // ── Mocks ────────────────────────────────────────────────────────────────────
@@ -2,8 +2,8 @@ import { describe, expect, it, jest } from "@jest/globals";
import { DockerProvider } from "../../../../src/service/network/DockerProvider.ts"; import { DockerProvider } from "../../../../src/service/network/DockerProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/types/network.ts"; import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -2,8 +2,8 @@ import { describe, expect, it, jest } from "@jest/globals";
import { GameProvider } from "../../../../src/service/network/GameProvider.ts"; import { GameProvider } from "../../../../src/service/network/GameProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/types/network.ts"; import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
import { describe, expect, it, jest } from "@jest/globals"; import { describe, expect, it, jest } from "@jest/globals";
import { GrpcProvider } from "../../../../src/service/network/GrpcProvider.ts"; import { GrpcProvider } from "../../../../src/service/network/GrpcProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more