@oneuptime/common 13.0.8 → 14.0.1

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 (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -30,6 +30,14 @@ import ExternalStatusPageMonitorCriteria from "./Criteria/ExternalStatusPageMoni
30
30
  import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
31
31
  import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
32
32
  import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
33
+ import AffectedResourceList, {
34
+ AffectedResourceListDetail,
35
+ AffectedResourceListEntry,
36
+ } from "./AffectedResourceList";
37
+ import RootCauseList, {
38
+ RootCauseListDetail,
39
+ RootCauseListItem,
40
+ } from "./RootCauseList";
33
41
  import DataToProcess from "./DataToProcess";
34
42
  import Monitor from "../../../Models/DatabaseModels/Monitor";
35
43
  import MonitorCriteria from "../../../Types/Monitor/MonitorCriteria";
@@ -120,6 +128,15 @@ export interface TelemetryExplorerDeepLink {
120
128
  dropped: Array<string>;
121
129
  }
122
130
 
131
+ /**
132
+ * Which affected-resources rows breached the matched criteria, and which
133
+ * end of the value range is the worst.
134
+ */
135
+ interface AffectedResourceBreachPredicate {
136
+ matches: (value: number) => boolean;
137
+ worstIsLowest: boolean;
138
+ }
139
+
123
140
  export default class MonitorCriteriaEvaluator {
124
141
  public static async processMonitorStep(input: {
125
142
  dataToProcess: DataToProcess;
@@ -1552,9 +1569,7 @@ ${contextBlock}
1552
1569
  samples: breachingSamples,
1553
1570
  totalSamples: ctx.totalSamplesInWindow,
1554
1571
  unit: ctx.unit,
1555
- metricName: ctx.metricName,
1556
1572
  unitHeuristicMetricName: unitHeuristicMetricName,
1557
- alias: ctx.alias,
1558
1573
  components: ctx.components || [],
1559
1574
  })}`,
1560
1575
  );
@@ -1610,11 +1625,25 @@ ${contextBlock}
1610
1625
  }
1611
1626
 
1612
1627
  /**
1613
- * Build the **Breaching Samples** markdown section — a table of
1614
- * timestamps, values, and any group-by attributes. Caps the row count
1615
- * so a 30-minute window at 1-second granularity doesn't dump thousands
1616
- * of lines onto the root-cause page; the caller can always drill in
1617
- * via the metric explorer link.
1628
+ * Build the **Breaching Samples** markdown section — a numbered list of
1629
+ * the samples that breached, oldest first. Caps the list so a 30-minute
1630
+ * window at 1-second granularity doesn't dump thousands of lines onto
1631
+ * the root-cause page; the caller can always drill in via the metric
1632
+ * explorer link.
1633
+ *
1634
+ * 1. `2026-08-14T10:30:00.000Z` — **1.07 GB**
1635
+ * - `a`: 537 MB
1636
+ * - `b`: 1.5 sec
1637
+ * - `k8s.pod.name`: `web-1`
1638
+ *
1639
+ * It used to be a table — Timestamp | Metric | Alias | Value, a column
1640
+ * per formula component and one per attribute key — which grew wider
1641
+ * with every attribute and read badly in the email card, in Slack and on
1642
+ * the dashboard (see RootCauseList). Its Metric and Alias columns
1643
+ * repeated the same two values on every row, the ones the Metric
1644
+ * Details list directly above already states, so the list leaves them
1645
+ * out; and a component or attribute a sample has no value for is left
1646
+ * out rather than printed as "-".
1618
1647
  *
1619
1648
  * Timestamps are emitted as inline code wrapping ISO 8601 strings so
1620
1649
  * the client-side markdown viewer can localize them to the viewer's
@@ -1624,16 +1653,14 @@ ${contextBlock}
1624
1653
  samples: Array<MetricBreachingSample>;
1625
1654
  totalSamples?: number | undefined;
1626
1655
  unit: string | null;
1627
- metricName: string;
1628
1656
  /**
1629
1657
  * The metric name to reason about, or undefined for a formula.
1630
1658
  * See MonitorCriteriaEvaluator.metricNameForUnitHeuristics.
1631
1659
  */
1632
1660
  unitHeuristicMetricName: string | undefined;
1633
- alias: string;
1634
1661
  components: Array<MetricComponent>;
1635
1662
  }): string {
1636
- const MAX_ROWS: number = 20;
1663
+ const MAX_SAMPLES_SHOWN: number = 20;
1637
1664
 
1638
1665
  // Sort chronologically and de-duplicate any accidental repeats
1639
1666
  const sorted: Array<MetricBreachingSample> = [...input.samples].sort(
@@ -1646,10 +1673,13 @@ ${contextBlock}
1646
1673
 
1647
1674
  const displayedSamples: Array<MetricBreachingSample> = sorted.slice(
1648
1675
  0,
1649
- MAX_ROWS,
1676
+ MAX_SAMPLES_SHOWN,
1650
1677
  );
1651
1678
 
1652
- // Collect attribute keys that appear on any displayed sample
1679
+ /*
1680
+ * Collect attribute keys that appear on any displayed sample, so every
1681
+ * item lists the attributes it has in the same order.
1682
+ */
1653
1683
  const attrKeySet: Set<string> = new Set<string>();
1654
1684
  for (const s of displayedSamples) {
1655
1685
  for (const k of Object.keys(s.attributes || {})) {
@@ -1658,107 +1688,74 @@ ${contextBlock}
1658
1688
  }
1659
1689
  const attrKeys: Array<string> = Array.from(attrKeySet);
1660
1690
 
1661
- /*
1662
- * Escape pipe characters that could appear in the metric display
1663
- * name (formulas like "a | b" are unlikely but possible) so they
1664
- * don't break GitHub-flavored-markdown tables.
1665
- */
1666
- const escapeCell: (value: string) => string = (value: string): string => {
1667
- return value.replace(/\|/g, "\\|");
1668
- };
1669
-
1670
- /*
1671
- * Column layout:
1672
- * Timestamp | Metric | Alias | Value | <component_1> | ... | <attr_1> | ...
1673
- *
1674
- * The component columns let the reader see what each variable of a
1675
- * formula resolved to at the breach time — e.g. "when c = a + b
1676
- * breached 100, a was 55, b was 46". They are omitted for plain
1677
- * metric criteria.
1678
- */
1679
- const headerCells: Array<string> = [
1680
- "Timestamp",
1681
- "Metric",
1682
- "Alias",
1683
- "Value",
1684
- ];
1685
-
1686
- /*
1687
- * The component columns carry no unit in their header any more. Each
1688
- * cell now names the unit it actually landed on, and auto-scaling
1689
- * means neighbouring rows can land on different ones — a header that
1690
- * read "a (By)" over cells reading "900 KB" and "1.2 MB" states a
1691
- * unit that neither row uses. The configured unit is not lost: the
1692
- * Components bullet list above the table still records it.
1693
- */
1694
- for (const component of input.components) {
1695
- headerCells.push(component.alias);
1696
- }
1697
-
1698
- headerCells.push(...attrKeys);
1699
-
1700
- const headerRow: string = `| ${headerCells.join(" | ")} |`;
1701
- const dividerRow: string = `| ${headerCells
1702
- .map(() => {
1703
- return "---";
1704
- })
1705
- .join(" | ")} |`;
1706
-
1707
- const metricCell: string = `\`${escapeCell(input.metricName)}\``;
1708
- const aliasCell: string = input.alias
1709
- ? `\`${escapeCell(input.alias)}\``
1710
- : "-";
1711
-
1712
- const dataRows: Array<string> = displayedSamples.map(
1713
- (s: MetricBreachingSample) => {
1714
- const timestampIso: string = new Date(s.timestamp).toISOString();
1715
- const cells: Array<string> = [
1716
- `\`${timestampIso}\``,
1717
- metricCell,
1718
- aliasCell,
1719
- MetricValueFormatter.format({
1720
- value: s.value,
1721
- unit: input.unit,
1722
- metricName: input.unitHeuristicMetricName,
1723
- }),
1724
- ];
1691
+ const items: Array<RootCauseListItem> = displayedSamples.map(
1692
+ (s: MetricBreachingSample): RootCauseListItem => {
1693
+ const details: Array<RootCauseListDetail> = [];
1725
1694
 
1695
+ /*
1696
+ * What each variable of a formula resolved to at the breach time —
1697
+ * e.g. "when c = a + b breached 100, a was 55, b was 46". Absent
1698
+ * for plain metric criteria.
1699
+ */
1726
1700
  for (const component of input.components) {
1727
1701
  const match: MetricComponentValue | undefined = (
1728
1702
  s.componentValues || []
1729
1703
  ).find((cv: MetricComponentValue) => {
1730
1704
  return cv.alias === component.alias;
1731
1705
  });
1732
- if (match && typeof match.value === "number") {
1733
- /*
1734
- * Each component keeps its OWN unit. Component values are
1735
- * indexed off the per-query series and never go through the
1736
- * sample→threshold-unit conversion the main Value column
1737
- * does, so they are in the component's legendUnit — mixing
1738
- * units across the row is deliberate, and is why every cell
1739
- * has to say which one it is in.
1740
- */
1741
- cells.push(
1742
- MetricValueFormatter.format({
1743
- value: match.value,
1744
- unit: component.unit,
1745
- metricName:
1746
- MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1747
- metricName: component.name,
1748
- isFormula: component.isFormula,
1749
- }),
1750
- }),
1751
- );
1752
- } else {
1753
- cells.push("-");
1706
+
1707
+ if (!match || typeof match.value !== "number") {
1708
+ continue;
1754
1709
  }
1710
+
1711
+ /*
1712
+ * Each component keeps its OWN unit. Component values are
1713
+ * indexed off the per-query series and never go through the
1714
+ * sample→threshold-unit conversion the sample's value does, so
1715
+ * they are in the component's legendUnit — which is why each
1716
+ * one names the unit it is in. Its configured unit is in the
1717
+ * Components list of the Metric Details above.
1718
+ */
1719
+ details.push({
1720
+ label: RootCauseList.code(component.alias),
1721
+ value: MetricValueFormatter.format({
1722
+ value: match.value,
1723
+ unit: component.unit,
1724
+ metricName: MonitorCriteriaEvaluator.metricNameForUnitHeuristics({
1725
+ metricName: component.name,
1726
+ isFormula: component.isFormula,
1727
+ }),
1728
+ }),
1729
+ });
1755
1730
  }
1756
1731
 
1732
+ /*
1733
+ * The attributes identify the series the sample came from. Keys
1734
+ * and values are telemetry, so both go in code spans:
1735
+ * RootCauseList.code keeps whatever they contain inside the span.
1736
+ */
1757
1737
  for (const k of attrKeys) {
1758
1738
  const v: unknown = (s.attributes as Record<string, unknown>)[k];
1759
- cells.push(v === undefined || v === null ? "-" : String(v));
1739
+
1740
+ if (v === undefined || v === null) {
1741
+ continue;
1742
+ }
1743
+
1744
+ details.push({
1745
+ label: RootCauseList.code(k),
1746
+ value: RootCauseList.code(String(v)),
1747
+ });
1760
1748
  }
1761
- return `| ${cells.join(" | ")} |`;
1749
+
1750
+ return {
1751
+ title: RootCauseList.code(new Date(s.timestamp).toISOString()),
1752
+ value: `**${MetricValueFormatter.format({
1753
+ value: s.value,
1754
+ unit: input.unit,
1755
+ metricName: input.unitHeuristicMetricName,
1756
+ })}**`,
1757
+ details: details,
1758
+ };
1762
1759
  },
1763
1760
  );
1764
1761
 
@@ -1769,14 +1766,17 @@ ${contextBlock}
1769
1766
  totalSamples: input.totalSamples,
1770
1767
  }),
