@oneuptime/common 14.0.3 → 14.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (663) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  3. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  4. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  5. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  6. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  9. package/Models/DatabaseModels/Incident.ts +55 -0
  10. package/Models/DatabaseModels/Index.ts +16 -0
  11. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  13. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  14. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  18. package/Server/Middleware/MultipartFormData.ts +1 -1
  19. package/Server/Middleware/TelemetryIngest.ts +15 -8
  20. package/Server/Services/AlertReminderRuleService.ts +100 -24
  21. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  22. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  23. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  24. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  25. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  26. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  27. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  28. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  29. package/Server/Services/DatabaseServerService.ts +4546 -0
  30. package/Server/Services/DatabaseService.ts +14 -8
  31. package/Server/Services/DockerResourceService.ts +43 -2
  32. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  33. package/Server/Services/Index.ts +16 -0
  34. package/Server/Services/InventoryItemService.ts +113 -5
  35. package/Server/Services/IoTDeviceService.ts +145 -37
  36. package/Server/Services/MetricService.ts +1 -1
  37. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  38. package/Server/Services/PodmanResourceService.ts +43 -2
  39. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  40. package/Server/Services/ServiceService.ts +1 -1
  41. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  42. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  43. package/Server/Services/UserService.ts +14 -1
  44. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  45. package/Server/Types/Database/QueryHelper.ts +84 -2
  46. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  47. package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
  48. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  49. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  50. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  51. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
  52. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  53. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
  54. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  55. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  56. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  57. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  58. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  59. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  60. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  61. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  62. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  63. package/Server/Utils/StartServer.ts +26 -6
  64. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  65. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  66. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  67. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  68. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  69. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  70. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  71. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  72. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  73. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  74. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  75. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  76. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  77. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  78. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  79. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  80. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  81. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  82. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  83. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  84. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  85. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  86. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  87. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  88. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  89. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  90. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  91. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  92. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  93. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  94. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  95. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  96. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  97. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  98. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  99. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  100. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  101. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  102. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  103. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  104. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  105. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  106. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  107. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  108. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  109. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  110. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  111. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  112. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  113. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  114. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  115. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  116. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  117. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  118. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  119. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  120. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  121. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  122. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  123. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  124. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  125. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  126. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  127. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  128. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  129. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  130. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  131. package/Tests/ClassStrings.ts +178 -0
  132. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  133. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  134. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  135. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  136. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  137. package/Tests/RenderedMarkup.ts +518 -0
  138. package/Tests/ResponsiveVisibility.test.ts +490 -4
  139. package/Tests/ResponsiveVisibility.ts +505 -52
  140. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  141. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  142. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  143. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  144. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  145. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  146. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  147. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  148. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  149. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  150. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  151. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  152. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  153. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  154. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  155. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  156. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  157. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  158. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  159. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  160. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  161. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  162. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  163. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  164. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  165. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  166. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  167. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  168. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  169. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  170. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  171. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  172. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  173. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  174. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  175. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  176. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  177. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  178. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  179. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  180. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  181. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  182. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  183. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  184. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  185. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  186. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  187. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  188. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  189. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  190. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  191. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  192. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  193. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  194. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  195. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  196. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  197. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  198. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  199. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
  200. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  201. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  202. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  203. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  204. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  205. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  206. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  207. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  208. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  209. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  210. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  211. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  212. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  213. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  214. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  215. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  216. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  217. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  218. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  219. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  220. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  221. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  222. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  223. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  224. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  225. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  226. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  227. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  228. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  229. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  230. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  231. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  232. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  233. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  234. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  235. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  236. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  237. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  238. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  239. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  240. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  241. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  242. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
  243. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
  244. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  245. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  246. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  247. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  248. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  249. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  250. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
  251. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  252. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  253. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  254. package/Tests/UI/Components/Card.test.tsx +93 -2
  255. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  256. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  257. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  258. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  259. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  260. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  261. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  262. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  263. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  264. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  265. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  266. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  267. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  268. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  269. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  270. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  271. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  272. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  273. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  274. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
  275. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
  276. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  277. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  278. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  279. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  280. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  281. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  282. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  283. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  284. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  285. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  286. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  287. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  288. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  289. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  290. package/Tests/tsconfig.json +16 -0
  291. package/Types/AI/AIResourceContext.ts +7 -0
  292. package/Types/BaseDatabase/AggregationType.ts +7 -0
  293. package/Types/Dashboard/DashboardTemplates.ts +22 -8
  294. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  295. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  296. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  297. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  298. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  299. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  300. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  301. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  302. package/Types/Email/EmailTemplateType.ts +1 -0
  303. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  304. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  305. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  306. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  307. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  308. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  309. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  310. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  311. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  312. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  313. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  314. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  315. package/Types/Permission.ts +267 -0
  316. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  317. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  318. package/Types/Rules/RuleRun.ts +12 -0
  319. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  320. package/Types/Rum/WebVitals.ts +13 -0
  321. package/Types/Telemetry/EntityType.ts +23 -0
  322. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  323. package/Types/Telemetry/ServiceType.ts +8 -0
  324. package/UI/Components/Banner/Banner.tsx +1 -1
  325. package/UI/Components/Card/Card.tsx +1 -1
  326. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  327. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  328. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  329. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  330. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  331. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  332. package/UI/Components/Forms/BasicForm.tsx +1 -1
  333. package/UI/Components/Header/Header.tsx +1 -1
  334. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  335. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  336. package/UI/Components/Input/Input.tsx +11 -0
  337. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  338. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  339. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  340. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  341. package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
  342. package/UI/Components/Navbar/NavBar.tsx +9 -2
  343. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  344. package/UI/Components/Page/Page.tsx +1 -1
  345. package/UI/Components/Pagination/Pagination.tsx +3 -3
  346. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  347. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  348. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  349. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  350. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  351. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  352. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  353. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  354. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  355. package/UI/Styles/Theme.css +8 -1
  356. package/UI/Utils/Navigation.ts +89 -42
  357. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  358. package/UI/Utils/TelemetryService.ts +4 -3
  359. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  360. package/Utils/LabelRuleImportExport.ts +2 -0
  361. package/Utils/MetricUnitUtil.ts +103 -11
  362. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  363. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  364. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  365. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  366. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  367. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  368. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  369. package/Utils/Telemetry/EntityKey.ts +176 -0
  370. package/Utils/ValueFormatter.ts +16 -0
  371. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  372. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  373. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  374. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  375. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  376. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  377. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  378. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  379. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  380. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  381. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  382. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  383. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  384. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  385. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  386. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  387. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  388. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  389. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  390. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  391. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  393. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  395. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  397. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  398. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  399. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  400. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  401. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  402. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  403. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  404. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  405. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  406. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  407. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  408. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  409. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  410. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  411. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  412. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  413. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  414. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  415. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  416. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  417. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  418. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  419. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  420. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  421. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  422. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  423. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  424. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  425. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  426. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  427. package/build/dist/Server/Services/DatabaseService.js +8 -4
  428. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  429. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  430. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  431. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  432. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  433. package/build/dist/Server/Services/Index.js +16 -0
  434. package/build/dist/Server/Services/Index.js.map +1 -1
  435. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  436. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  437. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  438. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  439. package/build/dist/Server/Services/MetricService.js +1 -1
  440. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  441. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  442. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  443. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  444. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  445. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  446. package/build/dist/Server/Services/ServiceService.js +1 -1
  447. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  448. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  449. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  450. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  451. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  452. package/build/dist/Server/Services/UserService.js +13 -1
  453. package/build/dist/Server/Services/UserService.js.map +1 -1
  454. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  455. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  456. package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
  457. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  458. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  459. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  460. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
  461. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  462. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  463. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  464. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  465. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  466. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  467. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
  468. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  469. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  470. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  471. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  472. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  473. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  474. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  475. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  476. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  477. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  478. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  479. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  480. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  481. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  482. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  483. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  484. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  485. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  486. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  487. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  488. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  489. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  490. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  491. package/build/dist/Server/Utils/StartServer.js +20 -4
  492. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  493. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  494. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  495. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  496. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  497. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  498. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  499. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  500. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  501. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  502. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  503. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  504. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  505. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  506. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  507. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  508. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  509. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  510. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  512. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  513. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  514. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  515. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  516. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  517. package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
  518. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  519. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  520. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  521. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  522. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  523. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  524. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  525. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  526. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  527. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  528. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  529. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  530. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  531. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  532. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  533. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  534. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  535. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  536. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  537. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  538. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  539. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  540. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  541. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  542. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  543. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  544. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  545. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  546. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  547. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  548. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  549. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  550. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  551. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  552. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  553. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  554. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  555. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  556. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  557. package/build/dist/Types/Permission.js +234 -0
  558. package/build/dist/Types/Permission.js.map +1 -1
  559. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  560. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  561. package/build/dist/Types/Rules/RuleRun.js +4 -0
  562. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  563. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  564. package/build/dist/Types/Rum/WebVitals.js +12 -0
  565. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  566. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  567. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  568. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  569. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  570. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  571. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  572. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  573. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  574. package/build/dist/UI/Components/Card/Card.js +1 -1
  575. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  576. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  577. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  578. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  579. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  580. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  581. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  582. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  583. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  584. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  585. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  586. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  587. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  588. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  589. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  590. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  591. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  592. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  593. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  594. package/build/dist/UI/Components/Input/Input.js +1 -1
  595. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  596. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  597. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  598. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  600. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  601. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  602. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  603. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  604. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
  605. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  606. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  607. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  608. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  609. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  610. package/build/dist/UI/Components/Page/Page.js +1 -1
  611. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  612. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  613. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  614. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  615. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  616. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  617. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  618. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  619. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  620. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  621. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  622. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  626. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  627. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  628. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  629. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  630. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  631. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  632. package/build/dist/UI/Utils/Navigation.js +66 -33
  633. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  634. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  635. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  636. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  637. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  638. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  639. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  640. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  641. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  642. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  643. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  644. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  645. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  646. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  647. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  648. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  649. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  650. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  651. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  652. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  653. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  654. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  655. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  656. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  657. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  658. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  659. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  660. package/build/dist/Utils/ValueFormatter.js +15 -0
  661. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  662. package/package.json +1 -1
  663. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2125 @@
