fix monitor stats bug

This commit is contained in:
Alex Holliday
2026-02-18 00:06:02 +00:00
parent c7ccdafec2
commit 31698bd002
6 changed files with 121 additions and 5 deletions
@@ -0,0 +1,81 @@
import { MonitorStatsModel } from "../models/MonitorStats.js";
import { logger } from "@/utils/logger.js";
/**
* Cleanup duplicate MonitorStats documents
* Keeps the most recent document (by updatedAt) for each monitorId
* and deletes all older duplicates
*/
export async function cleanupDuplicateMonitorStats(): Promise<void> {
const SERVICE_NAME = "Migration:CleanupDuplicateMonitorStats";
try {
logger.info({ service: SERVICE_NAME, message: "Starting cleanup of duplicate MonitorStats" });
// Find all duplicate monitorIds
const duplicates = await MonitorStatsModel.aggregate([
{
$group: {
_id: "$monitorId",
ids: { $push: "$_id" },
updatedAts: { $push: "$updatedAt" },
count: { $sum: 1 },
},
},
{ $match: { count: { $gt: 1 } } },
]);
if (duplicates.length === 0) {
logger.info({ service: SERVICE_NAME, message: "No duplicate MonitorStats found" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Found ${duplicates.length} monitors with duplicate stats`,
});
let totalDeleted = 0;
// For each set of duplicates, keep the newest and delete the rest
for (const duplicate of duplicates) {
const monitorId = duplicate._id;
const { ids, updatedAts } = duplicate;
type DocPair = { id: any; updatedAt: Date };
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
const docs: DocPair[] = ids.map((id: any, index: number) => ({
id,
updatedAt: updatedAts[index],
}));
docs.sort((a: DocPair, b: DocPair) => b.updatedAt.getTime() - a.updatedAt.getTime());
// Keep the first (newest), delete the rest
const toDelete = docs.slice(1).map((doc: DocPair) => doc.id);
if (toDelete.length > 0) {
const result = await MonitorStatsModel.deleteMany({
_id: { $in: toDelete },
});
totalDeleted += result.deletedCount ?? 0;
logger.debug({
service: SERVICE_NAME,
message: `Deleted ${result.deletedCount} duplicate stats for monitor ${monitorId}`,
});
}
}
logger.info({
service: SERVICE_NAME,
message: `Cleanup complete. Deleted ${totalDeleted} duplicate MonitorStats documents`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error during MonitorStats cleanup: ${errorMessage}` });
throw error;
}
}
+2
View File
@@ -1,5 +1,6 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import MigrationModel from "../models/Migration.js";
type MigrationEntry = {
@@ -10,6 +11,7 @@ type MigrationEntry = {
const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
+1
View File
@@ -18,6 +18,7 @@ const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
ref: "Monitor",
immutable: true,
index: true,
unique: true,
required: true,
},
avgResponseTime: {
@@ -5,6 +5,7 @@ export interface IMonitorStatsRepository {
// single fetch
findByMonitorId(monitorId: string): Promise<MonitorStats>;
// update
updateByMonitorId(monitorId: string, data: Omit<MonitorStats, "id" | "monitorId" | "createdAt" | "updatedAt">): Promise<MonitorStats>;
// delete
deleteByMonitorId(monitorId: string): Promise<MonitorStats>;
deleteByMonitorIds(monitorIds: string[]): Promise<number>;
@@ -46,6 +46,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
return this.toEntity(monitorStats);
};
updateByMonitorId = async (monitorId: string, data: Omit<MonitorStats, "id" | "monitorId" | "createdAt" | "updatedAt">): Promise<MonitorStats> => {
const updated = await MonitorStatsModel.findOneAndUpdate({ monitorId: new mongoose.Types.ObjectId(monitorId) }, { $set: data }, { new: true });
if (!updated) {
throw new AppError({ message: "Monitor stats not found", status: 404 });
}
return this.toEntity(updated);
};
deleteByMonitorId = async (monitorId: string) => {
const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) });
if (!deleted) {
@@ -52,8 +52,8 @@ export class StatusService implements IStatusService {
try {
const monitorId = monitor.id;
const { responseTime, status } = networkResponse;
let stats: Omit<MonitorStats, "id" | "createdAt" | "updatedAt"> | null = null;
stats = await this.monitorStatsRepository
let existingStats: MonitorStats | null = null;
existingStats = await this.monitorStatsRepository
.findByMonitorId(monitorId)
.then((result) => result)
.catch(() => {
@@ -64,9 +64,12 @@ export class StatusService implements IStatusService {
});
return null;
});
if (!stats) {
let stats: Omit<MonitorStats, "id" | "monitorId" | "createdAt" | "updatedAt">;
if (!existingStats) {
// Initialize new stats
stats = {
monitorId,
avgResponseTime: 0,
maxResponseTime: 0,
totalChecks: 0,
@@ -76,6 +79,19 @@ export class StatusService implements IStatusService {
lastResponseTime: 0,
lastCheckTimestamp: 0,
};
} else {
// Use existing stats (omit id, monitorId, createdAt, updatedAt)
stats = {
avgResponseTime: existingStats.avgResponseTime,
maxResponseTime: existingStats.maxResponseTime,
totalChecks: existingStats.totalChecks,
totalUpChecks: existingStats.totalUpChecks,
totalDownChecks: existingStats.totalDownChecks,
uptimePercentage: existingStats.uptimePercentage,
lastResponseTime: existingStats.lastResponseTime,
lastCheckTimestamp: existingStats.lastCheckTimestamp,
timeOfLastFailure: existingStats.timeOfLastFailure,
};
}
// Update stats
@@ -123,7 +139,14 @@ export class StatusService implements IStatusService {
// latest check
stats.lastCheckTimestamp = new Date().getTime();
await this.monitorStatsRepository.create(stats);
// Create or update
if (!existingStats) {
await this.monitorStatsRepository.create({ monitorId, ...stats });
} else {
await this.monitorStatsRepository.updateByMonitorId(monitorId, stats);
}
return true;
} catch (error: any) {
this.logger.error({