@oneuptime/common 14.0.5 → 14.0.6
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/Server/Services/MetricService.ts +1 -1
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MonitorCriteriaEvaluator reaches the template renderer, which loads the
|
|
3
|
+
* native isolated-vm addon. Nothing here uses the sandbox and the
|
|
4
|
+
* prebuilt binary cannot always dlopen in the test environment.
|
|
5
|
+
*/
|
|
6
|
+
jest.mock("isolated-vm", () => {
|
|
7
|
+
return {};
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
import Monitor from "../../../../Models/DatabaseModels/Monitor";
|
|
11
|
+
import MonitorCriteriaEvaluator from "../../../../Server/Utils/Monitor/MonitorCriteriaEvaluator";
|
|
12
|
+
import MetricMonitorCriteria from "../../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
|
|
13
|
+
import AggregateModel from "../../../../Types/BaseDatabase/AggregatedModel";
|
|
14
|
+
import AggregatedResult from "../../../../Types/BaseDatabase/AggregatedResult";
|
|
15
|
+
import FilterCondition from "../../../../Types/Filter/FilterCondition";
|
|
16
|
+
import {
|
|
17
|
+
CheckOn,
|
|
18
|
+
CriteriaFilter,
|
|
19
|
+
EvaluateOverTimeType,
|
|
20
|
+
FilterType,
|
|
21
|
+
} from "../../../../Types/Monitor/CriteriaFilter";
|
|
22
|
+
import MetricMonitorResponse from "../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
23
|
+
import MonitorCriteria from "../../../../Types/Monitor/MonitorCriteria";
|
|
24
|
+
import MonitorCriteriaInstance from "../../../../Types/Monitor/MonitorCriteriaInstance";
|
|
25
|
+
import MonitorEvaluationSummary, {
|
|
26
|
+
MonitorEvaluationCriteriaResult,
|
|
27
|
+
MonitorEvaluationFilterResult,
|
|
28
|
+
} from "../../../../Types/Monitor/MonitorEvaluationSummary";
|
|
29
|
+
import MonitorStep from "../../../../Types/Monitor/MonitorStep";
|
|
30
|
+
import MonitorType from "../../../../Types/Monitor/MonitorType";
|
|
31
|
+
import MetricAliasData from "../../../../Types/Metrics/MetricAliasData";
|
|
32
|
+
import MetricQueryConfigData from "../../../../Types/Metrics/MetricQueryConfigData";
|
|
33
|
+
import MetricQueryData from "../../../../Types/Metrics/MetricQueryData";
|
|
34
|
+
import MetricsViewConfig from "../../../../Types/Metrics/MetricsViewConfig";
|
|
35
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
36
|
+
import ProbeApiIngestResponse from "../../../../Types/Probe/ProbeApiIngestResponse";
|
|
37
|
+
import RollingTime from "../../../../Types/RollingTime/RollingTime";
|
|
38
|
+
import { describe, expect, test } from "@jest/globals";
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* REGRESSION: a decimal metric threshold typed in the dashboard was
|
|
42
|
+
* truncated to an integer before it was compared.
|
|
43
|
+
*
|
|
44
|
+
* The criteria form saves CriteriaFilter.value as the STRING the user
|
|
45
|
+
* typed, and MetricMonitorCriteria read it through
|
|
46
|
+
* CompareCriteria.convertToNumber, which runs parseInt. So "0.9" on a
|
|
47
|
+
* ratio metric was compared as 0 — "error ratio > 0.9" fired on every
|
|
48
|
+
* non-zero sample — and "85.5" was compared as 85. The evaluation log
|
|
49
|
+
* meanwhile rendered the threshold with Number(), so it read "expected
|
|
50
|
+
* to be greater than 0.9" right next to a criteria it said was met by
|
|
51
|
+
* 0.5.
|
|
52
|
+
*
|
|
53
|
+
* Metric-value thresholds are now parsed with parseFloat. Every other
|
|
54
|
+
* checkOn keeps parseInt (ProfileMonitorCriteria.test.ts pins that).
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const CRITERIA_ID: string = "decimal-threshold";
|
|
58
|
+
|
|
59
|
+
function buildStep(criteriaInstance: MonitorCriteriaInstance): MonitorStep {
|
|
60
|
+
const metricAliasData: MetricAliasData = {
|
|
61
|
+
metricVariable: "a",
|
|
62
|
+
title: "Error Ratio",
|
|
63
|
+
description: undefined,
|
|
64
|
+
legend: undefined,
|
|
65
|
+
legendUnit: undefined,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const queryConfig: MetricQueryConfigData = {
|
|
69
|
+
metricAliasData,
|
|
70
|
+
metricQueryData: {
|
|
71
|
+
filterData: { metricName: "http.server.error_rate" },
|
|
72
|
+
} as unknown as MetricQueryData,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const metricViewConfig: MetricsViewConfig = {
|
|
76
|
+
queryConfigs: [queryConfig],
|
|
77
|
+
formulaConfigs: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const criteria: MonitorCriteria = new MonitorCriteria();
|
|
81
|
+
criteria.data = {
|
|
82
|
+
monitorCriteriaInstanceArray: [criteriaInstance],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
86
|
+
monitorStep.data = {
|
|
87
|
+
id: ObjectID.generate().toString(),
|
|
88
|
+
monitorCriteria: criteria,
|
|
89
|
+
} as unknown as MonitorStep["data"];
|
|
90
|
+
monitorStep.data!.metricMonitor = {
|
|
91
|
+
metricViewConfig,
|
|
92
|
+
rollingTime: RollingTime.Past1Minute,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return monitorStep;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildResponse(sampleValues: Array<number>): MetricMonitorResponse {
|
|
99
|
+
const aggregated: AggregatedResult = {
|
|
100
|
+
data: sampleValues.map((value: number) => {
|
|
101
|
+
return {
|
|
102
|
+
timestamp: new Date("2026-09-25T00:00:00.000Z"),
|
|
103
|
+
value,
|
|
104
|
+
} as AggregateModel;
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
projectId: ObjectID.generate(),
|
|
110
|
+
monitorId: ObjectID.generate(),
|
|
111
|
+
metricResult: [aggregated],
|
|
112
|
+
metricViewConfig: { queryConfigs: [], formulaConfigs: [] },
|
|
113
|
+
} as unknown as MetricMonitorResponse;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The filter exactly as the dashboard criteria form saves it.
|
|
117
|
+
function dashboardFilter(input: {
|
|
118
|
+
filterType: FilterType;
|
|
119
|
+
value: string;
|
|
120
|
+
}): CriteriaFilter {
|
|
121
|
+
return {
|
|
122
|
+
checkOn: CheckOn.MetricValue,
|
|
123
|
+
filterType: input.filterType,
|
|
124
|
+
value: input.value,
|
|
125
|
+
metricMonitorOptions: {
|
|
126
|
+
metricAlias: "a",
|
|
127
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function criteriaInstance(filter: CriteriaFilter): MonitorCriteriaInstance {
|
|
133
|
+
const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
|
|
134
|
+
instance.data = {
|
|
135
|
+
id: CRITERIA_ID,
|
|
136
|
+
name: "Error ratio breach",
|
|
137
|
+
description: "",
|
|
138
|
+
monitorStatusId: undefined,
|
|
139
|
+
filterCondition: FilterCondition.All,
|
|
140
|
+
filters: [filter],
|
|
141
|
+
incidents: [],
|
|
142
|
+
alerts: [],
|
|
143
|
+
createAlerts: true,
|
|
144
|
+
createIncidents: true,
|
|
145
|
+
isEnabled: true,
|
|
146
|
+
} as unknown as MonitorCriteriaInstance["data"];
|
|
147
|
+
return instance;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function processMonitorStep(input: {
|
|
151
|
+
filter: CriteriaFilter;
|
|
152
|
+
sampleValues: Array<number>;
|
|
153
|
+
}): Promise<{
|
|
154
|
+
response: ProbeApiIngestResponse;
|
|
155
|
+
filterResult: MonitorEvaluationFilterResult;
|
|
156
|
+
}> {
|
|
157
|
+
const monitor: Monitor = new Monitor();
|
|
158
|
+
monitor._id = ObjectID.generate().toString();
|
|
159
|
+
monitor.projectId = ObjectID.generate();
|
|
160
|
+
monitor.monitorType = MonitorType.Metrics;
|
|
161
|
+
monitor.name = "Error ratio";
|
|
162
|
+
|
|
163
|
+
const summary: MonitorEvaluationSummary = {
|
|
164
|
+
criteriaResults: [],
|
|
165
|
+
events: [],
|
|
166
|
+
} as unknown as MonitorEvaluationSummary;
|
|
167
|
+
|
|
168
|
+
const response: ProbeApiIngestResponse =
|
|
169
|
+
await MonitorCriteriaEvaluator.processMonitorStep({
|
|
170
|
+
dataToProcess: buildResponse(input.sampleValues),
|
|
171
|
+
monitorStep: buildStep(criteriaInstance(input.filter)),
|
|
172
|
+
monitor,
|
|
173
|
+
probeApiIngestResponse: {
|
|
174
|
+
monitorId: monitor.id!,
|
|
175
|
+
rootCause: null,
|
|
176
|
+
},
|
|
177
|
+
evaluationSummary: summary,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const criteriaResult: MonitorEvaluationCriteriaResult | undefined =
|
|
181
|
+
summary.criteriaResults[0];
|
|
182
|
+
expect(criteriaResult).toBeDefined();
|
|
183
|
+
|
|
184
|
+
const filterResult: MonitorEvaluationFilterResult | undefined =
|
|
185
|
+
criteriaResult!.filters[0];
|
|
186
|
+
expect(filterResult).toBeDefined();
|
|
187
|
+
|
|
188
|
+
return { response, filterResult: filterResult! };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function metricCriteriaMet(input: {
|
|
192
|
+
filter: CriteriaFilter;
|
|
193
|
+
sampleValues: Array<number>;
|
|
194
|
+
}): Promise<string | null> {
|
|
195
|
+
const criteria: MonitorCriteriaInstance = criteriaInstance(input.filter);
|
|
196
|
+
|
|
197
|
+
return await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
198
|
+
dataToProcess: buildResponse(input.sampleValues),
|
|
199
|
+
criteriaFilter: input.filter,
|
|
200
|
+
monitorStep: buildStep(criteria),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
describe("decimal metric thresholds, end to end through processMonitorStep", () => {
|
|
205
|
+
test('"> 0.9" does not fire on a 0.5 sample', async () => {
|
|
206
|
+
/*
|
|
207
|
+
* The bug: "0.9" was parsed as 0, and 0.5 > 0 met the criteria and
|
|
208
|
+
* opened an incident.
|
|
209
|
+
*/
|
|
210
|
+
const { response, filterResult } = await processMonitorStep({
|
|
211
|
+
filter: dashboardFilter({
|
|
212
|
+
filterType: FilterType.GreaterThan,
|
|
213
|
+
value: "0.9",
|
|
214
|
+
}),
|
|
215
|
+
sampleValues: [0.5],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(response.criteriaMetId).toBeUndefined();
|
|
219
|
+
expect(response.rootCause).toBeNull();
|
|
220
|
+
expect(filterResult.met).toBe(false);
|
|
221
|
+
expect(filterResult.value).toBe("0.9");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('"> 0.9" fires on a 0.95 sample and the root cause quotes 0.9', async () => {
|
|
225
|
+
const { response, filterResult } = await processMonitorStep({
|
|
226
|
+
filter: dashboardFilter({
|
|
227
|
+
filterType: FilterType.GreaterThan,
|
|
228
|
+
value: "0.9",
|
|
229
|
+
}),
|
|
230
|
+
sampleValues: [0.95],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(response.criteriaMetId).toBe(CRITERIA_ID);
|
|
234
|
+
expect(filterResult.met).toBe(true);
|
|
235
|
+
expect(response.rootCause).toContain("0.9");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('"> 85.5" does not fire on 85.2, which is above the old truncated 85', async () => {
|
|
239
|
+
const { response, filterResult } = await processMonitorStep({
|
|
240
|
+
filter: dashboardFilter({
|
|
241
|
+
filterType: FilterType.GreaterThan,
|
|
242
|
+
value: "85.5",
|
|
243
|
+
}),
|
|
244
|
+
sampleValues: [85.2],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(response.criteriaMetId).toBeUndefined();
|
|
248
|
+
expect(filterResult.met).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('"> 85.5" fires on 85.7', async () => {
|
|
252
|
+
const { response, filterResult } = await processMonitorStep({
|
|
253
|
+
filter: dashboardFilter({
|
|
254
|
+
filterType: FilterType.GreaterThan,
|
|
255
|
+
value: "85.5",
|
|
256
|
+
}),
|
|
257
|
+
sampleValues: [85.7],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(response.criteriaMetId).toBe(CRITERIA_ID);
|
|
261
|
+
expect(filterResult.met).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("the evaluation log quotes the threshold that was actually compared", async () => {
|
|
265
|
+
/*
|
|
266
|
+
* Before the fix this filter was logged as met, with the message
|
|
267
|
+
* "Error Ratio was 0.5 in 1 reading, above the 0 threshold."
|
|
268
|
+
*/
|
|
269
|
+
const notMet: { filterResult: MonitorEvaluationFilterResult } =
|
|
270
|
+
await processMonitorStep({
|
|
271
|
+
filter: dashboardFilter({
|
|
272
|
+
filterType: FilterType.GreaterThan,
|
|
273
|
+
value: "0.9",
|
|
274
|
+
}),
|
|
275
|
+
sampleValues: [0.5],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(notMet.filterResult.met).toBe(false);
|
|
279
|
+
expect(notMet.filterResult.message).toContain(
|
|
280
|
+
"expected to be greater than 0.9",
|
|
281
|
+
);
|
|
282
|
+
expect(notMet.filterResult.message).not.toContain("the 0 threshold");
|
|
283
|
+
|
|
284
|
+
const met: { filterResult: MonitorEvaluationFilterResult } =
|
|
285
|
+
await processMonitorStep({
|
|
286
|
+
filter: dashboardFilter({
|
|
287
|
+
filterType: FilterType.GreaterThan,
|
|
288
|
+
value: "0.9",
|
|
289
|
+
}),
|
|
290
|
+
sampleValues: [0.95],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(met.filterResult.met).toBe(true);
|
|
294
|
+
expect(met.filterResult.message).toContain("0.9 threshold");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("decimal metric thresholds in MetricMonitorCriteria", () => {
|
|
299
|
+
test('"< 0.5" fires on 0.3 (it compared as "< 0" before and never fired)', async () => {
|
|
300
|
+
expect(
|
|
301
|
+
await metricCriteriaMet({
|
|
302
|
+
filter: dashboardFilter({
|
|
303
|
+
filterType: FilterType.LessThan,
|
|
304
|
+
value: "0.5",
|
|
305
|
+
}),
|
|
306
|
+
sampleValues: [0.3],
|
|
307
|
+
}),
|
|
308
|
+
).toBeTruthy();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('"< 0.5" does not fire on 0.7', async () => {
|
|
312
|
+
expect(
|
|
313
|
+
await metricCriteriaMet({
|
|
314
|
+
filter: dashboardFilter({
|
|
315
|
+
filterType: FilterType.LessThan,
|
|
316
|
+
value: "0.5",
|
|
317
|
+
}),
|
|
318
|
+
sampleValues: [0.7],
|
|
319
|
+
}),
|
|
320
|
+
).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('">= 0.25" treats the boundary as met and 0.2 as not met', async () => {
|
|
324
|
+
const filter: CriteriaFilter = dashboardFilter({
|
|
325
|
+
filterType: FilterType.GreaterThanOrEqualTo,
|
|
326
|
+
value: "0.25",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(
|
|
330
|
+
await metricCriteriaMet({ filter, sampleValues: [0.25] }),
|
|
331
|
+
).toBeTruthy();
|
|
332
|
+
expect(await metricCriteriaMet({ filter, sampleValues: [0.2] })).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('"= 0.25" matches 0.25 and not 0', async () => {
|
|
336
|
+
const filter: CriteriaFilter = dashboardFilter({
|
|
337
|
+
filterType: FilterType.EqualTo,
|
|
338
|
+
value: "0.25",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(
|
|
342
|
+
await metricCriteriaMet({ filter, sampleValues: [0.25] }),
|
|
343
|
+
).toBeTruthy();
|
|
344
|
+
expect(await metricCriteriaMet({ filter, sampleValues: [0] })).toBeNull();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('a leading-dot ".5" is 0.5 (parseInt made it NaN, so the criteria never fired)', async () => {
|
|
348
|
+
expect(
|
|
349
|
+
await metricCriteriaMet({
|
|
350
|
+
filter: dashboardFilter({
|
|
351
|
+
filterType: FilterType.GreaterThan,
|
|
352
|
+
value: ".5",
|
|
353
|
+
}),
|
|
354
|
+
sampleValues: [0.6],
|
|
355
|
+
}),
|
|
356
|
+
).toBeTruthy();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("a negative decimal threshold keeps its fraction", async () => {
|
|
360
|
+
const filter: CriteriaFilter = dashboardFilter({
|
|
361
|
+
filterType: FilterType.LessThan,
|
|
362
|
+
value: "-0.5",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// parseInt("-0.5") is -0, so -0.2 < -0 used to fire.
|
|
366
|
+
expect(
|
|
367
|
+
await metricCriteriaMet({ filter, sampleValues: [-0.2] }),
|
|
368
|
+
).toBeNull();
|
|
369
|
+
expect(
|
|
370
|
+
await metricCriteriaMet({ filter, sampleValues: [-0.7] }),
|
|
371
|
+
).toBeTruthy();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("an integer string threshold behaves exactly as before", async () => {
|
|
375
|
+
const filter: CriteriaFilter = dashboardFilter({
|
|
376
|
+
filterType: FilterType.GreaterThan,
|
|
377
|
+
value: "90",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(
|
|
381
|
+
await metricCriteriaMet({ filter, sampleValues: [91] }),
|
|
382
|
+
).toBeTruthy();
|
|
383
|
+
expect(await metricCriteriaMet({ filter, sampleValues: [90] })).toBeNull();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("a numeric (template-authored) decimal threshold is unchanged", async () => {
|
|
387
|
+
const filter: CriteriaFilter = {
|
|
388
|
+
...dashboardFilter({ filterType: FilterType.GreaterThan, value: "" }),
|
|
389
|
+
value: 0.9,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
await metricCriteriaMet({ filter, sampleValues: [0.95] }),
|
|
394
|
+
).toBeTruthy();
|
|
395
|
+
expect(await metricCriteriaMet({ filter, sampleValues: [0.5] })).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("a non-numeric or empty threshold leaves the criteria inert", async () => {
|
|
399
|
+
for (const value of ["abc", "", " "]) {
|
|
400
|
+
expect(
|
|
401
|
+
await metricCriteriaMet({
|
|
402
|
+
filter: dashboardFilter({
|
|
403
|
+
filterType: FilterType.GreaterThan,
|
|
404
|
+
value,
|
|
405
|
+
}),
|
|
406
|
+
sampleValues: [1000],
|
|
407
|
+
}),
|
|
408
|
+
).toBeNull();
|
|
409
|
+
expect(
|
|
410
|
+
await metricCriteriaMet({
|
|
411
|
+
filter: dashboardFilter({ filterType: FilterType.LessThan, value }),
|
|
412
|
+
sampleValues: [-1000],
|
|
413
|
+
}),
|
|
414
|
+
).toBeNull();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
});
|
package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts
CHANGED
|
@@ -1355,6 +1355,102 @@ describe("A threshold saved as a string reads like the number", () => {
|
|
|
1355
1355
|
expect(rootCause).not.toContain("node/pve1");
|
|
1356
1356
|
expect(rootCause).not.toContain("node/pve3");
|
|
1357
1357
|
});
|
|
1358
|
+
|
|
1359
|
+
describe('pve-node-high-cpu with its decimal threshold as "0.9"', () => {
|
|
1360
|
+
/*
|
|
1361
|
+
* The template fires on `> 0.9` and recovers on `<= 0.9` over a 0–1
|
|
1362
|
+
* ratio. Read with parseInt, "0.9" was 0: the monitor went offline on
|
|
1363
|
+
* any CPU at all, and could only recover at exactly 0.
|
|
1364
|
+
*/
|
|
1365
|
+
function highCpuStep(): MonitorStep {
|
|
1366
|
+
const monitorStep: MonitorStep = stringifyThresholds(
|
|
1367
|
+
ungroup(proxmoxStep("pve-node-high-cpu")),
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
// Fixture guard: an ungrouped `> "0.9"`.
|
|
1371
|
+
const filter: CriteriaFilter = metricFiltersOf(firingOf(monitorStep))[0]!;
|
|
1372
|
+
expect(filter.filterType).toBe(FilterType.GreaterThan);
|
|
1373
|
+
expect(filter.value).toBe("0.9");
|
|
1374
|
+
expect(filter.metricMonitorOptions?.thresholdUnit).toBeFalsy();
|
|
1375
|
+
|
|
1376
|
+
return monitorStep;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function nodeCpuResponse(input: {
|
|
1380
|
+
monitorStep: MonitorStep;
|
|
1381
|
+
windowValue: number;
|
|
1382
|
+
nodes: Dictionary<number>;
|
|
1383
|
+
}): MetricMonitorResponse {
|
|
1384
|
+
return ungroupedResponse({
|
|
1385
|
+
monitorStep: input.monitorStep,
|
|
1386
|
+
fixture: PROXMOX,
|
|
1387
|
+
declaredUnits: { pve_cpu_usage_ratio: "1" },
|
|
1388
|
+
valuesByAlias: { node_cpu: [input.windowValue] },
|
|
1389
|
+
scans: [
|
|
1390
|
+
{
|
|
1391
|
+
alias: "node_cpu",
|
|
1392
|
+
metricUnit: "1",
|
|
1393
|
+
resources: Object.entries(input.nodes).map(
|
|
1394
|
+
([resourceId, value]: [string, number]) => {
|
|
1395
|
+
return {
|
|
1396
|
+
resourceId: resourceId,
|
|
1397
|
+
resourceType: "node",
|
|
1398
|
+
scope: "node",
|
|
1399
|
+
metricValue: value,
|
|
1400
|
+
lowestMetricValue: value,
|
|
1401
|
+
};
|
|
1402
|
+
},
|
|
1403
|
+
),
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
test("a cluster at 50% CPU does not fire, and the recovery criteria holds", async () => {
|
|
1410
|
+
const monitorStep: MonitorStep = highCpuStep();
|
|
1411
|
+
const recovery: MonitorCriteriaInstance =
|
|
1412
|
+
monitorStep.data!.monitorCriteria!.data!
|
|
1413
|
+
.monitorCriteriaInstanceArray[1]!;
|
|
1414
|
+
|
|
1415
|
+
const response: ProbeApiIngestResponse = await evaluate({
|
|
1416
|
+
monitorStep: monitorStep,
|
|
1417
|
+
fixture: PROXMOX,
|
|
1418
|
+
response: nodeCpuResponse({
|
|
1419
|
+
monitorStep: monitorStep,
|
|
1420
|
+
windowValue: 0.5,
|
|
1421
|
+
nodes: { "node/pve1": 0.5 },
|
|
1422
|
+
}),
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
expect(rootCauseFor(response, firingOf(monitorStep))).toBe("");
|
|
1426
|
+
expect(response.criteriaMetId?.toString()).toBe(
|
|
1427
|
+
recovery.data!.id!.toString(),
|
|
1428
|
+
);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
test("only the node past 0.9 is listed", async () => {
|
|
1432
|
+
const monitorStep: MonitorStep = highCpuStep();
|
|
1433
|
+
|
|
1434
|
+
const rootCause: string = await firingRootCause({
|
|
1435
|
+
monitorStep: monitorStep,
|
|
1436
|
+
fixture: PROXMOX,
|
|
1437
|
+
response: nodeCpuResponse({
|
|
1438
|
+
monitorStep: monitorStep,
|
|
1439
|
+
windowValue: 0.95,
|
|
1440
|
+
nodes: { "node/pve2": 0.5, "node/pve1": 0.95 },
|
|
1441
|
+
}),
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
const heads: Array<string> = entryHeads(
|
|
1445
|
+
listSection(rootCause, "Affected Resources"),
|
|
1446
|
+
);
|
|
1447
|
+
|
|
1448
|
+
expect(heads).toHaveLength(1);
|
|
1449
|
+
expect(heads[0]).toContain("`node/pve1`");
|
|
1450
|
+
// The node at 50% is healthy under a "> 0.9" criteria.
|
|
1451
|
+
expect(rootCause).not.toContain("node/pve2");
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1358
1454
|
});
|
|
1359
1455
|
|
|
1360
1456
|
/*
|
|
@@ -9,14 +9,16 @@ describe("AggregationType", () => {
|
|
|
9
9
|
describe("PercentileAggregationLevels", () => {
|
|
10
10
|
test("maps each percentile enum to its quantile fraction", () => {
|
|
11
11
|
expect(PercentileAggregationLevels[AggregationType.P50]).toBe(0.5);
|
|
12
|
+
expect(PercentileAggregationLevels[AggregationType.P75]).toBe(0.75);
|
|
12
13
|
expect(PercentileAggregationLevels[AggregationType.P90]).toBe(0.9);
|
|
13
14
|
expect(PercentileAggregationLevels[AggregationType.P95]).toBe(0.95);
|
|
14
15
|
expect(PercentileAggregationLevels[AggregationType.P99]).toBe(0.99);
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
test("contains exactly the
|
|
18
|
+
test("contains exactly the five percentile aggregations", () => {
|
|
18
19
|
expect(Object.keys(PercentileAggregationLevels).sort()).toEqual([
|
|
19
20
|
"P50",
|
|
21
|
+
"P75",
|
|
20
22
|
"P90",
|
|
21
23
|
"P95",
|
|
22
24
|
"P99",
|
|
@@ -27,6 +29,7 @@ describe("AggregationType", () => {
|
|
|
27
29
|
describe("isPercentileAggregation", () => {
|
|
28
30
|
test("returns true for every percentile aggregation", () => {
|
|
29
31
|
expect(isPercentileAggregation(AggregationType.P50)).toBe(true);
|
|
32
|
+
expect(isPercentileAggregation(AggregationType.P75)).toBe(true);
|
|
30
33
|
expect(isPercentileAggregation(AggregationType.P90)).toBe(true);
|
|
31
34
|
expect(isPercentileAggregation(AggregationType.P95)).toBe(true);
|
|
32
35
|
expect(isPercentileAggregation(AggregationType.P99)).toBe(true);
|
|
@@ -44,6 +47,7 @@ describe("AggregationType", () => {
|
|
|
44
47
|
describe("getPercentileLevel", () => {
|
|
45
48
|
test("returns the quantile fraction for percentile aggregations", () => {
|
|
46
49
|
expect(getPercentileLevel(AggregationType.P50)).toBe(0.5);
|
|
50
|
+
expect(getPercentileLevel(AggregationType.P75)).toBe(0.75);
|
|
47
51
|
expect(getPercentileLevel(AggregationType.P99)).toBe(0.99);
|
|
48
52
|
});
|
|
49
53
|
|