mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-24 11:59:39 -05:00
fix monitor stats bug
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user