mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add allAlertsResolvedAt and allIncidentsResolvedAt fields for resolve delay calculations
This commit is contained in:
@@ -543,6 +543,31 @@ export default class AlertEpisode extends BaseModel {
|
||||
})
|
||||
public resolvedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadAlertEpisode,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "All Alerts Resolved At",
|
||||
description:
|
||||
"When all alerts in this episode were first detected as resolved. Used for resolve delay calculation.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public allAlertsResolvedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -542,6 +542,31 @@ export default class IncidentEpisode extends BaseModel {
|
||||
})
|
||||
public resolvedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadIncidentEpisode,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "All Incidents Resolved At",
|
||||
description:
|
||||
"When all incidents in this episode were first detected as resolved. Used for resolve delay calculation.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public allIncidentsResolvedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -94,26 +94,18 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
|
||||
// Update episode's alertCount and lastAlertAddedAt
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
try {
|
||||
await AlertEpisodeService.updateAlertCount(
|
||||
createdItem.alertEpisodeId!,
|
||||
);
|
||||
await AlertEpisodeService.updateLastAlertAddedAt(
|
||||
createdItem.alertEpisodeId!,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error updating episode counts in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
logger.error(
|
||||
`Critical error in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
});
|
||||
try {
|
||||
await AlertEpisodeService.updateAlertCount(
|
||||
createdItem.alertEpisodeId!,
|
||||
);
|
||||
await AlertEpisodeService.updateLastAlertAddedAt(
|
||||
createdItem.alertEpisodeId!,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error updating episode counts in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get alert details for feed
|
||||
const alert: Alert | null = await AlertService.findOneById({
|
||||
|
||||
@@ -815,11 +815,12 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
});
|
||||
|
||||
// Clear resolved timestamp
|
||||
// Clear resolved timestamp and allAlertsResolvedAt
|
||||
await this.updateOneById({
|
||||
id: episodeId,
|
||||
data: {
|
||||
resolvedAt: undefined as any,
|
||||
allAlertsResolvedAt: undefined as any,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
|
||||
@@ -688,6 +688,7 @@ class AlertGroupingEngineServiceClass {
|
||||
newEpisode.alertGroupingRuleId = rule.id!;
|
||||
newEpisode.groupingKey = groupingKey;
|
||||
newEpisode.isManuallyCreated = false;
|
||||
newEpisode.lastAlertAddedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
// Set severity from alert
|
||||
if (alert.alertSeverityId) {
|
||||
|
||||
@@ -96,26 +96,18 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
|
||||
// Update episode's incidentCount and lastIncidentAddedAt
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
try {
|
||||
await IncidentEpisodeService.updateIncidentCount(
|
||||
createdItem.incidentEpisodeId!,
|
||||
);
|
||||
await IncidentEpisodeService.updateLastIncidentAddedAt(
|
||||
createdItem.incidentEpisodeId!,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error updating episode counts in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
logger.error(
|
||||
`Critical error in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
});
|
||||
try {
|
||||
await IncidentEpisodeService.updateIncidentCount(
|
||||
createdItem.incidentEpisodeId!,
|
||||
);
|
||||
await IncidentEpisodeService.updateLastIncidentAddedAt(
|
||||
createdItem.incidentEpisodeId!,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error updating episode counts in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get incident details for feed
|
||||
const incident: Incident | null = await IncidentService.findOneById({
|
||||
|
||||
@@ -609,6 +609,17 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
cascadeToIncidents: cascadeToIncidents,
|
||||
});
|
||||
|
||||
// Clear allIncidentsResolvedAt when episode is reopened
|
||||
await this.updateOneById({
|
||||
id: episodeId,
|
||||
data: {
|
||||
allIncidentsResolvedAt: undefined as any,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
|
||||
@@ -757,6 +757,7 @@ class IncidentGroupingEngineServiceClass {
|
||||
newEpisode.incidentGroupingRuleId = rule.id!;
|
||||
newEpisode.groupingKey = groupingKey;
|
||||
newEpisode.isManuallyCreated = false;
|
||||
newEpisode.lastIncidentAddedAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
// Set severity from incident
|
||||
if (incident.incidentSeverityId) {
|
||||
|
||||
@@ -38,6 +38,7 @@ RunCron(
|
||||
projectId: true,
|
||||
alertGroupingRuleId: true,
|
||||
lastAlertAddedAt: true,
|
||||
allAlertsResolvedAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -134,7 +135,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
|
||||
// Check if all alerts are in resolved state or higher
|
||||
let allResolved: boolean = true;
|
||||
let lastResolvedAt: Date | null = null;
|
||||
|
||||
for (const alertId of alertIds) {
|
||||
const alert: Alert | null = await AlertService.findOneById({
|
||||
@@ -143,7 +143,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
currentAlertState: {
|
||||
order: true,
|
||||
},
|
||||
updatedAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -160,31 +159,58 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
allResolved = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Track the latest resolved time among alerts
|
||||
if (alert.updatedAt) {
|
||||
if (!lastResolvedAt || alert.updatedAt > lastResolvedAt) {
|
||||
lastResolvedAt = alert.updatedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allResolved) {
|
||||
// If any alert is unresolved, clear allAlertsResolvedAt
|
||||
if (episode.allAlertsResolvedAt) {
|
||||
await AlertEpisodeService.updateOneById({
|
||||
id: episode.id,
|
||||
data: {
|
||||
allAlertsResolvedAt: undefined as any,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`AlertEpisode:AutoResolve - Episode ${episode.id} has unresolved alerts`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All alerts are resolved. Check if resolve delay has passed (only if enabled)
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0 && lastResolvedAt) {
|
||||
const timeSinceLastResolved: number =
|
||||
// All alerts are resolved. Set allAlertsResolvedAt if not already set.
|
||||
if (!episode.allAlertsResolvedAt) {
|
||||
await AlertEpisodeService.updateOneById({
|
||||
id: episode.id,
|
||||
data: {
|
||||
allAlertsResolvedAt: OneUptimeDate.getCurrentDate(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// If resolve delay is enabled, return and wait for the delay
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0) {
|
||||
logger.debug(
|
||||
`AlertEpisode:AutoResolve - Episode ${episode.id} all alerts resolved, starting resolve delay (${resolveDelayMinutes} minutes)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if resolve delay has passed (only if enabled)
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0 && episode.allAlertsResolvedAt) {
|
||||
const timeSinceAllResolved: number =
|
||||
OneUptimeDate.getDifferenceInMinutes(
|
||||
lastResolvedAt,
|
||||
episode.allAlertsResolvedAt,
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
);
|
||||
|
||||
if (timeSinceLastResolved < resolveDelayMinutes) {
|
||||
if (timeSinceAllResolved < resolveDelayMinutes) {
|
||||
logger.debug(
|
||||
`AlertEpisode:AutoResolve - Episode ${episode.id} waiting for resolve delay (${resolveDelayMinutes} minutes)`,
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ RunCron(
|
||||
projectId: true,
|
||||
incidentGroupingRuleId: true,
|
||||
lastIncidentAddedAt: true,
|
||||
allIncidentsResolvedAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -137,7 +138,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
|
||||
// Check if all incidents are in resolved state or higher
|
||||
let allResolved: boolean = true;
|
||||
let lastResolvedAt: Date | null = null;
|
||||
|
||||
for (const incidentId of incidentIds) {
|
||||
const incident: Incident | null = await IncidentService.findOneById({
|
||||
@@ -146,7 +146,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
currentIncidentState: {
|
||||
order: true,
|
||||
},
|
||||
updatedAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -163,31 +162,58 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
|
||||
allResolved = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Track the latest resolved time among incidents
|
||||
if (incident.updatedAt) {
|
||||
if (!lastResolvedAt || incident.updatedAt > lastResolvedAt) {
|
||||
lastResolvedAt = incident.updatedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allResolved) {
|
||||
// If any incident is unresolved, clear allIncidentsResolvedAt
|
||||
if (episode.allIncidentsResolvedAt) {
|
||||
await IncidentEpisodeService.updateOneById({
|
||||
id: episode.id,
|
||||
data: {
|
||||
allIncidentsResolvedAt: undefined as any,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`IncidentEpisode:AutoResolve - Episode ${episode.id} has unresolved incidents`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All incidents are resolved. Check if resolve delay has passed (only if enabled)
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0 && lastResolvedAt) {
|
||||
const timeSinceLastResolved: number =
|
||||
// All incidents are resolved. Set allIncidentsResolvedAt if not already set.
|
||||
if (!episode.allIncidentsResolvedAt) {
|
||||
await IncidentEpisodeService.updateOneById({
|
||||
id: episode.id,
|
||||
data: {
|
||||
allIncidentsResolvedAt: OneUptimeDate.getCurrentDate(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// If resolve delay is enabled, return and wait for the delay
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0) {
|
||||
logger.debug(
|
||||
`IncidentEpisode:AutoResolve - Episode ${episode.id} all incidents resolved, starting resolve delay (${resolveDelayMinutes} minutes)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if resolve delay has passed (only if enabled)
|
||||
if (enableResolveDelay && resolveDelayMinutes > 0 && episode.allIncidentsResolvedAt) {
|
||||
const timeSinceAllResolved: number =
|
||||
OneUptimeDate.getDifferenceInMinutes(
|
||||
lastResolvedAt,
|
||||
episode.allIncidentsResolvedAt,
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
);
|
||||
|
||||
if (timeSinceLastResolved < resolveDelayMinutes) {
|
||||
if (timeSinceAllResolved < resolveDelayMinutes) {
|
||||
logger.debug(
|
||||
`IncidentEpisode:AutoResolve - Episode ${episode.id} waiting for resolve delay (${resolveDelayMinutes} minutes)`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user