openapi docs, tests
This commit is contained in:
1 parent
8ccc7cbe9a
commit
f85ed902e4
4 files changed
+566
No files matched your search
@@ -7467,6 +7467,425 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/monitors/docker/details/{monitorId}/containers/{containerName}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"summary": "Get details for one container on a Docker host monitor",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"required": true,
|
||||
"name": "monitorId",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"required": true,
|
||||
"name": "containerName",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["recent", "hour", "day", "week", "month", "all"]
|
||||
},
|
||||
"required": false,
|
||||
"name": "dateRange",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [true]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"monitor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "dns", "unknown"]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "number"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"interval": {
|
||||
"type": "number"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["up", "down", "paused", "initializing", "maintenance", "breached"]
|
||||
},
|
||||
"statusWindowSize": {
|
||||
"type": "number"
|
||||
},
|
||||
"statusWindowThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"ignoreTlsErrors": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"proxyMode": {
|
||||
"type": "string",
|
||||
"enum": ["inherit", "none", "custom"]
|
||||
},
|
||||
"proxyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"useAdvancedMatching": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"jsonPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"expectedValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"matchMethod": {
|
||||
"type": "string",
|
||||
"enum": ["equal", "include", "regex"]
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "HEAD"]
|
||||
},
|
||||
"notifications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customUpCodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"sshPrivateKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"cpuAlertThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"memoryAlertThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"diskAlertThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"tempAlertThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"selectedDisks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"gameId": {
|
||||
"type": "string"
|
||||
},
|
||||
"grpcServiceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"geoCheckEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"geoCheckLocations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["EU", "NA", "AS", "SA", "AF", "OC"]
|
||||
}
|
||||
},
|
||||
"geoCheckInterval": {
|
||||
"type": "number"
|
||||
},
|
||||
"dnsServer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dnsRecordType": {
|
||||
"type": "string",
|
||||
"enum": ["A", "AAAA", "CNAME", "MX", "TXT", "NS"]
|
||||
},
|
||||
"teamId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastEvaluatedAt": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"url",
|
||||
"isActive",
|
||||
"interval",
|
||||
"status",
|
||||
"statusWindowSize",
|
||||
"statusWindowThreshold",
|
||||
"ignoreTlsErrors",
|
||||
"proxyMode",
|
||||
"useAdvancedMatching",
|
||||
"method",
|
||||
"notifications",
|
||||
"tags",
|
||||
"cpuAlertThreshold",
|
||||
"memoryAlertThreshold",
|
||||
"diskAlertThreshold",
|
||||
"tempAlertThreshold",
|
||||
"selectedDisks",
|
||||
"geoCheckEnabled",
|
||||
"geoCheckLocations",
|
||||
"geoCheckInterval",
|
||||
"teamId",
|
||||
"userId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"lastEvaluatedAt",
|
||||
"id"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aggregate": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"avgCpuPct": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"avgMemoryUsedBytes": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"avgMemoryPct": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"minRestartCount": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"maxRestartCount": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["_id", "avgCpuPct", "avgMemoryUsedBytes", "avgMemoryPct", "minRestartCount", "maxRestartCount"]
|
||||
}
|
||||
},
|
||||
"restartsInRange": {
|
||||
"type": "number"
|
||||
},
|
||||
"latest": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"properties": {
|
||||
"container": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["created", "running", "paused", "restarting", "removing", "exited", "dead"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"health": {
|
||||
"type": "string",
|
||||
"enum": ["healthy", "unhealthy", "starting", "none"]
|
||||
},
|
||||
"cpuPct": {
|
||||
"type": "number"
|
||||
},
|
||||
"memoryUsedBytes": {
|
||||
"type": "number"
|
||||
},
|
||||
"memoryLimitBytes": {
|
||||
"type": "number"
|
||||
},
|
||||
"memoryPct": {
|
||||
"type": "number"
|
||||
},
|
||||
"restartCount": {
|
||||
"type": "number"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "image", "state", "status", "health"]
|
||||
},
|
||||
"checkedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["container", "checkedAt"]
|
||||
}
|
||||
},
|
||||
"required": ["aggregate", "restartsInRange", "latest"]
|
||||
}
|
||||
},
|
||||
"required": ["monitor", "stats"]
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg", "data"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [false]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [false]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [false]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/monitors/pagespeed/details/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
getDockerDetailsByIdParamValidation,
|
||||
getDockerDetailsByIdQueryValidation,
|
||||
getDockerContainerNameParamValidation,
|
||||
getDockerContainerByNameQueryValidation,
|
||||
dockerDetailsResponseSchema,
|
||||
dockerContainerDetailsResponseSchema,
|
||||
monitorResponseSchema,
|
||||
uptimeDetailsResponseSchema,
|
||||
} from "@/api/validation/monitorValidation.js";
|
||||
@@ -118,6 +121,16 @@ registry.registerPath({
|
||||
responses: { "200": okJson(dockerDetailsResponseSchema), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/docker/details/{monitorId}/containers/{containerName}",
|
||||
tags,
|
||||
summary: "Get details for one container on a Docker host monitor",
|
||||
security: bearer,
|
||||
request: { params: getDockerContainerNameParamValidation, query: getDockerContainerByNameQueryValidation },
|
||||
responses: { "200": okJson(dockerContainerDetailsResponseSchema), ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/monitors/pagespeed/details/{monitorId}",
|
||||
|
||||
@@ -484,3 +484,29 @@ export const dockerDetailsResponseSchema = z.object({
|
||||
}),
|
||||
monitorStats: monitorStatsResponseSchema.nullable(),
|
||||
});
|
||||
|
||||
// Keep aligned with DockerContainerStatsBucket in domain/checks/check.type.ts.
|
||||
export const dockerContainerStatsBucketResponseSchema = z.object({
|
||||
_id: z.string(),
|
||||
avgCpuPct: z.number().nullable(),
|
||||
avgMemoryUsedBytes: z.number().nullable(),
|
||||
avgMemoryPct: z.number().nullable(),
|
||||
minRestartCount: z.number().nullable(),
|
||||
maxRestartCount: z.number().nullable(),
|
||||
});
|
||||
|
||||
// Response of GET /monitors/docker/details/{monitorId}/containers/{containerName}. Keep
|
||||
// aligned with DockerContainerDetailsResult in domain/monitors/monitor.type.ts.
|
||||
export const dockerContainerDetailsResponseSchema = z.object({
|
||||
monitor: monitorResponseSchema.omit({ _id: true }).extend({ id: z.string() }),
|
||||
stats: z.object({
|
||||
aggregate: z.array(dockerContainerStatsBucketResponseSchema),
|
||||
restartsInRange: z.number(),
|
||||
latest: z
|
||||
.object({
|
||||
container: dockerContainerResponseSchema,
|
||||
checkedAt: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
}),
|
||||
});
|
||||
@@ -29,6 +29,7 @@ const createMonitorsRepositoryMock = () =>
|
||||
const createChecksRepositoryMock = () =>
|
||||
({
|
||||
findByDateRangeAndMonitorId: jest.fn(),
|
||||
findDockerContainerChecks: jest.fn(),
|
||||
deleteByMonitorId: jest.fn(),
|
||||
}) as unknown as IChecksRepository;
|
||||
|
||||
@@ -486,6 +487,113 @@ describe("MonitorService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDockerContainerByName", () => {
|
||||
const makeBucket = (overrides: Partial<Record<string, number | string | null>> = {}) => ({
|
||||
_id: "2024-01-01T00:00:00Z",
|
||||
avgCpuPct: 12.5,
|
||||
avgMemoryUsedBytes: 1024,
|
||||
avgMemoryPct: 50,
|
||||
minRestartCount: 0,
|
||||
maxRestartCount: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("returns container details for a docker monitor", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
const monitor = makeMonitor({ type: "docker" });
|
||||
const container = { id: "abc", name: "checkmate-mongo", image: "mongo:8.0", state: "running", status: "Up 3 hours", health: "healthy" };
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
|
||||
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
|
||||
aggregate: [makeBucket()],
|
||||
latest: { container, checkedAt: "2024-01-01T00:00:00Z" },
|
||||
});
|
||||
|
||||
const { service } = createService({ monitorsRepository, checksRepository });
|
||||
const result = await service.getDockerContainerByName({
|
||||
teamId: TEAM_ID,
|
||||
monitorId: MONITOR_ID,
|
||||
containerName: "checkmate-mongo",
|
||||
dateRange: "recent",
|
||||
});
|
||||
|
||||
expect(checksRepository.findDockerContainerChecks).toHaveBeenCalledWith(MONITOR_ID, "checkmate-mongo", "recent");
|
||||
expect(result.monitor).toMatchObject({ id: MONITOR_ID });
|
||||
expect(result.stats.latest?.container).toMatchObject({ name: "checkmate-mongo" });
|
||||
expect(result.stats.restartsInRange).toBe(0);
|
||||
});
|
||||
|
||||
it("sums restarts within and between buckets", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
|
||||
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
|
||||
aggregate: [
|
||||
makeBucket({ _id: "01", minRestartCount: 0, maxRestartCount: 2 }),
|
||||
makeBucket({ _id: "02", minRestartCount: 3, maxRestartCount: 3 }),
|
||||
makeBucket({ _id: "03", minRestartCount: null, maxRestartCount: null }),
|
||||
makeBucket({ _id: "04", minRestartCount: 3, maxRestartCount: 4 }),
|
||||
],
|
||||
latest: null,
|
||||
});
|
||||
|
||||
const { service } = createService({ monitorsRepository, checksRepository });
|
||||
const result = await service.getDockerContainerByName({
|
||||
teamId: TEAM_ID,
|
||||
monitorId: MONITOR_ID,
|
||||
containerName: "checkmate-mongo",
|
||||
dateRange: "day",
|
||||
});
|
||||
|
||||
// 2 within bucket 01, +1 between 01 and 02, +1 within 04 (null bucket skipped)
|
||||
expect(result.stats.restartsInRange).toBe(4);
|
||||
});
|
||||
|
||||
it("counts a restart-count drop as a recreate", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
|
||||
(checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({
|
||||
aggregate: [
|
||||
makeBucket({ _id: "01", minRestartCount: 5, maxRestartCount: 5 }),
|
||||
makeBucket({ _id: "02", minRestartCount: 1, maxRestartCount: 1 }),
|
||||
],
|
||||
latest: null,
|
||||
});
|
||||
|
||||
const { service } = createService({ monitorsRepository, checksRepository });
|
||||
const result = await service.getDockerContainerByName({
|
||||
teamId: TEAM_ID,
|
||||
monitorId: MONITOR_ID,
|
||||
containerName: "checkmate-mongo",
|
||||
dateRange: "day",
|
||||
});
|
||||
|
||||
// counter dropped 5 -> 1: the recreate itself plus the 1 restart the new counter shows
|
||||
expect(result.stats.restartsInRange).toBe(2);
|
||||
});
|
||||
|
||||
it("throws 404 when monitor not found", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(null);
|
||||
const { service } = createService({ monitorsRepository });
|
||||
|
||||
await expect(
|
||||
service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: "missing", containerName: "checkmate-mongo", dateRange: "recent" })
|
||||
).rejects.toThrow("Monitor with ID missing not found.");
|
||||
});
|
||||
|
||||
it("throws 400 when monitor type is not docker", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "http" }));
|
||||
const { service } = createService({ monitorsRepository });
|
||||
|
||||
await expect(
|
||||
service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "recent" })
|
||||
).rejects.toThrow("monitors are not supported for docker container details");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPageSpeedDetailsById", () => {
|
||||
it("returns pagespeed details for a pagespeed monitor", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
|
||||
Reference in new issue
Block a user