@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -100,6 +100,11 @@ import MonitorStepPodmanMonitor from "../../../Types/Monitor/MonitorStepPodmanMo
100
100
  import MonitorStepProxmoxMonitor from "../../../Types/Monitor/MonitorStepProxmoxMonitor";
101
101
  import MonitorStepCephMonitor from "../../../Types/Monitor/MonitorStepCephMonitor";
102
102
  import MonitorStepDockerSwarmMonitor from "../../../Types/Monitor/MonitorStepDockerSwarmMonitor";
103
+ import MetricValueFormatter from "../../../Utils/Monitor/MetricValueFormatter";
104
+ import { getKubernetesMetricByMetricName } from "../../../Types/Monitor/KubernetesMetricCatalog";
105
+ import { getProxmoxMetricByMetricName } from "../../../Types/Monitor/ProxmoxMetricCatalog";
106
+ import { getCephMetricByMetricName } from "../../../Types/Monitor/CephMetricCatalog";
107
+ import { getDockerSwarmMetricByMetricName } from "../../../Types/Monitor/DockerSwarmMetricCatalog";
103
108
 
104
109
  /**
105
110
  * A cross-signal deep link into a telemetry explorer, plus the scope
@@ -1433,13 +1438,28 @@ ${contextBlock}
1433
1438
  return null;
1434
1439
  }
1435
1440
 
1441
+ const unitHeuristicMetricName: string | undefined =
1442
+ MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1443
+ metricName: ctx.metricName,
1444
+ isFormula: ctx.isFormula,
1445
+ });
1446
+
1436
1447
  const lines: Array<string> = [];
1437
1448
  lines.push(`- Metric: \`${ctx.metricName}\``);
1438
1449
  if (ctx.alias) {
1439
1450
  lines.push(`- Alias: \`${ctx.alias}\``);
1440
1451
  }
1441
- if (ctx.unit) {
1442
- lines.push(`- Unit: ${ctx.unit}`);
1452
+ /*
1453
+ * Spell the unit out — "Bytes", not the UCUM "By" the exporter wrote —
1454
+ * and drop the line entirely for units that name no dimension, so a
1455
+ * ratio metric no longer reports the bare "Unit: 1".
1456
+ */
1457
+ const readableUnit: string | null = MetricValueFormatter.getReadableUnit(
1458
+ ctx.unit,
1459
+ unitHeuristicMetricName,
1460
+ );
1461
+ if (readableUnit) {
1462
+ lines.push(`- Unit: ${readableUnit}`);
1443
1463
  }
