@oneuptime/common 14.0.2 → 14.0.5

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 (765) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  4. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  5. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  6. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  9. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  10. package/Models/DatabaseModels/Incident.ts +55 -0
  11. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  12. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  13. package/Models/DatabaseModels/Index.ts +18 -0
  14. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  15. package/Models/DatabaseModels/Project.ts +60 -0
  16. package/Models/DatabaseModels/RumApplication.ts +52 -1
  17. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  18. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  19. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  25. package/Server/Middleware/MultipartFormData.ts +1 -1
  26. package/Server/Middleware/TelemetryIngest.ts +15 -8
  27. package/Server/Services/AlertReminderRuleService.ts +100 -24
  28. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  29. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  30. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  31. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  32. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  33. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  34. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  35. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  36. package/Server/Services/DatabaseServerService.ts +4546 -0
  37. package/Server/Services/DatabaseService.ts +14 -8
  38. package/Server/Services/DockerResourceService.ts +43 -2
  39. package/Server/Services/IncidentAlertService.ts +1811 -0
  40. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  41. package/Server/Services/IncidentService.ts +210 -10
  42. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  43. package/Server/Services/Index.ts +18 -0
  44. package/Server/Services/InventoryItemService.ts +113 -5
  45. package/Server/Services/IoTDeviceService.ts +145 -37
  46. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  47. package/Server/Services/PodmanResourceService.ts +43 -2
  48. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  49. package/Server/Services/ServiceService.ts +1 -1
  50. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  51. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  52. package/Server/Services/UserService.ts +14 -1
  53. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  54. package/Server/Types/Database/QueryHelper.ts +30 -1
  55. package/Server/Utils/AI/Toolbox/ResourceTools.ts +69 -1
  56. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  57. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +188 -6
  58. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  59. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2262 -755
  60. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  61. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  62. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  63. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  64. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  65. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  66. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  67. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  69. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  70. package/Server/Utils/StartServer.ts +26 -6
  71. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  72. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  73. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  74. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  75. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  76. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  77. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  78. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  79. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  80. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  81. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  82. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  83. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  84. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  85. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  86. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  87. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  88. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  89. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  90. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  91. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  92. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  93. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  94. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  95. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  96. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  97. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  98. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  99. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  100. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  101. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  102. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  103. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  104. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  105. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  106. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  107. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  108. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  109. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  110. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  111. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  112. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  113. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  114. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  115. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  116. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  117. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  118. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  119. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  120. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  121. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  122. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  123. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  124. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  125. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  126. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  127. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  128. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  129. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  130. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  131. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  132. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  133. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  134. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  135. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  136. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  137. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  138. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  139. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  140. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  141. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  142. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  143. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  144. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  145. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  146. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  147. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  148. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  149. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  150. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  151. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  152. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  153. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  154. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  155. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  156. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  157. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  158. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  159. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  160. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  161. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  162. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  163. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  164. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  165. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  166. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  167. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  168. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  169. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  170. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  171. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  172. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  173. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  174. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  175. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  176. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  177. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  178. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  179. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  180. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  181. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  182. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  183. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  184. package/Tests/ClassStrings.ts +178 -0
  185. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  186. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  187. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  188. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  189. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  190. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  191. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  192. package/Tests/RenderedMarkup.ts +518 -0
  193. package/Tests/ResponsiveVisibility.test.ts +490 -4
  194. package/Tests/ResponsiveVisibility.ts +505 -52
  195. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  196. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  197. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  198. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  199. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  200. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  201. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  202. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  203. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  204. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  205. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  206. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  207. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  208. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  209. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  210. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  211. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  212. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  213. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  214. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  215. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  216. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  217. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  218. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  219. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  220. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  221. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  222. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  223. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  224. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  225. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  226. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  227. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  228. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  229. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  230. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  231. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  232. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  233. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  234. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  235. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  236. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  237. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  238. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  239. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  240. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  241. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  242. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  243. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  244. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  245. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  246. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  247. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  248. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  249. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  250. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  251. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  252. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1719 -0
  253. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  254. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  255. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  256. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  257. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  258. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  259. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  260. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  261. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  262. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  263. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  264. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  265. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  266. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  267. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  268. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  269. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  270. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  271. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  272. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  273. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  274. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  275. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  276. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  277. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  278. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  279. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  280. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  281. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  282. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  283. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  284. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  285. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  286. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  287. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  288. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  289. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  290. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  291. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  292. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  293. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +298 -0
  294. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +271 -0
  295. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  296. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  297. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  298. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  299. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  300. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  301. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +181 -0
  302. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  303. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  304. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  305. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  306. package/Tests/UI/Components/Card.test.tsx +93 -2
  307. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  308. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  309. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  310. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  311. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  312. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  313. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  314. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  315. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  316. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  317. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  318. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  319. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  320. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  321. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  322. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  323. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  324. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  325. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  326. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  327. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  328. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  329. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  330. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  331. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  332. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  333. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  334. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  335. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  336. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  337. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  338. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  339. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  340. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  341. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  342. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  343. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +80 -0
  344. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +23 -0
  345. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  346. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  347. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  348. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  349. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  350. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  351. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  352. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  353. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  354. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  355. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  356. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  357. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  358. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  359. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  360. package/Tests/tsconfig.json +16 -0
  361. package/Types/AI/AIResourceContext.ts +7 -0
  362. package/Types/Dashboard/DashboardTemplates.ts +2 -1
  363. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  364. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  365. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  366. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  367. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  368. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  369. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  370. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  371. package/Types/Email/EmailTemplateType.ts +1 -0
  372. package/Types/Incident/IncidentAlertLink.ts +38 -0
  373. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  374. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  375. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  376. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  377. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  378. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  379. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  380. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  381. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  382. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  383. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  384. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  385. package/Types/Permission.ts +315 -0
  386. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  387. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  388. package/Types/Rules/RuleRun.ts +12 -0
  389. package/Types/Rum/SessionReplay.ts +25 -6
  390. package/Types/Rum/WebVitals.ts +52 -0
  391. package/Types/Telemetry/EntityType.ts +23 -0
  392. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  393. package/Types/Telemetry/ServiceType.ts +8 -0
  394. package/UI/Components/Banner/Banner.tsx +1 -1
  395. package/UI/Components/Card/Card.tsx +1 -1
  396. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  397. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  398. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  399. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  400. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  401. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  402. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  403. package/UI/Components/Forms/BasicForm.tsx +1 -1
  404. package/UI/Components/Header/Header.tsx +1 -1
  405. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  406. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  407. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  408. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  409. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  410. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  411. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  412. package/UI/Components/ModelTable/BaseModelTable.tsx +99 -21
  413. package/UI/Components/ModelTable/Column.ts +5 -0
  414. package/UI/Components/Navbar/NavBar.tsx +9 -2
  415. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  416. package/UI/Components/Page/Page.tsx +1 -1
  417. package/UI/Components/Pagination/Pagination.tsx +3 -3
  418. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  419. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  420. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  421. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  422. package/UI/Components/Table/TableHeader.tsx +47 -2
  423. package/UI/Components/Table/Types/Column.ts +7 -0
  424. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  425. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  426. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  427. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  428. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  429. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  430. package/UI/Styles/Theme.css +8 -1
  431. package/UI/Utils/Navigation.ts +89 -42
  432. package/UI/Utils/PermissionGate.ts +11 -2
  433. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  434. package/UI/Utils/TelemetryService.ts +4 -3
  435. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  436. package/Utils/LabelRuleImportExport.ts +2 -0
  437. package/Utils/MetricUnitUtil.ts +103 -11
  438. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  439. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  440. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  441. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  442. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  443. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  444. package/Utils/Rum/SessionTraceState.ts +212 -0
  445. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  446. package/Utils/Telemetry/EntityKey.ts +176 -0
  447. package/Utils/ValueFormatter.ts +16 -0
  448. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  449. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  451. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  453. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  454. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  455. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  456. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  457. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  458. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  459. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  460. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  461. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  462. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  463. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  464. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  465. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  466. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  467. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  469. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  470. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  471. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/Index.js +18 -0
  473. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  476. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  478. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  480. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  482. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  484. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  495. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  496. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  497. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  498. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  499. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  500. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  501. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  502. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  503. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  504. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  505. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  506. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  507. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  508. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  509. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  510. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  511. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  512. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  513. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  514. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  515. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  516. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  517. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  518. package/build/dist/Server/Services/DatabaseService.js +8 -4
  519. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  520. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  521. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  522. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  523. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  524. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  525. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  526. package/build/dist/Server/Services/IncidentService.js +164 -12
  527. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  528. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  529. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  530. package/build/dist/Server/Services/Index.js +18 -0
  531. package/build/dist/Server/Services/Index.js.map +1 -1
  532. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  533. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  534. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  535. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  536. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  537. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  538. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  539. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  540. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  541. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  542. package/build/dist/Server/Services/ServiceService.js +1 -1
  543. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  544. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  545. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  546. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  547. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  548. package/build/dist/Server/Services/UserService.js +13 -1
  549. package/build/dist/Server/Services/UserService.js.map +1 -1
  550. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  551. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  552. package/build/dist/Server/Types/Database/QueryHelper.js +26 -1
  553. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  554. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +46 -0
  555. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  556. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  557. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  558. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +134 -13
  559. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  560. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  561. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  562. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  563. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  564. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  565. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  566. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  567. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  568. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  569. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  570. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  571. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  572. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  573. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  574. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  575. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  576. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  577. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  578. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  579. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  580. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  581. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  582. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  583. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  584. package/build/dist/Server/Utils/StartServer.js +20 -4
  585. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  586. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  587. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  588. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  589. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  590. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  591. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  592. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  593. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  594. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  595. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  596. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  597. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  598. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  599. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  600. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  601. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  602. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  603. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  604. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  605. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  606. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  607. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  608. package/build/dist/Types/Dashboard/DashboardTemplates.js +2 -1
  609. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  610. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  611. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  612. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  613. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  614. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  615. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  616. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  617. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  618. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  619. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  620. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  621. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  622. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  623. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  624. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  625. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  626. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  627. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  628. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  629. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  630. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  631. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  632. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  633. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  634. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  635. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  636. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  637. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  638. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  639. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  640. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  641. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  642. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  643. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  644. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  645. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  646. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  647. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  648. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  649. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  650. package/build/dist/Types/Permission.js +276 -0
  651. package/build/dist/Types/Permission.js.map +1 -1
  652. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  653. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  654. package/build/dist/Types/Rules/RuleRun.js +4 -0
  655. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  656. package/build/dist/Types/Rum/WebVitals.js +25 -0
  657. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  658. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  659. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  660. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  661. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  662. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  663. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  664. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  665. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  666. package/build/dist/UI/Components/Card/Card.js +1 -1
  667. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  668. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  669. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  670. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  671. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  672. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  673. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  674. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  675. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  676. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  677. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  678. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  679. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  680. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  681. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  682. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  683. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  684. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  685. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  686. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  687. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  688. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  689. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  690. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  692. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  694. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  695. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  696. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  697. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  698. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +70 -18
  699. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  700. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  701. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  702. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  703. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  704. package/build/dist/UI/Components/Page/Page.js +1 -1
  705. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  706. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  707. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  708. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  709. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  710. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  711. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  712. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  713. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  714. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  715. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  716. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  717. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  718. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  719. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  720. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  721. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  722. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  723. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  724. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  725. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  727. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  728. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  729. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  730. package/build/dist/UI/Utils/Navigation.js +66 -33
  731. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  732. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  733. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  734. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  735. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  736. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  737. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  738. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  739. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  740. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  741. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  742. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  743. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  744. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  745. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  746. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  747. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  748. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  749. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  750. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  751. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  752. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  753. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  754. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  755. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  756. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  757. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  758. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  759. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  760. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  761. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  762. package/build/dist/Utils/ValueFormatter.js +15 -0
  763. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  764. package/package.json +1 -1
  765. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2254 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
