Merge pull request #3995 from bluewave-labs/fix/decision-refactor
fix: convert decision objecty into a state object
This commit is contained in:
15 files changed
+149
-271
No files matched your search
@@ -54,45 +54,35 @@ export class IncidentService implements IIncidentService {
|
||||
}
|
||||
|
||||
handleIncident = async (monitor: Monitor, decision: MonitorActionDecision, check: Check): Promise<Incident | null> => {
|
||||
if (!decision.shouldCreateIncident && !decision.shouldResolveIncident) {
|
||||
return null;
|
||||
}
|
||||
const { transition } = decision;
|
||||
if (transition === null) return null;
|
||||
|
||||
const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
|
||||
|
||||
if (decision.shouldCreateIncident) {
|
||||
if (activeIncident) {
|
||||
return activeIncident;
|
||||
} else {
|
||||
let statusCode = check.statusCode;
|
||||
let message: string | undefined;
|
||||
|
||||
// For threshold breaches, use 9999 status code and build descriptive message
|
||||
if (decision.incidentReason === "threshold_breach") {
|
||||
statusCode = 9999;
|
||||
message = this.notificationMessageBuilder.buildThresholdBreachMessage(monitor, check, decision.thresholdBreaches);
|
||||
}
|
||||
|
||||
const incident = {
|
||||
switch (transition) {
|
||||
case "status_down":
|
||||
case "threshold_breach": {
|
||||
if (activeIncident) return activeIncident;
|
||||
// A threshold breach has no HTTP status
|
||||
const isBreach = transition === "threshold_breach";
|
||||
return await this.incidentsRepository.create({
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
startTime: Date.now().toString(),
|
||||
status: true,
|
||||
statusCode,
|
||||
message,
|
||||
};
|
||||
return await this.incidentsRepository.create(incident);
|
||||
statusCode: isBreach ? 9999 : check.statusCode,
|
||||
message: isBreach ? this.notificationMessageBuilder.buildThresholdBreachMessage(monitor, check, decision.thresholdBreaches) : undefined,
|
||||
});
|
||||
}
|
||||
case "status_up":
|
||||
case "threshold_resolved": {
|
||||
if (!activeIncident) return null;
|
||||
activeIncident.status = false;
|
||||
activeIncident.endTime = Date.now().toString();
|
||||
activeIncident.resolutionType = "automatic";
|
||||
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
|
||||
}
|
||||
}
|
||||
|
||||
if (!decision.shouldResolveIncident || !activeIncident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeIncident.status = false;
|
||||
activeIncident.endTime = Date.now().toString();
|
||||
activeIncident.resolutionType = "automatic";
|
||||
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
|
||||
};
|
||||
|
||||
resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string) => {
|
||||
|
||||
@@ -39,24 +39,21 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
||||
clientHost,
|
||||
metadata: {
|
||||
teamId: monitor.teamId,
|
||||
notificationReason: decision.notificationReason,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private determineNotificationType(decision: MonitorActionDecision, monitor: Monitor): NotificationType {
|
||||
// Down status has highest priority (critical)
|
||||
if (monitor.status === "down") {
|
||||
return "monitor_down";
|
||||
}
|
||||
|
||||
const isDocker = monitor.type === "docker";
|
||||
switch (decision.notificationReason) {
|
||||
switch (decision.transition) {
|
||||
case "status_down":
|
||||
return "monitor_down";
|
||||
case "threshold_breach":
|
||||
return isDocker ? "container_breach" : "threshold_breach";
|
||||
case "threshold_resolved":
|
||||
return isDocker ? "container_resolved" : "threshold_resolved";
|
||||
default:
|
||||
case "status_up":
|
||||
case null:
|
||||
return "monitor_up";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +115,7 @@ export class NotificationsService implements INotificationsService {
|
||||
};
|
||||
|
||||
handleNotifications = async (monitor: Monitor, check: Check, decision: MonitorActionDecision) => {
|
||||
if (!decision.shouldSendNotification) {
|
||||
return false;
|
||||
}
|
||||
if (decision.transition === null) return false;
|
||||
|
||||
// Send notifications based on decision
|
||||
return await this.sendNotifications(monitor, check, decision);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { HardwareMetricKey } from "@/domain/monitors/monitor.type.js";
|
||||
import { MonitorActionDecision } from "@/worker/worker.interface.js";
|
||||
|
||||
export const NotificationChannels = [
|
||||
"email",
|
||||
@@ -120,6 +119,5 @@ export interface NotificationMessage {
|
||||
clientHost: string;
|
||||
metadata: {
|
||||
teamId: string;
|
||||
notificationReason: MonitorActionDecision["notificationReason"];
|
||||
};
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import type { QueueMode } from "@/domain/app-settings/app-settings.type.js";
|
||||
|
||||
export type JobHandler = (job: Job) => Promise<void>;
|
||||
export type JobHandlers = Record<JobType, JobHandler>;
|
||||
|
||||
export const MonitorTransitions = ["status_down", "status_up", "threshold_breach", "threshold_resolved"] as const;
|
||||
export type MonitorTransition = (typeof MonitorTransitions)[number];
|
||||
|
||||
export interface MonitorActionDecision {
|
||||
shouldCreateIncident: boolean;
|
||||
shouldResolveIncident: boolean;
|
||||
shouldSendNotification: boolean;
|
||||
incidentReason: "status_down" | "threshold_breach" | null;
|
||||
notificationReason: "status_change" | "threshold_breach" | "threshold_resolved" | null;
|
||||
transition: MonitorTransition | null;
|
||||
thresholdBreaches?: HardwareBreaches;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,7 @@ import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js";
|
||||
import { JobHandler, MonitorActionDecision, MonitorEvaluation } from "@/worker/worker.interface.js";
|
||||
|
||||
const SERVICE_NAME = "WorkerPipeline";
|
||||
const NO_ACTION: MonitorActionDecision = {
|
||||
shouldCreateIncident: false,
|
||||
shouldResolveIncident: false,
|
||||
shouldSendNotification: false,
|
||||
incidentReason: null,
|
||||
notificationReason: null,
|
||||
};
|
||||
|
||||
// The three entry points: the queue calls the two handlers, the buffer calls ingestChecks.
|
||||
// produce and evaluateCheck stay public on the class so tests can drive one stage without the queue or the buffer.
|
||||
export interface IWorkerPipeline {
|
||||
@@ -272,7 +266,7 @@ export class WorkerPipeline implements IWorkerPipeline {
|
||||
return {
|
||||
monitor,
|
||||
check,
|
||||
decision: { ...NO_ACTION },
|
||||
decision: { transition: null },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -288,30 +282,21 @@ export class WorkerPipeline implements IWorkerPipeline {
|
||||
};
|
||||
|
||||
private decide = (statusChange: StatusChangeResult): MonitorActionDecision => {
|
||||
const { monitor, statusChanged, prevStatus } = statusChange;
|
||||
const decision: MonitorActionDecision = { ...NO_ACTION };
|
||||
if (!statusChanged) return decision;
|
||||
const { monitor, statusChanged, prevStatus, thresholdBreaches } = statusChange;
|
||||
if (!statusChanged) return { transition: null };
|
||||
|
||||
if (monitor.status === "down") {
|
||||
decision.shouldCreateIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.incidentReason = "status_down";
|
||||
decision.notificationReason = "status_change";
|
||||
} else if (monitor.status === "breached") {
|
||||
if (statusChange.thresholdBreaches) decision.thresholdBreaches = statusChange.thresholdBreaches;
|
||||
decision.shouldCreateIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.incidentReason = "threshold_breach";
|
||||
decision.notificationReason = "threshold_breach";
|
||||
} else if (monitor.status === "up" && prevStatus === "breached") {
|
||||
decision.shouldResolveIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.notificationReason = "threshold_resolved";
|
||||
} else if (monitor.status === "up" && prevStatus === "down") {
|
||||
decision.shouldResolveIncident = true;
|
||||
decision.shouldSendNotification = true;
|
||||
decision.notificationReason = "status_change";
|
||||
if (monitor.status === "down") return { transition: "status_down" };
|
||||
|
||||
if (monitor.status === "breached") {
|
||||
return thresholdBreaches
|
||||
? // Hardware
|
||||
{ transition: "threshold_breach", thresholdBreaches }
|
||||
: // Docker
|
||||
{ transition: "threshold_breach" };
|
||||
}
|
||||
return decision;
|
||||
|
||||
if (monitor.status === "up" && prevStatus === "breached") return { transition: "threshold_resolved" };
|
||||
if (monitor.status === "up" && prevStatus === "down") return { transition: "status_up" };
|
||||
return { transition: null };
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user