@oneuptime/common 10.0.65 → 10.0.66

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 (210) hide show
  1. package/Models/DatabaseModels/DockerHostOwnerTeam.ts +464 -0
  2. package/Models/DatabaseModels/DockerHostOwnerUser.ts +463 -0
  3. package/Models/DatabaseModels/Index.ts +24 -0
  4. package/Models/DatabaseModels/KubernetesClusterOwnerTeam.ts +464 -0
  5. package/Models/DatabaseModels/KubernetesClusterOwnerUser.ts +463 -0
  6. package/Models/DatabaseModels/KubernetesResource.ts +548 -0
  7. package/Models/DatabaseModels/MetricPipelineRule.ts +804 -0
  8. package/Models/DatabaseModels/MetricRecordingRule.ts +470 -0
  9. package/Models/DatabaseModels/Monitor.ts +2 -0
  10. package/Models/DatabaseModels/Project.ts +53 -0
  11. package/Models/DatabaseModels/Service.ts +79 -0
  12. package/Models/DatabaseModels/TraceDropFilter.ts +508 -0
  13. package/Models/DatabaseModels/TracePipeline.ts +436 -0
  14. package/Models/DatabaseModels/TracePipelineProcessor.ts +454 -0
  15. package/Models/DatabaseModels/TraceRecordingRule.ts +470 -0
  16. package/Models/DatabaseModels/TraceScrubRule.ts +546 -0
  17. package/Server/API/KubernetesResourceAPI.ts +129 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.ts +399 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.ts +205 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.ts +335 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.ts +29 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.ts +53 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -1
  24. package/Server/Services/DockerHostOwnerTeamService.ts +10 -0
  25. package/Server/Services/DockerHostOwnerUserService.ts +10 -0
  26. package/Server/Services/KubernetesClusterOwnerTeamService.ts +10 -0
  27. package/Server/Services/KubernetesClusterOwnerUserService.ts +10 -0
  28. package/Server/Services/KubernetesResourceService.ts +351 -0
  29. package/Server/Services/MetricPipelineRuleService.ts +10 -0
  30. package/Server/Services/MetricRecordingRuleService.ts +10 -0
  31. package/Server/Services/TraceDropFilterService.ts +10 -0
  32. package/Server/Services/TracePipelineProcessorService.ts +10 -0
  33. package/Server/Services/TracePipelineService.ts +10 -0
  34. package/Server/Services/TraceRecordingRuleService.ts +10 -0
  35. package/Server/Services/TraceScrubRuleService.ts +10 -0
  36. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +71 -9
  37. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +483 -75
  38. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +379 -6
  39. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +502 -0
  40. package/Tests/Utils/MetricUnitUtil.test.ts +216 -0
  41. package/Tests/Utils/Metrics/MetricFormulaEvaluator.test.ts +269 -0
  42. package/Tests/Utils/Metrics/MetricResultUnitConverter.test.ts +231 -0
  43. package/Tests/Utils/RecordingRuleExpression.test.ts +177 -0
  44. package/Types/Kubernetes/KubernetesInventoryExtractor.ts +327 -0
  45. package/Types/Kubernetes/KubernetesObjectParser.ts +1949 -0
  46. package/Types/Metrics/MetricDownsamplingRetentionDays.ts +49 -0
  47. package/Types/Metrics/MetricFormulaConfigData.ts +4 -0
  48. package/Types/Metrics/MetricPipelineRuleFilterCondition.ts +136 -0
  49. package/Types/Metrics/MetricPipelineRuleType.ts +27 -0
  50. package/Types/Metrics/RecordingRuleDefinition.ts +180 -0
  51. package/Types/Monitor/CriteriaFilter.ts +43 -0
  52. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +70 -0
  53. package/Types/Permission.ts +520 -0
  54. package/Types/Trace/TraceAggregationType.ts +17 -0
  55. package/Types/Trace/TraceDropFilterAction.ts +6 -0
  56. package/Types/Trace/TracePipelineProcessorType.ts +56 -0
  57. package/Types/Trace/TraceRecordingRuleDefinition.ts +218 -0
  58. package/Types/Trace/TraceScrubAction.ts +7 -0
  59. package/Types/Trace/TraceScrubField.ts +8 -0
  60. package/Types/Trace/TraceScrubPatternType.ts +10 -0
  61. package/UI/Components/CardSelect/CardSelect.tsx +9 -1
  62. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +6 -10
  63. package/UI/Components/Forms/Fields/FormField.tsx +1 -0
  64. package/UI/Components/Forms/Types/Field.ts +1 -0
  65. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +57 -0
  66. package/UI/Components/Page/Page.tsx +6 -0
  67. package/Utils/MetricUnitUtil.ts +289 -0
  68. package/Utils/Metrics/MetricFormulaEvaluator.ts +610 -0
  69. package/Utils/Metrics/MetricResultUnitConverter.ts +91 -0
  70. package/Utils/Metrics/RecordingRuleExpression.ts +359 -0
  71. package/Utils/ValueFormatter.ts +137 -13
  72. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js +480 -0
  73. package/build/dist/Models/DatabaseModels/DockerHostOwnerTeam.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js +479 -0
  75. package/build/dist/Models/DatabaseModels/DockerHostOwnerUser.js.map +1 -0
  76. package/build/dist/Models/DatabaseModels/Index.js +24 -0
  77. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js +480 -0
  79. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerTeam.js.map +1 -0
  80. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js +479 -0
  81. package/build/dist/Models/DatabaseModels/KubernetesClusterOwnerUser.js.map +1 -0
  82. package/build/dist/Models/DatabaseModels/KubernetesResource.js +590 -0
  83. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -0
  84. package/build/dist/Models/DatabaseModels/MetricPipelineRule.js +836 -0
  85. package/build/dist/Models/DatabaseModels/MetricPipelineRule.js.map +1 -0
  86. package/build/dist/Models/DatabaseModels/MetricRecordingRule.js +497 -0
  87. package/build/dist/Models/DatabaseModels/MetricRecordingRule.js.map +1 -0
  88. package/build/dist/Models/DatabaseModels/Monitor.js +2 -0
  89. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Project.js +53 -0
  91. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Service.js +79 -0
  93. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +536 -0
  95. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -0
  96. package/build/dist/Models/DatabaseModels/TracePipeline.js +462 -0
  97. package/build/dist/Models/DatabaseModels/TracePipeline.js.map +1 -0
  98. package/build/dist/Models/DatabaseModels/TracePipelineProcessor.js +476 -0
  99. package/build/dist/Models/DatabaseModels/TracePipelineProcessor.js.map +1 -0
  100. package/build/dist/Models/DatabaseModels/TraceRecordingRule.js +497 -0
  101. package/build/dist/Models/DatabaseModels/TraceRecordingRule.js.map +1 -0
  102. package/build/dist/Models/DatabaseModels/TraceScrubRule.js +575 -0
  103. package/build/dist/Models/DatabaseModels/TraceScrubRule.js.map +1 -0
  104. package/build/dist/Server/API/KubernetesResourceAPI.js +98 -0
  105. package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.js +144 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776504277320-MigrationName.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.js +82 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776505976155-AddTracePipelineTables.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.js +118 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776509413763-MigrationName.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.js +16 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776541018853-MigrationName.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.js +24 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776544084793-MigrationName.js.map +1 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  118. package/build/dist/Server/Services/DockerHostOwnerTeamService.js +9 -0
  119. package/build/dist/Server/Services/DockerHostOwnerTeamService.js.map +1 -0
  120. package/build/dist/Server/Services/DockerHostOwnerUserService.js +9 -0
  121. package/build/dist/Server/Services/DockerHostOwnerUserService.js.map +1 -0
  122. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js +9 -0
  123. package/build/dist/Server/Services/KubernetesClusterOwnerTeamService.js.map +1 -0
  124. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js +9 -0
  125. package/build/dist/Server/Services/KubernetesClusterOwnerUserService.js.map +1 -0
  126. package/build/dist/Server/Services/KubernetesResourceService.js +237 -0
  127. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -0
  128. package/build/dist/Server/Services/MetricPipelineRuleService.js +9 -0
  129. package/build/dist/Server/Services/MetricPipelineRuleService.js.map +1 -0
  130. package/build/dist/Server/Services/MetricRecordingRuleService.js +9 -0
  131. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -0
  132. package/build/dist/Server/Services/TraceDropFilterService.js +9 -0
  133. package/build/dist/Server/Services/TraceDropFilterService.js.map +1 -0
  134. package/build/dist/Server/Services/TracePipelineProcessorService.js +9 -0
  135. package/build/dist/Server/Services/TracePipelineProcessorService.js.map +1 -0
  136. package/build/dist/Server/Services/TracePipelineService.js +9 -0
  137. package/build/dist/Server/Services/TracePipelineService.js.map +1 -0
  138. package/build/dist/Server/Services/TraceRecordingRuleService.js +9 -0
  139. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -0
  140. package/build/dist/Server/Services/TraceScrubRuleService.js +9 -0
  141. package/build/dist/Server/Services/TraceScrubRuleService.js.map +1 -0
  142. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +56 -9
  143. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  144. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +335 -53
  145. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  146. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +277 -5
  147. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  148. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +407 -0
  149. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -0
  150. package/build/dist/Tests/Utils/MetricUnitUtil.test.js +159 -0
  151. package/build/dist/Tests/Utils/MetricUnitUtil.test.js.map +1 -0
  152. package/build/dist/Tests/Utils/Metrics/MetricFormulaEvaluator.test.js +224 -0
  153. package/build/dist/Tests/Utils/Metrics/MetricFormulaEvaluator.test.js.map +1 -0
  154. package/build/dist/Tests/Utils/Metrics/MetricResultUnitConverter.test.js +180 -0
  155. package/build/dist/Tests/Utils/Metrics/MetricResultUnitConverter.test.js.map +1 -0
  156. package/build/dist/Tests/Utils/RecordingRuleExpression.test.js +142 -0
  157. package/build/dist/Tests/Utils/RecordingRuleExpression.test.js.map +1 -0
  158. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +200 -0
  159. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -0
  160. package/build/dist/Types/Kubernetes/KubernetesObjectParser.js +1205 -0
  161. package/build/dist/Types/Kubernetes/KubernetesObjectParser.js.map +1 -0
  162. package/build/dist/Types/Metrics/MetricDownsamplingRetentionDays.js +32 -0
  163. package/build/dist/Types/Metrics/MetricDownsamplingRetentionDays.js.map +1 -0
  164. package/build/dist/Types/Metrics/MetricPipelineRuleFilterCondition.js +103 -0
  165. package/build/dist/Types/Metrics/MetricPipelineRuleFilterCondition.js.map +1 -0
  166. package/build/dist/Types/Metrics/MetricPipelineRuleType.js +27 -0
  167. package/build/dist/Types/Metrics/MetricPipelineRuleType.js.map +1 -0
  168. package/build/dist/Types/Metrics/RecordingRuleDefinition.js +110 -0
  169. package/build/dist/Types/Metrics/RecordingRuleDefinition.js.map +1 -0
  170. package/build/dist/Types/Monitor/CriteriaFilter.js +22 -0
  171. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  172. package/build/dist/Types/Monitor/MetricMonitor/MetricCriteriaContext.js +2 -0
  173. package/build/dist/Types/Monitor/MetricMonitor/MetricCriteriaContext.js.map +1 -0
  174. package/build/dist/Types/Permission.js +454 -0
  175. package/build/dist/Types/Permission.js.map +1 -1
  176. package/build/dist/Types/Trace/TraceAggregationType.js +18 -0
  177. package/build/dist/Types/Trace/TraceAggregationType.js.map +1 -0
  178. package/build/dist/Types/Trace/TraceDropFilterAction.js +7 -0
  179. package/build/dist/Types/Trace/TraceDropFilterAction.js.map +1 -0
  180. package/build/dist/Types/Trace/TracePipelineProcessorType.js +10 -0
  181. package/build/dist/Types/Trace/TracePipelineProcessorType.js.map +1 -0
  182. package/build/dist/Types/Trace/TraceRecordingRuleDefinition.js +145 -0
  183. package/build/dist/Types/Trace/TraceRecordingRuleDefinition.js.map +1 -0
  184. package/build/dist/Types/Trace/TraceScrubAction.js +8 -0
  185. package/build/dist/Types/Trace/TraceScrubAction.js.map +1 -0
  186. package/build/dist/Types/Trace/TraceScrubField.js +9 -0
  187. package/build/dist/Types/Trace/TraceScrubField.js.map +1 -0
  188. package/build/dist/Types/Trace/TraceScrubPatternType.js +11 -0
  189. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -0
  190. package/build/dist/UI/Components/CardSelect/CardSelect.js +3 -1
  191. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -9
  193. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  194. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  195. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  196. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +30 -0
  197. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  198. package/build/dist/UI/Components/Page/Page.js +1 -0
  199. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  200. package/build/dist/Utils/MetricUnitUtil.js +232 -0
  201. package/build/dist/Utils/MetricUnitUtil.js.map +1 -0
  202. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js +453 -0
  203. package/build/dist/Utils/Metrics/MetricFormulaEvaluator.js.map +1 -0
  204. package/build/dist/Utils/Metrics/MetricResultUnitConverter.js +61 -0
  205. package/build/dist/Utils/Metrics/MetricResultUnitConverter.js.map +1 -0
  206. package/build/dist/Utils/Metrics/RecordingRuleExpression.js +298 -0
  207. package/build/dist/Utils/Metrics/RecordingRuleExpression.js.map +1 -0
  208. package/build/dist/Utils/ValueFormatter.js +123 -13
  209. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  210. package/package.json +1 -1