1771
1768
  "",
1772
- headerRow,
1773
- dividerRow,
1774
- ...dataRows,
1769
+ RootCauseList.render(items),
1775
1770
  ];
1776
1771
 
1777
1772
  if (sorted.length > displayedSamples.length) {
1773
+ /*
1774
+ * The blank line matters: without it the note is a lazy
1775
+ * continuation of the last item, not a paragraph of its own.
1776
+ */
1778
1777
  lines.push(
1779
- `\n_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`,
1778
+ "",
1779
+ `_Showing the first ${displayedSamples.length} of ${sorted.length} breaching samples._`,
1780
1780
  );
1781
1781
  }
1782
1782
 
@@ -2018,7 +2018,7 @@ ${contextBlock}
2018
2018
  }
2019
2019
 
2020
2020
  /**
2021
- * The unit for a value in a per-platform "Affected Resources" table.
2021
+ * The unit for a value in a per-platform "Affected Resources" list.
2022
2022
  *
2023
2023
  * These rows are NOT the same numbers as the criteria's breaching
2024
2024
  * samples: the worker collects them with a raw `MetricService.findBy`
@@ -2060,10 +2060,11 @@ ${contextBlock}
2060
2060
  }
2061
2061
 
2062
2062
  /**
2063
- * Render one cell of a platform breakdown table's Value column.
2063
+ * Render the value of one resource in a platform "Affected Resources"
2064
+ * list.
2064
2065
  *
2065
- * Bold, because that is how these tables have always drawn the value —
2066
- * it is the one number in the row a reader scans for.
2066
+ * Bold, because that is how the breakdown has always drawn the value —
2067
+ * it is the one number on the line a reader scans for.
2067
2068
  */