1444
1464
  if (ctx.aggregationType) {
1445
1465
  lines.push(`- Aggregation: ${ctx.aggregationType}`);
@@ -1459,8 +1479,16 @@ ${contextBlock}
1459
1479
  if (ctx.components && ctx.components.length > 0) {
1460
1480
  const componentLines: Array<string> = ctx.components.map(
1461
1481
  (component: MetricComponent) => {
1462
- const unitSuffix: string = component.unit
1463
- ? ` — unit: ${component.unit}`
1482
+ const componentUnit: string | null =
1483
+ MetricValueFormatter.getReadableUnit(
1484
+ component.unit,
1485
+ MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1486
+ metricName: component.name,
1487
+ isFormula: component.isFormula,
1488
+ }),
1489
+ );
1490
+ const unitSuffix: string = componentUnit
1491
+ ? ` — unit: ${componentUnit}`
1464
1492
  : "";
1465
1493
  const typeSuffix: string = component.isFormula ? " (formula)" : "";
1466
1494
  return ` - \`${component.alias}\` = \`${component.name}\`${typeSuffix}${unitSuffix}`;
@@ -1504,6 +1532,7 @@ ${contextBlock}
1504
1532
  totalSamples: ctx.totalSamplesInWindow,
1505
1533
  unit: ctx.unit,
1506
1534
  metricName: ctx.metricName,
1535
+ unitHeuristicMetricName: unitHeuristicMetricName,
1507
1536
  alias: ctx.alias,
1508
1537
  components: ctx.components || [],
1509
1538
  })}`,
@@ -1575,6 +1604,11 @@ ${contextBlock}
1575
1604
  totalSamples?: number | undefined;
1576
1605
  unit: string | null;
1577
1606
  metricName: string;
1607
+ /**
1608
+ * The metric name to reason about, or undefined for a formula.
1609
+ * See MonitorCriteriaEvaluator.metricNameForUnitHeuristics.
1610
+ */
1611
+ unitHeuristicMetricName: string | undefined;
1578
1612
  alias: string;
1579
1613
  components: Array<MetricComponent>;
1580
1614
  }): string {
@@ -1628,17 +1662,20 @@ ${contextBlock}
1628
1662
  "Value",
1629
1663
  ];
1630
1664
 
1665
+ /*
1666
+ * The component columns carry no unit in their header any more. Each
1667
+ * cell now names the unit it actually landed on, and auto-scaling
1668
+ * means neighbouring rows can land on different ones — a header that
1669
+ * read "a (By)" over cells reading "900 KB" and "1.2 MB" states a
1670
+ * unit that neither row uses. The configured unit is not lost: the
1671
+ * Components bullet list above the table still records it.
1672
+ */
1631
1673
  for (const component of input.components) {
1632
- const label: string = component.unit
1633
- ? `${component.alias} (${component.unit})`
1634
- : component.alias;
1635
- headerCells.push(label);
1674
+ headerCells.push(component.alias);
1636
1675
  }
1637
1676
 
1638
1677
  headerCells.push(...attrKeys);
1639
1678
 
1640
- const unitSuffix: string = input.unit ? ` ${input.unit}` : "";
1641
-
1642
1679
  const headerRow: string = `| ${headerCells.join(" | ")} |`;
1643
1680
  const dividerRow: string = `| ${headerCells
1644
1681
  .map(() => {
@@ -1658,7 +1695,11 @@ ${contextBlock}
1658
1695
  `\`${timestampIso}\``,
1659
1696
  metricCell,
1660
1697
  aliasCell,
1661
- `${MonitorCriteriaEvaluator.formatNumberForDisplay(s.value)}${unitSuffix}`,
1698
+ MetricValueFormatter.format({
1699
+ value: s.value,
1700
+ unit: input.unit,
1701
+ metricName: input.unitHeuristicMetricName,
1702
+ }),
1662
1703
  ];
1663
1704
 
1664
1705
  for (const component of input.components) {
@@ -1668,11 +1709,24 @@ ${contextBlock}
1668
1709
  return cv.alias === component.alias;
1669
1710
  });
1670
1711
  if (match && typeof match.value === "number") {
1671
- const componentUnitSuffix: string = component.unit
1672
- ? ` ${component.unit}`
1673
- : "";
1712
+ /*
1713
+ * Each component keeps its OWN unit. Component values are
1714
+ * indexed off the per-query series and never go through the
1715
+ * sample→threshold-unit conversion the main Value column
1716
+ * does, so they are in the component's legendUnit — mixing
1717
+ * units across the row is deliberate, and is why every cell
1718
+ * has to say which one it is in.
1719
+ */
1674
1720
  cells.push(
1675
- `${MonitorCriteriaEvaluator.formatNumberForDisplay(match.value)}${componentUnitSuffix}`,
1721
+ MetricValueFormatter.format({
1722
+ value: match.value,
1723
+ unit: component.unit,
1724
+ metricName:
1725
+ MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1726
+ metricName: component.name,
1727
+ isFormula: component.isFormula,
1728
+ }),
1729
+ }),
1676
1730
  );
1677
1731
  } else {
1678
1732
  cells.push("-");
@@ -1722,16 +1776,6 @@ ${contextBlock}
1722
1776
  return `${input.breachingCount} sample${input.breachingCount === 1 ? "" : "s"} breached the threshold.`;
1723
1777
  }
1724
1778
 
