mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-29 05:00:24 -05:00
feat: add incident model, module and service with StatusService integration
- Create Incident model with resolution tracking fields - Implement IncidentModule for data access - Implement IncidentService with business logic - Integrate with StatusService to auto-create/resolve incidents on status change
This commit is contained in:
@@ -17,6 +17,7 @@ import DiagnosticService from "../service/v1/business/diagnosticService.js";
|
||||
import InviteService from "../service/v1/business/inviteService.js";
|
||||
import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js";
|
||||
import MonitorService from "../service/v1/business/monitorService.js";
|
||||
import IncidentService from "../service/v1/business/incidentService.js";
|
||||
import papaparse from "papaparse";
|
||||
import axios from "axios";
|
||||
import got from "got";
|
||||
@@ -57,6 +58,7 @@ import MonitorStats from "../db/v1/models/MonitorStats.js";
|
||||
import Notification from "../db/v1/models/Notification.js";
|
||||
import RecoveryToken from "../db/v1/models/RecoveryToken.js";
|
||||
import AppSettings from "../db/v1/models/AppSettings.js";
|
||||
import Incident from "../db/v1/models/Incident.js";
|
||||
|
||||
import InviteModule from "../db/v1/modules/inviteModule.js";
|
||||
import CheckModule from "../db/v1/modules/checkModule.js";
|
||||
@@ -67,6 +69,7 @@ import MonitorModule from "../db/v1/modules/monitorModule.js";
|
||||
import NotificationModule from "../db/v1/modules/notificationModule.js";
|
||||
import RecoveryModule from "../db/v1/modules/recoveryModule.js";
|
||||
import SettingsModule from "../db/v1/modules/settingsModule.js";
|
||||
import IncidentModule from "../db/v1/modules/incidentModule.js";
|
||||
|
||||
// V2 Business
|
||||
import AuthServiceV2 from "../service/v2/business/AuthService.js";
|
||||
@@ -120,6 +123,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const notificationModule = new NotificationModule({ Notification, Monitor });
|
||||
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
|
||||
const settingsModule = new SettingsModule({ AppSettings });
|
||||
const incidentModule = new IncidentModule({ logger, Incident, Monitor, User });
|
||||
|
||||
const db = new MongoDB({
|
||||
logger,
|
||||
@@ -133,6 +137,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
notificationModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
incidentModule,
|
||||
});
|
||||
|
||||
await db.connect();
|
||||
@@ -153,7 +158,17 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
});
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
const bufferService = new BufferService({ db, logger, envSettings });
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService });
|
||||
|
||||
const errorService = new ErrorService();
|
||||
|
||||
const incidentService = new IncidentService({
|
||||
db,
|
||||
logger,
|
||||
errorService,
|
||||
stringService,
|
||||
});
|
||||
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService });
|
||||
|
||||
const notificationUtils = new NotificationUtils({
|
||||
stringService,
|
||||
@@ -169,8 +184,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
notificationUtils,
|
||||
});
|
||||
|
||||
const errorService = new ErrorService();
|
||||
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||
db,
|
||||
logger,
|
||||
@@ -275,6 +288,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
inviteService,
|
||||
maintenanceWindowService,
|
||||
monitorService,
|
||||
incidentService,
|
||||
errorService,
|
||||
logger,
|
||||
//v2
|
||||
|
||||
@@ -19,6 +19,7 @@ class MongoDB {
|
||||
pageSpeedCheckModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
incidentModule,
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.envSettings = envSettings;
|
||||
@@ -34,6 +35,7 @@ class MongoDB {
|
||||
this.settingsModule = settingsModule;
|
||||
this.statusPageModule = statusPageModule;
|
||||
this.networkCheckModule = networkCheckModule;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const IncidentSchema = mongoose.Schema({
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
required: true,
|
||||
immutable: true,
|
||||
index: true
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
required: true,
|
||||
immutable: true,
|
||||
index: true
|
||||
},
|
||||
startTime: {
|
||||
type: Date,
|
||||
required: true,
|
||||
immutable: true
|
||||
},
|
||||
endTime: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ["active", "resolved"],
|
||||
default: "active",
|
||||
index: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
default: null
|
||||
},
|
||||
resolutionType: {
|
||||
type: String,
|
||||
enum: ["automatic", "manual"],
|
||||
default: null
|
||||
},
|
||||
resolvedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
default: null
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
checks: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Check"
|
||||
}]
|
||||
}, { timestamps: true });
|
||||
|
||||
IncidentSchema.index({ monitorId: 1, status: 1 });
|
||||
IncidentSchema.index({ teamId: 1, status: 1 });
|
||||
IncidentSchema.index({ teamId: 1, startTime: -1 });
|
||||
IncidentSchema.index({ status: 1, startTime: -1 });
|
||||
IncidentSchema.index({ resolutionType: 1, status: 1 });
|
||||
IncidentSchema.index({ resolvedBy: 1, status: 1 });
|
||||
IncidentSchema.index({ createdAt: -1 });
|
||||
|
||||
const Incident = mongoose.model("Incident", IncidentSchema);
|
||||
|
||||
export default Incident;
|
||||
@@ -0,0 +1,258 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
const SERVICE_NAME = "incidentModule";
|
||||
|
||||
const dateRangeLookup = {
|
||||
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
|
||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
all: undefined,
|
||||
};
|
||||
|
||||
class IncidentModule {
|
||||
constructor({ logger, Incident, Monitor, User }) {
|
||||
this.logger = logger;
|
||||
this.Incident = Incident;
|
||||
this.Monitor = Monitor;
|
||||
this.User = User;
|
||||
}
|
||||
|
||||
createIncident = async (incidentData) => {
|
||||
try {
|
||||
const incident = await this.Incident.create(incidentData);
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createIncidents = async (incidents) => {
|
||||
try {
|
||||
await this.Incident.insertMany(incidents, { ordered: false });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createIncidents";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getActiveIncidentByMonitor = async (monitorId) => {
|
||||
try {
|
||||
return await this.Incident.findOne({ monitorId: new ObjectId(monitorId), status: "active" });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getActiveIncidentByMonitor";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentById = async (incidentId) => {
|
||||
try {
|
||||
return await this.Incident.findById(incidentId)
|
||||
.populate("monitorId", "name type url")
|
||||
.populate("resolvedBy", "name email");
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
resolveIncident = async (incidentId, { resolutionType = "automatic", resolvedBy = null, comment = null, endTime = new Date() } = {}) => {
|
||||
try {
|
||||
const update = {
|
||||
status: "resolved",
|
||||
endTime,
|
||||
resolutionType,
|
||||
resolvedBy,
|
||||
...(comment !== null && { comment }),
|
||||
};
|
||||
const incident = await this.Incident.findOneAndUpdate(
|
||||
{ _id: new ObjectId(incidentId) },
|
||||
{ $set: update },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!incident) {
|
||||
throw new Error("Incident not found");
|
||||
}
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "resolveIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
addCheckToIncident = async (incidentId, checkId) => {
|
||||
try {
|
||||
const incident = await this.Incident.findOneAndUpdate(
|
||||
{ _id: new ObjectId(incidentId) },
|
||||
{ $addToSet: { checks: new ObjectId(checkId) } },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!incident) {
|
||||
throw new Error("Incident not found");
|
||||
}
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "addCheckToIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, filter, page, rowsPerPage, status, monitorId, resolutionType }) => {
|
||||
try {
|
||||
page = parseInt(page);
|
||||
rowsPerPage = parseInt(rowsPerPage);
|
||||
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
...(status && { status }),
|
||||
...(monitorId && { monitorId: new ObjectId(monitorId) }),
|
||||
...(resolutionType && { resolutionType }),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
startTime: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
if (filter !== undefined) {
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "active":
|
||||
matchStage.status = "active";
|
||||
break;
|
||||
case "resolved":
|
||||
matchStage.status = "resolved";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const incidents = await this.Incident.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { startTime: sortOrder } },
|
||||
{
|
||||
$facet: {
|
||||
summary: [{ $count: "incidentsCount" }],
|
||||
incidents: [{ $skip: skip }, { $limit: rowsPerPage }],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
incidentsCount: {
|
||||
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
|
||||
},
|
||||
incidents: {
|
||||
$ifNull: ["$incidents", []],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return incidents[0];
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentsByTeam";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async ({ teamId, dateRange }) => {
|
||||
try {
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
startTime: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const summary = await this.Incident.aggregate([
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 },
|
||||
manualResolutions: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
resolved: 0,
|
||||
manual: 0,
|
||||
};
|
||||
|
||||
summary.forEach((item) => {
|
||||
result.total += item.count;
|
||||
if (item._id === "active") {
|
||||
result.active = item.count;
|
||||
}
|
||||
if (item._id === "resolved") {
|
||||
result.resolved = item.count;
|
||||
}
|
||||
result.manual += item.manualResolutions;
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentSummary";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteIncidentsByMonitor = async (monitorId) => {
|
||||
try {
|
||||
const result = await this.Incident.deleteMany({ monitorId: new ObjectId(monitorId) });
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteIncidentsByMonitor";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteIncidentsByTeamId = async (teamId) => {
|
||||
try {
|
||||
const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 });
|
||||
const monitorIds = teamMonitors.map((monitor) => monitor._id);
|
||||
const deleteResult = await this.Incident.deleteMany({ monitorId: { $in: monitorIds } });
|
||||
return deleteResult.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteIncidentsByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default IncidentModule;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Typography, Select } from "@mui/material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import type { SelectProps } from "@mui/material/Select";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
export const SelectInput: React.FC<SelectProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
sx={{
|
||||
height: "34px",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemTypes = string | number;
|
||||
interface SelectItem {
|
||||
_id: ItemTypes;
|
||||
name: string;
|
||||
}
|
||||
export type CustomSelectProps = SelectProps & {
|
||||
items: SelectItem[];
|
||||
placeholder?: string;
|
||||
isHidden?: boolean;
|
||||
hasError?: boolean;
|
||||
};
|
||||
|
||||
export const SelectFromItems: React.FC<CustomSelectProps> = (
|
||||
{ items, placeholder, isHidden = false, hasError = false, ...props }
|
||||
) => {
|
||||
return (
|
||||
<SelectInput
|
||||
error={hasError}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
displayEmpty
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
renderValue={(selected) => {
|
||||
if (!selected) {
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
color="text.secondary"
|
||||
>
|
||||
{placeholder ?? ""}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
const selectedItem = items.find((item) => item._id === selected);
|
||||
const displayName = selectedItem ? selectedItem.name : placeholder;
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
title={displayName}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item._id}
|
||||
value={item._id}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectInput>
|
||||
);
|
||||
}
|
||||
|
||||
SelectInput.displayName = "SelectInput";
|
||||
@@ -0,0 +1,277 @@
|
||||
const SERVICE_NAME = "incidentService";
|
||||
|
||||
class IncidentService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, logger, errorService, stringService }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return IncidentService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createIncident = async (monitor, check) => {
|
||||
try {
|
||||
if (!monitor || !monitor._id) {
|
||||
throw this.errorService.createBadRequestError("Monitor is required");
|
||||
}
|
||||
|
||||
if (!check || !check._id) {
|
||||
throw this.errorService.createBadRequestError("Check is required");
|
||||
}
|
||||
|
||||
const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id);
|
||||
|
||||
if (activeIncident) {
|
||||
await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id);
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "createIncident",
|
||||
message: `Check added to existing active incident for monitor ${monitor.name}`,
|
||||
incidentId: activeIncident._id,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
|
||||
return activeIncident;
|
||||
}
|
||||
|
||||
const incidentData = {
|
||||
monitorId: monitor._id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
startTime: new Date(),
|
||||
status: "active",
|
||||
message: check.message || null,
|
||||
statusCode: check.statusCode || null,
|
||||
checks: [check._id],
|
||||
};
|
||||
|
||||
const incident = await this.db.incidentModule.createIncident(incidentData);
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "createIncident",
|
||||
message: `New incident created for monitor ${monitor.name}`,
|
||||
incidentId: incident._id,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "createIncident",
|
||||
message: error.message,
|
||||
monitorId: monitor?._id,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
resolveIncident = async (monitor, check) => {
|
||||
try {
|
||||
if (!monitor || !monitor._id) {
|
||||
throw this.errorService.createBadRequestError("Monitor is required");
|
||||
}
|
||||
|
||||
const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id);
|
||||
|
||||
if (!activeIncident) {
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: `No active incident found for monitor ${monitor.name}`,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id);
|
||||
|
||||
const resolvedIncident = await this.db.incidentModule.resolveIncident(activeIncident._id, {
|
||||
resolutionType: "automatic",
|
||||
resolvedBy: null,
|
||||
endTime: new Date(),
|
||||
});
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: `Incident automatically resolved for monitor ${monitor.name}`,
|
||||
incidentId: resolvedIncident._id,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
|
||||
return resolvedIncident;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: error.message,
|
||||
monitorId: monitor?._id,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
resolveIncidentManually = async ({ incidentId, userId, teamId, comment }) => {
|
||||
try {
|
||||
if (!incidentId) {
|
||||
throw this.errorService.createBadRequestError("No incident ID in request");
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw this.errorService.createBadRequestError("No user ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const incident = await this.db.incidentModule.getIncidentById(incidentId);
|
||||
|
||||
if (!incident) {
|
||||
throw this.errorService.createNotFoundError("Incident not found");
|
||||
}
|
||||
|
||||
if (!incident.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
if (incident.status === "resolved") {
|
||||
throw this.errorService.createBadRequestError("Incident is already resolved");
|
||||
}
|
||||
|
||||
const resolvedIncident = await this.db.incidentModule.resolveIncident(incidentId, {
|
||||
resolutionType: "manual",
|
||||
resolvedBy: userId,
|
||||
comment: comment || null,
|
||||
endTime: new Date(),
|
||||
});
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "resolveIncidentManually",
|
||||
message: `Incident manually resolved by user`,
|
||||
incidentId: resolvedIncident._id,
|
||||
userId,
|
||||
});
|
||||
|
||||
return resolvedIncident;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "resolveIncidentManually",
|
||||
message: error.message,
|
||||
incidentId,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
getIncidentsByTeam = async ({ teamId, query }) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const { sortOrder, dateRange, filter, page, rowsPerPage, status, monitorId, resolutionType } = query || {};
|
||||
|
||||
const result = await this.db.incidentModule.getIncidentsByTeam({
|
||||
teamId,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
status,
|
||||
monitorId,
|
||||
resolutionType,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getIncidentsByTeam",
|
||||
message: error.message,
|
||||
teamId,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
getIncidentSummary = async ({ teamId, query }) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const { dateRange } = query || {};
|
||||
|
||||
const summary = await this.db.incidentModule.getIncidentSummary({
|
||||
teamId,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
return summary;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getIncidentSummary",
|
||||
message: error.message,
|
||||
teamId,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentById = async ({ incidentId, teamId }) => {
|
||||
try {
|
||||
if (!incidentId) {
|
||||
throw this.errorService.createBadRequestError("No incident ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const incident = await this.db.incidentModule.getIncidentById(incidentId);
|
||||
|
||||
if (!incident) {
|
||||
throw this.errorService.createNotFoundError("Incident not found");
|
||||
}
|
||||
|
||||
if (!incident.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getIncidentById",
|
||||
message: error.message,
|
||||
incidentId,
|
||||
error: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default IncidentService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MonitorStats from "../../../db/v1/models/MonitorStats.js";
|
||||
import Check from "../../../db/v1/models/Check.js";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
class StatusService {
|
||||
@@ -7,11 +8,13 @@ class StatusService {
|
||||
/**
|
||||
* @param {{
|
||||
* buffer: import("./bufferService.js").BufferService
|
||||
* incidentService: import("../business/incidentService.js").IncidentService
|
||||
* }}
|
||||
*/ constructor({ db, logger, buffer }) {
|
||||
*/ constructor({ db, logger, buffer, incidentService }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
this.incidentService = incidentService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -170,6 +173,41 @@ class StatusService {
|
||||
prevStatus,
|
||||
newStatus,
|
||||
});
|
||||
|
||||
let savedCheck = check;
|
||||
if (!check._id) {
|
||||
try {
|
||||
const checkModel = new Check(check);
|
||||
savedCheck = await checkModel.save();
|
||||
} catch (checkError) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Failed to save check immediately: ${checkError.message}`,
|
||||
monitorId: monitor._id,
|
||||
stack: checkError.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (newStatus === false) {
|
||||
await this.incidentService.createIncident(monitor, savedCheck);
|
||||
} else if (prevStatus === false) {
|
||||
await this.incidentService.resolveIncident(monitor, savedCheck);
|
||||
}
|
||||
} catch (incidentError) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Failed to handle incident: ${incidentError.message}`,
|
||||
monitorId: monitor._id,
|
||||
statusChanged,
|
||||
prevStatus,
|
||||
newStatus,
|
||||
stack: incidentError.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
monitor.status = newStatus;
|
||||
|
||||
Reference in New Issue
Block a user