@oneuptime/common 10.0.71 → 10.0.73

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 (115) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/Incident.ts +55 -0
  3. package/Models/DatabaseModels/Project.ts +26 -0
  4. package/Models/DatabaseModels/Service.ts +2 -2
  5. package/Models/DatabaseModels/StatusPage.ts +80 -0
  6. package/Server/API/StatusPageAPI.ts +4 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1777018175127-AddTelemetryRetentionSettings.ts +25 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Services/AnalyticsDatabaseService.ts +17 -7
  12. package/Server/Services/OpenTelemetryIngestService.ts +36 -5
  13. package/Server/Services/ServiceService.ts +27 -1
  14. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
  15. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
  16. package/Server/Utils/Monitor/MonitorAlert.ts +91 -7
  17. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
  18. package/Server/Utils/Monitor/MonitorIncident.ts +133 -8
  19. package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
  20. package/Server/Utils/Monitor/MonitorResource.ts +2 -0
  21. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
  22. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
  23. package/Types/AdminDashboard/AdminDashboardLanguage.ts +33 -0
  24. package/Types/Infrastructure/BasicMetrics.ts +75 -0
  25. package/Types/Metrics/MetricQueryData.ts +11 -0
  26. package/Types/Monitor/CriteriaFilter.ts +10 -0
  27. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
  28. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
  29. package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
  30. package/Types/Monitor/MonitorMetricType.ts +34 -0
  31. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
  32. package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
  33. package/Types/StatusPage/StatusPageLanguage.ts +30 -0
  34. package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
  35. package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
  36. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
  37. package/UI/Components/Charts/Line/LineChart.tsx +16 -11
  38. package/UI/Components/Filters/FiltersForm.tsx +26 -2
  39. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
  40. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
  41. package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
  42. package/Utils/Monitor/MonitorMetricType.ts +309 -19
  43. package/build/dist/Models/DatabaseModels/Alert.js +57 -0
  44. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  45. package/build/dist/Models/DatabaseModels/Incident.js +57 -0
  46. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  47. package/build/dist/Models/DatabaseModels/Project.js +27 -0
  48. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/Service.js +1 -2
  50. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  52. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  53. package/build/dist/Server/API/StatusPageAPI.js +4 -0
  54. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  55. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777018175127-AddTelemetryRetentionSettings.js +14 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777018175127-AddTelemetryRetentionSettings.js.map +1 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  63. package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
  64. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  65. package/build/dist/Server/Services/OpenTelemetryIngestService.js +30 -4
  66. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  67. package/build/dist/Server/Services/ServiceService.js +22 -1
  68. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  69. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
  70. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  71. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
  72. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  73. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +66 -12
  74. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  75. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
  76. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  77. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +91 -15
  78. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  79. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
  80. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  81. package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
  82. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  83. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
  84. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  85. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
  86. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
  87. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js +22 -0
  88. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js.map +1 -0
  89. package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
  90. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  91. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
  92. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
  93. package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
  94. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  95. package/build/dist/Types/StatusPage/StatusPageLanguage.js +22 -0
  96. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
  97. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
  98. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  99. package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
  100. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  101. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
  102. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  103. package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
  104. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  105. package/build/dist/UI/Components/Filters/FiltersForm.js +6 -2
  106. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  107. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
  108. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
  109. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
  110. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
  111. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
  112. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
  113. package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
  114. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  115. package/package.json +1 -1
@@ -8,6 +8,7 @@ import MetricCriteriaContext, {
8
8
  MetricComponent,
9
9
  MetricComponentValue,
10
10
  } from "../../../../Types/Monitor/MetricMonitor/MetricCriteriaContext";
11
+ import MetricSeriesResult from "../../../../Types/Monitor/MetricMonitor/MetricSeriesResult";
11
12
  import MonitorStep from "../../../../Types/Monitor/MonitorStep";
12
13
  import { JSONObject } from "../../../../Types/JSON";
13
14
  import DataToProcess from "../DataToProcess";
@@ -23,6 +24,20 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
23
24
  import MetricUnitUtil from "../../../../Utils/MetricUnitUtil";
24
25
  import MetricFormulaEvaluator from "../../../../Utils/Metrics/MetricFormulaEvaluator";