1725
- private static formatNumberForDisplay(value: number): string {
1726
- if (!Number.isFinite(value)) {
1727
- return String(value);
1728
- }
1729
- if (Number.isInteger(value)) {
1730
- return value.toString();
1731
- }
1732
- return Number(value.toFixed(2)).toString();
1733
- }
1734
-
1735
1779
  private static buildMetricExplorerDeepLink(input: {
1736
1780
  monitor: Monitor;
1737
1781
  ctx: MetricCriteriaContext;
@@ -1923,6 +1967,27 @@ ${contextBlock}
1923
1967
  };
1924
1968
  }
1925
1969
 
1970
+ /**
1971
+ * The metric name to hand a unit heuristic, or undefined when there
1972
+ * isn't one.
1973
+ *
1974
+ * `MetricCriteriaContext.metricName` and `MetricComponent.name` are the
1975
+ * FORMULA EXPRESSION when the criteria (or the component) targets a
1976
+ * formula — MetricMonitorCriteria.buildContext falls back to
1977
+ * `metricFormula`. The only thing a name is used for downstream is
1978
+ * ValueFormatter.isFractionMetric, which asks whether the name ends in
1979
+ * `.utilization` / `_ratio` / `_percent` and multiplies a "1"-unit value
1980
+ * by 100 when it does. A formula written as `a / b_ratio` ends in
1981
+ * `_ratio` and would silently be reported at 100× its real value, so a
1982
+ * formula gets no name at all.
1983
+ */
1984
+ private static metricNameForUnitHeuristics(input: {
1985
+ metricName: string | undefined;
1986
+ isFormula: boolean | undefined;
1987
+ }): string | undefined {
1988
+ return input.isFormula ? undefined : input.metricName;
1989
+ }
1990
+
1926
1991
  private static formatScopeDroppedHint(dropped: Array<string>): string {
1927
1992
  if (dropped.length === 0) {
1928
1993
  return "";
@@ -1931,6 +1996,61 @@ ${contextBlock}
1931
1996
  return ` _(scope partially carried — not applied: ${dropped.join(", ")})_`;
1932
1997
  }
1933
1998
 
1999
+ /**
2000
+ * The unit for a value in a per-platform "Affected Resources" table.
2001
+ *
2002
+ * These rows are NOT the same numbers as the criteria's breaching
2003
+ * samples: the worker collects them with a raw `MetricService.findBy`
2004
+ * scan and never runs them through MetricResultUnitConverter, so they
2005
+ * are in the metric's NATIVE unit while the criteria threshold was
2006
+ * compared in the user's chosen display unit. Labelling them with the
2007
+ * query's legendUnit would therefore attach a confidently wrong unit to
2008
+ * a correct number.
2009
+ *
2010
+ * The catalog is the right source precisely because it also declares
2011
+ * the native unit. It is the same lookup, on the same metric name, that
2012
+ * the worker already ran to resolve `metricFriendlyName`, so the unit
2013
+ * shown here can never disagree with the metric name shown beside it.
2014
+ */
2015
+ private static getPlatformMetricUnit(input: {
2016
+ platform: "kubernetes" | "proxmox" | "dockerSwarm" | "ceph";
2017
+ metricName: string;
2018
+ }): string | undefined {
2019
+ switch (input.platform) {
2020
+ case "kubernetes":
2021
+ return getKubernetesMetricByMetricName(input.metricName)?.unit;
2022
+ case "proxmox":
2023
+ return getProxmoxMetricByMetricName(input.metricName)?.unit;
2024
+ case "dockerSwarm":
2025
+ return getDockerSwarmMetricByMetricName(input.metricName)?.unit;
2026
+ case "ceph":
2027
+ return getCephMetricByMetricName(input.metricName)?.unit;
2028
+ default:
2029
+ return undefined;
2030
+ }
2031
+ }
2032
+
2033
+ /**
2034
+ * Render one cell of a platform breakdown table's Value column.
2035
+ *
2036
+ * Bold, because that is how these tables have always drawn the value —
2037
+ * it is the one number in the row a reader scans for.
2038
+ */
2039
+ private static formatPlatformResourceValue(input: {
2040
+ platform: "kubernetes" | "proxmox" | "dockerSwarm" | "ceph";
2041
+ metricName: string;
2042
+ value: number;
2043
+ }): string {
2044
+ return `**${MetricValueFormatter.format({
2045
+ value: input.value,
2046
+ unit: MonitorCriteriaEvaluator.getPlatformMetricUnit({
2047
+ platform: input.platform,
2048
+ metricName: input.metricName,
2049
+ }),
2050
+ metricName: input.metricName,
2051
+ })}**`;
2052
+ }
2053
+
1934
2054
  private static async buildKubernetesRootCauseContext(input: {
1935
2055
  dataToProcess: DataToProcess;
1936
2056
  monitorStep: MonitorStep;
@@ -2071,7 +2191,13 @@ ${contextBlock}
2071
2191
  cells.push(resource.nodeName ? `\`${resource.nodeName}\`` : "-");
2072
2192
  }
2073
2193
 
2074
- cells.push(`**${resource.metricValue}**`);
2194
+ cells.push(
2195
+ MonitorCriteriaEvaluator.formatPlatformResourceValue({
2196
+ platform: "kubernetes",
2197
+ metricName: breakdown.metricName,
2198
+ value: resource.metricValue,
2199
+ }),
2200
+ );
2075
2201
 
2076
2202
  resourceLines.push(`| ${cells.join(" | ")} |`);
2077
2203
  }
@@ -2478,7 +2604,13 @@ ${contextBlock}
2478
2604
  : "-";
2479
2605
 
2480
2606
  resourceLines.push(
2481
- `| ${resourceCell} | ${typeCell} | ${nodeCell} | **${resource.metricValue}** |`,
2607
+ `| ${resourceCell} | ${typeCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
2608
+ {
2609
+ platform: "proxmox",
2610
+ metricName: breakdown.metricName,
2611
+ value: resource.metricValue,
2612
+ },
2613
+ )} |`,
2482
2614
  );
2483
2615
  }
