@oneuptime/common 10.0.70 → 10.0.72

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.
Files changed (144) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/Incident.ts +55 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +6 -4
  4. package/Models/DatabaseModels/Project.ts +5 -5
  5. package/Models/DatabaseModels/StatusPage.ts +80 -0
  6. package/Server/API/StatusPageAPI.ts +4 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +6 -3
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  12. package/Server/Services/AIBillingService.ts +2 -2
  13. package/Server/Services/AnalyticsDatabaseService.ts +17 -7
  14. package/Server/Services/BillingService.ts +116 -48
  15. package/Server/Services/NotificationService.ts +2 -2
  16. package/Server/Types/Database/QueryUtil.ts +13 -7
  17. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
  18. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
  19. package/Server/Utils/Monitor/MonitorAlert.ts +170 -7
  20. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
  21. package/Server/Utils/Monitor/MonitorIncident.ts +212 -8
  22. package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
  23. package/Server/Utils/Monitor/MonitorResource.ts +2 -0
  24. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
  25. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
  26. package/Types/BaseDatabase/IncludesNone.ts +1 -4
  27. package/Types/Email.ts +50 -0
  28. package/Types/Infrastructure/BasicMetrics.ts +75 -0
  29. package/Types/Metrics/MetricQueryData.ts +11 -0
  30. package/Types/Monitor/CriteriaFilter.ts +10 -0
  31. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
  32. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
  33. package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
  34. package/Types/Monitor/MonitorMetricType.ts +34 -0
  35. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
  36. package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
  37. package/Types/StatusPage/StatusPageLanguage.ts +29 -0
  38. package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
  39. package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
  40. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
  41. package/UI/Components/Charts/Line/LineChart.tsx +16 -11
  42. package/UI/Components/Filters/DateFilter.tsx +16 -8
  43. package/UI/Components/Filters/EntityFilter.tsx +33 -18
  44. package/UI/Components/Filters/FilterViewer.tsx +7 -5
  45. package/UI/Components/Filters/FiltersForm.tsx +27 -5
  46. package/UI/Components/Filters/NumberFilter.tsx +3 -2
  47. package/UI/Components/Filters/TextFilter.tsx +5 -4
  48. package/UI/Components/ModelTable/BaseModelTable.tsx +5 -3
  49. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
  50. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
  51. package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
  52. package/Utils/Monitor/MonitorMetricType.ts +309 -19
  53. package/build/dist/Models/DatabaseModels/Alert.js +57 -0
  54. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/Incident.js +57 -0
  56. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +6 -4
  58. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  59. package/build/dist/Models/DatabaseModels/Project.js +5 -5
  60. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  62. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  63. package/build/dist/Server/API/StatusPageAPI.js +4 -0
  64. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +4 -2
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -1
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +1 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  75. package/build/dist/Server/Services/AIBillingService.js +2 -2
  76. package/build/dist/Server/Services/AIBillingService.js.map +1 -1
  77. package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
  78. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  79. package/build/dist/Server/Services/BillingService.js +99 -39
  80. package/build/dist/Server/Services/BillingService.js.map +1 -1
  81. package/build/dist/Server/Services/NotificationService.js +2 -2
  82. package/build/dist/Server/Services/NotificationService.js.map +1 -1
  83. package/build/dist/Server/Types/Database/QueryUtil.js +13 -7
  84. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  85. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
  86. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  87. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
  88. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  89. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +134 -12
  90. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  91. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
  92. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  93. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +159 -15
  94. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  95. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
  96. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  97. package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
  98. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  99. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
  100. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  101. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
  102. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
  103. package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -1
  104. package/build/dist/Types/Email.js +42 -0
  105. package/build/dist/Types/Email.js.map +1 -1
  106. package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
  107. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  108. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
  109. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
  110. package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
  111. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  112. package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
  113. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
  114. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
  115. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  116. package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
  117. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  118. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
  119. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  120. package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
  121. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  122. package/build/dist/UI/Components/Filters/DateFilter.js +1 -4
  123. package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
  124. package/build/dist/UI/Components/Filters/EntityFilter.js +21 -14
  125. package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
  126. package/build/dist/UI/Components/Filters/FilterViewer.js +1 -2
  127. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  128. package/build/dist/UI/Components/Filters/FiltersForm.js +7 -3
  129. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  130. package/build/dist/UI/Components/Filters/NumberFilter.js +0 -1
  131. package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
  132. package/build/dist/UI/Components/Filters/TextFilter.js +5 -4
  133. package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
  134. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +5 -3
  135. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  136. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
  137. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
  138. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
  139. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
  140. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
  141. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
  142. package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
  143. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  144. package/package.json +1 -1