@@ -45,10 +45,16 @@ import URL from "../../../Types/API/URL";
45
45
  import IP from "../../../Types/IP/IP";
46
46
  import Hostname from "../../../Types/API/Hostname";
47
47
  import Port from "../../../Types/Port";
48
+ import { DashboardClientUrl } from "../../EnvironmentConfig";
48
49
  import MetricMonitorResponse, {
49
50
  KubernetesAffectedResource,
50
51
  KubernetesResourceBreakdown,
51
52
  } from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
53
+ import MetricCriteriaContext, {
54
+ MetricBreachingSample,
55
+ MetricComponent,
56
+ MetricComponentValue,
57
+ } from "../../../Types/Monitor/MetricMonitor/MetricCriteriaContext";
52
58
  import MonitorStepDockerMonitor from "../../../Types/Monitor/MonitorStepDockerMonitor";
53
59
 
54
60
  export default class MonitorCriteriaEvaluator {
@@ -108,13 +114,9 @@ export default class MonitorCriteriaEvaluator {
108
114
  if (rootCause) {
109
115
  input.probeApiIngestResponse.criteriaMetId = criteriaInstance.data?.id;
110
116
  input.probeApiIngestResponse.rootCause = `
111
- **Created because the following criteria was met**:
117
+ **Created because the following criteria was met**:
112
118
 
113
119
  **Criteria Name**: ${criteriaInstance.data?.name}
114
- `;
115
-
116
- input.probeApiIngestResponse.rootCause += `
117
- **Filter Conditions Met**: ${rootCause}
118
120
  `;
119
121
 
120
122
  const contextBlock: string | null =
@@ -122,9 +124,28 @@ export default class MonitorCriteriaEvaluator {
122
124
  dataToProcess: input.dataToProcess,
123
125
  monitorStep: input.monitorStep,
124
126
  monitor: input.monitor,
127
+ criteriaInstance: criteriaInstance,
125
128
  });
126
129
 
127
- if (contextBlock) {
130
+ /*
131
+ * For metric monitors, render the metric identity (name, unit,
132
+ * filter attrs, breaching series) before the comparison line so
133
+ * the reader has context when they reach "Filter Conditions Met".
134
+ */
135
+ const isMetricMonitor: boolean =
136
+ input.monitor.monitorType === MonitorType.Metrics;
137
+
138
+ if (contextBlock && isMetricMonitor) {
139
+ input.probeApiIngestResponse.rootCause += `
140
+ ${contextBlock}
141
+ `;
142
+ }
143
+
144
+ input.probeApiIngestResponse.rootCause += `
145
+ **Filter Conditions Met**: ${rootCause}
146
+ `;
147
+
148
+ if (contextBlock && !isMetricMonitor) {
128
149
  input.probeApiIngestResponse.rootCause += `
129
150
  ${contextBlock}
130
151
  `;
@@ -568,6 +589,7 @@ ${contextBlock}
568
589
  dataToProcess: DataToProcess;
569
590
  monitorStep: MonitorStep;
570
591
  monitor: Monitor;
592
+ criteriaInstance?: MonitorCriteriaInstance;
571
593
  }): Promise<string | null> {
572
594
  // Handle Kubernetes monitors with rich resource context
573
595
  if (input.monitor.monitorType === MonitorType.Kubernetes) {
@@ -581,6 +603,17 @@ ${contextBlock}
581
603
  return MonitorCriteriaEvaluator.buildDockerRootCauseContext(input);
582
604
  }
583
605
 
606
+ // Handle generic Metric monitors with metric identity + breaching series
607
+ if (
608
+ input.monitor.monitorType === MonitorType.Metrics &&
609
+ input.criteriaInstance
610
+ ) {
611
+ return MonitorCriteriaEvaluator.buildMetricRootCauseContext({
612
+ criteriaInstance: input.criteriaInstance,
613
+ monitor: input.monitor,
614
+ });
615
+ }
616
+
584
617
  const requestDetails: Array<string> = [];
585
618
  const responseDetails: Array<string> = [];
586
619
  const failureDetails: Array<string> = [];
@@ -689,6 +722,346 @@ ${contextBlock}
689
722
  return sections.join("\n");
690
723
  }
691
724
 
725
+ private static buildMetricRootCauseContext(input: {
726
+ criteriaInstance: MonitorCriteriaInstance;
727
+ monitor: Monitor;
728
+ }): string | null {
729
+ /*
730
+ * Pick the first populated metric context across the instance's filters.
731
+ * Only metric-value filters populate this at evaluation time, so this
732
+ * effectively returns the context for the filter that ran.
733
+ */
734
+ const ctx: MetricCriteriaContext | undefined = (
735
+ input.criteriaInstance.data?.filters || []
736
+ )
737
+ .map((f: CriteriaFilter) => {
738
+ return f.metricCriteriaContext;
739
+ })
740
+ .find(
741
+ (c: MetricCriteriaContext | undefined): c is MetricCriteriaContext => {
742
+ return Boolean(c);
743
+ },
744
+ );
745
+
746
+ if (!ctx) {
747
+ return null;
748
+ }
749
+
750
+ const lines: Array<string> = [];
751
+ lines.push(`- Metric: \`${ctx.metricName}\``);
752
+ if (ctx.alias) {
753
+ lines.push(`- Alias: \`${ctx.alias}\``);
754
+ }
755
+ if (ctx.unit) {
756
+ lines.push(`- Unit: ${ctx.unit}`);
757
+ }
758
+ if (ctx.aggregationType) {
759
+ lines.push(`- Aggregation: ${ctx.aggregationType}`);
760
+ }
761
+ if (ctx.isFormula && ctx.formulaExpression) {
762
+ lines.push(`- Formula: \`${ctx.formulaExpression}\``);
763
+ }
764
+ if (ctx.timeWindowMinutes) {
765
+ lines.push(`- Time Window: last ${ctx.timeWindowMinutes} minutes`);
766
+ }
767
+
768
+ /*
769
+ * For formulas, enumerate the underlying variables so the on-call
770
+ * engineer can trace `c = a + b` back to "a is container.cpu.time,
771
+ * b is container.memory.usage in bytes" without clicking away.
772
+ */
773
+ if (ctx.components && ctx.components.length > 0) {
774
+ const componentLines: Array<string> = ctx.components.map(
775
+ (component: MetricComponent) => {
776
+ const unitSuffix: string = component.unit
777
+ ? ` — unit: ${component.unit}`
778
+ : "";
779
+ const typeSuffix: string = component.isFormula ? " (formula)" : "";
780
+ return ` - \`${component.alias}\` = \`${component.name}\`${typeSuffix}${unitSuffix}`;
781
+ },
782
+ );
783
+ lines.push(`- Components:\n${componentLines.join("\n")}`);
784
+ }
785
+
786
+ const filterKeys: Array<string> = Object.keys(ctx.filterAttributes || {});
787
+ if (filterKeys.length > 0) {
788
+ const filterLines: Array<string> = filterKeys.map((k: string) => {
789
+ const v: unknown = (ctx.filterAttributes as Record<string, unknown>)[k];
790
+ return ` - \`${k}\` = \`${String(v)}\``;
791
+ });
792
+ lines.push(`- Filters:\n${filterLines.join("\n")}`);
793
+ }
794
+
795
+ if (ctx.groupBy.length > 0) {
796
+ lines.push(
797
+ `- Grouped By: ${ctx.groupBy
798
+ .map((g: string) => {
799
+ return `\`${g}\``;
800
+ })
801
+ .join(", ")}`,
802
+ );
803
+ }
804
+
805
+ const sections: Array<string> = [`**Metric Details**\n${lines.join("\n")}`];
806
+
807
+ const breachingSamples: Array<MetricBreachingSample> =
808
+ ctx.breachingSamples && ctx.breachingSamples.length > 0
809
+ ? ctx.breachingSamples
810
+ : ctx.breachingSample
811
+ ? [ctx.breachingSample]
812
+ : [];
813
+
814
+ if (breachingSamples.length > 0) {
815
+ sections.push(
816
+ `\n\n${MonitorCriteriaEvaluator.formatBreachingSamplesSection({
817
+ samples: breachingSamples,
818
+ totalSamples: ctx.totalSamplesInWindow,
819
+ unit: ctx.unit,
820
+ metricName: ctx.metricName,
821
+ alias: ctx.alias,
822
+ components: ctx.components || [],
823
+ })}`,
824
+ );
825
+ }
826
+
827
+ const deepLink: string | null =
828
+ MonitorCriteriaEvaluator.buildMetricExplorerDeepLink({
829
+ monitor: input.monitor,
830
+ ctx,
831
+ });
832
+
833
+ if (deepLink) {
834
+ sections.push(`\n\n[Open metric in dashboard](${deepLink})`);
835
+ }
836
+
837
+ return sections.join("\n");
838
+ }
839
+
840
+ /**
841
+ * Build the **Breaching Samples** markdown section — a table of
842
+ * timestamps, values, and any group-by attributes. Caps the row count
843
+ * so a 30-minute window at 1-second granularity doesn't dump thousands
844
+ * of lines onto the root-cause page; the caller can always drill in
845
+ * via the metric explorer link.
846
+ *
847
+ * Timestamps are emitted as inline code wrapping ISO 8601 strings so
848
+ * the client-side markdown viewer can localize them to the viewer's
849
+ * timezone (without losing the canonical instant).
850
+ */
851
+ private static formatBreachingSamplesSection(input: {
852
+ samples: Array<MetricBreachingSample>;
853
+ totalSamples?: number | undefined;
854
+ unit: string | null;
855
+ metricName: string;
856
+ alias: string;
857
+ components: Array<MetricComponent>;
858
+ }): string {
859
+ const MAX_ROWS: number = 20;
860
+
861
+ // Sort chronologically and de-duplicate any accidental repeats
862
+ const sorted: Array<MetricBreachingSample> = [...input.samples].sort(
863
+ (a: MetricBreachingSample, b: MetricBreachingSample) => {
864
+ return (
865
+ new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
866
+ );
867
+ },
868
+ );
869
+
870
+ const displayedSamples: Array<MetricBreachingSample> = sorted.slice(
871
+ 0,
872
+ MAX_ROWS,
873
+ );
874
+
875
+ // Collect attribute keys that appear on any displayed sample
876
+ const attrKeySet: Set<string> = new Set<string>();
877
+ for (const s of displayedSamples) {
878
+ for (const k of Object.keys(s.attributes || {})) {
879
+ attrKeySet.add(k);
880
+ }
881
+ }
882
+ const attrKeys: Array<string> = Array.from(attrKeySet);
883
+
884
+ /*
885
+ * Escape pipe characters that could appear in the metric display
886
+ * name (formulas like "a | b" are unlikely but possible) so they
887
+ * don't break GitHub-flavored-markdown tables.
888
+ */
889
+ const escapeCell: (value: string) => string = (value: string): string => {
890
+ return value.replace(/\|/g, "\\|");
891
+ };
892
+
893
+ /*
894
+ * Column layout:
895
+ * Timestamp | Metric | Alias | Value | <component_1> | ... | <attr_1> | ...
896
+ *
897
+ * The component columns let the reader see what each variable of a
898
+ * formula resolved to at the breach time — e.g. "when c = a + b
899
+ * breached 100, a was 55, b was 46". They are omitted for plain
900
+ * metric criteria.
901
+ */
902
+ const headerCells: Array<string> = [
903
+ "Timestamp",
904
+ "Metric",
905
+ "Alias",
906
+ "Value",
907
+ ];
908
+
909
+ for (const component of input.components) {
910
+ const label: string = component.unit
911
+ ? `${component.alias} (${component.unit})`
912
+ : component.alias;
913
+ headerCells.push(label);
914
+ }
915
+
916
+ headerCells.push(...attrKeys);
917
+
918
+ const unitSuffix: string = input.unit ? ` ${input.unit}` : "";
919
+
920
+ const headerRow: string = `| ${headerCells.join(" | ")} |`;
921
+ const dividerRow: string = `| ${headerCells
922
+ .map(() => {
923
+ return "---";
924
+ })
925
+ .join(" | ")} |`;
926
+
927
+ const metricCell: string = `\`${escapeCell(input.metricName)}\``;
928
+ const aliasCell: string = input.alias
929
+ ? `\`${escapeCell(input.alias)}\``
930
+ : "-";
931
+
932
+ const dataRows: Array<string> = displayedSamples.map(
933
+ (s: MetricBreachingSample) => {
934
+ const timestampIso: string = new Date(s.timestamp).toISOString();
935
+ const cells: Array<string> = [
936
+ `\`${timestampIso}\``,
937
+ metricCell,
938
+ aliasCell,
939
+ `${MonitorCriteriaEvaluator.formatNumberForDisplay(s.value)}${unitSuffix}`,
940
+ ];
941
+
942
+ for (const component of input.components) {
943
+ const match: MetricComponentValue | undefined = (
944
+ s.componentValues || []
945
+ ).find((cv: MetricComponentValue) => {
946
+ return cv.alias === component.alias;
947
+ });
948
+ if (match && typeof match.value === "number") {
949
+ const componentUnitSuffix: string = component.unit
950
+ ? ` ${component.unit}`
951
+ : "";
952
+ cells.push(
953
+ `${MonitorCriteriaEvaluator.formatNumberForDisplay(match.value)}${componentUnitSuffix}`,
954
+ );
955
+ } else {
956
+ cells.push("-");
957
+ }
958
+ }
959
+
960
+ for (const k of attrKeys) {
961
+ const v: unknown = (s.attributes as Record<string, unknown>)[k];
962
+ cells.push(v === undefined || v === null ? "-" : String(v));
963
+ }
964
+ return `| ${cells.join(" | ")} |`;
965
+ },
966
+ );
967
+
968
+ const lines: Array<string> = [
969
+ `**Breaching Samples**`,
970
+ MonitorCriteriaEvaluator.formatBreachingSamplesSummary({
971
+ breachingCount: sorted.length,
972
+ totalSamples: input.totalSamples,
973
+ }),
974
+ "",
975
+ headerRow,
976
+ dividerRow,
977
+ ...dataRows,
978
+ ];
979
+
980
+ if (sorted.length > displayedSamples.length) {
981
+ lines.push(
982
+ `\n_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`,
983
+ );
984
+ }
985
+
986
+ return lines.join("\n");
987
+ }
988
+
989
+ private static formatBreachingSamplesSummary(input: {
990
+ breachingCount: number;
991
+ totalSamples?: number | undefined;
992
+ }): string {
993
+ if (
994
+ typeof input.totalSamples === "number" &&
995
+ input.totalSamples > 0 &&
996
+ input.totalSamples >= input.breachingCount
997
+ ) {
998
+ return `${input.breachingCount} of ${input.totalSamples} samples breached the threshold.`;
999
+ }
1000
+ return `${input.breachingCount} sample${input.breachingCount === 1 ? "" : "s"} breached the threshold.`;
1001
+ }
1002
+
1003
+ private static formatNumberForDisplay(value: number): string {
1004
+ if (!Number.isFinite(value)) {
1005
+ return String(value);
1006
+ }
1007
+ if (Number.isInteger(value)) {
1008
+ return value.toString();
1009
+ }
1010
+ return Number(value.toFixed(2)).toString();
1011
+ }
1012
+
1013
+ private static buildMetricExplorerDeepLink(input: {
1014
+ monitor: Monitor;
1015
+ ctx: MetricCriteriaContext;
1016
+ }): string | null {
1017
+ const projectId: string | undefined = input.monitor.projectId?.toString();
1018
+
1019
+ if (!projectId) {
1020
+ return null;
1021
+ }
1022
+
1023
+ /*
1024
+ * Metric explorer expects a JSON-encoded `metricQueries` param plus
1025
+ * optional start/end times. The shape is documented by
1026
+ * MetricExplorer.getMetricQueriesFromQuery(): it reads metricName,
1027
+ * attributes, and aggregationType (correctly spelled, unlike the
1028
+ * internal persisted field).
1029
+ */
1030
+ const aggregationType: string | undefined =
1031
+ input.ctx.aggregationType || undefined;
1032
+
1033
+ const query: {
1034
+ metricName: string;
1035
+ attributes: JSONObject;
1036
+ aggregationType?: string;
1037
+ } = {
1038
+ metricName: input.ctx.metricName,
1039
+ attributes: input.ctx.filterAttributes || {},
1040
+ ...(aggregationType ? { aggregationType } : {}),
1041
+ };
1042
+
1043
+ // Time window: breach moment +- 15 minutes (or fall back to last hour).
1044
+ const now: Date = OneUptimeDate.getCurrentDate();
1045
+ const breachTime: Date | undefined = input.ctx.breachingSample?.timestamp;
1046
+ const startTime: Date = breachTime
1047
+ ? OneUptimeDate.addRemoveMinutes(breachTime, -30)
1048
+ : OneUptimeDate.addRemoveHours(now, -1);
1049
+ const endTime: Date = breachTime
1050
+ ? OneUptimeDate.addRemoveMinutes(breachTime, 15)
1051
+ : now;
1052
+
1053
+ const params: URLSearchParams = new URLSearchParams();
1054
+ params.set("metricQueries", JSON.stringify([query]));
1055
+ params.set("startTime", OneUptimeDate.toString(startTime));
1056
+ params.set("endTime", OneUptimeDate.toString(endTime));
1057
+
1058
+ /*
1059
+ * The route that actually reads these URL params is the metric
1060
+ * explorer at /metrics/view — the /metrics index is the metric list.
1061
+ */
1062
+ return `${DashboardClientUrl.toString()}/${projectId}/metrics/view?${params.toString()}`;
1063
+ }
1064
+
692
1065
  private static async buildKubernetesRootCauseContext(input: {
693
1066
  dataToProcess: DataToProcess;
694
1067
  monitorStep: MonitorStep;