2068
2069
  private static formatPlatformResourceValue(input: {
2069
2070
  platform: "kubernetes" | "proxmox" | "vmware" | "dockerSwarm" | "ceph";
@@ -2080,10 +2081,94 @@ ${contextBlock}
2080
2081
  })}**`;
2081
2082
  }
2082
2083
 
2084
+ /*
2085
+ * One Kubernetes resource as an "Affected Resources" list item.
2086
+ *
2087
+ * The item is titled by the most specific object the series names —
2088
+ * container, then pod, then workload, then node, then namespace — because
2089
+ * that is the thing that is actually breaching; everything above it in
2090
+ * the hierarchy is listed underneath as context. A series with no
2091
+ * Kubernetes identity at all is a cluster-level series, so it is titled
2092
+ * by the cluster.
2093
+ *
2094
+ * A container is titled together with its pod. Every replica of a
2095
+ * workload runs a container of the same name, so "Container `checkout`"
2096
+ * alone would title three crash-looping replicas identically and leave
2097
+ * the pod that tells them apart buried in the bullets.
2098
+ */
2099
+ private static getKubernetesAffectedResourceEntry(input: {
2100
+ resource: KubernetesAffectedResource;
2101
+ clusterName: string;
2102
+ value: string;
2103
+ }): AffectedResourceListEntry {
2104
+ const resource: KubernetesAffectedResource = input.resource;
2105
+
2106
+ // "Deployment", "StatefulSet", ... — the worker only ever sets one of those.
2107
+ const workloadLabel: string = resource.workloadType || "Workload";
2108
+
2109
+ let kind: string = "Cluster";
2110
+ let name: string = AffectedResourceList.code(input.clusterName);
2111
+
2112
+ if (resource.containerName) {
2113
+ kind = "Container";
2114
+ name = resource.podName
2115
+ ? `${AffectedResourceList.code(resource.containerName)} in pod ${AffectedResourceList.code(resource.podName)}`
2116
+ : AffectedResourceList.code(resource.containerName);
2117
+ } else if (resource.podName) {
2118
+ kind = "Pod";
2119
+ name = AffectedResourceList.code(resource.podName);
2120
+ } else if (resource.workloadName) {
2121
+ kind = workloadLabel;
2122
+ name = AffectedResourceList.code(resource.workloadName);
2123
+ } else if (resource.nodeName) {
2124
+ kind = "Node";
2125
+ name = AffectedResourceList.code(resource.nodeName);
2126
+ } else if (resource.namespace) {
2127
+ kind = "Namespace";
2128
+ name = AffectedResourceList.code(resource.namespace);
2129
+ }
2130
+
2131
+ const details: Array<AffectedResourceListDetail> = [];
2132
+
2133
+ if (resource.namespace && kind !== "Namespace") {
2134
+ details.push({
2135
+ label: "Namespace",
2136
+ value: AffectedResourceList.code(resource.namespace),
2137
+ });
2138
+ }
2139
+
2140
+ const titledByWorkload: boolean =
2141
+ !resource.containerName &&
2142
+ !resource.podName &&
2143
+ Boolean(resource.workloadName);
2144
+
2145
+ if (resource.workloadName && !titledByWorkload) {
2146
+ details.push({
2147
+ label: workloadLabel,
2148
+ value: AffectedResourceList.code(resource.workloadName),
2149
+ });
2150
+ }
2151
+
2152
+ if (resource.nodeName && kind !== "Node") {
2153
+ details.push({
2154
+ label: "Node",
2155
+ value: AffectedResourceList.code(resource.nodeName),
2156
+ });
2157
+ }
2158
+
2159
+ return {
2160
+ kind: kind,
2161
+ name: name,
2162
+ value: input.value,
2163
+ details: details,
2164
+ };
2165
+ }
2166
+
2083
2167
  private static async buildKubernetesRootCauseContext(input: {
2084
2168
  dataToProcess: DataToProcess;
2085
2169
  monitorStep: MonitorStep;
2086
2170
  monitor: Monitor;
2171
+ criteriaInstance?: MonitorCriteriaInstance | undefined;
2087
2172
  }): Promise<string | null> {
2088
2173
  const metricResponse: MetricMonitorResponse =
2089
2174
  input.dataToProcess as MetricMonitorResponse;
@@ -2116,20 +2201,40 @@ ${contextBlock}
2116
2201
 
2117
2202
  // Affected resources
2118
2203
  if (breakdown.affectedResources && breakdown.affectedResources.length > 0) {
2119
- const resourceLines: Array<string> = [];
2204
+ /*
2205
+ * Keep the rows that satisfy the criteria that just matched, worst
2206
+ * first — NOT simply the non-zero rows. k8s-node-not-ready fires on
2207
+ * `k8s.node.condition_ready = 0`, so the zero rows are the NotReady
2208
+ * nodes; the old hardcoded `> 0` dropped exactly those and named a
2209
+ * healthy node in the analysis below.
2210
+ */
2211
+ const breach: AffectedResourceBreachPredicate =
2212
+ MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
2213
+ criteriaInstance: input.criteriaInstance,
2214
+ floorEqualityFiresOnFall: true,
2215
+ });
2120
2216
 
2121
- // Sort by metric value descending (worst first) and filter out zero-value resources
2122
- const sortedResources: Array<KubernetesAffectedResource> = [
2123
- ...breakdown.affectedResources,
2124
- ]
2125
- .filter((r: KubernetesAffectedResource) => {
2126
- return r.metricValue > 0;
2127
- })
2128
- .sort(
2129
- (a: KubernetesAffectedResource, b: KubernetesAffectedResource) => {
2130
- return b.metricValue - a.metricValue;
2131
- },
2132
- );
2217
+ const sortedResources: Array<KubernetesAffectedResource> =
2218
+ breakdown.affectedResources
2219
+ .map((r: KubernetesAffectedResource) => {
2220
+ /*
2221
+ * A fall criteria breached on the resource's lowest sample
2222
+ * in the window, not its highest.
2223
+ */
2224
+ return breach.worstIsLowest
2225
+ ? { ...r, metricValue: r.lowestMetricValue ?? r.metricValue }
2226
+ : r;
2227
+ })
2228
+ .filter((r: KubernetesAffectedResource) => {
2229
+ return breach.matches(r.metricValue);
2230
+ })
2231
+ .sort(
2232
+ (a: KubernetesAffectedResource, b: KubernetesAffectedResource) => {
2233
+ return breach.worstIsLowest
2234
+ ? a.metricValue - b.metricValue
2235
+ : b.metricValue - a.metricValue;
2236
+ },
2237
+ );
2133
2238
 
2134
2239
  if (sortedResources.length === 0) {
2135
2240
  return sections.join("\n");
@@ -2137,108 +2242,31 @@ ${contextBlock}
2137
2242
 
2138
2243
  // Show top 10 affected resources
2139
2244
  const resourcesToShow: Array<KubernetesAffectedResource> =
2140
- sortedResources.slice(0, 10);
2141
-
2142
- // Determine which columns are present across all resources
2143
- const hasNamespace: boolean = resourcesToShow.some(
2144
- (r: KubernetesAffectedResource) => {
2145
- return r.namespace;
2146
- },
2147
- );
2148
- const hasWorkload: boolean = resourcesToShow.some(
2149
- (r: KubernetesAffectedResource) => {
2150
- return r.workloadType && r.workloadName;
2151
- },
2152
- );
2153
- const hasPod: boolean = resourcesToShow.some(
2154
- (r: KubernetesAffectedResource) => {
2155
- return r.podName;
2156
- },
2157
- );
2158
- const hasContainer: boolean = resourcesToShow.some(
2159
- (r: KubernetesAffectedResource) => {
2160
- return r.containerName;
2161
- },
2162
- );
2163
- const hasNode: boolean = resourcesToShow.some(
2164
- (r: KubernetesAffectedResource) => {
2165
- return r.nodeName;
2166
- },
2167
- );
2168
-
2169
- // Build table header
2170
- const headerCells: Array<string> = [];
2171
- if (hasNamespace) {
2172
- headerCells.push("Namespace");
2173
- }
2174
- if (hasWorkload) {
2175
- headerCells.push("Workload Type");
2176
- headerCells.push("Workload");
2177
- }
2178
- if (hasPod) {
2179
- headerCells.push("Pod");
2180
- }
2181
- if (hasContainer) {
2182
- headerCells.push("Container");
2183
- }
2184
- if (hasNode) {
2185
- headerCells.push("Node");
2186
- }
2187
- headerCells.push("Value");
2188
-
2189
- const headerRow: string = `| ${headerCells.join(" | ")} |`;
2190
- const separatorRow: string = `| ${headerCells
2191
- .map(() => {
2192
- return "---";
2193
- })
2194
- .join(" | ")} |`;
2195
-
2196
- resourceLines.push(headerRow);
2197
- resourceLines.push(separatorRow);
2198
-
2199
- for (const resource of resourcesToShow) {
2200
- const cells: Array<string> = [];
2201
-
2202
- if (hasNamespace) {
2203
- cells.push(resource.namespace ? `\`${resource.namespace}\`` : "-");
2204
- }
2205
- if (hasWorkload) {
2206
- cells.push(resource.workloadType ? `${resource.workloadType}` : "-");
2207
- cells.push(
2208
- resource.workloadName ? `\`${resource.workloadName}\`` : "-",
2209
- );
2210
- }
2211
- if (hasPod) {
2212
- cells.push(resource.podName ? `\`${resource.podName}\`` : "-");
2213
- }
2214
- if (hasContainer) {
2215
- cells.push(
2216
- resource.containerName ? `\`${resource.containerName}\`` : "-",
2217
- );
2218
- }
2219
- if (hasNode) {
2220
- cells.push(resource.nodeName ? `\`${resource.nodeName}\`` : "-");
2221
- }
2222
-
2223
- cells.push(
2224
- MonitorCriteriaEvaluator.formatPlatformResourceValue({
2225
- platform: "kubernetes",
2226
- metricName: breakdown.metricName,
2227
- value: resource.metricValue,
2228
- }),
2229
- );
2230
-
2231
- resourceLines.push(`| ${cells.join(" | ")} |`);
2232
- }
2233
-
2234
- if (sortedResources.length > 10) {
2235
- resourceLines.push(
2236
- `\n*... and ${sortedResources.length - 10} more affected resources*`,
2237
- );
2238
- }
2245
+ sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2239
2246
 
