@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.
Files changed (97) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/Incident.ts +55 -0
  3. package/Models/DatabaseModels/StatusPage.ts +80 -0
  4. package/Server/API/StatusPageAPI.ts +4 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Services/AnalyticsDatabaseService.ts +17 -7
  9. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
  10. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
  11. package/Server/Utils/Monitor/MonitorAlert.ts +91 -7
  12. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
  13. package/Server/Utils/Monitor/MonitorIncident.ts +133 -8
  14. package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
  15. package/Server/Utils/Monitor/MonitorResource.ts +2 -0
  16. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
  17. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
  18. package/Types/Infrastructure/BasicMetrics.ts +75 -0
  19. package/Types/Metrics/MetricQueryData.ts +11 -0
  20. package/Types/Monitor/CriteriaFilter.ts +10 -0
  21. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
  22. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
  23. package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
  24. package/Types/Monitor/MonitorMetricType.ts +34 -0
  25. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
  26. package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
  27. package/Types/StatusPage/StatusPageLanguage.ts +29 -0
  28. package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
  29. package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
  30. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
  31. package/UI/Components/Charts/Line/LineChart.tsx +16 -11
  32. package/UI/Components/Filters/FiltersForm.tsx +26 -2
  33. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
  34. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
  35. package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
  36. package/Utils/Monitor/MonitorMetricType.ts +309 -19
  37. package/build/dist/Models/DatabaseModels/Alert.js +57 -0
  38. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  39. package/build/dist/Models/DatabaseModels/Incident.js +57 -0
  40. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  41. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  42. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  43. package/build/dist/Server/API/StatusPageAPI.js +4 -0
  44. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
  52. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
  54. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  55. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
  56. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  57. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +66 -12
  58. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  59. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
  60. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  61. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +91 -15
  62. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  63. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
  64. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  65. package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
  66. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  67. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
  68. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  69. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
  70. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
  71. package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
  72. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  73. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
  74. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
  75. package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
  76. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  77. package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
  78. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
  79. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
  80. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  81. package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
  82. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  83. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
  84. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  85. package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
  86. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  87. package/build/dist/UI/Components/Filters/FiltersForm.js +6 -2
  88. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  89. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
  90. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
  91. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
  92. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
  93. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
  94. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
  95. package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
  96. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  97. 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 {