new container endpoint,service, repo methods
This commit is contained in:
1 parent
d347e29a28
commit
8ccc7cbe9a
9 files changed
+195
-4
No files matched your search
@@ -19,6 +19,8 @@ import {
|
||||
bulkPauseMonitorBodyValidation,
|
||||
getDockerDetailsByIdParamValidation,
|
||||
getDockerDetailsByIdQueryValidation,
|
||||
getDockerContainerNameParamValidation,
|
||||
getDockerContainerByNameQueryValidation,
|
||||
} from "@/api/validation/monitorValidation.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import * as whoiser from "whoiser";
|
||||
@@ -34,6 +36,7 @@ export interface IMonitorController {
|
||||
getHardwareDetailsById: RequestHandler;
|
||||
getPageSpeedDetailsById: RequestHandler;
|
||||
getDockerDetailsById: RequestHandler;
|
||||
getDockerContainerByName: RequestHandler;
|
||||
getGeoChecksByMonitorId: RequestHandler;
|
||||
getMonitorById: RequestHandler;
|
||||
createMonitor: RequestHandler;
|
||||
@@ -173,6 +176,28 @@ class MonitorController implements IMonitorController {
|
||||
});
|
||||
});
|
||||
|
||||
getDockerContainerByName = catchAsync(async (req: Request, res: Response) => {
|
||||
const validatedParams = getDockerContainerNameParamValidation.parse(req.params);
|
||||
const validatedQuery = getDockerContainerByNameQueryValidation.parse(req.query);
|
||||
|
||||
const { monitorId, containerName } = validatedParams;
|
||||
const dateRange = validatedQuery.dateRange || "recent";
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getDockerContainerByName({
|
||||
teamId,
|
||||
monitorId,
|
||||
containerName,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Docker container retrieved successfully",
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
getGeoChecksByMonitorId = catchAsync(async (req: Request, res: Response) => {
|
||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
||||
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
|
||||
|
||||
@@ -20,6 +20,7 @@ export const createMonitorRoutes = (monitorController: IMonitorController): Rout
|
||||
|
||||
// Docker routes
|
||||
router.get("/docker/details/:monitorId", monitorController.getDockerDetailsById);
|
||||
router.get("/docker/details/:monitorId/containers/:containerName", monitorController.getDockerContainerByName);
|
||||
|
||||
// Geo checks routes
|
||||
router.get("/:monitorId/geo-checks", monitorController.getGeoChecksByMonitorId);
|
||||
|
||||
@@ -326,6 +326,12 @@ export const getHardwareDetailsByIdQueryValidation = z.object({
|
||||
export const getDockerDetailsByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required") });
|
||||
export const getDockerDetailsByIdQueryValidation = z.object({ dateRange: z.enum(DateRanges).optional() });
|
||||
|
||||
export const getDockerContainerNameParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
containerName: z.string().min(1, "Container name is required"),
|
||||
});
|
||||
export const getDockerContainerByNameQueryValidation = z.object({ dateRange: z.enum(DateRanges).optional() });
|
||||
|
||||
// Canonical monitor shape returned by /monitors endpoints. Keep aligned with
|
||||
// what the controllers actually serialize.
|
||||
export const monitorResponseSchema = z
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CheckModel from "@/domain/checks/check.model.js";
|
||||
import { DockerStatsBucket } from "@/domain/checks/check.type.js";
|
||||
import { DockerContainerStatsBucket, DockerStatsBucket } from "@/domain/checks/check.type.js";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
type DateRange = { start: Date; end: Date };
|
||||
@@ -52,3 +52,43 @@ export const getDockerLatestCheck = async (monitorId: string) =>
|
||||
.sort({ createdAt: -1 })
|
||||
.select("containers containerSummary createdAt")
|
||||
.lean();
|
||||
|
||||
export const getDockerContainerStats = async (
|
||||
monitorId: string,
|
||||
containerName: string,
|
||||
dates: DateRange,
|
||||
dateString: string
|
||||
): Promise<DockerContainerStatsBucket[]> =>
|
||||
CheckModel.aggregate<DockerContainerStatsBucket>([
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
"metadata.type": "docker",
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
"containers.name": containerName,
|
||||
},
|
||||
},
|
||||
{ $unwind: "$containers" },
|
||||
{ $match: { "containers.name": containerName } },
|
||||
{
|
||||
$group: {
|
||||
_id: { $dateToString: { format: dateString, date: "$createdAt" } },
|
||||
avgCpuPct: { $avg: "$containers.cpuPct" },
|
||||
avgMemoryUsedBytes: { $avg: "$containers.memoryUsedBytes" },
|
||||
avgMemoryPct: { $avg: "$containers.memoryPct" },
|
||||
minRestartCount: { $min: "$containers.restartCount" },
|
||||
maxRestartCount: { $max: "$containers.restartCount" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
]);
|
||||
|
||||
export const getDockerContainerLatestCheck = async (monitorId: string, containerName: string) =>
|
||||
CheckModel.findOne({
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
"metadata.type": "docker",
|
||||
"containers.name": containerName,
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.select("containers createdAt")
|
||||
.lean();
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ChecksSummary,
|
||||
DailyCheckBucket,
|
||||
DockerChecksResult,
|
||||
DockerContainerStats,
|
||||
HardwareChecksResult,
|
||||
PageSpeedChecksResult,
|
||||
UptimeChecksResult,
|
||||
@@ -40,6 +41,7 @@ export interface IChecksRepository {
|
||||
dateRange: DateRange,
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult | DockerChecksResult>;
|
||||
findDockerContainerChecks(monitorId: string, containerName: string, dateRange: DateRange): Promise<Omit<DockerContainerStats, "restartsInRange">>;
|
||||
findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise<ChecksSummary>;
|
||||
findUnevaluatedByMonitorId(monitorId: string, since: number): Promise<Check[]>;
|
||||
getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string): Promise<DailyCheckBucket[]>;
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
DockerContainerStats,
|
||||
GotTimings,
|
||||
HardwareCheckStats,
|
||||
} from "@/domain/checks/check.type.js";
|
||||
@@ -24,7 +25,14 @@ import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@
|
||||
import { CheckFilter, DateRange } from "@/types/query.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { NETWORK_ERROR } from "@/types/network.js";
|
||||
import { getDockerLatestCheck, getDockerStats, getDockerTotalChecks, getDockerUpChecks } from "@/domain/checks/check.docker.aggregation.js";
|
||||
import {
|
||||
getDockerContainerLatestCheck,
|
||||
getDockerContainerStats,
|
||||
getDockerLatestCheck,
|
||||
getDockerStats,
|
||||
getDockerTotalChecks,
|
||||
getDockerUpChecks,
|
||||
} from "@/domain/checks/check.docker.aggregation.js";
|
||||
|
||||
const SERVICE_NAME = "ChecksRepository";
|
||||
|
||||
@@ -311,6 +319,30 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString);
|
||||
};
|
||||
|
||||
findDockerContainerChecks = async (
|
||||
monitorId: string,
|
||||
containerName: string,
|
||||
dateRange: DateRange
|
||||
): Promise<Omit<DockerContainerStats, "restartsInRange">> => {
|
||||
const dates = { start: getDateForRange(dateRange), end: new Date() };
|
||||
const dateString = getDateFormat(dateRange);
|
||||
const [aggregate, latestDoc] = await Promise.all([
|
||||
getDockerContainerStats(monitorId, containerName, dates, dateString),
|
||||
getDockerContainerLatestCheck(monitorId, containerName),
|
||||
]);
|
||||
const latestContainer = latestDoc?.containers?.find((c) => c.name === containerName);
|
||||
return {
|
||||
aggregate,
|
||||
latest:
|
||||
latestDoc && latestContainer
|
||||
? {
|
||||
container: latestContainer,
|
||||
checkedAt: toDateString(latestDoc.createdAt),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
findSummaryByTeamId = async (teamId: string, dateRange: DateRange) => {
|
||||
const baseMatch = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
|
||||
@@ -281,3 +281,21 @@ export interface DockerStats {
|
||||
export interface DockerChecksResult extends DockerStats {
|
||||
monitorType: "docker";
|
||||
}
|
||||
|
||||
export type DockerContainerStatsBucket = {
|
||||
_id: string;
|
||||
avgCpuPct: number | null;
|
||||
avgMemoryUsedBytes: number | null;
|
||||
avgMemoryPct: number | null;
|
||||
minRestartCount: number | null;
|
||||
maxRestartCount: number | null;
|
||||
};
|
||||
|
||||
export interface DockerContainerStats {
|
||||
aggregate: DockerContainerStatsBucket[];
|
||||
restartsInRange: number;
|
||||
latest: {
|
||||
container: DockerContainerInfo;
|
||||
checkedAt: string;
|
||||
} | null;
|
||||
}
|
||||
@@ -8,9 +8,16 @@ import type {
|
||||
GamesMap,
|
||||
GroupedGeoCheckResult,
|
||||
DockerDetailsResult,
|
||||
DockerContainerDetailsResult,
|
||||
} from "@/domain/monitors/monitor.type.js";
|
||||
import { supportsGeoCheck, supportsUptimeDetails } from "@/domain/monitors/monitor.type.js";
|
||||
import type { UptimeChecksResult, HardwareChecksResult, PageSpeedChecksResult, DockerChecksResult } from "@/domain/checks/check.type.js";
|
||||
import type {
|
||||
UptimeChecksResult,
|
||||
HardwareChecksResult,
|
||||
PageSpeedChecksResult,
|
||||
DockerChecksResult,
|
||||
DockerContainerStatsBucket,
|
||||
} from "@/domain/checks/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 { IGeoChecksRepository } from "@/domain/geo-checks/geo-check.repository.interface.js";
|
||||
@@ -31,6 +38,24 @@ const isUptimeChecksResult = (
|
||||
result: UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult | DockerChecksResult
|
||||
): result is UptimeChecksResult => supportsUptimeDetails(result.monitorType);
|
||||
|
||||
// Docker's RestartCount is cumulative and resets to 0 when a container is recreated, so restarts in the
|
||||
// date range are recovered from bucket min/max deltas; a drop between buckets means a recreate.
|
||||
const computeRestartsInRange = (aggregate: DockerContainerStatsBucket[]): number => {
|
||||
let restarts = 0;
|
||||
let prevMax: number | null = null;
|
||||
for (const bucket of aggregate) {
|
||||
const { minRestartCount: min, maxRestartCount: max } = bucket;
|
||||
if (min === null || max === null) continue;
|
||||
restarts += Math.max(0, max - min);
|
||||
if (prevMax !== null) {
|
||||
const delta = min - prevMax;
|
||||
restarts += delta >= 0 ? delta : min + 1;
|
||||
}
|
||||
prevMax = max;
|
||||
}
|
||||
return restarts;
|
||||
};
|
||||
|
||||
export interface IMonitorService {
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||
@@ -42,6 +67,12 @@ export interface IMonitorService {
|
||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<HardwareDetailsResult>;
|
||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<PageSpeedDetailsResult>;
|
||||
getDockerDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise<DockerDetailsResult>;
|
||||
getDockerContainerByName(args: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
containerName: string;
|
||||
dateRange: DateRange;
|
||||
}): Promise<DockerContainerDetailsResult>;
|
||||
getGeoChecksByMonitorId(args: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
@@ -323,6 +354,37 @@ export class MonitorService implements IMonitorService {
|
||||
};
|
||||
};
|
||||
|
||||
getDockerContainerByName = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
containerName,
|
||||
dateRange,
|
||||
}: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
containerName: string;
|
||||
dateRange: DateRange;
|
||||
}): Promise<DockerContainerDetailsResult> => {
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "docker") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for docker container details`, status: 400 });
|
||||
}
|
||||
|
||||
const { aggregate, latest } = await this.checksRepository.findDockerContainerChecks(monitor.id, containerName, dateRange);
|
||||
|
||||
return {
|
||||
monitor,
|
||||
stats: {
|
||||
aggregate,
|
||||
latest,
|
||||
restartsInRange: computeRestartsInRange(aggregate),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
getGeoChecksByMonitorId = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CheckSnapshot, DockerStats } from "@/domain/checks/check.type.js";
|
||||
import type { CheckSnapshot, DockerContainerStats, DockerStats } from "@/domain/checks/check.type.js";
|
||||
export type { CheckSnapshot } from "@/domain/checks/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";
|
||||
@@ -160,6 +160,11 @@ export interface DockerDetailsResult {
|
||||
monitorStats: MonitorStats | null;
|
||||
}
|
||||
|
||||
export interface DockerContainerDetailsResult {
|
||||
monitor: Monitor;
|
||||
stats: DockerContainerStats;
|
||||
}
|
||||
|
||||
export interface PageSpeedDetailsResult {
|
||||
monitorData: {
|
||||
monitor: Monitor;
|
||||
|
||||
Reference in new issue
Block a user