4
+ import MetricMonitorCriteria from "../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
5
+ import MonitorStepResourceIdentity from "../../../Server/Utils/Monitor/MonitorStepResourceIdentity";
6
+ import { SeriesResourceRefs } from "../../../Server/Utils/Monitor/SeriesResourceLabels";
7
+ import AggregateModel from "../../../Types/BaseDatabase/AggregatedModel";
8
+ import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
9
+ import FilterCondition from "../../../Types/Filter/FilterCondition";
10
+ import MetricFormulaConfigData from "../../../Types/Metrics/MetricFormulaConfigData";
11
+ import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
12
+ import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
13
+ import MetricsViewConfig from "../../../Types/Metrics/MetricsViewConfig";
14
+ import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
15
+ import {
16
+ CheckOn,
17
+ CriteriaFilter,
18
+ EvaluateOverTimeType,
19
+ FilterType,
20
+ NoDataPolicy,
21
+ } from "../../../Types/Monitor/CriteriaFilter";
22
+ import {
23
+ DATABASE_ALERT_METRICS,
24
+ DATABASE_SERVER_ID_SCOPE_ATTRIBUTE,
25
+ DatabaseAlertMetric,
26
+ DatabaseAlertTemplate,
27
+ DatabaseAlertTemplateArgs,
28
+ UNALERTABLE_DATABASE_COUNTERS,
29
+ getAllDatabaseAlertTemplates,
30
+ getDatabaseAlertMetric,
31
+ getDatabaseAlertTemplateById,
32
+ getDatabaseAlertTemplates,
33
+ getDatabaseEnginesWithAlertTemplates,
34
+ } from "../../../Types/Monitor/DatabaseAlertTemplates";
35
+ import {
36
+ DATABASE_SERVER_METRICS,
37
+ DatabaseServerMetricDefinition,
38
+ } from "../../../Types/DatabaseServer/DatabaseServerMetricCatalog";
39
+ import {
40
+ DatabaseSystemDescriptor,
41
+ getDatabaseSystemDescriptor,
42
+ } from "../../../Types/DatabaseServer/DatabaseSystem";
43
+ import MonitorCriteriaInstance from "../../../Types/Monitor/MonitorCriteriaInstance";
44
+ import MonitorStep from "../../../Types/Monitor/MonitorStep";
45
+ import MonitorStepMetricViewConfigUtil from "../../../Types/Monitor/MonitorStepMetricViewConfigUtil";
46
+ import MonitorSteps from "../../../Types/Monitor/MonitorSteps";
47
+ import MonitorType from "../../../Types/Monitor/MonitorType";
48
+ import ObjectID from "../../../Types/ObjectID";
49
+ import RollingTime from "../../../Types/RollingTime/RollingTime";
50
+ import MetricFormulaEvaluator from "../../../Utils/Metrics/MetricFormulaEvaluator";
51
+ import MetricValueFormatter from "../../../Utils/Monitor/MetricValueFormatter";
52
+ import ValueFormatter from "../../../Utils/ValueFormatter";
53
+ import { describe, expect, test } from "@jest/globals";
54
+
55
+ /*
56
+ * The database alert library is the only way a Database's Alerts and
57
+ * Incidents tabs fill up without a user hand-building a Metrics monitor with
58
+ * a UUID filter they had to copy from the Documentation tab. Everything that
59
+ * can go wrong with it is silent:
60
+ *
61
+ * - a query that forgets the database scope watches the whole project and
62
+ * links its alerts to nothing;
63
+ * - a metric name the receiver does not emit (a typo, an optional metric
64
+ * nobody enabled, a counter) is a monitor that never fires — or, for a
65
+ * cumulative counter, fires once and never clears;
66
+ * - a criteria alias that matches no query falls back to query slot 0 and
67
+ * thresholds the wrong number;
68
+ * - a template offered to the wrong engine is a PostgreSQL monitor on a
69
+ * Redis server.
70
+ *
71
+ * So these assert all of it, for every template, and then EVALUATE the
72
+ * templates against synthetic receiver data through the real metric criteria
73
+ * evaluator and formula engine, so "fires when X, recovers when Y" is proven
74
+ * rather than described.
75
+ */
76
+
77
+ const DATABASE_SERVER_ID: string = "d0000000-0000-4000-8000-000000000042";
78
+
79
+ const REPO_ROOT: string = path.join(__dirname, "..", "..", "..", "..", "..");
80
+
81
+ function buildArgs(
82
+ overrides?: Partial<DatabaseAlertTemplateArgs>,
83
+ ): DatabaseAlertTemplateArgs {
84
+ return {
85
+ databaseServerId: DATABASE_SERVER_ID,
86
+ onlineMonitorStatusId: ObjectID.generate(),
87
+ offlineMonitorStatusId: ObjectID.generate(),
88
+ defaultIncidentSeverityId: ObjectID.generate(),
89
+ defaultAlertSeverityId: ObjectID.generate(),
90
+ monitorName: "PostgreSQL db.prod:5432",
91
+ ...overrides,
92
+ };
93
+ }
94
+
95
+ function getTemplate(id: string): DatabaseAlertTemplate {
96
+ const template: DatabaseAlertTemplate | undefined =
97
+ getDatabaseAlertTemplateById(id);
98
+
99
+ if (!template) {
100
+ throw new Error(`No database alert template ${id}`);
101
+ }
102
+
103
+ return template;
104
+ }
105
+
106
+ function getViewConfig(step: MonitorStep): MetricsViewConfig {
107
+ const viewConfig: MetricsViewConfig | undefined =
108
+ step.data?.metricMonitor?.metricViewConfig;
109
+
110
+ if (!viewConfig) {
111
+ throw new Error("The template built no metric monitor config");
112
+ }
113
+
114
+ return viewConfig;
115
+ }
116
+
117
+ function getCriteriaInstances(
118
+ step: MonitorStep,
119
+ ): Array<MonitorCriteriaInstance> {
120
+ return step.data?.monitorCriteria?.data?.monitorCriteriaInstanceArray || [];
121
+ }
122
+
123
+ function getUnhealthy(step: MonitorStep): MonitorCriteriaInstance {
124
+ return getCriteriaInstances(step).find(
125
+ (instance: MonitorCriteriaInstance): boolean => {
126
+ return Boolean(instance.data?.createIncidents);
127
+ },
128
+ )!;
129
+ }
130
+
131
+ function getHealthy(step: MonitorStep): MonitorCriteriaInstance {
132
+ return getCriteriaInstances(step).find(
133
+ (instance: MonitorCriteriaInstance): boolean => {
134
+ return !instance.data?.createIncidents;
135
+ },
136
+ )!;
137
+ }
138
+
139
+ const ALL_TEMPLATES: Array<DatabaseAlertTemplate> =
140
+ getAllDatabaseAlertTemplates();
141
+
142
+ const TEMPLATE_CASES: Array<[string, DatabaseAlertTemplate]> =
143
+ ALL_TEMPLATES.map(
144
+ (template: DatabaseAlertTemplate): [string, DatabaseAlertTemplate] => {
145
+ return [template.id, template];
146
+ },
147
+ );
148
+
149
+ describe("DatabaseAlertTemplates — the library", () => {
150
+ test("ships templates for the engines the collector receivers cover", () => {
151
+ expect(ALL_TEMPLATES.length).toBeGreaterThanOrEqual(30);
152
+ expect(getDatabaseEnginesWithAlertTemplates().sort()).toEqual(
153
+ [
154
+ "couchdb",
155
+ "elasticsearch",
156
+ "memcached",
157
+ "microsoft.sql_server",
158
+ "mongodb",
159
+ "mysql",
160
+ "oracle.db",
161
+ "postgresql",
162
+ "redis",
163
+ ].sort(),
164
+ );
165
+ });
166
+
167
+ test("ids are unique and self-prefixed", () => {
168
+ const ids: Array<string> = ALL_TEMPLATES.map(
169
+ (template: DatabaseAlertTemplate): string => {
170
+ return template.id;
171
+ },
172
+ );
173
+
174
+ expect(new Set<string>(ids).size).toBe(ids.length);
175
+
176
+ for (const id of ids) {
177
+ expect(id.startsWith("database-")).toBe(true);
178
+ }
179
+ });
180
+
181
+ test("names are unique within the set one database is offered", () => {
182
+ /*
183
+ * A database only ever sees one engine's set, so that is where a name
184
+ * collision would produce two indistinguishable cards (and two monitors
185
+ * named alike). Across engines "Engine Metrics Stopped" repeats on
186
+ * purpose.
187
+ */
188
+ for (const engine of getDatabaseEnginesWithAlertTemplates()) {
189
+ const names: Array<string> = getDatabaseAlertTemplates(engine).map(
190
+ (template: DatabaseAlertTemplate): string => {
191
+ return template.name;
192
+ },
193
+ );
194
+
195
+ expect(new Set<string>(names).size).toBe(names.length);
196
+ }
197
+ });
198
+
199
+ test.each(TEMPLATE_CASES)(
200
+ "%s is a complete Metrics template",
201
+ (_id: string, template: DatabaseAlertTemplate) => {
202
+ expect(template.monitorType).toBe(MonitorType.Metrics);
203
+ expect(template.name.trim().length).toBeGreaterThan(0);
204
+ expect(template.description.trim().length).toBeGreaterThan(20);
205
+ expect(["Critical", "Warning"]).toContain(template.severity);
206
+ expect([
207
+ "Availability",
208
+ "Connections",
209
+ "Replication",
210
+ "Memory",
211
+ "Performance",
212
+ "Storage",
213
+ ]).toContain(template.category);
214
+ expect(template.metricNames.length).toBeGreaterThan(0);
215
+ },
216
+ );
217
+
218
+ test("every engine offers an Engine Metrics Stopped template first", () => {
219
+ /*
220
+ * The closest thing to "database down" the engine telemetry can express,
221
+ * and the most important card on the page — so it leads every engine's
222
+ * list, and is Critical.
223
+ */
224
+ for (const engine of getDatabaseEnginesWithAlertTemplates()) {
225
+ const templates: Array<DatabaseAlertTemplate> =
226
+ getDatabaseAlertTemplates(engine);
227
+
228
+ expect(templates[0]!.name).toBe("Engine Metrics Stopped");
229
+ expect(templates[0]!.severity).toBe("Critical");
230
+ expect(templates[0]!.category).toBe("Availability");
231
+ }
232
+ });
233
+ });
234
+
235
+ describe("DatabaseAlertTemplates — engine matching", () => {
236
+ test("an engine gets exactly the templates of the receivers that monitor it", () => {
237
+ for (const engine of getDatabaseEnginesWithAlertTemplates()) {
238
+ const descriptor: DatabaseSystemDescriptor | null =
239
+ getDatabaseSystemDescriptor(engine);
240
+
241
+ expect(descriptor).not.toBeNull();
242
+
243
+ const offered: Array<DatabaseAlertTemplate> =
244
+ getDatabaseAlertTemplates(engine);
245
+
246
+ expect(offered.length).toBeGreaterThan(0);
247
+
248
+ for (const template of offered) {
249
+ expect(descriptor!.receiverTypes).toContain(template.receiver);
250
+ }
251
+
252
+ // ...and nothing written against another receiver.
253
+ for (const template of ALL_TEMPLATES) {
254
+ if (!descriptor!.receiverTypes.includes(template.receiver)) {
255
+ expect(offered).not.toContain(template);
256
+ }
257
+ }
258
+ }
259
+ });
260
+
261
+ test("every template's engine is a known engine its receiver monitors", () => {
262
+ for (const template of ALL_TEMPLATES) {
263
+ const descriptor: DatabaseSystemDescriptor | null =
264
+ getDatabaseSystemDescriptor(template.engine);
265
+
266
+ expect(descriptor).not.toBeNull();
267
+ expect(descriptor!.system).toBe(template.engine);
268
+ expect(descriptor!.receiverTypes).toContain(template.receiver);
269
+ }
270
+ });
271
+
272
+ test("never offers one engine's templates to another", () => {
273
+ const postgresIds: Set<string> = new Set<string>(
274
+ getDatabaseAlertTemplates("postgresql").map(
275
+ (template: DatabaseAlertTemplate): string => {
276
+ return template.id;
277
+ },
278
+ ),
279
+ );
280
+
281
+ for (const engine of ["redis", "mysql", "mongodb"]) {
282
+ for (const template of getDatabaseAlertTemplates(engine)) {
283
+ expect(postgresIds.has(template.id)).toBe(false);
284
+ }
285
+ }
286
+ });
287
+
288
+ test("accepts aliases and any casing of the engine", () => {
289
+ const expected: Array<string> = getDatabaseAlertTemplates("postgresql").map(
290
+ (template: DatabaseAlertTemplate): string => {
291
+ return template.id;
292
+ },
293
+ );
294
+
295
+ for (const spelling of ["postgres", "PostgreSQL", " pg "]) {
296
+ expect(
297
+ getDatabaseAlertTemplates(spelling).map(
298
+ (template: DatabaseAlertTemplate): string => {
299
+ return template.id;
300
+ },
301
+ ),
302
+ ).toEqual(expected);
303
+ }
304
+ });
305
+
306
+ test("a fork the family's receiver monitors gets the family's templates", () => {
307
+ const mysqlIds: Array<string> = getDatabaseAlertTemplates("mysql").map(
308
+ (template: DatabaseAlertTemplate): string => {
309
+ return template.id;
310
+ },
311
+ );
312
+ const redisIds: Array<string> = getDatabaseAlertTemplates("redis").map(
313
+ (template: DatabaseAlertTemplate): string => {
314
+ return template.id;
315
+ },
316
+ );
317
+
318
+ expect(
319
+ getDatabaseAlertTemplates("mariadb").map(
320
+ (template: DatabaseAlertTemplate): string => {
321
+ return template.id;
322
+ },
323
+ ),
324
+ ).toEqual(mysqlIds);
325
+ expect(
326
+ getDatabaseAlertTemplates("valkey").map(
327
+ (template: DatabaseAlertTemplate): string => {
328
+ return template.id;
329
+ },
330
+ ),
331
+ ).toEqual(redisIds);
332
+ });
333
+
334
+ test("an engine no template receiver covers gets nothing, never a guess", () => {
335
+ for (const engine of ["cassandra", "clickhouse", "neo4j", "sqlite"]) {
336
+ expect(getDatabaseAlertTemplates(engine)).toEqual([]);
337
+ }
338
+ });
339
+
340
+ test("a wire-compatible engine the receiver does not monitor gets nothing", () => {
341
+ /*
342
+ * CockroachDB speaks the PostgreSQL protocol, but the `postgresql`
343
+ * receiver does not monitor it — PostgreSQL templates on it would query
344
+ * metrics nobody emits. Only asserted when the engine is known at all.
345
+ */
346
+ if (getDatabaseSystemDescriptor("cockroachdb")) {
347
+ expect(getDatabaseAlertTemplates("cockroachdb")).toEqual([]);
348
+ }
349
+ });
350
+
351
+ test("an unknown, empty or missing engine gets nothing", () => {
352
+ for (const engine of [null, undefined, "", " ", "not-a-database"]) {
353
+ expect(getDatabaseAlertTemplates(engine)).toEqual([]);
354
+ }
355
+ });
356
+ });
357
+
358
+ describe("DatabaseAlertTemplates — every metric is one the receiver emits and a static threshold can use", () => {
359
+ test("the metric registry has no duplicates and names real engines", () => {
360
+ const names: Array<string> = DATABASE_ALERT_METRICS.map(
361
+ (metric: DatabaseAlertMetric): string => {
362
+ return metric.metricName;
363
+ },
364
+ );
365
+
366
+ expect(new Set<string>(names).size).toBe(names.length);
367
+
368
+ for (const metric of DATABASE_ALERT_METRICS) {
369
+ expect(getDatabaseSystemDescriptor(metric.engine)).not.toBeNull();
370
+ expect(metric.unit.length).toBeGreaterThan(0);
371
+ }
372
+ });
373
+
374
+ test.each(TEMPLATE_CASES)(
375
+ "%s reads only registered, thresholdable metrics of its own engine",
376
+ (_id: string, template: DatabaseAlertTemplate) => {
377
+ for (const metricName of template.metricNames) {
378
+ const metric: DatabaseAlertMetric | undefined =
379
+ getDatabaseAlertMetric(metricName);
380
+
381
+ expect(metric).toBeDefined();
382
+ expect(metric!.engine).toBe(template.engine);
383
+
384
+ /*
385
+ * THE rule of this library: the monitor path has no rate, so a
386
+ * cumulative counter compared against a threshold fires once and
387
+ * never clears.
388
+ */
389
+ expect(metric!.kind).not.toBe("counter");
390
+ }
391
+ },
392
+ );
393
+
394
+ test.each(TEMPLATE_CASES)(
395
+ "%s reads metric names carrying its receiver's prefix",
396
+ (_id: string, template: DatabaseAlertTemplate) => {
397
+ const descriptor: DatabaseSystemDescriptor = getDatabaseSystemDescriptor(
398
+ template.engine,
399
+ )!;
400
+
401
+ for (const metricName of template.metricNames) {
402
+ /*
403
+ * The Elasticsearch receiver also emits the node JVM's `jvm.*`
404
+ * metrics in the same batch — the one sanctioned exception.
405
+ */
406
+ if (
407
+ template.engine === "elasticsearch" &&
408
+ metricName.startsWith("jvm.")
409
+ ) {
410
+ continue;
411
+ }
412
+
413
+ expect(
414
+ descriptor.receiverMetricPrefixes.some((prefix: string): boolean => {
415
+ return metricName.startsWith(prefix);
416
+ }),
417
+ ).toBe(true);
418
+ }
419
+ },
420
+ );
421
+
422
+ test("no template reads a counter the library documents as unalertable", () => {
423
+ const counters: Set<string> = new Set<string>(
424
+ UNALERTABLE_DATABASE_COUNTERS.map(
425
+ (counter: { metricName: string }): string => {
426
+ return counter.metricName;
427
+ },
428
+ ),
429
+ );
430
+
431
+ expect(counters.size).toBeGreaterThan(5);
432
+
433
+ for (const template of ALL_TEMPLATES) {
434
+ for (const metricName of template.metricNames) {
435
+ expect(counters.has(metricName)).toBe(false);
436
+ }
437
+ }
438
+
439
+ // ...and none of them is registered as thresholdable.
440
+ for (const counter of UNALERTABLE_DATABASE_COUNTERS) {
441
+ expect(getDatabaseAlertMetric(counter.metricName)).toBeUndefined();
442
+ }
443
+ });
444
+
445
+ test("agrees with the Overview's metric catalog about which metrics are counters", () => {
446
+ /*
447
+ * DatabaseServerMetricCatalog charts a "counter" as a rate. If it calls a
448
+ * metric a counter, no template may threshold it; if it calls one a gauge,
449
+ * the registry here must not disagree.
450
+ */
451
+ for (const definition of DATABASE_SERVER_METRICS) {
452
+ const metric: DatabaseAlertMetric | undefined = getDatabaseAlertMetric(
453
+ definition.metricName,
454
+ );
455
+
456
+ if (!metric) {
457
+ continue;
458
+ }
459
+
460
+ if (definition.kind === "counter") {
461
+ expect(metric.kind).toBe("counter");
462
+ } else {
463
+ expect(metric.kind).not.toBe("counter");
464
+ }
465
+ }
466
+
467
+ const catalogCounters: Array<string> = DATABASE_SERVER_METRICS.filter(
468
+ (definition: DatabaseServerMetricDefinition): boolean => {
469
+ return definition.kind === "counter";
470
+ },
471
+ ).map((definition: DatabaseServerMetricDefinition): string => {
472
+ return definition.metricName;
473
+ });
474
+
475
+ for (const template of ALL_TEMPLATES) {
476
+ for (const metricName of template.metricNames) {
477
+ expect(catalogCounters).not.toContain(metricName);
478
+ }
479
+ }
480
+ });
481
+
482
+ test("an uptime ('elapsed') metric is only ever used to detect a restart", () => {
483
+ for (const template of ALL_TEMPLATES) {
484
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
485
+ const usesElapsed: boolean = template.metricNames.some(
486
+ (metricName: string): boolean => {
487
+ return getDatabaseAlertMetric(metricName)?.kind === "elapsed";
488
+ },
489
+ );
490
+
491
+ if (!usesElapsed) {
492
+ continue;
493
+ }
494
+
495
+ const filter: CriteriaFilter = getUnhealthy(step).data!.filters[0]!;
496
+
497
+ // A heartbeat (fires only on no data) or a "below" comparison.
498
+ expect(
499
+ filter.metricMonitorOptions?.onNoDataPolicy === NoDataPolicy.Trigger ||
500
+ filter.filterType === FilterType.LessThan,
501
+ ).toBe(true);
502
+ }
503
+ });
504
+
505
+ test("a template reading an optional metric says so, naming the metric", () => {
506
+ for (const template of ALL_TEMPLATES) {
507
+ for (const metricName of template.metricNames) {
508
+ const metric: DatabaseAlertMetric = getDatabaseAlertMetric(metricName)!;
509
+
510
+ if (metric.enabledByDefault) {
511
+ continue;
512
+ }
513
+
514
+ expect(template.description).toContain(metricName);
515
+ expect(template.description.toLowerCase()).toContain("enable");
516
+ }
517
+ }
518
+ });
519
+
520
+ test("every metric names the receiver that monitors its engine", () => {
521
+ for (const metric of DATABASE_ALERT_METRICS) {
522
+ expect(
523
+ getDatabaseSystemDescriptor(metric.engine)!.receiverTypes,
524
+ ).toContain(metric.receiver);
525
+ }
526
+
527
+ for (const template of ALL_TEMPLATES) {
528
+ for (const metricName of template.metricNames) {
529
+ expect(getDatabaseAlertMetric(metricName)!.receiver).toBe(
530
+ template.receiver,
531
+ );
532
+ }
533
+ }
534
+ });
535
+
536
+ function agentConfigPath(receiver: string): string {
537
+ return path.join(
538
+ REPO_ROOT,
539
+ "agents",
540
+ "DatabaseAgent",
541
+ "configs",
542
+ `${receiver}.yaml`,
543
+ );
544
+ }
545
+
546
+ // Every regex metacharacter, backslash included, taken literally.
547
+ function escapeRegExp(value: string): string {
548
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
549
+ }
550
+
551
+ function metricSwitchPattern(metricName: string, enabled: boolean): RegExp {
552
+ return new RegExp(
553
+ `\\n\\s+${escapeRegExp(metricName)}:\\s*\\n\\s+enabled:\\s*${
554
+ enabled ? "true" : "false"
555
+ }`,
556
+ );
557
+ }
558
+
559
+ test("every optional metric is switched on by the Database Agent's config for its receiver", () => {
560
+ /*
561
+ * The descriptions promise "the Database Agent enables it". This reads the
562
+ * agent's actual collector config and holds the promise to it: an
563
+ * optional metric the agent does not switch on would make its template
564
+ * silently inert for every agent user.
565
+ */
566
+ const optional: Array<DatabaseAlertMetric> = DATABASE_ALERT_METRICS.filter(
567
+ (metric: DatabaseAlertMetric): boolean => {
568
+ return !metric.enabledByDefault;
569
+ },
570
+ );
571
+
572
+ expect(optional.length).toBeGreaterThan(0);
573
+
574
+ for (const metric of optional) {
575
+ const configPath: string = agentConfigPath(metric.receiver);
576
+
577
+ expect(fs.existsSync(configPath)).toBe(true);
578
+ expect(fs.readFileSync(configPath, "utf8")).toMatch(
579
+ metricSwitchPattern(metric.metricName, true),
580
+ );
581
+ }
582
+ });
583
+
584
+ test("no default-on metric a template reads is switched off by a Database Agent config", () => {
585
+ for (const metric of DATABASE_ALERT_METRICS) {
586
+ const configPath: string = agentConfigPath(metric.receiver);
587
+
588
+ if (!fs.existsSync(configPath)) {
589
+ // No agent config for this receiver: nothing can switch it off.
590
+ continue;
591
+ }
592
+
593
+ expect(fs.readFileSync(configPath, "utf8")).not.toMatch(
594
+ metricSwitchPattern(metric.metricName, false),
595
+ );
596
+ }
597
+ });
598
+ });
599
+
600
+ /*
601
+ * Default-on in the receivers' metadata, yet measured never to arrive on the
602
+ * Database Agent's default setups (collector-contrib 0.161.0): SQL Server
603
+ * 2022 on Linux, connected directly, reads sys.dm_os_performance_counters,
604
+ * which has none of the first three (Windows performance counters only);
605
+ * Oracle Free 23 connected to its pluggable database (FREEPDB1) returned
606
+ * none of the last three. The Overview's catalog leaves the same ones out.
607
+ */
608
+ const NEVER_ARRIVE_ON_AGENT_SETUPS: Array<string> = [
609
+ "sqlserver.transaction.rate",
610
+ "sqlserver.transaction_log.usage",
611
+ "sqlserver.lock.wait_time.avg",
612
+ "oracledb.sessions.limit",
613
+ "oracledb.processes.usage",
614
+ "oracledb.processes.limit",
615
+ ];
616
+
617
+ /*
618
+ * Every metric seen arriving in those two runs, from the agent's own configs
619
+ * (agents/DatabaseAgent/configs/sqlserver.yaml and oracledb.yaml).
620
+ */
621
+ const SEEN_ARRIVING_FROM_AGENT: Record<string, Array<string>> = {
622
+ sqlserver: [
623
+ "sqlserver.batch.request.rate",
624
+ "sqlserver.batch.sql_compilation.rate",
625
+ "sqlserver.batch.sql_recompilation.rate",
626
+ "sqlserver.cpu.utilization",
627
+ "sqlserver.database.io",
628
+ "sqlserver.database.latency",
629
+ "sqlserver.deadlock.rate",
630
+ "sqlserver.lock.wait.rate",
631
+ "sqlserver.memory.grants.pending.count",
632
+ "sqlserver.page.buffer_cache.hit_ratio",
633
+ "sqlserver.page.life_expectancy",
634
+ "sqlserver.processes.blocked",
635
+ "sqlserver.user.connection.count",
636
+ ],
637
+ oracledb: [
638
+ "oracledb.cpu_time",
639
+ "oracledb.db.time",
640
+ "oracledb.enqueue_deadlocks",
641
+ "oracledb.exchange_deadlocks",
642
+ "oracledb.executions",
643
+ "oracledb.hard_parses",
644
+ "oracledb.logical_reads",
645
+ "oracledb.parse_calls",
646
+ "oracledb.pga_memory",
647
+ "oracledb.physical_reads",
648
+ "oracledb.sessions.usage",
649
+ "oracledb.sga.limit",
650
+ "oracledb.sga.usage",
651
+ "oracledb.tablespace.utilization",
652
+ "oracledb.tablespace_size.limit",
653
+ "oracledb.tablespace_size.usage",
654
+ "oracledb.user_commits",
655
+ "oracledb.user_rollbacks",
656
+ ],
657
+ };
658
+
659
+ describe("DatabaseAlertTemplates — only metrics that arrive on the Database Agent's default setup", () => {
660
+ test.each(NEVER_ARRIVE_ON_AGENT_SETUPS)(
661
+ "%s (never arrives on the agent's default setup) is neither registered nor read",
662
+ (metricName: string) => {
663
+ expect(getDatabaseAlertMetric(metricName)).toBeUndefined();
664
+
665
+ for (const template of ALL_TEMPLATES) {
666
+ expect(template.metricNames).not.toContain(metricName);
667
+ }
668
+ },
669
+ );
670
+
671
+ test.each(Object.keys(SEEN_ARRIVING_FROM_AGENT))(
672
+ "every metric a %s template reads was seen arriving from the agent",
673
+ (receiver: string) => {
674
+ const templates: Array<DatabaseAlertTemplate> = ALL_TEMPLATES.filter(
675
+ (template: DatabaseAlertTemplate): boolean => {
676
+ return template.receiver === receiver;
677
+ },
678
+ );
679
+
680
+ expect(templates.length).toBeGreaterThan(1);
681
+
682
+ for (const template of templates) {
683
+ for (const metricName of template.metricNames) {
684
+ expect(SEEN_ARRIVING_FROM_AGENT[receiver]).toContain(metricName);
685
+ }
686
+ }
687
+ },
688
+ );
689
+
690
+ test("no template thresholds a SQL Server '/sec' metric, a since-start total over a direct connection", () => {
691
+ /*
692
+ * Measured: sqlserver.batch.request.rate read 8, 13, 18, 23 on
693
+ * consecutive 10-second scrapes of an idle server — the counter's raw
694
+ * value, not a per-second rate. A threshold on one fires on the first
695
+ * deadlock and never clears.
696
+ */
697
+ const sqlServerRate: RegExp = /^sqlserver\..*\.rate$/;
698
+
699
+ for (const template of ALL_TEMPLATES) {
700
+ for (const metricName of template.metricNames) {
701
+ expect(sqlServerRate.test(metricName)).toBe(false);
702
+ }
703
+ }
704
+
705
+ expect(getDatabaseAlertMetric("sqlserver.deadlock.rate")).toBeUndefined();
706
+ expect(
707
+ UNALERTABLE_DATABASE_COUNTERS.some(
708
+ (counter: { metricName: string }): boolean => {
709
+ return counter.metricName === "sqlserver.deadlock.rate";
710
+ },
711
+ ),
712
+ ).toBe(true);
713
+ });
714
+
715
+ test("every Engine Metrics Stopped heartbeat reads a metric the receiver emits by default", () => {
716
+ /*
717
+ * The heartbeat going silent IS the alert. One that only an agent
718
+ * config switches on would fire, forever, the moment it was created on a
719
+ * database fed by a team's own collector that left it off.
720
+ */
721
+ for (const engine of getDatabaseEnginesWithAlertTemplates()) {
722
+ const heartbeat: DatabaseAlertTemplate =
723
+ getDatabaseAlertTemplates(engine)[0]!;
724
+
725
+ expect(heartbeat.name).toBe("Engine Metrics Stopped");
726
+
727
+ for (const metricName of heartbeat.metricNames) {
728
+ expect(getDatabaseAlertMetric(metricName)!.enabledByDefault).toBe(true);
729
+ }
730
+ }
731
+ });
732
+
733
+ test("SQL Server's and Oracle's sets, in display order", () => {
734
+ expect(
735
+ getDatabaseAlertTemplates("microsoft.sql_server").map(
736
+ (template: DatabaseAlertTemplate): string => {
737
+ return template.id;
738
+ },
739
+ ),
740
+ ).toEqual([
741
+ "database-sqlserver-engine-metrics-stopped",
742
+ "database-sqlserver-buffer-cache-hit-ratio-low",
743
+ "database-sqlserver-page-life-expectancy-low",
744
+ "database-sqlserver-sessions-blocked",
745
+ "database-sqlserver-memory-grants-pending",
746
+ ]);
747
+
748
+ expect(
749
+ getDatabaseAlertTemplates("oracle.db").map(
750
+ (template: DatabaseAlertTemplate): string => {
751
+ return template.id;
752
+ },
753
+ ),
754
+ ).toEqual([
755
+ "database-oracle-engine-metrics-stopped",
756
+ "database-oracle-tablespace-nearly-full",
757
+ ]);
758
+ });
759
+ });
760
+
761
+ describe("DatabaseAlertTemplates — the monitor step each template builds", () => {
762
+ test.each(TEMPLATE_CASES)(
763
+ "%s passes monitor validation as a Metrics monitor",
764
+ (_id: string, template: DatabaseAlertTemplate) => {
765
+ const steps: MonitorSteps = new MonitorSteps();
766
+ steps.data = {
767
+ monitorStepsInstanceArray: [template.getMonitorStep(buildArgs())],
768
+ defaultMonitorStatusId: ObjectID.generate(),
769
+ };
770
+
771
+ expect(MonitorSteps.getValidationError(steps, MonitorType.Metrics)).toBe(
772
+ null,
773
+ );
774
+ },
775
+ );
776
+
777
+ test.each(TEMPLATE_CASES)(
778
+ "%s scopes EVERY query to the one database, and nothing else",
779
+ (_id: string, template: DatabaseAlertTemplate) => {
780
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
781
+ const viewConfig: MetricsViewConfig = getViewConfig(step);
782
+
783
+ expect(viewConfig.queryConfigs.length).toBe(template.metricNames.length);
784
+
785
+ viewConfig.queryConfigs.forEach(
786
+ (queryConfig: MetricQueryConfigData, index: number) => {
787
+ expect(queryConfig.metricQueryData.filterData.metricName).toBe(
788
+ template.metricNames[index],
789
+ );
790
+ expect(
791
+ (
792
+ queryConfig.metricQueryData.filterData.attributes as Record<
793
+ string,
794
+ unknown
795
+ >
796
+ )[DATABASE_SERVER_ID_SCOPE_ATTRIBUTE],
797
+ ).toBe(DATABASE_SERVER_ID);
798
+ },
799
+ );
800
+
801
+ /*
802
+ * Not scoped by primaryEntityId: a user's collector that keeps a
803
+ * service.name makes that Service the rows' primary entity, and the
804
+ * monitor would match nothing.
805
+ */
806
+ expect(step.data?.metricMonitor?.telemetryServiceIds || []).toEqual([]);
807
+ },
808
+ );
809
+
810
+ test("the scope follows the database it is built for", () => {
811
+ const otherId: string = "d0000000-0000-4000-8000-0000000000ff";
812
+
813
+ for (const template of ALL_TEMPLATES) {
814
+ const step: MonitorStep = template.getMonitorStep(
815
+ buildArgs({ databaseServerId: otherId }),
816
+ );
817
+
818
+ for (const queryConfig of getViewConfig(step).queryConfigs) {
819
+ expect(
820
+ (
821
+ queryConfig.metricQueryData.filterData.attributes as Record<
822
+ string,
823
+ unknown
824
+ >
825
+ )[DATABASE_SERVER_ID_SCOPE_ATTRIBUTE],
826
+ ).toBe(otherId);
827
+ }
828
+ }
829
+ });
830
+
831
+ test.each(TEMPLATE_CASES)(
832
+ "%s is ungrouped, so every alert reads as this database",
833
+ (_id: string, template: DatabaseAlertTemplate) => {
834
+ expect(
835
+ MonitorStep.getGroupByAttributeKeys(
836
+ template.getMonitorStep(buildArgs()),
837
+ ),
838
+ ).toEqual([]);
839
+ },
840
+ );
841
+
842
+ test.each(TEMPLATE_CASES)(
843
+ "%s has distinct aliases, and every criteria alias resolves to one",
844
+ (_id: string, template: DatabaseAlertTemplate) => {
845
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
846
+ const viewConfig: MetricsViewConfig = getViewConfig(step);
847
+
848
+ const aliases: Array<string> = [
849
+ ...viewConfig.queryConfigs.map(
850
+ (queryConfig: MetricQueryConfigData): string => {
851
+ return queryConfig.metricAliasData?.metricVariable || "";
852
+ },
853
+ ),
854
+ ...viewConfig.formulaConfigs.map(
855
+ (formulaConfig: MetricFormulaConfigData): string => {
856
+ return formulaConfig.metricAliasData.metricVariable || "";
857
+ },
858
+ ),
859
+ ];
860
+
861
+ expect(aliases).not.toContain("");
862
+ expect(new Set<string>(aliases).size).toBe(aliases.length);
863
+
864
+ const offered: Array<string> =
865
+ MonitorStepMetricViewConfigUtil.getMetricVariables(step.data);
866
+
867
+ for (const instance of getCriteriaInstances(step)) {
868
+ for (const filter of instance.data?.filters || []) {
869
+ expect(filter.checkOn).toBe(CheckOn.MetricValue);
870
+ expect(offered).toContain(filter.metricMonitorOptions?.metricAlias);
871
+ }
872
+ }
873
+ },
874
+ );
875
+
876
+ test.each(TEMPLATE_CASES)(
877
+ "%s ships one breach and one recovery criteria",
878
+ (_id: string, template: DatabaseAlertTemplate) => {
879
+ const args: DatabaseAlertTemplateArgs = buildArgs();
880
+ const step: MonitorStep = template.getMonitorStep(args);
881
+ const instances: Array<MonitorCriteriaInstance> =
882
+ getCriteriaInstances(step);
883
+
884
+ expect(instances.length).toBe(2);
885
+
886
+ const unhealthy: MonitorCriteriaInstance = getUnhealthy(step);
887
+ const healthy: MonitorCriteriaInstance = getHealthy(step);
888
+
889
+ expect(unhealthy.data!.createIncidents).toBe(true);
890
+ expect(unhealthy.data!.createAlerts).toBe(true);
891
+ expect(unhealthy.data!.monitorStatusId).toEqual(
892
+ args.offlineMonitorStatusId,
893
+ );
894
+ expect(unhealthy.data!.incidents[0]!.title).toBe(
895
+ `[Database] ${template.name} - ${args.monitorName}`,
896
+ );
897
+ expect(unhealthy.data!.incidents[0]!.autoResolveIncident).toBe(true);
898
+ expect(unhealthy.data!.alerts[0]!.autoResolveAlert).toBe(true);
899
+ expect(unhealthy.data!.name.trim().length).toBeGreaterThan(0);
900
+ expect(unhealthy.data!.description).toMatch(/^Triggers when /);
901
+ expect(unhealthy.data!.description).not.toContain("undefined");
902
+
903
+ expect(healthy.data!.name).toBe("Healthy");
904
+ expect(healthy.data!.createIncidents).toBe(false);
905
+ expect(healthy.data!.createAlerts).toBe(false);
906
+ expect(healthy.data!.monitorStatusId).toEqual(args.onlineMonitorStatusId);
907
+
908
+ // Recovery never also fires on missing data.
909
+ for (const filter of healthy.data!.filters) {
910
+ expect(filter.metricMonitorOptions?.onNoDataPolicy).toBeUndefined();
911
+ }
912
+ },
913
+ );
914
+
915
+ test.each(TEMPLATE_CASES)(
916
+ "%s survives the JSON round trip a saved monitor takes",
917
+ (_id: string, template: DatabaseAlertTemplate) => {
918
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
919
+ const restored: MonitorStep = MonitorStep.fromJSON(step.toJSON());
920
+
921
+ expect(
922
+ restored.data?.metricMonitor?.metricViewConfig.queryConfigs.map(
923
+ (queryConfig: MetricQueryConfigData) => {
924
+ return queryConfig.metricQueryData.filterData;
925
+ },
926
+ ),
927
+ ).toEqual(
928
+ getViewConfig(step).queryConfigs.map(
929
+ (queryConfig: MetricQueryConfigData) => {
930
+ return queryConfig.metricQueryData.filterData;
931
+ },
932
+ ),
933
+ );
934
+
935
+ expect(
936
+ getUnhealthy(restored).data!.filters[0]!.metricMonitorOptions,
937
+ ).toEqual(getUnhealthy(step).data!.filters[0]!.metricMonitorOptions);
938
+ },
939
+ );
940
+
941
+ test.each(TEMPLATE_CASES)(
942
+ "%s links what it opens to the database (MonitorStepResourceIdentity)",
943
+ (_id: string, template: DatabaseAlertTemplate) => {
944
+ const monitor: Monitor = new Monitor();
945
+ monitor.monitorType = template.monitorType;
946
+ monitor.projectId = ObjectID.generate();
947
+ monitor.monitorSteps = new MonitorSteps();
948
+ monitor.monitorSteps.data = {
949
+ monitorStepsInstanceArray: [template.getMonitorStep(buildArgs())],
950
+ defaultMonitorStatusId: ObjectID.generate(),
951
+ };
952
+
953
+ const refs: SeriesResourceRefs =
954
+ MonitorStepResourceIdentity.extractResourceRefsFromMonitor({
955
+ monitor: monitor,
956
+ });
957
+
958
+ expect(refs.databaseServerIds).toEqual([DATABASE_SERVER_ID]);
959
+ // ...and claims no other resource.
960
+ expect(refs.serviceIds).toEqual([]);
961
+ expect(refs.hostNames).toEqual([]);
962
+ },
963
+ );
964
+ });
965
+
966
+ /*
967
+ * ---------------------------------------------------------------------------
968
+ * Behaviour: run templates against synthetic receiver data through the real
969
+ * formula engine and metric criteria evaluator.
970
+ * ---------------------------------------------------------------------------
971
+ */
972
+
973
+ type SamplesByAlias = Record<string, Array<number>>;
974
+
975
+ /*
976
+ * The native unit of every metric a template reads, keyed the way the worker
977
+ * loads it from MetricType (lowercased name → the receiver's own unit). The
978
+ * evaluator falls back to it when a query carries no legend unit, and the
979
+ * alert and incident text is formatted in it.
980
+ */
981
+ const NATIVE_UNITS_BY_METRIC_NAME: Record<string, string> = {};
982
+
983
+ for (const metric of DATABASE_ALERT_METRICS) {
984
+ NATIVE_UNITS_BY_METRIC_NAME[metric.metricName.toLowerCase()] = metric.unit;
985
+ }
986
+
987
+ /*
988
+ * What the telemetry worker hands the evaluator: one aggregated result per
989
+ * query (in query order), then one per formula, evaluated by the same
990
+ * MetricFormulaEvaluator the worker uses.
991
+ */
992
+ function buildResponse(
993
+ step: MonitorStep,
994
+ samplesByAlias: SamplesByAlias,
995
+ ): MetricMonitorResponse {
996
+ const viewConfig: MetricsViewConfig = getViewConfig(step);
997
+ const start: number = Date.UTC(2026, 0, 1, 12, 0, 0);
998
+
999
+ const results: Array<AggregatedResult> = viewConfig.queryConfigs.map(
1000
+ (queryConfig: MetricQueryConfigData): AggregatedResult => {
1001
+ const samples: Array<number> =
1002
+ samplesByAlias[queryConfig.metricAliasData?.metricVariable || ""] || [];
1003
+
1004
+ return {
1005
+ data: samples.map((value: number, index: number): AggregateModel => {
1006
+ return {
1007
+ timestamp: new Date(start + index * 60000),
1008
+ value: value,
1009
+ } as AggregateModel;
1010
+ }),
1011
+ };
1012
+ },
1013
+ );
1014
+
1015
+ viewConfig.formulaConfigs.forEach(
1016
+ (formulaConfig: MetricFormulaConfigData, index: number) => {
1017
+ results.push(
1018
+ MetricFormulaEvaluator.evaluateFormula({
1019
+ formula: formulaConfig.metricFormulaData.metricFormula,
1020
+ queryConfigs: viewConfig.queryConfigs,
1021
+ formulaConfigs: viewConfig.formulaConfigs.slice(0, index),
1022
+ results: results,
1023
+ }),
1024
+ );
1025
+ },
1026
+ );
1027
+
1028
+ return {
1029
+ projectId: ObjectID.generate(),
1030
+ metricResult: results,
1031
+ metricViewConfig: viewConfig,
1032
+ monitorId: ObjectID.generate(),
1033
+ nativeUnitsByMetricName: NATIVE_UNITS_BY_METRIC_NAME,
1034
+ };
1035
+ }
1036
+
1037
+ // The root cause one criteria filter writes, or null when it is not met.
1038
+ async function getRootCause(
1039
+ step: MonitorStep,
1040
+ filter: CriteriaFilter,
1041
+ response: MetricMonitorResponse,
1042
+ ): Promise<string | null> {
1043
+ return await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
1044
+ dataToProcess: response,
1045
+ // A copy: the evaluator writes its context back onto the filter.
1046
+ criteriaFilter: JSON.parse(JSON.stringify(filter)) as CriteriaFilter,
1047
+ monitorStep: step,
1048
+ });
1049
+ }
1050
+
1051
+ async function isInstanceMet(
1052
+ step: MonitorStep,
1053
+ instance: MonitorCriteriaInstance,
1054
+ response: MetricMonitorResponse,
1055
+ ): Promise<boolean> {
1056
+ const results: Array<boolean> = [];
1057
+
1058
+ for (const filter of instance.data!.filters) {
1059
+ const rootCause: string | null = await getRootCause(step, filter, response);
1060
+
1061
+ results.push(rootCause !== null);
1062
+ }
1063
+
1064
+ return instance.data!.filterCondition === FilterCondition.All
1065
+ ? results.every((met: boolean): boolean => {
1066
+ return met;
1067
+ })
1068
+ : results.some((met: boolean): boolean => {
1069
+ return met;
1070
+ });
1071
+ }
1072
+
1073
+ async function evaluate(
1074
+ templateId: string,
1075
+ samplesByAlias: SamplesByAlias,
1076
+ ): Promise<{ breached: boolean; healthy: boolean }> {
1077
+ const step: MonitorStep = getTemplate(templateId).getMonitorStep(buildArgs());
1078
+ const response: MetricMonitorResponse = buildResponse(step, samplesByAlias);
1079
+
1080
+ return {
1081
+ breached: await isInstanceMet(step, getUnhealthy(step), response),
1082
+ healthy: await isInstanceMet(step, getHealthy(step), response),
1083
+ };
1084
+ }
1085
+
1086
+ /*
1087
+ * One datapoint row as ingest stores it: the metric, its datapoint
1088
+ * attributes plus the database stamp every resource block of the batch
1089
+ * carries, and the one-minute bucket the scrape lands in.
1090
+ */
1091
+ interface ReceiverRow {
1092
+ minute: number;
1093
+ metricName: string;
1094
+ attributes: Record<string, string>;
1095
+ value: number;
1096
+ }
1097
+
1098
+ function foldBucket(
1099
+ values: Array<number>,
1100
+ aggregationType: MetricsAggregationType,
1101
+ ): number {
1102
+ switch (aggregationType) {
1103
+ case MetricsAggregationType.Sum:
1104
+ return values.reduce((total: number, value: number): number => {
1105
+ return total + value;
1106
+ }, 0);
1107
+ case MetricsAggregationType.Avg:
1108
+ return (
1109
+ values.reduce((total: number, value: number): number => {
1110
+ return total + value;
1111
+ }, 0) / values.length
1112
+ );
1113
+ case MetricsAggregationType.Min:
1114
+ return Math.min(...values);
1115
+ case MetricsAggregationType.Max:
1116
+ return Math.max(...values);
1117
+ case MetricsAggregationType.Count:
1118
+ return values.length;
1119
+ default:
1120
+ throw new Error(`No test fold for ${aggregationType}`);
1121
+ }
1122
+ }
1123
+
1124
+ /*
1125
+ * What the worker's ClickHouse query does with raw rows: keep the rows of the
1126
+ * query's metric whose attributes match every filter (the database scope
1127
+ * included), and fold each minute bucket with the query's aggregation. The
1128
+ * templates' aggregation contract is about exactly this step, so a receiver
1129
+ * quirk — a metric recorded twice in one scrape — has to go through it to be
1130
+ * caught.
1131
+ */
1132
+ function aggregateRows(
1133
+ step: MonitorStep,
1134
+ rows: Array<ReceiverRow>,
1135
+ ): SamplesByAlias {
1136
+ const samplesByAlias: SamplesByAlias = {};
1137
+
1138
+ for (const queryConfig of getViewConfig(step).queryConfigs) {
1139
+ const filterData: MetricQueryConfigData["metricQueryData"]["filterData"] =
1140
+ queryConfig.metricQueryData.filterData;
1141
+ const filters: Record<string, unknown> = (filterData.attributes ||
1142
+ {}) as Record<string, unknown>;
1143
+ const valuesByMinute: Map<number, Array<number>> = new Map<
1144
+ number,
1145
+ Array<number>
1146
+ >();
1147
+
1148
+ for (const row of rows) {
1149
+ const matches: boolean =
1150
+ row.metricName === filterData.metricName &&
1151
+ Object.keys(filters).every((key: string): boolean => {
1152
+ return row.attributes[key] === filters[key];
1153
+ });
1154
+
1155
+ if (!matches) {
1156
+ continue;
1157
+ }
1158
+
1159
+ valuesByMinute.set(row.minute, [
1160
+ ...(valuesByMinute.get(row.minute) || []),
1161
+ row.value,
1162
+ ]);
1163
+ }
1164
+
1165
+ samplesByAlias[queryConfig.metricAliasData!.metricVariable!] = Array.from(
1166
+ valuesByMinute.keys(),
1167
+ )
1168
+ .sort((a: number, b: number): number => {
1169
+ return a - b;
1170
+ })
1171
+ .map((minute: number): number => {
1172
+ return foldBucket(
1173
+ valuesByMinute.get(minute)!,
1174
+ filterData.aggegationType as MetricsAggregationType,
1175
+ );
1176
+ });
1177
+ }
1178
+
1179
+ return samplesByAlias;
1180
+ }
1181
+
1182
+ async function evaluateRows(
1183
+ templateId: string,
1184
+ rows: Array<ReceiverRow>,
1185
+ ): Promise<{ breached: boolean; healthy: boolean }> {
1186
+ const step: MonitorStep = getTemplate(templateId).getMonitorStep(buildArgs());
1187
+
1188
+ return await evaluate(templateId, aggregateRows(step, rows));
1189
+ }
1190
+
1191
+ // Rows a receiver records on every scrape of a ten-minute window.
1192
+ function everyMinute(
1193
+ rows: Array<Omit<ReceiverRow, "minute">>,
1194
+ minutes: number = 10,
1195
+ ): Array<ReceiverRow> {
1196
+ const all: Array<ReceiverRow> = [];
1197
+
1198
+ for (let minute: number = 0; minute < minutes; minute++) {
1199
+ for (const row of rows) {
1200
+ all.push({
1201
+ ...row,
1202
+ minute: minute,
1203
+ attributes: {
1204
+ ...row.attributes,
1205
+ [DATABASE_SERVER_ID_SCOPE_ATTRIBUTE]: DATABASE_SERVER_ID,
1206
+ },
1207
+ });
1208
+ }
1209
+ }
1210
+
1211
+ return all;
1212
+ }
1213
+
1214
+ describe("DatabaseAlertTemplates — behaviour against receiver data", () => {
1215
+ describe("PostgreSQL connections vs max_connections (a same-scrape Sum/Sum ratio)", () => {
1216
+ test("fires at 96% of max_connections, summed across databases and scrapes", async () => {
1217
+ /*
1218
+ * Three databases' backends per scrape, two scrapes per bucket: the
1219
+ * worker's Sum folds 2 × (60 + 20 + 16) = 192 backends over 2 × 100 =
1220
+ * 200 max_connections — 96%, exactly as if nothing had been summed.
1221
+ */
1222
+ expect(
1223
+ await evaluate("database-postgresql-connections-exhausted", {
1224
+ pg_backends: [192, 192, 194],
1225
+ pg_max_connections: [200, 200, 200],
1226
+ }),
1227
+ ).toEqual({ breached: true, healthy: false });
1228
+ });
1229
+
1230
+ test("recovers well below the limit", async () => {
1231
+ expect(
1232
+ await evaluate("database-postgresql-connections-exhausted", {
1233
+ pg_backends: [100, 90, 80],
1234
+ pg_max_connections: [200, 200, 200],
1235
+ }),
1236
+ ).toEqual({ breached: false, healthy: true });
1237
+ });
1238
+
1239
+ test("holds its status inside the recovery dead band instead of flapping", async () => {
1240
+ // 93%: below the 95% breach, above the 85.5% recovery.
1241
+ expect(
1242
+ await evaluate("database-postgresql-connections-exhausted", {
1243
+ pg_backends: [186, 186, 186],
1244
+ pg_max_connections: [200, 200, 200],
1245
+ }),
1246
+ ).toEqual({ breached: false, healthy: false });
1247
+ });
1248
+
1249
+ test("a single spike does not fire the sustained warning", async () => {
1250
+ expect(
1251
+ await evaluate("database-postgresql-connections-high", {
1252
+ pg_backends: [100, 170, 100],
1253
+ pg_max_connections: [200, 200, 200],
1254
+ }),
1255
+ ).toEqual({ breached: false, healthy: false });
1256
+
1257
+ expect(
1258
+ await evaluate("database-postgresql-connections-high", {
1259
+ pg_backends: [170, 172, 180],
1260
+ pg_max_connections: [200, 200, 200],
1261
+ }),
1262
+ ).toEqual({ breached: true, healthy: false });
1263
+ });
1264
+ });
1265
+
1266
+ describe("PostgreSQL replica replay lag", () => {
1267
+ test("fires when the worst replica stays 30 s or more behind", async () => {
1268
+ expect(
1269
+ await evaluate("database-postgresql-replica-replay-lag", {
1270
+ pg_replay_lag: [45, 60, 38],
1271
+ }),
1272
+ ).toEqual({ breached: true, healthy: false });
1273
+ });
1274
+
1275
+ test("recovers when replicas catch up", async () => {
1276
+ expect(
1277
+ await evaluate("database-postgresql-replica-replay-lag", {
1278
+ pg_replay_lag: [0, 1, 2],
1279
+ }),
1280
+ ).toEqual({ breached: false, healthy: true });
1281
+ });
1282
+
1283
+ test("the slowest replica's replay delay fires, whatever the other operations read", async () => {
1284
+ /*
1285
+ * postgresql.wal.delay as the receiver records it: seconds as a
1286
+ * double, one row per replica and operation.
1287
+ */
1288
+ const replica: (
1289
+ client: string,
1290
+ replay: number,
1291
+ ) => Array<Omit<ReceiverRow, "minute">> = (
1292
+ client: string,
1293
+ replay: number,
1294
+ ): Array<Omit<ReceiverRow, "minute">> => {
1295
+ return [
1296
+ {
1297
+ metricName: "postgresql.wal.delay",
1298
+ attributes: { operation: "write", replication_client: client },
1299
+ value: 0.002,
1300
+ },
1301
+ {
1302
+ metricName: "postgresql.wal.delay",
1303
+ attributes: { operation: "flush", replication_client: client },
1304
+ value: 0.004,
1305
+ },
1306
+ {
1307
+ metricName: "postgresql.wal.delay",
1308
+ attributes: { operation: "replay", replication_client: client },
1309
+ value: replay,
1310
+ },
1311
+ ];
1312
+ };
1313
+
1314
+ expect(
1315
+ await evaluateRows(
1316
+ "database-postgresql-replica-replay-lag",
1317
+ everyMinute(
1318
+ [...replica("10.0.0.7", 0.3), ...replica("10.0.0.8", 45.5)],
1319
+ 5,
1320
+ ),
1321
+ ),
1322
+ ).toEqual({ breached: true, healthy: false });
1323
+
1324
+ expect(
1325
+ await evaluateRows(
1326
+ "database-postgresql-replica-replay-lag",
1327
+ everyMinute(
1328
+ [...replica("10.0.0.7", 0.3), ...replica("10.0.0.8", 0.6)],
1329
+ 5,
1330
+ ),
1331
+ ),
1332
+ ).toEqual({ breached: false, healthy: true });
1333
+ });
1334
+
1335
+ test("reads the replay operation only", () => {
1336
+ const step: MonitorStep = getTemplate(
1337
+ "database-postgresql-replica-replay-lag",
1338
+ ).getMonitorStep(buildArgs());
1339
+
1340
+ expect(
1341
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1342
+ .attributes,
1343
+ ).toEqual({
1344
+ operation: "replay",
1345
+ [DATABASE_SERVER_ID_SCOPE_ATTRIBUTE]: DATABASE_SERVER_ID,
1346
+ });
1347
+ expect(
1348
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1349
+ .aggegationType,
1350
+ ).toBe(MetricsAggregationType.Max);
1351
+ });
1352
+ });
1353
+
1354
+ describe("Engine Metrics Stopped (the receiver's heartbeat going silent)", () => {
1355
+ test.each(
1356
+ getDatabaseEnginesWithAlertTemplates().map((engine: string): string => {
1357
+ return getDatabaseAlertTemplates(engine)[0]!.id;
1358
+ }),
1359
+ )(
1360
+ "%s fires on a silent window and is healthy while data flows",
1361
+ async (templateId: string) => {
1362
+ const step: MonitorStep =
1363
+ getTemplate(templateId).getMonitorStep(buildArgs());
1364
+ const alias: string =
1365
+ getViewConfig(step).queryConfigs[0]!.metricAliasData!.metricVariable!;
1366
+
1367
+ expect(await evaluate(templateId, { [alias]: [] })).toEqual({
1368
+ breached: true,
1369
+ healthy: false,
1370
+ });
1371
+
1372
+ expect(await evaluate(templateId, { [alias]: [2, 2, 2] })).toEqual({
1373
+ breached: false,
1374
+ healthy: true,
1375
+ });
1376
+ },
1377
+ );
1378
+
1379
+ test("carries the Trigger no-data policy on the breach side only", () => {
1380
+ for (const engine of getDatabaseEnginesWithAlertTemplates()) {
1381
+ const step: MonitorStep =
1382
+ getDatabaseAlertTemplates(engine)[0]!.getMonitorStep(buildArgs());
1383
+
1384
+ expect(
1385
+ getUnhealthy(step).data!.filters[0]!.metricMonitorOptions
1386
+ ?.onNoDataPolicy,
1387
+ ).toBe(NoDataPolicy.Trigger);
1388
+ }
1389
+ });
1390
+ });
1391
+
1392
+ describe("Server Restarted (uptime)", () => {
1393
+ test("fires on the first young uptime sample in the window", async () => {
1394
+ expect(
1395
+ await evaluate("database-mysql-restarted", {
1396
+ mysql_uptime: [86400, 86460, 45],
1397
+ }),
1398
+ ).toEqual({ breached: true, healthy: false });
1399
+ });
1400
+
1401
+ test("is healthy once the whole window has aged past the dead band", async () => {
1402
+ expect(
1403
+ await evaluate("database-redis-restarted", {
1404
+ redis_uptime: [7200, 7260, 7320],
1405
+ }),
1406
+ ).toEqual({ breached: false, healthy: true });
1407
+ });
1408
+
1409
+ test("uses MongoDB's millisecond uptime unit", async () => {
1410
+ expect(
1411
+ await evaluate("database-mongodb-restarted", {
1412
+ mongodb_uptime: [120000],
1413
+ }),
1414
+ ).toEqual({ breached: true, healthy: false });
1415
+
1416
+ // 30 minutes in ms: long past ten minutes.
1417
+ expect(
1418
+ await evaluate("database-mongodb-restarted", {
1419
+ mongodb_uptime: [1800000, 1860000],
1420
+ }),
1421
+ ).toEqual({ breached: false, healthy: true });
1422
+ });
1423
+
1424
+ test("reads the post-restart value of a bucket (Min), and fires on AnyValue", () => {
1425
+ for (const id of [
1426
+ "database-mysql-restarted",
1427
+ "database-redis-restarted",
1428
+ "database-mongodb-restarted",
1429
+ ]) {
1430
+ const step: MonitorStep = getTemplate(id).getMonitorStep(buildArgs());
1431
+
1432
+ expect(
1433
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1434
+ .aggegationType,
1435
+ ).toBe(MetricsAggregationType.Min);
1436
+ expect(
1437
+ getUnhealthy(step).data!.filters[0]!.metricMonitorOptions
1438
+ ?.metricAggregationType,
1439
+ ).toBe(EvaluateOverTimeType.AnyValue);
1440
+ expect(
1441
+ getHealthy(step).data!.filters[0]!.metricMonitorOptions
1442
+ ?.metricAggregationType,
1443
+ ).toBe(EvaluateOverTimeType.AllValues);
1444
+ }
1445
+ });
1446
+ });
1447
+
1448
+ describe("Redis memory fragmentation (two conditions)", () => {
1449
+ const GB: number = 1073741824;
1450
+ const MB: number = 1048576;
1451
+
1452
+ test("fires on a high ratio on a large instance", async () => {
1453
+ expect(
1454
+ await evaluate("database-redis-memory-fragmentation", {
1455
+ redis_fragmentation_ratio: [1.9, 2.1, 2.0],
1456
+ redis_fragmentation_memory_used: [4 * GB, 4 * GB, 4 * GB],
1457
+ }),
1458
+ ).toEqual({ breached: true, healthy: false });
1459
+ });
1460
+
1461
+ test("ignores the naturally high ratio of a tiny instance", async () => {
1462
+ expect(
1463
+ await evaluate("database-redis-memory-fragmentation", {
1464
+ redis_fragmentation_ratio: [6.5, 7.0, 6.8],
1465
+ redis_fragmentation_memory_used: [2 * MB, 2 * MB, 2 * MB],
1466
+ }),
1467
+ ).toEqual({ breached: false, healthy: true });
1468
+ });
1469
+
1470
+ test("recovers when the ratio clears, whatever the size", async () => {
1471
+ expect(
1472
+ await evaluate("database-redis-memory-fragmentation", {
1473
+ redis_fragmentation_ratio: [1.05, 1.1, 1.08],
1474
+ redis_fragmentation_memory_used: [4 * GB, 4 * GB, 4 * GB],
1475
+ }),
1476
+ ).toEqual({ breached: false, healthy: true });
1477
+ });
1478
+ });
1479
+
1480
+ describe("Redis memory near maxmemory", () => {
1481
+ test("fires at 95% of maxmemory", async () => {
1482
+ expect(
1483
+ await evaluate("database-redis-memory-near-maxmemory", {
1484
+ redis_memory_used: [950, 960, 955],
1485
+ redis_maxmemory: [1000, 1000, 1000],
1486
+ }),
1487
+ ).toEqual({ breached: true, healthy: false });
1488
+ });
1489
+
1490
+ test("never fires on a server with no maxmemory set (maxmemory = 0)", async () => {
1491
+ /*
1492
+ * Division by zero is not finite, so the formula engine drops those
1493
+ * points: an unlimited server has no "near the limit" and the criteria
1494
+ * sees no data — which it ignores rather than breaching on.
1495
+ */
1496
+ expect(
1497
+ await evaluate("database-redis-memory-near-maxmemory", {
1498
+ redis_memory_used: [950, 960, 955],
1499
+ redis_maxmemory: [0, 0, 0],
1500
+ }),
1501
+ ).toEqual({ breached: false, healthy: false });
1502
+ });
1503
+ });
1504
+
1505
+ describe("MongoDB connections vs the connections it can accept", () => {
1506
+ test("fires at 95% of current + available", async () => {
1507
+ expect(
1508
+ await evaluate("database-mongodb-connections-exhausted", {
1509
+ mongodb_connections_current: [950, 960, 970],
1510
+ mongodb_connections_available: [50, 40, 30],
1511
+ }),
1512
+ ).toEqual({ breached: true, healthy: false });
1513
+ });
1514
+
1515
+ test("is unaffected by MongoDB repeating the server-wide counts per database", async () => {
1516
+ // Four databases each report 100 current / 900 available: still 10%.
1517
+ expect(
1518
+ await evaluate("database-mongodb-connections-exhausted", {
1519
+ mongodb_connections_current: [400, 400, 400],
1520
+ mongodb_connections_available: [3600, 3600, 3600],
1521
+ }),
1522
+ ).toEqual({ breached: false, healthy: true });
1523
+ });
1524
+ });
1525
+
1526
+ describe("MySQL", () => {
1527
+ test("dirty pages at 80% of the buffer pool fire; a full but clean pool does not", async () => {
1528
+ expect(
1529
+ await evaluate("database-mysql-buffer-pool-dirty", {
1530
+ mysql_buffer_pool_dirty: [800, 810, 820],
1531
+ mysql_buffer_pool_size: [1000, 1000, 1000],
1532
+ }),
1533
+ ).toEqual({ breached: true, healthy: false });
1534
+
1535
+ expect(
1536
+ await evaluate("database-mysql-buffer-pool-dirty", {
1537
+ mysql_buffer_pool_dirty: [50, 40, 60],
1538
+ mysql_buffer_pool_size: [1000, 1000, 1000],
1539
+ }),
1540
+ ).toEqual({ breached: false, healthy: true });
1541
+ });
1542
+
1543
+ /*
1544
+ * Regression (e2e, collector-contrib 0.161.0): offered to MariaDB 11.4,
1545
+ * the template opened an alert reading "232167.26% (dirty 37666816 /
1546
+ * size 16224)" for a pool 18% dirty, and it never cleared: the mysql
1547
+ * receiver reports MariaDB's mysql.buffer_pool.limit as the pool's PAGE
1548
+ * count (8112), not its 134217728 bytes. One scrape of each server,
1549
+ * every buffer-pool metric exactly as ClickHouse stored it.
1550
+ */
1551
+ const MARIADB_11_4_SCRAPE: Array<Omit<ReceiverRow, "minute">> = [
1552
+ { metricName: "mysql.buffer_pool.limit", attributes: {}, value: 8112 },
1553
+ {
1554
+ metricName: "mysql.buffer_pool.usage",
1555
+ attributes: { status: "dirty" },
1556
+ value: 16203776,
1557
+ },
1558
+ {
1559
+ metricName: "mysql.buffer_pool.usage",
1560
+ attributes: { status: "clean" },
1561
+ value: 4653056,
1562
+ },
1563
+ {
1564
+ metricName: "mysql.buffer_pool.data_pages",
1565
+ attributes: { status: "dirty" },
1566
+ value: 989,
1567
+ },
1568
+ {
1569
+ metricName: "mysql.buffer_pool.data_pages",
1570
+ attributes: { status: "clean" },
1571
+ value: 284,
1572
+ },
1573
+ {
1574
+ metricName: "mysql.buffer_pool.pages",
1575
+ attributes: { kind: "total" },
1576
+ value: 8112,
1577
+ },
1578
+ {
1579
+ metricName: "mysql.buffer_pool.pages",
1580
+ attributes: { kind: "data" },
1581
+ value: 1273,
1582
+ },
1583
+ {
1584
+ metricName: "mysql.buffer_pool.pages",
1585
+ attributes: { kind: "free" },
1586
+ value: 6839,
1587
+ },
1588
+ {
1589
+ metricName: "mysql.buffer_pool.pages",
1590
+ attributes: { kind: "misc" },
1591
+ value: 0,
1592
+ },
1593
+ ];
1594
+
1595
+ const MYSQL_8_4_SCRAPE: Array<Omit<ReceiverRow, "minute">> = [
1596
+ {
1597
+ metricName: "mysql.buffer_pool.limit",
1598
+ attributes: {},
1599
+ value: 134217728,
1600
+ },
1601
+ {
1602
+ metricName: "mysql.buffer_pool.usage",
1603
+ attributes: { status: "dirty" },
1604
+ value: 0,
1605
+ },
1606
+ {
1607
+ metricName: "mysql.buffer_pool.usage",
1608
+ attributes: { status: "clean" },
1609
+ value: 19431424,
1610
+ },
1611
+ {
1612
+ metricName: "mysql.buffer_pool.data_pages",
1613
+ attributes: { status: "dirty" },
1614
+ value: 0,
1615
+ },
1616
+ {
1617
+ metricName: "mysql.buffer_pool.data_pages",
1618
+ attributes: { status: "clean" },
1619
+ value: 1186,
1620
+ },
1621
+ {
1622
+ metricName: "mysql.buffer_pool.pages",
1623
+ attributes: { kind: "total" },
1624
+ value: 8192,
1625
+ },
1626
+ {
1627
+ metricName: "mysql.buffer_pool.pages",
1628
+ attributes: { kind: "data" },
1629
+ value: 1186,
1630
+ },
1631
+ {
1632
+ metricName: "mysql.buffer_pool.pages",
1633
+ attributes: { kind: "free" },
1634
+ value: 7006,
1635
+ },
1636
+ {
1637
+ metricName: "mysql.buffer_pool.pages",
1638
+ attributes: { kind: "misc" },
1639
+ value: 0,
1640
+ },
1641
+ ];
1642
+
1643
+ // The same scrape with `dirtyPages` of the pool's pages dirty.
1644
+ function withDirtyPages(
1645
+ scrape: Array<Omit<ReceiverRow, "minute">>,
1646
+ dirtyPages: number,
1647
+ ): Array<Omit<ReceiverRow, "minute">> {
1648
+ return scrape.map(
1649
+ (row: Omit<ReceiverRow, "minute">): Omit<ReceiverRow, "minute"> => {
1650
+ if (
1651
+ row.metricName === "mysql.buffer_pool.data_pages" &&
1652
+ row.attributes["status"] === "dirty"
1653
+ ) {
1654
+ return { ...row, value: dirtyPages };
1655
+ }
1656
+ if (
1657
+ row.metricName === "mysql.buffer_pool.usage" &&
1658
+ row.attributes["status"] === "dirty"
1659
+ ) {
1660
+ return { ...row, value: dirtyPages * 16384 };
1661
+ }
1662
+ return row;
1663
+ },
1664
+ );
1665
+ }
1666
+
1667
+ test("MariaDB 12% dirty is healthy, though its buffer_pool.limit is a page count", async () => {
1668
+ expect(
1669
+ await evaluateRows(
1670
+ "database-mysql-buffer-pool-dirty",
1671
+ everyMinute(MARIADB_11_4_SCRAPE),
1672
+ ),
1673
+ ).toEqual({ breached: false, healthy: true });
1674
+ });
1675
+
1676
+ test("MariaDB 80% dirty fires", async () => {
1677
+ expect(
1678
+ await evaluateRows(
1679
+ "database-mysql-buffer-pool-dirty",
1680
+ everyMinute(withDirtyPages(MARIADB_11_4_SCRAPE, 6500)),
1681
+ ),
1682
+ ).toEqual({ breached: true, healthy: false });
1683
+ });
1684
+
1685
+ test("MySQL 8.4, clean, is healthy; 80% dirty fires", async () => {
1686
+ expect(
1687
+ await evaluateRows(
1688
+ "database-mysql-buffer-pool-dirty",
1689
+ everyMinute(MYSQL_8_4_SCRAPE),
1690
+ ),
1691
+ ).toEqual({ breached: false, healthy: true });
1692
+ expect(
1693
+ await evaluateRows(
1694
+ "database-mysql-buffer-pool-dirty",
1695
+ everyMinute(withDirtyPages(MYSQL_8_4_SCRAPE, 6554)),
1696
+ ),
1697
+ ).toEqual({ breached: true, healthy: false });
1698
+ });
1699
+
1700
+ test("reads dirty data pages over the pool's TOTAL pages, never the byte metrics", () => {
1701
+ const template: DatabaseAlertTemplate = getTemplate(
1702
+ "database-mysql-buffer-pool-dirty",
1703
+ );
1704
+ expect(template.metricNames).toEqual([
1705
+ "mysql.buffer_pool.data_pages",
1706
+ "mysql.buffer_pool.pages",
1707
+ ]);
1708
+ expect(
1709
+ getViewConfig(template.getMonitorStep(buildArgs())).queryConfigs.map(
1710
+ (queryConfig: MetricQueryConfigData) => {
1711
+ const attributes: Record<string, unknown> = {
1712
+ ...(queryConfig.metricQueryData.filterData.attributes as Record<
1713
+ string,
1714
+ unknown
1715
+ >),
1716
+ };
1717
+ delete attributes[DATABASE_SERVER_ID_SCOPE_ATTRIBUTE];
1718
+ return [
1719
+ queryConfig.metricQueryData.filterData.metricName,
1720
+ attributes,
1721
+ ];
1722
+ },
1723
+ ),
1724
+ ).toEqual([
1725
+ ["mysql.buffer_pool.data_pages", { status: "dirty" }],
1726
+ // Unpinned, total + data + free + misc would halve the ratio.
1727
+ ["mysql.buffer_pool.pages", { kind: "total" }],
1728
+ ]);
1729
+ });
1730
+
1731
+ test("filters the thread kind it thresholds", () => {
1732
+ const running: MonitorStep = getTemplate(
1733
+ "database-mysql-threads-running-high",
1734
+ ).getMonitorStep(buildArgs());
1735
+ const connected: MonitorStep = getTemplate(
1736
+ "database-mysql-connections-high",
1737
+ ).getMonitorStep(buildArgs());
1738
+
1739
+ expect(
1740
+ (
1741
+ getViewConfig(running).queryConfigs[0]!.metricQueryData.filterData
1742
+ .attributes as Record<string, unknown>
1743
+ )["kind"],
1744
+ ).toBe("running");
1745
+ expect(
1746
+ (
1747
+ getViewConfig(connected).queryConfigs[0]!.metricQueryData.filterData
1748
+ .attributes as Record<string, unknown>
1749
+ )["kind"],
1750
+ ).toBe("connected");
1751
+ });
1752
+
1753
+ test("replica lag fires at 30 s and recovers at 0", async () => {
1754
+ expect(
1755
+ await evaluate("database-mysql-replica-lag", {
1756
+ mysql_replica_lag: [40, 55, 31],
1757
+ }),
1758
+ ).toEqual({ breached: true, healthy: false });
1759
+ expect(
1760
+ await evaluate("database-mysql-replica-lag", {
1761
+ mysql_replica_lag: [0, 0, 0],
1762
+ }),
1763
+ ).toEqual({ breached: false, healthy: true });
1764
+ });
1765
+ });
1766
+
1767
+ describe("SQL Server", () => {
1768
+ test("blocked sessions seen throughout the window fire; a short burst does not", async () => {
1769
+ expect(
1770
+ await evaluate("database-sqlserver-sessions-blocked", {
1771
+ sqlserver_processes_blocked: [1, 3, 2],
1772
+ }),
1773
+ ).toEqual({ breached: true, healthy: false });
1774
+
1775
+ // One minute of blocking is ordinary lock contention, not a chain.
1776
+ expect(
1777
+ await evaluate("database-sqlserver-sessions-blocked", {
1778
+ sqlserver_processes_blocked: [0, 6, 0],
1779
+ }),
1780
+ ).toEqual({ breached: false, healthy: false });
1781
+
1782
+ expect(
1783
+ await evaluate("database-sqlserver-sessions-blocked", {
1784
+ sqlserver_processes_blocked: [0, 0, 0],
1785
+ }),
1786
+ ).toEqual({ breached: false, healthy: true });
1787
+ });
1788
+
1789
+ test("blocked sessions keep the worst scrape of each minute, over ten minutes", () => {
1790
+ const step: MonitorStep = getTemplate(
1791
+ "database-sqlserver-sessions-blocked",
1792
+ ).getMonitorStep(buildArgs());
1793
+
1794
+ expect(
1795
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1796
+ .aggegationType,
1797
+ ).toBe(MetricsAggregationType.Max);
1798
+ expect(step.data!.metricMonitor!.rollingTime).toBe(
1799
+ RollingTime.Past10Minutes,
1800
+ );
1801
+ });
1802
+
1803
+ test("queries waiting for a memory grant fire when sustained and recover at 0", async () => {
1804
+ expect(
1805
+ await evaluate("database-sqlserver-memory-grants-pending", {
1806
+ sqlserver_memory_grants_pending: [2, 4, 1],
1807
+ }),
1808
+ ).toEqual({ breached: true, healthy: false });
1809
+ expect(
1810
+ await evaluate("database-sqlserver-memory-grants-pending", {
1811
+ sqlserver_memory_grants_pending: [0, 0, 0],
1812
+ }),
1813
+ ).toEqual({ breached: false, healthy: true });
1814
+ });
1815
+
1816
+ test("page life expectancy fires LOW and recovers high", async () => {
1817
+ expect(
1818
+ await evaluate("database-sqlserver-page-life-expectancy-low", {
1819
+ sqlserver_page_life_expectancy: [120, 90, 60],
1820
+ }),
1821
+ ).toEqual({ breached: true, healthy: false });
1822
+ expect(
1823
+ await evaluate("database-sqlserver-page-life-expectancy-low", {
1824
+ sqlserver_page_life_expectancy: [3000, 3100, 3200],
1825
+ }),
1826
+ ).toEqual({ breached: false, healthy: true });
1827
+ });
1828
+ });
1829
+
1830
+ describe("Oracle tablespace fullness (a 0..1 share, compared in percent)", () => {
1831
+ test("the fullest tablespace at 93% fires", async () => {
1832
+ expect(
1833
+ await evaluate("database-oracle-tablespace-nearly-full", {
1834
+ oracle_tablespace_utilization: [0.93, 0.95, 0.94],
1835
+ }),
1836
+ ).toEqual({ breached: true, healthy: false });
1837
+ });
1838
+
1839
+ test("recovers well below the threshold and holds inside the dead band", async () => {
1840
+ expect(
1841
+ await evaluate("database-oracle-tablespace-nearly-full", {
1842
+ oracle_tablespace_utilization: [0.5, 0.52, 0.51],
1843
+ }),
1844
+ ).toEqual({ breached: false, healthy: true });
1845
+
1846
+ // 85%: below the 90% breach, above the 81% recovery.
1847
+ expect(
1848
+ await evaluate("database-oracle-tablespace-nearly-full", {
1849
+ oracle_tablespace_utilization: [0.85, 0.85, 0.85],
1850
+ }),
1851
+ ).toEqual({ breached: false, healthy: false });
1852
+ });
1853
+
1854
+ test("a raw 0..1 value is never compared against 90 directly", async () => {
1855
+ /*
1856
+ * Without the percentage formula a full tablespace (1.0) would sit far
1857
+ * below a threshold of 90 and never fire.
1858
+ */
1859
+ expect(
1860
+ await evaluate("database-oracle-tablespace-nearly-full", {
1861
+ oracle_tablespace_utilization: [1, 1, 1],
1862
+ }),
1863
+ ).toEqual({ breached: true, healthy: false });
1864
+
1865
+ const step: MonitorStep = getTemplate(
1866
+ "database-oracle-tablespace-nearly-full",
1867
+ ).getMonitorStep(buildArgs());
1868
+
1869
+ expect(getUnhealthy(step).data!.filters[0]!.metricMonitorOptions).toEqual(
1870
+ expect.objectContaining({
1871
+ metricAlias: "oracle_tablespace_percent",
1872
+ }),
1873
+ );
1874
+ expect(
1875
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1876
+ .aggegationType,
1877
+ ).toBe(MetricsAggregationType.Max);
1878
+ });
1879
+ });
1880
+
1881
+ describe("Elasticsearch cluster health (a 0/1 status series)", () => {
1882
+ test("red fires on 1 and recovers on 0 with no dead band", async () => {
1883
+ expect(
1884
+ await evaluate("database-elasticsearch-cluster-red", {
1885
+ elasticsearch_health_red: [1, 1, 1],
1886
+ }),
1887
+ ).toEqual({ breached: true, healthy: false });
1888
+ expect(
1889
+ await evaluate("database-elasticsearch-cluster-red", {
1890
+ elasticsearch_health_red: [0, 0, 0],
1891
+ }),
1892
+ ).toEqual({ breached: false, healthy: true });
1893
+
1894
+ const step: MonitorStep = getTemplate(
1895
+ "database-elasticsearch-cluster-red",
1896
+ ).getMonitorStep(buildArgs());
1897
+
1898
+ expect(getHealthy(step).data!.filters[0]!.value).toBe(1);
1899
+ expect(getHealthy(step).data!.filters[0]!.filterType).toBe(
1900
+ FilterType.LessThan,
1901
+ );
1902
+ });
1903
+
1904
+ test("yellow filters the yellow status", () => {
1905
+ const step: MonitorStep = getTemplate(
1906
+ "database-elasticsearch-cluster-yellow",
1907
+ ).getMonitorStep(buildArgs());
1908
+
1909
+ expect(
1910
+ (
1911
+ getViewConfig(step).queryConfigs[0]!.metricQueryData.filterData
1912
+ .attributes as Record<string, unknown>
1913
+ )["status"],
1914
+ ).toBe("yellow");
1915
+ });
1916
+ });
1917
+
1918
+ describe("Elasticsearch JVM heap (the receiver records heap.used twice per scrape)", () => {
1919
+ const GIB: number = 1073741824;
1920
+
1921
+ /*
1922
+ * One scrape of a healthy three-node cluster exactly as the v0.161.0
1923
+ * receiver records it: per node, from _nodes/stats, heap max, heap used
1924
+ * and heap utilization (a 0..1 share); and once more at CLUSTER level,
1925
+ * from _cluster/stats, jvm.memory.heap.used for the whole cluster — with
1926
+ * no matching heap.max. Both resource blocks carry the database stamp.
1927
+ */
1928
+ function clusterScrape(
1929
+ utilizationByNode: Array<number>,
1930
+ ): Array<Omit<ReceiverRow, "minute">> {
1931
+ const rows: Array<Omit<ReceiverRow, "minute">> = [];
1932
+ let clusterUsed: number = 0;
1933
+
1934
+ utilizationByNode.forEach((utilization: number, index: number) => {
1935
+ const node: Record<string, string> = {
1936
+ "elasticsearch.node.name": `node-${index + 1}`,
1937
+ };
1938
+ const used: number = utilization * 4 * GIB;
1939
+ clusterUsed += used;
1940
+
1941
+ rows.push(
1942
+ {
1943
+ metricName: "jvm.memory.heap.max",
1944
+ attributes: node,
1945
+ value: 4 * GIB,
1946
+ },
1947
+ { metricName: "jvm.memory.heap.used", attributes: node, value: used },
1948
+ {
1949
+ metricName: "jvm.memory.heap.utilization",
1950
+ attributes: node,
1951
+ value: utilization,
1952
+ },
1953
+ );
1954
+ });
1955
+
1956
+ rows.push({
1957
+ metricName: "jvm.memory.heap.used",
1958
+ attributes: {},
1959
+ value: clusterUsed,
1960
+ });
1961
+
1962
+ return rows;
1963
+ }
1964
+
1965
+ test("the fixture holds the trap: Σ heap.used / Σ heap.max reads 100% on a cluster at 50%", () => {
1966
+ const rows: Array<ReceiverRow> = everyMinute(
1967
+ clusterScrape([0.5, 0.5, 0.5]),
1968
+ 1,
1969
+ );
1970
+ const sum: (metricName: string) => number = (
1971
+ metricName: string,
1972
+ ): number => {
1973
+ return rows
1974
+ .filter((row: ReceiverRow): boolean => {
1975
+ return row.metricName === metricName;
1976
+ })
1977
+ .reduce((total: number, row: ReceiverRow): number => {
1978
+ return total + row.value;
1979
+ }, 0);
1980
+ };
1981
+
1982
+ expect(
1983
+ (sum("jvm.memory.heap.used") / sum("jvm.memory.heap.max")) * 100,
1984
+ ).toBe(100);
1985
+ });
1986
+
1987
+ test("a cluster at 50% heap is healthy", async () => {
1988
+ expect(
1989
+ await evaluateRows(
1990
+ "database-elasticsearch-jvm-heap-high",
1991
+ everyMinute(clusterScrape([0.5, 0.5, 0.5])),
1992
+ ),
1993
+ ).toEqual({ breached: false, healthy: true });
1994
+ });
1995
+
1996
+ test("a single-node cluster at 50% heap is healthy too", async () => {
1997
+ expect(
1998
+ await evaluateRows(
1999
+ "database-elasticsearch-jvm-heap-high",
2000
+ everyMinute(clusterScrape([0.5])),
2001
+ ),
2002
+ ).toEqual({ breached: false, healthy: true });
2003
+ });
2004
+
2005
+ test("one node held at 90% fires, even while the cluster average is low", async () => {
2006
+ expect(
2007
+ await evaluateRows(
2008
+ "database-elasticsearch-jvm-heap-high",
2009
+ everyMinute(clusterScrape([0.3, 0.9, 0.3])),
2010
+ ),
2011
+ ).toEqual({ breached: true, healthy: false });
2012
+ });
2013
+
2014
+ test("holds its status inside the recovery dead band", async () => {
2015
+ // 80%: below the 85% breach, above the 76.5% recovery.
2016
+ expect(
2017
+ await evaluateRows(
2018
+ "database-elasticsearch-jvm-heap-high",
2019
+ everyMinute(clusterScrape([0.8, 0.5, 0.5])),
2020
+ ),
2021
+ ).toEqual({ breached: false, healthy: false });
2022
+ });
2023
+
2024
+ test("reads the per-node utilization with Max, compared in percent", () => {
2025
+ const step: MonitorStep = getTemplate(
2026
+ "database-elasticsearch-jvm-heap-high",
2027
+ ).getMonitorStep(buildArgs());
2028
+
2029
+ expect(
2030
+ getViewConfig(step).queryConfigs.map(
2031
+ (queryConfig: MetricQueryConfigData) => {
2032
+ return [
2033
+ queryConfig.metricQueryData.filterData.metricName,
2034
+ queryConfig.metricQueryData.filterData.aggegationType,
2035
+ ];
2036
+ },
2037
+ ),
2038
+ ).toEqual([["jvm.memory.heap.utilization", MetricsAggregationType.Max]]);
2039
+ expect(getUnhealthy(step).data!.filters[0]!.metricMonitorOptions).toEqual(
2040
+ expect.objectContaining({ metricAlias: "elasticsearch_heap_percent" }),
2041
+ );
2042
+ });
2043
+ });
2044
+
2045
+ describe("Redis fragmentation reads as the ratio it is, not a percentage", () => {
2046
+ test("the alert text says 1.62 against 1.5, never 162% against 150%", async () => {
2047
+ /*
2048
+ * redis.memory.fragmentation_ratio has the dimensionless unit "1" and
2049
+ * a `_ratio` name, which the notification formatter otherwise takes for
2050
+ * a 0..1 fraction and renders ×100. It is not one: healthy is ~1.0.
2051
+ */
2052
+ const step: MonitorStep = getTemplate(
2053
+ "database-redis-memory-fragmentation",
2054
+ ).getMonitorStep(buildArgs());
2055
+ const rootCause: string | null = await getRootCause(
2056
+ step,
2057
+ getUnhealthy(step).data!.filters[0]!,
2058
+ buildResponse(step, {
2059
+ redis_fragmentation_ratio: [1.62, 1.62, 1.62],
2060
+ redis_fragmentation_memory_used: [4294967296, 4294967296, 4294967296],
2061
+ }),
2062
+ );
2063
+
2064
+ expect(rootCause).not.toBeNull();
2065
+ expect(rootCause).toContain("1.62");
2066
+ expect(rootCause).toContain("1.5");
2067
+ expect(rootCause).not.toContain("%");
2068
+ expect(rootCause).not.toContain("162");
2069
+ });
2070
+
2071
+ test.each(TEMPLATE_CASES)(
2072
+ "%s never compares a raw >1 ratio the formatter would render ×100",
2073
+ (_id: string, template: DatabaseAlertTemplate) => {
2074
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
2075
+
2076
+ for (const queryConfig of getViewConfig(step).queryConfigs) {
2077
+ const metricName: string = queryConfig.metricQueryData.filterData
2078
+ .metricName as string;
2079
+ const nativeUnit: string | undefined =
2080
+ getDatabaseAlertMetric(metricName)?.unit;
2081
+
2082
+ if (
2083
+ nativeUnit !== "1" ||
2084
+ !ValueFormatter.isFractionMetric(metricName)
2085
+ ) {
2086
+ continue;
2087
+ }
2088
+
2089
+ /*
2090
+ * A genuine 0..1 share (a `.utilization`) is compared through a
2091
+ * ×100 formula instead; anything else must say it is no fraction.
2092
+ */
2093
+ if (metricName.endsWith(".utilization")) {
2094
+ continue;
2095
+ }
2096
+
2097
+ expect(queryConfig.metricAliasData?.legendUnit).toBeDefined();
2098
+ expect(queryConfig.metricAliasData?.legendUnit).not.toBe("1");
2099
+ }
2100
+ },
2101
+ );
2102
+ });
2103
+
2104
+ describe("byte thresholds read as the round figure their label names", () => {
2105
+ /*
2106
+ * The alert text formats bytes on the dashboard's decimal ladder
2107
+ * (1,000 B = 1 KB), so a "1 GB" threshold written as 1073741824 reached
2108
+ * on-call as "1.07 GB". Every byte threshold is the decimal figure its
2109
+ * label names.
2110
+ */
2111
+ function byteComparisons(
2112
+ template: DatabaseAlertTemplate,
2113
+ ): Array<{ threshold: number; label: string }> {
2114
+ const step: MonitorStep = template.getMonitorStep(buildArgs());
2115
+ const unhealthy: MonitorCriteriaInstance = getUnhealthy(step);
2116
+ const comparisons: Array<{ threshold: number; label: string }> = [];
2117
+
2118
+ for (const filter of unhealthy.data!.filters) {
2119
+ const queryConfig: MetricQueryConfigData | undefined = getViewConfig(
2120
+ step,
2121
+ ).queryConfigs.find((candidate: MetricQueryConfigData): boolean => {
2122
+ return (
2123
+ candidate.metricAliasData?.metricVariable ===
2124
+ filter.metricMonitorOptions?.metricAlias
2125
+ );
2126
+ });
2127
+ const metricName: string | undefined = queryConfig?.metricQueryData
2128
+ .filterData.metricName as string | undefined;
2129
+
2130
+ if (
2131
+ !metricName ||
2132
+ getDatabaseAlertMetric(metricName)?.unit !== "By" ||
2133
+ queryConfig?.metricAliasData?.legendUnit
2134
+ ) {
2135
+ continue;
2136
+ }
2137
+
2138
+ comparisons.push({
2139
+ threshold: Number(filter.value),
2140
+ // The breach's name carries its own label; a second filter's is in the description.
2141
+ label: `${unhealthy.data!.name} ${unhealthy.data!.description}`,
2142
+ });
2143
+ }
2144
+
2145
+ return comparisons;
2146
+ }
2147
+
2148
+ test.each(TEMPLATE_CASES)(
2149
+ "%s names each byte threshold as the alert text renders it",
2150
+ (_id: string, template: DatabaseAlertTemplate) => {
2151
+ for (const comparison of byteComparisons(template)) {
2152
+ expect(comparison.label).toContain(
2153
+ MetricValueFormatter.format({
2154
+ value: comparison.threshold,
2155
+ unit: "By",
2156
+ }),
2157
+ );
2158
+ }
2159
+ },
2160
+ );
2161
+
2162
+ test("the byte-lag alert says 1 GB, the figure its name promises", async () => {
2163
+ const step: MonitorStep = getTemplate(
2164
+ "database-postgresql-replication-byte-lag",
2165
+ ).getMonitorStep(buildArgs());
2166
+ const rootCause: string | null = await getRootCause(
2167
+ step,
2168
+ getUnhealthy(step).data!.filters[0]!,
2169
+ buildResponse(step, {
2170
+ pg_replication_delay: [1500000000, 1600000000, 1700000000],
2171
+ }),
2172
+ );
2173
+
2174
+ expect(rootCause).toContain("1 GB");
2175
+ expect(rootCause).not.toContain("1.07 GB");
2176
+ });
2177
+ });
2178
+ });
2179
+
2180
+ /*
2181
+ * Receiver behaviour the metadata does not show, each measured against the
2182
+ * v0.161.0 source. A template reading any of these watches nothing.
2183
+ */
2184
+ describe("DatabaseAlertTemplates — signals that can never fire are not offered", () => {
2185
+ test("no template reads mysql.buffer_pool.limit, a page count on MariaDB", () => {
2186
+ expect(getDatabaseAlertMetric("mysql.buffer_pool.limit")).toBeUndefined();
2187
+ for (const template of ALL_TEMPLATES) {
2188
+ expect(template.metricNames).not.toContain("mysql.buffer_pool.limit");
2189
+ }
2190
+ });
2191
+
2192
+ test("no template reads mongodb.health, which can only ever read 1", () => {
2193
+ /*
2194
+ * The receiver records mongodb.health from serverStatus's `ok` — only
2195
+ * after serverStatus SUCCEEDED (it returns early on the error the Go
2196
+ * driver makes of an ok:0 reply). A member in RECOVERING or ROLLBACK
2197
+ * still answers ok:1, and a server that is down emits nothing, which
2198
+ * "Engine Metrics Stopped" already covers.
2199
+ */
2200
+ expect(getDatabaseAlertTemplateById("database-mongodb-unhealthy")).toBe(
2201
+ undefined,
2202
+ );
2203
+ expect(getDatabaseAlertMetric("mongodb.health")).toBeUndefined();
2204
+
2205
+ for (const template of ALL_TEMPLATES) {
2206
+ expect(template.metricNames).not.toContain("mongodb.health");
2207
+ }
2208
+ });
2209
+
2210
+ test("no template reads postgresql.wal.lag, which a default-on feature gate replaces", () => {
2211
+ /*
2212
+ * `postgresqlreceiver.preciselagmetrics` is a BETA gate in 0.161.0 —
2213
+ * enabled by default — under which the receiver records
2214
+ * postgresql.wal.delay INSTEAD of postgresql.wal.lag. wal.delay is off
2215
+ * by default, so the Database Agent enables it.
2216
+ */
2217
+ expect(getDatabaseAlertMetric("postgresql.wal.lag")).toBeUndefined();
2218
+
2219
+ for (const template of ALL_TEMPLATES) {
2220
+ expect(template.metricNames).not.toContain("postgresql.wal.lag");
2221
+ }
2222
+
2223
+ expect(
2224
+ getTemplate("database-postgresql-replica-replay-lag").metricNames,
2225
+ ).toEqual(["postgresql.wal.delay"]);
2226
+ expect(getDatabaseAlertMetric("postgresql.wal.delay")).toEqual(
2227
+ expect.objectContaining({ kind: "gauge", unit: "s" }),
2228
+ );
2229
+ });
2230
+
2231
+ test("no template sums a metric the receiver records at two levels in one scrape", () => {
2232
+ /*
2233
+ * The Elasticsearch receiver records jvm.memory.heap.used per node AND
2234
+ * once more for the whole cluster (from _cluster/stats), in the same
2235
+ * scrape and with the same stamp: any Sum over it double counts.
2236
+ */
2237
+ const recordedTwicePerScrape: Array<string> = ["jvm.memory.heap.used"];
2238
+
2239
+ for (const template of ALL_TEMPLATES) {
2240
+ for (const queryConfig of getViewConfig(
2241
+ template.getMonitorStep(buildArgs()),
2242
+ ).queryConfigs) {
2243
+ const filterData: MetricQueryConfigData["metricQueryData"]["filterData"] =
2244
+ queryConfig.metricQueryData.filterData;
2245
+
2246
+ if (recordedTwicePerScrape.includes(filterData.metricName as string)) {
2247
+ expect(filterData.aggegationType).not.toBe(
2248
+ MetricsAggregationType.Sum,
2249
+ );
2250
+ }
2251
+ }
2252
+ }
2253
+ });
2254
+ });