@oneuptime/common 10.0.71 → 10.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Alert.ts +55 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/StatusPage.ts +80 -0
- package/Server/API/StatusPageAPI.ts +4 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AnalyticsDatabaseService.ts +17 -7
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +91 -7
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
- package/Server/Utils/Monitor/MonitorIncident.ts +133 -8
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
- package/Server/Utils/Monitor/MonitorResource.ts +2 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
- package/Types/Infrastructure/BasicMetrics.ts +75 -0
- package/Types/Metrics/MetricQueryData.ts +11 -0
- package/Types/Monitor/CriteriaFilter.ts +10 -0
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
- package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
- package/Types/Monitor/MonitorMetricType.ts +34 -0
- package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
- package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
- package/Types/StatusPage/StatusPageLanguage.ts +29 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
- package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
- package/UI/Components/Charts/Line/LineChart.tsx +16 -11
- package/UI/Components/Filters/FiltersForm.tsx +26 -2
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
- package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
- package/Utils/Monitor/MonitorMetricType.ts +309 -19
- package/build/dist/Models/DatabaseModels/Alert.js +57 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +57 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +4 -0
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +66 -12
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +91 -15
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
- package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
- package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
- package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
- package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +6 -2
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
- package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
2
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
2
3
|
import { JSONObject } from "../../../Types/JSON";
|
|
3
4
|
import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
|
|
4
5
|
import IncomingMonitorRequest from "../../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
|
@@ -21,6 +22,7 @@ import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMo
|
|
|
21
22
|
import ExternalStatusPageMonitorResponse, {
|
|
22
23
|
ExternalStatusPageComponentStatus,
|
|
23
24
|
} from "../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
|
|
25
|
+
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
24
26
|
import Typeof from "../../../Types/Typeof";
|
|
25
27
|
import VMUtil from "../VM/VMAPI";
|
|
26
28
|
import DataToProcess from "./DataToProcess";
|
|
@@ -37,6 +39,21 @@ export default class MonitorTemplateUtil {
|
|
|
37
39
|
public static buildTemplateStorageMap(data: {
|
|
38
40
|
monitorType: MonitorType;
|
|
39
41
|
dataToProcess: DataToProcess;
|
|
42
|
+
/**
|
|
43
|
+
* The monitor that fired this criterion. Used to expose identity
|
|
44
|
+
* fields (`{{monitorName}}`, `{{monitorId}}`, etc.) to incident
|
|
45
|
+
* and alert title/description templates. Optional for backwards
|
|
46
|
+
* compatibility with existing callers.
|
|
47
|
+
*/
|
|
48
|
+
monitor?: Monitor | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* When set, the attribute values identifying the specific series
|
|
51
|
+
* this template is being rendered for. Each label is exposed to
|
|
52
|
+
* the template under its own key (so `{{host.name}}` works) and
|
|
53
|
+
* also collected under a `seriesLabels` object for iteration.
|
|
54
|
+
* Only populated when a metric monitor fires per-series.
|
|
55
|
+
*/
|
|
56
|
+
seriesLabels?: JSONObject | undefined;
|
|
40
57
|
}): JSONObject {
|
|
41
58
|
let storageMap: JSONObject = {};
|
|
42
59
|
|
|
@@ -315,6 +332,31 @@ export default class MonitorTemplateUtil {
|
|
|
315
332
|
} as JSONObject;
|
|
316
333
|
}
|
|
317
334
|
|
|
335
|
+
if (
|
|
336
|
+
data.monitorType === MonitorType.Metrics ||
|
|
337
|
+
data.monitorType === MonitorType.Kubernetes ||
|
|
338
|
+
data.monitorType === MonitorType.Docker
|
|
339
|
+
) {
|
|
340
|
+
const metricResponse: MetricMonitorResponse =
|
|
341
|
+
data.dataToProcess as MetricMonitorResponse;
|
|
342
|
+
|
|
343
|
+
const queryConfigs: Array<unknown> =
|
|
344
|
+
metricResponse.metricViewConfig?.queryConfigs || [];
|
|
345
|
+
|
|
346
|
+
const firstQuery: unknown = queryConfigs[0];
|
|
347
|
+
const metricName: string | undefined = (
|
|
348
|
+
firstQuery as
|
|
349
|
+
| {
|
|
350
|
+
metricQueryData?: { filterData?: { metricName?: string } };
|
|
351
|
+
}
|
|
352
|
+
| undefined
|
|
353
|
+
)?.metricQueryData?.filterData?.metricName;
|
|
354
|
+
|
|
355
|
+
storageMap = {
|
|
356
|
+
metricName: metricName || "",
|
|
357
|
+
} as JSONObject;
|
|
358
|
+
}
|
|
359
|
+
|
|
318
360
|
if (data.monitorType === MonitorType.ExternalStatusPage) {
|
|
319
361
|
const externalStatusPageResponse:
|
|
320
362
|
| ExternalStatusPageMonitorResponse
|
|
@@ -347,6 +389,63 @@ export default class MonitorTemplateUtil {
|
|
|
347
389
|
logger.error(err);
|
|
348
390
|
}
|
|
349
391
|
|
|
392
|
+
/*
|
|
393
|
+
* Fold series labels onto the storage map so templates like
|
|
394
|
+
* `{{host.name}}` or `{{resource.k8s.container.name}}` resolve at
|
|
395
|
+
* render time. The template engine walks dotted paths as nested
|
|
396
|
+
* property access (`host` → `.name`), so for each dotted label
|
|
397
|
+
* key we build up a nested object rather than storing the flat
|
|
398
|
+
* key. Also expose the full label map under `seriesLabels` for
|
|
399
|
+
* iteration-style templates.
|
|
400
|
+
*/
|
|
401
|
+
if (data.seriesLabels && Object.keys(data.seriesLabels).length > 0) {
|
|
402
|
+
for (const key of Object.keys(data.seriesLabels)) {
|
|
403
|
+
const value: unknown = data.seriesLabels[key];
|
|
404
|
+
if (value === undefined || value === null) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const parts: Array<string> = key.split(".");
|
|
408
|
+
let cursor: JSONObject = storageMap;
|
|
409
|
+
for (let i: number = 0; i < parts.length - 1; i++) {
|
|
410
|
+
const part: string = parts[i]!;
|
|
411
|
+
const existing: unknown = cursor[part];
|
|
412
|
+
if (
|
|
413
|
+
!existing ||
|
|
414
|
+
typeof existing !== "object" ||
|
|
415
|
+
Array.isArray(existing)
|
|
416
|
+
) {
|
|
417
|
+
cursor[part] = {};
|
|
418
|
+
}
|
|
419
|
+
cursor = cursor[part] as JSONObject;
|
|
420
|
+
}
|
|
421
|
+
cursor[parts[parts.length - 1]!] = value as JSONObject[string];
|
|
422
|
+
}
|
|
423
|
+
storageMap["seriesLabels"] = data.seriesLabels;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/*
|
|
427
|
+
* Monitor identity fields. Always exposed (when a monitor is provided),
|
|
428
|
+
* independent of monitorType, so templates like `{{monitorName}}` work
|
|
429
|
+
* uniformly across Server/VM, Probe, Synthetic, Metric monitors, etc.
|
|
430
|
+
*/
|
|
431
|
+
if (data.monitor) {
|
|
432
|
+
if (data.monitor.name) {
|
|
433
|
+
storageMap["monitorName"] = data.monitor.name;
|
|
434
|
+
}
|
|
435
|
+
if (data.monitor.id) {
|
|
436
|
+
storageMap["monitorId"] = data.monitor.id.toString();
|
|
437
|
+
}
|
|
438
|
+
if (data.monitor.description) {
|
|
439
|
+
storageMap["monitorDescription"] = data.monitor.description;
|
|
440
|
+
}
|
|
441
|
+
if (data.monitor.slug) {
|
|
442
|
+
storageMap["monitorSlug"] = data.monitor.slug;
|
|
443
|
+
}
|
|
444
|
+
if (data.monitor.monitorType) {
|
|
445
|
+
storageMap["monitorType"] = data.monitor.monitorType;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
350
449
|
logger.debug(`Storage Map: ${JSON.stringify(storageMap, null, 2)}`);
|
|
351
450
|
|
|
352
451
|
return storageMap;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MetricMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
|
|
2
|
+
import MetricSeriesFingerprint from "../../../../../Utils/Metrics/MetricSeriesFingerprint";
|
|
2
3
|
import AggregateModel from "../../../../../Types/BaseDatabase/AggregatedModel";
|
|
3
4
|
import AggregatedResult from "../../../../../Types/BaseDatabase/AggregatedResult";
|
|
4
5
|
import {
|
|
@@ -500,3 +501,270 @@ describe("MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
|
|
|
500
501
|
expect(message).not.toContain("110, 120, 130, 140, 150, 160");
|
|
501
502
|
});
|
|
502
503
|
});
|
|
504
|
+
|
|
505
|
+
describe("MetricMonitorCriteria.evaluateAllSeries — per-host alerting", () => {
|
|
506
|
+
/*
|
|
507
|
+
* Build a fixture whose metric response carries a pre-computed
|
|
508
|
+
* seriesBreakdown with two hosts over threshold and one under. This
|
|
509
|
+
* mirrors what MonitorTelemetryMonitor produces when groupByAttributeKeys
|
|
510
|
+
* is set.
|
|
511
|
+
*/
|
|
512
|
+
function buildInputsWithSeriesBreakdown(input: {
|
|
513
|
+
criteriaFilter: CriteriaFilter;
|
|
514
|
+
seriesSamples: Array<{
|
|
515
|
+
labels: Record<string, string>;
|
|
516
|
+
values: Array<number>;
|
|
517
|
+
}>;
|
|
518
|
+
}): {
|
|
519
|
+
criteriaFilter: CriteriaFilter;
|
|
520
|
+
monitorStep: MonitorStep;
|
|
521
|
+
dataToProcess: MetricMonitorResponse;
|
|
522
|
+
} {
|
|
523
|
+
const aliasData: MetricAliasData = {
|
|
524
|
+
metricVariable: "a",
|
|
525
|
+
title: "CPU",
|
|
526
|
+
description: undefined,
|
|
527
|
+
legend: undefined,
|
|
528
|
+
legendUnit: "%",
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const queryConfig: MetricQueryConfigData = {
|
|
532
|
+
metricAliasData: aliasData,
|
|
533
|
+
metricQueryData: {
|
|
534
|
+
filterData: {
|
|
535
|
+
metricName: "cpu.usage",
|
|
536
|
+
},
|
|
537
|
+
groupByAttributeKeys: ["host.name"],
|
|
538
|
+
} as unknown as MetricQueryData,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const metricViewConfig: MetricsViewConfig = {
|
|
542
|
+
queryConfigs: [queryConfig],
|
|
543
|
+
formulaConfigs: [],
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const monitorStep: MonitorStep = new MonitorStep();
|
|
547
|
+
monitorStep.data = {
|
|
548
|
+
id: ObjectID.generate().toString(),
|
|
549
|
+
monitorCriteria: { data: undefined } as never,
|
|
550
|
+
} as unknown as MonitorStep["data"];
|
|
551
|
+
monitorStep.data!.metricMonitor = {
|
|
552
|
+
metricViewConfig,
|
|
553
|
+
rollingTime: RollingTime.Past1Minute,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const seriesBreakdown: Array<{
|
|
557
|
+
fingerprint: string;
|
|
558
|
+
labels: Record<string, string>;
|
|
559
|
+
aggregatedResults: Array<AggregatedResult>;
|
|
560
|
+
}> = input.seriesSamples.map(
|
|
561
|
+
(s: { labels: Record<string, string>; values: Array<number> }) => {
|
|
562
|
+
return {
|
|
563
|
+
fingerprint: Object.values(s.labels).join("|"),
|
|
564
|
+
labels: s.labels,
|
|
565
|
+
aggregatedResults: [
|
|
566
|
+
{
|
|
567
|
+
data: s.values.map((v: number) => {
|
|
568
|
+
return {
|
|
569
|
+
timestamp: new Date(),
|
|
570
|
+
value: v,
|
|
571
|
+
attributes: s.labels,
|
|
572
|
+
} as unknown as AggregateModel;
|
|
573
|
+
}),
|
|
574
|
+
} as AggregatedResult,
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// Flat metricResult = union of all series samples, mirroring the worker.
|
|
581
|
+
const flatData: Array<AggregateModel> = input.seriesSamples.flatMap(
|
|
582
|
+
(s: { labels: Record<string, string>; values: Array<number> }) => {
|
|
583
|
+
return s.values.map((v: number) => {
|
|
584
|
+
return {
|
|
585
|
+
timestamp: new Date(),
|
|
586
|
+
value: v,
|
|
587
|
+
attributes: s.labels,
|
|
588
|
+
} as unknown as AggregateModel;
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const dataToProcess: MetricMonitorResponse = {
|
|
594
|
+
projectId: ObjectID.generate(),
|
|
595
|
+
metricResult: [{ data: flatData } as AggregatedResult],
|
|
596
|
+
metricViewConfig,
|
|
597
|
+
monitorId: ObjectID.generate(),
|
|
598
|
+
seriesBreakdown:
|
|
599
|
+
seriesBreakdown as unknown as MetricMonitorResponse["seriesBreakdown"],
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
criteriaFilter: input.criteriaFilter,
|
|
604
|
+
monitorStep,
|
|
605
|
+
dataToProcess,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
test("no seriesBreakdown → single synthetic evaluation (backward compatible)", () => {
|
|
610
|
+
const criteriaFilter: CriteriaFilter = {
|
|
611
|
+
checkOn: CheckOn.MetricValue,
|
|
612
|
+
filterType: FilterType.GreaterThan,
|
|
613
|
+
value: "80",
|
|
614
|
+
metricMonitorOptions: {
|
|
615
|
+
metricAlias: "a",
|
|
616
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const inputs: ReturnType<typeof buildInputs> = buildInputs({
|
|
621
|
+
metricNativeUnit: "%",
|
|
622
|
+
sampleValues: [95],
|
|
623
|
+
criteriaFilter,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const results: ReturnType<typeof MetricMonitorCriteria.evaluateAllSeries> =
|
|
627
|
+
MetricMonitorCriteria.evaluateAllSeries(inputs);
|
|
628
|
+
expect(results).toHaveLength(1);
|
|
629
|
+
expect(results[0]!.fingerprint).toBeUndefined();
|
|
630
|
+
expect(results[0]!.rootCause).toBeTruthy();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("seriesBreakdown with 2 breaching + 1 non-breaching → only breaching series return rootCause", () => {
|
|
634
|
+
const criteriaFilter: CriteriaFilter = {
|
|
635
|
+
checkOn: CheckOn.MetricValue,
|
|
636
|
+
filterType: FilterType.GreaterThan,
|
|
637
|
+
value: "80",
|
|
638
|
+
metricMonitorOptions: {
|
|
639
|
+
metricAlias: "a",
|
|
640
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const inputs: ReturnType<typeof buildInputsWithSeriesBreakdown> =
|
|
645
|
+
buildInputsWithSeriesBreakdown({
|
|
646
|
+
criteriaFilter,
|
|
647
|
+
seriesSamples: [
|
|
648
|
+
{ labels: { "host.name": "prod-01" }, values: [95] },
|
|
649
|
+
{ labels: { "host.name": "prod-02" }, values: [92] },
|
|
650
|
+
{ labels: { "host.name": "prod-03" }, values: [50] },
|
|
651
|
+
],
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const results: ReturnType<typeof MetricMonitorCriteria.evaluateAllSeries> =
|
|
655
|
+
MetricMonitorCriteria.evaluateAllSeries(inputs);
|
|
656
|
+
expect(results).toHaveLength(3);
|
|
657
|
+
|
|
658
|
+
const breaching: typeof results = results.filter(
|
|
659
|
+
(r: (typeof results)[number]) => {
|
|
660
|
+
return r.rootCause !== null;
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
expect(breaching).toHaveLength(2);
|
|
664
|
+
|
|
665
|
+
const breachingHosts: Array<string> = breaching
|
|
666
|
+
.map((r: (typeof results)[number]) => {
|
|
667
|
+
return r.labels["host.name"] as string;
|
|
668
|
+
})
|
|
669
|
+
.sort();
|
|
670
|
+
expect(breachingHosts).toEqual(["prod-01", "prod-02"]);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("each series gets its own breaching-samples context", () => {
|
|
674
|
+
const criteriaFilter: CriteriaFilter = {
|
|
675
|
+
checkOn: CheckOn.MetricValue,
|
|
676
|
+
filterType: FilterType.GreaterThan,
|
|
677
|
+
value: "80",
|
|
678
|
+
metricMonitorOptions: {
|
|
679
|
+
metricAlias: "a",
|
|
680
|
+
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const inputs: ReturnType<typeof buildInputsWithSeriesBreakdown> =
|
|
685
|
+
buildInputsWithSeriesBreakdown({
|
|
686
|
+
criteriaFilter,
|
|
687
|
+
seriesSamples: [
|
|
688
|
+
{ labels: { "host.name": "prod-01" }, values: [95, 97] },
|
|
689
|
+
{ labels: { "host.name": "prod-02" }, values: [92] },
|
|
690
|
+
],
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const results: ReturnType<typeof MetricMonitorCriteria.evaluateAllSeries> =
|
|
694
|
+
MetricMonitorCriteria.evaluateAllSeries(inputs);
|
|
695
|
+
const prod01: (typeof results)[number] = results.find(
|
|
696
|
+
(r: (typeof results)[number]) => {
|
|
697
|
+
return r.labels["host.name"] === "prod-01";
|
|
698
|
+
},
|
|
699
|
+
)!;
|
|
700
|
+
const prod02: (typeof results)[number] = results.find(
|
|
701
|
+
(r: (typeof results)[number]) => {
|
|
702
|
+
return r.labels["host.name"] === "prod-02";
|
|
703
|
+
},
|
|
704
|
+
)!;
|
|
705
|
+
|
|
706
|
+
expect(prod01.context.breachingSamples).toHaveLength(2);
|
|
707
|
+
expect(prod02.context.breachingSamples).toHaveLength(1);
|
|
708
|
+
expect(prod01.context.seriesLabels).toEqual({ "host.name": "prod-01" });
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("MetricSeriesFingerprint", () => {
|
|
713
|
+
test("fingerprint is stable regardless of key order", () => {
|
|
714
|
+
const a: { [k: string]: string } = {
|
|
715
|
+
"host.name": "prod-01",
|
|
716
|
+
region: "us-east",
|
|
717
|
+
};
|
|
718
|
+
const b: { [k: string]: string } = {
|
|
719
|
+
region: "us-east",
|
|
720
|
+
"host.name": "prod-01",
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
expect(MetricSeriesFingerprint.computeFingerprint(a)).toBe(
|
|
724
|
+
MetricSeriesFingerprint.computeFingerprint(b),
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("different label values → different fingerprints", () => {
|
|
729
|
+
expect(
|
|
730
|
+
MetricSeriesFingerprint.computeFingerprint({ "host.name": "prod-01" }),
|
|
731
|
+
).not.toBe(
|
|
732
|
+
MetricSeriesFingerprint.computeFingerprint({ "host.name": "prod-02" }),
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("empty labels → sentinel WholeMonitorFingerprint", () => {
|
|
737
|
+
expect(MetricSeriesFingerprint.computeFingerprint({})).toBe(
|
|
738
|
+
MetricSeriesFingerprint.WholeMonitorFingerprint,
|
|
739
|
+
);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("missing attribute keys canonicalize to empty string (stable fingerprint)", () => {
|
|
743
|
+
const sample1: AggregateModel = {
|
|
744
|
+
timestamp: new Date(),
|
|
745
|
+
value: 42,
|
|
746
|
+
attributes: { "host.name": "prod-01" },
|
|
747
|
+
} as unknown as AggregateModel;
|
|
748
|
+
const sample2: AggregateModel = {
|
|
749
|
+
timestamp: new Date(),
|
|
750
|
+
value: 42,
|
|
751
|
+
attributes: { "host.name": "prod-01", region: "us-east" },
|
|
752
|
+
} as unknown as AggregateModel;
|
|
753
|
+
|
|
754
|
+
const labels1: import("../../../../../Types/JSON").JSONObject =
|
|
755
|
+
MetricSeriesFingerprint.extractSeriesLabels({
|
|
756
|
+
sample: sample1,
|
|
757
|
+
attributeKeys: ["host.name"],
|
|
758
|
+
});
|
|
759
|
+
const labels2: import("../../../../../Types/JSON").JSONObject =
|
|
760
|
+
MetricSeriesFingerprint.extractSeriesLabels({
|
|
761
|
+
sample: sample2,
|
|
762
|
+
attributeKeys: ["host.name"],
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// When only host.name is selected, region doesn't affect the fingerprint
|
|
766
|
+
expect(MetricSeriesFingerprint.computeFingerprint(labels1)).toBe(
|
|
767
|
+
MetricSeriesFingerprint.computeFingerprint(labels2),
|
|
768
|
+
);
|
|
769
|
+
});
|
|
770
|
+
});
|
|
@@ -4,11 +4,30 @@ export interface MemoryMetrics {
|
|
|
4
4
|
used: number;
|
|
5
5
|
percentUsed: number;
|
|
6
6
|
percentFree: number;
|
|
7
|
+
|
|
8
|
+
available?: number | undefined;
|
|
9
|
+
buffers?: number | undefined;
|
|
10
|
+
cached?: number | undefined;
|
|
11
|
+
|
|
12
|
+
swapTotal?: number | undefined;
|
|
13
|
+
swapUsed?: number | undefined;
|
|
14
|
+
swapFree?: number | undefined;
|
|
15
|
+
swapPercentUsed?: number | undefined;
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
export interface CPUMetrics {
|
|
10
19
|
percentUsed: number;
|
|
11
20
|
cores: number;
|
|
21
|
+
|
|
22
|
+
perCorePercent?: Array<number> | undefined;
|
|
23
|
+
timeUserPercent?: number | undefined;
|
|
24
|
+
timeSystemPercent?: number | undefined;
|
|
25
|
+
timeIdlePercent?: number | undefined;
|
|
26
|
+
timeIoWaitPercent?: number | undefined;
|
|
27
|
+
timeStealPercent?: number | undefined;
|
|
28
|
+
timeNicePercent?: number | undefined;
|
|
29
|
+
timeIrqPercent?: number | undefined;
|
|
30
|
+
timeSoftIrqPercent?: number | undefined;
|
|
12
31
|
}
|
|
13
32
|
|
|
14
33
|
export interface BasicDiskMetrics {
|
|
@@ -18,10 +37,66 @@ export interface BasicDiskMetrics {
|
|
|
18
37
|
diskPath: string;
|
|
19
38
|
percentUsed: number;
|
|
20
39
|
percentFree: number;
|
|
40
|
+
|
|
41
|
+
device?: string | undefined;
|
|
42
|
+
fstype?: string | undefined;
|
|
43
|
+
readBytes?: number | undefined;
|
|
44
|
+
writeBytes?: number | undefined;
|
|
45
|
+
readCount?: number | undefined;
|
|
46
|
+
writeCount?: number | undefined;
|
|
47
|
+
ioTimeMs?: number | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NetworkInterfaceMetrics {
|
|
51
|
+
interfaceName: string;
|
|
52
|
+
bytesReceived: number;
|
|
53
|
+
bytesSent: number;
|
|
54
|
+
packetsReceived: number;
|
|
55
|
+
packetsSent: number;
|
|
56
|
+
errorsIn: number;
|
|
57
|
+
errorsOut: number;
|
|
58
|
+
dropsIn: number;
|
|
59
|
+
dropsOut: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface NetworkMetrics {
|
|
63
|
+
interfaces?: Array<NetworkInterfaceMetrics> | undefined;
|
|
64
|
+
totalBytesReceived?: number | undefined;
|
|
65
|
+
totalBytesSent?: number | undefined;
|
|
66
|
+
totalPacketsReceived?: number | undefined;
|
|
67
|
+
totalPacketsSent?: number | undefined;
|
|
68
|
+
connectionsEstablished?: number | undefined;
|
|
69
|
+
connectionsListen?: number | undefined;
|
|
70
|
+
connectionsTotal?: number | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LoadMetrics {
|
|
74
|
+
load1: number;
|
|
75
|
+
load5: number;
|
|
76
|
+
load15: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface HostMetrics {
|
|
80
|
+
platform?: string | undefined;
|
|
81
|
+
platformFamily?: string | undefined;
|
|
82
|
+
platformVersion?: string | undefined;
|
|
83
|
+
kernelVersion?: string | undefined;
|
|
84
|
+
kernelArch?: string | undefined;
|
|
85
|
+
os?: string | undefined;
|
|
86
|
+
uptimeSeconds?: number | undefined;
|
|
87
|
+
bootTime?: number | undefined;
|
|
88
|
+
hostId?: string | undefined;
|
|
89
|
+
virtualizationSystem?: string | undefined;
|
|
90
|
+
virtualizationRole?: string | undefined;
|
|
91
|
+
numProcesses?: number | undefined;
|
|
21
92
|
}
|
|
22
93
|
|
|
23
94
|
export default interface BasicInfrastructureMetrics {
|
|
24
95
|
cpuMetrics: CPUMetrics;
|
|
25
96
|
memoryMetrics: MemoryMetrics;
|
|
26
97
|
diskMetrics: Array<BasicDiskMetrics>;
|
|
98
|
+
|
|
99
|
+
networkMetrics?: NetworkMetrics | undefined;
|
|
100
|
+
loadMetrics?: LoadMetrics | undefined;
|
|
101
|
+
hostMetrics?: HostMetrics | undefined;
|
|
27
102
|
}
|
|
@@ -6,4 +6,15 @@ import MetricsQuery from "./MetricsQuery";
|
|
|
6
6
|
export default interface MetricQueryData {
|
|
7
7
|
filterData: FilterData<MetricsQuery>;
|
|
8
8
|
groupBy?: GroupBy<Metric> | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* OpenTelemetry attribute keys (e.g. "host.name", "service.name") to
|
|
11
|
+
* group this query by. Stored alongside groupBy because attributes
|
|
12
|
+
* live inside a nested Map column and can't be expressed through
|
|
13
|
+
* GroupBy<Metric>, which only references top-level columns.
|
|
14
|
+
*
|
|
15
|
+
* When set, the monitor worker emits one series per unique value
|
|
16
|
+
* combination of these keys — enabling per-host (or per-service, per-
|
|
17
|
+
* whatever) incident creation from a single metric monitor.
|
|
18
|
+
*/
|
|
19
|
+
groupByAttributeKeys?: Array<string> | undefined;
|
|
9
20
|
}
|
|
@@ -19,6 +19,11 @@ export enum CheckOn {
|
|
|
19
19
|
DiskUsagePercent = "Disk Usage (in %)",
|
|
20
20
|
CPUUsagePercent = "CPU Usage (in %)",
|
|
21
21
|
MemoryUsagePercent = "Memory Usage (in %)",
|
|
22
|
+
LoadAverage1Min = "Load Average (1 minute)",
|
|
23
|
+
LoadAverage5Min = "Load Average (5 minute)",
|
|
24
|
+
LoadAverage15Min = "Load Average (15 minute)",
|
|
25
|
+
SwapUsagePercent = "Swap Usage (in %)",
|
|
26
|
+
CPUIoWaitPercent = "CPU IO Wait (in %)",
|
|
22
27
|
ExpiresInHours = "Expires In Hours",
|
|
23
28
|
ExpiresInDays = "Expires In Days",
|
|
24
29
|
IsSelfSignedCertificate = "Is Self Signed Certificate",
|
|
@@ -296,6 +301,11 @@ export class CriteriaFilterUtil {
|
|
|
296
301
|
checkOn === CheckOn.DiskUsagePercent ||
|
|
297
302
|
checkOn === CheckOn.CPUUsagePercent ||
|
|
298
303
|
checkOn === CheckOn.MemoryUsagePercent ||
|
|
304
|
+
checkOn === CheckOn.LoadAverage1Min ||
|
|
305
|
+
checkOn === CheckOn.LoadAverage5Min ||
|
|
306
|
+
checkOn === CheckOn.LoadAverage15Min ||
|
|
307
|
+
checkOn === CheckOn.SwapUsagePercent ||
|
|
308
|
+
checkOn === CheckOn.CPUIoWaitPercent ||
|
|
299
309
|
checkOn === CheckOn.IsOnline ||
|
|
300
310
|
checkOn === CheckOn.SnmpResponseTime ||
|
|
301
311
|
checkOn === CheckOn.SnmpIsOnline ||
|
|
@@ -47,6 +47,17 @@ export default interface MetricCriteriaContext {
|
|
|
47
47
|
filterAttributes: JSONObject;
|
|
48
48
|
groupBy: Array<string>;
|
|
49
49
|
timeWindowMinutes?: number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Fingerprint of the specific series this context represents when the
|
|
52
|
+
* monitor is configured for per-series alerting. Undefined for
|
|
53
|
+
* traditional whole-monitor evaluation.
|
|
54
|
+
*/
|
|
55
|
+
seriesFingerprint?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Label values identifying the series (e.g. {host.name: prod-01}).
|
|
58
|
+
* Populated alongside seriesFingerprint.
|
|
59
|
+
*/
|
|
60
|
+
seriesLabels?: JSONObject | undefined;
|
|
50
61
|
breachingSample?: MetricBreachingSample | undefined;
|
|
51
62
|
/**
|
|
52
63
|
* All samples in the evaluation window that breached the threshold,
|
|
@@ -4,6 +4,7 @@ import MonitorEvaluationSummary from "../MonitorEvaluationSummary";
|
|
|
4
4
|
import MetricsViewConfig from "../../Metrics/MetricsViewConfig";
|
|
5
5
|
import ObjectID from "../../ObjectID";
|
|
6
6
|
import Dictionary from "../../Dictionary";
|
|
7
|
+
import MetricSeriesResult from "./MetricSeriesResult";
|
|
7
8
|
|
|
8
9
|
export interface KubernetesAffectedResource {
|
|
9
10
|
podName?: string | undefined;
|
|
@@ -31,4 +32,13 @@ export default interface MetricMonitorResponse {
|
|
|
31
32
|
monitorId: ObjectID;
|
|
32
33
|
evaluationSummary?: MonitorEvaluationSummary | undefined;
|
|
33
34
|
kubernetesResourceBreakdown?: KubernetesResourceBreakdown | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Per-series breakdown when any queryConfig sets groupByAttributeKeys.
|
|
37
|
+
* Each entry carries a fingerprint, the label values identifying that
|
|
38
|
+
* series, and the aggregated-per-query results scoped to that series
|
|
39
|
+
* (including per-series formula results). Absent for traditional
|
|
40
|
+
* whole-monitor evaluation, in which case criteria evaluate against
|
|
41
|
+
* `metricResult` as before.
|
|
42
|
+
*/
|
|
43
|
+
seriesBreakdown?: Array<MetricSeriesResult> | undefined;
|
|
34
44
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import AggregatedResult from "../../BaseDatabase/AggregatedResult";
|
|
2
|
+
import { JSONObject } from "../../JSON";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One series within a metric monitor evaluation. When a metric monitor
|
|
6
|
+
* is configured with group-by attributes (e.g. host.name), the worker
|
|
7
|
+
* splits the aggregated results into one entry per unique label
|
|
8
|
+
* combination. Each series is then evaluated independently so the
|
|
9
|
+
* criteria can fire one incident per affected series.
|
|
10
|
+
*
|
|
11
|
+
* `aggregatedResults` is aligned with the monitor's queryConfigs +
|
|
12
|
+
* formulaConfigs arrays (same length, same order) so per-series
|
|
13
|
+
* formula evaluation reuses the same indexing the criteria evaluator
|
|
14
|
+
* expects.
|
|
15
|
+
*/
|
|
16
|
+
export default interface MetricSeriesResult {
|
|
17
|
+
fingerprint: string;
|
|
18
|
+
labels: JSONObject;
|
|
19
|
+
aggregatedResults: Array<AggregatedResult>;
|
|
20
|
+
}
|
|
@@ -6,6 +6,40 @@ enum MonitorMetricType {
|
|
|
6
6
|
MemoryUsagePercent = "oneuptime.monitor.memory.usage.percent",
|
|
7
7
|
IsOnline = "oneuptime.monitor.online",
|
|
8
8
|
ExecutionTime = "oneuptime.monitor.execution.time",
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Extended server/VM metrics. Emitted when the agent payload contains them;
|
|
12
|
+
* absent for older agents, which keeps the pipeline backwards-compatible.
|
|
13
|
+
*/
|
|
14
|
+
LoadAverage1Min = "oneuptime.monitor.load.avg.1min",
|
|
15
|
+
LoadAverage5Min = "oneuptime.monitor.load.avg.5min",
|
|
16
|
+
LoadAverage15Min = "oneuptime.monitor.load.avg.15min",
|
|
17
|
+
|
|
18
|
+
SwapUsagePercent = "oneuptime.monitor.memory.swap.usage.percent",
|
|
19
|
+
MemoryAvailableBytes = "oneuptime.monitor.memory.available.bytes",
|
|
20
|
+
|
|
21
|
+
CPUTimeUserPercent = "oneuptime.monitor.cpu.time.user.percent",
|
|
22
|
+
CPUTimeSystemPercent = "oneuptime.monitor.cpu.time.system.percent",
|
|
23
|
+
CPUTimeIoWaitPercent = "oneuptime.monitor.cpu.time.iowait.percent",
|
|
24
|
+
CPUTimeIdlePercent = "oneuptime.monitor.cpu.time.idle.percent",
|
|
25
|
+
CPUTimeStealPercent = "oneuptime.monitor.cpu.time.steal.percent",
|
|
26
|
+
|
|
27
|
+
DiskReadBytesTotal = "oneuptime.monitor.disk.io.read.bytes.total",
|
|
28
|
+
DiskWriteBytesTotal = "oneuptime.monitor.disk.io.write.bytes.total",
|
|
29
|
+
DiskReadOpsTotal = "oneuptime.monitor.disk.io.read.ops.total",
|
|
30
|
+
DiskWriteOpsTotal = "oneuptime.monitor.disk.io.write.ops.total",
|
|
31
|
+
|
|
32
|
+
NetworkBytesReceivedTotal = "oneuptime.monitor.network.bytes.received.total",
|
|
33
|
+
NetworkBytesSentTotal = "oneuptime.monitor.network.bytes.sent.total",
|
|
34
|
+
NetworkPacketsReceivedTotal = "oneuptime.monitor.network.packets.received.total",
|
|
35
|
+
NetworkPacketsSentTotal = "oneuptime.monitor.network.packets.sent.total",
|
|
36
|
+
NetworkErrorsIn = "oneuptime.monitor.network.errors.in",
|
|
37
|
+
NetworkErrorsOut = "oneuptime.monitor.network.errors.out",
|
|
38
|
+
NetworkConnectionsEstablished = "oneuptime.monitor.network.connections.established",
|
|
39
|
+
NetworkConnectionsListen = "oneuptime.monitor.network.connections.listen",
|
|
40
|
+
|
|
41
|
+
HostUptimeSeconds = "oneuptime.monitor.host.uptime.seconds",
|
|
42
|
+
ProcessCountTotal = "oneuptime.monitor.process.count.total",
|
|
9
43
|
}
|
|
10
44
|
|
|
11
45
|
export default MonitorMetricType;
|
|
@@ -6,6 +6,14 @@ export interface ServerProcess {
|
|
|
6
6
|
pid: number;
|
|
7
7
|
name: string;
|
|
8
8
|
command: string;
|
|
9
|
+
|
|
10
|
+
cpuPercent?: number | undefined;
|
|
11
|
+
memoryBytes?: number | undefined;
|
|
12
|
+
memoryPercent?: number | undefined;
|
|
13
|
+
status?: string | undefined;
|
|
14
|
+
threads?: number | undefined;
|
|
15
|
+
createTimeMs?: number | undefined;
|
|
16
|
+
username?: string | undefined;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export default interface ServerMonitorResponse {
|