2240
2247
  sections.push(
2241
- `\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
2248
+ AffectedResourceList.render({
2249
+ heading: "Affected Resources",
2250
+ overflowNoun: "affected resources",
2251
+ totalCount: sortedResources.length,
2252
+ entries: resourcesToShow.map(
2253
+ (
2254
+ resource: KubernetesAffectedResource,
2255
+ ): AffectedResourceListEntry => {
2256
+ return MonitorCriteriaEvaluator.getKubernetesAffectedResourceEntry(
2257
+ {
2258
+ resource: resource,
2259
+ clusterName: breakdown.clusterName,
2260
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
2261
+ platform: "kubernetes",
2262
+ metricName: breakdown.metricName,
2263
+ value: resource.metricValue,
2264
+ }),
2265
+ },
2266
+ );
2267
+ },
2268
+ ),
2269
+ }),
2242
2270
  );
2243
2271
 
2244
2272
  // Add root cause analysis based on metric type
@@ -2509,6 +2537,92 @@ ${contextBlock}
2509
2537
  return sections.length > 0 ? sections.join("\n") : null;
2510
2538
  }
2511
2539
 
2540
+ /*
2541
+ * What a Proxmox resource is, from the `pve.type` / `pve.scope` the
2542
+ * agent's transform/pve-identity processor derives from its `id`
2543
+ * (node/…, qemu/…, lxc/…, storage/…, cluster/…). A value that processor
2544
+ * does not produce is shown as it arrived rather than guessed at.
2545
+ */
2546
+ private static getProxmoxAffectedResourceKind(
2547
+ resource: ProxmoxAffectedResource,
2548
+ ): string {
2549
+ switch (resource.resourceType) {
2550
+ case "qemu":
2551
+ return "Virtual Machine";
2552
+ case "lxc":
2553
+ return "Container";
2554
+ case "node":
2555
+ return "Node";
2556
+ case "storage":
2557
+ return "Storage";
2558
+ default:
2559
+ break;
2560
+ }
2561
+
2562
+ switch (resource.scope) {
2563
+ case "guest":
2564
+ return "Guest";
2565
+ case "node":
2566
+ return "Node";
2567
+ case "storage":
2568
+ return "Storage";
2569
+ case "cluster":
2570
+ return "Cluster";
2571
+ default:
2572
+ break;
2573
+ }
2574
+
2575
+ return resource.resourceType || resource.scope || "Resource";
2576
+ }
2577
+
2578
+ /*
2579
+ * One Proxmox resource as an "Affected Resources" list item: its kind,
2580
+ * its name next to its `qemu/100`-style id, and the node it runs on.
2581
+ */
2582
+ private static getProxmoxAffectedResourceEntry(input: {
2583
+ resource: ProxmoxAffectedResource;
2584
+ clusterName: string;
2585
+ value: string;
2586
+ }): AffectedResourceListEntry {
2587
+ const resource: ProxmoxAffectedResource = input.resource;
2588
+
2589
+ let kind: string =
2590
+ MonitorCriteriaEvaluator.getProxmoxAffectedResourceKind(resource);
2591
+ let name: string = AffectedResourceList.codeWithId({
2592
+ name: resource.resourceName,
2593
+ id: resource.resourceId,
2594
+ });
2595
+
2596
+ if (!name && resource.nodeName) {
2597
+ // A per-node series that carries no id of its own.
2598
+ kind = "Node";
2599
+ name = AffectedResourceList.code(resource.nodeName);
2600
+ }
2601
+
2602
+ if (!name) {
2603
+ // Nothing narrower than the cluster — a cluster-wide series.
2604
+ kind = "Cluster";
2605
+ name = AffectedResourceList.code(input.clusterName);
2606
+ }
2607
+
2608
+ const details: Array<AffectedResourceListDetail> = [];
2609
+
2610
+ // A node's own row would only repeat its name as "Node: …".
2611
+ if (resource.nodeName && kind !== "Node") {
2612
+ details.push({
2613
+ label: "Node",
2614
+ value: AffectedResourceList.code(resource.nodeName),
2615
+ });
2616
+ }
2617
+
2618
+ return {
2619
+ kind: kind,
2620
+ name: name,
2621
+ value: input.value,
2622
+ details: details,
2623
+ };
2624
+ }
2625
+
2512
2626
  private static buildProxmoxRootCauseContext(input: {
2513
2627
  dataToProcess: DataToProcess;
2514
2628
  monitorStep: MonitorStep;
@@ -2577,8 +2691,8 @@ ${contextBlock}
2577
2691
  );
2578
2692
  }
2579
2693
 
2580
- // Affected resources (Resource / Type / Node / Value)
2581
- let renderedBreakdownTable: boolean = false;
2694
+ // Affected resources: a ranked list of kind, name and node
2695
+ let renderedBreakdownList: boolean = false;
2582
2696
 
2583
2697
  if (breakdown && breakdown.affectedResources.length > 0) {
2584
2698
  /*
@@ -2586,7 +2700,7 @@ ${contextBlock}
2586
2700
  * first, top 10. Note that for availability metrics (pve_up)
2587
2701
  * zero-valued rows ARE the down resources — those still drive
2588
2702
  * alerting through the per-series criteria evaluation; this
2589
- * table is supplementary context only.
2703
+ * list is supplementary context only.
2590
2704
  */
2591
2705
  const sortedResources: Array<ProxmoxAffectedResource> = [
2592
2706
  ...breakdown.affectedResources,
@@ -2599,7 +2713,7 @@ ${contextBlock}
2599
2713
  });
2600
2714
 
2601
2715
  /*
2602
- * Skip the table when no row carries any identity label
2716
+ * Skip the list when no row carries any identity label
2603
2717
  * (cluster-wide series) — it would add nothing.
2604
2718
  */
2605
2719
  const hasIdentity: boolean = sortedResources.some(
@@ -2610,55 +2724,41 @@ ${contextBlock}
2610
2724
 
2611
2725
  if (sortedResources.length > 0 && hasIdentity) {
2612
2726
  const resourcesToShow: Array<ProxmoxAffectedResource> =
2613
- sortedResources.slice(0, 10);
2614
-
2615
- const resourceLines: Array<string> = [];
2616
- resourceLines.push(`| Resource | Type | Node | Value |`);
2617
- resourceLines.push(`| --- | --- | --- | --- |`);
2618
-
2619
- for (const resource of resourcesToShow) {
2620
- let resourceCell: string = "-";
2621
- if (resource.resourceName && resource.resourceId) {
2622
- resourceCell = `\`${resource.resourceName}\` (\`${resource.resourceId}\`)`;
2623
- } else if (resource.resourceId) {
2624
- resourceCell = `\`${resource.resourceId}\``;
2625
- } else if (resource.resourceName) {
2626
- resourceCell = `\`${resource.resourceName}\``;
2627
- }
2628
-
2629
- const typeCell: string =
2630
- resource.resourceType || resource.scope || "-";
2631
- const nodeCell: string = resource.nodeName
2632
- ? `\`${resource.nodeName}\``
2633
- : "-";
2634
-
2635
- resourceLines.push(
2636
- `| ${resourceCell} | ${typeCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
2637
- {
2638
- platform: "proxmox",
2639
- metricName: breakdown.metricName,
2640
- value: resource.metricValue,
2641
- },
2642
- )} |`,
2643
- );
2644
- }
2645
-
2646
- if (sortedResources.length > 10) {
2647
- resourceLines.push(
2648
- `\n*... and ${sortedResources.length - 10} more affected resources*`,
2649
- );
2650
- }
2727
+ sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2651
2728
 
