From f56f91c31db314eaa449dc77f65b30f367962e2b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 30 Dec 2025 19:49:13 +0000 Subject: [PATCH] rollup --- server/src/db/models/monitors/StatsDaily.ts | 13 +++ server/src/db/models/monitors/StatsHourly.ts | 10 ++ .../src/services/business/MonitorService.ts | 26 ++++- .../business/StatsAggregationService.ts | 110 +++++++++++++++++- 4 files changed, 156 insertions(+), 3 deletions(-) diff --git a/server/src/db/models/monitors/StatsDaily.ts b/server/src/db/models/monitors/StatsDaily.ts index a2832a7b9..dfaa4b445 100644 --- a/server/src/db/models/monitors/StatsDaily.ts +++ b/server/src/db/models/monitors/StatsDaily.ts @@ -19,6 +19,12 @@ export interface IStatsDaily disk?: IInfraDiskEntry[]; host?: IInfraHost; net?: IInfraNetEntry[]; + // Docker rollups + dockerRunningPercent?: number; + dockerHealthyPercent?: number; + dockerRunningContainers?: number; + dockerHealthyContainers?: number; + dockerTotalContainers?: number; } const InfraCpuSchema = new Schema( @@ -132,6 +138,13 @@ const StatsDailySchema = new Schema( disk: { type: [InfraDiskEntrySchema], required: false, default: undefined }, host: { type: InfraHostSchema, required: false }, net: { type: [InfraNetEntrySchema], required: false, default: undefined }, + + // Docker + dockerRunningPercent: { type: Number }, + dockerHealthyPercent: { type: Number }, + dockerRunningContainers: { type: Number }, + dockerHealthyContainers: { type: Number }, + dockerTotalContainers: { type: Number }, }, { timestamps: true, collection: "stats_daily" } ); diff --git a/server/src/db/models/monitors/StatsHourly.ts b/server/src/db/models/monitors/StatsHourly.ts index ffdb3846a..1f3bcda8c 100644 --- a/server/src/db/models/monitors/StatsHourly.ts +++ b/server/src/db/models/monitors/StatsHourly.ts @@ -96,6 +96,11 @@ export interface IStatsHourly disk?: IInfraDiskEntry[]; host?: IInfraHost; net?: IInfraNetEntry[]; + dockerRunningPercent?: number; + dockerHealthyPercent?: number; + dockerRunningContainers?: number; + dockerHealthyContainers?: number; + dockerTotalContainers?: number; } const InfraCpuSchema = new Schema( @@ -209,6 +214,11 @@ const StatsHourlySchema = new Schema( disk: { type: [InfraDiskEntrySchema], required: false, default: undefined }, host: { type: InfraHostSchema, required: false }, net: { type: [InfraNetEntrySchema], required: false, default: undefined }, + dockerRunningPercent: { type: Number }, + dockerHealthyPercent: { type: Number }, + dockerRunningContainers: { type: Number }, + dockerHealthyContainers: { type: Number }, + dockerTotalContainers: { type: Number }, }, { timestamps: true, collection: "stats_hourly" } ); diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts index c7219aed9..f67efdf1b 100644 --- a/server/src/services/business/MonitorService.ts +++ b/server/src/services/business/MonitorService.ts @@ -560,14 +560,24 @@ class MonitorService implements IMonitorService { runningPercent: { $cond: [ { $gt: ["$totalContainers", 0] }, - { $multiply: [{ $divide: ["$runningContainers", "$totalContainers"] }, 100] }, + { + $multiply: [ + { $divide: ["$runningContainers", "$totalContainers"] }, + 100, + ], + }, null, ], }, healthyPercent: { $cond: [ { $gt: ["$totalContainers", 0] }, - { $multiply: [{ $divide: ["$healthyContainers", "$totalContainers"] }, 100] }, + { + $multiply: [ + { $divide: ["$healthyContainers", "$totalContainers"] }, + 100, + ], + }, null, ], }, @@ -744,6 +754,18 @@ class MonitorService implements IMonitorService { 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; }); diff --git a/server/src/services/business/StatsAggregationService.ts b/server/src/services/business/StatsAggregationService.ts index b6f0d2aee..2a767635a 100644 --- a/server/src/services/business/StatsAggregationService.ts +++ b/server/src/services/business/StatsAggregationService.ts @@ -50,6 +50,12 @@ class StatsAggregationService implements IStatsAggregationService { disk: d.disk, host: d.host, net: d.net, + // Docker (present when type === "docker") + dockerRunningPercent: d.dockerRunningPercent, + dockerHealthyPercent: d.dockerHealthyPercent, + dockerRunningContainers: d.dockerRunningContainers, + dockerHealthyContainers: d.dockerHealthyContainers, + dockerTotalContainers: d.dockerTotalContainers, }, }, upsert: true, @@ -441,6 +447,82 @@ class StatsAggregationService implements IStatsAggregationService { bulkOps.push(this.makeUpsertOp(el, windowStart, windowEnd)) ); + // 4) Docker hourly + const dockerAgg = await Check.aggregate([ + { $match: { ...matchWindow, "metadata.type": "docker" } }, + { + $project: { + status: 1, + responseTime: 1, + createdAt: 1, + dockerContainers: 1, + "metadata.monitorId": 1, + "metadata.teamId": 1, + "metadata.type": 1, + }, + }, + { $sort: { createdAt: 1 } }, + { + $addFields: { + total: { $size: { $ifNull: ["$dockerContainers", []] } }, + running: { + $size: { + $filter: { + input: { $ifNull: ["$dockerContainers", []] }, + as: "c", + cond: { $eq: ["$$c.running", true] }, + }, + }, + }, + healthy: { + $size: { + $filter: { + input: { $ifNull: ["$dockerContainers", []] }, + as: "c", + cond: { $eq: ["$$c.health.healthy", true] }, + }, + }, + }, + }, + }, + { + $addFields: { + runningPercent: { + $cond: [ + { $gt: ["$total", 0] }, + { $multiply: [{ $divide: ["$running", "$total"] }, 100] }, + null, + ], + }, + healthyPercent: { + $cond: [ + { $gt: ["$total", 0] }, + { $multiply: [{ $divide: ["$healthy", "$total"] }, 100] }, + null, + ], + }, + }, + }, + { + $group: { + _id: "$metadata.monitorId", + teamId: { $first: "$metadata.teamId" }, + type: { $first: "$metadata.type" }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + dockerRunningPercent: { $avg: "$runningPercent" }, + dockerHealthyPercent: { $avg: "$healthyPercent" }, + dockerTotalContainers: { $last: "$total" }, + dockerRunningContainers: { $last: "$running" }, + dockerHealthyContainers: { $last: "$healthy" }, + }, + }, + ]); + + dockerAgg.forEach((el) => + bulkOps.push(this.makeUpsertOp(el, windowStart, windowEnd)) + ); + if (bulkOps.length > 0) { const res = await StatsHourly.bulkWrite(bulkOps, { ordered: false }); upserts += res.upsertedCount + (res.modifiedCount ?? 0); @@ -457,7 +539,11 @@ class StatsAggregationService implements IStatsAggregationService { // Fetch hourly docs for the day const hourly = await StatsHourly.find({ windowStart: { $gte: dayStart, $lt: dayEnd }, - }).lean(); + }) + .select( + "monitorId teamId type windowStart count avgResponseTime upChecks downChecks avgResponseTimeUp avgResponseTimeDown accessibility bestPractices seo performance cls si fcp lcp tbt cpu memory disk host net dockerRunningPercent dockerHealthyPercent dockerTotalContainers dockerRunningContainers dockerHealthyContainers" + ) + .lean(); type Weighted = { sum: number; w: number }; const addW = (acc: Weighted, v?: number, w = 0) => { @@ -521,6 +607,12 @@ class StatsAggregationService implements IStatsAggregationService { }; net: Map; lastTs: number; // for choosing "last" fields + // docker + dockerRunningPercent?: Weighted; + dockerHealthyPercent?: Weighted; + dockerTotalContainers?: number; + dockerRunningContainers?: number; + dockerHealthyContainers?: number; } const groups = new Map(); @@ -585,6 +677,12 @@ class StatsAggregationService implements IStatsAggregationService { addW(g.lcp, (h as any).lcp, w); 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); + const ts = new Date(h.windowStart).getTime(); if (ts >= g.lastTs) { g.lastTs = ts; @@ -612,6 +710,11 @@ class StatsAggregationService implements IStatsAggregationService { kernel_version: host.kernel_version, pretty_name: host.pretty_name, }; + + // docker last snapshot + (g as any).dockerTotalContainers = (h as any).dockerTotalContainers; + (g as any).dockerRunningContainers = (h as any).dockerRunningContainers; + (g as any).dockerHealthyContainers = (h as any).dockerHealthyContainers; } // weighted CPU averages @@ -772,6 +875,11 @@ class StatsAggregationService implements IStatsAggregationService { disk: diskArr.length > 0 ? diskArr : undefined, host: g.host, net: netArr.length > 0 ? netArr : undefined, + dockerRunningPercent: avg((g as any).dockerRunningPercent!), + dockerHealthyPercent: avg((g as any).dockerHealthyPercent!), + dockerTotalContainers: (g as any).dockerTotalContainers, + dockerRunningContainers: (g as any).dockerRunningContainers, + dockerHealthyContainers: (g as any).dockerHealthyContainers, }, };