From 36cdb20dfc152307c6e309f04c65be8e35732c47 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 5 Jan 2026 17:58:47 +0000 Subject: [PATCH] types --- server/src/db/models/checks/Check.ts | 2 +- server/src/db/models/index.ts | 2 +- server/src/db/models/monitors/Monitor.ts | 22 +- server/src/db/models/monitors/MonitorStats.ts | 4 +- server/src/db/models/monitors/StatsDaily.ts | 3 +- server/src/db/models/monitors/StatsHourly.ts | 2 +- server/src/init/services.ts | 2 +- .../repositories/checks/IChecksRepository.ts | 5 + .../checks/MongoCheckRepository.ts | 69 +++ .../monitors/MongoMonitorRepository.ts | 1 - server/src/services/business/CheckService.ts | 3 +- .../src/services/business/MonitorService.ts | 559 +----------------- .../business/StatsAggregationService.ts | 20 +- server/src/services/business/TeamService.ts | 7 +- .../services/infrastructure/NetworkService.ts | 31 +- server/src/validation/checks.ts | 13 +- server/src/validation/monitor.ts | 8 +- 17 files changed, 159 insertions(+), 594 deletions(-) diff --git a/server/src/db/models/checks/Check.ts b/server/src/db/models/checks/Check.ts index 03c519c1d..c23fe888f 100644 --- a/server/src/db/models/checks/Check.ts +++ b/server/src/db/models/checks/Check.ts @@ -4,7 +4,7 @@ import { MonitorTypes, MonitorStatus, MonitorStatuses, -} from "../monitors/Monitor.js"; +} from "@/types/domain/index.js"; import type { Response } from "got"; export type GotTimings = Response["timings"]; export interface ITimingPhases { diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 0cacb729f..b6c3c2ed5 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -7,7 +7,7 @@ export type { IDockerContainerSummary, } from "./checks/Check.js"; -export { Monitor, MonitorStatuses, MonitorTypes } from "./monitors/Monitor.js"; +export { Monitor } from "./monitors/Monitor.js"; export type { IMonitor } from "./monitors/Monitor.js"; export { MonitorStats } from "./monitors/MonitorStats.js"; diff --git a/server/src/db/models/monitors/Monitor.ts b/server/src/db/models/monitors/Monitor.ts index 0037f63ce..47cde2a02 100644 --- a/server/src/db/models/monitors/Monitor.ts +++ b/server/src/db/models/monitors/Monitor.ts @@ -7,26 +7,8 @@ import { StatsDaily, StatsHourly, } from "@/db/models/index.js"; - -export const MonitorTypes = [ - "http", - "https", - "port", - "ping", - "infrastructure", - "pagespeed", - "docker", -] as const; -export type MonitorType = (typeof MonitorTypes)[number]; - -export const MonitorStatuses = [ - "up", - "down", - "paused", - "initializing", - "resuming", -] as const; -export type MonitorStatus = (typeof MonitorStatuses)[number]; +import type { MonitorType, MonitorStatus } from "@/types/domain/index.js"; +import { MonitorStatuses, MonitorTypes } from "@/types/domain/index.js"; export interface IMonitorThresholds { cpu?: number; diff --git a/server/src/db/models/monitors/MonitorStats.ts b/server/src/db/models/monitors/MonitorStats.ts index 15bda5578..bb164e1f4 100644 --- a/server/src/db/models/monitors/MonitorStats.ts +++ b/server/src/db/models/monitors/MonitorStats.ts @@ -1,5 +1,5 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; -import { MonitorStatus, MonitorStatuses } from "./Monitor.js"; +import mongoose, { Schema } from "mongoose"; +import { MonitorStatus, MonitorStatuses } from "@/types/domain/index.js"; export interface IMonitorStats extends mongoose.Document { monitorId: mongoose.Types.ObjectId; diff --git a/server/src/db/models/monitors/StatsDaily.ts b/server/src/db/models/monitors/StatsDaily.ts index dfaa4b445..c3ad612a6 100644 --- a/server/src/db/models/monitors/StatsDaily.ts +++ b/server/src/db/models/monitors/StatsDaily.ts @@ -1,5 +1,4 @@ -import mongoose, { Schema, Types } from "mongoose"; -import type { MonitorType } from "@/db/models/monitors/Monitor.js"; +import mongoose, { Schema } from "mongoose"; import type { IInfraCpu, IInfraDiskEntry, diff --git a/server/src/db/models/monitors/StatsHourly.ts b/server/src/db/models/monitors/StatsHourly.ts index 1f3bcda8c..5228433cd 100644 --- a/server/src/db/models/monitors/StatsHourly.ts +++ b/server/src/db/models/monitors/StatsHourly.ts @@ -1,5 +1,5 @@ import mongoose, { Schema, Types } from "mongoose"; -import type { MonitorType } from "@/db/models/monitors/Monitor.js"; +import type { MonitorType } from "@/types/domain/index.js"; // Common/base metrics across monitor types export interface IStatsBase { diff --git a/server/src/init/services.ts b/server/src/init/services.ts index b233b6add..0a45e2cdd 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -88,7 +88,7 @@ export const initServices = async () => { checksRepository ); const queueService = new QueueService(jobQueue); - const teamService = new TeamService(jobQueue); + const teamService = new TeamService(jobQueue, monitorRepository); const roleService = new RoleService(); const teamMemberService = new TeamMemberService(); const statusPageService = new StatusPageService(); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index ea5cd0399..713a4fcfa 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -15,6 +15,11 @@ export interface IChecksRepository { monitor: Monitor, startDate: Date ): Promise; + findDateRangeChecksByMonitor( + monitor: Monitor, + startDate: Date, + range: string + ): Promise; // Update // Delete } diff --git a/server/src/repositories/checks/MongoCheckRepository.ts b/server/src/repositories/checks/MongoCheckRepository.ts index f454058cf..ef63aba6b 100644 --- a/server/src/repositories/checks/MongoCheckRepository.ts +++ b/server/src/repositories/checks/MongoCheckRepository.ts @@ -3,6 +3,7 @@ import { CheckEntity, AggregateCheck, Monitor } from "@/types/domain/index.js"; import type { ICheck } from "@/db/models/index.js"; import { Check } from "@/db/models/index.js"; import mongoose from "mongoose"; +import { StatsHourly, StatsDaily } from "@/db/models/index.js"; export type LatestChecksByMonitor = Array<{ id: string; @@ -513,6 +514,74 @@ class MongoCheckRepository implements IChecksRepository { return checks.map(this.toAggregateCheck); }; + + findDateRangeChecksByMonitor = async ( + monitor: Monitor, + startDate: Date, + range: string + ) => { + const useDaily = range === "30d"; + const Model = useDaily ? StatsDaily : StatsHourly; + const endDate = new Date(); + + const docs = await Model.find({ + monitorId: monitor.id, + windowStart: { $gte: startDate, $lt: endDate }, + }) + .sort({ windowStart: -1 }) + .lean(); + + const checks = (docs as any[]).map((d) => { + const base: any = { + _id: new Date(d.windowStart).toISOString(), + count: d.count ?? 0, + avgResponseTime: d.avgResponseTime ?? 0, + upChecks: d.upChecks ?? 0, + downChecks: d.downChecks ?? 0, + avgResponseTimeUp: d.avgResponseTimeUp ?? null, + avgResponseTimeDown: d.avgResponseTimeDown ?? null, + }; + if (monitor.type === "pagespeed") { + return { + ...base, + accessibility: d.accessibility, + seo: d.seo, + bestPractices: d.bestPractices, + performance: d.performance, + cls: d.cls, + si: d.si, + fcp: d.fcp, + lcp: d.lcp, + tbt: d.tbt, + }; + } + if (monitor.type === "infrastructure") { + return { + ...base, + cpu: d.cpu, + memory: d.memory, + disk: d.disk, + host: d.host, + net: d.net, + }; + } + if (monitor.type === "docker") { + return { + ...base, + totalContainers: d.dockerTotalContainers, + runningContainers: d.dockerRunningContainers, + healthyContainers: d.dockerHealthyContainers, + totalExposedPorts: d.dockerTotalExposedPorts, + uniqueImages: d.dockerUniqueImages, + runningPercent: d.dockerRunningPercent, + healthyPercent: d.dockerHealthyPercent, + }; + } + return base; + }); + + return checks; + }; } export default MongoCheckRepository; diff --git a/server/src/repositories/monitors/MongoMonitorRepository.ts b/server/src/repositories/monitors/MongoMonitorRepository.ts index f6fc700e2..78cc2c2de 100644 --- a/server/src/repositories/monitors/MongoMonitorRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorRepository.ts @@ -1,6 +1,5 @@ import { Monitor, IMonitor } from "@/db/models/index.js"; import { IMonitorRepository, TeamQueryConfig } from "@/repositories/index.js"; -import { IImportedMonitor } from "@/services/business/MonitorService.js"; import type { Monitor as MonitorEntity } from "@/types/domain/monitor.js"; import mongoose from "mongoose"; class MongoMonitorRepository implements IMonitorRepository { diff --git a/server/src/services/business/CheckService.ts b/server/src/services/business/CheckService.ts index a0d30de53..a210d35ed 100644 --- a/server/src/services/business/CheckService.ts +++ b/server/src/services/business/CheckService.ts @@ -1,7 +1,6 @@ import { ICheck, Check, Monitor, ISystemInfo } from "@/db/models/index.js"; import type { IDockerPayload } from "@/services/infrastructure/NetworkService.js"; -import { MonitorStatus } from "@/db/models/monitors/Monitor.js"; -import { MonitorType } from "@/db/models/monitors/Monitor.js"; +import { MonitorStatus, MonitorType } from "@/types/domain/index.js"; import { StatusResponse } from "../infrastructure/NetworkService.js"; import type { ICapturePayload, diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts index 71ba6004a..28ecf9a55 100644 --- a/server/src/services/business/MonitorService.ts +++ b/server/src/services/business/MonitorService.ts @@ -1,16 +1,12 @@ -import mongoose from "mongoose"; - -import { MonitorStats, Check } from "@/db/models/index.js"; -import { StatsHourly, StatsDaily } from "@/db/models/index.js"; +import { MonitorStats } from "@/db/models/index.js"; import ApiError from "@/utils/ApiError.js"; import { IJobQueue } from "@/services/infrastructure/JobQueue.js"; import { MonitorWithChecksResponse } from "@/types/index.js"; -import { MonitorStatus, MonitorType } from "@/db/models/monitors/Monitor.js"; +import { MonitorStatus } from "@/types/domain/index.js"; import { Entitlements } from "@/types/entitlements.js"; import { getStartDate } from "@/utils/TimeUtils.js"; -import { IMonitorRepository } from "@/repositories/index.js"; -import type IChecksRepository from "@/repositories/checks/IChecksRepository.js"; -import type { Monitor } from "@/types/domain/index.js"; +import { IMonitorRepository, IChecksRepository } from "@/repositories/index.js"; +import type { Monitor, MonitorType } from "@/types/domain/index.js"; export enum MonitorImportCode { OK = "OK", @@ -176,518 +172,6 @@ class MonitorService implements IMonitorService { return monitor; }; - private getBaseGroup = (): Record => { - return { - _id: { $dateTrunc: { date: "$createdAt", unit: "minute" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - }; - }; - - private getBaseProjection = (): object => { - return { status: 1, responseTime: 1, createdAt: 1 }; - }; - - private getPageSpeedGroup = (): Record => { - return { - _id: { $dateTrunc: { date: "$createdAt", unit: "minute" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - accessibility: { $avg: "$lighthouse.accessibility" }, - bestPractices: { $avg: "$lighthouse.bestPractices" }, - seo: { $avg: "$lighthouse.seo" }, - performance: { $avg: "$lighthouse.performance" }, - cls: { $avg: "$lighthouse.audits.cls.score" }, - si: { $avg: "$lighthouse.audits.si.score" }, - fcp: { $avg: "$lighthouse.audits.fcp.score" }, - lcp: { $avg: "$lighthouse.audits.lcp.score" }, - tbt: { $avg: "$lighthouse.audits.tbt.score" }, - }; - }; - - private getPageSpeedProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["lighthouse.accessibility"] = 1; - projectStage["lighthouse.seo"] = 1; - projectStage["lighthouse.bestPractices"] = 1; - projectStage["lighthouse.performance"] = 1; - projectStage["lighthouse.audits.cls.score"] = 1; - projectStage["lighthouse.audits.si.score"] = 1; - projectStage["lighthouse.audits.fcp.score"] = 1; - projectStage["lighthouse.audits.lcp.score"] = 1; - projectStage["lighthouse.audits.tbt.score"] = 1; - return projectStage; - }; - - private getInfraGroup = (): Record => { - return { - _id: { $dateTrunc: { date: "$createdAt", unit: "minute" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - physicalCores: { $last: "$system.cpu.physical_core" }, - logicalCores: { $last: "$system.cpu.logical_core" }, - frequency: { $avg: "$system.cpu.frequency" }, - currentFrequency: { $last: "$system.cpu.current_frequency" }, - tempsArrays: { $push: "$system.cpu.temperature" }, - freePercent: { $avg: "$system.cpu.free_percent" }, - usedPercent: { $avg: "$system.cpu.usage_percent" }, - total_bytes: { $last: "$system.memory.total_bytes" }, - available_bytes: { $last: "$system.memory.available_bytes" }, - used_bytes: { $last: "$system.memory.used_bytes" }, - memory_usage_percent: { $avg: "$system.memory.usage_percent" }, - disksArray: { $push: "$system.disk" }, - os: { $last: "$system.host.os" }, - platform: { $last: "$system.host.platform" }, - kernel_version: { $last: "$system.host.kernel_version" }, - pretty_name: { $last: "$system.host.pretty_name" }, - netsArray: { $push: "$system.net" }, - }; - }; - - private getInfraProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["system.cpu.physical_core"] = 1; - projectStage["system.cpu.logical_core"] = 1; - projectStage["system.cpu.frequency"] = 1; - projectStage["system.cpu.current_frequency"] = 1; - projectStage["system.cpu.temperature"] = 1; - projectStage["system.cpu.free_percent"] = 1; - projectStage["system.cpu.usage_percent"] = 1; - projectStage["system.memory.total_bytes"] = 1; - projectStage["system.memory.available_bytes"] = 1; - projectStage["system.memory.used_bytes"] = 1; - projectStage["system.memory.usage_percent"] = 1; - projectStage["system.disk"] = 1; - projectStage["system.host.os"] = 1; - projectStage["system.host.platform"] = 1; - projectStage["system.host.kernel_version"] = 1; - projectStage["system.host.pretty_name"] = 1; - projectStage["system.net"] = 1; - return projectStage; - }; - - private getDockerGroup = (): Record => { - return { - _id: { $dateTrunc: { date: "$createdAt", unit: "minute" } }, - count: { $sum: 1 }, - avgResponseTime: { $last: "$responseTime" }, - totalContainers: { - $last: { $size: { $ifNull: ["$dockerContainers", []] } }, - }, - runningContainers: { - $last: { - $size: { - $filter: { - input: { $ifNull: ["$dockerContainers", []] }, - as: "c", - cond: { $eq: ["$$c.running", true] }, - }, - }, - }, - }, - healthyContainers: { - $last: { - $size: { - $filter: { - input: { $ifNull: ["$dockerContainers", []] }, - as: "c", - cond: { $eq: ["$$c.health.healthy", true] }, - }, - }, - }, - }, - }; - }; - - private getDockerProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["dockerContainers"] = 1; - return projectStage; - }; - - private getFinalProjection = (type: string): object => { - if (type === "pagespeed") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - accessibility: "$accessibility", - seo: "$seo", - bestPractices: "$bestPractices", - performance: "$performance", - cls: "$cls", - si: "$si", - fcp: "$fcp", - lcp: "$lcp", - tbt: "$tbt", - }; - } - - if (type === "infrastructure") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - cpu: { - physical_core: "$physicalCores", - logical_core: "$logicalCores", - frequency: "$frequency", - current_frequency: "$currentFrequency", - temperature: { - $map: { - input: { - $range: [ - 0, - { - $size: { - $ifNull: [{ $arrayElemAt: ["$tempsArrays", 0] }, []], - }, - }, - ], - }, - as: "idx", - in: { - $avg: { - $map: { - input: { $ifNull: ["$tempsArrays", []] }, - as: "arr", - in: { $arrayElemAt: ["$$arr", "$$idx"] }, - }, - }, - }, - }, - }, - free_percent: "$freePercent", - used_percent: "$usedPercent", - }, - memory: { - total_bytes: "$total_bytes", - available_bytes: "$available_bytes", - used_bytes: "$used_bytes", - usage_percent: "$memory_usage_percent", - }, - disk: { - $map: { - input: { - $range: [ - 0, - { - $size: { - $ifNull: [{ $arrayElemAt: ["$disksArray", 0] }, []], - }, - }, - ], - }, - as: "idx", - in: { - $let: { - vars: { - diskGroup: { - $map: { - input: { $ifNull: ["$disksArray", []] }, - as: "diskArr", - in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, - }, - }, - }, - in: { - device: { $arrayElemAt: ["$$diskGroup.device", 0] }, - total_bytes: { $avg: "$$diskGroup.total_bytes" }, - free_bytes: { $avg: "$$diskGroup.free_bytes" }, - used_bytes: { $avg: "$$diskGroup.used_bytes" }, - usage_percent: { $avg: "$$diskGroup.usage_percent" }, - total_inodes: { $avg: "$$diskGroup.total_inodes" }, - free_inodes: { $avg: "$$diskGroup.free_inodes" }, - used_inodes: { $avg: "$$diskGroup.used_inodes" }, - inodes_usage_percent: { - $avg: "$$diskGroup.inodes_usage_percent", - }, - read_bytes: { $avg: "$$diskGroup.read_bytes" }, - write_bytes: { $avg: "$$diskGroup.write_bytes" }, - read_time: { $avg: "$$diskGroup.read_time" }, - write_time: { $avg: "$$diskGroup.write_time" }, - }, - }, - }, - }, - }, - host: { - os: "$os", - platform: "$platform", - kernel_version: "$kernel_version", - pretty_name: "$pretty_name", - }, - net: { - $map: { - input: { - $range: [ - 0, - { - $size: { $ifNull: [{ $arrayElemAt: ["$netsArray", 0] }, []] }, - }, - ], - }, - as: "idx", - in: { - $let: { - vars: { - netGroup: { - $map: { - input: { $ifNull: ["$netsArray", []] }, - as: "netArr", - in: { $arrayElemAt: ["$$netArr", "$$idx"] }, - }, - }, - }, - in: { - name: { $arrayElemAt: ["$$netGroup.name", 0] }, - bytes_sent: { $avg: "$$netGroup.bytes_sent" }, - bytes_recv: { $avg: "$$netGroup.bytes_recv" }, - packets_sent: { $avg: "$$netGroup.packets_sent" }, - packets_recv: { $avg: "$$netGroup.packets_recv" }, - err_in: { $avg: "$$netGroup.err_in" }, - err_out: { $avg: "$$netGroup.err_out" }, - drop_in: { $avg: "$$netGroup.drop_in" }, - drop_out: { $avg: "$$netGroup.drop_out" }, - fifo_in: { $avg: "$$netGroup.fifo_in" }, - fifo_out: { $avg: "$$netGroup.fifo_out" }, - }, - }, - }, - }, - }, - }; - } - if (type === "docker") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - totalContainers: "$totalContainers", - runningContainers: "$runningContainers", - healthyContainers: "$healthyContainers", - runningContainersSnapshot: "$runningContainersSnapshot", - totalExposedPorts: "$totalExposedPorts", - uniqueImages: "$uniqueImages", - runningPercent: { - $cond: [ - { $gt: ["$totalContainers", 0] }, - { - $multiply: [ - { $divide: ["$runningContainers", "$totalContainers"] }, - 100, - ], - }, - null, - ], - }, - healthyPercent: { - $cond: [ - { $gt: ["$totalContainers", 0] }, - { - $multiply: [ - { $divide: ["$healthyContainers", "$totalContainers"] }, - 100, - ], - }, - null, - ], - }, - }; - } - return {}; - }; - - private getChecksRecent = async (monitor: Monitor, startDate: Date) => { - const endDate = new Date(); - const matchStage: { - "metadata.monitorId": mongoose.Types.ObjectId; - createdAt: { $gte: Date; $lt: Date }; - } = { - "metadata.monitorId": new mongoose.Types.ObjectId(monitor.id), - createdAt: { $gte: startDate, $lt: endDate }, - }; - - let groupClause; - if (monitor.type === "pagespeed") { - groupClause = this.getPageSpeedGroup(); - } else if (monitor.type === "infrastructure") { - groupClause = this.getInfraGroup(); - } else if (monitor.type === "docker") { - groupClause = this.getDockerGroup(); - } else { - groupClause = this.getBaseGroup(); - } - - let projectStage; - if (monitor.type === "pagespeed") { - projectStage = this.getPageSpeedProjection(); - } else if (monitor.type === "infrastructure") { - projectStage = this.getInfraProjection(); - } else if (monitor.type === "docker") { - projectStage = this.getDockerProjection(); - } else { - projectStage = this.getBaseProjection(); - } - - let finalProjection = {}; - if ( - monitor.type === "pagespeed" || - monitor.type === "infrastructure" || - monitor.type === "docker" - ) { - finalProjection = this.getFinalProjection(monitor.type); - } else { - finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; - } - - const pipeline: any[] = [ - { $match: matchStage }, - { $project: { ...projectStage, status: 1 } }, - ]; - - // For infrastructure and docker monitors we rely on $last in group, which requires ordering by createdAt - if (monitor.type === "infrastructure" || monitor.type === "docker") { - pipeline.push({ $sort: { createdAt: 1 } }); - } - - pipeline.push( - { - $group: { - ...groupClause, - upChecks: { $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] } }, - downChecks: { $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] } }, - sumResponseUp: { - $sum: { $cond: [{ $eq: ["$status", "up"] }, "$responseTime", 0] }, - }, - sumResponseDown: { - $sum: { $cond: [{ $eq: ["$status", "down"] }, "$responseTime", 0] }, - }, - }, - }, - { $sort: { _id: -1 } }, - { - $project: { - ...finalProjection, - upChecks: 1, - downChecks: 1, - avgResponseTimeUp: { - $cond: [ - { $gt: ["$upChecks", 0] }, - { $divide: ["$sumResponseUp", "$upChecks"] }, - null, - ], - }, - avgResponseTimeDown: { - $cond: [ - { $gt: ["$downChecks", 0] }, - { $divide: ["$sumResponseDown", "$downChecks"] }, - null, - ], - }, - }, - } - ); - - const checks = await Check.aggregate(pipeline) - .allowDiskUse(true) - .hint({ "metadata.monitorId": 1, createdAt: 1 }); - - // Get monitor stats - const monitorStats = await MonitorStats.findOne({ - monitorId: monitor.id, - }); - - if (!monitorStats) { - throw new ApiError("Monitor stats not found", 404); - } - - return { - monitor: monitor, - checks, - stats: monitorStats, - }; - }; - - private getChecksOtherRanges = async ( - monitor: Monitor, - range: string, - startDate: Date - ) => { - const monitorStats = await MonitorStats.findOne({ - monitorId: monitor.id, - }); - if (!monitorStats) { - throw new ApiError("Monitor stats not found", 404); - } - - const useDaily = range === "30d"; - const Model = useDaily ? StatsDaily : StatsHourly; - const endDate = new Date(); - - const docs = await Model.find({ - monitorId: monitor.id, - windowStart: { $gte: startDate, $lt: endDate }, - }) - .sort({ windowStart: -1 }) - .lean(); - - const checks = (docs as any[]).map((d) => { - const base: any = { - _id: new Date(d.windowStart).toISOString(), - count: d.count ?? 0, - avgResponseTime: d.avgResponseTime ?? 0, - upChecks: d.upChecks ?? 0, - downChecks: d.downChecks ?? 0, - avgResponseTimeUp: d.avgResponseTimeUp ?? null, - avgResponseTimeDown: d.avgResponseTimeDown ?? null, - }; - if (monitor.type === "pagespeed") { - return { - ...base, - accessibility: d.accessibility, - seo: d.seo, - bestPractices: d.bestPractices, - performance: d.performance, - cls: d.cls, - si: d.si, - fcp: d.fcp, - lcp: d.lcp, - tbt: d.tbt, - }; - } - if (monitor.type === "infrastructure") { - return { - ...base, - cpu: d.cpu, - memory: d.memory, - disk: d.disk, - host: d.host, - net: d.net, - }; - } - if (monitor.type === "docker") { - return { - ...base, - totalContainers: d.dockerTotalContainers, - runningContainers: d.dockerRunningContainers, - healthyContainers: d.dockerHealthyContainers, - totalExposedPorts: d.dockerTotalExposedPorts, - uniqueImages: d.dockerUniqueImages, - runningPercent: d.dockerRunningPercent, - healthyPercent: d.dockerHealthyPercent, - }; - } - return base; - }); - - return { - monitor: monitor, - checks, - stats: monitorStats, - }; - }; - getEmbedChecks = async ( teamId: string, monitorId: string, @@ -702,30 +186,39 @@ class MonitorService implements IMonitorService { throw new ApiError("Monitor not found", 404); } + const stats = await MonitorStats.findOne({ + monitorId: monitor.id, + }); + + if (!stats) { + throw new ApiError("Monitor stats not found", 404); + } + const startDate = getStartDate(range); if (range === "24h" || range === "7d" || range === "30d") { - return await this.getChecksOtherRanges(monitor, range, startDate); - } else { - const checks = await this.checksRepository.findRecentChecksByMonitor( + const checks = await this.checksRepository.findDateRangeChecksByMonitor( monitor, - startDate + startDate, + range + ); + + return { + checks, + monitor, + stats, + }; + } else { + const checks = await this.checksRepository.findRecentChecksByMonitor( + monitor, + startDate ); - const stats = await MonitorStats.findOne({ - monitorId: monitor.id, - }); - - if (!stats) { - throw new ApiError("Monitor stats not found", 404); - } - return { checks, monitor, stats, }; - // return await this.getChecksRecent(monitor, startDate); } }; diff --git a/server/src/services/business/StatsAggregationService.ts b/server/src/services/business/StatsAggregationService.ts index 2a767635a..c3209d9a6 100644 --- a/server/src/services/business/StatsAggregationService.ts +++ b/server/src/services/business/StatsAggregationService.ts @@ -2,7 +2,7 @@ import mongoose from "mongoose"; import { Check } from "@/db/models/index.js"; import { StatsHourly } from "@/db/models/index.js"; import { StatsDaily } from "@/db/models/index.js"; -import type { MonitorType } from "@/db/models/monitors/Monitor.js"; +import type { MonitorType } from "@/types/domain/index.js"; export interface IStatsAggregationService { upsertHourly: (windowStart: Date, windowEnd: Date) => Promise; @@ -678,10 +678,20 @@ class StatsAggregationService implements IStatsAggregationService { addW(g.tbt, (h as any).tbt, w); // docker - if (!(g as any).dockerRunningPercent) (g as any).dockerRunningPercent = { sum: 0, w: 0 }; - if (!(g as any).dockerHealthyPercent) (g as any).dockerHealthyPercent = { sum: 0, w: 0 }; - addW((g as any).dockerRunningPercent!, (h as any).dockerRunningPercent, w); - addW((g as any).dockerHealthyPercent!, (h as any).dockerHealthyPercent, w); + if (!(g as any).dockerRunningPercent) + (g as any).dockerRunningPercent = { sum: 0, w: 0 }; + if (!(g as any).dockerHealthyPercent) + (g as any).dockerHealthyPercent = { sum: 0, w: 0 }; + addW( + (g as any).dockerRunningPercent!, + (h as any).dockerRunningPercent, + w + ); + addW( + (g as any).dockerHealthyPercent!, + (h as any).dockerHealthyPercent, + w + ); const ts = new Date(h.windowStart).getTime(); if (ts >= g.lastTs) { diff --git a/server/src/services/business/TeamService.ts b/server/src/services/business/TeamService.ts index 505c9cf3a..6f09e5240 100644 --- a/server/src/services/business/TeamService.ts +++ b/server/src/services/business/TeamService.ts @@ -9,6 +9,7 @@ import { OrgMembership, IUserContext, } from "@/db/models/index.js"; +import type { IMonitorRepository } from "@/repositories/index.js"; import type { IJobQueue } from "../infrastructure/JobQueue.js"; import ApiError from "@/utils/ApiError.js"; import { PERMISSIONS } from "@/types/permissions.js"; @@ -35,10 +36,12 @@ export interface ITeamService { class TeamService implements ITeamService { public SERVICE_NAME: string; private jobQueue: IJobQueue; + private monitorsRepository: IMonitorRepository; - constructor(jobQueue: IJobQueue) { + constructor(jobQueue: IJobQueue, monitorsRepository: IMonitorRepository) { this.SERVICE_NAME = SERVICE_NAME; this.jobQueue = jobQueue; + this.monitorsRepository = monitorsRepository; } create = async ( @@ -149,7 +152,7 @@ class TeamService implements ITeamService { await membership.deleteOne(); } - const monitors = await Monitor.find({ teamId }); + const monitors = await this.monitorsRepository.findByTeamId(teamId); await Promise.all( monitors.map((monitor) => this.jobQueue.deleteJob(monitor)) ); diff --git a/server/src/services/infrastructure/NetworkService.ts b/server/src/services/infrastructure/NetworkService.ts index b797d6950..f4df4a38d 100644 --- a/server/src/services/infrastructure/NetworkService.ts +++ b/server/src/services/infrastructure/NetworkService.ts @@ -1,7 +1,6 @@ import { Got, HTTPError } from "got"; import ping from "ping"; import net from "net"; -import { IMonitor } from "@/db/models/index.js"; import { GotTimings } from "@/db/models/checks/Check.js"; import type { Response } from "got"; import type { @@ -10,23 +9,23 @@ import type { ILighthouseResult, } from "@/db/models/index.js"; import type { TLSSocket } from "node:tls"; -import { MonitorType, MonitorStatus } from "@/db/models/monitors/Monitor.js"; +import { MonitorType, MonitorStatus } from "@/types/domain/index.js"; import ApiError from "@/utils/ApiError.js"; import { config } from "@/config/index.js"; import CacheableLookup from "cacheable-lookup"; import { getChildLogger } from "@/logger/Logger.js"; import type { IDockerContainerSummary } from "@/db/models/index.js"; -import type { Monitor as MonitorEntity } from "@/types/domain/monitor.js"; +import type { Monitor } from "@/types/domain/monitor.js"; const SERVICE_NAME = "NetworkService"; const logger = getChildLogger(SERVICE_NAME); export interface INetworkService { - requestHttp: (monitor: MonitorEntity) => Promise; - requestInfrastructure: (monitor: MonitorEntity) => Promise; - requestStatus: (monitor: MonitorEntity) => Promise; - requestPagespeed: (monitor: MonitorEntity) => Promise; - requestPing: (monitor: MonitorEntity) => Promise; - requestPort: (monitor: MonitorEntity) => Promise; + requestHttp: (monitor: Monitor) => Promise; + requestInfrastructure: (monitor: Monitor) => Promise; + requestStatus: (monitor: Monitor) => Promise; + requestPagespeed: (monitor: Monitor) => Promise; + requestPing: (monitor: Monitor) => Promise; + requestPort: (monitor: Monitor) => Promise; } export interface ICapturePayload { @@ -93,7 +92,7 @@ class NetworkService implements INetworkService { } private buildStatusResponse = ( - monitor: MonitorEntity, + monitor: Monitor, response: Response | null, certificateExpiryOrError?: Date | null | any, maybeError?: any | null @@ -145,7 +144,7 @@ class NetworkService implements INetworkService { return statusResponse; }; - requestHttp = async (monitor: MonitorEntity) => { + requestHttp = async (monitor: Monitor) => { try { const url = monitor.url; if (!url) { @@ -191,7 +190,7 @@ class NetworkService implements INetworkService { } }; - requestInfrastructure = async (monitor: MonitorEntity) => { + requestInfrastructure = async (monitor: Monitor) => { const url = monitor.url; if (!url) { throw new Error("No URL provided"); @@ -226,7 +225,7 @@ class NetworkService implements INetworkService { return statusResponse; }; - requestPagespeed = async (monitor: MonitorEntity) => { + requestPagespeed = async (monitor: Monitor) => { const apiKey = config.PAGESPEED_API_KEY; if (!apiKey) { throw new Error("No API key provided for pagespeed monitor"); @@ -266,7 +265,7 @@ class NetworkService implements INetworkService { } }; - requestPing = async (monitor: MonitorEntity) => { + requestPing = async (monitor: Monitor) => { const response = await ping.promise.probe(monitor.url); const status = response?.alive === true ? "up" : "down"; @@ -287,7 +286,7 @@ class NetworkService implements INetworkService { }; }; - requestPort = async (monitor: MonitorEntity) => { + requestPort = async (monitor: Monitor) => { const response = { monitorId: monitor.id, teamId: monitor.teamId, @@ -359,7 +358,7 @@ class NetworkService implements INetworkService { } }; - requestStatus = async (monitor: MonitorEntity) => { + requestStatus = async (monitor: Monitor) => { switch (monitor?.type) { case "http": return await this.requestHttp(monitor); // uses GOT diff --git a/server/src/validation/checks.ts b/server/src/validation/checks.ts index 646863d37..6b606516c 100644 --- a/server/src/validation/checks.ts +++ b/server/src/validation/checks.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import mongoose from "mongoose"; -import { MonitorStatuses } from "@/db/models/index.js"; +import { MonitorStatuses } from "@/types/domain/index.js"; export const checksStatusIdQuerySchema = z.object({ status: z.enum(MonitorStatuses, { error: "Invalid status" }), @@ -29,15 +29,20 @@ export const checksStatusIdQuerySchema = z.object({ return z.NEVER; } if (num < 0) { - ctx.addIssue({ code: "custom", message: "rowsPerPage must be greater than 0" }); + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be greater than 0", + }); return z.NEVER; } if (num > 100) { - ctx.addIssue({ code: "custom", message: "rowsPerPage must be less than or equal to 100" }); + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be less than or equal to 100", + }); return z.NEVER; } return num; }), range: z.string().min(1), }); - diff --git a/server/src/validation/monitor.ts b/server/src/validation/monitor.ts index 3db91854a..e9f8e6225 100644 --- a/server/src/validation/monitor.ts +++ b/server/src/validation/monitor.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { MonitorTypes } from "@/db/models/index.js"; +import { MonitorTypes } from "@/types/domain/index.js"; import { urlRegex } from "./common.js"; export const monitorSchema = z @@ -43,7 +43,10 @@ export const monitorSchema = z }); } - if ((data.type === "infrastructure" || data.type === "docker") && !data.secret) { + if ( + (data.type === "infrastructure" || data.type === "docker") && + !data.secret + ) { ctx.addIssue({ code: "custom", message: `Secret is required for ${data.type} monitors`, @@ -118,4 +121,3 @@ export const monitorPatchSchema = monitorSchema }); } }); -