@oneuptime/common 11.5.5 → 11.5.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/Models/DatabaseModels/User.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +37 -0
- package/Server/Middleware/ProjectAuthorization.ts +40 -15
- package/Server/Services/TeamMemberService.ts +2 -0
- package/Server/Services/UserService.ts +10 -0
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +16 -6
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +14 -0
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +14 -3
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +130 -12
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +553 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +590 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageFormatterUnits.test.ts +254 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +821 -0
- package/build/dist/Models/DatabaseModels/User.js +46 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +26 -0
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +40 -14
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +2 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +8 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +15 -7
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +8 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +96 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import MonitorCriteriaObservationBuilder from "../../../../Server/Utils/Monitor/MonitorCriteriaObservationBuilder";
|
|
2
|
+
import Monitor from "../../../../Models/DatabaseModels/Monitor";
|
|
3
|
+
import {
|
|
4
|
+
CheckOn,
|
|
5
|
+
CriteriaFilter,
|
|
6
|
+
EvaluateOverTimeType,
|
|
7
|
+
FilterType,
|
|
8
|
+
} from "../../../../Types/Monitor/CriteriaFilter";
|
|
9
|
+
import AggregateModel from "../../../../Types/BaseDatabase/AggregatedModel";
|
|
10
|
+
import AggregatedResult from "../../../../Types/BaseDatabase/AggregatedResult";
|
|
11
|
+
import MetricAliasData from "../../../../Types/Metrics/MetricAliasData";
|
|
12
|
+
import MetricFormulaConfigData from "../../../../Types/Metrics/MetricFormulaConfigData";
|
|
13
|
+
import MetricQueryConfigData from "../../../../Types/Metrics/MetricQueryConfigData";
|
|
14
|
+
import MetricQueryData from "../../../../Types/Metrics/MetricQueryData";
|
|
15
|
+
import MetricsViewConfig from "../../../../Types/Metrics/MetricsViewConfig";
|
|
16
|
+
import MetricMonitorResponse from "../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
17
|
+
import MonitorStep from "../../../../Types/Monitor/MonitorStep";
|
|
18
|
+
import RollingTime from "../../../../Types/RollingTime/RollingTime";
|
|
19
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Assembles the monitor + metric response + step trio that
|
|
23
|
+
* MonitorCriteriaObservationBuilder needs, for a single metric alias "a"
|
|
24
|
+
* with a given native/legend unit and sample values. Mirrors the fixtures
|
|
25
|
+
* used by MetricMonitorCriteria.test.ts and MonitorCriteriaMessageBuilderUnits
|
|
26
|
+
* .test.ts, but additionally allows an optional nativeUnitsByMetricName map so
|
|
27
|
+
* we can exercise the native-unit fallback path in resolveMetricUnits.
|
|
28
|
+
*/
|
|
29
|
+
function buildInputs(input: {
|
|
30
|
+
metricNativeUnit: string;
|
|
31
|
+
sampleValues: Array<number>;
|
|
32
|
+
criteriaFilter: CriteriaFilter;
|
|
33
|
+
metricName?: string;
|
|
34
|
+
nativeUnitsByMetricName?: { [key: string]: string };
|
|
35
|
+
}): {
|
|
36
|
+
monitor: Monitor;
|
|
37
|
+
criteriaFilter: CriteriaFilter;
|
|
38
|
+
monitorStep: MonitorStep;
|
|
39
|
+
dataToProcess: MetricMonitorResponse;
|
|
40
|
+
} {
|
|
41
|
+
const metricName: string = input.metricName ?? "response_time";
|
|
42
|
+
|
|
43
|
+
const aliasData: MetricAliasData = {
|
|
44
|
+
metricVariable: "a",
|
|
45
|
+
title: "Response Time",
|
|
46
|
+
description: undefined,
|
|
47
|
+
legend: undefined,
|
|
48
|
+
legendUnit: input.metricNativeUnit,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const queryConfig: MetricQueryConfigData = {
|
|
52
|
+
metricAliasData: aliasData,
|
|
53
|
+
metricQueryData: {
|
|
54
|
+
filterData: {
|
|
55
|
+
metricName: metricName,
|
|
56
|
+
},
|
|
57
|
+
} as unknown as MetricQueryData,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const metricViewConfig: MetricsViewConfig = {
|
|
61
|
+
queryConfigs: [queryConfig],
|
|
62
|
+
formulaConfigs: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
66
|
+
monitorStep.data = {
|
|
67
|
+
id: ObjectID.generate().toString(),
|
|
68
|
+
monitorCriteria: { data: undefined } as never,
|
|
69
|
+
} as unknown as MonitorStep["data"];
|
|
70
|
+
monitorStep.data!.metricMonitor = {
|
|
71
|
+
metricViewConfig,
|
|
72
|
+
rollingTime: RollingTime.Past1Minute,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const aggregated: AggregatedResult = {
|
|
76
|
+
data: input.sampleValues.map((v: number) => {
|
|
77
|
+
return {
|
|
78
|
+
timestamp: new Date(),
|
|
79
|
+
value: v,
|
|
80
|
+
} as AggregateModel;
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const dataToProcess: MetricMonitorResponse = {
|
|
85
|
+
projectId: ObjectID.generate(),
|
|
86
|
+
metricResult: [aggregated],
|
|
87
|
+
metricViewConfig,
|
|
88
|
+
monitorId: ObjectID.generate(),
|
|
89
|
+
nativeUnitsByMetricName: input.nativeUnitsByMetricName,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
monitor: new Monitor(),
|
|
94
|
+
criteriaFilter: input.criteriaFilter,
|
|
95
|
+
monitorStep,
|
|
96
|
+
dataToProcess,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* Two-query variant (alias "a" and alias "b") so we can prove the observation
|
|
102
|
+
* builder resolves the criteria's alias to the correct series + unit. Each
|
|
103
|
+
* alias has its own legendUnit and its own aggregated result, positioned by
|
|
104
|
+
* index the way MonitorCriteriaDataExtractor.extractMetricValues expects.
|
|
105
|
+
*/
|
|
106
|
+
function buildTwoAliasInputs(input: {
|
|
107
|
+
aliasAUnit: string;
|
|
108
|
+
aliasBUnit: string;
|
|
109
|
+
aliasASamples: Array<number>;
|
|
110
|
+
aliasBSamples: Array<number>;
|
|
111
|
+
criteriaFilter: CriteriaFilter;
|
|
112
|
+
}): {
|
|
113
|
+
monitor: Monitor;
|
|
114
|
+
criteriaFilter: CriteriaFilter;
|
|
115
|
+
monitorStep: MonitorStep;
|
|
116
|
+
dataToProcess: MetricMonitorResponse;
|
|
117
|
+
} {
|
|
118
|
+
const queryA: MetricQueryConfigData = {
|
|
119
|
+
metricAliasData: {
|
|
120
|
+
metricVariable: "a",
|
|
121
|
+
title: "",
|
|
122
|
+
description: "",
|
|
123
|
+
legend: "",
|
|
124
|
+
legendUnit: input.aliasAUnit,
|
|
125
|
+
},
|
|
126
|
+
metricQueryData: {
|
|
127
|
+
filterData: {
|
|
128
|
+
metricName: "request_latency",
|
|
129
|
+
},
|
|
130
|
+
} as unknown as MetricQueryData,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const queryB: MetricQueryConfigData = {
|
|
134
|
+
metricAliasData: {
|
|
135
|
+
metricVariable: "b",
|
|
136
|
+
title: "",
|
|
137
|
+
description: "",
|
|
138
|
+
legend: "",
|
|
139
|
+
legendUnit: input.aliasBUnit,
|
|
140
|
+
},
|
|
141
|
+
metricQueryData: {
|
|
142
|
+
filterData: {
|
|
143
|
+
metricName: "db_latency",
|
|
144
|
+
},
|
|
145
|
+
} as unknown as MetricQueryData,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const metricViewConfig: MetricsViewConfig = {
|
|
149
|
+
queryConfigs: [queryA, queryB],
|
|
150
|
+
formulaConfigs: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
154
|
+
monitorStep.data = {
|
|
155
|
+
id: ObjectID.generate().toString(),
|
|
156
|
+
monitorCriteria: { data: undefined } as never,
|
|
157
|
+
} as unknown as MonitorStep["data"];
|
|
158
|
+
monitorStep.data!.metricMonitor = {
|
|
159
|
+
metricViewConfig,
|
|
160
|
+
rollingTime: RollingTime.Past1Minute,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const resultA: AggregatedResult = {
|
|
164
|
+
data: input.aliasASamples.map((v: number) => {
|
|
165
|
+
return { timestamp: new Date(), value: v } as AggregateModel;
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
const resultB: AggregatedResult = {
|
|
169
|
+
data: input.aliasBSamples.map((v: number) => {
|
|
170
|
+
return { timestamp: new Date(), value: v } as AggregateModel;
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const dataToProcess: MetricMonitorResponse = {
|
|
175
|
+
projectId: ObjectID.generate(),
|
|
176
|
+
metricResult: [resultA, resultB],
|
|
177
|
+
metricViewConfig,
|
|
178
|
+
monitorId: ObjectID.generate(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
monitor: new Monitor(),
|
|
183
|
+
criteriaFilter: input.criteriaFilter,
|
|
184
|
+
monitorStep,
|
|
185
|
+
dataToProcess,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/*
|
|
190
|
+
* Formula-alias variant: a base query (alias "a") plus a FORMULA (alias "c")
|
|
191
|
+
* that references it. extractMetricValues positions the formula's series at
|
|
192
|
+
* queryConfigs.length + formulaIndex, so metricResult[0] is the base query
|
|
193
|
+
* series and metricResult[1] is the formula series. Lets us prove a criteria
|
|
194
|
+
* targeting the formula alias "c" resolves the FORMULA's legendUnit — not the
|
|
195
|
+
* base query's legendUnit.
|
|
196
|
+
*/
|
|
197
|
+
function buildFormulaAliasInputs(input: {
|
|
198
|
+
queryUnit: string;
|
|
199
|
+
formulaUnit: string;
|
|
200
|
+
querySamples: Array<number>;
|
|
201
|
+
formulaSamples: Array<number>;
|
|
202
|
+
criteriaFilter: CriteriaFilter;
|
|
203
|
+
}): {
|
|
204
|
+
monitor: Monitor;
|
|
205
|
+
criteriaFilter: CriteriaFilter;
|
|
206
|
+
monitorStep: MonitorStep;
|
|
207
|
+
dataToProcess: MetricMonitorResponse;
|
|
208
|
+
} {
|
|
209
|
+
const queryConfig: MetricQueryConfigData = {
|
|
210
|
+
metricAliasData: {
|
|
211
|
+
metricVariable: "a",
|
|
212
|
+
title: "",
|
|
213
|
+
description: "",
|
|
214
|
+
legend: "",
|
|
215
|
+
legendUnit: input.queryUnit,
|
|
216
|
+
},
|
|
217
|
+
metricQueryData: {
|
|
218
|
+
filterData: {
|
|
219
|
+
metricName: "x",
|
|
220
|
+
},
|
|
221
|
+
} as unknown as MetricQueryData,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const formulaConfig: MetricFormulaConfigData = {
|
|
225
|
+
metricAliasData: {
|
|
226
|
+
metricVariable: "c",
|
|
227
|
+
title: "",
|
|
228
|
+
description: "",
|
|
229
|
+
legend: "",
|
|
230
|
+
legendUnit: input.formulaUnit,
|
|
231
|
+
},
|
|
232
|
+
metricFormulaData: {
|
|
233
|
+
metricFormula: "a",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const metricViewConfig: MetricsViewConfig = {
|
|
238
|
+
queryConfigs: [queryConfig],
|
|
239
|
+
formulaConfigs: [formulaConfig],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
243
|
+
monitorStep.data = {
|
|
244
|
+
id: ObjectID.generate().toString(),
|
|
245
|
+
monitorCriteria: { data: undefined } as never,
|
|
246
|
+
} as unknown as MonitorStep["data"];
|
|
247
|
+
monitorStep.data!.metricMonitor = {
|
|
248
|
+
metricViewConfig,
|
|
249
|
+
rollingTime: RollingTime.Past1Minute,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const queryResult: AggregatedResult = {
|
|
253
|
+
data: input.querySamples.map((v: number) => {
|
|
254
|
+
return { timestamp: new Date(), value: v } as AggregateModel;
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
const formulaResult: AggregatedResult = {
|
|
258
|
+
data: input.formulaSamples.map((v: number) => {
|
|
259
|
+
return { timestamp: new Date(), value: v } as AggregateModel;
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const dataToProcess: MetricMonitorResponse = {
|
|
264
|
+
projectId: ObjectID.generate(),
|
|
265
|
+
// index 0 => base query "a", index 1 => formula "c" (positional).
|
|
266
|
+
metricResult: [queryResult, formulaResult],
|
|
267
|
+
metricViewConfig,
|
|
268
|
+
monitorId: ObjectID.generate(),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
monitor: new Monitor(),
|
|
273
|
+
criteriaFilter: input.criteriaFilter,
|
|
274
|
+
monitorStep,
|
|
275
|
+
dataToProcess,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/*
|
|
280
|
+
* Convenience for a metric-value criteria filter with the common options.
|
|
281
|
+
*/
|
|
282
|
+
function metricFilter(options: {
|
|
283
|
+
filterType?: FilterType;
|
|
284
|
+
value?: string;
|
|
285
|
+
metricAlias?: string;
|
|
286
|
+
thresholdUnit?: string;
|
|
287
|
+
}): CriteriaFilter {
|
|
288
|
+
return {
|
|
289
|
+
checkOn: CheckOn.MetricValue,
|
|
290
|
+
filterType: options.filterType ?? FilterType.GreaterThan,
|
|
291
|
+
value: options.value ?? "1",
|
|
292
|
+
metricMonitorOptions: {
|
|
293
|
+
metricAlias: options.metricAlias ?? "a",
|
|
294
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
295
|
+
thresholdUnit: options.thresholdUnit,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
describe("MonitorCriteriaObservationBuilder.describeFilterObservation — metric value", () => {
|
|
301
|
+
/*
|
|
302
|
+
* Native/legend unit "sec": every rendered number carries " sec" and the
|
|
303
|
+
* three-point series produces the plural "(min .. max ..) across 3 data
|
|
304
|
+
* points" shape. "latest" is the LAST sample, not the max.
|
|
305
|
+
*/
|
|
306
|
+
test("native unit 'sec' — three equal samples render with the sec suffix", () => {
|
|
307
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "5" });
|
|
308
|
+
|
|
309
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
310
|
+
metricNativeUnit: "sec",
|
|
311
|
+
sampleValues: [0.06, 0.06, 0.06],
|
|
312
|
+
criteriaFilter,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const observation: string | null =
|
|
316
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
317
|
+
|
|
318
|
+
expect(observation).toBe(
|
|
319
|
+
"Metric Value (a) recorded latest 0.06 sec (min 0.06 sec, max 0.06 sec) across 3 data points.",
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
/*
|
|
324
|
+
* Legend unit "ms": two-point series. Each value is toFixed(2), so 40 -> 40.00
|
|
325
|
+
* and 60 -> 60.00; latest is 60 (the last element).
|
|
326
|
+
*/
|
|
327
|
+
test("legend unit 'ms' — two samples render latest/min/max with ms", () => {
|
|
328
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "10" });
|
|
329
|
+
|
|
330
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
331
|
+
metricNativeUnit: "ms",
|
|
332
|
+
sampleValues: [40, 60],
|
|
333
|
+
criteriaFilter,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const observation: string | null =
|
|
337
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
338
|
+
|
|
339
|
+
expect(observation).toBe(
|
|
340
|
+
"Metric Value (a) recorded latest 60.00 ms (min 40.00 ms, max 60.00 ms) across 2 data points.",
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/*
|
|
345
|
+
* thresholdUnit "sec" over a native "ms" metric: 2500 ms converts to 2.5 sec
|
|
346
|
+
* and the label follows the threshold unit. Single sample => singular
|
|
347
|
+
* "across 1 data point" with no (min .. max ..) clause.
|
|
348
|
+
*/
|
|
349
|
+
test("thresholdUnit 'sec' over native 'ms' — converts 2500ms to 2.50 sec", () => {
|
|
350
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
351
|
+
value: "2",
|
|
352
|
+
thresholdUnit: "sec",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
356
|
+
metricNativeUnit: "ms",
|
|
357
|
+
sampleValues: [2500],
|
|
358
|
+
criteriaFilter,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const observation: string | null =
|
|
362
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
363
|
+
|
|
364
|
+
expect(observation).toBe(
|
|
365
|
+
"Metric Value (a) recorded latest 2.50 sec across 1 data point.",
|
|
366
|
+
);
|
|
367
|
+
expect(observation).toContain("2.50 sec");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* thresholdUnit "GB" over a native "bytes" metric: 2.5e9 bytes converts to a
|
|
372
|
+
* clean 2.5 GB, rendered "2.50 GB".
|
|
373
|
+
*/
|
|
374
|
+
test("thresholdUnit 'GB' over native 'bytes' — converts 2.5e9 bytes to 2.50 GB", () => {
|
|
375
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
376
|
+
value: "2",
|
|
377
|
+
thresholdUnit: "GB",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
381
|
+
metricNativeUnit: "bytes",
|
|
382
|
+
sampleValues: [2.5e9],
|
|
383
|
+
criteriaFilter,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const observation: string | null =
|
|
387
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
388
|
+
|
|
389
|
+
expect(observation).toBe(
|
|
390
|
+
"Metric Value (a) recorded latest 2.50 GB across 1 data point.",
|
|
391
|
+
);
|
|
392
|
+
expect(observation).toContain("2.50 GB");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* Incompatible families (native "ms", thresholdUnit "MB"): conversion no-ops,
|
|
397
|
+
* so 5000 passes through UNCONVERTED as "5000.00" but is still LABELLED "MB".
|
|
398
|
+
*/
|
|
399
|
+
test("incompatible thresholdUnit 'MB' over native 'ms' — value unconverted, still labelled MB", () => {
|
|
400
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
401
|
+
value: "1000",
|
|
402
|
+
thresholdUnit: "MB",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
406
|
+
metricNativeUnit: "ms",
|
|
407
|
+
sampleValues: [5000],
|
|
408
|
+
criteriaFilter,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const observation: string | null =
|
|
412
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
413
|
+
|
|
414
|
+
expect(observation).toBe(
|
|
415
|
+
"Metric Value (a) recorded latest 5000.00 MB across 1 data point.",
|
|
416
|
+
);
|
|
417
|
+
// Unconverted value passed through.
|
|
418
|
+
expect(observation).toContain("5000.00");
|
|
419
|
+
// But labelled with the (incompatible) threshold unit.
|
|
420
|
+
expect(observation).toContain("MB");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
/*
|
|
424
|
+
* Dimensionless OTel unit "1" is suppressed: a fraction like 0.06 renders
|
|
425
|
+
* WITHOUT any suffix — never the misleading "0.06 1".
|
|
426
|
+
*/
|
|
427
|
+
test("dimensionless native '1' — no unit suffix", () => {
|
|
428
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "0.9" });
|
|
429
|
+
|
|
430
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
431
|
+
metricNativeUnit: "1",
|
|
432
|
+
sampleValues: [0.06],
|
|
433
|
+
criteriaFilter,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const observation: string | null =
|
|
437
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
438
|
+
|
|
439
|
+
expect(observation).toBe(
|
|
440
|
+
"Metric Value (a) recorded latest 0.06 across 1 data point.",
|
|
441
|
+
);
|
|
442
|
+
expect(observation).toContain("latest 0.06 ");
|
|
443
|
+
expect(observation).not.toContain("0.06 1");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
/*
|
|
447
|
+
* No known unit at all (empty legendUnit, no nativeUnitsByMetricName map):
|
|
448
|
+
* numbers render with no suffix.
|
|
449
|
+
*/
|
|
450
|
+
test("missing/empty native unit — no unit suffix", () => {
|
|
451
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "1" });
|
|
452
|
+
|
|
453
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
454
|
+
metricNativeUnit: "",
|
|
455
|
+
sampleValues: [0.06],
|
|
456
|
+
criteriaFilter,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const observation: string | null =
|
|
460
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
461
|
+
|
|
462
|
+
expect(observation).toBe(
|
|
463
|
+
"Metric Value (a) recorded latest 0.06 across 1 data point.",
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
/*
|
|
468
|
+
* nativeUnitsByMetricName supplies the unit when the alias has no legendUnit:
|
|
469
|
+
* the map value ("ms", keyed by the lowercased metric name) becomes the
|
|
470
|
+
* sample unit.
|
|
471
|
+
*/
|
|
472
|
+
test("native-unit fallback via nativeUnitsByMetricName when legendUnit empty", () => {
|
|
473
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "10" });
|
|
474
|
+
|
|
475
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
476
|
+
metricNativeUnit: "",
|
|
477
|
+
metricName: "http.server.duration",
|
|
478
|
+
sampleValues: [40, 60],
|
|
479
|
+
criteriaFilter,
|
|
480
|
+
nativeUnitsByMetricName: { "http.server.duration": "ms" },
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const observation: string | null =
|
|
484
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
485
|
+
|
|
486
|
+
expect(observation).toBe(
|
|
487
|
+
"Metric Value (a) recorded latest 60.00 ms (min 40.00 ms, max 60.00 ms) across 2 data points.",
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
/*
|
|
492
|
+
* A metricResult present but with an empty data array yields the
|
|
493
|
+
* "returned no data points." message (no unit, no numbers).
|
|
494
|
+
*/
|
|
495
|
+
test("no samples — returns the no-data-points message", () => {
|
|
496
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "1" });
|
|
497
|
+
|
|
498
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
499
|
+
metricNativeUnit: "sec",
|
|
500
|
+
sampleValues: [],
|
|
501
|
+
criteriaFilter,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const observation: string | null =
|
|
505
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
506
|
+
|
|
507
|
+
expect(observation).toBe("Metric Value (a) returned no data points.");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/*
|
|
511
|
+
* Alias resolution: with two queries (a: ms, b: sec) a criteria targeting "b"
|
|
512
|
+
* summarizes the "b" series with its "sec" unit — proving the right series and
|
|
513
|
+
* unit are chosen, not alias "a"'s ms values.
|
|
514
|
+
*/
|
|
515
|
+
test("alias resolution — criteria on 'b' summarizes the b series in sec", () => {
|
|
516
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
517
|
+
value: "1",
|
|
518
|
+
metricAlias: "b",
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const inputs: ReturnType<typeof buildTwoAliasInputs> = buildTwoAliasInputs({
|
|
522
|
+
aliasAUnit: "ms",
|
|
523
|
+
aliasBUnit: "sec",
|
|
524
|
+
aliasASamples: [40, 60],
|
|
525
|
+
aliasBSamples: [1, 2, 3],
|
|
526
|
+
criteriaFilter,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const observation: string | null =
|
|
530
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
531
|
+
|
|
532
|
+
expect(observation).toBe(
|
|
533
|
+
"Metric Value (b) recorded latest 3.00 sec (min 1.00 sec, max 3.00 sec) across 3 data points.",
|
|
534
|
+
);
|
|
535
|
+
// The a-series (ms, 40/60) must not leak in.
|
|
536
|
+
expect(observation).not.toContain("(a)");
|
|
537
|
+
expect(observation).not.toContain("ms");
|
|
538
|
+
expect(observation).not.toContain("60.00");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
/*
|
|
542
|
+
* Flagship fraction->percent: native/legend unit "1" (dimensionless) with a
|
|
543
|
+
* thresholdUnit of "%". The RAW "1" drives the numeric conversion (0.06 *
|
|
544
|
+
* 100 = 6) while the DISPLAY unit is the real "%" — never "0.06 1". The
|
|
545
|
+
* display unit resolved for this same criteria is "%".
|
|
546
|
+
*/
|
|
547
|
+
test("fraction->percent — native '1' + thresholdUnit '%' converts 0.06 to 6.00 %", () => {
|
|
548
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
549
|
+
value: "2",
|
|
550
|
+
thresholdUnit: "%",
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
554
|
+
metricNativeUnit: "1",
|
|
555
|
+
sampleValues: [0.06],
|
|
556
|
+
criteriaFilter,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const observation: string | null =
|
|
560
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
561
|
+
|
|
562
|
+
expect(observation).toBe(
|
|
563
|
+
"Metric Value (a) recorded latest 6.00 % across 1 data point.",
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const unit: string | undefined =
|
|
567
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
568
|
+
|
|
569
|
+
expect(unit).toBe("%");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
/*
|
|
573
|
+
* Reverse conversion with threshold-side "1" suppression: native/legend unit
|
|
574
|
+
* "%" with a thresholdUnit of "1". The RAW "1" drives the conversion (6 %
|
|
575
|
+
* -> 0.06 fraction) but the dimensionless "1" is SUPPRESSED for display, so
|
|
576
|
+
* the value renders with NO unit suffix. Display unit resolves to undefined.
|
|
577
|
+
*/
|
|
578
|
+
test("percent->fraction — native '%' + thresholdUnit '1' converts 6 to 0.06, no suffix", () => {
|
|
579
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
580
|
+
value: "0.5",
|
|
581
|
+
thresholdUnit: "1",
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
585
|
+
metricNativeUnit: "%",
|
|
586
|
+
sampleValues: [6],
|
|
587
|
+
criteriaFilter,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const observation: string | null =
|
|
591
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
592
|
+
|
|
593
|
+
expect(observation).toBe(
|
|
594
|
+
"Metric Value (a) recorded latest 0.06 across 1 data point.",
|
|
595
|
+
);
|
|
596
|
+
// The suppressed "1" must never leak in as a suffix.
|
|
597
|
+
expect(observation).not.toContain("0.06 1");
|
|
598
|
+
|
|
599
|
+
const unit: string | undefined =
|
|
600
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
601
|
+
|
|
602
|
+
expect(unit).toBeUndefined();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
* Formula alias resolution: the criteria targets the FORMULA alias "c",
|
|
607
|
+
* whose series lives at metricResult[1] (queryConfigs.length + formulaIndex).
|
|
608
|
+
* The observation must render "(c)" and use the FORMULA's legendUnit "sec",
|
|
609
|
+
* not the base query "a"'s "ms". No thresholdUnit, so values pass through
|
|
610
|
+
* unconverted (latest 2.50, min 1.50, max 2.50 over two points).
|
|
611
|
+
*/
|
|
612
|
+
test("formula alias 'c' — observation uses the formula's legendUnit 'sec'", () => {
|
|
613
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
614
|
+
value: "1",
|
|
615
|
+
metricAlias: "c",
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const inputs: ReturnType<typeof buildFormulaAliasInputs> =
|
|
619
|
+
buildFormulaAliasInputs({
|
|
620
|
+
queryUnit: "ms",
|
|
621
|
+
formulaUnit: "sec",
|
|
622
|
+
querySamples: [40, 60],
|
|
623
|
+
formulaSamples: [1.5, 2.5],
|
|
624
|
+
criteriaFilter,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const observation: string | null =
|
|
628
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
629
|
+
|
|
630
|
+
expect(observation).toBe(
|
|
631
|
+
"Metric Value (c) recorded latest 2.50 sec (min 1.50 sec, max 2.50 sec) across 2 data points.",
|
|
632
|
+
);
|
|
633
|
+
// The formula alias, not the base query, is summarized.
|
|
634
|
+
expect(observation).toContain("(c)");
|
|
635
|
+
expect(observation).not.toContain("(a)");
|
|
636
|
+
expect(observation).not.toContain("ms");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
/*
|
|
640
|
+
* Bit-units family: native/legend unit "mbit" converted into thresholdUnit
|
|
641
|
+
* "gbit" (mbit.toCanonical=1e6, gbit.toCanonical=1e9), so 1500 mbit -> 1.5
|
|
642
|
+
* gbit, labelled with the threshold unit.
|
|
643
|
+
*/
|
|
644
|
+
test("bit family — native 'mbit' + thresholdUnit 'gbit' converts 1500 to 1.50 gbit", () => {
|
|
645
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
646
|
+
value: "1",
|
|
647
|
+
thresholdUnit: "gbit",
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
651
|
+
metricNativeUnit: "mbit",
|
|
652
|
+
sampleValues: [1500],
|
|
653
|
+
criteriaFilter,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const observation: string | null =
|
|
657
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
658
|
+
|
|
659
|
+
expect(observation).toBe(
|
|
660
|
+
"Metric Value (a) recorded latest 1.50 gbit across 1 data point.",
|
|
661
|
+
);
|
|
662
|
+
expect(observation).toContain("1.50 gbit");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
/*
|
|
666
|
+
* Unknown sample unit (empty legendUnit and no nativeUnitsByMetricName) but a
|
|
667
|
+
* thresholdUnit "sec" is set. The conversion no-ops on the !sampleUnit guard
|
|
668
|
+
* so 5 passes through unconverted, yet the value is still LABELLED "sec"
|
|
669
|
+
* because the display unit follows the threshold unit.
|
|
670
|
+
*/
|
|
671
|
+
test("unknown sampleUnit + thresholdUnit 'sec' — value unconverted, still labelled sec", () => {
|
|
672
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
673
|
+
value: "1",
|
|
674
|
+
thresholdUnit: "sec",
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
678
|
+
metricNativeUnit: "",
|
|
679
|
+
sampleValues: [5],
|
|
680
|
+
criteriaFilter,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const observation: string | null =
|
|
684
|
+
MonitorCriteriaObservationBuilder.describeFilterObservation(inputs);
|
|
685
|
+
|
|
686
|
+
expect(observation).toBe(
|
|
687
|
+
"Metric Value (a) recorded latest 5.00 sec across 1 data point.",
|
|
688
|
+
);
|
|
689
|
+
// Unconverted value, but labelled with the threshold unit.
|
|
690
|
+
expect(observation).toContain("5.00 sec");
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe("MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit", () => {
|
|
695
|
+
/*
|
|
696
|
+
* Metric criteria, native "sec", no thresholdUnit => the native unit wins.
|
|
697
|
+
*/
|
|
698
|
+
test("native 'sec' with no thresholdUnit => 'sec'", () => {
|
|
699
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "5" });
|
|
700
|
+
|
|
701
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
702
|
+
metricNativeUnit: "sec",
|
|
703
|
+
sampleValues: [0.5],
|
|
704
|
+
criteriaFilter,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const unit: string | undefined =
|
|
708
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
709
|
+
|
|
710
|
+
expect(unit).toBe("sec");
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
/*
|
|
714
|
+
* thresholdUnit always wins over the native unit, even in a compatible family.
|
|
715
|
+
*/
|
|
716
|
+
test("thresholdUnit 'MB' over native 'ms' => 'MB' (threshold wins)", () => {
|
|
717
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
718
|
+
value: "1000",
|
|
719
|
+
thresholdUnit: "MB",
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
723
|
+
metricNativeUnit: "ms",
|
|
724
|
+
sampleValues: [5000],
|
|
725
|
+
criteriaFilter,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const unit: string | undefined =
|
|
729
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
730
|
+
|
|
731
|
+
expect(unit).toBe("MB");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
/*
|
|
735
|
+
* thresholdUnit "sec" wins even when it is convertible from native "ms".
|
|
736
|
+
*/
|
|
737
|
+
test("thresholdUnit 'sec' over convertible native 'ms' => 'sec'", () => {
|
|
738
|
+
const criteriaFilter: CriteriaFilter = metricFilter({
|
|
739
|
+
value: "2",
|
|
740
|
+
thresholdUnit: "sec",
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
744
|
+
metricNativeUnit: "ms",
|
|
745
|
+
sampleValues: [2500],
|
|
746
|
+
criteriaFilter,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const unit: string | undefined =
|
|
750
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
751
|
+
|
|
752
|
+
expect(unit).toBe("sec");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
/*
|
|
756
|
+
* Dimensionless native "1" resolves to undefined (suppressed), so callers can
|
|
757
|
+
* safely append it as a suffix without producing "0.06 1".
|
|
758
|
+
*/
|
|
759
|
+
test("dimensionless native '1' => undefined", () => {
|
|
760
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "0.9" });
|
|
761
|
+
|
|
762
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
763
|
+
metricNativeUnit: "1",
|
|
764
|
+
sampleValues: [0.06],
|
|
765
|
+
criteriaFilter,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const unit: string | undefined =
|
|
769
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
770
|
+
|
|
771
|
+
expect(unit).toBeUndefined();
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
/*
|
|
775
|
+
* Non-metric criteria (checkOn !== MetricValue) short-circuit to undefined.
|
|
776
|
+
*/
|
|
777
|
+
test("non-metric criteria (ResponseTime) => undefined", () => {
|
|
778
|
+
const criteriaFilter: CriteriaFilter = {
|
|
779
|
+
checkOn: CheckOn.ResponseTime,
|
|
780
|
+
filterType: FilterType.GreaterThan,
|
|
781
|
+
value: "500",
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
785
|
+
metricNativeUnit: "sec",
|
|
786
|
+
sampleValues: [0.5],
|
|
787
|
+
criteriaFilter,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const unit: string | undefined =
|
|
791
|
+
MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit(inputs);
|
|
792
|
+
|
|
793
|
+
expect(unit).toBeUndefined();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
/*
|
|
797
|
+
* A dataToProcess with no metricResult (empty object cast) must resolve to
|
|
798
|
+
* undefined without throwing.
|
|
799
|
+
*/
|
|
800
|
+
test("dataToProcess with no metricResult => undefined (no throw)", () => {
|
|
801
|
+
const criteriaFilter: CriteriaFilter = metricFilter({ value: "5" });
|
|
802
|
+
|
|
803
|
+
const base: ReturnType<typeof buildInputs> = buildInputs({
|
|
804
|
+
metricNativeUnit: "sec",
|
|
805
|
+
sampleValues: [0.5],
|
|
806
|
+
criteriaFilter,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
let unit: string | undefined;
|
|
810
|
+
const call: () => void = () => {
|
|
811
|
+
unit = MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit({
|
|
812
|
+
criteriaFilter,
|
|
813
|
+
monitorStep: base.monitorStep,
|
|
814
|
+
dataToProcess: {} as unknown as MetricMonitorResponse,
|
|
815
|
+
});
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
expect(call).not.toThrow();
|
|
819
|
+
expect(unit).toBeUndefined();
|
|
820
|
+
});
|
|
821
|
+
});
|