@@ -208,6 +208,77 @@ export default class ServerMonitorCriteria {
208
208
  });
209
209
  }
210
210
 
211
+ if (
212
+ !(input.dataToProcess as ServerMonitorResponse)
213
+ .onlyCheckRequestReceivedAt &&
214
+ (input.criteriaFilter.checkOn === CheckOn.LoadAverage1Min ||
215
+ input.criteriaFilter.checkOn === CheckOn.LoadAverage5Min ||
216
+ input.criteriaFilter.checkOn === CheckOn.LoadAverage15Min)
217
+ ) {
218
+ threshold = CompareCriteria.convertToNumber(threshold);
219
+
220
+ const loadMetrics:
221
+ | { load1: number; load5: number; load15: number }
222
+ | undefined = (input.dataToProcess as ServerMonitorResponse)
223
+ .basicInfrastructureMetrics?.loadMetrics;
224
+
225
+ let currentLoad: number | undefined = undefined;
226
+ if (input.criteriaFilter.checkOn === CheckOn.LoadAverage1Min) {
227
+ currentLoad = loadMetrics?.load1;
228
+ } else if (input.criteriaFilter.checkOn === CheckOn.LoadAverage5Min) {
229
+ currentLoad = loadMetrics?.load5;
230
+ } else if (input.criteriaFilter.checkOn === CheckOn.LoadAverage15Min) {
231
+ currentLoad = loadMetrics?.load15;
232
+ }
233
+
234
+ const value: number | Array<number> =
235
+ (overTimeValue as Array<number>) || currentLoad || 0;
236
+
237
+ return CompareCriteria.compareCriteriaNumbers({
238
+ value: value,
239
+ threshold: threshold as number,
240
+ criteriaFilter: input.criteriaFilter,
241
+ });
242
+ }
243
+
244
+ if (
245
+ input.criteriaFilter.checkOn === CheckOn.SwapUsagePercent &&
246
+ !(input.dataToProcess as ServerMonitorResponse).onlyCheckRequestReceivedAt
247
+ ) {
248
+ threshold = CompareCriteria.convertToNumber(threshold);
249
+
250
+ const swapPercent: number | Array<number> =
251
+ (overTimeValue as Array<number>) ||
252
+ (input.dataToProcess as ServerMonitorResponse)
253
+ .basicInfrastructureMetrics?.memoryMetrics?.swapPercentUsed ||
254
+ 0;
255
+
256
+ return CompareCriteria.compareCriteriaNumbers({
257
+ value: swapPercent,
258
+ threshold: threshold as number,
259
+ criteriaFilter: input.criteriaFilter,
260
+ });
261
+ }
262
+
263
+ if (
264
+ input.criteriaFilter.checkOn === CheckOn.CPUIoWaitPercent &&
265
+ !(input.dataToProcess as ServerMonitorResponse).onlyCheckRequestReceivedAt
266
+ ) {
267
+ threshold = CompareCriteria.convertToNumber(threshold);
268
+
269
+ const ioWaitPercent: number | Array<number> =
270
+ (overTimeValue as Array<number>) ||
271
+ (input.dataToProcess as ServerMonitorResponse)
272
+ .basicInfrastructureMetrics?.cpuMetrics?.timeIoWaitPercent ||
273
+ 0;
274
+
275
+ return CompareCriteria.compareCriteriaNumbers({
276
+ value: ioWaitPercent,
277
+ threshold: threshold as number,
278
+ criteriaFilter: input.criteriaFilter,
279
+ });
280
+ }
281
+
211
282
  if (
212
283
  input.criteriaFilter.checkOn === CheckOn.ServerProcessName &&
213
284
  threshold &&
@@ -16,7 +16,9 @@ import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
16
16
  import { DisableAutomaticAlertCreation } from "../../EnvironmentConfig";
17
17
  import AlertService from "../../Services/AlertService";
18
18
  import AlertSeverityService from "../../Services/AlertSeverityService";
19
+ import AlertStateService from "../../Services/AlertStateService";
19
20
  import AlertStateTimelineService from "../../Services/AlertStateTimelineService";
21
+ import AlertState from "../../../Models/DatabaseModels/AlertState";
20
22
  import logger, { LogAttributes } from "../Logger";
21
23
  import CaptureSpan from "../Telemetry/CaptureSpan";
22
24
  import DataToProcess from "./DataToProcess";
@@ -24,6 +26,7 @@ import MonitorTemplateUtil from "./MonitorTemplateUtil";
24
26
  import { JSONObject } from "../../../Types/JSON";
25
27
  import OneUptimeDate from "../../../Types/Date";
26
28
  import MonitorEvaluationSummary from "../../../Types/Monitor/MonitorEvaluationSummary";
29
+ import { PerSeriesCriteriaMatch } from "../../../Types/Probe/ProbeApiIngestResponse";
27
30
 
28
31
  export default class MonitorAlert {
29
32
  @CaptureSpan()
@@ -34,6 +37,7 @@ export default class MonitorAlert {
34
37
  criteriaInstance: MonitorCriteriaInstance | null;
35
38
  dataToProcess: DataToProcess;
36
39
  evaluationSummary?: MonitorEvaluationSummary | undefined;
40
+ breachingSeriesFingerprints?: Set<string> | undefined;
37
41
  }): Promise<Array<Alert>> {
38
42
  // check active alerts and if there are open alerts, do not cretae anothr alert.
39
43
  const openAlerts: Array<Alert> = await AlertService.findBy({
@@ -47,10 +51,14 @@ export default class MonitorAlert {
47
51
  limit: LIMIT_PER_PROJECT,
48
52
  select: {
49
53
  _id: true,
54
+ title: true,
50
55
  createdCriteriaId: true,
51
56
  projectId: true,
52
57
  alertNumber: true,
53
58
  alertNumberWithPrefix: true,
59
+ currentAlertStateId: true,
60
+ seriesFingerprint: true,
61
+ seriesLabels: true,
54
62
  },
55
63
  props: {
56
64
  isRoot: true,
@@ -65,6 +73,7 @@ export default class MonitorAlert {
65
73
  autoResolveCriteriaInstanceIdAlertIdsDictionary:
66
74
  input.autoResolveCriteriaInstanceIdAlertIdsDictionary,
67
75
  criteriaInstance: input.criteriaInstance,
76
+ breachingSeriesFingerprints: input.breachingSeriesFingerprints,
68
77
  });
69
78
 
70
79
  if (shouldClose) {
@@ -103,6 +112,7 @@ export default class MonitorAlert {
103
112
  props: {
104
113
  telemetryQuery?: TelemetryQuery | undefined;
105
114
  };
115
+ matchesPerSeries?: Array<PerSeriesCriteriaMatch> | undefined;
106
116
  }): Promise<void> {
107
117
  const alertLogAttributes: LogAttributes = {
108
118
  projectId: input.monitor.projectId?.toString(),
@@ -113,6 +123,16 @@ export default class MonitorAlert {
113
123
  `${input.monitor.id?.toString()} - Check open alerts.`,
114
124
  alertLogAttributes,
115
125
  );
126
+
127
+ const breachingSeriesFingerprints: Set<string> | undefined =
128
+ input.matchesPerSeries
129
+ ? new Set<string>(
130
+ input.matchesPerSeries.map((m: PerSeriesCriteriaMatch) => {
131
+ return m.fingerprint;
132
+ }),
133
+ )
134
+ : undefined;
135
+
116
136
  // check active alerts and if there are open alerts, do not cretae anothr alert.
117
137
  const openAlerts: Array<Alert> =
118
138
  await this.checkOpenAlertsAndCloseIfResolved({
@@ -123,19 +143,31 @@ export default class MonitorAlert {
123
143
  criteriaInstance: input.criteriaInstance,
124
144
  dataToProcess: input.dataToProcess,
125
145
  evaluationSummary: input.evaluationSummary,
146
+ breachingSeriesFingerprints,
126
147
  });
127
148
 
128
- if (input.criteriaInstance.data?.createAlerts) {
129
- // create alerts
149
+ if (!input.criteriaInstance.data?.createAlerts) {
150
+ return;
151
+ }
152
+
153
+ const seriesToProcess: Array<PerSeriesCriteriaMatch | undefined> =
154
+ input.matchesPerSeries && input.matchesPerSeries.length > 0
155
+ ? input.matchesPerSeries
156
+ : [undefined];
130
157
 
131
- for (const criteriaAlert of input.criteriaInstance.data?.alerts || []) {
132
- // should create alert.
158
+ for (const criteriaAlert of input.criteriaInstance.data?.alerts || []) {
159
+ for (const seriesMatch of seriesToProcess) {
160
+ const seriesFingerprint: string | undefined = seriesMatch?.fingerprint;
161
+ const seriesLabels: JSONObject | undefined = seriesMatch?.labels;
162
+ const seriesRootCause: string =
163
+ seriesMatch?.rootCause || input.rootCause;
133
164
 
134
165
  const alreadyOpenAlert: Alert | undefined = openAlerts.find(
135
166
  (alert: Alert) => {
136
167
  return (
137
168
  alert.createdCriteriaId ===
138
- input.criteriaInstance.data?.id.toString()
169
+ input.criteriaInstance.data?.id.toString() &&
170
+ (alert.seriesFingerprint || undefined) === seriesFingerprint
139
171
  );
140
172
  },
141
173
  );
@@ -153,9 +185,12 @@ export default class MonitorAlert {
153
185
  );
154
186
 
155
187
  if (hasAlreadyOpenAlert) {
188
+ const renderedAlertTitle: string =
189
+ alreadyOpenAlert?.title || criteriaAlert.title;
190
+
156
191
  input.evaluationSummary?.events.push({
157
192
  type: "alert-skipped",
158
- title: `Alert already active: ${criteriaAlert.title}`,
193
+ title: `Alert already active: ${renderedAlertTitle}`,
159
194
  message:
160
195
  "Skipped creating a new alert because an active alert exists for this criteria.",
161
196
  relatedCriteriaId: input.criteriaInstance.data?.id,
@@ -180,6 +215,8 @@ export default class MonitorAlert {
180
215
  MonitorTemplateUtil.buildTemplateStorageMap({
181
216
  monitorType: input.monitor.monitorType!,
182
217
  dataToProcess: input.dataToProcess,
218
+ monitor: input.monitor,
219
+ seriesLabels,
183
220
  });
184
221
 
185
222
  alert.title = MonitorTemplateUtil.processTemplateString({
@@ -221,13 +258,20 @@ export default class MonitorAlert {
221
258
 
222
259
  alert.monitor = input.monitor;
223
260
  alert.projectId = input.monitor.projectId!;
224
- alert.rootCause = input.rootCause;
261
+ alert.rootCause = seriesRootCause;
225
262
  alert.createdStateLog = JSON.parse(
226
263
  JSON.stringify(input.dataToProcess, null, 2),
227
264
  );
228
265
 
229
266
  alert.createdCriteriaId = input.criteriaInstance.data.id.toString();
230
267
 
268
+ if (seriesFingerprint) {
269
+ alert.seriesFingerprint = seriesFingerprint;
270
+ }
271
+ if (seriesLabels && Object.keys(seriesLabels).length > 0) {
272
+ alert.seriesLabels = seriesLabels;
273
+ }
274
+
231
275
  alert.onCallDutyPolicies =
232
276
  criteriaAlert.onCallPolicyIds?.map((id: ObjectID) => {
233
277
  const onCallPolicy: OnCallDutyPolicy = new OnCallDutyPolicy();
@@ -328,6 +372,82 @@ export default class MonitorAlert {
328
372
  input.openAlert.projectId!,
329
373
  );
330
374
 
375
+ /*
376
+ * Skip the Resolved insert if the alert's timeline is already at or past
377
+ * the Resolved state in the project's workflow order. Two cases:
378
+ * 1. Latest timeline state is Resolved but Alert.currentAlertStateId is
379
+ * stuck on an earlier state (partial-failure from a prior resolve).
380
+ * Re-inserting Resolved would throw "Alert state cannot be same as
381
+ * previous state" from AlertStateTimelineService.onBeforeCreate.
382
+ * 2. The project defines a custom state after Resolved (e.g. Closed) and
383
+ * the alert has moved into it. Inserting Resolved would throw
384
+ * "cannot transition to Resolved from Closed because Resolved is
385
+ * before Closed in the order of alert states."
386
+ * Either failure bubbles up through ingest workers and causes monitors to
387
+ * flap. Reconcile Alert.currentAlertStateId if out of sync with the
388
+ * timeline, then return.
389
+ */
390
+ const [resolvedState, latestTimeline]: [
391
+ AlertState | null,
392
+ AlertStateTimeline | null,
393
+ ] = await Promise.all([
394
+ AlertStateService.findOneBy({
395
+ query: {
396
+ _id: resolvedStateId.toString(),
397
+ },
398
+ select: {
399
+ order: true,
400
+ },
401
+ props: {
402
+ isRoot: true,
403
+ },
404
+ }),
405
+ AlertStateTimelineService.findOneBy({
406
+ query: {
407
+ alertId: input.openAlert.id!,
408
+ },
409
+ sort: {
410
+ startsAt: SortOrder.Descending,
411
+ },
412
+ select: {
413
+ alertStateId: true,
414
+ alertState: {
415
+ order: true,
416
+ },
417
+ },
418
+ props: {
419
+ isRoot: true,
420
+ },
421
+ }),
422
+ ]);
423
+
424
+ const latestOrder: number | undefined | null =
425
+ latestTimeline?.alertState?.order;
426
+ const resolvedOrder: number | undefined | null = resolvedState?.order;
427
+
428
+ if (
429
+ latestTimeline?.alertStateId &&
430
+ typeof latestOrder === "number" &&
431
+ typeof resolvedOrder === "number" &&
432
+ latestOrder >= resolvedOrder
433
+ ) {
434
+ if (
435
+ input.openAlert.currentAlertStateId?.toString() !==
436
+ latestTimeline.alertStateId.toString()
437
+ ) {
438
+ await AlertService.updateOneById({
439
+ id: input.openAlert.id!,
440
+ data: {
441
+ currentAlertStateId: latestTimeline.alertStateId,
442
+ },
443
+ props: {
444
+ isRoot: true,
445
+ },
446
+ });
447
+ }
448
+ return;
449
+ }
450
+
331
451
  const alertStateTimeline: AlertStateTimeline = new AlertStateTimeline();
332
452
  alertStateTimeline.alertId = input.openAlert.id!;
333
453
  alertStateTimeline.alertStateId = resolvedStateId;
@@ -357,7 +477,50 @@ export default class MonitorAlert {
357
477
  openAlert: Alert;
358
478
  autoResolveCriteriaInstanceIdAlertIdsDictionary: Dictionary<Array<string>>;
359
479
  criteriaInstance: MonitorCriteriaInstance | null; // null if no criteia met.
480
+ breachingSeriesFingerprints?: Set<string> | undefined;
360
481
  }): boolean {
482
+ const openSeriesFingerprint: string | undefined =
483
+ input.openAlert.seriesFingerprint || undefined;
484
+
485
+ /*
486
+ * Per-series auto-resolve: when a breaching-series set is given and
487
+ * this alert has a fingerprint, resolve whenever the fingerprint is
488
+ * no longer in the set, independent of whether other series on the
489
+ * same monitor are still breaching.
490
+ */
491
+ if (
492
+ input.breachingSeriesFingerprints !== undefined &&
493
+ openSeriesFingerprint
494
+ ) {
495
+ const stillBreaching: boolean = input.breachingSeriesFingerprints.has(
496
+ openSeriesFingerprint,
497
+ );
498
+
499
+ if (stillBreaching) {
500
+ return false;
501
+ }
502
+
503
+ if (!input.openAlert.createdCriteriaId?.toString()) {
504
+ return false;
505
+ }
506
+
507
+ const autoResolveTemplates: Array<string> | undefined =
508
+ input.autoResolveCriteriaInstanceIdAlertIdsDictionary[
509
+ input.openAlert.createdCriteriaId.toString()
510
+ ];
511
+
512
+ /*
513
+ * Alert auto-resolve lists templates by criteria; presence of any
514
+ * template for this criteria means "this criteria's alerts are
515
+ * configured to auto-resolve", so resolve this series.
516
+ */
517
+ if (autoResolveTemplates && autoResolveTemplates.length > 0) {
518
+ return true;
519
+ }
520
+
521
+ return false;
522
+ }
523
+
361
524
  if (
362
525
  input.openAlert.createdCriteriaId?.toString() ===
363
526
  input.criteriaInstance?.data?.id.toString()
@@ -9,7 +9,9 @@ import SSLMonitorCriteria from "./Criteria/SSLMonitorCriteria";
9
9
  import ServerMonitorCriteria from "./Criteria/ServerMonitorCriteria";
10
10
  import SyntheticMonitoringCriteria from "./Criteria/SyntheticMonitor";
11
11
  import LogMonitorCriteria from "./Criteria/LogMonitorCriteria";
12
- import MetricMonitorCriteria from "./Criteria/MetricMonitorCriteria";
12
+ import MetricMonitorCriteria, {
13
+ MetricSeriesEvaluationResult,
14
+ } from "./Criteria/MetricMonitorCriteria";
13
15
  import TraceMonitorCriteria from "./Criteria/TraceMonitorCriteria";
14
16
  import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
15
17
  import ProfileMonitorCriteria from "./Criteria/ProfileMonitorCriteria";
@@ -31,7 +33,9 @@ import MonitorEvaluationSummary, {
31
33
  MonitorEvaluationEvent,
32
34
  MonitorEvaluationFilterResult,
33
35
  } from "../../../Types/Monitor/MonitorEvaluationSummary";
34
- import ProbeApiIngestResponse from "../../../Types/Probe/ProbeApiIngestResponse";
36
+ import ProbeApiIngestResponse, {
37
+ PerSeriesCriteriaMatch,
38
+ } from "../../../Types/Probe/ProbeApiIngestResponse";
35
39
  import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
36
40
  import RequestFailedDetails from "../../../Types/Probe/RequestFailedDetails";
37
41
  import IncomingMonitorRequest from "../../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
@@ -50,6 +54,7 @@ import MetricMonitorResponse, {
50
54
  KubernetesAffectedResource,
51
55
  KubernetesResourceBreakdown,
52
56
  } from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
57
+ import MetricSeriesResult from "../../../Types/Monitor/MetricMonitor/MetricSeriesResult";
53
58
  import MetricCriteriaContext, {
54
59
  MetricBreachingSample,
55
60
  MetricComponent,
@@ -156,6 +161,26 @@ ${contextBlock}
156
161
  **Cause**: ${(input.dataToProcess as ProbeMonitorResponse).failureCause || ""}
157
162
  `;
158
163
  }
164
+
165
+ /*
166
+ * When this is a metric-style monitor with per-series results,
167
+ * compute the per-series match list so MonitorResource can fan
168
+ * out one incident per affected series. We do this *after* the
169
+ * scalar criteriaMetId/rootCause is populated so legacy readers
170
+ * still see a usable response if perSeriesMatches is ignored.
171
+ */
172
+ const perSeriesMatches: Array<PerSeriesCriteriaMatch> =
173
+ MonitorCriteriaEvaluator.collectPerSeriesMatches({
174
+ dataToProcess: input.dataToProcess,
175
+ monitor: input.monitor,
176
+ monitorStep: input.monitorStep,
177
+ criteriaInstance,
178
+ });
179
+
180
+ if (perSeriesMatches.length > 0) {
181
+ input.probeApiIngestResponse.perSeriesMatches = perSeriesMatches;
182
+ }
183
+
159
184
  break;
160
185
  }
161
186
  }
@@ -163,6 +188,150 @@ ${contextBlock}
163
188
  return input.probeApiIngestResponse;
164
189
  }
165
190
 
191
+ /**
192
+ * For metric-backed monitors (Metrics/Kubernetes/Docker) with
193
+ * per-series aggregated results, re-evaluate the matched criteria
194
+ * once per series and return one entry per series that breached.
195
+ * Returns an empty array when the monitor is not series-aware or
196
+ * no series matched — the caller falls back to the single-incident
197
+ * path in that case.
198
+ */
199
+ private static collectPerSeriesMatches(input: {
200
+ dataToProcess: DataToProcess;
201
+ monitor: Monitor;
202
+ monitorStep: MonitorStep;
203
+ criteriaInstance: MonitorCriteriaInstance;
204
+ }): Array<PerSeriesCriteriaMatch> {
205
+ if (
206
+ input.monitor.monitorType !== MonitorType.Metrics &&
207
+ input.monitor.monitorType !== MonitorType.Kubernetes &&
208
+ input.monitor.monitorType !== MonitorType.Docker
209
+ ) {
210
+ return [];
211
+ }
212
+
213
+ const metricResponse: MetricMonitorResponse =
214
+ input.dataToProcess as MetricMonitorResponse;
215
+
216
+ const seriesBreakdown: Array<MetricSeriesResult> | undefined =
217
+ metricResponse.seriesBreakdown;
218
+
219
+ if (!seriesBreakdown || seriesBreakdown.length === 0) {
220
+ return [];
221
+ }
222
+
223
+ const criteriaId: string | undefined = input.criteriaInstance.data?.id;
224
+ if (!criteriaId) {
225
+ return [];
226
+ }
227
+
228
+ const filterCondition: FilterCondition =
229
+ input.criteriaInstance.data?.filterCondition || FilterCondition.All;
230
+
231
+ const filters: Array<CriteriaFilter> =
232
+ input.criteriaInstance.data?.filters || [];
233
+
234
+ /*
235
+ * Evaluate every metric-value filter against every series. We only
236
+ * handle CheckOn.MetricValue for now — the criteria evaluator's
237
+ * other filter types don't carry series information.
238
+ */
239
+ const metricFilters: Array<CriteriaFilter> = filters.filter(
240
+ (f: CriteriaFilter) => {
241
+ return f.checkOn === CheckOn.MetricValue;
242
+ },
243
+ );
244
+
245
+ if (metricFilters.length === 0) {
246
+ return [];
247
+ }
248
+
249
+ /*
250
+ * Key: fingerprint. Value: results of each metric filter for that
251
+ * series, tracked so we can apply FilterCondition.All/Any per series.
252
+ */
253
+ const resultsByFingerprint: Map<
254
+ string,
255
+ {
256
+ labels: JSONObject;
257
+ filterResults: Array<MetricSeriesEvaluationResult>;
258
+ }
259
+ > = new Map();
260
+
261
+ for (const criteriaFilter of metricFilters) {
262
+ const evaluations: Array<MetricSeriesEvaluationResult> =
263
+ MetricMonitorCriteria.evaluateAllSeries({
264
+ dataToProcess: input.dataToProcess,
265
+ criteriaFilter,
266
+ monitorStep: input.monitorStep,
267
+ });
268
+
269
+ for (const evaluation of evaluations) {
270
+ const fingerprint: string | undefined = evaluation.fingerprint;
271
+ if (!fingerprint) {
272
+ continue;
273
+ }
274
+
275
+ const existing:
276
+ | {
277
+ labels: JSONObject;
278
+ filterResults: Array<MetricSeriesEvaluationResult>;
279
+ }
280
+ | undefined = resultsByFingerprint.get(fingerprint);
281
+
282
+ if (existing) {
283
+ existing.filterResults.push(evaluation);
284
+ } else {
285
+ resultsByFingerprint.set(fingerprint, {
286
+ labels: evaluation.labels,
287
+ filterResults: [evaluation],
288
+ });
289
+ }
290
+ }
291
+ }
292
+
293
+ const matches: Array<PerSeriesCriteriaMatch> = [];
294
+
295
+ for (const [fingerprint, entry] of resultsByFingerprint) {
296
+ // Were all/any of the metric filters met for this series?
297
+ const matched: Array<MetricSeriesEvaluationResult> =
298
+ entry.filterResults.filter((r: MetricSeriesEvaluationResult) => {
299
+ return r.rootCause !== null;
300
+ });
301
+
302
+ const seriesMatched: boolean =
303
+ filterCondition === FilterCondition.All
304
+ ? matched.length === entry.filterResults.length &&
305
+ entry.filterResults.length > 0
306
+ : matched.length > 0;
307
+
308
+ if (!seriesMatched) {
309
+ continue;
310
+ }
311
+
312
+ /*
313
+ * For the per-series rootCause, concatenate the matched filter
314
+ * messages so the incident message reflects exactly what
315
+ * breached on this specific series.
316
+ */
317
+ const rootCauseLines: Array<string> = matched.map(
318
+ (r: MetricSeriesEvaluationResult) => {
319
+ return `- ${r.rootCause}`;
320
+ },
321
+ );
322
+
323
+ matches.push({
324
+ criteriaMetId: criteriaId,
325
+ fingerprint,
326
+ labels: entry.labels,
327
+ rootCause: rootCauseLines.join("\n"),
328
+ metricContext: matched[0]?.context,
329
+ });
330
+ }
331
+
332
+ return matches;
333
+ }
334
+
166
335
  private static async processMonitorCriteriaInstance(input: {
167
336
  dataToProcess: DataToProcess;
168
337
  monitorStep: MonitorStep;