2652
2729
  sections.push(
2653
- `\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
2730
+ AffectedResourceList.render({
2731
+ heading: "Affected Resources",
2732
+ overflowNoun: "affected resources",
2733
+ totalCount: sortedResources.length,
2734
+ entries: resourcesToShow.map(
2735
+ (
2736
+ resource: ProxmoxAffectedResource,
2737
+ ): AffectedResourceListEntry => {
2738
+ return MonitorCriteriaEvaluator.getProxmoxAffectedResourceEntry(
2739
+ {
2740
+ resource: resource,
2741
+ clusterName: breakdown.clusterName,
2742
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue(
2743
+ {
2744
+ platform: "proxmox",
2745
+ metricName: breakdown.metricName,
2746
+ value: resource.metricValue,
2747
+ },
2748
+ ),
2749
+ },
2750
+ );
2751
+ },
2752
+ ),
2753
+ }),
2654
2754
  );
2655
- renderedBreakdownTable = true;
2755
+ renderedBreakdownList = true;
2656
2756
  }
2657
2757
  }
2658
2758
 
2659
- // Metric results summary (fallback context when no table rendered)
2759
+ // Metric results summary (fallback context when no list rendered)
2660
2760
  if (
2661
- !renderedBreakdownTable &&
2761
+ !renderedBreakdownList &&
2662
2762
  metricResponse.metricResult &&
2663
2763
  metricResponse.metricResult.length > 0
2664
2764
  ) {
@@ -2684,8 +2784,8 @@ ${contextBlock}
2684
2784
  * Which vSphere object one VMware breakdown row describes, resolved
2685
2785
  * from the identity attributes the vcenter receiver stamped on it —
2686
2786
  * most specific first, in the same order the ingest inventory scan
2687
- * uses, so the Kind column here and the inventory's `kind` never
2688
- * disagree about the same series. A VM row carries its parent host
2787
+ * uses, so the kind an Affected Resources item is titled with and the
2788
+ * inventory's `kind` never disagree about the same series. A VM row carries its parent host
2689
2789
  * (and often a resource pool), so the VM check must run before the
2690
2790
  * host and pool checks.
2691
2791
  */
@@ -2719,6 +2819,98 @@ ${contextBlock}
2719
2819
  return undefined;
2720
2820
  }
2721
2821
 
2822
+ /*
2823
+ * One vSphere object as an "Affected Resources" list item, titled by
2824
+ * the object itself, with the ESXi host and cluster it sits under
2825
+ * listed beneath it.
2826
+ */
2827
+ private static getVMwareAffectedResourceEntry(input: {
2828
+ resource: VMwareAffectedResource;
2829
+ vcenterName: string;
2830
+ value: string;
2831
+ }): AffectedResourceListEntry {
2832
+ const resource: VMwareAffectedResource = input.resource;
2833
+
2834
+ const kind: string | undefined =
2835
+ MonitorCriteriaEvaluator.getVMwareAffectedResourceKind(resource);
2836
+
2837
+ let name: string = "";
2838
+ let showHost: boolean = false;
2839
+
2840
+ switch (kind) {
2841
+ case "Virtual Machine":
2842
+ /*
2843
+ * The VM's display name, falling back to its instance UUID
2844
+ * (`vcenter.vm.id`) when the receiver stamped no name. The Host
2845
+ * detail is the ESXi host the VM is running on.
2846
+ */
2847
+ name = AffectedResourceList.code(resource.vmName || resource.vmId);
2848
+ showHost = true;
2849
+ break;
2850
+ case "Resource Pool":
2851
+ /*
2852
+ * Pool names are only unique within a parent, so the inventory
2853
+ * path is shown next to the name. The Host detail is the owner
2854
+ * host for a pool on a standalone ESXi host (pools under a
2855
+ * cluster show the cluster).
2856
+ */
2857
+ name = AffectedResourceList.codeWithId({
2858
+ name: resource.resourcePoolName,
2859
+ id: resource.resourcePoolPath,
2860
+ });
2861
+ showHost = true;
2862
+ break;
2863
+ case "Host":
2864
+ // The host IS the resource; a "Host:" detail would repeat it.
2865
+ name = AffectedResourceList.code(resource.hostName);
2866
+ break;
2867
+ case "Datastore":
2868
+ name = AffectedResourceList.code(resource.datastoreName);
2869
+ break;
2870
+ case "Cluster":
2871
+ name = AffectedResourceList.code(resource.clusterName);
2872
+ break;
2873
+ case "Datacenter":
2874
+ name = AffectedResourceList.code(resource.datacenterName);
2875
+ break;
2876
+ default:
2877
+ break;
2878
+ }
2879
+
2880
+ const details: Array<AffectedResourceListDetail> = [];
2881
+
2882
+ if (showHost && resource.hostName) {
2883
+ details.push({
2884
+ label: "Host",
2885
+ value: AffectedResourceList.code(resource.hostName),
2886
+ });
2887
+ }
2888
+
2889
+ if (kind !== "Cluster" && resource.clusterName) {
2890
+ details.push({
2891
+ label: "Cluster",
2892
+ value: AffectedResourceList.code(resource.clusterName),
2893
+ });
2894
+ }
2895
+
2896
+ if (!kind || !name) {
2897
+ // No vSphere identity on the series — it describes the vCenter as a whole.
2898
+ return {
2899
+ kind: "vCenter",
2900
+ name: AffectedResourceList.code(input.vcenterName),
2901
+ value: input.value,
2902
+ details: details,
2903
+ };
2904
+ }
2905
+
2906
+ return {
2907
+ kind: kind,
2908
+ name: name,
2909
+ value: input.value,
2910
+ details: details,
2911
+ };
2912
+ }
2913
+
2722
2914
  private static buildVMwareRootCauseContext(input: {
2723
2915
  dataToProcess: DataToProcess;
2724
2916
  monitorStep: MonitorStep;
@@ -2802,18 +2994,18 @@ ${contextBlock}
2802
2994
  sections.push(`**vCenter Details**\n${vcenterDetails.join("\n")}`);
2803
2995
  }
2804
2996
 
2805
- // Affected resources (Resource / Kind / Host / Cluster / Value)
2806
- let renderedBreakdownTable: boolean = false;
2997
+ // Affected resources: a ranked list of object, host and cluster
2998
+ let renderedBreakdownList: boolean = false;
2807
2999
 
2808
3000
  if (breakdown && breakdown.affectedResources.length > 0) {
2809
3001
  /*
2810
3002
  * K8s/Proxmox-parity render: drop zero-value rows, worst (highest)
2811
3003
  * first, top 10. For count metrics filtered to an unhealthy state
2812
3004
  * (`vcenter.datacenter.host.count{status=red}`) the non-zero rows
2813
- * ARE the objects with a problem, so the table reads correctly;
3005
+ * ARE the objects with a problem, so the list reads correctly;
2814
3006
  * a zero row means "nothing in that state" and adds nothing. The
2815
3007
  * per-series criteria evaluation is what actually alerts; this
2816
- * table is supplementary context only.
3008
+ * list is supplementary context only.
2817
3009
  */
2818
3010
  const sortedResources: Array<VMwareAffectedResource> = [
2819
3011
  ...breakdown.affectedResources,
@@ -2826,7 +3018,7 @@ ${contextBlock}
2826
3018
  });
2827
3019
 
2828
3020
  /*
2829
- * Skip the table when no row carries any identity attribute — it
3021
+ * Skip the list when no row carries any identity attribute — it
2830
3022
  * would add nothing. Every vcenter-receiver series carries at
2831
3023
  * least `vcenter.datacenter.name`, so this only trips on a
2832
3024
  * breakdown the worker could not attribute at all.
@@ -2841,96 +3033,35 @@ ${contextBlock}
2841
3033
 
2842
3034
  if (sortedResources.length > 0 && hasIdentity) {
2843
3035
  const resourcesToShow: Array<VMwareAffectedResource> =
2844
- sortedResources.slice(0, 10);
2845
-
2846
- const resourceLines: Array<string> = [];
2847
- resourceLines.push(`| Resource | Kind | Host | Cluster | Value |`);
2848
- resourceLines.push(`| --- | --- | --- | --- | --- |`);
2849
-
2850
- for (const resource of resourcesToShow) {
2851
- const kind: string | undefined =
2852
- MonitorCriteriaEvaluator.getVMwareAffectedResourceKind(resource);
2853
-
2854
- let resourceCell: string = "-";
2855
- let hostCell: string = "-";
2856
-
2857
- switch (kind) {
2858
- case "Virtual Machine":
2859
- /*
2860
- * The VM's display name, falling back to its instance
2861
- * UUID (`vcenter.vm.id`) when the receiver stamped no
2862
- * name. The Host column carries the ESXi host the VM is
2863
- * running on.
2864
- */
2865
- resourceCell = resource.vmName
2866
- ? `\`${resource.vmName}\``
2867
- : `\`${resource.vmId}\``;
2868
- hostCell = resource.hostName ? `\`${resource.hostName}\`` : "-";
2869
- break;
2870
- case "Resource Pool":
2871
- /*
2872
- * Pool names are only unique within a parent, so the
2873
- * inventory path is shown next to the name. The Host
2874
- * column is the owner host for a pool on a standalone
2875
- * ESXi host (pools under a cluster show the cluster).
2876
- */
2877
- if (resource.resourcePoolName && resource.resourcePoolPath) {
2878
- resourceCell = `\`${resource.resourcePoolName}\` (\`${resource.resourcePoolPath}\`)`;
2879
- } else {
2880
- resourceCell = `\`${resource.resourcePoolPath || resource.resourcePoolName}\``;
2881
- }
2882
- hostCell = resource.hostName ? `\`${resource.hostName}\`` : "-";
2883
- break;
2884
- case "Host":
2885
- // The host IS the resource; repeating it in the Host column adds nothing.
2886
- resourceCell = `\`${resource.hostName}\``;
2887
- break;
2888
- case "Datastore":
2889
- resourceCell = `\`${resource.datastoreName}\``;
2890
- break;
2891
- case "Cluster":
2892
- resourceCell = `\`${resource.clusterName}\``;
2893
- break;
2894
- case "Datacenter":
2895
- resourceCell = `\`${resource.datacenterName}\``;
2896
- break;
2897
- default:
2898
- break;
2899
- }
2900
-
2901
- const kindCell: string = kind || "-";
2902
- const clusterCell: string =
2903
- kind !== "Cluster" && resource.clusterName
2904
- ? `\`${resource.clusterName}\``
2905
- : "-";
2906
-
2907
- resourceLines.push(
2908
- `| ${resourceCell} | ${kindCell} | ${hostCell} | ${clusterCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
2909
- {
2910
- platform: "vmware",
2911
- metricName: breakdown.metricName,
2912
- value: resource.metricValue,
2913
- },
2914
- )} |`,
2915
- );
2916
- }
2917
-
2918
- if (sortedResources.length > 10) {
2919
- resourceLines.push(
2920
- `\n*... and ${sortedResources.length - 10} more affected resources*`,
2921
- );
2922
- }
3036
+ sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
2923
3037
 