2484
2616
 
@@ -2640,7 +2772,13 @@ ${contextBlock}
2640
2772
  : "-";
2641
2773
 
2642
2774
  resourceLines.push(
2643
- `| ${taskCell} | ${serviceCell} | ${nodeCell} | **${resource.metricValue}** |`,
2775
+ `| ${taskCell} | ${serviceCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
2776
+ {
2777
+ platform: "dockerSwarm",
2778
+ metricName: breakdown.metricName,
2779
+ value: resource.metricValue,
2780
+ },
2781
+ )} |`,
2644
2782
  );
2645
2783
  }
2646
2784
 
@@ -2865,7 +3003,13 @@ ${contextBlock}
2865
3003
  : "-";
2866
3004
 
2867
3005
  resourceLines.push(
2868
- `| ${daemonCell} | ${poolCell} | ${hostCell} | **${resource.metricValue}** |`,
3006
+ `| ${daemonCell} | ${poolCell} | ${hostCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
3007
+ {
3008
+ platform: "ceph",
3009
+ metricName: breakdown.metricName,
3010
+ value: resource.metricValue,
3011
+ },
3012
+ )} |`,
2869
3013
  );
2870
3014
  }
2871
3015
 
@@ -2914,6 +3058,27 @@ ${contextBlock}
2914
3058
  const metricName: string = breakdown.metricName;
2915
3059
  const lines: Array<string> = [];
2916
3060
 
3061
+ /*
3062
+ * The top resource's value, in the unit the catalog says the metric
3063
+ * is actually reported in.
3064
+ *
3065
+ * The three "is at N%" sentences below used to hardcode that percent
3066
+ * sign. Two of the three metrics they fire for are not percentages at
3067
+ * all — k8s.node.memory.usage and k8s.node.filesystem.usage are
3068
+ * `bytes`, so a node holding 8 GiB reported "memory usage is at
3069
+ * 8589934592.0%" — and the third, k8s.node.cpu.utilization, is the
3070
+ * kubeletstats cores gauge that four other places in this repo warn
3071
+ * is misnamed, so 1.4 cores in use read as "1.4% CPU utilization".
3072
+ */
3073
+ const topResourceValue: string = MetricValueFormatter.format({
3074
+ value: topResource.metricValue,
3075
+ unit: MonitorCriteriaEvaluator.getPlatformMetricUnit({
3076
+ platform: "kubernetes",
3077
+ metricName: metricName,
3078
+ }),
3079
+ metricName: metricName,
3080
+ });
3081
+
2917
3082
  if (
2918
3083
  metricName === "k8s.container.restarts" ||
2919
3084
  metricName.includes("restart")
@@ -2923,7 +3088,7 @@ ${contextBlock}
2923
3088
  );