25
26
 
27
+ /**
28
+ * Result of evaluating a single criteria filter against a single metric
29
+ * series. `rootCause` is null when the filter did not match; otherwise
30
+ * it's the human-readable comparison message. `context` always reflects
31
+ * the metric identity for this series (used to render the metric
32
+ * details + breaching samples section of the incident root cause).
33
+ */
34
+ export interface MetricSeriesEvaluationResult {
35
+ fingerprint: string | undefined;
36
+ labels: JSONObject;
37
+ rootCause: string | null;
38
+ context: MetricCriteriaContext;
39
+ }
40
+
26
41
  export default class MetricMonitorCriteria {
27
42
  @CaptureSpan()
28
43
  public static async isMonitorInstanceCriteriaFilterMet(input: {
@@ -30,8 +45,50 @@ export default class MetricMonitorCriteria {
30
45
  criteriaFilter: CriteriaFilter;
31
46
  monitorStep: MonitorStep;
32
47
  }): Promise<string | null> {
33
- // Metric Monitoring Check
48
+ const evaluations: Array<MetricSeriesEvaluationResult> =
49
+ MetricMonitorCriteria.evaluateAllSeries(input);
50
+
51
+ /*
52
+ * Backwards-compat: the scalar entrypoint collapses per-series
53
+ * evaluation down to the first matching series so existing callers
54
+ * (single-incident path) keep working. The per-series code path uses
55
+ * `evaluateAllSeries` directly.
56
+ */
57
+ const match: MetricSeriesEvaluationResult | undefined = evaluations.find(
58
+ (e: MetricSeriesEvaluationResult) => {
59
+ return e.rootCause !== null;
60
+ },
61
+ );
62
+
63
+ /*
64
+ * Always populate the legacy single-context field so the root-cause
65
+ * renderer can still read metric identity from the criteria filter
66
+ * even when nothing matched. Pick the first evaluation's context.
67
+ */
68
+ if (evaluations.length > 0) {
69
+ input.criteriaFilter.metricCriteriaContext = (
70
+ match || evaluations[0]!
71
+ ).context;
72
+ }
34
73
 
74
+ return match ? match.rootCause : null;
75
+ }
76
+
77
+ /**
78
+ * Evaluate a single criteria filter against every series produced by
79
+ * the monitor. For monitors without group-by, this returns a single
80
+ * evaluation covering all aggregated results (legacy behavior). For
81
+ * monitors with group-by attributes set, it returns one evaluation
82
+ * per unique series fingerprint — each with its own
83
+ * `MetricCriteriaContext` carrying that series' breaching samples
84
+ * and labels. The caller fans this out into one incident per
85
+ * breaching series.
86
+ */
87
+ public static evaluateAllSeries(input: {
88
+ dataToProcess: DataToProcess;
89
+ criteriaFilter: CriteriaFilter;
90
+ monitorStep: MonitorStep;
91
+ }): Array<MetricSeriesEvaluationResult> {
35
92
  if (
36
93
  input.criteriaFilter.metricMonitorOptions &&
37
94
  !input.criteriaFilter.metricMonitorOptions.metricAggregationType
@@ -41,20 +98,14 @@ export default class MetricMonitorCriteria {
41
98
  }
42
99
 
43
100
  if (input.criteriaFilter.checkOn !== CheckOn.MetricValue) {
44
- return null;
101
+ return [];
45
102
  }
46
103
 
47
- const rawThreshold: number | null = CompareCriteria.convertToNumber(
48
- input.criteriaFilter.value,
49
- );
50
-
51
- const metricAlias: string =
52
- input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
53
-
54
104
  const metricResponse: MetricMonitorResponse =
55
105
  input.dataToProcess as MetricMonitorResponse;
56
- const metricAggregatedResult: Array<AggregatedResult> =
57
- metricResponse.metricResult || [];
106
+
107
+ const seriesBreakdown: Array<MetricSeriesResult> | undefined =
108
+ metricResponse.seriesBreakdown;
58
109
 
59
110
  const queryConfigs: Array<MetricQueryConfigData> =
60
111
  input.monitorStep.data?.metricMonitor?.metricViewConfig?.queryConfigs ||
@@ -63,6 +114,60 @@ export default class MetricMonitorCriteria {
63
114
  input.monitorStep.data?.metricMonitor?.metricViewConfig?.formulaConfigs ||
64
115
  [];
65
116
 
117
+ /*
118
+ * Series-less path: one synthetic "all-series" evaluation over the
119
+ * flat metricResult. Preserves the pre-group-by behavior exactly.
120
+ */
121
+ if (!seriesBreakdown || seriesBreakdown.length === 0) {
122
+ const result: MetricSeriesEvaluationResult =
123
+ MetricMonitorCriteria.evaluateOneSeries({
124
+ criteriaFilter: input.criteriaFilter,
125
+ aggregatedResults: metricResponse.metricResult || [],
126
+ queryConfigs,
127
+ formulaConfigs,
128
+ seriesFingerprint: undefined,
129
+ seriesLabels: {},
130
+ });
131
+ return [result];
132
+ }
133
+
134
+ return seriesBreakdown.map((series: MetricSeriesResult) => {
135
+ return MetricMonitorCriteria.evaluateOneSeries({
136
+ criteriaFilter: input.criteriaFilter,
137
+ aggregatedResults: series.aggregatedResults,
138
+ queryConfigs,
139
+ formulaConfigs,
140
+ seriesFingerprint: series.fingerprint,
141
+ seriesLabels: series.labels,
142
+ });
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Core evaluation loop: compare the samples for one metric series
148
+ * against the criteria threshold. Builds the metric identity context,
149
+ * identifies breaching samples, and assembles the human-readable
150
+ * root-cause message. Factored out so `evaluateAllSeries` can invoke
151
+ * it once per series without duplicating logic.
152
+ */
153
+ private static evaluateOneSeries(input: {
154
+ criteriaFilter: CriteriaFilter;
155
+ aggregatedResults: Array<AggregatedResult>;
156
+ queryConfigs: Array<MetricQueryConfigData>;
157
+ formulaConfigs: Array<MetricFormulaConfigData>;
158
+ seriesFingerprint: string | undefined;
159
+ seriesLabels: JSONObject;
160
+ }): MetricSeriesEvaluationResult {
161
+ const rawThreshold: number | null = CompareCriteria.convertToNumber(
162
+ input.criteriaFilter.value,
163
+ );
164
+
165
+ const metricAlias: string =
166
+ input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
167
+
168
+ const metricAggregatedResult: Array<AggregatedResult> =
169
+ input.aggregatedResults;
170
+
66
171
  /*
67
172
  * Resolve which query/formula the alias refers to. Use explicit index
68
173
  * checks (not `findIndex() || -1`, which incorrectly falls back to -1
@@ -73,25 +178,25 @@ export default class MetricMonitorCriteria {
73
178
  let aliasIndex: number = -1;
74
179
 
75
180
  if (metricAlias) {
76
- const qIdx: number = queryConfigs.findIndex(
181
+ const qIdx: number = input.queryConfigs.findIndex(
77
182
  (q: MetricQueryConfigData) => {
78
183
  return q.metricAliasData?.metricVariable === metricAlias;
79
184
  },
80
185
  );
81
186
 
82
187
  if (qIdx >= 0) {
83
- matchedQuery = queryConfigs[qIdx] || null;
188
+ matchedQuery = input.queryConfigs[qIdx] || null;
84
189
  aliasIndex = qIdx;
85
190
  } else {
86
- const fIdx: number = formulaConfigs.findIndex(
191
+ const fIdx: number = input.formulaConfigs.findIndex(
87
192
  (f: MetricFormulaConfigData) => {
88
193
  return f.metricAliasData?.metricVariable === metricAlias;
89
194
  },
90
195
  );
91
196
 
92
197
  if (fIdx >= 0) {
93
- matchedFormula = formulaConfigs[fIdx] || null;
94
- aliasIndex = queryConfigs.length + fIdx;
198
+ matchedFormula = input.formulaConfigs[fIdx] || null;
199
+ aliasIndex = input.queryConfigs.length + fIdx;
95
200
  }
96
201
  }
97
202
  }
@@ -105,8 +210,8 @@ export default class MetricMonitorCriteria {
105
210
  ? metricAggregatedResult[aliasIndex]
106
211
  : metricAggregatedResult[0];
107
212
 
108
- if (!matchedQuery && !matchedFormula && queryConfigs[0]) {
109
- matchedQuery = queryConfigs[0];
213
+ if (!matchedQuery && !matchedFormula && input.queryConfigs[0]) {
214
+ matchedQuery = input.queryConfigs[0];
110
215
  }
111
216
 
112
217
  /*
@@ -119,14 +224,24 @@ export default class MetricMonitorCriteria {
119
224
  matchedFormula,
120
225
  metricAlias,
121
226
  criteriaFilter: input.criteriaFilter,
122
- queryConfigs,
123
- formulaConfigs,
227
+ queryConfigs: input.queryConfigs,
228
+ formulaConfigs: input.formulaConfigs,
124
229
  });
125
230
 
126
- input.criteriaFilter.metricCriteriaContext = metricContext;
231
+ if (input.seriesFingerprint) {
232
+ metricContext.seriesFingerprint = input.seriesFingerprint;
233
+ }
234
+ if (input.seriesLabels && Object.keys(input.seriesLabels).length > 0) {
235
+ metricContext.seriesLabels = input.seriesLabels;
236
+ }
127
237
 
128
238
  if (rawThreshold === null) {
129
- return null;
239
+ return {
240
+ fingerprint: input.seriesFingerprint,
241
+ labels: input.seriesLabels,
242
+ rootCause: null,
243
+ context: metricContext,
244
+ };
130
245
  }
131
246
 
132
247
  /*
@@ -180,11 +295,21 @@ export default class MetricMonitorCriteria {
180
295
  NoDataPolicy.Ignore;
181
296
 
182
297
  if (policy === NoDataPolicy.Ignore) {
183
- return null;
298
+ return {
299
+ fingerprint: input.seriesFingerprint,
300
+ labels: input.seriesLabels,
301
+ rootCause: null,
302
+ context: metricContext,
303
+ };
184
304
  }
185
305
 
186
306
  if (policy === NoDataPolicy.Trigger) {
187
- return `No data received for ${metricContext.metricName} in the evaluation window — triggering per no-data policy.`;
307
+ return {
308
+ fingerprint: input.seriesFingerprint,
309
+ labels: input.seriesLabels,
310
+ rootCause: `No data received for ${metricContext.metricName} in the evaluation window — triggering per no-data policy.`,
311
+ context: metricContext,
312
+ };
188
313
  }
189
314
 
190
315
  // TreatAsZero: fall through to the comparator with value 0.
@@ -206,7 +331,12 @@ export default class MetricMonitorCriteria {
206
331
  });
207
332
 
208
333
  if (!comparisonMessage) {
209
- return null;
334
+ return {
335
+ fingerprint: input.seriesFingerprint,
336
+ labels: input.seriesLabels,
337
+ rootCause: null,
338
+ context: metricContext,
339
+ };
210
340
  }
211
341
 
212
342
  /*
@@ -232,8 +362,8 @@ export default class MetricMonitorCriteria {
232
362
  matchedFormula
233
363
  ? MetricMonitorCriteria.buildComponentValueLookup({
234
364
  components: metricContext.components || [],
235
- queryConfigs,
236
- formulaConfigs,
365
+ queryConfigs: input.queryConfigs,
366
+ formulaConfigs: input.formulaConfigs,
237
367
  metricAggregatedResult,
238
368
  })
239
369
  : null;
@@ -276,7 +406,12 @@ export default class MetricMonitorCriteria {
276
406
  metricContext.breachingSamples = breachingSamples;
277
407
  }
278
408
 
279
- return comparisonMessage;
409
+ return {
410
+ fingerprint: input.seriesFingerprint,
411
+ labels: input.seriesLabels,
412
+ rootCause: comparisonMessage,
413
+ context: metricContext,
414
+ };
280
415
  }
281
416
 
282
417
  private static buildComponentValueLookup(input: {
@@ -438,6 +573,17 @@ export default class MetricMonitorCriteria {
438
573
  ? Object.keys(q.metricQueryData.groupBy as Record<string, unknown>)
439
574
  : [];
440
575
 
576
+ /*
577
+ * Include user-selected attribute keys as part of the groupBy view
578
+ * so the root-cause block shows "Grouped By: host.name" not just the
579
+ * raw columns ClickHouse was asked to partition on.
580
+ */
581
+ const groupByAttributeKeys: Array<string> =
582
+ q?.metricQueryData?.groupByAttributeKeys || [];
583
+ const allGroupBy: Array<string> = Array.from(
584
+ new Set([...groupBy, ...groupByAttributeKeys]),
585
+ );
586
+
441
587
  const components: Array<MetricComponent> | undefined = f
442
588
  ? MetricMonitorCriteria.buildFormulaComponents({
443
589
  formulaConfig: f,
@@ -454,7 +600,7 @@ export default class MetricMonitorCriteria {
454
600
  isFormula: Boolean(f),
455
601
  formulaExpression: f?.metricFormulaData?.metricFormula,
456
602
  filterAttributes,
457
- groupBy,
603
+ groupBy: allGroupBy,
458
604
  timeWindowMinutes:
459
605
  input.criteriaFilter.evaluateOverTimeOptions?.timeValueInMinutes,
460
606
  ...(components && components.length > 0 ? { components } : {}),
@@ -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 &&
@@ -26,6 +26,7 @@ import MonitorTemplateUtil from "./MonitorTemplateUtil";
26
26
  import { JSONObject } from "../../../Types/JSON";
27
27
  import OneUptimeDate from "../../../Types/Date";
28
28
  import MonitorEvaluationSummary from "../../../Types/Monitor/MonitorEvaluationSummary";
29
+ import { PerSeriesCriteriaMatch } from "../../../Types/Probe/ProbeApiIngestResponse";
29
30
 
30
31
  export default class MonitorAlert {
31
32
  @CaptureSpan()
@@ -36,6 +37,7 @@ export default class MonitorAlert {
36
37
  criteriaInstance: MonitorCriteriaInstance | null;
37
38
  dataToProcess: DataToProcess;
38
39
  evaluationSummary?: MonitorEvaluationSummary | undefined;
40
+ breachingSeriesFingerprints?: Set<string> | undefined;
39
41
  }): Promise<Array<Alert>> {
40
42
  // check active alerts and if there are open alerts, do not cretae anothr alert.
41
43
  const openAlerts: Array<Alert> = await AlertService.findBy({
@@ -49,11 +51,14 @@ export default class MonitorAlert {
49
51
  limit: LIMIT_PER_PROJECT,
50
52
  select: {
51
53
  _id: true,
54
+ title: true,
52
55
  createdCriteriaId: true,
53
56
  projectId: true,
54
57
  alertNumber: true,
55
58
  alertNumberWithPrefix: true,
56
59
  currentAlertStateId: true,
60
+ seriesFingerprint: true,
61
+ seriesLabels: true,
57
62
  },
58
63
  props: {
59
64
  isRoot: true,
@@ -68,6 +73,7 @@ export default class MonitorAlert {
68
73
  autoResolveCriteriaInstanceIdAlertIdsDictionary:
69
74
  input.autoResolveCriteriaInstanceIdAlertIdsDictionary,
70
75
  criteriaInstance: input.criteriaInstance,
76
+ breachingSeriesFingerprints: input.breachingSeriesFingerprints,
71
77
  });
72
78
 
73
79
  if (shouldClose) {
@@ -106,6 +112,7 @@ export default class MonitorAlert {
106
112
  props: {
107
113
  telemetryQuery?: TelemetryQuery | undefined;
108
114
  };
115
+ matchesPerSeries?: Array<PerSeriesCriteriaMatch> | undefined;
109
116
  }): Promise<void> {
110
117
  const alertLogAttributes: LogAttributes = {
111
118
  projectId: input.monitor.projectId?.toString(),
@@ -116,6 +123,16 @@ export default class MonitorAlert {
116
123
  `${input.monitor.id?.toString()} - Check open alerts.`,
117
124
  alertLogAttributes,
118
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
+
119
136
  // check active alerts and if there are open alerts, do not cretae anothr alert.
120
137
  const openAlerts: Array<Alert> =
121
138
  await this.checkOpenAlertsAndCloseIfResolved({
@@ -126,19 +143,31 @@ export default class MonitorAlert {
126
143
  criteriaInstance: input.criteriaInstance,
127
144
  dataToProcess: input.dataToProcess,
128
145
  evaluationSummary: input.evaluationSummary,
146
+ breachingSeriesFingerprints,
129
147
  });
130
148
 
131
- if (input.criteriaInstance.data?.createAlerts) {
132
- // create alerts
149
+ if (!input.criteriaInstance.data?.createAlerts) {
150
+ return;
151
+ }
133
152
 
134
- for (const criteriaAlert of input.criteriaInstance.data?.alerts || []) {
135
- // should create alert.
153
+ const seriesToProcess: Array<PerSeriesCriteriaMatch | undefined> =
154
+ input.matchesPerSeries && input.matchesPerSeries.length > 0
155
+ ? input.matchesPerSeries
156
+ : [undefined];
157
+
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;
136
164
 
137
165
  const alreadyOpenAlert: Alert | undefined = openAlerts.find(
138
166
  (alert: Alert) => {
139
167
  return (
140
168
  alert.createdCriteriaId ===
141
- input.criteriaInstance.data?.id.toString()
169
+ input.criteriaInstance.data?.id.toString() &&
170
+ (alert.seriesFingerprint || undefined) === seriesFingerprint
142
171
  );
143
172
  },
144
173
  );
@@ -156,9 +185,12 @@ export default class MonitorAlert {
156
185
  );
157
186
 
158
187
  if (hasAlreadyOpenAlert) {
188
+ const renderedAlertTitle: string =
189
+ alreadyOpenAlert?.title || criteriaAlert.title;
190
+
159
191
  input.evaluationSummary?.events.push({
160
192
  type: "alert-skipped",
161
- title: `Alert already active: ${criteriaAlert.title}`,
193
+ title: `Alert already active: ${renderedAlertTitle}`,
162
194
  message:
163
195
  "Skipped creating a new alert because an active alert exists for this criteria.",
164
196
  relatedCriteriaId: input.criteriaInstance.data?.id,
@@ -183,6 +215,8 @@ export default class MonitorAlert {
183
215
  MonitorTemplateUtil.buildTemplateStorageMap({
184
216
  monitorType: input.monitor.monitorType!,
185
217
  dataToProcess: input.dataToProcess,
218
+ monitor: input.monitor,
219
+ seriesLabels,
186
220
  });
187
221
 
188
222
  alert.title = MonitorTemplateUtil.processTemplateString({
@@ -224,13 +258,20 @@ export default class MonitorAlert {
224
258
 
225
259
  alert.monitor = input.monitor;
226
260
  alert.projectId = input.monitor.projectId!;
227
- alert.rootCause = input.rootCause;
261
+ alert.rootCause = seriesRootCause;
228
262
  alert.createdStateLog = JSON.parse(
229
263
  JSON.stringify(input.dataToProcess, null, 2),
230
264
  );
231
265
 
232
266
  alert.createdCriteriaId = input.criteriaInstance.data.id.toString();
233
267
 
268
+ if (seriesFingerprint) {
269
+ alert.seriesFingerprint = seriesFingerprint;
270
+ }
271
+ if (seriesLabels && Object.keys(seriesLabels).length > 0) {
272
+ alert.seriesLabels = seriesLabels;
273
+ }
274
+
234
275
  alert.onCallDutyPolicies =
235
276
  criteriaAlert.onCallPolicyIds?.map((id: ObjectID) => {
236
277
  const onCallPolicy: OnCallDutyPolicy = new OnCallDutyPolicy();
@@ -436,7 +477,50 @@ export default class MonitorAlert {
436
477
  openAlert: Alert;
437
478
  autoResolveCriteriaInstanceIdAlertIdsDictionary: Dictionary<Array<string>>;
438
479
  criteriaInstance: MonitorCriteriaInstance | null; // null if no criteia met.
480
+ breachingSeriesFingerprints?: Set<string> | undefined;
439
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
+
440
524
  if (
441
525
  input.openAlert.createdCriteriaId?.toString() ===
442
526
  input.criteriaInstance?.data?.id.toString()