2924
3038
  sections.push(
2925
- `\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
3039
+ AffectedResourceList.render({
3040
+ heading: "Affected Resources",
3041
+ overflowNoun: "affected resources",
3042
+ totalCount: sortedResources.length,
3043
+ entries: resourcesToShow.map(
3044
+ (resource: VMwareAffectedResource): AffectedResourceListEntry => {
3045
+ return MonitorCriteriaEvaluator.getVMwareAffectedResourceEntry({
3046
+ resource: resource,
3047
+ vcenterName: breakdown.vcenterName,
3048
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
3049
+ platform: "vmware",
3050
+ metricName: breakdown.metricName,
3051
+ value: resource.metricValue,
3052
+ }),
3053
+ });
3054
+ },
3055
+ ),
3056
+ }),
2926
3057
  );
2927
- renderedBreakdownTable = true;
3058
+ renderedBreakdownList = true;
2928
3059
  }
2929
3060
  }
2930
3061
 
2931
- // Metric results summary (fallback context when no table rendered)
3062
+ // Metric results summary (fallback context when no list rendered)
2932
3063
  if (
2933
- !renderedBreakdownTable &&
3064
+ !renderedBreakdownList &&
2934
3065
  metricResponse.metricResult &&
2935
3066
  metricResponse.metricResult.length > 0
2936
3067
  ) {
@@ -2952,6 +3083,55 @@ ${contextBlock}
2952
3083
  return sections.length > 0 ? sections.join("\n") : null;
2953
3084
  }
2954
3085
 
3086
+ /*
3087
+ * One Docker Swarm task as an "Affected Tasks" list item, with the
3088
+ * service it belongs to and the node it is scheduled on beneath it.
3089
+ */
3090
+ private static getDockerSwarmAffectedResourceEntry(input: {
3091
+ resource: DockerSwarmAffectedResource;
3092
+ clusterName: string;
3093
+ value: string;
3094
+ }): AffectedResourceListEntry {
3095
+ const resource: DockerSwarmAffectedResource = input.resource;
3096
+
3097
+ let kind: string = "Cluster";
3098
+ let name: string = AffectedResourceList.code(input.clusterName);
3099
+
3100
+ if (resource.containerName) {
3101
+ kind = "Task";
3102
+ name = AffectedResourceList.code(resource.containerName);
3103
+ } else if (resource.serviceName) {
3104
+ kind = "Service";
3105
+ name = AffectedResourceList.code(resource.serviceName);
3106
+ } else if (resource.nodeName) {
3107
+ kind = "Node";
3108
+ name = AffectedResourceList.code(resource.nodeName);
3109
+ }
3110
+
3111
+ const details: Array<AffectedResourceListDetail> = [];
3112
+
3113
+ if (resource.serviceName && kind !== "Service") {
3114
+ details.push({
3115
+ label: "Service",
3116
+ value: AffectedResourceList.code(resource.serviceName),
3117
+ });
3118
+ }
3119
+
3120
+ if (resource.nodeName && kind !== "Node") {
3121
+ details.push({
3122
+ label: "Node",
3123
+ value: AffectedResourceList.code(resource.nodeName),
3124
+ });
3125
+ }
3126
+
3127
+ return {
3128
+ kind: kind,
3129
+ name: name,
3130
+ value: input.value,
3131
+ details: details,
3132
+ };
3133
+ }
3134
+
2955
3135
  private static buildDockerSwarmRootCauseContext(input: {
2956
3136
  dataToProcess: DataToProcess;
2957
3137
  monitorStep: MonitorStep;
@@ -3020,15 +3200,15 @@ ${contextBlock}
3020
3200
  );
3021
3201
  }
3022
3202
 
3023
- // Affected resources (Task / Service / Node / Value)
3024
- let renderedBreakdownTable: boolean = false;
3203
+ // Affected tasks: a ranked list of task, service and node
3204
+ let renderedBreakdownList: boolean = false;
3025
3205
 
3026
3206
  if (breakdown && breakdown.affectedResources.length > 0) {
3027
3207
  /*
3028
3208
  * K8s/Proxmox-parity render: drop zero-value rows, worst (highest)
3029
3209
  * first, top 10. For the task-down template the breaching rows ARE
3030
3210
  * the zero-uptime ones — those still drive alerting through the
3031
- * per-series criteria evaluation; this table is supplementary
3211
+ * per-series criteria evaluation; this list is supplementary
3032
3212
  * context only.
3033
3213
  */
3034
3214
  const sortedResources: Array<DockerSwarmAffectedResource> = [
@@ -3044,7 +3224,7 @@ ${contextBlock}
3044
3224
  );
3045
3225
 
3046
3226
  /*
3047
- * Skip the table when no row carries any identity label
3227
+ * Skip the list when no row carries any identity label
3048
3228
  * (cluster-wide series) — it would add nothing.
3049
3229
  */
3050
3230
  const hasIdentity: boolean = sortedResources.some(
@@ -3055,50 +3235,41 @@ ${contextBlock}
3055
3235
 
3056
3236
  if (sortedResources.length > 0 && hasIdentity) {
3057
3237
  const resourcesToShow: Array<DockerSwarmAffectedResource> =
3058
- sortedResources.slice(0, 10);
3059
-
3060
- const resourceLines: Array<string> = [];
3061
- resourceLines.push(`| Task / Container | Service | Node | Value |`);
3062
- resourceLines.push(`| --- | --- | --- | --- |`);
3063
-
3064
- for (const resource of resourcesToShow) {
3065
- const taskCell: string = resource.containerName
3066
- ? `\`${resource.containerName}\``
3067
- : "-";
3068
- const serviceCell: string = resource.serviceName
3069
- ? `\`${resource.serviceName}\``
3070
- : "-";
3071
- const nodeCell: string = resource.nodeName
3072
- ? `\`${resource.nodeName}\``
3073
- : "-";
3074
-
3075
- resourceLines.push(
3076
- `| ${taskCell} | ${serviceCell} | ${nodeCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
3077
- {
3078
- platform: "dockerSwarm",
3079
- metricName: breakdown.metricName,
3080
- value: resource.metricValue,
3081
- },
3082
- )} |`,
3083
- );
3084
- }
3085
-
3086
- if (sortedResources.length > 10) {
3087
- resourceLines.push(
3088
- `\n*... and ${sortedResources.length - 10} more affected tasks*`,
3089
- );
3090
- }
3238
+ sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
3091
3239
 