2924
3089
  if (topResource.containerName) {
2925
3090
  lines.push(
2926
- `The container \`${topResource.containerName}\` in pod \`${topResource.podName || "unknown"}\` has restarted **${topResource.metricValue}** times.`,
3091
+ `The container \`${topResource.containerName}\` in pod \`${topResource.podName || "unknown"}\` has restarted **${topResourceValue}** times.`,
2927
3092
  );
2928
3093
  }
2929
3094
  lines.push(
@@ -2959,7 +3124,7 @@ ${contextBlock}
2959
3124
  lines.push(`One or more nodes have transitioned to a NotReady state.`);
2960
3125
  if (topResource.nodeName) {
2961
3126
  lines.push(
2962
- `Node \`${topResource.nodeName}\` is reporting NotReady (value: ${topResource.metricValue}).`,
3127
+ `Node \`${topResource.nodeName}\` is reporting NotReady (value: ${topResourceValue}).`,
2963
3128
  );
2964
3129
  }
2965
3130
  lines.push(
@@ -2986,7 +3151,7 @@ ${contextBlock}
2986
3151
  lines.push(`Node CPU utilization has exceeded the configured threshold.`);
2987
3152
  if (topResource.nodeName) {
2988
3153
  lines.push(
2989
- `Node \`${topResource.nodeName}\` is at **${topResource.metricValue.toFixed(1)}%** CPU utilization.`,
3154
+ `Node \`${topResource.nodeName}\` is at **${topResourceValue}** CPU utilization.`,
2990
3155
  );
2991
3156
  }
2992
3157
  lines.push(
@@ -3007,7 +3172,7 @@ ${contextBlock}
3007
3172
  );
3008
3173
  if (topResource.nodeName) {
3009
3174
  lines.push(
3010
- `Node \`${topResource.nodeName}\` memory usage is at **${topResource.metricValue.toFixed(1)}%**.`,
3175
+ `Node \`${topResource.nodeName}\` memory usage is at **${topResourceValue}**.`,
3011
3176
  );
3012
3177
  }
3013
3178
  lines.push(
@@ -3025,7 +3190,7 @@ ${contextBlock}
3025
3190
  );
3026
3191
  if (topResource.workloadName) {
3027
3192
  lines.push(
3028
- `${topResource.workloadType || "Deployment"} \`${topResource.workloadName}\` has **${topResource.metricValue}** unavailable replica(s).`,
3193
+ `${topResource.workloadType || "Deployment"} \`${topResource.workloadName}\` has **${topResourceValue}** unavailable replica(s).`,
3029
3194
  );
3030
3195
  }
3031
3196
  lines.push(
@@ -3041,7 +3206,7 @@ ${contextBlock}
3041
3206
  lines.push(`Kubernetes Job has failed pods.`);
3042
3207
  if (topResource.workloadName) {
3043
3208
  lines.push(
3044
- `Job \`${topResource.workloadName}\` has **${topResource.metricValue}** failed pod(s).`,
3209
+ `Job \`${topResource.workloadName}\` has **${topResourceValue}** failed pod(s).`,
3045
3210
  );
3046
3211
  }
3047
3212
  lines.push(
@@ -3060,7 +3225,7 @@ ${contextBlock}
3060
3225
  );
3061
3226
  if (topResource.nodeName) {
3062
3227
  lines.push(
3063
- `Node \`${topResource.nodeName}\` filesystem usage is at **${topResource.metricValue.toFixed(1)}%**.`,
3228
+ `Node \`${topResource.nodeName}\` filesystem usage is at **${topResourceValue}**.`,
3064
3229
  );
3065
3230
  }
3066
3231
  lines.push(
@@ -3076,7 +3241,7 @@ ${contextBlock}
3076
3241
  lines.push(`DaemonSet has misscheduled or unavailable nodes.`);
3077
3242
  if (topResource.workloadName) {
3078
3243
  lines.push(
3079
- `DaemonSet \`${topResource.workloadName}\` has **${topResource.metricValue}** misscheduled node(s).`,
3244
+ `DaemonSet \`${topResource.workloadName}\` has **${topResourceValue}** misscheduled node(s).`,
3080
3245
  );
3081
3246
  }
3082
3247
  lines.push(
@@ -2,10 +2,21 @@ import {
2
2
  CriteriaFilter,
3
3
  FilterType,
4
4
  } from "../../../Types/Monitor/CriteriaFilter";
5
+ import MetricValueFormatter from "../../../Utils/Monitor/MetricValueFormatter";
6
+
7
+ /**
8
+ * How a criteria's values are displayed: the unit, and the metric name
9
+ * the formatter's fraction / epoch rules are keyed on.
10
+ */
11
+ export interface CriteriaDisplayOptions {
12
+ unit?: string | undefined;
13
+ metricName?: string | undefined;
14
+ }
5
15
 
6
16
  export default class MonitorCriteriaExpectationBuilder {
7
17
  public static getCriteriaFilterDescription(
8
18
  criteriaFilter: CriteriaFilter,
19
+ options?: CriteriaDisplayOptions,
9
20
  ): string {
10
21
  const parts: Array<string> = [criteriaFilter.checkOn];
11
22
 
@@ -14,15 +25,51 @@ export default class MonitorCriteriaExpectationBuilder {
14
25
  }
15
26
 
16
27
  if (criteriaFilter.value !== undefined && criteriaFilter.value !== null) {
17
- parts.push(String(criteriaFilter.value));
28
+ const formatted: string | number =
29
+ MonitorCriteriaExpectationBuilder.formatThreshold(
30
+ criteriaFilter.value,
31
+ options,
32
+ ) ?? criteriaFilter.value;
33
+
34
+ parts.push(String(formatted));
18
35
  }
19
36
 
20
37
  return parts.join(" ").trim();
21
38
  }
22
39
 
40
+ /*
41
+ * The threshold, rendered exactly as an observed value would be — or
42
+ * returned untouched when it cannot be.
43
+ *
44
+ * Untouched covers three cases, and each matters: no unit was resolved
45
+ * (every non-metric checkOn, whose unit lives in its CheckOn label), the
46
+ * threshold is a string (the Contains / StartsWith family, which must
47
+ * quote what the user typed), or it is not a finite number.
48
+ */
49
+ private static formatThreshold(
50
+ value: string | number | undefined,
51
+ options?: CriteriaDisplayOptions,
52
+ ): string | number | undefined {
53
+ if (!options?.unit) {
54
+ return value;
55
+ }
56
+
57
+ const numeric: number = Number(value);
58
+
59
+ if (typeof value === "boolean" || !Number.isFinite(numeric)) {
60
+ return value;
61
+ }
62
+
63
+ return MetricValueFormatter.format({
64
+ value: numeric,
65
+ unit: options.unit,
66
+ metricName: options.metricName,
67
+ });
68
+ }
69
+
23
70
  public static describeCriteriaExpectation(
24
71
  criteriaFilter: CriteriaFilter,
25
- options?: { unit?: string | undefined },
72
+ options?: CriteriaDisplayOptions,
26
73
  ): string | null {
27
74
  if (!criteriaFilter.filterType) {
28
75
  return null;
@@ -30,35 +77,61 @@ export default class MonitorCriteriaExpectationBuilder {
30
77
 
31
78
  let expectation: string;
32
79
 
80
+ /*
81
+ * Render the threshold the way the observed value is rendered, so the
82
+ * two halves of one sentence cannot describe one quantity differently.
83
+ * Reformatting only the observation produced "recorded latest 1.07 GB
84
+ * (expected to be greater than 1000000000 By)".
85
+ *
86
+ * A NON-numeric threshold falls back to the previous text verbatim —
87
+ * `criteriaFilter.value` is `string | number | undefined`, and the
88
+ * string filter types below must keep quoting exactly what the user
89
+ * typed. So must a threshold with no unit: `formatThreshold` is only
90
+ * installed when a unit was resolved, which happens for the two metric
91
+ * checkOns and nothing else.
92
+ */
33
93
  const value: string | number | undefined = criteriaFilter.value;
34
94
 
95
+ /*
96
+ * The threshold as the six value-comparison cases below should print
97
+ * it, unit included. Only those six get it: the string comparisons
98
+ * quote what the user typed, and the heartbeat windows carry their own
99
+ * "minutes" — formatting their value produced "within 5 sec minutes".
100
+ */
101
+ const comparedValue: string | number | undefined =
102
+ MonitorCriteriaExpectationBuilder.formatThreshold(value, options);
103
+
35
104
  /*
36
105
  * Suffix numeric-threshold comparisons with the metric's display unit
37
106
  * (e.g. "greater than 5 sec") so the threshold reads in the same unit
38
107
  * as the observed value. Only the value-comparison filter types below
39
108
  * carry a numeric threshold — the others (empty, boolean, heartbeat
40
109
  * windows, …) supply their own wording and get no suffix.
110
+ *
111
+ * When the value was formatted above it already carries its unit, so
112
+ * this suffix collapses to empty and the unit is not printed twice.
41
113
  */
42
- const unitSuffix: string = options?.unit ? ` ${options.unit}` : "";
114
+ const unitSuffix: string =
115
+ options?.unit && comparedValue === value ? ` ${options.unit}` : "";
43
116
 
44
117
  switch (criteriaFilter.filterType) {
45
118
  case FilterType.GreaterThan:
46
- expectation = `to be greater than ${value}${unitSuffix}`;
119
+ expectation = `to be greater than ${comparedValue}${unitSuffix}`;
47
120
  break;
48
121
  case FilterType.GreaterThanOrEqualTo:
49
- expectation = `to be greater than or equal to ${value}${unitSuffix}`;
122
+ expectation = `to be greater than or equal to ${comparedValue}${unitSuffix}`;
50
123
  break;
51
124
  case FilterType.LessThan:
52
- expectation = `to be less than ${value}${unitSuffix}`;
125
+ expectation = `to be less than ${comparedValue}${unitSuffix}`;
53
126
  break;
54
127
  case FilterType.LessThanOrEqualTo:
55
- expectation = `to be less than or equal to ${value}${unitSuffix}`;
128
+ expectation = `to be less than or equal to ${comparedValue}${unitSuffix}`;
56
129
  break;
57
130
  case FilterType.EqualTo:
58
- expectation = `to equal ${value}${unitSuffix}`;
131
+ expectation = `to equal ${comparedValue}${unitSuffix}`;
59
132
  break;
60
133
  case FilterType.NotEqualTo:
61
- expectation = `to not equal ${value}${unitSuffix}`;
134
+ expectation = `to not equal ${comparedValue}${unitSuffix}`;
62
135
  break;
63
136
  case FilterType.Contains:
64
137
  expectation = `to contain ${value}`;
@@ -59,17 +59,19 @@ export default class MonitorCriteriaMessageBuilder {
59
59
  * the observed value — "recorded latest 0.06 sec (expected to be greater
60
60
  * than 5 sec)" rather than the unitless "0.06 (expected ... 5)".
61
61
  */
62
- const metricDisplayUnit: string | undefined =
63
- MonitorCriteriaObservationBuilder.getMetricValueDisplayUnit({
64
- criteriaFilter: input.criteriaFilter,
65
- dataToProcess: input.dataToProcess,
66
- monitorStep: input.monitorStep,
67
- });
62
+ const metricDisplay: {
63
+ unit: string | undefined;
64
+ metricName: string | undefined;
65
+ } = MonitorCriteriaObservationBuilder.getMetricValueDisplayContext({
66
+ criteriaFilter: input.criteriaFilter,
67
+ dataToProcess: input.dataToProcess,
68
+ monitorStep: input.monitorStep,
69
+ });
68
70
 
69
71
  const expectation: string | null =
70
72
  MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(
71
73
  input.criteriaFilter,
72
- { unit: metricDisplayUnit },
74
+ metricDisplay,
73
75
  );
74
76
 
75
77
  const observation: string | null =
@@ -89,9 +91,16 @@ export default class MonitorCriteriaMessageBuilder {
89
91
  }
90
92
 
91
93
  if (expectation) {
94
+ /*
95
+ * Both halves of this sentence quote the same threshold, so both
96
+ * must render it the same way — otherwise it reads "Metric Value
97
+ * greater than 1000000000 did not satisfy the configured condition
98
+ * (expected to be greater than 1 GB)".
99
+ */
92
100
  const description: string =
93
101
  MonitorCriteriaExpectationBuilder.getCriteriaFilterDescription(
94
102
  input.criteriaFilter,
103
+ metricDisplay,
95
104
  );
96
105
 
97
106
  return `${description} did not satisfy the configured condition (${expectation}).`;
@@ -136,9 +136,24 @@ export default class MonitorCriteriaMessageFormatter {
136
136
  return null;
137
137
  }
138
138
 
139
+ /**
140
+ * "latest 1.07 GB (min 900 KB, max 1.07 GB) across 3 data points".
141
+ *
142
+ * `format`, when supplied, renders ONE value — unit included — and is
143
+ * applied to latest, min and max independently. That independence is the
144
+ * point: auto-scaling can land min and max on different rungs of the
145
+ * ladder, which a single trailing unit suffix cannot express. It is the
146
+ * same reason CompareCriteria puts the unit on each number rather than on
147
+ * the sentence, and using the same callback in both places is what stops
148
+ * the evaluation log and the alert email describing one sample two ways.
149
+ *
150
+ * Without it the `unit` string is appended as before, so the caller that
151
+ * passes no unit at all (execution time) stays byte-identical.
152
+ */
139
153
  public static summarizeNumericSeries(
140
154
  values: Array<number>,
141
155
  unit?: string | undefined,
156
+ format?: ((value: number) => string) | undefined,
142
157
  ): string | null {
143
158
  if (!values.length) {
144
159
  return null;
@@ -156,29 +171,26 @@ export default class MonitorCriteriaMessageFormatter {
156
171
  */
157
172
  const unitSuffix: string = unit ? ` ${unit}` : "";
158
173
 
159
- const latestFormatted: string | null =
160
- MonitorCriteriaMessageFormatter.formatNumber(latest, {
161
- maximumFractionDigits: 2,
162
- });
174
+ const renderValue: (value: number) => string = (value: number): string => {
175
+ if (format) {
176
+ return format(value);
177
+ }
178
+
179
+ const formatted: string | null =
180
+ MonitorCriteriaMessageFormatter.formatNumber(value, {
181
+ maximumFractionDigits: 2,
182
+ });
183
+
184
+ return `${formatted ?? value}${unitSuffix}`;
185
+ };
163
186
 
164
- let summary: string = `latest ${latestFormatted ?? latest}${unitSuffix}`;
187
+ let summary: string = `latest ${renderValue(latest)}`;
165
188
 
166
189
  if (values.length > 1) {
167
190
  const min: number = Math.min(...values);
168
191
  const max: number = Math.max(...values);
169
192
 
170
- const minFormatted: string | null =
171
- MonitorCriteriaMessageFormatter.formatNumber(min, {
172
- maximumFractionDigits: 2,
173
- });
174
- const maxFormatted: string | null =
175
- MonitorCriteriaMessageFormatter.formatNumber(max, {
176
- maximumFractionDigits: 2,
177
- });
178
-
179
- summary += ` (min ${minFormatted ?? min}${unitSuffix}, max ${
180
- maxFormatted ?? max
181
- }${unitSuffix})`;
193
+ summary += ` (min ${renderValue(min)}, max ${renderValue(max)})`;
182
194
  }
183
195
 
184
196
  summary += ` across ${values.length} data point${