1
+ /*
2
+ * MonitorCriteriaEvaluator reaches the template renderer, which loads the
3
+ * native isolated-vm addon. Nothing here uses the sandbox and the
4
+ * prebuilt binary cannot always dlopen in the test environment.
5
+ */
6
+ jest.mock("isolated-vm", () => {
7
+ return {};
8
+ });
9
+
10
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
11
+ import MonitorCriteriaEvaluator from "../../../../Server/Utils/Monitor/MonitorCriteriaEvaluator";
12
+ import Dictionary from "../../../../Types/Dictionary";
13
+ import MetricFormulaConfigData from "../../../../Types/Metrics/MetricFormulaConfigData";
14
+ import MetricQueryConfigData from "../../../../Types/Metrics/MetricQueryConfigData";
15
+ import MetricsAggregationType from "../../../../Types/Metrics/MetricsAggregationType";
16
+ import MetricsViewConfig from "../../../../Types/Metrics/MetricsViewConfig";
17
+ import {
18
+ CephAlertTemplate,
19
+ CephAlertTemplateArgs,
20
+ getCephAlertTemplateById,
21
+ } from "../../../../Types/Monitor/CephAlertTemplates";
22
+ import { getCephMetricByMetricName } from "../../../../Types/Monitor/CephMetricCatalog";
23
+ import {
24
+ CheckOn,
25
+ CriteriaFilter,
26
+ FilterType,
27
+ } from "../../../../Types/Monitor/CriteriaFilter";
28
+ import {
29
+ DockerSwarmAlertTemplate,
30
+ DockerSwarmAlertTemplateArgs,
31
+ getDockerSwarmAlertTemplateById,
32
+ } from "../../../../Types/Monitor/DockerSwarmAlertTemplates";
33
+ import { getDockerSwarmMetricByMetricName } from "../../../../Types/Monitor/DockerSwarmMetricCatalog";
34
+ import {
35
+ buildKubernetesMonitorStep,
36
+ buildOfflineCriteriaInstance,
37
+ getKubernetesAlertTemplateById,
38
+ KubernetesAlertTemplate,
39
+ KubernetesAlertTemplateArgs,
40
+ } from "../../../../Types/Monitor/KubernetesAlertTemplates";
41
+ import { getKubernetesMetricByMetricName } from "../../../../Types/Monitor/KubernetesMetricCatalog";
42
+ import MetricMonitorResponse, {
43
+ CephAffectedResource,
44
+ CephResourceBreakdown,
45
+ DockerSwarmAffectedResource,
46
+ DockerSwarmResourceBreakdown,
47
+ KubernetesAffectedResource,
48
+ KubernetesResourceBreakdown,
49
+ ProxmoxAffectedResource,
50
+ ProxmoxResourceBreakdown,
51
+ VMwareAffectedResource,
52
+ VMwareResourceBreakdown,
53
+ } from "../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
54
+ import MonitorCriteriaInstance from "../../../../Types/Monitor/MonitorCriteriaInstance";
55
+ import MonitorStep from "../../../../Types/Monitor/MonitorStep";
56
+ import { KubernetesResourceScope } from "../../../../Types/Monitor/MonitorStepKubernetesMonitor";
57
+ import {
58
+ getProxmoxAlertTemplateById,
59
+ ProxmoxAlertTemplate,
60
+ ProxmoxAlertTemplateArgs,
61
+ } from "../../../../Types/Monitor/ProxmoxAlertTemplates";
62
+ import { getProxmoxMetricByMetricName } from "../../../../Types/Monitor/ProxmoxMetricCatalog";
63
+ import {
64
+ getVMwareAlertTemplateById,
65
+ VMwareAlertTemplate,
66
+ VMwareAlertTemplateArgs,
67
+ } from "../../../../Types/Monitor/VMwareAlertTemplates";
68
+ import { getVMwareMetricByMetricName } from "../../../../Types/Monitor/VMwareMetricCatalog";
69
+ import ObjectID from "../../../../Types/ObjectID";
70
+ import RollingTime from "../../../../Types/RollingTime/RollingTime";
71
+ import { describe, expect, test } from "@jest/globals";
72
+
73
+ /*
74
+ * The UNGROUPED half of the platform "Affected Resources" list.
75
+ *
76
+ * A grouped platform monitor lists the series its criteria matched. An
77
+ * ungrouped one has no series, so the list comes from the worker's raw
78
+ * datapoint scan — and the worker scans once PER QUERY. That is where the
79
+ * reported email went wrong: the platform breakdown was a single field
80
+ * each query's scan overwrote, so a ratio monitor listed its LAST query
81
+ * (the denominator, k8s.node.allocatable_memory) in raw bytes under a
82
+ * "> 85%" criteria, every node included.
83
+ *
84
+ * The worker now returns one scan per query, tagged with the query's
85
+ * alias (`*ResourceBreakdowns[].metricAlias`), plus the unit the scan's
86
+ * values are in. These tests drive the platform builders directly with
87
+ * perSeriesMatches omitted and pin what the raw-scan path renders:
88
+ *
89
+ * - only the scan taken for the criteria's own query is listed; a
90
+ * formula has no raw scan, so it gets no list at all, and a
91
+ * pre-tag (untagged, singular) breakdown still renders as it did;
92
+ * - values are formatted in the catalog's unit, else the scan's own
93
+ * unit — never a bare ten-digit byte count, never a cores gauge
94
+ * turned into a percentage;
95
+ * - rows are filtered and ordered by the matched criteria's direction:
96
+ * a criteria that fires when the metric FALLS lists the resources at
97
+ * their LOWEST sample, lowest first;
98
+ * - the "- Metric:" / "- Formula:" / "- Namespace:" detail lines say
99
+ * what the criteria compared, without "x (`x`)" duplication.
100
+ */
101
+
102
+ type BuilderInput = {
103
+ dataToProcess: MetricMonitorResponse;
104
+ monitorStep: MonitorStep;
105
+ monitor: Monitor;
106
+ criteriaInstance?: MonitorCriteriaInstance | undefined;
107
+ };
108
+
109
+ type EvaluatorPrivate = {
110
+ buildKubernetesRootCauseContext: (
111
+ input: BuilderInput,
112
+ ) => Promise<string | null>;
113
+ buildProxmoxRootCauseContext: (input: BuilderInput) => string | null;
114
+ buildVMwareRootCauseContext: (input: BuilderInput) => string | null;
115
+ buildDockerSwarmRootCauseContext: (input: BuilderInput) => string | null;
116
+ buildCephRootCauseContext: (input: BuilderInput) => string | null;
117
+ };
118
+
119
+ const Evaluator: EvaluatorPrivate =
120
+ MonitorCriteriaEvaluator as unknown as EvaluatorPrivate;
121
+
122
+ const CLUSTER: string = "oneuptime-test";
123
+ const VCENTER: string = "vcsa-prod";
124
+
125
+ /*
126
+ * A metric no platform catalog knows. The worker falls back to the metric
127
+ * name as its friendly name, and to the exporter-declared unit for its
128
+ * values — the path k8s.node.allocatable_memory took before it had a
129
+ * catalog entry.
130
+ */
131
+ const UNCATALOGED_METRIC: string = "k8s.custom.bytes_metric";
132
+
133
+ /*
134
+ * Nothing ten digits long may reach the email: that is a raw byte count
135
+ * the reader would have to divide in their head.
136
+ */
137
+ const RAW_NUMBER: RegExp = /\b\d{10,}\b/;
138
+
139
+ // ---------- catalog lookups (never hard-code a friendly name) ----------
140
+
141
+ function catalogName(input: {
142
+ friendlyName: string | undefined;
143
+ metricName: string;
144
+ }): string {
145
+ if (!input.friendlyName) {
146
+ throw new Error(`${input.metricName} has no catalog entry`);
147
+ }
148
+
149
+ return input.friendlyName;
150
+ }
151
+
152
+ function kubernetesName(metricName: string): string {
153
+ return catalogName({
154
+ friendlyName: getKubernetesMetricByMetricName(metricName)?.friendlyName,
155
+ metricName: metricName,
156
+ });
157
+ }
158
+
159
+ function proxmoxName(metricName: string): string {
160
+ return catalogName({
161
+ friendlyName: getProxmoxMetricByMetricName(metricName)?.friendlyName,
162
+ metricName: metricName,
163
+ });
164
+ }
165
+
166
+ function vmwareName(metricName: string): string {
167
+ return catalogName({
168
+ friendlyName: getVMwareMetricByMetricName(metricName)?.friendlyName,
169
+ metricName: metricName,
170
+ });
171
+ }
172
+
173
+ function dockerSwarmName(metricName: string): string {
174
+ return catalogName({
175
+ friendlyName: getDockerSwarmMetricByMetricName(metricName)?.friendlyName,
176
+ metricName: metricName,
177
+ });
178
+ }
179
+
180
+ function cephName(metricName: string): string {
181
+ return catalogName({
182
+ friendlyName: getCephMetricByMetricName(metricName)?.friendlyName,
183
+ metricName: metricName,
184
+ });
185
+ }
186
+
187
+ // ---------- hand-built monitor configs and criteria ----------
188
+
189
+ function queryConfig(input: {
190
+ alias: string;
191
+ metricName: string;
192
+ }): MetricQueryConfigData {
193
+ return {
194
+ metricAliasData: {
195
+ metricVariable: input.alias,
196
+ title: input.alias,
197
+ description: input.alias,
198
+ legend: input.alias,
199
+ legendUnit: undefined,
200
+ },
201
+ metricQueryData: {
202
+ filterData: {
203
+ metricName: input.metricName,
204
+ attributes: {},
205
+ aggegationType: MetricsAggregationType.Avg,
206
+ aggregateBy: {},
207
+ },
208
+ },
209
+ };
210
+ }
211
+
212
+ function formulaConfig(input: {
213
+ alias: string;
214
+ legend: string;
215
+ expression: string;
216
+ }): MetricFormulaConfigData {
217
+ return {
218
+ metricAliasData: {
219
+ metricVariable: input.alias,
220
+ title: input.legend,
221
+ description: input.legend,
222
+ legend: input.legend,
223
+ legendUnit: "%",
224
+ },
225
+ metricFormulaData: {
226
+ metricFormula: input.expression,
227
+ },
228
+ };
229
+ }
230
+
231
+ /*
232
+ * An incident-opening criteria on one alias, shaped exactly as the
233
+ * template builders shape theirs.
234
+ */
235
+ function criteriaOn(input: {
236
+ alias: string;
237
+ filterType: FilterType;
238
+ value: number;
239
+ }): MonitorCriteriaInstance {
240
+ return buildOfflineCriteriaInstance({
241
+ offlineMonitorStatusId: ObjectID.generate(),
242
+ incidentSeverityId: ObjectID.generate(),
243
+ alertSeverityId: ObjectID.generate(),
244
+ monitorName: "Raw Scan Monitor",
245
+ metricAlias: input.alias,
246
+ filterType: input.filterType,
247
+ value: input.value,
248
+ });
249
+ }
250
+
251
+ function kubernetesStep(input: {
252
+ metricViewConfig: MetricsViewConfig;
253
+ namespaceFilter?: string | undefined;
254
+ }): MonitorStep {
255
+ const alias: string =
256
+ input.metricViewConfig.queryConfigs[0]?.metricAliasData?.metricVariable ||
257
+ "query";
258
+
259
+ return buildKubernetesMonitorStep({
260
+ kubernetesMonitor: {
261
+ clusterIdentifier: CLUSTER,
262
+ resourceScope: KubernetesResourceScope.Cluster,
263
+ resourceFilters: input.namespaceFilter
264
+ ? { namespace: input.namespaceFilter }
265
+ : {},
266
+ metricViewConfig: input.metricViewConfig,
267
+ rollingTime: RollingTime.Past5Minutes,
268
+ },
269
+ offlineCriteriaInstance: criteriaOn({
270
+ alias: alias,
271
+ filterType: FilterType.GreaterThan,
272
+ value: 0,
273
+ }),
274
+ onlineCriteriaInstance: criteriaOn({
275
+ alias: alias,
276
+ filterType: FilterType.EqualTo,
277
+ value: 0,
278
+ }),
279
+ });
280
+ }
281
+
282
+ /*
283
+ * Swap a platform template's queries for a hand-built set, keeping the
284
+ * rest of the step (cluster identifier, resource filters) as shipped.
285
+ */
286
+ function withMetricViewConfig(
287
+ step: MonitorStep,
288
+ metricViewConfig: MetricsViewConfig,
289
+ ): MonitorStep {
290
+ const monitors: Array<{ metricViewConfig: MetricsViewConfig } | undefined> = [
291
+ step.data?.kubernetesMonitor,
292
+ step.data?.proxmoxMonitor,
293
+ step.data?.vmwareMonitor,
294
+ step.data?.dockerSwarmMonitor,
295
+ step.data?.cephMonitor,
296
+ ];
297
+
298
+ for (const monitor of monitors) {
299
+ if (monitor) {
300
+ monitor.metricViewConfig = metricViewConfig;
301
+ }
302
+ }
303
+
304
+ return step;
305
+ }
306
+
307
+ // ---------- real templates ----------
308
+
309
+ function templateArgs(): KubernetesAlertTemplateArgs &
310
+ ProxmoxAlertTemplateArgs &
311
+ DockerSwarmAlertTemplateArgs &
312
+ CephAlertTemplateArgs {
313
+ return {
314
+ clusterIdentifier: CLUSTER,
315
+ onlineMonitorStatusId: ObjectID.generate(),
316
+ offlineMonitorStatusId: ObjectID.generate(),
317
+ defaultIncidentSeverityId: ObjectID.generate(),
318
+ defaultAlertSeverityId: ObjectID.generate(),
319
+ monitorName: "Raw Scan Monitor",
320
+ };
321
+ }
322
+
323
+ function kubernetesTemplateStep(templateId: string): MonitorStep {
324
+ const template: KubernetesAlertTemplate | undefined =
325
+ getKubernetesAlertTemplateById(templateId);
326
+
327
+ if (!template) {
328
+ throw new Error(`${templateId} template missing`);
329
+ }
330
+
331
+ return template.getMonitorStep(templateArgs());
332
+ }
333
+
334
+ function proxmoxTemplateStep(templateId: string): MonitorStep {
335
+ const template: ProxmoxAlertTemplate | undefined =
336
+ getProxmoxAlertTemplateById(templateId);
337
+
338
+ if (!template) {
339
+ throw new Error(`${templateId} template missing`);
340
+ }
341
+
342
+ return template.getMonitorStep(templateArgs());
343
+ }
344
+
345
+ function vmwareTemplateStep(templateId: string): MonitorStep {
346
+ const template: VMwareAlertTemplate | undefined =
347
+ getVMwareAlertTemplateById(templateId);
348
+
349
+ if (!template) {
350
+ throw new Error(`${templateId} template missing`);
351
+ }
352
+
353
+ const args: VMwareAlertTemplateArgs = {
354
+ vcenterIdentifier: VCENTER,
355
+ onlineMonitorStatusId: ObjectID.generate(),
356
+ offlineMonitorStatusId: ObjectID.generate(),
357
+ defaultIncidentSeverityId: ObjectID.generate(),
358
+ defaultAlertSeverityId: ObjectID.generate(),
359
+ monitorName: "Raw Scan Monitor",
360
+ };
361
+
362
+ return template.getMonitorStep(args);
363
+ }
364
+
365
+ function dockerSwarmTemplateStep(templateId: string): MonitorStep {
366
+ const template: DockerSwarmAlertTemplate | undefined =
367
+ getDockerSwarmAlertTemplateById(templateId);
368
+
369
+ if (!template) {
370
+ throw new Error(`${templateId} template missing`);
371
+ }
372
+
373
+ return template.getMonitorStep(templateArgs());
374
+ }
375
+
376
+ function cephTemplateStep(templateId: string): MonitorStep {
377
+ const template: CephAlertTemplate | undefined =
378
+ getCephAlertTemplateById(templateId);
379
+
380
+ if (!template) {
381
+ throw new Error(`${templateId} template missing`);
382
+ }
383
+
384
+ return template.getMonitorStep(templateArgs());
385
+ }
386
+
387
+ /** The template's FIRING criteria — the one that opens the incident. */
388
+ function firingCriteria(step: MonitorStep): MonitorCriteriaInstance {
389
+ const instance: MonitorCriteriaInstance | undefined =
390
+ step.data?.monitorCriteria?.data?.monitorCriteriaInstanceArray[0];
391
+
392
+ if (!instance) {
393
+ throw new Error("firing criteria instance missing");
394
+ }
395
+
396
+ return instance;
397
+ }
398
+
399
+ /** The metric-value filter of a criteria, for fixture guards. */
400
+ function metricFilter(criteria: MonitorCriteriaInstance): CriteriaFilter {
401
+ const filter: CriteriaFilter | undefined = (
402
+ criteria.data?.filters || []
403
+ ).find((f: CriteriaFilter) => {
404
+ return f.checkOn === CheckOn.MetricValue;
405
+ });
406
+
407
+ if (!filter) {
408
+ throw new Error("metric-value filter missing");
409
+ }
410
+
411
+ return filter;
412
+ }
413
+
414
+ /** The step's first query, as the worker tags its scan. */
415
+ function firstQuery(step: MonitorStep): { alias: string; metricName: string } {
416
+ const query: MetricQueryConfigData | undefined =
417
+ MonitorStep.getMetricsViewConfig(step)?.queryConfigs[0];
418
+
419
+ const alias: string = query?.metricAliasData?.metricVariable || "";
420
+ const metricName: string =
421
+ (query?.metricQueryData?.filterData?.metricName as string | undefined) ||
422
+ "";
423
+
424
+ if (!alias || !metricName) {
425
+ throw new Error("template has no aliased query");
426
+ }
427
+
428
+ return { alias: alias, metricName: metricName };
429
+ }
430
+
431
+ // ---------- worker-shaped responses ----------
432
+
433
+ function metricResponse(
434
+ overrides: Partial<MetricMonitorResponse>,
435
+ ): MetricMonitorResponse {
436
+ return {
437
+ projectId: ObjectID.generate(),
438
+ monitorId: ObjectID.generate(),
439
+ metricResult: [],
440
+ metricViewConfig: { queryConfigs: [], formulaConfigs: [] },
441
+ ...overrides,
442
+ };
443
+ }
444
+
445
+ /*
446
+ * One Kubernetes raw scan the way MonitorTelemetryMonitor builds it: the
447
+ * catalog's friendly name else the metric name, the query's attribute
448
+ * filters under their stored `resource.`-prefixed keys, and the query's
449
+ * alias as the tag.
450
+ */
451
+ function kubernetesScan(input: {
452
+ alias?: string | undefined;
453
+ metricName: string;
454
+ metricUnit?: string | undefined;
455
+ attributes?: Dictionary<string> | undefined;
456
+ resources: Array<KubernetesAffectedResource>;
457
+ }): KubernetesResourceBreakdown {
458
+ return {
459
+ clusterName: CLUSTER,
460
+ metricName: input.metricName,
461
+ metricFriendlyName:
462
+ getKubernetesMetricByMetricName(input.metricName)?.friendlyName ||
463
+ input.metricName,
464
+ metricAlias: input.alias,
465
+ metricUnit: input.metricUnit,
466
+ attributes: input.attributes || {
467
+ "resource.k8s.cluster.name": CLUSTER,
468
+ },
469
+ affectedResources: input.resources,
470
+ };
471
+ }
472
+
473
+ async function kubernetesContext(input: {
474
+ step: MonitorStep;
475
+ response: MetricMonitorResponse;
476
+ criteria?: MonitorCriteriaInstance | undefined;
477
+ }): Promise<string | null> {
478
+ return Evaluator.buildKubernetesRootCauseContext({
479
+ dataToProcess: input.response,
480
+ monitorStep: input.step,
481
+ monitor: new Monitor(),
482
+ criteriaInstance: input.criteria,
483
+ });
484
+ }
485
+
486
+ /*
487
+ * ============================================================
488
+ * 1. Which scan is listed (Kubernetes)
489
+ * ============================================================
490
+ *
491
+ * The shape of the reported monitor, hand-built: used ÷ allocatable × 100
492
+ * with NO group-by, so the evaluator has only the raw scans to go on.
493
+ * The allocatable query is an uncataloged metric reported in "By", so its
494
+ * values can only be read in the unit the worker recorded on the scan.
495
+ */
496
+
497
+ const NODE_1: string = "gke-pool-1-1nd7";
498
+ const NODE_2: string = "gke-pool-1-7qrq";
499
+
500
+ const USED_BYTES: Dictionary<number> = {
501
+ [NODE_1]: 228943289000, // 228.94 GB → "229 GB"
502
+ [NODE_2]: 99800000000, // 99.8 GB
503
+ };
504
+
505
+ const ALLOCATABLE_BYTES: Dictionary<number> = {
506
+ [NODE_1]: 257760964608, // 257.76 GB → "258 GB"
507
+ [NODE_2]: 191483559936, // 191.48 GB → "191 GB"
508
+ };
509
+
510
+ function memoryRatioConfig(): MetricsViewConfig {
511
+ return {
512
+ queryConfigs: [
513
+ queryConfig({ alias: "used_mem", metricName: "k8s.node.memory.usage" }),
514
+ queryConfig({ alias: "alloc_mem", metricName: UNCATALOGED_METRIC }),
515
+ ],
516
+ formulaConfigs: [
517
+ formulaConfig({
518
+ alias: "mem_pct",
519
+ legend: "Node Memory Utilization (%)",
520
+ expression: "(used_mem / alloc_mem) * 100",
521
+ }),
522
+ ],
523
+ };
524
+ }
525
+
526
+ function nodeRows(
527
+ values: Dictionary<number>,
528
+ ): Array<KubernetesAffectedResource> {
529
+ return Object.keys(values).map(
530
+ (nodeName: string): KubernetesAffectedResource => {
531
+ return {
532
+ nodeName: nodeName,
533
+ metricValue: values[nodeName]!,
534
+ lowestMetricValue: values[nodeName]!,
535
+ };
536
+ },
537
+ );
538
+ }
539
+
540
+ function usedScan(alias: string | undefined): KubernetesResourceBreakdown {
541
+ return kubernetesScan({
542
+ alias: alias,
543
+ metricName: "k8s.node.memory.usage",
544
+ metricUnit: "By",
545
+ resources: nodeRows(USED_BYTES),
546
+ });
547
+ }
548
+
549
+ function allocatableScan(
550
+ alias: string | undefined,
551
+ ): KubernetesResourceBreakdown {
552
+ return kubernetesScan({
553
+ alias: alias,
554
+ metricName: UNCATALOGED_METRIC,
555
+ metricUnit: "By",
556
+ resources: nodeRows(ALLOCATABLE_BYTES),
557
+ });
558
+ }
559
+
560
+ const ALLOCATABLE_LIST: string = [
561
+ "**Affected Resources** (2 total)",
562
+ "",
563
+ `1. **Node** \`${NODE_1}\` — **258 GB**`,
564
+ `2. **Node** \`${NODE_2}\` — **191 GB**`,
565
+ ].join("\n");
566
+
567
+ describe("Kubernetes raw scan: the criteria's own query is the one listed", () => {
568
+ test("fixture guard: the uncataloged metric really has no catalog entry", () => {
569
+ expect(getKubernetesMetricByMetricName(UNCATALOGED_METRIC)).toBeUndefined();
570
+ });
571
+
572
+ test("a criteria on `alloc_mem` lists the alloc_mem scan, in the scan's unit", async () => {
573
+ const context: string | null = await kubernetesContext({
574
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
575
+ response: metricResponse({
576
+ kubernetesResourceBreakdowns: [
577
+ usedScan("used_mem"),
578
+ allocatableScan("alloc_mem"),
579
+ ],
580
+ }),
581
+ criteria: criteriaOn({
582
+ alias: "alloc_mem",
583
+ filterType: FilterType.GreaterThan,
584
+ value: 100,
585
+ }),
586
+ });
587
+
588
+ const text: string = context || "";
589
+
590
+ expect(text).toContain(ALLOCATABLE_LIST);
591
+
592
+ // None of the sibling (used_mem) scan's numbers.
593
+ expect(text).not.toContain("229 GB");
594
+ expect(text).not.toContain("99.8 GB");
595
+
596
+ // "258 GB", never the 257760964608 it used to print.
597
+ expect(text).not.toMatch(RAW_NUMBER);
598
+
599
+ expect(text).toContain(`Most affected node: \`${NODE_1}\` (**258 GB**)`);
600
+ });
601
+
602
+ test("a criteria on `used_mem` lists the used_mem scan instead", async () => {
603
+ const context: string | null = await kubernetesContext({
604
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
605
+ response: metricResponse({
606
+ kubernetesResourceBreakdowns: [
607
+ usedScan("used_mem"),
608
+ allocatableScan("alloc_mem"),
609
+ ],
610
+ }),
611
+ criteria: criteriaOn({
612
+ alias: "used_mem",
613
+ filterType: FilterType.GreaterThan,
614
+ value: 100,
615
+ }),
616
+ });
617
+
618
+ const text: string = context || "";
619
+
620
+ expect(text).toContain(
621
+ [
622
+ "**Affected Resources** (2 total)",
623
+ "",
624
+ `1. **Node** \`${NODE_1}\` — **229 GB**`,
625
+ `2. **Node** \`${NODE_2}\` — **99.8 GB**`,
626
+ ].join("\n"),
627
+ );
628
+ expect(text).toContain(
629
+ `- Metric: ${kubernetesName("k8s.node.memory.usage")} (\`k8s.node.memory.usage\`)`,
630
+ );
631
+ expect(text).toContain(`Node \`${NODE_1}\` memory usage is at **229 GB**.`);
632
+ expect(text).not.toContain("258 GB");
633
+ expect(text).not.toMatch(RAW_NUMBER);
634
+ });
635
+
636
+ test("the alias tag is matched case-insensitively", async () => {
637
+ const context: string | null = await kubernetesContext({
638
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
639
+ response: metricResponse({
640
+ kubernetesResourceBreakdowns: [
641
+ usedScan("USED_MEM"),
642
+ allocatableScan("ALLOC_MEM"),
643
+ ],
644
+ }),
645
+ criteria: criteriaOn({
646
+ alias: "alloc_mem",
647
+ filterType: FilterType.GreaterThan,
648
+ value: 100,
649
+ }),
650
+ });
651
+
652
+ expect(context || "").toContain(ALLOCATABLE_LIST);
653
+ });
654
+
655
+ test("a criteria on the FORMULA lists no scan at all — neither operand is the ratio", async () => {
656
+ const context: string | null = await kubernetesContext({
657
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
658
+ response: metricResponse({
659
+ kubernetesResourceBreakdowns: [
660
+ usedScan("used_mem"),
661
+ allocatableScan("alloc_mem"),
662
+ ],
663
+ }),
664
+ criteria: criteriaOn({
665
+ alias: "mem_pct",
666
+ filterType: FilterType.GreaterThan,
667
+ value: 85,
668
+ }),
669
+ });
670
+
671
+ const text: string = context || "";
672
+
673
+ expect(text).not.toContain("**Affected Resources**");
674
+ expect(text).not.toContain(NODE_1);
675
+ expect(text).not.toContain(NODE_2);
676
+ expect(text).not.toContain(" GB");
677
+ expect(text).not.toMatch(RAW_NUMBER);
678
+ });
679
+
680
+ test("a criteria on the FORMULA still names the formula and its operands", async () => {
681
+ /*
682
+ * With no list to show, the details block is all the reader gets —
683
+ * the other four platforms keep it for a formula target.
684
+ */
685
+ const context: string | null = await kubernetesContext({
686
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
687
+ response: metricResponse({
688
+ kubernetesResourceBreakdowns: [
689
+ usedScan("used_mem"),
690
+ allocatableScan("alloc_mem"),
691
+ ],
692
+ }),
693
+ criteria: criteriaOn({
694
+ alias: "mem_pct",
695
+ filterType: FilterType.GreaterThan,
696
+ value: 85,
697
+ }),
698
+ });
699
+
700
+ expect(context).toContain(
701
+ [
702
+ "**Kubernetes Cluster Details**",
703
+ `- Cluster: ${CLUSTER}`,
704
+ "- Metric: Node Memory Utilization (%)",
705
+ "- Formula: `(used_mem / alloc_mem) * 100`",
706
+ ` - \`used_mem\` = ${kubernetesName("k8s.node.memory.usage")} (\`k8s.node.memory.usage\`)`,
707
+ ` - \`alloc_mem\` = \`${UNCATALOGED_METRIC}\``,
708
+ ].join("\n"),
709
+ );
710
+ expect(context).not.toContain("**Affected Resources**");
711
+ });
712
+
713
+ test("tagged scans whose alias matches no query are not borrowed", async () => {
714
+ const context: string | null = await kubernetesContext({
715
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
716
+ response: metricResponse({
717
+ kubernetesResourceBreakdowns: [
718
+ usedScan("stale_used"),
719
+ allocatableScan("stale_alloc"),
720
+ ],
721
+ }),
722
+ criteria: criteriaOn({
723
+ alias: "alloc_mem",
724
+ filterType: FilterType.GreaterThan,
725
+ value: 100,
726
+ }),
727
+ });
728
+
729
+ const text: string = context || "";
730
+
731
+ expect(text).not.toContain("**Affected Resources**");
732
+ expect(text).not.toContain("258 GB");
733
+ expect(text).not.toContain("229 GB");
734
+ });
735
+
736
+ test("a legacy untagged singular breakdown still renders for a query target", async () => {
737
+ const context: string | null = await kubernetesContext({
738
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
739
+ response: metricResponse({
740
+ kubernetesResourceBreakdown: allocatableScan(undefined),
741
+ }),
742
+ criteria: criteriaOn({
743
+ alias: "alloc_mem",
744
+ filterType: FilterType.GreaterThan,
745
+ value: 100,
746
+ }),
747
+ });
748
+
749
+ const text: string = context || "";
750
+
751
+ expect(text).toContain(`- Metric: \`${UNCATALOGED_METRIC}\``);
752
+ expect(text).toContain(ALLOCATABLE_LIST);
753
+ expect(text).not.toMatch(RAW_NUMBER);
754
+ });
755
+
756
+ test("a legacy untagged singular breakdown is NOT trusted for a formula target", async () => {
757
+ const context: string | null = await kubernetesContext({
758
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
759
+ response: metricResponse({
760
+ kubernetesResourceBreakdown: allocatableScan(undefined),
761
+ }),
762
+ criteria: criteriaOn({
763
+ alias: "mem_pct",
764
+ filterType: FilterType.GreaterThan,
765
+ value: 85,
766
+ }),
767
+ });
768
+
769
+ const text: string = context || "";
770
+
771
+ expect(text).not.toContain("**Affected Resources**");
772
+ expect(text).not.toContain("258 GB");
773
+ });
774
+
775
+ test("with no criteria at all, the legacy singular breakdown renders as before", async () => {
776
+ const context: string | null = await kubernetesContext({
777
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
778
+ response: metricResponse({
779
+ kubernetesResourceBreakdown: allocatableScan(undefined),
780
+ }),
781
+ });
782
+
783
+ const text: string = context || "";
784
+
785
+ expect(text).toContain(
786
+ [
787
+ "**Kubernetes Cluster Details**",
788
+ `- Cluster: ${CLUSTER}`,
789
+ `- Metric: \`${UNCATALOGED_METRIC}\``,
790
+ ].join("\n"),
791
+ );
792
+ expect(text).toContain(ALLOCATABLE_LIST);
793
+ });
794
+
795
+ test("with no criteria, a single tagged scan is used but two are not guessed between", async () => {
796
+ const single: string | null = await kubernetesContext({
797
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
798
+ response: metricResponse({
799
+ kubernetesResourceBreakdowns: [allocatableScan("alloc_mem")],
800
+ }),
801
+ });
802
+
803
+ expect(single || "").toContain(ALLOCATABLE_LIST);
804
+
805
+ const ambiguous: string | null = await kubernetesContext({
806
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
807
+ response: metricResponse({
808
+ kubernetesResourceBreakdowns: [
809
+ usedScan("used_mem"),
810
+ allocatableScan("alloc_mem"),
811
+ ],
812
+ }),
813
+ });
814
+
815
+ expect(ambiguous || "").not.toContain("**Affected Resources**");
816
+ });
817
+ });
818
+
819
+ /*
820
+ * ============================================================
821
+ * 1b. Which scan is listed (Proxmox, VMware, Docker Swarm, Ceph)
822
+ * ============================================================
823
+ *
824
+ * The same selection on every other platform: two tagged scans and a
825
+ * formula over them. A criteria on one query lists that query's scan; a
826
+ * criteria on the formula lists nothing but still says what was compared.
827
+ */
828
+
829
+ type PlatformSelectionCase = {
830
+ platform: string;
831
+ listHeading: string;
832
+ render: (criteriaInstance: MonitorCriteriaInstance) => string | null;
833
+ queryAlias: string;
834
+ expectedQueryList: Array<string>;
835
+ siblingValue: string;
836
+ formulaAlias: string;
837
+ expectedFormulaLines: Array<string>;
838
+ resourceName: string;
839
+ };
840
+
841
+ function proxmoxSelectionCase(): PlatformSelectionCase {
842
+ const guest: Omit<ProxmoxAffectedResource, "metricValue"> = {
843
+ resourceId: "qemu/100",
844
+ resourceName: "web-vm",
845
+ resourceType: "qemu",
846
+ scope: "guest",
847
+ nodeName: "pve1",
848
+ };
849
+
850
+ const scan: (input: {
851
+ alias: string;
852
+ metricName: string;
853
+ value: number;
854
+ }) => ProxmoxResourceBreakdown = (input: {
855
+ alias: string;
856
+ metricName: string;
857
+ value: number;
858
+ }): ProxmoxResourceBreakdown => {
859
+ return {
860
+ clusterName: CLUSTER,
861
+ metricName: input.metricName,
862
+ metricFriendlyName: proxmoxName(input.metricName),
863
+ metricAlias: input.alias,
864
+ metricUnit: "bytes",
865
+ attributes: {},
866
+ affectedResources: [
867
+ { ...guest, metricValue: input.value, lowestMetricValue: input.value },
868
+ ],
869
+ };
870
+ };
871
+
872
+ const step: MonitorStep = withMetricViewConfig(
873
+ proxmoxTemplateStep("pve-node-offline"),
874
+ {
875
+ queryConfigs: [
876
+ queryConfig({
877
+ alias: "mem_used",
878
+ metricName: "pve_memory_usage_bytes",
879
+ }),
880
+ queryConfig({ alias: "mem_size", metricName: "pve_memory_size_bytes" }),
881
+ ],
882
+ formulaConfigs: [
883
+ formulaConfig({
884
+ alias: "mem_pct",
885
+ legend: "Guest Memory Used (%)",
886
+ expression: "(mem_used / mem_size) * 100",
887
+ }),
888
+ ],
889
+ },
890
+ );
891
+
892
+ const response: MetricMonitorResponse = metricResponse({
893
+ proxmoxResourceBreakdowns: [
894
+ scan({
895
+ alias: "mem_used",
896
+ metricName: "pve_memory_usage_bytes",
897
+ value: 4000000000,
898
+ }),
899
+ scan({
900
+ alias: "mem_size",
901
+ metricName: "pve_memory_size_bytes",
902
+ value: 8000000000,
903
+ }),
904
+ ],
905
+ });
906
+
907
+ return {
908
+ platform: "Proxmox",
909
+ listHeading: "**Affected Resources**",
910
+ render: (criteriaInstance: MonitorCriteriaInstance): string | null => {
911
+ return Evaluator.buildProxmoxRootCauseContext({
912
+ dataToProcess: response,
913
+ monitorStep: step,
914
+ monitor: new Monitor(),
915
+ criteriaInstance: criteriaInstance,
916
+ });
917
+ },
918
+ queryAlias: "mem_size",
919
+ expectedQueryList: [
920
+ "**Affected Resources** (1 total)",
921
+ "",
922
+ "1. **Virtual Machine** `web-vm` (`qemu/100`) — **8 GB**",
923
+ " - Node: `pve1`",
924
+ ],
925
+ siblingValue: "4 GB",
926
+ formulaAlias: "mem_pct",
927
+ expectedFormulaLines: [
928
+ "- Metric: Guest Memory Used (%)",
929
+ "- Formula: `(mem_used / mem_size) * 100`",
930
+ ` - \`mem_used\` = ${proxmoxName("pve_memory_usage_bytes")} (\`pve_memory_usage_bytes\`)`,
931
+ ` - \`mem_size\` = ${proxmoxName("pve_memory_size_bytes")} (\`pve_memory_size_bytes\`)`,
932
+ ],
933
+ resourceName: "web-vm",
934
+ };
935
+ }
936
+
937
+ function vmwareSelectionCase(): PlatformSelectionCase {
938
+ const host: Omit<VMwareAffectedResource, "metricValue"> = {
939
+ datacenterName: "dc1",
940
+ clusterName: "prod-cluster",
941
+ hostName: "esx-1",
942
+ };
943
+
944
+ const scan: (input: {
945
+ alias: string;
946
+ metricName: string;
947
+ value: number;
948
+ }) => VMwareResourceBreakdown = (input: {
949
+ alias: string;
950
+ metricName: string;
951
+ value: number;
952
+ }): VMwareResourceBreakdown => {
953
+ return {
954
+ vcenterName: VCENTER,
955
+ metricName: input.metricName,
956
+ metricFriendlyName: vmwareName(input.metricName),
957
+ metricAlias: input.alias,
958
+ metricUnit: "%",
959
+ attributes: {},
960
+ affectedResources: [
961
+ { ...host, metricValue: input.value, lowestMetricValue: input.value },
962
+ ],
963
+ };
964
+ };
965
+
966
+ const step: MonitorStep = withMetricViewConfig(
967
+ vmwareTemplateStep("vmware-host-cpu-saturation"),
968
+ {
969
+ queryConfigs: [
970
+ queryConfig({
971
+ alias: "cpu_util",
972
+ metricName: "vcenter.host.cpu.utilization",
973
+ }),
974
+ queryConfig({
975
+ alias: "mem_util",
976
+ metricName: "vcenter.host.memory.utilization",
977
+ }),
978
+ ],
979
+ formulaConfigs: [
980
+ formulaConfig({
981
+ alias: "host_pressure",
982
+ legend: "Host Pressure (%)",
983
+ expression: "(cpu_util + mem_util) / 2",
984
+ }),
985
+ ],
986
+ },
987
+ );
988
+
989
+ const response: MetricMonitorResponse = metricResponse({
990
+ vmwareResourceBreakdowns: [
991
+ scan({
992
+ alias: "cpu_util",
993
+ metricName: "vcenter.host.cpu.utilization",
994
+ value: 92.5,
995
+ }),
996
+ scan({
997
+ alias: "mem_util",
998
+ metricName: "vcenter.host.memory.utilization",
999
+ value: 71.25,
1000
+ }),
1001
+ ],
1002
+ });
1003
+
1004
+ return {
1005
+ platform: "VMware",
1006
+ listHeading: "**Affected Resources**",
1007
+ render: (criteriaInstance: MonitorCriteriaInstance): string | null => {
1008
+ return Evaluator.buildVMwareRootCauseContext({
1009
+ dataToProcess: response,
1010
+ monitorStep: step,
1011
+ monitor: new Monitor(),
1012
+ criteriaInstance: criteriaInstance,
1013
+ });
1014
+ },
1015
+ queryAlias: "mem_util",
1016
+ expectedQueryList: [
1017
+ "**Affected Resources** (1 total)",
1018
+ "",
1019
+ "1. **Host** `esx-1` — **71.25%**",
1020
+ " - Cluster: `prod-cluster`",
1021
+ ],
1022
+ siblingValue: "92.50%",
1023
+ formulaAlias: "host_pressure",
1024
+ expectedFormulaLines: [
1025
+ "- Metric: Host Pressure (%)",
1026
+ "- Formula: `(cpu_util + mem_util) / 2`",
1027
+ ` - \`cpu_util\` = ${vmwareName("vcenter.host.cpu.utilization")} (\`vcenter.host.cpu.utilization\`)`,
1028
+ ` - \`mem_util\` = ${vmwareName("vcenter.host.memory.utilization")} (\`vcenter.host.memory.utilization\`)`,
1029
+ ],
1030
+ resourceName: "esx-1",
1031
+ };
1032
+ }
1033
+
1034
+ function dockerSwarmSelectionCase(): PlatformSelectionCase {
1035
+ const task: Omit<DockerSwarmAffectedResource, "metricValue"> = {
1036
+ containerName: "web.1.abc",
1037
+ serviceName: "web",
1038
+ nodeName: "swarm-1",
1039
+ };
1040
+
1041
+ const scan: (input: {
1042
+ alias: string;
1043
+ metricName: string;
1044
+ value: number;
1045
+ }) => DockerSwarmResourceBreakdown = (input: {
1046
+ alias: string;
1047
+ metricName: string;
1048
+ value: number;
1049
+ }): DockerSwarmResourceBreakdown => {
1050
+ return {
1051
+ clusterName: CLUSTER,
1052
+ metricName: input.metricName,
1053
+ metricFriendlyName: dockerSwarmName(input.metricName),
1054
+ metricAlias: input.alias,
1055
+ metricUnit: "%",
1056
+ attributes: {},
1057
+ affectedResources: [
1058
+ { ...task, metricValue: input.value, lowestMetricValue: input.value },
1059
+ ],
1060
+ };
1061
+ };
1062
+
1063
+ const step: MonitorStep = withMetricViewConfig(
1064
+ dockerSwarmTemplateStep("docker-swarm-task-down"),
1065
+ {
1066
+ queryConfigs: [
1067
+ queryConfig({ alias: "cpu", metricName: "container.cpu.utilization" }),
1068
+ queryConfig({
1069
+ alias: "mem_pct",
1070
+ metricName: "container.memory.percent",
1071
+ }),
1072
+ ],
1073
+ formulaConfigs: [
1074
+ formulaConfig({
1075
+ alias: "pressure",
1076
+ legend: "Task Pressure (%)",
1077
+ expression: "(cpu + mem_pct) / 2",
1078
+ }),
1079
+ ],
1080
+ },
1081
+ );
1082
+
1083
+ const response: MetricMonitorResponse = metricResponse({
1084
+ dockerSwarmResourceBreakdowns: [
1085
+ scan({
1086
+ alias: "cpu",
1087
+ metricName: "container.cpu.utilization",
1088
+ value: 40,
1089
+ }),
1090
+ scan({
1091
+ alias: "mem_pct",
1092
+ metricName: "container.memory.percent",
1093
+ value: 88.5,
1094
+ }),
1095
+ ],
1096
+ });
1097
+
1098
+ return {
1099
+ platform: "Docker Swarm",
1100
+ listHeading: "**Affected Tasks**",
1101
+ render: (criteriaInstance: MonitorCriteriaInstance): string | null => {
1102
+ return Evaluator.buildDockerSwarmRootCauseContext({
1103
+ dataToProcess: response,
1104
+ monitorStep: step,
1105
+ monitor: new Monitor(),
1106
+ criteriaInstance: criteriaInstance,
1107
+ });
1108
+ },
1109
+ queryAlias: "mem_pct",
1110
+ expectedQueryList: [
1111
+ "**Affected Tasks** (1 total)",
1112
+ "",
1113
+ "1. **Task** `web.1.abc` — **88.50%**",
1114
+ " - Service: `web`",
1115
+ " - Node: `swarm-1`",
1116
+ ],
1117
+ siblingValue: "40.00%",
1118
+ formulaAlias: "pressure",
1119
+ expectedFormulaLines: [
1120
+ "- Metric: Task Pressure (%)",
1121
+ "- Formula: `(cpu + mem_pct) / 2`",
1122
+ ` - \`cpu\` = ${dockerSwarmName("container.cpu.utilization")} (\`container.cpu.utilization\`)`,
1123
+ ` - \`mem_pct\` = ${dockerSwarmName("container.memory.percent")} (\`container.memory.percent\`)`,
1124
+ ],
1125
+ resourceName: "web.1.abc",
1126
+ };
1127
+ }
1128
+
1129
+ function cephSelectionCase(): PlatformSelectionCase {
1130
+ const osd: Omit<CephAffectedResource, "metricValue"> = {
1131
+ daemon: "osd.3",
1132
+ hostname: "ceph-1",
1133
+ };
1134
+
1135
+ const scan: (input: {
1136
+ alias: string;
1137
+ metricName: string;
1138
+ value: number;
1139
+ }) => CephResourceBreakdown = (input: {
1140
+ alias: string;
1141
+ metricName: string;
1142
+ value: number;
1143
+ }): CephResourceBreakdown => {
1144
+ return {
1145
+ clusterName: CLUSTER,
1146
+ metricName: input.metricName,
1147
+ metricFriendlyName: cephName(input.metricName),
1148
+ metricAlias: input.alias,
1149
+ metricUnit: "bytes",
1150
+ attributes: {},
1151
+ affectedResources: [
1152
+ { ...osd, metricValue: input.value, lowestMetricValue: input.value },
1153
+ ],
1154
+ };
1155
+ };
1156
+
1157
+ const step: MonitorStep = withMetricViewConfig(
1158
+ cephTemplateStep("ceph-osd-down"),
1159
+ {
1160
+ queryConfigs: [
1161
+ queryConfig({
1162
+ alias: "osd_used",
1163
+ metricName: "ceph_osd_stat_bytes_used",
1164
+ }),
1165
+ queryConfig({ alias: "osd_total", metricName: "ceph_osd_stat_bytes" }),
1166
+ ],
1167
+ formulaConfigs: [
1168
+ formulaConfig({
1169
+ alias: "osd_fill",
1170
+ legend: "OSD Fill (%)",
1171
+ expression: "(osd_used / osd_total) * 100",
1172
+ }),
1173
+ ],
1174
+ },
1175
+ );
1176
+
1177
+ const response: MetricMonitorResponse = metricResponse({
1178
+ cephResourceBreakdowns: [
1179
+ scan({
1180
+ alias: "osd_used",
1181
+ metricName: "ceph_osd_stat_bytes_used",
1182
+ value: 1500000000000,
1183
+ }),
1184
+ scan({
1185
+ alias: "osd_total",
1186
+ metricName: "ceph_osd_stat_bytes",
1187
+ value: 4000000000000,
1188
+ }),
1189
+ ],
1190
+ });
1191
+
1192
+ return {
1193
+ platform: "Ceph",
1194
+ listHeading: "**Affected Resources**",
1195
+ render: (criteriaInstance: MonitorCriteriaInstance): string | null => {
1196
+ return Evaluator.buildCephRootCauseContext({
1197
+ dataToProcess: response,
1198
+ monitorStep: step,
1199
+ monitor: new Monitor(),
1200
+ criteriaInstance: criteriaInstance,
1201
+ });
1202
+ },
1203
+ queryAlias: "osd_used",
1204
+ expectedQueryList: [
1205
+ "**Affected Resources** (1 total)",
1206
+ "",
1207
+ "1. **Daemon** `osd.3` — **1.5 TB**",
1208
+ " - Host: `ceph-1`",
1209
+ ],
1210
+ siblingValue: "4 TB",
1211
+ formulaAlias: "osd_fill",
1212
+ expectedFormulaLines: [
1213
+ "- Metric: OSD Fill (%)",
1214
+ "- Formula: `(osd_used / osd_total) * 100`",
1215
+ ` - \`osd_used\` = ${cephName("ceph_osd_stat_bytes_used")} (\`ceph_osd_stat_bytes_used\`)`,
1216
+ ` - \`osd_total\` = ${cephName("ceph_osd_stat_bytes")} (\`ceph_osd_stat_bytes\`)`,
1217
+ ],
1218
+ resourceName: "osd.3",
1219
+ };
1220
+ }
1221
+
1222
+ describe("Platform raw scan: the criteria's own query is the one listed", () => {
1223
+ const cases: Array<PlatformSelectionCase> = [
1224
+ proxmoxSelectionCase(),
1225
+ vmwareSelectionCase(),
1226
+ dockerSwarmSelectionCase(),
1227
+ cephSelectionCase(),
1228
+ ];
1229
+
1230
+ cases.forEach((platformCase: PlatformSelectionCase) => {
1231
+ describe(platformCase.platform, () => {
1232
+ test("a criteria on one query lists that query's scan, not its sibling's", () => {
1233
+ const text: string =
1234
+ platformCase.render(
1235
+ criteriaOn({
1236
+ alias: platformCase.queryAlias,
1237
+ filterType: FilterType.GreaterThan,
1238
+ value: 1,
1239
+ }),
1240
+ ) || "";
1241
+
1242
+ expect(text).toContain(platformCase.expectedQueryList.join("\n"));
1243
+ expect(text).not.toContain(platformCase.siblingValue);
1244
+ expect(text).not.toMatch(RAW_NUMBER);
1245
+ });
1246
+
1247
+ test("a criteria on the formula lists nothing but names the formula", () => {
1248
+ const text: string =
1249
+ platformCase.render(
1250
+ criteriaOn({
1251
+ alias: platformCase.formulaAlias,
1252
+ filterType: FilterType.GreaterThan,
1253
+ value: 85,
1254
+ }),
1255
+ ) || "";
1256
+
1257
+ expect(text).toContain(platformCase.expectedFormulaLines.join("\n"));
1258
+ expect(text).not.toContain(platformCase.listHeading);
1259
+ expect(text).not.toContain(platformCase.resourceName);
1260
+ expect(text).not.toMatch(RAW_NUMBER);
1261
+ });
1262
+ });
1263
+ });
1264
+ });
1265
+
1266
+ /*
1267
+ * ============================================================
1268
+ * 2. Unit fallback: the catalog's unit beats the scan's
1269
+ * ============================================================
1270
+ */
1271
+
1272
+ describe("Platform raw scan: the catalog unit wins over the scan's declared unit", () => {
1273
+ test("k8s.node.cpu.utilization is a cores gauge: 1.4 reads '1.4 cores', never '140.00%'", async () => {
1274
+ /*
1275
+ * kubeletstats declares the metric "1", which on a `.utilization`
1276
+ * name would read as a fraction and render ×100. The catalog knows
1277
+ * it is cores.
1278
+ */
1279
+ const metricName: string = "k8s.node.cpu.utilization";
1280
+
1281
+ expect(getKubernetesMetricByMetricName(metricName)?.unit).toBe("cores");
1282
+
1283
+ const context: string | null = await kubernetesContext({
1284
+ step: kubernetesStep({
1285
+ metricViewConfig: {
1286
+ queryConfigs: [
1287
+ queryConfig({ alias: "node_cpu", metricName: metricName }),
1288
+ ],
1289
+ formulaConfigs: [],
1290
+ },
1291
+ }),
1292
+ response: metricResponse({
1293
+ kubernetesResourceBreakdowns: [
1294
+ kubernetesScan({
1295
+ alias: "node_cpu",
1296
+ metricName: metricName,
1297
+ metricUnit: "1",
1298
+ resources: [
1299
+ { nodeName: "node-a", metricValue: 0.6, lowestMetricValue: 0.2 },
1300
+ { nodeName: "node-b", metricValue: 1.4, lowestMetricValue: 1.1 },
1301
+ ],
1302
+ }),
1303
+ ],
1304
+ }),
1305
+ criteria: criteriaOn({
1306
+ alias: "node_cpu",
1307
+ filterType: FilterType.GreaterThan,
1308
+ value: 1,
1309
+ }),
1310
+ });
1311
+
1312
+ const text: string = context || "";
1313
+
1314
+ /*
1315
+ * Only node-b is past the "> 1" threshold; node-a, at 0.6 cores, is
1316
+ * a healthy node and is not listed under the alert.
1317
+ */
1318
+ expect(text).toContain("**Affected Resources** (1 total)");
1319
+ expect(text).toContain("1. **Node** `node-b` — **1.4 cores**");
1320
+ expect(text).not.toContain("node-a");
1321
+ expect(text).toContain(
1322
+ `- Metric: ${kubernetesName(metricName)} (\`${metricName}\`)`,
1323
+ );
1324
+ expect(text).toContain(
1325
+ "Node `node-b` is at **1.4 cores** CPU utilization.",
1326
+ );
1327
+ expect(text).not.toContain("140.00%");
1328
+ expect(text).not.toContain("%");
1329
+ });
1330
+
1331
+ test("VMware host CPU is already 0–100: 92.5 reads '92.50%', never '9250.00%'", () => {
1332
+ const step: MonitorStep = vmwareTemplateStep("vmware-host-cpu-saturation");
1333
+ const query: { alias: string; metricName: string } = firstQuery(step);
1334
+
1335
+ expect(getVMwareMetricByMetricName(query.metricName)?.unit).toBe("%");
1336
+
1337
+ const context: string | null = Evaluator.buildVMwareRootCauseContext({
1338
+ dataToProcess: metricResponse({
1339
+ vmwareResourceBreakdowns: [
1340
+ {
1341
+ vcenterName: VCENTER,
1342
+ metricName: query.metricName,
1343
+ metricFriendlyName: vmwareName(query.metricName),
1344
+ metricAlias: query.alias,
1345
+ // What the receiver might declare; the catalog must win.
1346
+ metricUnit: "1",
1347
+ attributes: {},
1348
+ affectedResources: [
1349
+ {
1350
+ hostName: "esx-1",
1351
+ clusterName: "prod-cluster",
1352
+ datacenterName: "dc1",
1353
+ metricValue: 92.5,
1354
+ lowestMetricValue: 91,
1355
+ },
1356
+ {
1357
+ hostName: "esx-2",
1358
+ clusterName: "prod-cluster",
1359
+ datacenterName: "dc1",
1360
+ metricValue: 95.25,
1361
+ lowestMetricValue: 93,
1362
+ },
1363
+ {
1364
+ hostName: "esx-3",
1365
+ clusterName: "prod-cluster",
1366
+ datacenterName: "dc1",
1367
+ metricValue: 0,
1368
+ lowestMetricValue: 0,
1369
+ },
1370
+ ],
1371
+ },
1372
+ ],
1373
+ }),
1374
+ monitorStep: step,
1375
+ monitor: new Monitor(),
1376
+ criteriaInstance: firingCriteria(step),
1377
+ });
1378
+
1379
+ const text: string = context || "";
1380
+
1381
+ expect(text).toContain(
1382
+ [
1383
+ "**Affected Resources** (2 total)",
1384
+ "",
1385
+ "1. **Host** `esx-2` — **95.25%**",
1386
+ " - Cluster: `prod-cluster`",
1387
+ "2. **Host** `esx-1` — **92.50%**",
1388
+ " - Cluster: `prod-cluster`",
1389
+ ].join("\n"),
1390
+ );
1391
+ expect(text).toContain(`- vCenter: ${VCENTER}`);
1392
+ expect(text).toContain(
1393
+ `- Metric: ${vmwareName(query.metricName)} (\`${query.metricName}\`)`,
1394
+ );
1395
+ expect(text).not.toContain("esx-3");
1396
+ expect(text).not.toContain("9250");
1397
+ expect(text).not.toContain("9525");
1398
+ });
1399
+ });
1400
+
1401
+ /*
1402
+ * ============================================================
1403
+ * 3. The ungrouped Kubernetes templates
1404
+ * ============================================================
1405
+ *
1406
+ * Four shipped templates are deliberately NOT grouped — each is one
1407
+ * cluster-wide number — so their incidents can only ever list the raw
1408
+ * scan. Each is driven with its real step and firing criteria, and a
1409
+ * scan tagged with the template's own alias.
1410
+ */
1411
+
1412
+ describe("Ungrouped Kubernetes templates: rows and direction from the raw scan", () => {
1413
+ test("k8s-etcd-no-leader (= 0) lists the leaderless member at its LOWEST sample", async () => {
1414
+ const step: MonitorStep = kubernetesTemplateStep("k8s-etcd-no-leader");
1415
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1416
+ const query: { alias: string; metricName: string } = firstQuery(step);
1417
+
1418
+ // Fixture guard: an incident-opening `= 0` on an ungrouped query.
1419
+ expect(metricFilter(criteria).filterType).toBe(FilterType.EqualTo);
1420
+ expect(metricFilter(criteria).value).toBe(0);
1421
+ expect(criteria.data?.createIncidents).toBe(true);
1422
+ expect(MonitorStep.getGroupByAttributeKeys(step)).toEqual([]);
1423
+
1424
+ const context: string | null = await kubernetesContext({
1425
+ step: step,
1426
+ criteria: criteria,
1427
+ response: metricResponse({
1428
+ kubernetesResourceBreakdowns: [
1429
+ kubernetesScan({
1430
+ alias: query.alias,
1431
+ metricName: query.metricName,
1432
+ resources: [
1433
+ {
1434
+ podName: "etcd-cp-2",
1435
+ namespace: "kube-system",
1436
+ nodeName: "cp-2",
1437
+ metricValue: 1,
1438
+ lowestMetricValue: 1,
1439
+ },
1440
+ /*
1441
+ * Lost its leader for one scrape: the highest sample is 1,
1442
+ * the Min-aggregated criteria fired on the 0.
1443
+ */
1444
+ {
1445
+ podName: "etcd-cp-1",
1446
+ namespace: "kube-system",
1447
+ nodeName: "cp-1",
1448
+ metricValue: 1,
1449
+ lowestMetricValue: 0,
1450
+ },
1451
+ {
1452
+ podName: "etcd-cp-3",
1453
+ namespace: "kube-system",
1454
+ nodeName: "cp-3",
1455
+ metricValue: 1,
1456
+ lowestMetricValue: 1,
1457
+ },
1458
+ ],
1459
+ }),
1460
+ ],
1461
+ }),
1462
+ });
1463
+
1464
+ const text: string = context || "";
1465
+
1466
+ expect(text).toContain(
1467
+ [
1468
+ "**Affected Resources** (1 total)",
1469
+ "",
1470
+ "1. **Pod** `etcd-cp-1` — **0**",
1471
+ " - Namespace: `kube-system`",
1472
+ " - Node: `cp-1`",
1473
+ ].join("\n"),
1474
+ );
1475
+ expect(text).toContain(
1476
+ `- Metric: ${kubernetesName(query.metricName)} (\`${query.metricName}\`)`,
1477
+ );
1478
+ expect(text).toContain("Most affected pod: `etcd-cp-1` (**0**)");
1479
+ expect(text).not.toContain("etcd-cp-2");
1480
+ expect(text).not.toContain("etcd-cp-3");
1481
+ });
1482
+
1483
+ test("k8s-apiserver-throttling (>= 200) lists the busiest API server first, idle ones dropped", async () => {
1484
+ const step: MonitorStep = kubernetesTemplateStep(
1485
+ "k8s-apiserver-throttling",
1486
+ );
1487
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1488
+ const query: { alias: string; metricName: string } = firstQuery(step);
1489
+
1490
+ expect(metricFilter(criteria).filterType).toBe(
1491
+ FilterType.GreaterThanOrEqualTo,
1492
+ );
1493
+ expect(MonitorStep.getGroupByAttributeKeys(step)).toEqual([]);
1494
+
1495
+ const context: string | null = await kubernetesContext({
1496
+ step: step,
1497
+ criteria: criteria,
1498
+ response: metricResponse({
1499
+ kubernetesResourceBreakdowns: [
1500
+ kubernetesScan({
1501
+ alias: query.alias,
1502
+ metricName: query.metricName,
1503
+ resources: [
1504
+ {
1505
+ podName: "kube-apiserver-cp-2",
1506
+ namespace: "kube-system",
1507
+ metricValue: 210,
1508
+ lowestMetricValue: 205,
1509
+ },
1510
+ {
1511
+ podName: "kube-apiserver-cp-1",
1512
+ namespace: "kube-system",
1513
+ metricValue: 250,
1514
+ lowestMetricValue: 190,
1515
+ },
1516
+ // Just restarted: nothing in flight.
1517
+ {
1518
+ podName: "kube-apiserver-cp-3",
1519
+ namespace: "kube-system",
1520
+ metricValue: 0,
1521
+ lowestMetricValue: 0,
1522
+ },
1523
+ ],
1524
+ }),
1525
+ ],
1526
+ }),
1527
+ });
1528
+
1529
+ const text: string = context || "";
1530
+
1531
+ /*
1532
+ * An upward criteria reads each server's HIGHEST sample; the catalog
1533
+ * calls it a "count", which is left bare rather than printed as
1534
+ * "250 count".
1535
+ */
1536
+ expect(text).toContain(
1537
+ [
1538
+ "**Affected Resources** (2 total)",
1539
+ "",
1540
+ "1. **Pod** `kube-apiserver-cp-1` — **250**",
1541
+ " - Namespace: `kube-system`",
1542
+ "2. **Pod** `kube-apiserver-cp-2` — **210**",
1543
+ " - Namespace: `kube-system`",
1544
+ ].join("\n"),
1545
+ );
1546
+ expect(text).toContain(
1547
+ `Kubernetes metric \`${query.metricName}\` (${kubernetesName(query.metricName)}) has breached the configured threshold.`,
1548
+ );
1549
+ expect(text).toContain(
1550
+ "Most affected pod: `kube-apiserver-cp-1` (**250**)",
1551
+ );
1552
+ expect(text).not.toContain("kube-apiserver-cp-3");
1553
+ expect(text).not.toContain("count");
1554
+ });
1555
+
1556
+ test("k8s-apiserver-throttling: a scrape with no Kubernetes identity is titled by the cluster", async () => {
1557
+ const step: MonitorStep = kubernetesTemplateStep(
1558
+ "k8s-apiserver-throttling",
1559
+ );
1560
+ const query: { alias: string; metricName: string } = firstQuery(step);
1561
+
1562
+ const context: string | null = await kubernetesContext({
1563
+ step: step,
1564
+ criteria: firingCriteria(step),
1565
+ response: metricResponse({
1566
+ kubernetesResourceBreakdowns: [
1567
+ kubernetesScan({
1568
+ alias: query.alias,
1569
+ metricName: query.metricName,
1570
+ resources: [{ metricValue: 250, lowestMetricValue: 190 }],
1571
+ }),
1572
+ ],
1573
+ }),
1574
+ });
1575
+
1576
+ expect(context).toContain(
1577
+ [
1578
+ "**Affected Resources** (1 total)",
1579
+ "",
1580
+ `1. **Cluster** \`${CLUSTER}\` — **250**`,
1581
+ ].join("\n"),
1582
+ );
1583
+ });
1584
+
1585
+ test("k8s-scheduler-backlog (> 0) lists the scheduler with a queue at its highest sample", async () => {
1586
+ const step: MonitorStep = kubernetesTemplateStep("k8s-scheduler-backlog");
1587
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1588
+ const query: { alias: string; metricName: string } = firstQuery(step);
1589
+
1590
+ expect(metricFilter(criteria).filterType).toBe(FilterType.GreaterThan);
1591
+ expect(metricFilter(criteria).value).toBe(0);
1592
+ expect(MonitorStep.getGroupByAttributeKeys(step)).toEqual([]);
1593
+
1594
+ const context: string | null = await kubernetesContext({
1595
+ step: step,
1596
+ criteria: criteria,
1597
+ response: metricResponse({
1598
+ kubernetesResourceBreakdowns: [
1599
+ kubernetesScan({
1600
+ alias: query.alias,
1601
+ metricName: query.metricName,
1602
+ resources: [
1603
+ // The standby scheduler holds no queue.
1604
+ {
1605
+ podName: "kube-scheduler-cp-2",
1606
+ namespace: "kube-system",
1607
+ metricValue: 0,
1608
+ lowestMetricValue: 0,
1609
+ },
1610
+ {
1611
+ podName: "kube-scheduler-cp-1",
1612
+ namespace: "kube-system",
1613
+ metricValue: 3,
1614
+ lowestMetricValue: 0,
1615
+ },
1616
+ ],
1617
+ }),
1618
+ ],
1619
+ }),
1620
+ });
1621
+
1622
+ const text: string = context || "";
1623
+
1624
+ expect(text).toContain(
1625
+ [
1626
+ "**Affected Resources** (1 total)",
1627
+ "",
1628
+ "1. **Pod** `kube-scheduler-cp-1` — **3**",
1629
+ " - Namespace: `kube-system`",
1630
+ ].join("\n"),
1631
+ );
1632
+ expect(text).toContain(
1633
+ `- Metric: ${kubernetesName(query.metricName)} (\`${query.metricName}\`)`,
1634
+ );
1635
+ expect(text).toContain("Most affected pod: `kube-scheduler-cp-1` (**3**)");
1636
+ expect(text).not.toContain("kube-scheduler-cp-2");
1637
+ });
1638
+
1639
+ describe("k8s-pod-pending (phase = 1)", () => {
1640
+ /*
1641
+ * k8s.pod.phase carries the phase in its VALUE: 1 = Pending,
1642
+ * 2 = Running, 3 = Succeeded, 4 = Failed, 5 = Unknown. The template
1643
+ * fires when the cluster-wide Min is exactly 1.
1644
+ */
1645
+ function podPendingContext(): Promise<string | null> {
1646
+ const step: MonitorStep = kubernetesTemplateStep("k8s-pod-pending");
1647
+ const query: { alias: string; metricName: string } = firstQuery(step);
1648
+
1649
+ return kubernetesContext({
1650
+ step: step,
1651
+ criteria: firingCriteria(step),
1652
+ response: metricResponse({
1653
+ kubernetesResourceBreakdowns: [
1654
+ kubernetesScan({
1655
+ alias: query.alias,
1656
+ metricName: query.metricName,
1657
+ resources: [
1658
+ {
1659
+ podName: "checkout-7d9f-running",
1660
+ namespace: "shop",
1661
+ metricValue: 2,
1662
+ lowestMetricValue: 2,
1663
+ },
1664
+ {
1665
+ podName: "checkout-7d9f-pending",
1666
+ namespace: "shop",
1667
+ metricValue: 1,
1668
+ lowestMetricValue: 1,
1669
+ },
1670
+ {
1671
+ podName: "report-job-failed",
1672
+ namespace: "batch",
1673
+ metricValue: 4,
1674
+ lowestMetricValue: 4,
1675
+ },
1676
+ {
1677
+ podName: "lost-pod",
1678
+ namespace: "shop",
1679
+ metricValue: 5,
1680
+ lowestMetricValue: 5,
1681
+ },
1682
+ ],
1683
+ }),
1684
+ ],
1685
+ }),
1686
+ });
1687
+ }
1688
+
1689
+ test("fixture guard: an incident-opening `= 1` on an ungrouped k8s.pod.phase query", () => {
1690
+ const step: MonitorStep = kubernetesTemplateStep("k8s-pod-pending");
1691
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1692
+
1693
+ expect(firstQuery(step).metricName).toBe("k8s.pod.phase");
1694
+ expect(metricFilter(criteria).filterType).toBe(FilterType.EqualTo);
1695
+ expect(metricFilter(criteria).value).toBe(1);
1696
+ expect(criteria.data?.createIncidents).toBe(true);
1697
+ expect(MonitorStep.getGroupByAttributeKeys(step)).toEqual([]);
1698
+ });
1699
+
1700
+ test("names the metric and takes the Pending analysis branch", async () => {
1701
+ const text: string = (await podPendingContext()) || "";
1702
+
1703
+ expect(text).toContain(
1704
+ `- Metric: ${kubernetesName("k8s.pod.phase")} (\`k8s.pod.phase\`)`,
1705
+ );
1706
+ expect(text).toContain(
1707
+ "Pods are stuck in Pending phase and unable to be scheduled.",
1708
+ );
1709
+ });
1710
+
1711
+ test("lists only the Pending pod, and the analysis sends the reader to it", async () => {
1712
+ /*
1713
+ * The criteria compared the phase code for EQUALITY with 1. A
1714
+ * Running (2), Failed (4) or Unknown (5) pod did not breach it, and
1715
+ * ranking them "highest first" puts the Unknown pod at the top and
1716
+ * into the `kubectl describe pod` the analysis recommends for
1717
+ * scheduling events.
1718
+ */
1719
+ const text: string = (await podPendingContext()) || "";
1720
+
1721
+ expect(text).toContain(
1722
+ [
1723
+ "**Affected Resources** (1 total)",
1724
+ "",
1725
+ "1. **Pod** `checkout-7d9f-pending` — **1**",
1726
+ " - Namespace: `shop`",
1727
+ ].join("\n"),
1728
+ );
1729
+ expect(text).toContain("kubectl describe pod checkout-7d9f-pending");
1730
+ expect(text).not.toContain("checkout-7d9f-running");
1731
+ expect(text).not.toContain("report-job-failed");
1732
+ expect(text).not.toContain("lost-pod");
1733
+ });
1734
+ });
1735
+ });
1736
+
1737
+ /*
1738
+ * ============================================================
1739
+ * 4. The other platforms' fall criteria on the raw scan
1740
+ * ============================================================
1741
+ */
1742
+
1743
+ describe("Platform templates: fall criteria list the resources that fell", () => {
1744
+ function proxmoxNodes(): Array<ProxmoxAffectedResource> {
1745
+ return [
1746
+ // Went down for one scrape: highest 1, lowest 0.
1747
+ {
1748
+ resourceId: "node/pve1",
1749
+ resourceType: "node",
1750
+ scope: "node",
1751
+ metricValue: 1,
1752
+ lowestMetricValue: 0,
1753
+ },
1754
+ {
1755
+ resourceId: "node/pve2",
1756
+ resourceType: "node",
1757
+ scope: "node",
1758
+ metricValue: 1,
1759
+ lowestMetricValue: 1,
1760
+ },
1761
+ {
1762
+ resourceId: "node/pve3",
1763
+ resourceType: "node",
1764
+ scope: "node",
1765
+ metricValue: 0,
1766
+ lowestMetricValue: 0,
1767
+ },
1768
+ ];
1769
+ }
1770
+
1771
+ const PROXMOX_DOWN_LIST: string = [
1772
+ "**Affected Resources** (2 total)",
1773
+ "",
1774
+ "1. **Node** `node/pve1` — **0**",
1775
+ "2. **Node** `node/pve3` — **0**",
1776
+ ].join("\n");
1777
+
1778
+ test("pve-node-offline (pve_up < 1) lists the down nodes at 0, healthy nodes absent", () => {
1779
+ const step: MonitorStep = proxmoxTemplateStep("pve-node-offline");
1780
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1781
+ const query: { alias: string; metricName: string } = firstQuery(step);
1782
+
1783
+ expect(metricFilter(criteria).filterType).toBe(FilterType.LessThan);
1784
+ expect(metricFilter(criteria).value).toBe(1);
1785
+
1786
+ const text: string =
1787
+ Evaluator.buildProxmoxRootCauseContext({
1788
+ dataToProcess: metricResponse({
1789
+ proxmoxResourceBreakdowns: [
1790
+ {
1791
+ clusterName: CLUSTER,
1792
+ metricName: query.metricName,
1793
+ metricFriendlyName: proxmoxName(query.metricName),
1794
+ metricAlias: query.alias,
1795
+ attributes: {},
1796
+ affectedResources: proxmoxNodes(),
1797
+ },
1798
+ ],
1799
+ }),
1800
+ monitorStep: step,
1801
+ monitor: new Monitor(),
1802
+ criteriaInstance: criteria,
1803
+ }) || "";
1804
+
1805
+ expect(text).toContain(PROXMOX_DOWN_LIST);
1806
+ expect(text).toContain(
1807
+ `- Metric: ${proxmoxName(query.metricName)} (\`${query.metricName}\`)`,
1808
+ );
1809
+ // The old `> 0` listed exactly this healthy node, and only it.
1810
+ expect(text).not.toContain("node/pve2");
1811
+ });
1812
+
1813
+ test("pve-node-offline reads a legacy untagged singular breakdown the same way", () => {
1814
+ const step: MonitorStep = proxmoxTemplateStep("pve-node-offline");
1815
+ const query: { alias: string; metricName: string } = firstQuery(step);
1816
+
1817
+ const text: string =
1818
+ Evaluator.buildProxmoxRootCauseContext({
1819
+ dataToProcess: metricResponse({
1820
+ proxmoxResourceBreakdown: {
1821
+ clusterName: CLUSTER,
1822
+ metricName: query.metricName,
1823
+ metricFriendlyName: proxmoxName(query.metricName),
1824
+ attributes: {},
1825
+ affectedResources: proxmoxNodes(),
1826
+ },
1827
+ }),
1828
+ monitorStep: step,
1829
+ monitor: new Monitor(),
1830
+ criteriaInstance: firingCriteria(step),
1831
+ }) || "";
1832
+
1833
+ expect(text).toContain(PROXMOX_DOWN_LIST);
1834
+ expect(text).not.toContain("node/pve2");
1835
+ });
1836
+
1837
+ test("docker-swarm-task-down (uptime < 60) lists the lowest uptime first, as a duration", () => {
1838
+ const step: MonitorStep = dockerSwarmTemplateStep("docker-swarm-task-down");
1839
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1840
+ const query: { alias: string; metricName: string } = firstQuery(step);
1841
+
1842
+ expect(metricFilter(criteria).filterType).toBe(FilterType.LessThan);
1843
+ expect(metricFilter(criteria).value).toBe(60);
1844
+ expect(getDockerSwarmMetricByMetricName(query.metricName)?.unit).toBe(
1845
+ "seconds",
1846
+ );
1847
+
1848
+ const text: string =
1849
+ Evaluator.buildDockerSwarmRootCauseContext({
1850
+ dataToProcess: metricResponse({
1851
+ dockerSwarmResourceBreakdowns: [
1852
+ {
1853
+ clusterName: CLUSTER,
1854
+ metricName: query.metricName,
1855
+ metricFriendlyName: dockerSwarmName(query.metricName),
1856
+ metricAlias: query.alias,
1857
+ metricUnit: "s",
1858
+ attributes: {},
1859
+ affectedResources: [
1860
+ // Ran for an hour, then restarted 45 seconds ago.
1861
+ {
1862
+ containerName: "web.1.abc",
1863
+ serviceName: "web",
1864
+ nodeName: "swarm-1",
1865
+ metricValue: 3600,
1866
+ lowestMetricValue: 45,
1867
+ },
1868
+ // Healthy and long-running.
1869
+ {
1870
+ containerName: "api.1.xyz",
1871
+ serviceName: "api",
1872
+ nodeName: "swarm-1",
1873
+ metricValue: 86400,
1874
+ lowestMetricValue: 86340,
1875
+ },
1876
+ // Crash-looping: never gets past half a minute.
1877
+ {
1878
+ containerName: "web.2.def",
1879
+ serviceName: "web",
1880
+ nodeName: "swarm-2",
1881
+ metricValue: 30,
1882
+ lowestMetricValue: 12,
1883
+ },
1884
+ ],
1885
+ },
1886
+ ],
1887
+ }),
1888
+ monitorStep: step,
1889
+ monitor: new Monitor(),
1890
+ criteriaInstance: criteria,
1891
+ }) || "";
1892
+
1893
+ expect(text).toContain(
1894
+ [
1895
+ "**Affected Tasks** (2 total)",
1896
+ "",
1897
+ "1. **Task** `web.2.def` — **12 sec**",
1898
+ " - Service: `web`",
1899
+ " - Node: `swarm-2`",
1900
+ "2. **Task** `web.1.abc` — **45 sec**",
1901
+ " - Service: `web`",
1902
+ " - Node: `swarm-1`",
1903
+ ].join("\n"),
1904
+ );
1905
+ expect(text).not.toContain("api.1.xyz");
1906
+ expect(text).not.toContain("86400");
1907
+ });
1908
+
1909
+ test("ceph-osd-down (ceph_osd_up < 1) lists the down OSDs at 0, up OSDs absent", () => {
1910
+ const step: MonitorStep = cephTemplateStep("ceph-osd-down");
1911
+ const criteria: MonitorCriteriaInstance = firingCriteria(step);
1912
+ const query: { alias: string; metricName: string } = firstQuery(step);
1913
+
1914
+ expect(metricFilter(criteria).filterType).toBe(FilterType.LessThan);
1915
+ expect(metricFilter(criteria).value).toBe(1);
1916
+
1917
+ const text: string =
1918
+ Evaluator.buildCephRootCauseContext({
1919
+ dataToProcess: metricResponse({
1920
+ cephResourceBreakdowns: [
1921
+ {
1922
+ clusterName: CLUSTER,
1923
+ metricName: query.metricName,
1924
+ metricFriendlyName: cephName(query.metricName),
1925
+ metricAlias: query.alias,
1926
+ attributes: {},
1927
+ affectedResources: [
1928
+ {
1929
+ daemon: "osd.3",
1930
+ hostname: "ceph-1",
1931
+ metricValue: 1,
1932
+ lowestMetricValue: 0,
1933
+ },
1934
+ {
1935
+ daemon: "osd.4",
1936
+ hostname: "ceph-1",
1937
+ metricValue: 1,
1938
+ lowestMetricValue: 1,
1939
+ },
1940
+ {
1941
+ daemon: "osd.7",
1942
+ hostname: "ceph-2",
1943
+ metricValue: 0,
1944
+ lowestMetricValue: 0,
1945
+ },
1946
+ ],
1947
+ },
1948
+ ],
1949
+ }),
1950
+ monitorStep: step,
1951
+ monitor: new Monitor(),
1952
+ criteriaInstance: criteria,
1953
+ }) || "";
1954
+
1955
+ expect(text).toContain(
1956
+ [
1957
+ "**Affected Resources** (2 total)",
1958
+ "",
1959
+ "1. **Daemon** `osd.3` — **0**",
1960
+ " - Host: `ceph-1`",
1961
+ "2. **Daemon** `osd.7` — **0**",
1962
+ " - Host: `ceph-2`",
1963
+ ].join("\n"),
1964
+ );
1965
+ expect(text).toContain(
1966
+ `- Metric: ${cephName(query.metricName)} (\`${query.metricName}\`)`,
1967
+ );
1968
+ expect(text).not.toContain("osd.4");
1969
+ expect(text).not.toContain("count");
1970
+ });
1971
+ });
1972
+
1973
+ /*
1974
+ * ============================================================
1975
+ * 5. The namespace line
1976
+ * ============================================================
1977
+ *
1978
+ * The worker records the query's attribute filters under their stored,
1979
+ * `resource.`-prefixed keys. The evaluator used to read the bare key,
1980
+ * which never occurs, so this line never rendered.
1981
+ */
1982
+
1983
+ describe("Kubernetes details: the namespace the query was scoped to", () => {
1984
+ function namespaceContext(input: {
1985
+ attributes: Dictionary<string>;
1986
+ namespaceFilter?: string | undefined;
1987
+ }): Promise<string | null> {
1988
+ return kubernetesContext({
1989
+ step: kubernetesStep({
1990
+ metricViewConfig: {
1991
+ queryConfigs: [
1992
+ queryConfig({
1993
+ alias: "failed",
1994
+ metricName: "k8s.job.failed_pods",
1995
+ }),
1996
+ ],
1997
+ formulaConfigs: [],
1998
+ },
1999
+ namespaceFilter: input.namespaceFilter,
2000
+ }),
2001
+ response: metricResponse({
2002
+ kubernetesResourceBreakdowns: [
2003
+ kubernetesScan({
2004
+ alias: "failed",
2005
+ metricName: "k8s.job.failed_pods",
2006
+ attributes: input.attributes,
2007
+ resources: [
2008
+ {
2009
+ workloadType: "Job",
2010
+ workloadName: "nightly-report",
2011
+ namespace: "payments",
2012
+ metricValue: 2,
2013
+ lowestMetricValue: 1,
2014
+ },
2015
+ ],
2016
+ }),
2017
+ ],
2018
+ }),
2019
+ criteria: criteriaOn({
2020
+ alias: "failed",
2021
+ filterType: FilterType.GreaterThan,
2022
+ value: 0,
2023
+ }),
2024
+ });
2025
+ }
2026
+
2027
+ test("reads the `resource.`-prefixed attribute the worker records", async () => {
2028
+ const context: string | null = await namespaceContext({
2029
+ attributes: {
2030
+ "resource.k8s.cluster.name": CLUSTER,
2031
+ "resource.k8s.namespace.name": "payments",
2032
+ },
2033
+ });
2034
+
2035
+ expect(context).toContain(
2036
+ [
2037
+ `- Cluster: ${CLUSTER}`,
2038
+ `- Metric: ${kubernetesName("k8s.job.failed_pods")} (\`k8s.job.failed_pods\`)`,
2039
+ "- Namespace: payments",
2040
+ ].join("\n"),
2041
+ );
2042
+ });
2043
+
2044
+ test("falls back to the step's namespace filter", async () => {
2045
+ const context: string | null = await namespaceContext({
2046
+ attributes: { "resource.k8s.cluster.name": CLUSTER },
2047
+ namespaceFilter: "payments",
2048
+ });
2049
+
2050
+ expect(context).toContain("- Namespace: payments");
2051
+ });
2052
+
2053
+ test("renders no namespace line when the query was not scoped to one", async () => {
2054
+ const context: string | null = await namespaceContext({
2055
+ attributes: { "resource.k8s.cluster.name": CLUSTER },
2056
+ });
2057
+
2058
+ // No cluster-level line (an entry's detail bullet is indented).
2059
+ expect(context).not.toMatch(/^- Namespace:/m);
2060
+ // The resource's own namespace still sits under its entry.
2061
+ expect(context).toContain(" - Namespace: `payments`");
2062
+ });
2063
+ });
2064
+
2065
+ /*
2066
+ * ============================================================
2067
+ * 6. The "- Metric:" line
2068
+ * ============================================================
2069
+ */
2070
+
2071
+ describe("Platform details: the metric line never repeats a name as its own friendly name", () => {
2072
+ test("a metric whose friendly name is its own name renders as just the code-quoted name", async () => {
2073
+ /*
2074
+ * The worker's friendly name for a metric the catalog does not know
2075
+ * is the metric name itself, which the old line printed twice:
2076
+ * "k8s.node.allocatable_memory (`k8s.node.allocatable_memory`)".
2077
+ */
2078
+ const scan: KubernetesResourceBreakdown = allocatableScan("alloc_mem");
2079
+
2080
+ expect(scan.metricFriendlyName).toBe(UNCATALOGED_METRIC);
2081
+
2082
+ const context: string | null = await kubernetesContext({
2083
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
2084
+ response: metricResponse({ kubernetesResourceBreakdowns: [scan] }),
2085
+ criteria: criteriaOn({
2086
+ alias: "alloc_mem",
2087
+ filterType: FilterType.GreaterThan,
2088
+ value: 100,
2089
+ }),
2090
+ });
2091
+
2092
+ const text: string = context || "";
2093
+
2094
+ expect(text).toContain(`- Metric: \`${UNCATALOGED_METRIC}\``);
2095
+ expect(text).not.toContain(
2096
+ `${UNCATALOGED_METRIC} (\`${UNCATALOGED_METRIC}\`)`,
2097
+ );
2098
+ expect(text.split("- Metric:").length - 1).toBe(1);
2099
+ // Nor does the analysis add the same name again in parentheses.
2100
+ expect(text).toContain(
2101
+ `Kubernetes metric \`${UNCATALOGED_METRIC}\` has breached the configured threshold.`,
2102
+ );
2103
+ });
2104
+
2105
+ test("a cataloged metric is named by the catalog, with its metric name beside it", async () => {
2106
+ const metricName: string = "k8s.node.memory.usage";
2107
+
2108
+ const context: string | null = await kubernetesContext({
2109
+ step: kubernetesStep({ metricViewConfig: memoryRatioConfig() }),
2110
+ response: metricResponse({
2111
+ kubernetesResourceBreakdowns: [usedScan("used_mem")],
2112
+ }),
2113
+ criteria: criteriaOn({
2114
+ alias: "used_mem",
2115
+ filterType: FilterType.GreaterThan,
2116
+ value: 100,
2117
+ }),
2118
+ });
2119
+
2120
+ expect(kubernetesName(metricName)).not.toBe(metricName);
2121
+ expect(context).toContain(
2122
+ `- Metric: ${kubernetesName(metricName)} (\`${metricName}\`)`,
2123
+ );
2124
+ });
2125
+ });