3092
3240
  sections.push(
3093
- `\n\n**Affected Tasks** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
3241
+ AffectedResourceList.render({
3242
+ heading: "Affected Tasks",
3243
+ overflowNoun: "affected tasks",
3244
+ totalCount: sortedResources.length,
3245
+ entries: resourcesToShow.map(
3246
+ (
3247
+ resource: DockerSwarmAffectedResource,
3248
+ ): AffectedResourceListEntry => {
3249
+ return MonitorCriteriaEvaluator.getDockerSwarmAffectedResourceEntry(
3250
+ {
3251
+ resource: resource,
3252
+ clusterName: breakdown.clusterName,
3253
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue(
3254
+ {
3255
+ platform: "dockerSwarm",
3256
+ metricName: breakdown.metricName,
3257
+ value: resource.metricValue,
3258
+ },
3259
+ ),
3260
+ },
3261
+ );
3262
+ },
3263
+ ),
3264
+ }),
3094
3265
  );
3095
- renderedBreakdownTable = true;
3266
+ renderedBreakdownList = true;
3096
3267
  }
3097
3268
  }
3098
3269
 
3099
- // Metric results summary (fallback context when no table rendered)
3270
+ // Metric results summary (fallback context when no list rendered)
3100
3271
  if (
3101
- !renderedBreakdownTable &&
3272
+ !renderedBreakdownList &&
3102
3273
  metricResponse.metricResult &&
3103
3274
  metricResponse.metricResult.length > 0
3104
3275
  ) {
@@ -3121,37 +3292,61 @@ ${contextBlock}
3121
3292
  }
3122
3293
 
3123
3294
  /*
3124
- * Which rows of the Ceph affected-resources table actually BREACHED.
3295
+ * Which rows of an affected-resources list actually BREACHED.
3125
3296
  *
3126
3297
  * The blanket `metricValue > 0` below is right for every count- or
3127
3298
  * level-style criteria ("> 0 active health checks", "> 85 % used"), but
3128
- * it is exactly backwards for the availability signals: the OSD Down /
3129
- * OSD Out / Quorum Degraded criteria fire on `< 1` over ceph_osd_up /
3130
- * ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows are the down
3131
- * daemons. Dropping them rendered a table of the HEALTHY daemons under
3132
- * an incident that tells the reader to "check the root cause for the
3133
- * affected ceph_daemon label".
3299
+ * it is exactly backwards for the availability signals: the Ceph OSD
3300
+ * Down / OSD Out / Quorum Degraded criteria fire on `< 1` over
3301
+ * ceph_osd_up / ceph_osd_in / ceph_mon_quorum_status, so the ZERO rows
3302
+ * are the down daemons. Dropping them rendered a list of the HEALTHY
3303
+ * daemons under an incident that tells the reader to "check the root
3304
+ * cause for the affected ceph_daemon label".
3134
3305
  *
3135
3306
  * Invert only for a criteria that fires when the metric FALLS.
3136
3307
  * Everything else — every `> 0` health check, PG count, latency and
3137
- * capacity ratio, and every `= 0` recovery criteria — keeps the
3138
- * existing `> 0`, worst-highest-first table byte for byte.
3308
+ * capacity ratio, and (by default) every `= 0` recovery criteria —
3309
+ * keeps the existing `> 0`, worst-highest-first list byte for byte.
3310
+ *
3311
+ * `floorEqualityFiresOnFall` additionally counts a FIRING `= 0` (or
3312
+ * below) as a fall. Kubernetes needs it: k8s-node-not-ready and
3313
+ * k8s-etcd-no-leader fire on `= 0` over k8s.node.condition_ready /
3314
+ * etcd_server_has_leader, and zero is the floor of those metrics, so
3315
+ * the only way to reach it is down. It is gated on the criteria opening
3316
+ * an incident or alert because the same `= 0` is also how the restart,
3317
+ * failed-pod and unavailable-replica templates express RECOVERY, and
3318
+ * that all-clear must not become a list of healthy zero rows. Ceph
3319
+ * leaves it off: all of its `= 0` criteria are recoveries.
3139
3320
  */
3140
- private static getCephBreachPredicate(
3141
- criteriaInstance?: MonitorCriteriaInstance | undefined,
3142
- ): { matches: (value: number) => boolean; worstIsLowest: boolean } {
3321
+ private static getAffectedResourceBreachPredicate(input: {
3322
+ criteriaInstance?: MonitorCriteriaInstance | undefined;
3323
+ floorEqualityFiresOnFall?: boolean | undefined;
3324
+ }): AffectedResourceBreachPredicate {
3143
3325
  const metricFilters: Array<CriteriaFilter> = (
3144
- criteriaInstance?.data?.filters || []
3326
+ input.criteriaInstance?.data?.filters || []
3145
3327
  ).filter((f: CriteriaFilter) => {
3146
3328
  return f.checkOn === CheckOn.MetricValue && typeof f.value === "number";
3147
3329
  });
3148
3330
 
3331
+ const opensIncidentOrAlert: boolean =
3332
+ input.criteriaInstance?.data?.createIncidents === true ||
3333
+ input.criteriaInstance?.data?.createAlerts === true;
3334
+
3149
3335
  const firesWhenMetricFalls: boolean =
3150
3336
  metricFilters.length > 0 &&
3151
3337
  metricFilters.every((f: CriteriaFilter) => {
3152
- return (
3338
+ if (
3153
3339
  f.filterType === FilterType.LessThan ||
3154
3340
  f.filterType === FilterType.LessThanOrEqualTo
3341
+ ) {
3342
+ return true;
3343
+ }
3344
+
3345
+ return (
3346
+ input.floorEqualityFiresOnFall === true &&
3347
+ opensIncidentOrAlert &&
3348
+ f.filterType === FilterType.EqualTo &&
3349
+ (f.value as number) <= 0
3155
3350
  );
3156
3351
  });
3157
3352
 
@@ -3169,6 +3364,10 @@ ${contextBlock}
3169
3364
  return metricFilters.some((f: CriteriaFilter) => {
3170
3365
  const threshold: number = f.value as number;
3171
3366
 
3367
+ if (f.filterType === FilterType.EqualTo) {
3368
+ return value === threshold;
3369
+ }
3370
+
3172
3371
  return f.filterType === FilterType.LessThan
3173
3372
  ? value < threshold
3174
3373
  : value <= threshold;
@@ -3182,6 +3381,85 @@ ${contextBlock}
3182
3381
  };
3183
3382
  }
3184
3383
 
3384
+ /*
3385
+ * One Ceph daemon or pool as an "Affected Resources" list item, with
3386
+ * the pool and host it belongs to beneath it.
3387
+ */
3388
+ private static getCephAffectedResourceEntry(input: {
3389
+ resource: CephAffectedResource;
3390
+ clusterName: string;
3391
+ metricName: string;
3392
+ value: string;
3393
+ }): AffectedResourceListEntry {
3394
+ const resource: CephAffectedResource = input.resource;
3395
+
3396
+ /*
3397
+ * The worker stores every series' `name` label as poolName, but only
3398
+ * ceph_pool_metadata's `name` is a pool — and that series always
3399
+ * carries pool_id too. On ceph_health_detail, `name` is the health
3400
+ * check (RECENT_CRASH, OSD_NEARFULL, ...), and titling it "Pool" told
3401
+ * the reader a pool called OSD_NEARFULL was the problem. So a name is
3402
+ * treated as a pool only when it arrives with a pool id.
3403
+ */
3404
+ const pool: string = resource.poolId
3405
+ ? AffectedResourceList.codeWithId({
3406
+ name: resource.poolName,
3407
+ id: resource.poolId,
3408
+ })
3409
+ : "";
3410
+
3411
+ const otherName: string = resource.poolId
3412
+ ? ""
3413
+ : AffectedResourceList.code(resource.poolName);
3414
+
3415
+ const otherNameKind: string =
3416
+ input.metricName === "ceph_health_detail" ? "Health Check" : "Resource";
3417
+
3418
+ let kind: string = "Cluster";
3419
+ let name: string = AffectedResourceList.code(input.clusterName);
3420
+
3421
+ if (resource.daemon) {
3422
+ kind = "Daemon";
3423
+ name = AffectedResourceList.code(resource.daemon);
3424
+ } else if (pool) {
3425
+ kind = "Pool";
3426
+ name = pool;
3427
+ } else if (otherName) {
3428
+ kind = otherNameKind;
3429
+ name = otherName;
3430
+ } else if (resource.hostname) {
3431
+ kind = "Host";
3432
+ name = AffectedResourceList.code(resource.hostname);
3433
+ }
3434
+
3435
+ const details: Array<AffectedResourceListDetail> = [];
3436
+
3437
+ if (pool && name !== pool) {
3438
+ details.push({ label: "Pool", value: pool });
3439
+ }
3440
+
3441
+ if (otherName && name !== otherName) {
3442
+ details.push({
3443
+ label: otherNameKind === "Resource" ? "Name" : otherNameKind,
3444
+ value: otherName,
3445
+ });
3446
+ }
3447
+
3448
+ if (resource.hostname && kind !== "Host") {
3449
+ details.push({
3450
+ label: "Host",
3451
+ value: AffectedResourceList.code(resource.hostname),
3452
+ });
3453
+ }
3454
+
3455
+ return {
3456
+ kind: kind,
3457
+ name: name,
3458
+ value: input.value,
3459
+ details: details,
3460
+ };
3461
+ }
3462
+
3185
3463
  private static buildCephRootCauseContext(input: {
3186
3464
  dataToProcess: DataToProcess;
3187
3465
  monitorStep: MonitorStep;
@@ -3237,8 +3515,8 @@ ${contextBlock}
3237
3515
  sections.push(`**Ceph Cluster Details**\n${clusterDetails.join("\n")}`);
3238
3516
  }
3239
3517
 
3240
- // Affected resources (Daemon / Pool / Host / Value)
3241
- let renderedBreakdownTable: boolean = false;
3518
+ // Affected resources: a ranked list of daemon, pool and host
3519
+ let renderedBreakdownList: boolean = false;
3242
3520
 
3243
3521
  if (breakdown && breakdown.affectedResources.length > 0) {
3244
3522
  /*
@@ -3247,12 +3525,10 @@ ${contextBlock}
3247
3525
  * ceph_osd_in / ceph_mon_quorum_status the criteria is `< 1`, so
3248
3526
  * the zero rows are the whole point of the incident.
3249
3527
  */
3250
- const breach: {
3251
- matches: (value: number) => boolean;
3252
- worstIsLowest: boolean;
3253
- } = MonitorCriteriaEvaluator.getCephBreachPredicate(
3254
- input.criteriaInstance,
3255
- );
3528
+ const breach: AffectedResourceBreachPredicate =
3529
+ MonitorCriteriaEvaluator.getAffectedResourceBreachPredicate({
3530
+ criteriaInstance: input.criteriaInstance,
3531
+ });
3256
3532
 
3257
3533
  const sortedResources: Array<CephAffectedResource> = [
3258
3534
  ...breakdown.affectedResources,
@@ -3267,7 +3543,7 @@ ${contextBlock}
3267
3543
  });
3268
3544
 
3269
3545
  /*
3270
- * Skip the table when no row carries any identity label
3546
+ * Skip the list when no row carries any identity label
3271
3547
  * (cluster-wide series like ceph_health_status) — it would add
3272
3548
  * nothing.
3273
3549
  */
@@ -3279,57 +3555,36 @@ ${contextBlock}
3279
3555
 
3280
3556
  if (sortedResources.length > 0 && hasIdentity) {
3281
3557
  const resourcesToShow: Array<CephAffectedResource> =
3282
- sortedResources.slice(0, 10);
3283
-
3284
- const resourceLines: Array<string> = [];
3285
- resourceLines.push(`| Daemon | Pool | Host | Value |`);
3286
- resourceLines.push(`| --- | --- | --- | --- |`);
3287
-
3288
- for (const resource of resourcesToShow) {
3289
- const daemonCell: string = resource.daemon
3290
- ? `\`${resource.daemon}\``
3291
- : "-";
3292
-
3293
- let poolCell: string = "-";
3294
- if (resource.poolName && resource.poolId) {
3295
- poolCell = `\`${resource.poolName}\` (\`${resource.poolId}\`)`;
3296
- } else if (resource.poolName) {
3297
- poolCell = `\`${resource.poolName}\``;
3298
- } else if (resource.poolId) {
3299
- poolCell = `\`${resource.poolId}\``;
3300
- }
3301
-
3302
- const hostCell: string = resource.hostname
3303
- ? `\`${resource.hostname}\``
3304
- : "-";
3305
-
3306
- resourceLines.push(
3307
- `| ${daemonCell} | ${poolCell} | ${hostCell} | ${MonitorCriteriaEvaluator.formatPlatformResourceValue(
3308
- {
3309
- platform: "ceph",
3310
- metricName: breakdown.metricName,
3311
- value: resource.metricValue,
3312
- },
3313
- )} |`,
3314
- );
3315
- }
3316
-
3317
- if (sortedResources.length > 10) {
3318
- resourceLines.push(
3319
- `\n*... and ${sortedResources.length - 10} more affected resources*`,
3320
- );
3321
- }
3558
+ sortedResources.slice(0, AffectedResourceList.MAX_ENTRIES);
3322
3559
 
