@oneuptime/common 11.3.21 → 11.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
- package/Models/AnalyticsModels/Log.ts +1 -0
- package/Models/AnalyticsModels/Metric.ts +1 -0
- package/Models/AnalyticsModels/MonitorLog.ts +1 -0
- package/Models/AnalyticsModels/Span.ts +1 -0
- package/Models/DatabaseModels/Alert.ts +100 -0
- package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
- package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
- package/Models/DatabaseModels/AlertSeverity.ts +2 -0
- package/Models/DatabaseModels/AlertState.ts +2 -0
- package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
- package/Models/DatabaseModels/Incident.ts +100 -0
- package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
- package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
- package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
- package/Models/DatabaseModels/Index.ts +6 -0
- package/Models/DatabaseModels/Label.ts +2 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
- package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
- package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/AlertReminderRuleService.ts +253 -0
- package/Server/Services/AlertService.ts +106 -0
- package/Server/Services/IncidentReminderRuleService.ts +256 -0
- package/Server/Services/IncidentService.ts +143 -0
- package/Server/Services/Index.ts +6 -0
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
- package/Server/Services/ScheduledMaintenanceService.ts +127 -0
- package/Server/Services/TelemetryExceptionService.ts +123 -0
- package/Server/Services/UserNotificationSettingService.ts +24 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
- package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
- package/Types/Email/EmailTemplateType.ts +3 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
- package/Types/Permission.ts +144 -0
- package/Types/Reminder/ReminderStopState.ts +6 -0
- package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
- package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Alert.js +103 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +103 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +6 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Label.js +2 -0
- package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +87 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +116 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +3 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/Permission.js +126 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
2
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
3
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
4
|
+
import DatabaseService from "./DatabaseService";
|
|
5
|
+
import Model from "../../Models/DatabaseModels/AlertReminderRule";
|
|
6
|
+
import Alert from "../../Models/DatabaseModels/Alert";
|
|
7
|
+
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
|
8
|
+
import AlertState from "../../Models/DatabaseModels/AlertState";
|
|
9
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
10
|
+
import AlertService from "./AlertService";
|
|
11
|
+
import AlertStateService from "./AlertStateService";
|
|
12
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
13
|
+
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
14
|
+
import ObjectID from "../../Types/ObjectID";
|
|
15
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
16
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
17
|
+
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
18
|
+
|
|
19
|
+
export class Service extends DatabaseService<Model> {
|
|
20
|
+
public constructor() {
|
|
21
|
+
super(Model);
|
|
22
|
+
if (IsBillingEnabled) {
|
|
23
|
+
this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@CaptureSpan()
|
|
28
|
+
protected override async onBeforeCreate(
|
|
29
|
+
createBy: CreateBy<Model>,
|
|
30
|
+
): Promise<OnCreate<Model>> {
|
|
31
|
+
// Auto-assign order on creation
|
|
32
|
+
if (!createBy.data.order) {
|
|
33
|
+
const highestOrderRule: Model | null = await this.findOneBy({
|
|
34
|
+
query: {
|
|
35
|
+
projectId: createBy.data.projectId!,
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
order: true,
|
|
39
|
+
},
|
|
40
|
+
sort: {
|
|
41
|
+
order: SortOrder.Descending,
|
|
42
|
+
},
|
|
43
|
+
props: {
|
|
44
|
+
isRoot: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
createBy.data.order = (highestOrderRule?.order || 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
createBy,
|
|
53
|
+
carryForward: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@CaptureSpan()
|
|
58
|
+
protected override async onCreateSuccess(
|
|
59
|
+
_onCreate: OnCreate<Model>,
|
|
60
|
+
createdItem: Model,
|
|
61
|
+
): Promise<Model> {
|
|
62
|
+
if (createdItem.projectId) {
|
|
63
|
+
await this.refreshSchedulesForOpenAlerts(createdItem.projectId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return createdItem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@CaptureSpan()
|
|
70
|
+
protected override async onUpdateSuccess(
|
|
71
|
+
onUpdate: OnUpdate<Model>,
|
|
72
|
+
_updatedItemIds: Array<ObjectID>,
|
|
73
|
+
): Promise<OnUpdate<Model>> {
|
|
74
|
+
const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
|
|
75
|
+
| ObjectID
|
|
76
|
+
| undefined;
|
|
77
|
+
|
|
78
|
+
if (projectId) {
|
|
79
|
+
await this.refreshSchedulesForOpenAlerts(projectId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return onUpdate;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@CaptureSpan()
|
|
86
|
+
protected override async onDeleteSuccess(
|
|
87
|
+
onDelete: OnDelete<Model>,
|
|
88
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
89
|
+
): Promise<OnDelete<Model>> {
|
|
90
|
+
const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
|
|
91
|
+
| ObjectID
|
|
92
|
+
| undefined;
|
|
93
|
+
|
|
94
|
+
if (projectId) {
|
|
95
|
+
await this.refreshSchedulesForOpenAlerts(projectId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return onDelete;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@CaptureSpan()
|
|
102
|
+
public async refreshSchedulesForOpenAlerts(
|
|
103
|
+
projectId: ObjectID,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
const unresolvedStates: Array<AlertState> =
|
|
107
|
+
await AlertStateService.findBy({
|
|
108
|
+
query: {
|
|
109
|
+
projectId: projectId,
|
|
110
|
+
isResolvedState: false,
|
|
111
|
+
},
|
|
112
|
+
select: {
|
|
113
|
+
_id: true,
|
|
114
|
+
},
|
|
115
|
+
limit: LIMIT_PER_PROJECT,
|
|
116
|
+
skip: 0,
|
|
117
|
+
props: {
|
|
118
|
+
isRoot: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const unresolvedStateIds: Array<ObjectID> = unresolvedStates
|
|
123
|
+
.map((state: AlertState) => {
|
|
124
|
+
return state.id!;
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
|
|
128
|
+
if (unresolvedStateIds.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const openAlerts: Array<Alert> = await AlertService.findBy({
|
|
133
|
+
query: {
|
|
134
|
+
projectId: projectId,
|
|
135
|
+
currentAlertStateId: QueryHelper.any(unresolvedStateIds),
|
|
136
|
+
},
|
|
137
|
+
select: {
|
|
138
|
+
_id: true,
|
|
139
|
+
},
|
|
140
|
+
limit: LIMIT_MAX,
|
|
141
|
+
skip: 0,
|
|
142
|
+
props: {
|
|
143
|
+
isRoot: true,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for (const alert of openAlerts) {
|
|
148
|
+
try {
|
|
149
|
+
await AlertService.refreshReminderSchedule({
|
|
150
|
+
alertId: alert.id!,
|
|
151
|
+
projectId: projectId,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.error(
|
|
155
|
+
`Failed to refresh reminder schedule for alert ${alert.id}: ${error}`,
|
|
156
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error(
|
|
162
|
+
`Failed to refresh reminder schedules for open alerts: ${error}`,
|
|
163
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@CaptureSpan()
|
|
169
|
+
public async findMatchingRule(data: {
|
|
170
|
+
projectId: ObjectID;
|
|
171
|
+
alertSeverityId?: ObjectID | undefined;
|
|
172
|
+
labelIds?: Array<ObjectID> | undefined;
|
|
173
|
+
}): Promise<Model | null> {
|
|
174
|
+
// Get all enabled rules sorted by order. First matching rule wins.
|
|
175
|
+
const rules: Array<Model> = await this.findBy({
|
|
176
|
+
query: {
|
|
177
|
+
projectId: data.projectId,
|
|
178
|
+
isEnabled: true,
|
|
179
|
+
},
|
|
180
|
+
sort: {
|
|
181
|
+
order: SortOrder.Ascending,
|
|
182
|
+
},
|
|
183
|
+
select: {
|
|
184
|
+
_id: true,
|
|
185
|
+
name: true,
|
|
186
|
+
order: true,
|
|
187
|
+
reminderIntervalInMinutes: true,
|
|
188
|
+
stopRemindersOnState: true,
|
|
189
|
+
alertSeverities: {
|
|
190
|
+
_id: true,
|
|
191
|
+
},
|
|
192
|
+
labels: {
|
|
193
|
+
_id: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
limit: 100,
|
|
197
|
+
skip: 0,
|
|
198
|
+
props: {
|
|
199
|
+
isRoot: true,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for (const rule of rules) {
|
|
204
|
+
if (rule.alertSeverities && rule.alertSeverities.length > 0) {
|
|
205
|
+
if (!data.alertSeverityId) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const severityIds: Array<string> = rule.alertSeverities.map(
|
|
210
|
+
(severity: AlertSeverity) => {
|
|
211
|
+
return severity.id?.toString() || "";
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (!severityIds.includes(data.alertSeverityId.toString())) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (rule.labels && rule.labels.length > 0) {
|
|
221
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
|
|
226
|
+
return label.id?.toString() || "";
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const hasMatchingLabel: boolean = data.labelIds.some(
|
|
230
|
+
(labelId: ObjectID) => {
|
|
231
|
+
return ruleLabelIds.includes(labelId.toString());
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (!hasMatchingLabel) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Rule with no severities and no labels matches all alerts.
|
|
241
|
+
logger.debug(
|
|
242
|
+
`Alert reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
|
|
243
|
+
{ projectId: data.projectId?.toString() } as LogAttributes,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return rule;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default new Service();
|
|
@@ -50,6 +50,8 @@ import Label from "../../Models/DatabaseModels/Label";
|
|
|
50
50
|
import LabelService from "./LabelService";
|
|
51
51
|
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
|
52
52
|
import AlertSeverityService from "./AlertSeverityService";
|
|
53
|
+
import AlertReminderRuleService from "./AlertReminderRuleService";
|
|
54
|
+
import AlertReminderRule from "../../Models/DatabaseModels/AlertReminderRule";
|
|
53
55
|
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
|
54
56
|
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
|
55
57
|
import AlertWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/Alert";
|
|
@@ -121,6 +123,64 @@ export class Service extends DatabaseService<Model> {
|
|
|
121
123
|
return false;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
@CaptureSpan()
|
|
127
|
+
public async refreshReminderSchedule(data: {
|
|
128
|
+
alertId: ObjectID;
|
|
129
|
+
projectId: ObjectID;
|
|
130
|
+
}): Promise<void> {
|
|
131
|
+
const alert: Model | null = await this.findOneById({
|
|
132
|
+
id: data.alertId,
|
|
133
|
+
select: {
|
|
134
|
+
enableReminders: true,
|
|
135
|
+
alertSeverityId: true,
|
|
136
|
+
labels: {
|
|
137
|
+
_id: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
props: {
|
|
141
|
+
isRoot: true,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!alert) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let nextReminderNotificationAt: Date | null = null;
|
|
150
|
+
|
|
151
|
+
if (alert.enableReminders !== false) {
|
|
152
|
+
const matchingRule: AlertReminderRule | null =
|
|
153
|
+
await AlertReminderRuleService.findMatchingRule({
|
|
154
|
+
projectId: data.projectId,
|
|
155
|
+
alertSeverityId: alert.alertSeverityId,
|
|
156
|
+
labelIds: alert.labels?.map((label: Label) => {
|
|
157
|
+
return label.id!;
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
matchingRule &&
|
|
163
|
+
matchingRule.reminderIntervalInMinutes &&
|
|
164
|
+
!(await this.isAlertResolved({ alertId: data.alertId }))
|
|
165
|
+
) {
|
|
166
|
+
nextReminderNotificationAt = OneUptimeDate.addRemoveMinutes(
|
|
167
|
+
OneUptimeDate.getCurrentDate(),
|
|
168
|
+
matchingRule.reminderIntervalInMinutes,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await this.updateOneById({
|
|
174
|
+
id: data.alertId,
|
|
175
|
+
data: {
|
|
176
|
+
nextReminderNotificationAt: nextReminderNotificationAt,
|
|
177
|
+
},
|
|
178
|
+
props: {
|
|
179
|
+
isRoot: true,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
124
184
|
@CaptureSpan()
|
|
125
185
|
public async acknowledgeAlert(
|
|
126
186
|
alertId: ObjectID,
|
|
@@ -479,6 +539,25 @@ export class Service extends DatabaseService<Model> {
|
|
|
479
539
|
return Promise.resolve();
|
|
480
540
|
}
|
|
481
541
|
})
|
|
542
|
+
.then(async () => {
|
|
543
|
+
// Schedule reminder notifications for alert if a matching reminder rule exists
|
|
544
|
+
try {
|
|
545
|
+
if (createdItem.projectId && createdItem.id) {
|
|
546
|
+
await this.refreshReminderSchedule({
|
|
547
|
+
alertId: createdItem.id,
|
|
548
|
+
projectId: createdItem.projectId,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
} catch (error) {
|
|
552
|
+
logger.error(
|
|
553
|
+
`Reminder scheduling failed in AlertService.onCreateSuccess: ${error}`,
|
|
554
|
+
{
|
|
555
|
+
projectId: createdItem.projectId?.toString(),
|
|
556
|
+
alertId: createdItem.id?.toString(),
|
|
557
|
+
} as LogAttributes,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
})
|
|
482
561
|
.catch((error: Error) => {
|
|
483
562
|
logger.error(
|
|
484
563
|
`Critical error in AlertService sequential operations: ${error}`,
|
|
@@ -1093,6 +1172,33 @@ ${alertSeverity.name}
|
|
|
1093
1172
|
}
|
|
1094
1173
|
}
|
|
1095
1174
|
|
|
1175
|
+
// Re-evaluate reminder schedule when severity or labels change or reminders are toggled
|
|
1176
|
+
if (
|
|
1177
|
+
(onUpdate.updateBy.data.alertSeverity &&
|
|
1178
|
+
(onUpdate.updateBy.data.alertSeverity as any)._id) ||
|
|
1179
|
+
(onUpdate.updateBy.data.labels &&
|
|
1180
|
+
Array.isArray(onUpdate.updateBy.data.labels)) ||
|
|
1181
|
+
Object.prototype.hasOwnProperty.call(
|
|
1182
|
+
onUpdate.updateBy.data,
|
|
1183
|
+
"enableReminders",
|
|
1184
|
+
)
|
|
1185
|
+
) {
|
|
1186
|
+
try {
|
|
1187
|
+
await this.refreshReminderSchedule({
|
|
1188
|
+
alertId: alertId,
|
|
1189
|
+
projectId: projectId,
|
|
1190
|
+
});
|
|
1191
|
+
} catch (reminderError) {
|
|
1192
|
+
logger.error(
|
|
1193
|
+
`Reminder rescheduling failed in AlertService.onUpdateSuccess: ${reminderError}`,
|
|
1194
|
+
{
|
|
1195
|
+
projectId: projectId?.toString(),
|
|
1196
|
+
alertId: alertId?.toString(),
|
|
1197
|
+
} as LogAttributes,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1096
1202
|
if (shouldAddAlertFeed) {
|
|
1097
1203
|
await AlertFeedService.createAlertFeedItem({
|
|
1098
1204
|
alertId: alertId,
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
2
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
3
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
4
|
+
import DatabaseService from "./DatabaseService";
|
|
5
|
+
import Model from "../../Models/DatabaseModels/IncidentReminderRule";
|
|
6
|
+
import Incident from "../../Models/DatabaseModels/Incident";
|
|
7
|
+
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
|
|
8
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
9
|
+
import IncidentState from "../../Models/DatabaseModels/IncidentState";
|
|
10
|
+
import IncidentService from "./IncidentService";
|
|
11
|
+
import IncidentStateService from "./IncidentStateService";
|
|
12
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
13
|
+
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
14
|
+
import ObjectID from "../../Types/ObjectID";
|
|
15
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
16
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
17
|
+
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
18
|
+
|
|
19
|
+
export class Service extends DatabaseService<Model> {
|
|
20
|
+
public constructor() {
|
|
21
|
+
super(Model);
|
|
22
|
+
if (IsBillingEnabled) {
|
|
23
|
+
this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@CaptureSpan()
|
|
28
|
+
protected override async onBeforeCreate(
|
|
29
|
+
createBy: CreateBy<Model>,
|
|
30
|
+
): Promise<OnCreate<Model>> {
|
|
31
|
+
// Auto-assign order on creation
|
|
32
|
+
if (!createBy.data.order) {
|
|
33
|
+
const highestOrderRule: Model | null = await this.findOneBy({
|
|
34
|
+
query: {
|
|
35
|
+
projectId: createBy.data.projectId!,
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
order: true,
|
|
39
|
+
},
|
|
40
|
+
sort: {
|
|
41
|
+
order: SortOrder.Descending,
|
|
42
|
+
},
|
|
43
|
+
props: {
|
|
44
|
+
isRoot: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
createBy.data.order = (highestOrderRule?.order || 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
createBy,
|
|
53
|
+
carryForward: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@CaptureSpan()
|
|
58
|
+
protected override async onCreateSuccess(
|
|
59
|
+
_onCreate: OnCreate<Model>,
|
|
60
|
+
createdItem: Model,
|
|
61
|
+
): Promise<Model> {
|
|
62
|
+
if (createdItem.projectId) {
|
|
63
|
+
await this.refreshSchedulesForOpenIncidents(createdItem.projectId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return createdItem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@CaptureSpan()
|
|
70
|
+
protected override async onUpdateSuccess(
|
|
71
|
+
onUpdate: OnUpdate<Model>,
|
|
72
|
+
_updatedItemIds: Array<ObjectID>,
|
|
73
|
+
): Promise<OnUpdate<Model>> {
|
|
74
|
+
const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
|
|
75
|
+
| ObjectID
|
|
76
|
+
| undefined;
|
|
77
|
+
|
|
78
|
+
if (projectId) {
|
|
79
|
+
await this.refreshSchedulesForOpenIncidents(projectId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return onUpdate;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@CaptureSpan()
|
|
86
|
+
protected override async onDeleteSuccess(
|
|
87
|
+
onDelete: OnDelete<Model>,
|
|
88
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
89
|
+
): Promise<OnDelete<Model>> {
|
|
90
|
+
const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
|
|
91
|
+
| ObjectID
|
|
92
|
+
| undefined;
|
|
93
|
+
|
|
94
|
+
if (projectId) {
|
|
95
|
+
await this.refreshSchedulesForOpenIncidents(projectId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return onDelete;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@CaptureSpan()
|
|
102
|
+
public async refreshSchedulesForOpenIncidents(
|
|
103
|
+
projectId: ObjectID,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
const unresolvedStates: Array<IncidentState> =
|
|
107
|
+
await IncidentStateService.findBy({
|
|
108
|
+
query: {
|
|
109
|
+
projectId: projectId,
|
|
110
|
+
isResolvedState: false,
|
|
111
|
+
},
|
|
112
|
+
select: {
|
|
113
|
+
_id: true,
|
|
114
|
+
},
|
|
115
|
+
limit: LIMIT_PER_PROJECT,
|
|
116
|
+
skip: 0,
|
|
117
|
+
props: {
|
|
118
|
+
isRoot: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const unresolvedStateIds: Array<ObjectID> = unresolvedStates
|
|
123
|
+
.map((state: IncidentState) => {
|
|
124
|
+
return state.id!;
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
|
|
128
|
+
if (unresolvedStateIds.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const openIncidents: Array<Incident> = await IncidentService.findBy({
|
|
133
|
+
query: {
|
|
134
|
+
projectId: projectId,
|
|
135
|
+
currentIncidentStateId: QueryHelper.any(unresolvedStateIds),
|
|
136
|
+
},
|
|
137
|
+
select: {
|
|
138
|
+
_id: true,
|
|
139
|
+
},
|
|
140
|
+
limit: LIMIT_MAX,
|
|
141
|
+
skip: 0,
|
|
142
|
+
props: {
|
|
143
|
+
isRoot: true,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for (const incident of openIncidents) {
|
|
148
|
+
try {
|
|
149
|
+
await IncidentService.refreshReminderSchedule({
|
|
150
|
+
incidentId: incident.id!,
|
|
151
|
+
projectId: projectId,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.error(
|
|
155
|
+
`Failed to refresh reminder schedule for incident ${incident.id}: ${error}`,
|
|
156
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error(
|
|
162
|
+
`Failed to refresh reminder schedules for open incidents: ${error}`,
|
|
163
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@CaptureSpan()
|
|
169
|
+
public async findMatchingRule(data: {
|
|
170
|
+
projectId: ObjectID;
|
|
171
|
+
incidentSeverityId?: ObjectID | undefined;
|
|
172
|
+
labelIds?: Array<ObjectID> | undefined;
|
|
173
|
+
}): Promise<Model | null> {
|
|
174
|
+
// Get all enabled rules sorted by order. First matching rule wins.
|
|
175
|
+
const rules: Array<Model> = await this.findBy({
|
|
176
|
+
query: {
|
|
177
|
+
projectId: data.projectId,
|
|
178
|
+
isEnabled: true,
|
|
179
|
+
},
|
|
180
|
+
sort: {
|
|
181
|
+
order: SortOrder.Ascending,
|
|
182
|
+
},
|
|
183
|
+
select: {
|
|
184
|
+
_id: true,
|
|
185
|
+
name: true,
|
|
186
|
+
order: true,
|
|
187
|
+
reminderIntervalInMinutes: true,
|
|
188
|
+
stopRemindersOnState: true,
|
|
189
|
+
incidentSeverities: {
|
|
190
|
+
_id: true,
|
|
191
|
+
},
|
|
192
|
+
labels: {
|
|
193
|
+
_id: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
limit: 100,
|
|
197
|
+
skip: 0,
|
|
198
|
+
props: {
|
|
199
|
+
isRoot: true,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for (const rule of rules) {
|
|
204
|
+
if (rule.incidentSeverities && rule.incidentSeverities.length > 0) {
|
|
205
|
+
if (!data.incidentSeverityId) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const severityIds: Array<string> = rule.incidentSeverities.map(
|
|
210
|
+
(severity: IncidentSeverity) => {
|
|
211
|
+
return severity.id?.toString() || "";
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (!severityIds.includes(data.incidentSeverityId.toString())) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (rule.labels && rule.labels.length > 0) {
|
|
221
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
|
|
226
|
+
return label.id?.toString() || "";
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const hasMatchingLabel: boolean = data.labelIds.some(
|
|
230
|
+
(labelId: ObjectID) => {
|
|
231
|
+
return ruleLabelIds.includes(labelId.toString());
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (!hasMatchingLabel) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
* Rule matched: (no severities OR severity matches) AND
|
|
242
|
+
* (no labels OR labels intersect). A rule with neither matches all.
|
|
243
|
+
*/
|
|
244
|
+
logger.debug(
|
|
245
|
+
`Incident reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
|
|
246
|
+
{ projectId: data.projectId?.toString() } as LogAttributes,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return rule;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default new Service();
|