3323
3560
  sections.push(
3324
- `\n\n**Affected Resources** (${sortedResources.length} total)\n\n${resourceLines.join("\n")}`,
3561
+ AffectedResourceList.render({
3562
+ heading: "Affected Resources",
3563
+ overflowNoun: "affected resources",
3564
+ totalCount: sortedResources.length,
3565
+ entries: resourcesToShow.map(
3566
+ (resource: CephAffectedResource): AffectedResourceListEntry => {
3567
+ return MonitorCriteriaEvaluator.getCephAffectedResourceEntry({
3568
+ resource: resource,
3569
+ clusterName: breakdown.clusterName,
3570
+ metricName: breakdown.metricName,
3571
+ value: MonitorCriteriaEvaluator.formatPlatformResourceValue({
3572
+ platform: "ceph",
3573
+ metricName: breakdown.metricName,
3574
+ value: resource.metricValue,
3575
+ }),
3576
+ });
3577
+ },
3578
+ ),
3579
+ }),
3325
3580
  );
3326
- renderedBreakdownTable = true;
3581
+ renderedBreakdownList = true;
3327
3582
  }
3328
3583
  }
3329
3584
 
3330
- // Metric results summary (fallback context when no table rendered)
3585
+ // Metric results summary (fallback context when no list rendered)
3331
3586
  if (
3332
- !renderedBreakdownTable &&
3587
+ !renderedBreakdownList &&
3333
3588
  metricResponse.metricResult &&
3334
3589
  metricResponse.metricResult.length > 0
3335
3590
  ) {