@oneuptime/common 14.0.3 → 14.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (663) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  3. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  4. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  5. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  6. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  9. package/Models/DatabaseModels/Incident.ts +55 -0
  10. package/Models/DatabaseModels/Index.ts +16 -0
  11. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  13. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  14. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  18. package/Server/Middleware/MultipartFormData.ts +1 -1
  19. package/Server/Middleware/TelemetryIngest.ts +15 -8
  20. package/Server/Services/AlertReminderRuleService.ts +100 -24
  21. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  22. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  23. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  24. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  25. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  26. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  27. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  28. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  29. package/Server/Services/DatabaseServerService.ts +4546 -0
  30. package/Server/Services/DatabaseService.ts +14 -8
  31. package/Server/Services/DockerResourceService.ts +43 -2
  32. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  33. package/Server/Services/Index.ts +16 -0
  34. package/Server/Services/InventoryItemService.ts +113 -5
  35. package/Server/Services/IoTDeviceService.ts +145 -37
  36. package/Server/Services/MetricService.ts +1 -1
  37. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  38. package/Server/Services/PodmanResourceService.ts +43 -2
  39. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  40. package/Server/Services/ServiceService.ts +1 -1
  41. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  42. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  43. package/Server/Services/UserService.ts +14 -1
  44. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  45. package/Server/Types/Database/QueryHelper.ts +84 -2
  46. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  47. package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
  48. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  49. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  50. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  51. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
  52. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  53. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
  54. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  55. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  56. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  57. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  58. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  59. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  60. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  61. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  62. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  63. package/Server/Utils/StartServer.ts +26 -6
  64. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  65. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  66. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  67. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  68. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  69. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  70. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  71. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  72. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  73. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  74. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  75. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  76. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  77. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  78. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  79. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  80. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  81. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  82. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  83. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  84. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  85. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  86. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  87. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  88. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  89. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  90. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  91. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  92. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  93. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  94. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  95. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  96. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  97. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  98. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  99. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  100. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  101. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  102. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  103. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  104. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  105. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  106. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  107. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  108. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  109. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  110. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  111. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  112. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  113. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  114. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  115. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  116. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  117. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  118. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  119. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  120. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  121. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  122. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  123. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  124. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  125. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  126. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  127. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  128. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  129. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  130. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  131. package/Tests/ClassStrings.ts +178 -0
  132. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  133. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  134. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  135. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  136. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  137. package/Tests/RenderedMarkup.ts +518 -0
  138. package/Tests/ResponsiveVisibility.test.ts +490 -4
  139. package/Tests/ResponsiveVisibility.ts +505 -52
  140. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  141. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  142. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  143. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  144. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  145. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  146. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  147. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  148. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  149. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  150. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  151. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  152. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  153. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  154. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  155. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  156. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  157. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  158. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  159. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  160. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  161. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  162. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  163. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  164. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  165. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  166. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  167. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  168. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  169. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  170. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  171. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  172. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  173. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  174. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  175. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  176. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  177. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  178. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  179. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  180. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  181. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  182. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  183. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  184. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  185. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  186. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  187. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  188. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  189. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  190. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  191. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  192. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  193. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  194. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  195. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  196. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  197. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  198. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  199. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
  200. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  201. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  202. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  203. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  204. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  205. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  206. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  207. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  208. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  209. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  210. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  211. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  212. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  213. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  214. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  215. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  216. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  217. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  218. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  219. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  220. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  221. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  222. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  223. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  224. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  225. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  226. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  227. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  228. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  229. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  230. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  231. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  232. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  233. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  234. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  235. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  236. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  237. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  238. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  239. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  240. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  241. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  242. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
  243. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
  244. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  245. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  246. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  247. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  248. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  249. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  250. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
  251. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  252. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  253. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  254. package/Tests/UI/Components/Card.test.tsx +93 -2
  255. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  256. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  257. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  258. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  259. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  260. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  261. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  262. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  263. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  264. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  265. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  266. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  267. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  268. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  269. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  270. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  271. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  272. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  273. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  274. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
  275. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
  276. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  277. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  278. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  279. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  280. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  281. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  282. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  283. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  284. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  285. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  286. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  287. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  288. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  289. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  290. package/Tests/tsconfig.json +16 -0
  291. package/Types/AI/AIResourceContext.ts +7 -0
  292. package/Types/BaseDatabase/AggregationType.ts +7 -0
  293. package/Types/Dashboard/DashboardTemplates.ts +22 -8
  294. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  295. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  296. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  297. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  298. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  299. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  300. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  301. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  302. package/Types/Email/EmailTemplateType.ts +1 -0
  303. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  304. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  305. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  306. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  307. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  308. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  309. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  310. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  311. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  312. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  313. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  314. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  315. package/Types/Permission.ts +267 -0
  316. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  317. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  318. package/Types/Rules/RuleRun.ts +12 -0
  319. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  320. package/Types/Rum/WebVitals.ts +13 -0
  321. package/Types/Telemetry/EntityType.ts +23 -0
  322. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  323. package/Types/Telemetry/ServiceType.ts +8 -0
  324. package/UI/Components/Banner/Banner.tsx +1 -1
  325. package/UI/Components/Card/Card.tsx +1 -1
  326. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  327. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  328. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  329. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  330. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  331. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  332. package/UI/Components/Forms/BasicForm.tsx +1 -1
  333. package/UI/Components/Header/Header.tsx +1 -1
  334. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  335. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  336. package/UI/Components/Input/Input.tsx +11 -0
  337. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  338. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  339. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  340. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  341. package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
  342. package/UI/Components/Navbar/NavBar.tsx +9 -2
  343. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  344. package/UI/Components/Page/Page.tsx +1 -1
  345. package/UI/Components/Pagination/Pagination.tsx +3 -3
  346. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  347. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  348. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  349. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  350. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  351. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  352. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  353. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  354. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  355. package/UI/Styles/Theme.css +8 -1
  356. package/UI/Utils/Navigation.ts +89 -42
  357. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  358. package/UI/Utils/TelemetryService.ts +4 -3
  359. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  360. package/Utils/LabelRuleImportExport.ts +2 -0
  361. package/Utils/MetricUnitUtil.ts +103 -11
  362. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  363. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  364. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  365. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  366. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  367. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  368. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  369. package/Utils/Telemetry/EntityKey.ts +176 -0
  370. package/Utils/ValueFormatter.ts +16 -0
  371. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  372. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  373. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  374. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  375. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  376. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  377. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  378. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  379. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  380. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  381. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  382. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  383. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  384. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  385. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  386. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  387. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  388. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  389. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  390. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  391. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  393. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  395. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  397. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  398. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  399. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  400. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  401. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  402. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  403. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  404. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  405. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  406. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  407. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  408. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  409. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  410. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  411. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  412. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  413. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  414. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  415. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  416. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  417. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  418. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  419. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  420. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  421. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  422. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  423. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  424. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  425. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  426. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  427. package/build/dist/Server/Services/DatabaseService.js +8 -4
  428. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  429. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  430. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  431. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  432. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  433. package/build/dist/Server/Services/Index.js +16 -0
  434. package/build/dist/Server/Services/Index.js.map +1 -1
  435. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  436. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  437. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  438. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  439. package/build/dist/Server/Services/MetricService.js +1 -1
  440. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  441. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  442. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  443. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  444. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  445. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  446. package/build/dist/Server/Services/ServiceService.js +1 -1
  447. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  448. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  449. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  450. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  451. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  452. package/build/dist/Server/Services/UserService.js +13 -1
  453. package/build/dist/Server/Services/UserService.js.map +1 -1
  454. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  455. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  456. package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
  457. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  458. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  459. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  460. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
  461. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  462. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  463. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  464. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  465. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  466. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  467. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
  468. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  469. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  470. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  471. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  472. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  473. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  474. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  475. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  476. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  477. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  478. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  479. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  480. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  481. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  482. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  483. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  484. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  485. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  486. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  487. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  488. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  489. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  490. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  491. package/build/dist/Server/Utils/StartServer.js +20 -4
  492. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  493. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  494. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  495. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  496. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  497. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  498. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  499. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  500. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  501. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  502. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  503. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  504. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  505. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  506. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  507. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  508. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  509. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  510. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  512. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  513. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  514. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  515. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  516. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  517. package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
  518. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  519. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  520. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  521. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  522. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  523. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  524. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  525. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  526. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  527. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  528. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  529. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  530. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  531. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  532. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  533. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  534. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  535. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  536. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  537. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  538. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  539. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  540. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  541. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  542. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  543. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  544. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  545. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  546. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  547. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  548. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  549. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  550. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  551. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  552. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  553. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  554. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  555. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  556. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  557. package/build/dist/Types/Permission.js +234 -0
  558. package/build/dist/Types/Permission.js.map +1 -1
  559. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  560. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  561. package/build/dist/Types/Rules/RuleRun.js +4 -0
  562. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  563. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  564. package/build/dist/Types/Rum/WebVitals.js +12 -0
  565. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  566. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  567. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  568. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  569. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  570. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  571. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  572. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  573. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  574. package/build/dist/UI/Components/Card/Card.js +1 -1
  575. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  576. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  577. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  578. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  579. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  580. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  581. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  582. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  583. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  584. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  585. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  586. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  587. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  588. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  589. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  590. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  591. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  592. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  593. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  594. package/build/dist/UI/Components/Input/Input.js +1 -1
  595. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  596. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  597. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  598. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  600. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  601. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  602. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  603. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  604. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
  605. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  606. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  607. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  608. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  609. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  610. package/build/dist/UI/Components/Page/Page.js +1 -1
  611. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  612. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  613. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  614. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  615. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  616. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  617. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  618. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  619. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  620. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  621. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  622. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  626. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  627. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  628. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  629. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  630. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  631. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  632. package/build/dist/UI/Utils/Navigation.js +66 -33
  633. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  634. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  635. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  636. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  637. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  638. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  639. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  640. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  641. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  642. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  643. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  644. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  645. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  646. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  647. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  648. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  649. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  650. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  651. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  652. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  653. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  654. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  655. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  656. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  657. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  658. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  659. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  660. package/build/dist/Utils/ValueFormatter.js +15 -0
  661. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  662. package/package.json +1 -1
  663. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,3272 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import DatabaseService from "./DatabaseService";
11
+ import DatabaseServerEndpointService, { ENDPOINT_MATCH_REFRESH_SECONDS, getOwnedByOtherDatabaseMessage, } from "./DatabaseServerEndpointService";
12
+ import DatabaseServerFeedService from "./DatabaseServerFeedService";
13
+ import DatabaseServerLabelRuleEngineService from "./DatabaseServerLabelRuleEngineService";
14
+ import DatabaseServerOwnerRuleEngineService from "./DatabaseServerOwnerRuleEngineService";
15
+ import DockerHostService from "./DockerHostService";
16
+ import KubernetesClusterService from "./KubernetesClusterService";
17
+ import PodmanHostService from "./PodmanHostService";
18
+ import Model from "../../Models/DatabaseModels/DatabaseServer";
19
+ import { DatabaseServerFeedEventType } from "../../Models/DatabaseModels/DatabaseServerFeed";
20
+ import DatabaseConfig from "../DatabaseConfig";
21
+ import GlobalCache from "../Infrastructure/GlobalCache";
22
+ import ModelPermission from "../Types/Database/Permissions/Index";
23
+ import QueryHelper from "../Types/Database/QueryHelper";
24
+ import RelationIdUtil from "../Utils/Database/RelationIdUtil";
25
+ import { truncateLongText, truncateShortText, } from "../Utils/Database/TruncateColumnValue";
26
+ import InProcessMemo from "../Utils/InProcessMemo";
27
+ import logger from "../Utils/Logger";
28
+ import ResourceFeedUtil from "../Utils/ResourceFeed/ResourceFeedUtil";
29
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
30
+ import ResourceHeartbeat from "../Utils/Telemetry/ResourceHeartbeat";
31
+ import URL from "../../Types/API/URL";
32
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
33
+ import { Blue500, Gray500, Green500, Yellow500 } from "../../Types/BrandColors";
34
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
35
+ import DatabaseServerDiscoverySource, { getDatabaseServerDiscoverySourceLabel, } from "../../Types/DatabaseServer/DatabaseServerDiscoverySource";
36
+ import { buildDatabaseServerDisplayName, buildDatabaseServerIdentifier, formatDatabaseEndpoint, getDatabaseEndpointScope, isKubernetesServiceDnsHost, parseDatabaseEndpointString, parseManualDatabaseEndpoint, } from "../../Types/DatabaseServer/DatabaseEndpoint";
37
+ import { DATABASE_SYSTEMS, getDatabaseSystemDisplayName, getDatabaseSystemFamily, getMoreSpecificDatabaseSystem, normalizeDatabaseSystem, } from "../../Types/DatabaseServer/DatabaseSystem";
38
+ import { ContainerWorkloadKind } from "../../Types/DatabaseServer/DatabaseContainerClassifier";
39
+ import OneUptimeDate from "../../Types/Date";
40
+ import BadDataException from "../../Types/Exception/BadDataException";
41
+ import ObjectID from "../../Types/ObjectID";
42
+ import { mergeDatabaseServerMemberKeys } from "../../Utils/Telemetry/DatabaseServerEntityKeys";
43
+ import crypto from "crypto";
44
+ /*
45
+ * Two heartbeats, two namespaces: a collector reporting engine metrics is a
46
+ * different fact from an application merely querying the database, and one
47
+ * must never be able to suppress the other inside a window.
48
+ */
49
+ const LAST_SEEN_CACHE_NAMESPACE = "database-server-last-seen";
50
+ const SIGHTING_CACHE_NAMESPACE = "database-server-sighting";
51
+ const LAST_SEEN_THROTTLE_SECONDS = 60;
52
+ const LABELS_APPLIED_CACHE_NAMESPACE = "database-server-labels-applied";
53
+ const LABELS_APPLIED_CACHE_TTL_SECONDS = 60;
54
+ /*
55
+ * How often, fleet-wide, a collector heartbeat may look at whether its row was
56
+ * auto-archived. A row is only ever auto-archived after days without being
57
+ * seen, so a heartbeat restoring it up to ten minutes late costs nothing, and
58
+ * the check stays one primary-key read per database per window.
59
+ */
60
+ const AUTO_RESTORE_CHECK_CACHE_NAMESPACE = "database-server-auto-restore-check";
61
+ const AUTO_RESTORE_CHECK_WINDOW_SECONDS = 600;
62
+ const DEFAULT_COLLECTOR_STALE_MINUTES = 15;
63
+ const MIN_COLLECTOR_STALE_MINUTES = 10;
64
+ /*
65
+ * How far collectorLastSeenAt may trail a collector's last data while it
66
+ * reports continuously: the heartbeat runs once per OTel ingest maintenance
67
+ * fence (MAINTENANCE_FENCE_TTL_SECONDS in OtelIngestBaseService: 5 minutes,
68
+ * jittered up to 25%, so 6.25) and a pod that saw the fence held waits up
69
+ * to its 30-second refusal memo more - 6.75 minutes, rounded up.
70
+ */
71
+ const COLLECTOR_HEARTBEAT_LAG_MINUTES = 7;
72
+ const DEFAULT_AUTO_ARCHIVE_DAYS = 7;
73
+ const MIN_AUTO_ARCHIVE_DAYS = 1;
74
+ // Rows archived per sweep; the rest wait for the next five-minute run.
75
+ const AUTO_ARCHIVE_BATCH_SIZE = 500;
76
+ /*
77
+ * A person's Restore holds the sweep off this long (or the archive window,
78
+ * if that is longer) unless discovery sees the database again first.
79
+ */
80
+ const MANUAL_RESTORE_GRACE_DAYS = 30;
81
+ const DEFAULT_AUTO_CREATE_BUDGET = 500;
82
+ /*
83
+ * The auto-create budget as the ingest hot path reads it: one count per
84
+ * project per minute per process, bumped locally by every create in between.
85
+ */
86
+ const AUTO_CREATE_BUDGET_CACHE_MS = 60 * 1000;
87
+ // One "budget reached" warning per project per ten minutes per process.
88
+ const AUTO_CREATE_BUDGET_WARNING_MS = 10 * 60 * 1000;
89
+ // Endpoint aliases one workload may claim per discovery run.
90
+ const MAX_WORKLOAD_ALIASES = 100;
91
+ /*
92
+ * A workload discovery has not seen for this long is gone: the endpoints its
93
+ * row holds may be handed to the workload that serves them now, and seeing
94
+ * one of them in a trace no longer brings its archived row back. Discovery
95
+ * runs every five minutes, so this is a dozen missed runs.
96
+ */
97
+ const WORKLOAD_GONE_MINUTES = 60;
98
+ /*
99
+ * A "workload" alias its workload has stopped producing is released once it
100
+ * has not been produced for this long. Produced aliases are re-stamped at
101
+ * least hourly (ENDPOINT_MATCH_REFRESH_SECONDS), so this is comfortably
102
+ * longer than any gap a healthy workload leaves, and one partial run
103
+ * releases nothing.
104
+ */
105
+ const WORKLOAD_ALIAS_RELEASE_MINUTES = 120;
106
+ /*
107
+ * A workload row whose columns discovery would write back unchanged is only
108
+ * rewritten once this long has passed since it was last written: discovery
109
+ * runs every five minutes, and rewriting every row every run (the member-key
110
+ * JSON included) costs a write per database per run for nothing. Its
111
+ * lastSeenAt is then at most this plus one discovery interval old - inside
112
+ * the dashboard's 30-minute "seen recently" window
113
+ * (DATABASE_SERVER_LIVE_WINDOW_MINUTES), and far inside
114
+ * WORKLOAD_GONE_MINUTES and the auto-archive sweep's one-hour margin behind
115
+ * its parent - so a skipped write never makes a live workload look inactive
116
+ * or gone.
117
+ */
118
+ const WORKLOAD_REFRESH_MINUTES = 15;
119
+ // How many of the project's cluster names a manual-create refusal lists.
120
+ const MAX_LISTED_CLUSTER_NAMES = 10;
121
+ /*
122
+ * The columns every discovery caller gets back. Enough to key telemetry
123
+ * (id, project, engine, endpoint), name it (name), decide on archive
124
+ * handling and weigh new engine evidence - but not the member-key map,
125
+ * which only the workload path reads.
126
+ */
127
+ const DISCOVERY_SELECT = {
128
+ _id: true,
129
+ projectId: true,
130
+ name: true,
131
+ dbSystem: true,
132
+ dbSystemSource: true,
133
+ databaseIdentifier: true,
134
+ workloadIdentifier: true,
135
+ workloadName: true,
136
+ workloadLastSeenAt: true,
137
+ discoverySource: true,
138
+ serverAddress: true,
139
+ serverPort: true,
140
+ kubernetesClusterId: true,
141
+ kubernetesNamespace: true,
142
+ isArchived: true,
143
+ autoArchivedAt: true,
144
+ };
145
+ /*
146
+ * The workload path also compares what it would write with what the row
147
+ * holds (see workloadColumnsChanged), so it reads every column it writes.
148
+ */
149
+ const WORKLOAD_SELECT = Object.assign(Object.assign({}, DISCOVERY_SELECT), { memberEntityKeys: true, dbVersion: true, collectorLastSeenAt: true, instanceCount: true, workloadKind: true, dockerHostId: true, podmanHostId: true, lastSeenAt: true });
150
+ /*
151
+ * "Nobody invested in this row", as one SQL predicate over `ds` (a
152
+ * DatabaseServer row): no retention override, and no label, owner, incident
153
+ * / alert / scheduled-maintenance link or person-added endpoint - each
154
+ * checked against the row's own project and live rows only. Labels and
155
+ * owners that rules or ingest attached on their own (automaticAssignments)
156
+ * do not count: a catch-all rule would otherwise keep every discovered row
157
+ * alive forever. Shared by the auto-archive sweep and by the workload path
158
+ * deciding whether a trace-discovered duplicate may hand over its endpoints.
159
+ */
160
+ const UNTOUCHED_DATABASE_SERVER_PREDICATE = `ds."retainTelemetryDataForDays" IS NULL
161
+ AND (ds."telemetryRetentionConfig" IS NULL OR ds."telemetryRetentionConfig"::text IN ('null', '{}'))
162
+ AND NOT EXISTS (
163
+ SELECT 1 FROM "DatabaseServerLabel" l
164
+ INNER JOIN "Label" lbl ON lbl."_id" = l."labelId"
165
+ WHERE l."databaseServerId" = ds."_id"
166
+ AND lbl."projectId" = ds."projectId"
167
+ AND lbl."deletedAt" IS NULL
168
+ AND NOT (COALESCE(ds."automaticAssignments" -> 'labelIds', '[]'::jsonb) @> jsonb_build_array(l."labelId"::text))
169
+ )
170
+ AND NOT EXISTS (
171
+ SELECT 1 FROM "DatabaseServerOwnerUser" ou
172
+ WHERE ou."databaseServerId" = ds."_id"
173
+ AND ou."projectId" = ds."projectId"
174
+ AND ou."deletedAt" IS NULL
175
+ AND NOT (COALESCE(ds."automaticAssignments" -> 'ownerUserIds', '[]'::jsonb) @> jsonb_build_array(ou."userId"::text))
176
+ )
177
+ AND NOT EXISTS (
178
+ SELECT 1 FROM "DatabaseServerOwnerTeam" ot
179
+ WHERE ot."databaseServerId" = ds."_id"
180
+ AND ot."projectId" = ds."projectId"
181
+ AND ot."deletedAt" IS NULL
182
+ AND NOT (COALESCE(ds."automaticAssignments" -> 'ownerTeamIds', '[]'::jsonb) @> jsonb_build_array(ot."teamId"::text))
183
+ )
184
+ AND NOT EXISTS (
185
+ SELECT 1 FROM "IncidentDatabaseServer" ids
186
+ INNER JOIN "Incident" i ON i."_id" = ids."incidentId"
187
+ WHERE ids."databaseServerId" = ds."_id"
188
+ AND i."projectId" = ds."projectId"
189
+ AND i."deletedAt" IS NULL
190
+ )
191
+ AND NOT EXISTS (
192
+ SELECT 1 FROM "AlertDatabaseServer" ads
193
+ INNER JOIN "Alert" a ON a."_id" = ads."alertId"
194
+ WHERE ads."databaseServerId" = ds."_id"
195
+ AND a."projectId" = ds."projectId"
196
+ AND a."deletedAt" IS NULL
197
+ )
198
+ AND NOT EXISTS (
199
+ SELECT 1 FROM "ScheduledMaintenanceDatabaseServer" sds
200
+ INNER JOIN "ScheduledMaintenance" sm ON sm."_id" = sds."scheduledMaintenanceId"
201
+ WHERE sds."databaseServerId" = ds."_id"
202
+ AND sm."projectId" = ds."projectId"
203
+ AND sm."deletedAt" IS NULL
204
+ )
205
+ AND NOT EXISTS (
206
+ SELECT 1 FROM "DatabaseServerEndpoint" e
207
+ WHERE e."databaseServerId" = ds."_id"
208
+ AND e."projectId" = ds."projectId"
209
+ AND e."source" = 'user'
210
+ AND e."deletedAt" IS NULL
211
+ )`;
212
+ const AUTOMATIC_ASSIGNMENT_KINDS = ["labelIds", "ownerUserIds", "ownerTeamIds"];
213
+ /*
214
+ * What a row's engine (dbSystem) was determined from, strongest first:
215
+ * - manual: a person chose it;
216
+ * - collector: an OTel Collector receiver or the Database Agent - the
217
+ * engine reporting on itself;
218
+ * - container: the image or Helm chart of a Kubernetes / Docker / Podman
219
+ * workload;
220
+ * - client-spans: what an application's client library called it. A
221
+ * wire-compatible client names the engine it speaks to, not the engine
222
+ * it reached (pgx says "postgresql" to CockroachDB).
223
+ */
224
+ export var DatabaseSystemEvidence;
225
+ (function (DatabaseSystemEvidence) {
226
+ DatabaseSystemEvidence["Manual"] = "manual";
227
+ DatabaseSystemEvidence["Collector"] = "collector";
228
+ DatabaseSystemEvidence["Container"] = "container";
229
+ DatabaseSystemEvidence["ClientSpans"] = "client-spans";
230
+ })(DatabaseSystemEvidence || (DatabaseSystemEvidence = {}));
231
+ const DATABASE_SYSTEM_EVIDENCE_RANK = {
232
+ [DatabaseSystemEvidence.Manual]: 4,
233
+ [DatabaseSystemEvidence.Collector]: 3,
234
+ [DatabaseSystemEvidence.Container]: 2,
235
+ [DatabaseSystemEvidence.ClientSpans]: 1,
236
+ };
237
+ const DATABASE_SYSTEM_EVIDENCE_LABEL = {
238
+ [DatabaseSystemEvidence.Manual]: "a person",
239
+ [DatabaseSystemEvidence.Collector]: "its OpenTelemetry Collector receiver or Database Agent",
240
+ [DatabaseSystemEvidence.Container]: "its container image",
241
+ [DatabaseSystemEvidence.ClientSpans]: "application traces",
242
+ };
243
+ /*
244
+ * The Databases product's root service. A DatabaseServer row is found and
245
+ * created by ENDPOINT (DatabaseServerEndpoint, one owner per endpoint per
246
+ * project) or by WORKLOAD (workloadIdentifier), never by name - two engines
247
+ * can share a host name and a person can rename any row.
248
+ *
249
+ * Invariants every discovery path relies on:
250
+ * - an endpoint belongs to at most one row, and an ambiguous match is never
251
+ * tie-broken. A claim never takes an endpoint away from a live row: the
252
+ * only moves are the workload path handing a DISCOVERED ("auto" /
253
+ * "workload") endpoint from a workload that is gone, or from an untouched
254
+ * trace-discovered duplicate, to the workload that serves it now. A
255
+ * person's ("user") endpoint is never moved or released by discovery;
256
+ * - a LOCAL-scope endpoint (single-label name, unqualified cluster-local
257
+ * DNS or private IP) never creates a row and never adopts one;
258
+ * - discovery only ever un-archives a row it archived itself
259
+ * (autoArchivedAt), never one a person archived;
260
+ * - an engine a person chose is never changed, and a weaker source never
261
+ * changes an engine a stronger one determined (DatabaseSystemEvidence)
262
+ * - a container image may still refine it to a fork, and undo a fork
263
+ * only client spans named (decideDatabaseSystem).
264
+ */
265
+ export class Service extends DatabaseService {
266
+ constructor() {
267
+ super(Model);
268
+ /*
269
+ * In-process front for the fleet-wide auto-restore gate: a collector
270
+ * heartbeat arrives per batch, and asking Redis per batch whether it is time
271
+ * to look again would cost more than the heartbeat's own gates.
272
+ */
273
+ this.autoRestoreCheckMemo = new InProcessMemo({
274
+ ttlInMs: 60 * 1000,
275
+ maxEntries: 10000,
276
+ });
277
+ // Per project: its auto-created row count, as last read (see getAutoCreateCount).
278
+ this.autoCreateCountMemo = new InProcessMemo({
279
+ ttlInMs: AUTO_CREATE_BUDGET_CACHE_MS,
280
+ maxEntries: 10000,
281
+ });
282
+ // Per project: a "budget reached" warning was logged recently.
283
+ this.autoCreateBudgetWarningMemo = new InProcessMemo({
284
+ ttlInMs: AUTO_CREATE_BUDGET_WARNING_MS,
285
+ maxEntries: 10000,
286
+ });
287
+ }
288
+ /*
289
+ * A person adding a database from the dashboard or the API. Root creates -
290
+ * every discovery path - pass through untouched: they compute identity and
291
+ * claim endpoints themselves.
292
+ *
293
+ * Everything set here is a user-creatable column on the model (name,
294
+ * description, dbSystem, serverAddress, serverPort, databaseIdentifier,
295
+ * discoverySource), because the column permission check runs after this
296
+ * hook. Setting a root-only column here would refuse every manual create.
297
+ *
298
+ * The create permission is checked FIRST, before any lookup: the refusals
299
+ * below name databases, and a caller who may not add a database must not
300
+ * be able to use them to learn what exists.
301
+ *
302
+ * The typed address is read by parseManualDatabaseEndpoint, whose refusals
303
+ * say what to change (`user@host`, a cluster qualifier on a public host, a
304
+ * loopback name, a port out of range), and a Kubernetes Service name typed
305
+ * without its cluster is refused in a project that has clusters (see
306
+ * refuseUnqualifiedKubernetesServiceName).
307
+ */
308
+ async onBeforeCreate(createBy) {
309
+ if (createBy.props.isRoot) {
310
+ return { createBy: createBy, carryForward: null };
311
+ }
312
+ const data = createBy.data;
313
+ ModelPermission.checkCreatePermissions(Model, data, createBy.props);
314
+ // The tenant column is stamped from props.tenantId only after this hook.
315
+ const projectId = createBy.props.tenantId || data.projectId || undefined;
316
+ if (!projectId) {
317
+ throw new BadDataException("Project ID is required to add a database.");
318
+ }
319
+ const dbSystem = normalizeDatabaseSystem(data.dbSystem);
320
+ if (!dbSystem) {
321
+ throw new BadDataException("Database engine is required. Choose the engine this database runs, for example PostgreSQL or MySQL.");
322
+ }
323
+ const manual = parseManualEndpoint({
324
+ serverAddress: data.serverAddress,
325
+ serverPort: data.serverPort,
326
+ dbSystem: dbSystem,
327
+ });
328
+ const endpoint = manual.endpoint;
329
+ const formatted = formatDatabaseEndpoint(endpoint);
330
+ const owner = await DatabaseServerEndpointService.findOwnerByEndpoint(projectId, formatted);
331
+ if (owner) {
332
+ throw new BadDataException(await getOwnedByOtherDatabaseMessage(formatted, owner, createBy.props));
333
+ }
334
+ await this.refuseUnqualifiedKubernetesServiceName({
335
+ projectId: projectId,
336
+ manual: manual,
337
+ serverAddress: data.serverAddress,
338
+ serverPort: data.serverPort,
339
+ dbSystem: dbSystem,
340
+ props: createBy.props,
341
+ });
342
+ const databaseIdentifier = buildDatabaseServerIdentifier(dbSystem, endpoint);
343
+ /*
344
+ * Normally unreachable - the row with this identifier would own the
345
+ * endpoint - but the unique index would otherwise answer with a raw
346
+ * constraint error.
347
+ */
348
+ const sameIdentity = await this.findOneBy({
349
+ query: {
350
+ projectId: projectId,
351
+ databaseIdentifier: databaseIdentifier,
352
+ },
353
+ select: {
354
+ _id: true,
355
+ name: true,
356
+ },
357
+ props: {
358
+ isRoot: true,
359
+ },
360
+ });
361
+ if (sameIdentity) {
362
+ // Named only when the caller may read it.
363
+ const sameIdentityName = sameIdentity.id
364
+ ? await this.getDatabaseServerNameIfReadable({
365
+ databaseServerId: sameIdentity.id,
366
+ props: createBy.props,
367
+ })
368
+ : "";
369
+ throw new BadDataException(`A ${getDatabaseSystemDisplayName(dbSystem)} database at ${formatted} already exists${sameIdentityName ? `: "${sameIdentityName}"` : ""}.`);
370
+ }
371
+ const name = typeof data.name === "string" ? data.name.trim() : "";
372
+ data.dbSystem = dbSystem;
373
+ data.serverAddress = endpoint.host;
374
+ if (endpoint.port !== null) {
375
+ data.serverPort = endpoint.port;
376
+ }
377
+ data.databaseIdentifier = databaseIdentifier;
378
+ // First creator wins, and a person creating it is "manual" - whatever was sent.
379
+ data.discoverySource = DatabaseServerDiscoverySource.Manual;
380
+ data.name =
381
+ name ||
382
+ buildDatabaseServerDisplayName({ system: dbSystem, endpoint: endpoint });
383
+ return {
384
+ createBy: createBy,
385
+ carryForward: {
386
+ primaryEndpoint: formatted,
387
+ },
388
+ };
389
+ }
390
+ async onCreateSuccess(onCreate, createdItem) {
391
+ var _a;
392
+ const primaryEndpoint = (_a = onCreate.carryForward) === null || _a === void 0 ? void 0 : _a.primaryEndpoint;
393
+ if (primaryEndpoint && createdItem.projectId && createdItem.id) {
394
+ await this.claimManualPrimaryEndpoint({
395
+ projectId: createdItem.projectId,
396
+ databaseServerId: createdItem.id,
397
+ endpoint: primaryEndpoint,
398
+ props: onCreate.createBy.props,
399
+ });
400
+ }
401
+ const createdByUserId = createdItem.createdByUserId ||
402
+ onCreate.createBy.props.userId ||
403
+ undefined;
404
+ this.runCreatedSideEffects(createdItem, createdByUserId);
405
+ return createdItem;
406
+ }
407
+ async onUpdateSuccess(onUpdate, updatedItemIds) {
408
+ const updateData = onUpdate.updateBy
409
+ .data;
410
+ /*
411
+ * A person archiving or restoring a row takes it out of discovery's
412
+ * hands: from now on only a person restores it. A person's Restore also
413
+ * holds off the auto-archive sweep (manuallyRestoredAt), which would
414
+ * otherwise re-archive a stale row within minutes. Discovery's own
415
+ * archive and restore are raw writes, so they never reach this hook.
416
+ */
417
+ if (ResourceFeedUtil.isArchiveChange(updateData) &&
418
+ !("autoArchivedAt" in updateData)) {
419
+ await this.recordArchiveDecisionByPerson(updatedItemIds, !updateData["isArchived"]);
420
+ }
421
+ /*
422
+ * Labels a person saves on a row are theirs, whichever rule or
423
+ * collector first attached them: they now count as investment.
424
+ */
425
+ if ("labels" in updateData) {
426
+ const labelIds = readRelationIds(updateData["labels"]);
427
+ for (const databaseServerId of updatedItemIds) {
428
+ await this.forgetAutomaticAssignments({
429
+ databaseServerId: databaseServerId,
430
+ kind: "labelIds",
431
+ ids: labelIds,
432
+ });
433
+ }
434
+ }
435
+ // An engine a person set is never overridden by discovery.
436
+ if ("dbSystem" in updateData) {
437
+ await this.recordDatabaseSystemSetByPerson(updatedItemIds);
438
+ }
439
+ this.writeDatabaseServerUpdatedFeed(onUpdate, updatedItemIds).catch((error) => {
440
+ logger.error(error);
441
+ });
442
+ return onUpdate;
443
+ }
444
+ /**
445
+ * The database that owns this endpoint - or, when none does and creating
446
+ * is allowed, a new one that does. Returns null when nothing owns the
447
+ * endpoint and no row may be created for it (creation not allowed, a
448
+ * LOCAL-scope endpoint, which only ever joins an existing row, or - for
449
+ * the collector path, which has no budget of its own - a project that has
450
+ * reached its auto-create budget).
451
+ *
452
+ * An owner that is found is also told about the sighting: the endpoint's
453
+ * lastMatchedAt moves (throttled), the reported engine is weighed against
454
+ * the row's (a stronger source may correct or refine it, a weaker one
455
+ * never does), and a row discovery archived is restored - unless it is
456
+ * the row of a workload that is gone, which a stale alias must not bring
457
+ * back. Application traces get null for such a row (isRetiredWorkloadRow):
458
+ * a trace that cannot restore it does not sight it either, so the trace
459
+ * path records no lastSeenAt on it and claims no endpoints for it.
460
+ *
461
+ * Races. Two writers can see the endpoint unowned at once. The row insert
462
+ * is guarded by the (projectId, databaseIdentifier) unique index - the
463
+ * loser re-reads the winner's row - and the endpoint claim by the
464
+ * (projectId, endpoint) one. If our new row loses the endpoint claim to a
465
+ * DIFFERENT row (a wire-compatible engine reported for the same endpoint,
466
+ * say), our row is an orphan with no endpoint: it is deleted before anyone
467
+ * saw it (it was inserted without hooks, so no feed, rule run or
468
+ * notification ever referred to it) and the owner's row is returned.
469
+ */
470
+ async findOrCreateByEndpoint(data) {
471
+ if (!data || !data.endpoint || !data.endpoint.host) {
472
+ return null;
473
+ }
474
+ const formatted = formatDatabaseEndpoint(data.endpoint);
475
+ const owner = await DatabaseServerEndpointService.findOwnerByEndpoint(data.projectId, formatted);
476
+ if (owner) {
477
+ const ownerRow = await this.findByIdInProject(data.projectId, owner.databaseServerId);
478
+ if (ownerRow && this.isRetiredFor(ownerRow, data.discoverySource)) {
479
+ return null;
480
+ }
481
+ await DatabaseServerEndpointService.markEndpointMatched(owner);
482
+ if (ownerRow) {
483
+ await this.recordOwnerSighting(ownerRow, data);
484
+ }
485
+ return ownerRow;
486
+ }
487
+ if (!data.allowCreate) {
488
+ return null;
489
+ }
490
+ if (getDatabaseEndpointScope(data.endpoint) === "local") {
491
+ return null;
492
+ }
493
+ // An unknown engine is kept verbatim, so clamp it to its column.
494
+ const dbSystem = cleanShortText(normalizeDatabaseSystem(data.dbSystem));
495
+ if (!dbSystem) {
496
+ return null;
497
+ }
498
+ /*
499
+ * The trace and container paths ask the budget themselves, once per run
500
+ * (AutoCreateBudget). The collector path runs on the ingest hot path and
501
+ * has no run to hang that on, so it is asked here - from a per-process
502
+ * cache, so a steady stream of known endpoints costs nothing.
503
+ */
504
+ if (data.discoverySource === DatabaseServerDiscoverySource.Collector &&
505
+ !(await this.allowsCollectorAutoCreate(data.projectId, formatted))) {
506
+ return null;
507
+ }
508
+ return await this.createEndpointDatabaseServer({
509
+ projectId: data.projectId,
510
+ dbSystem: dbSystem,
511
+ endpoint: data.endpoint,
512
+ formattedEndpoint: formatted,
513
+ discoverySource: data.discoverySource,
514
+ displayName: data.displayName,
515
+ collector: data.collector,
516
+ });
517
+ }
518
+ /**
519
+ * One row by id, only inside this project (an id from a resource attribute
520
+ * is untrusted input), with the columns discovery callers need.
521
+ */
522
+ async findByIdInProject(projectId, databaseServerId) {
523
+ if (!projectId || !databaseServerId) {
524
+ return null;
525
+ }
526
+ return await this.findOneBy({
527
+ query: {
528
+ _id: databaseServerId.toString(),
529
+ projectId: projectId,
530
+ },
531
+ select: DISCOVERY_SELECT,
532
+ props: {
533
+ isRoot: true,
534
+ },
535
+ });
536
+ }
537
+ /**
538
+ * The row of one Kubernetes / Docker / Podman database workload, found by
539
+ * workloadIdentifier, else ADOPTED from the endpoint row its aliases point
540
+ * at, else created (when allowed). Then: its aliases are claimed (an alias
541
+ * another row owns is skipped, never taken), its member keys merged, its
542
+ * workload columns refreshed and lastSeenAt stamped - never
543
+ * otelCollectorStatus, which only a collector sets.
544
+ *
545
+ * Adoption joins what application traces already found (an endpoint row
546
+ * named after the Service DNS) to the workload serving it, so the database
547
+ * does not show up twice. It happens only when the workload's GLOBAL-scope
548
+ * aliases are owned by exactly ONE row and that row is not already some
549
+ * other workload's. Two owners is ambiguous and is never tie-broken.
550
+ *
551
+ * Root, hook-free writes after the lookup; never throws (a worker run over
552
+ * many workloads must survive one bad one) - failures log and return null.
553
+ */
554
+ async upsertWorkloadDatabase(data) {
555
+ var _a;
556
+ try {
557
+ return await this.upsertWorkloadDatabaseOrThrow(data);
558
+ }
559
+ catch (error) {
560
+ logger.error(`DatabaseServerService.upsertWorkloadDatabase failed for workload ${(data === null || data === void 0 ? void 0 : data.workloadIdentifier) || "(none)"}: ${error instanceof Error ? error.message : String(error)}`, {
561
+ projectId: (_a = data === null || data === void 0 ? void 0 : data.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
562
+ });
563
+ return null;
564
+ }
565
+ }
566
+ /**
567
+ * Collector liveness: an OTel Collector database receiver (or the Database
568
+ * Agent) reported engine telemetry for this database. Refreshes lastSeenAt,
569
+ * collectorLastSeenAt and otelCollectorStatus "connected" once per window,
570
+ * and the agent / engine versions when they change. The engine version the
571
+ * collector reports is the one the row shows: the workload path's image
572
+ * tag never overwrites it while the collector is live.
573
+ *
574
+ * At most once per ten minutes per database, fleet-wide, the row is also
575
+ * reconciled with what the collector says: one it brings back after it was
576
+ * auto-archived is restored, and `dbSystem` (the engine the receiver or the
577
+ * agent reported) is weighed as collector evidence - it corrects an engine
578
+ * a container image or client spans guessed, and refines a family engine
579
+ * to a fork, but never overrides a person's choice or undoes a fork.
580
+ */
581
+ async recordCollectorHeartbeat(databaseServerId, extra) {
582
+ const agentVersion = cleanShortText(extra === null || extra === void 0 ? void 0 : extra.agentVersion);
583
+ const dbVersion = cleanShortText(extra === null || extra === void 0 ? void 0 : extra.dbVersion);
584
+ const extrasFingerprint = crypto
585
+ .createHash("sha256")
586
+ .update(JSON.stringify({
587
+ agentVersion: agentVersion,
588
+ dbVersion: dbVersion,
589
+ }))
590
+ .digest("hex");
591
+ const now = OneUptimeDate.getCurrentDate();
592
+ const liveness = {
593
+ lastSeenAt: now,
594
+ collectorLastSeenAt: now,
595
+ otelCollectorStatus: "connected",
596
+ };
597
+ const metadata = {};
598
+ if (agentVersion) {
599
+ metadata.agentVersion = agentVersion;
600
+ }
601
+ if (dbVersion) {
602
+ metadata.dbVersion = dbVersion;
603
+ }
604
+ /*
605
+ * One gated, non-blocking heartbeat write. The gates, the fail-open /
606
+ * fail-closed split and the liveness-only fallback all live in
607
+ * ResourceHeartbeat.
608
+ */
609
+ await ResourceHeartbeat.write({
610
+ service: this,
611
+ id: databaseServerId,
612
+ cacheNamespace: LAST_SEEN_CACHE_NAMESPACE,
613
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
614
+ liveness: liveness,
615
+ metadata: metadata,
616
+ fingerprint: extrasFingerprint,
617
+ describe: `database ${databaseServerId.toString()}`,
618
+ });
619
+ await this.reconcileAfterHeartbeat(databaseServerId, extra === null || extra === void 0 ? void 0 : extra.dbSystem);
620
+ }
621
+ /**
622
+ * Any-source liveness: an application called this database (a CLIENT
623
+ * span) or discovery saw it. Only lastSeenAt moves - a database that is
624
+ * merely queried says nothing about whether engine metrics are arriving,
625
+ * so otelCollectorStatus is left alone.
626
+ */
627
+ async recordSighting(databaseServerId) {
628
+ const liveness = {
629
+ lastSeenAt: OneUptimeDate.getCurrentDate(),
630
+ };
631
+ await ResourceHeartbeat.write({
632
+ service: this,
633
+ id: databaseServerId,
634
+ cacheNamespace: SIGHTING_CACHE_NAMESPACE,
635
+ throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS,
636
+ liveness: liveness,
637
+ fingerprint: "",
638
+ describe: `database ${databaseServerId.toString()} (sighting)`,
639
+ });
640
+ }
641
+ /**
642
+ * Additively attach labels to a database. Existing labels are never
643
+ * removed - manual labels set via the UI survive ingest. The set of
644
+ * labelIds passed in is fingerprinted and cached for 60s so a collector
645
+ * pushing the same labels every batch costs one cache lookup, not a
646
+ * join-table scan. The labels this adds are recorded as automatic, so they
647
+ * never count as somebody investing in the row (a label a person already
648
+ * put on the row is not re-added, and stays theirs).
649
+ */
650
+ async attachLabels(data) {
651
+ var _a;
652
+ if (!data.labelIds || data.labelIds.length === 0) {
653
+ return;
654
+ }
655
+ const cacheKey = data.databaseServerId.toString();
656
+ const fingerprint = fingerprintLabelIds(data.labelIds);
657
+ const cached = await GlobalCache.getString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey);
658
+ if (cached === fingerprint) {
659
+ return;
660
+ }
661
+ try {
662
+ const databaseServerIdStr = data.databaseServerId.toString();
663
+ const existingLabels = await this.getRepository()
664
+ .createQueryBuilder()
665
+ .relation(Model, "labels")
666
+ .of(databaseServerIdStr)
667
+ .loadMany();
668
+ const existingIds = new Set();
669
+ for (const lbl of existingLabels) {
670
+ const idStr = (_a = lbl._id) === null || _a === void 0 ? void 0 : _a.toString();
671
+ if (idStr) {
672
+ existingIds.add(idStr);
673
+ }
674
+ }
675
+ const toAddIds = [];
676
+ const seen = new Set();
677
+ for (const id of data.labelIds) {
678
+ const idStr = id.toString();
679
+ if (existingIds.has(idStr) || seen.has(idStr)) {
680
+ continue;
681
+ }
682
+ seen.add(idStr);
683
+ toAddIds.push(idStr);
684
+ }
685
+ if (toAddIds.length > 0) {
686
+ await this.getRepository()
687
+ .createQueryBuilder()
688
+ .relation(Model, "labels")
689
+ .of(databaseServerIdStr)
690
+ .add(toAddIds);
691
+ await this.recordAutomaticAssignments({
692
+ databaseServerId: data.databaseServerId,
693
+ kind: "labelIds",
694
+ ids: toAddIds,
695
+ });
696
+ }
697
+ await GlobalCache.setString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey, fingerprint, { expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS });
698
+ }
699
+ catch (err) {
700
+ logger.warn(`DatabaseServerService.attachLabels failed for database ${data.databaseServerId.toString()}: ${err instanceof Error ? err.message : String(err)}`);
701
+ }
702
+ }
703
+ /*
704
+ * Minutes of collector SILENCE before Engine metrics reads Disconnected
705
+ * (DATABASE_SERVER_COLLECTOR_STALE_MINUTES): 15 by default; an override
706
+ * below 10 is raised to 10 rather than allowed to flap healthy databases.
707
+ * Anything unparseable falls back to the default. It is measured from the
708
+ * last data, not from collectorLastSeenAt - see getCollectorSilenceCutoff.
709
+ */
710
+ getCollectorStaleThresholdMinutes() {
711
+ return readIntegerEnv({
712
+ name: "DATABASE_SERVER_COLLECTOR_STALE_MINUTES",
713
+ defaultValue: DEFAULT_COLLECTOR_STALE_MINUTES,
714
+ min: MIN_COLLECTOR_STALE_MINUTES,
715
+ });
716
+ }
717
+ /*
718
+ * The collectorLastSeenAt before which a collector has been silent for at
719
+ * least getCollectorStaleThresholdMinutes(). The heartbeat that moves
720
+ * collectorLastSeenAt runs behind the 5-minute OTel ingest maintenance
721
+ * fence, so while data arrives continuously the column trails the last
722
+ * data by up to COLLECTOR_HEARTBEAT_LAG_MINUTES. Comparing it with the
723
+ * threshold alone flipped databases after as little as ~8 minutes of real
724
+ * silence (e2e: 12m52s); adding the lag makes "15 minutes" a floor.
725
+ */
726
+ getCollectorSilenceCutoff(now) {
727
+ return OneUptimeDate.addRemoveMinutes(now, -(this.getCollectorStaleThresholdMinutes() +
728
+ COLLECTOR_HEARTBEAT_LAG_MINUTES));
729
+ }
730
+ /**
731
+ * Flip databases whose collector went quiet to "disconnected": silent for
732
+ * at least getCollectorStaleThresholdMinutes() (getCollectorSilenceCutoff).
733
+ * Keyed on collectorLastSeenAt, NOT lastSeenAt: an application still
734
+ * querying the database keeps lastSeenAt fresh, and that must not hide a
735
+ * dead collector. One conditional statement, so a heartbeat landing
736
+ * mid-sweep cannot be overwritten. Returns how many rows were flipped.
737
+ */
738
+ async markDisconnectedDatabaseServers() {
739
+ const threshold = this.getCollectorSilenceCutoff(OneUptimeDate.getCurrentDate());
740
+ const rows = await this.getRepository().manager.query(`WITH "disconnected" AS (
741
+ UPDATE "DatabaseServer"
742
+ SET "otelCollectorStatus" = 'disconnected', "updatedAt" = CURRENT_TIMESTAMP
743
+ WHERE "otelCollectorStatus" = 'connected'
744
+ AND ("collectorLastSeenAt" IS NULL OR "collectorLastSeenAt" < $1)
745
+ AND "deletedAt" IS NULL
746
+ RETURNING "_id"
747
+ )
748
+ SELECT COUNT(*)::int AS "count" FROM "disconnected"`, [threshold]);
749
+ return readCount(rows);
750
+ }
751
+ /*
752
+ * Days a discovered database may go unseen before the sweep archives it.
753
+ * Default 7, at least 1.
754
+ */
755
+ getAutoArchiveDays() {
756
+ return readIntegerEnv({
757
+ name: "DATABASE_SERVER_AUTO_ARCHIVE_DAYS",
758
+ defaultValue: DEFAULT_AUTO_ARCHIVE_DAYS,
759
+ min: MIN_AUTO_ARCHIVE_DAYS,
760
+ });
761
+ }
762
+ /**
763
+ * Archive discovered databases nobody has seen - and nobody has touched -
764
+ * for getAutoArchiveDays(). Discovery creates rows on its own, so without
765
+ * this a decommissioned database, or a one-off connection string in a
766
+ * trace, would sit in the list forever.
767
+ *
768
+ * "Untouched" is what makes this safe to do unasked: a manually added
769
+ * database is never archived, and neither is one somebody invested in -
770
+ * labels or owners a PERSON added (not the ones rules or ingest attached
771
+ * on their own), a link from an incident / alert / scheduled maintenance,
772
+ * an endpoint a person added, or a retention override. Every one of those
773
+ * checks is done in SQL, scoped to the row's project and to live rows
774
+ * (UNTOUCHED_DATABASE_SERVER_PREDICATE).
775
+ *
776
+ * Two more rows are left alone:
777
+ * - one a person restored from the archive, until discovery sees it
778
+ * again or the grace period passes - their Restore must stick;
779
+ * - one whose Kubernetes cluster / Docker / Podman host has gone quiet:
780
+ * discovery only visits connected parents, so while the parent is dark
781
+ * its databases are unseen for a reason that says nothing about them.
782
+ * Staleness is anchored to the parent's own last contact - a row is
783
+ * only stale if it went unseen well before its parent did - the way
784
+ * the Kubernetes and Docker inventory cleanups keep last-known state.
785
+ * A row whose parent was deleted ages out on its own.
786
+ *
787
+ * The row is marked autoArchivedAt, which is what lets discovery restore
788
+ * it the moment it is seen again - and never restore one a person
789
+ * archived. A workload's row is brought back only by its workload or a
790
+ * collector, never by application traces (isRetiredWorkloadRow), and its
791
+ * feed item says so. Bounded per run. Returns how many rows were archived.
792
+ */
793
+ async autoArchiveStaleDatabaseServers() {
794
+ const days = this.getAutoArchiveDays();
795
+ const now = OneUptimeDate.getCurrentDate();
796
+ const cutoff = OneUptimeDate.addRemoveDays(now, -days);
797
+ const restoreGraceCutoff = OneUptimeDate.addRemoveDays(now, -Math.max(MANUAL_RESTORE_GRACE_DAYS, days));
798
+ const archived = await this.getRepository().manager.query(`WITH "candidates" AS (
799
+ SELECT ds."_id"
800
+ FROM "DatabaseServer" ds
801
+ LEFT JOIN "KubernetesCluster" kc
802
+ ON kc."_id" = ds."kubernetesClusterId" AND kc."deletedAt" IS NULL
803
+ LEFT JOIN "DockerHost" dh
804
+ ON dh."_id" = ds."dockerHostId" AND dh."deletedAt" IS NULL
805
+ LEFT JOIN "PodmanHost" ph
806
+ ON ph."_id" = ds."podmanHostId" AND ph."deletedAt" IS NULL
807
+ WHERE ds."deletedAt" IS NULL
808
+ AND ds."isArchived" = false
809
+ AND ds."discoverySource" IS NOT NULL
810
+ AND ds."discoverySource" <> $3
811
+ AND COALESCE(ds."lastSeenAt", ds."createdAt") < $1
812
+ AND COALESCE(ds."lastSeenAt", ds."createdAt") < COALESCE(kc."lastSeenAt", dh."lastSeenAt", ph."lastSeenAt", $2) - INTERVAL '1 hour'
813
+ AND (
814
+ ds."manuallyRestoredAt" IS NULL
815
+ OR ds."manuallyRestoredAt" < $5
816
+ OR COALESCE(ds."lastSeenAt", ds."createdAt") > ds."manuallyRestoredAt"
817
+ )
818
+ AND ${UNTOUCHED_DATABASE_SERVER_PREDICATE}
819
+ ORDER BY COALESCE(ds."lastSeenAt", ds."createdAt") ASC
820
+ LIMIT $4
821
+ ),
822
+ "archived" AS (
823
+ UPDATE "DatabaseServer" stale
824
+ SET "isArchived" = true,
825
+ "archivedAt" = $2,
826
+ "autoArchivedAt" = $2,
827
+ "archivedByUserId" = NULL,
828
+ "updatedAt" = CURRENT_TIMESTAMP
829
+ FROM "candidates"
830
+ WHERE stale."_id" = "candidates"."_id"
831
+ AND stale."isArchived" = false
832
+ RETURNING stale."_id" AS "_id", stale."projectId" AS "projectId", stale."workloadIdentifier" AS "workloadIdentifier"
833
+ )
834
+ SELECT "_id", "projectId", "workloadIdentifier" FROM "archived"`, [
835
+ cutoff,
836
+ now,
837
+ DatabaseServerDiscoverySource.Manual,
838
+ AUTO_ARCHIVE_BATCH_SIZE,
839
+ restoreGraceCutoff,
840
+ ]);
841
+ const rows = Array.isArray(archived) ? archived : [];
842
+ for (const row of rows) {
843
+ if (!row || !row._id || !row.projectId) {
844
+ continue;
845
+ }
846
+ await this.writeAutoArchiveFeed({
847
+ databaseServerId: new ObjectID(row._id.toString()),
848
+ projectId: new ObjectID(row.projectId.toString()),
849
+ days: days,
850
+ isWorkload: Boolean(row.workloadIdentifier),
851
+ });
852
+ }
853
+ return rows.length;
854
+ }
855
+ /*
856
+ * How many discovered databases (collector, client spans, Kubernetes,
857
+ * Docker, Podman) a project may accumulate before discovery stops creating
858
+ * more on its own. Manual rows do not count and are never blocked, and a
859
+ * database linked to its agent by DATABASE_SERVER_ID never needs a create.
860
+ * Collector rows count: a collector keyed on pod IPs or pod host names
861
+ * would otherwise mint rows without bound. Default 500, at least 0 (0
862
+ * turns auto-creation off).
863
+ */
864
+ getAutoCreateBudget() {
865
+ return readIntegerEnv({
866
+ name: "DATABASE_SERVER_AUTO_CREATE_BUDGET",
867
+ defaultValue: DEFAULT_AUTO_CREATE_BUDGET,
868
+ min: 0,
869
+ });
870
+ }
871
+ /**
872
+ * True while the project holds fewer live, non-archived discovered
873
+ * databases than getAutoCreateBudget(). Archived rows do not count, so
874
+ * the auto-archive sweep frees budget. Always reads the count - the
875
+ * workers call it once per project per run (AutoCreateBudget).
876
+ */
877
+ async isUnderAutoCreateBudget(projectId) {
878
+ const budget = this.getAutoCreateBudget();
879
+ if (budget <= 0) {
880
+ return false;
881
+ }
882
+ return (await this.countAutoCreatedDatabaseServers(projectId)) < budget;
883
+ }
884
+ /**
885
+ * isUnderAutoCreateBudget for the ingest hot path: the count is read at
886
+ * most once a minute per project in this process, and every row this
887
+ * process creates in between is added to it, so a burst of new endpoints
888
+ * stops at the budget instead of a minute's worth past it.
889
+ */
890
+ async isUnderAutoCreateBudgetCached(projectId) {
891
+ const budget = this.getAutoCreateBudget();
892
+ if (budget <= 0) {
893
+ return false;
894
+ }
895
+ const key = projectId.toString();
896
+ let cached = this.autoCreateCountMemo.get(key);
897
+ if (!cached) {
898
+ cached = {
899
+ count: await this.countAutoCreatedDatabaseServers(projectId),
900
+ readAtMs: Date.now(),
901
+ };
902
+ this.autoCreateCountMemo.set(key, cached);
903
+ }
904
+ return cached.count < budget;
905
+ }
906
+ // Live, non-archived, non-manual rows of one project.
907
+ async countAutoCreatedDatabaseServers(projectId) {
908
+ const rows = await this.getRepository().manager.query(`SELECT COUNT(*)::int AS "count"
909
+ FROM "DatabaseServer"
910
+ WHERE "projectId" = $1
911
+ AND "deletedAt" IS NULL
912
+ AND "isArchived" = false
913
+ AND COALESCE("discoverySource", '') <> $2`, [projectId.toString(), DatabaseServerDiscoverySource.Manual]);
914
+ return readCount(rows);
915
+ }
916
+ /**
917
+ * Display name for this database, or an empty string when the row is gone.
918
+ * Feed writers call this on a best-effort basis, so a missing row must not
919
+ * throw and take the surrounding write down with it.
920
+ */
921
+ async getDatabaseServerName(data) {
922
+ const databaseServer = await this.findOneById({
923
+ id: data.databaseServerId,
924
+ select: {
925
+ name: true,
926
+ },
927
+ props: {
928
+ isRoot: true,
929
+ },
930
+ });
931
+ return (databaseServer === null || databaseServer === void 0 ? void 0 : databaseServer.name) || "";
932
+ }
933
+ /**
934
+ * The database's name - but only when `props` may read that row (project,
935
+ * label and Owned scopes applied); "" otherwise, or on any error. Refusal
936
+ * messages use it, so a caller with scoped permissions is never told the
937
+ * name of a database they cannot see. Without props (or as root) the name
938
+ * is read as root.
939
+ */
940
+ async getDatabaseServerNameIfReadable(data) {
941
+ if (!data.props || data.props.isRoot) {
942
+ return await this.getDatabaseServerName({
943
+ databaseServerId: data.databaseServerId,
944
+ });
945
+ }
946
+ try {
947
+ const readable = await this.findOneBy({
948
+ query: {
949
+ _id: data.databaseServerId.toString(),
950
+ },
951
+ select: {
952
+ name: true,
953
+ },
954
+ props: data.props,
955
+ });
956
+ return (readable === null || readable === void 0 ? void 0 : readable.name) || "";
957
+ }
958
+ catch (_a) {
959
+ return "";
960
+ }
961
+ }
962
+ /**
963
+ * Validate the database a child row (an owner, a feed note) is created
964
+ * for by a caller: the FK column and the relation object must agree
965
+ * (TypeORM would persist the relation object's id over the column's), and
966
+ * the database must be in the caller's project. Leaves the validated FK
967
+ * column as the only reference. Root creates are the caller's business.
968
+ */
969
+ async assertDatabaseServerReferenceInProject(createBy) {
970
+ const data = createBy.data;
971
+ const projectId = createBy.props.tenantId ||
972
+ data.getColumnValue("projectId") ||
973
+ undefined;
974
+ if (!projectId) {
975
+ throw new BadDataException("Project ID is required.");
976
+ }
977
+ const databaseServerId = RelationIdUtil.readConsistent(data, ["databaseServerId", "databaseServer"], "database");
978
+ data.setColumnValue("databaseServer", undefined);
979
+ if (!databaseServerId) {
980
+ throw new BadDataException("Select a database.");
981
+ }
982
+ const databaseServer = await this.findOneBy({
983
+ query: {
984
+ _id: databaseServerId.toString(),
985
+ projectId: projectId,
986
+ },
987
+ select: {
988
+ _id: true,
989
+ },
990
+ props: {
991
+ isRoot: true,
992
+ },
993
+ });
994
+ if (!databaseServer) {
995
+ throw new BadDataException("Database not found.");
996
+ }
997
+ data.setColumnValue("databaseServerId", databaseServerId);
998
+ return databaseServerId;
999
+ }
1000
+ /**
1001
+ * Record labels or owners that a rule or ingest attached to a database on
1002
+ * its own (DatabaseServer.automaticAssignments), so the auto-archive sweep
1003
+ * does not mistake them for a person's investment. One atomic statement;
1004
+ * ids deduped. Never throws - it only annotates a write that happened.
1005
+ */
1006
+ async recordAutomaticAssignments(data) {
1007
+ const ids = normalizeAssignmentIds(data.ids);
1008
+ if (ids.length === 0 || !AUTOMATIC_ASSIGNMENT_KINDS.includes(data.kind)) {
1009
+ return;
1010
+ }
1011
+ try {
1012
+ await this.getRepository().manager.query(`UPDATE "DatabaseServer"
1013
+ SET "automaticAssignments" =
1014
+ (CASE WHEN jsonb_typeof("automaticAssignments") = 'object' THEN "automaticAssignments" ELSE '{}'::jsonb END)
1015
+ || jsonb_build_object(
1016
+ $2::text,
1017
+ (
1018
+ SELECT COALESCE(jsonb_agg(DISTINCT assigned.id), '[]'::jsonb)
1019
+ FROM jsonb_array_elements_text(
1020
+ (CASE WHEN jsonb_typeof("automaticAssignments" -> $2::text) = 'array' THEN "automaticAssignments" -> $2::text ELSE '[]'::jsonb END)
1021
+ || $3::jsonb
1022
+ ) AS assigned(id)
1023
+ )
1024
+ )
1025
+ WHERE "_id" = $1`, [data.databaseServerId.toString(), data.kind, JSON.stringify(ids)]);
1026
+ }
1027
+ catch (error) {
1028
+ logger.warn(`DatabaseServerService: could not record automatic ${data.kind} on database ${data.databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
1029
+ }
1030
+ }
1031
+ /**
1032
+ * The opposite of recordAutomaticAssignments: a person (re)added these
1033
+ * labels or owners, so from now on they count as investment. Never throws.
1034
+ */
1035
+ async forgetAutomaticAssignments(data) {
1036
+ const ids = normalizeAssignmentIds(data.ids);
1037
+ if (ids.length === 0 || !AUTOMATIC_ASSIGNMENT_KINDS.includes(data.kind)) {
1038
+ return;
1039
+ }
1040
+ try {
1041
+ await this.getRepository().manager.query(`UPDATE "DatabaseServer"
1042
+ SET "automaticAssignments" = "automaticAssignments" || jsonb_build_object(
1043
+ $2::text,
1044
+ (
1045
+ SELECT COALESCE(jsonb_agg(assigned.id), '[]'::jsonb)
1046
+ FROM jsonb_array_elements_text("automaticAssignments" -> $2::text) AS assigned(id)
1047
+ WHERE NOT (assigned.id = ANY($3::text[]))
1048
+ )
1049
+ )
1050
+ WHERE "_id" = $1
1051
+ AND jsonb_typeof("automaticAssignments") = 'object'
1052
+ AND jsonb_typeof("automaticAssignments" -> $2::text) = 'array'`, [data.databaseServerId.toString(), data.kind, ids]);
1053
+ }
1054
+ catch (error) {
1055
+ logger.warn(`DatabaseServerService: could not forget automatic ${data.kind} on database ${data.databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
1056
+ }
1057
+ }
1058
+ async getDatabaseServerLinkInDashboard(projectId, databaseServerId) {
1059
+ const dashboardUrl = await DatabaseConfig.getDashboardUrl();
1060
+ return URL.fromString(dashboardUrl.toString()).addRoute(`/${projectId.toString()}/databases/${databaseServerId.toString()}`);
1061
+ }
1062
+ /**
1063
+ * "[Database PostgreSQL orders-db:5432](https://…)" - the form every feed
1064
+ * item uses to name the resource it is about.
1065
+ */
1066
+ async getDatabaseServerMarkdownLink(projectId, databaseServerId) {
1067
+ const name = await this.getDatabaseServerName({
1068
+ databaseServerId: databaseServerId,
1069
+ });
1070
+ const link = await this.getDatabaseServerLinkInDashboard(projectId, databaseServerId);
1071
+ return `[Database ${name}](${link.toString()})`;
1072
+ }
1073
+ /**
1074
+ * For tests: forget which rows this process recently checked for an
1075
+ * auto-archive restore, the way suites clear ResourceHeartbeat's memo.
1076
+ */
1077
+ clearAutoRestoreCheckMemo() {
1078
+ this.autoRestoreCheckMemo.clear();
1079
+ }
1080
+ // For tests: forget the cached auto-create counts and budget warnings.
1081
+ clearAutoCreateBudgetMemo() {
1082
+ this.autoCreateCountMemo.clear();
1083
+ this.autoCreateBudgetWarningMemo.clear();
1084
+ }
1085
+ async upsertWorkloadDatabaseOrThrow(data) {
1086
+ var _a;
1087
+ if (!data || !data.projectId) {
1088
+ return null;
1089
+ }
1090
+ const workloadIdentifier = truncateLongText(typeof data.workloadIdentifier === "string"
1091
+ ? data.workloadIdentifier.trim()
1092
+ : "");
1093
+ if (!workloadIdentifier) {
1094
+ return null;
1095
+ }
1096
+ const dbSystem = cleanShortText(normalizeDatabaseSystem(data.dbSystem));
1097
+ if (!dbSystem) {
1098
+ return null;
1099
+ }
1100
+ const projectId = data.projectId;
1101
+ const aliases = canonicalizeWorkloadAliases(data.aliases, dbSystem);
1102
+ // Who owns each alias today: one query, reused by adoption and claiming.
1103
+ const ownersByEndpoint = await this.findEndpointOwners(projectId, aliases.map((alias) => {
1104
+ return alias.endpoint;
1105
+ }));
1106
+ let row = await this.findByWorkloadIdentifier(projectId, workloadIdentifier);
1107
+ let createdHere = false;
1108
+ if (!row) {
1109
+ row = await this.adoptSameWorkloadOfEngineFamily({
1110
+ projectId: projectId,
1111
+ workloadIdentifier: workloadIdentifier,
1112
+ dbSystem: dbSystem,
1113
+ });
1114
+ }
1115
+ if (!row) {
1116
+ row = await this.adoptEndpointDatabaseServer({
1117
+ projectId: projectId,
1118
+ workloadIdentifier: workloadIdentifier,
1119
+ aliases: aliases,
1120
+ ownersByEndpoint: ownersByEndpoint,
1121
+ });
1122
+ }
1123
+ if (!row) {
1124
+ if (!data.allowCreate) {
1125
+ return null;
1126
+ }
1127
+ const created = await this.createWorkloadDatabaseServer({
1128
+ data: data,
1129
+ workloadIdentifier: workloadIdentifier,
1130
+ dbSystem: dbSystem,
1131
+ primaryAlias: aliases[0],
1132
+ });
1133
+ row = created.row;
1134
+ createdHere = created.createdHere;
1135
+ }
1136
+ if (!row || !row.id) {
1137
+ return null;
1138
+ }
1139
+ const now = OneUptimeDate.getCurrentDate();
1140
+ await this.claimWorkloadAliases({
1141
+ projectId: projectId,
1142
+ databaseServerId: row.id,
1143
+ workloadIdentifier: workloadIdentifier,
1144
+ discoverySource: data.discoverySource,
1145
+ aliases: aliases,
1146
+ ownersByEndpoint: ownersByEndpoint,
1147
+ primaryEndpoint: createdHere ? (_a = aliases[0]) === null || _a === void 0 ? void 0 : _a.endpoint : undefined,
1148
+ now: now,
1149
+ });
1150
+ const memberEntityKeys = mergeDatabaseServerMemberKeys(row.memberEntityKeys, Array.isArray(data.memberKeysSeenNow) ? data.memberKeysSeenNow : [], now);
1151
+ const instanceCount = toInstanceCount(data.instanceCount);
1152
+ /*
1153
+ * A plain record: PartialEntity's deep partial of the JSON member-key
1154
+ * column is too deep for the compiler to instantiate.
1155
+ */
1156
+ const update = {
1157
+ memberEntityKeys: memberEntityKeys,
1158
+ instanceCount: instanceCount,
1159
+ lastSeenAt: now,
1160
+ workloadLastSeenAt: now,
1161
+ };
1162
+ const workloadKind = cleanShortText(data.workloadKind);
1163
+ if (workloadKind) {
1164
+ update["workloadKind"] = workloadKind;
1165
+ }
1166
+ const workloadName = cleanLongText(data.workloadName);
1167
+ if (workloadName) {
1168
+ update["workloadName"] = workloadName;
1169
+ }
1170
+ const kubernetesNamespace = cleanShortText(data.kubernetesNamespace);
1171
+ if (kubernetesNamespace) {
1172
+ update["kubernetesNamespace"] = kubernetesNamespace;
1173
+ }
1174
+ if (data.kubernetesClusterId) {
1175
+ update["kubernetesClusterId"] = data.kubernetesClusterId;
1176
+ }
1177
+ if (data.dockerHostId) {
1178
+ update["dockerHostId"] = data.dockerHostId;
1179
+ }
1180
+ if (data.podmanHostId) {
1181
+ update["podmanHostId"] = data.podmanHostId;
1182
+ }
1183
+ /*
1184
+ * The image tag is the version of last resort: while a collector is
1185
+ * reporting the engine's own version (recordCollectorHeartbeat), that
1186
+ * one stands - otherwise the two writers would flip the column between
1187
+ * "16" and "16.4" all day.
1188
+ */
1189
+ const dbVersion = cleanShortText(data.dbVersion);
1190
+ if (dbVersion && (!this.isCollectorFresh(row, now) || !row.dbVersion)) {
1191
+ update["dbVersion"] = dbVersion;
1192
+ }
1193
+ /*
1194
+ * The image is evidence of the engine: it corrects what client spans
1195
+ * guessed for a row this workload adopted, refines a family engine to a
1196
+ * fork (redis -> valkey) and undoes a fork only client spans named. A
1197
+ * row created here already has it.
1198
+ */
1199
+ const previousSystem = row.dbSystem;
1200
+ const engine = createdHere
1201
+ ? null
1202
+ : decideDatabaseSystem({
1203
+ current: {
1204
+ system: row.dbSystem,
1205
+ evidence: getStoredDatabaseSystemEvidence(row),
1206
+ },
1207
+ incoming: {
1208
+ system: dbSystem,
1209
+ evidence: DatabaseSystemEvidence.Container,
1210
+ },
1211
+ });
1212
+ if (engine) {
1213
+ update["dbSystemSource"] = engine.evidence;
1214
+ if (engine.system !== normalizeDatabaseSystem(row.dbSystem)) {
1215
+ update["dbSystem"] = engine.system;
1216
+ const renamed = renameForDatabaseSystem(row, engine.system);
1217
+ if (renamed) {
1218
+ update["name"] = renamed;
1219
+ }
1220
+ }
1221
+ }
1222
+ row.workloadIdentifier = workloadIdentifier;
1223
+ /*
1224
+ * Nothing but the liveness stamps would change, and the row was written
1225
+ * recently: skip the write (see WORKLOAD_REFRESH_MINUTES). The row is
1226
+ * returned as it was read. Aliases and the auto-archive restore above
1227
+ * and below still run on every sighting.
1228
+ */
1229
+ if (workloadColumnsChanged(row, update) ||
1230
+ !this.isWorkloadRowFresh(row, now)) {
1231
+ await this.updateColumnsByIdWithoutHooks({
1232
+ id: row.id,
1233
+ data: update,
1234
+ });
1235
+ row.memberEntityKeys = memberEntityKeys;
1236
+ row.instanceCount = instanceCount;
1237
+ row.lastSeenAt = now;
1238
+ row.workloadLastSeenAt = now;
1239
+ if (engine) {
1240
+ row.dbSystemSource = engine.evidence;
1241
+ if (typeof update["dbSystem"] === "string") {
1242
+ row.dbSystem = update["dbSystem"];
1243
+ }
1244
+ if (typeof update["name"] === "string") {
1245
+ row.name = update["name"];
1246
+ }
1247
+ }
1248
+ if (typeof update["dbVersion"] === "string") {
1249
+ row.dbVersion = update["dbVersion"];
1250
+ }
1251
+ }
1252
+ if (engine && previousSystem && row.dbSystem !== previousSystem) {
1253
+ await this.writeEngineCorrectedFeed({
1254
+ row: row,
1255
+ previousSystem: previousSystem,
1256
+ evidence: engine.evidence,
1257
+ });
1258
+ }
1259
+ await this.restoreIfAutoArchived(row);
1260
+ return row;
1261
+ }
1262
+ /**
1263
+ * Weigh new evidence of a row's engine (see decideDatabaseSystem) and, if
1264
+ * it wins, write it: the engine, the evidence behind it, and the
1265
+ * auto-generated name when the row still carries the old one - a name a
1266
+ * person chose is kept. One compare-and-set on the engine AND the
1267
+ * evidence that were weighed (writeDatabaseSystemIfUnchanged): a writer
1268
+ * that loses it to a concurrent one changes nothing - not the row in
1269
+ * memory, not the feed - so two writers weighing at once never both
1270
+ * announce the change, and stronger evidence written meanwhile is never
1271
+ * overwritten with weaker. Never throws.
1272
+ */
1273
+ async applyDatabaseSystemEvidence(row, incoming) {
1274
+ if (!row.id || !row.projectId || !row.dbSystem) {
1275
+ return;
1276
+ }
1277
+ const decision = decideDatabaseSystem({
1278
+ current: {
1279
+ system: row.dbSystem,
1280
+ evidence: getStoredDatabaseSystemEvidence(row),
1281
+ },
1282
+ incoming: incoming,
1283
+ });
1284
+ if (!decision) {
1285
+ return;
1286
+ }
1287
+ const previousSystem = row.dbSystem;
1288
+ const engineChanged = decision.system !== normalizeDatabaseSystem(previousSystem);
1289
+ const renamed = engineChanged
1290
+ ? renameForDatabaseSystem(row, decision.system)
1291
+ : null;
1292
+ let written = null;
1293
+ try {
1294
+ written = await this.writeDatabaseSystemIfUnchanged({
1295
+ id: row.id,
1296
+ projectId: row.projectId,
1297
+ evidence: decision.evidence,
1298
+ system: engineChanged ? decision.system : undefined,
1299
+ rename: renamed && row.name ? { from: row.name, to: renamed } : undefined,
1300
+ expected: {
1301
+ dbSystem: previousSystem,
1302
+ dbSystemSource: row.dbSystemSource || null,
1303
+ },
1304
+ });
1305
+ }
1306
+ catch (error) {
1307
+ logger.warn(`DatabaseServerService: could not update the engine of database ${row.id.toString()}: ${error instanceof Error ? error.message : String(error)}`);
1308
+ return;
1309
+ }
1310
+ /*
1311
+ * Another writer changed the engine or its evidence since this row was
1312
+ * read (or the row is gone): theirs stands, and the change is theirs to
1313
+ * announce.
1314
+ */
1315
+ if (!written) {
1316
+ logger.debug(`DatabaseServerService: the engine of database ${row.id.toString()} changed while ${decision.evidence} evidence was weighed; left as the other writer set it.`);
1317
+ return;
1318
+ }
1319
+ row.dbSystemSource = decision.evidence;
1320
+ if (!engineChanged) {
1321
+ return;
1322
+ }
1323
+ row.dbSystem = decision.system;
1324
+ if (written.name) {
1325
+ row.name = written.name;
1326
+ }
1327
+ await this.writeEngineCorrectedFeed({
1328
+ row: row,
1329
+ previousSystem: previousSystem,
1330
+ evidence: decision.evidence,
1331
+ });
1332
+ }
1333
+ /*
1334
+ * The write of applyDatabaseSystemEvidence: the evidence, and - when the
1335
+ * engine changes - the engine, updatedAt and the generated name. The name
1336
+ * only moves while it is still `rename.from`, so a person renaming the
1337
+ * row meanwhile keeps their name. Matches only while the engine and its
1338
+ * evidence are still what was read. Returns the row's name as written, or
1339
+ * null when nothing matched.
1340
+ */
1341
+ async writeDatabaseSystemIfUnchanged(data) {
1342
+ var _a;
1343
+ const params = [data.evidence];
1344
+ const sets = [`"dbSystemSource" = $1`];
1345
+ if (data.system) {
1346
+ params.push(data.system);
1347
+ sets.push(`"dbSystem" = $${params.length}`);
1348
+ if (data.rename) {
1349
+ params.push(data.rename.from, data.rename.to);
1350
+ sets.push(`"name" = CASE WHEN "name" IS NOT DISTINCT FROM $${params.length - 1} THEN $${params.length} ELSE "name" END`);
1351
+ }
1352
+ // Recording stronger evidence for the same engine is bookkeeping.
1353
+ sets.push(`"updatedAt" = CURRENT_TIMESTAMP`);
1354
+ }
1355
+ params.push(data.id.toString(), data.projectId.toString(), data.expected.dbSystem, data.expected.dbSystemSource);
1356
+ const at = params.length - 3;
1357
+ /*
1358
+ * A CTE, so the statement is a SELECT of the row it wrote: TypeORM
1359
+ * answers a bare UPDATE with [rows, rowCount], matched or not.
1360
+ */
1361
+ const written = await this.getRepository().manager.query(`WITH "written" AS (
1362
+ UPDATE "DatabaseServer"
1363
+ SET ${sets.join(", ")}
1364
+ WHERE "_id" = $${at}
1365
+ AND "projectId" = $${at + 1}
1366
+ AND "deletedAt" IS NULL
1367
+ AND "dbSystem" IS NOT DISTINCT FROM $${at + 2}
1368
+ AND "dbSystemSource" IS NOT DISTINCT FROM $${at + 3}
1369
+ RETURNING "_id", "name"
1370
+ )
1371
+ SELECT "_id", "name" FROM "written"`, params);
1372
+ if (!Array.isArray(written) || written.length === 0) {
1373
+ return null;
1374
+ }
1375
+ const name = (_a = written[0]) === null || _a === void 0 ? void 0 : _a.name;
1376
+ return { name: typeof name === "string" ? name : null };
1377
+ }
1378
+ async writeEngineCorrectedFeed(data) {
1379
+ const row = data.row;
1380
+ if (!row.id || !row.projectId) {
1381
+ return;
1382
+ }
1383
+ try {
1384
+ logger.info(`DatabaseServerService: database ${row.id.toString()} is now identified as ${row.dbSystem} (was ${data.previousSystem}), from ${data.evidence} evidence.`, { projectId: row.projectId.toString() });
1385
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
1386
+ databaseServerId: row.id,
1387
+ projectId: row.projectId,
1388
+ databaseServerFeedEventType: DatabaseServerFeedEventType.DatabaseServerUpdated,
1389
+ displayColor: Gray500,
1390
+ feedInfoInMarkdown: `🔎 ${await this.getDatabaseServerMarkdownLink(row.projectId, row.id)} is now identified as **${getDatabaseSystemDisplayName(row.dbSystem)}** (it was shown as ${getDatabaseSystemDisplayName(data.previousSystem)}), from ${DATABASE_SYSTEM_EVIDENCE_LABEL[data.evidence]}.`,
1391
+ moreInformationInMarkdown: `Client libraries of wire-compatible databases report the engine they speak to, so a database first seen in application traces can be named after the wrong engine. A stronger source - a container image, a collector receiver or the Database Agent - corrects it. A fork (MariaDB, Valkey, ScyllaDB ...) refines the engine it forks when a source at least as strong names it, or its container image does; application traces alone never override an image or a collector. An engine a person chose is never changed.`,
1392
+ });
1393
+ }
1394
+ catch (error) {
1395
+ logger.warn(`DatabaseServerService: could not record the engine change of database ${row.id.toString()}: ${error instanceof Error ? error.message : String(error)}`);
1396
+ }
1397
+ }
1398
+ isCollectorFresh(row, now) {
1399
+ if (!row.collectorLastSeenAt) {
1400
+ return false;
1401
+ }
1402
+ // Live exactly as long as the disconnect sweep would leave it connected.
1403
+ return (new Date(row.collectorLastSeenAt).getTime() >
1404
+ this.getCollectorSilenceCutoff(now).getTime());
1405
+ }
1406
+ /*
1407
+ * True when the workload path wrote this row within
1408
+ * WORKLOAD_REFRESH_MINUTES: both liveness stamps it writes are that fresh.
1409
+ */
1410
+ isWorkloadRowFresh(row, now) {
1411
+ const freshAfter = OneUptimeDate.addRemoveMinutes(now, -WORKLOAD_REFRESH_MINUTES).getTime();
1412
+ for (const stamp of [row.lastSeenAt, row.workloadLastSeenAt]) {
1413
+ const time = stamp ? new Date(stamp).getTime() : NaN;
1414
+ if (!Number.isFinite(time) || time <= freshAfter) {
1415
+ return false;
1416
+ }
1417
+ }
1418
+ return true;
1419
+ }
1420
+ /*
1421
+ * True for the row of a Kubernetes / Docker / Podman workload that
1422
+ * discovery has not seen for WORKLOAD_GONE_MINUTES: its endpoints may move
1423
+ * to the workload that replaced it, and a trace still naming one of them
1424
+ * does not bring the row back from the archive.
1425
+ */
1426
+ isWorkloadGone(row, now = OneUptimeDate.getCurrentDate()) {
1427
+ if (!row.workloadIdentifier) {
1428
+ return false;
1429
+ }
1430
+ if (!row.workloadLastSeenAt) {
1431
+ return true;
1432
+ }
1433
+ return (new Date(row.workloadLastSeenAt).getTime() <
1434
+ OneUptimeDate.addRemoveMinutes(now, -WORKLOAD_GONE_MINUTES).getTime());
1435
+ }
1436
+ /*
1437
+ * True for a row discovery archived after its Kubernetes / Docker /
1438
+ * Podman workload went away (every such row: the sweep only archives a
1439
+ * row unseen for days, far past WORKLOAD_GONE_MINUTES). Only its workload
1440
+ * or a collector restores it - application traces still naming one of
1441
+ * its endpoints do not.
1442
+ */
1443
+ isRetiredWorkloadRow(row) {
1444
+ return Boolean(row.isArchived && row.autoArchivedAt && this.isWorkloadGone(row));
1445
+ }
1446
+ /*
1447
+ * isRetiredWorkloadRow, for a report from `source`: application traces
1448
+ * cannot bring the row back, so it is not theirs to sight either.
1449
+ */
1450
+ isRetiredFor(row, source) {
1451
+ return (source === DatabaseServerDiscoverySource.ClientSpans &&
1452
+ this.isRetiredWorkloadRow(row));
1453
+ }
1454
+ /*
1455
+ * What a report of an endpoint tells the row that owns it: the engine it
1456
+ * names is weighed as evidence (applyDatabaseSystemEvidence), and a row
1457
+ * discovery archived is restored - unless its workload is gone, which
1458
+ * only the workload path or a collector heartbeat brings back.
1459
+ */
1460
+ async recordOwnerSighting(ownerRow, data) {
1461
+ const evidence = getDatabaseSystemEvidenceForSource(data.discoverySource);
1462
+ if (evidence) {
1463
+ await this.applyDatabaseSystemEvidence(ownerRow, {
1464
+ system: data.dbSystem,
1465
+ evidence: evidence,
1466
+ });
1467
+ }
1468
+ if (!this.isWorkloadGone(ownerRow)) {
1469
+ await this.restoreIfAutoArchived(ownerRow);
1470
+ }
1471
+ }
1472
+ async findByWorkloadIdentifier(projectId, workloadIdentifier) {
1473
+ return await this.findOneBy({
1474
+ query: {
1475
+ projectId: projectId,
1476
+ workloadIdentifier: workloadIdentifier,
1477
+ },
1478
+ select: WORKLOAD_SELECT,
1479
+ props: {
1480
+ isRoot: true,
1481
+ },
1482
+ });
1483
+ }
1484
+ // endpoint -> the stored endpoint row, for the endpoints that have one.
1485
+ async findEndpointOwners(projectId, endpoints) {
1486
+ const owners = new Map();
1487
+ if (endpoints.length === 0) {
1488
+ return owners;
1489
+ }
1490
+ const rows = await DatabaseServerEndpointService.findBy({
1491
+ query: {
1492
+ projectId: projectId,
1493
+ endpoint: QueryHelper.any(endpoints),
1494
+ },
1495
+ select: {
1496
+ _id: true,
1497
+ endpoint: true,
1498
+ databaseServerId: true,
1499
+ source: true,
1500
+ isPrimary: true,
1501
+ lastMatchedAt: true,
1502
+ },
1503
+ limit: LIMIT_MAX,
1504
+ skip: 0,
1505
+ props: {
1506
+ isRoot: true,
1507
+ },
1508
+ });
1509
+ for (const endpointRow of rows) {
1510
+ if (endpointRow.endpoint && endpointRow.databaseServerId) {
1511
+ owners.set(endpointRow.endpoint, {
1512
+ endpointId: endpointRow._id
1513
+ ? new ObjectID(endpointRow._id.toString())
1514
+ : null,
1515
+ databaseServerId: endpointRow.databaseServerId.toString(),
1516
+ source: endpointRow.source || "auto",
1517
+ isPrimary: Boolean(endpointRow.isPrimary),
1518
+ lastMatchedAt: endpointRow.lastMatchedAt
1519
+ ? new Date(endpointRow.lastMatchedAt)
1520
+ : null,
1521
+ });
1522
+ }
1523
+ }
1524
+ return owners;
1525
+ }
1526
+ /*
1527
+ * The row of the SAME workload stored under another identifier of its
1528
+ * engine family. Identifiers are family-keyed
1529
+ * (buildWorkloadDatabaseServerIdentifier: a Valkey workload keys as
1530
+ * `redis|...`), so a workload re-classified within its family (Redis to
1531
+ * Valkey, MySQL to MariaDB) keeps its identifier and never gets here. What
1532
+ * does get here is a row written under the old ENGINE-keyed identifiers
1533
+ * (`valkey|...`, `mariadb|...`), whatever engine the workload reports
1534
+ * now - the fork itself included. Without this adoption it would become a
1535
+ * second database: a new row with none of the endpoints, history, labels
1536
+ * or incident links of the old one. Instead the old row is re-keyed to the
1537
+ * family key (a compare-and-set, like adoption) and the engine evidence is
1538
+ * weighed as usual. Two candidates is ambiguous and adopts neither.
1539
+ */
1540
+ async adoptSameWorkloadOfEngineFamily(data) {
1541
+ const separator = data.workloadIdentifier.indexOf("|");
1542
+ if (separator <= 0) {
1543
+ return null;
1544
+ }
1545
+ const workload = data.workloadIdentifier.substring(separator + 1);
1546
+ // Every engine of the family - the reported one too - but the key itself.
1547
+ const candidates = getDatabaseSystemsOfFamily(data.dbSystem)
1548
+ .map((system) => {
1549
+ return truncateLongText(`${system}|${workload}`.toLowerCase());
1550
+ })
1551
+ .filter((candidate) => {
1552
+ return candidate !== data.workloadIdentifier;
1553
+ });
1554
+ if (candidates.length === 0) {
1555
+ return null;
1556
+ }
1557
+ const rows = await this.findBy({
1558
+ query: {
1559
+ projectId: data.projectId,
1560
+ workloadIdentifier: QueryHelper.any(candidates),
1561
+ },
1562
+ select: {
1563
+ _id: true,
1564
+ workloadIdentifier: true,
1565
+ },
1566
+ limit: 2,
1567
+ skip: 0,
1568
+ props: {
1569
+ isRoot: true,
1570
+ },
1571
+ });
1572
+ if (rows.length !== 1 || !rows[0].id || !rows[0].workloadIdentifier) {
1573
+ return null;
1574
+ }
1575
+ const previousId = rows[0].id;
1576
+ const previousWorkload = rows[0].workloadIdentifier;
1577
+ try {
1578
+ await this.updateColumnsByIdWithoutHooks({
1579
+ id: previousId,
1580
+ data: {
1581
+ workloadIdentifier: data.workloadIdentifier,
1582
+ },
1583
+ expectedData: {
1584
+ workloadIdentifier: previousWorkload,
1585
+ },
1586
+ });
1587
+ logger.info(`DatabaseServerService: workload ${previousWorkload} is now ${data.workloadIdentifier} - the same workload under an engine of the same family; database ${previousId.toString()} kept.`, { projectId: data.projectId.toString() });
1588
+ }
1589
+ catch (error) {
1590
+ logger.debug(`DatabaseServerService: re-keying database ${previousId.toString()} to workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`);
1591
+ }
1592
+ // Whatever happened, the workload index now holds the truth.
1593
+ return await this.findByWorkloadIdentifier(data.projectId, data.workloadIdentifier);
1594
+ }
1595
+ async adoptEndpointDatabaseServer(data) {
1596
+ var _a;
1597
+ /*
1598
+ * Only GLOBAL-scope aliases vote. An unqualified cluster-local name or a
1599
+ * bare service name means something different in every cluster and
1600
+ * namespace, so it is never evidence that two things are the same
1601
+ * database.
1602
+ */
1603
+ const ownerIds = new Set();
1604
+ for (const alias of data.aliases) {
1605
+ if (alias.scope !== "global") {
1606
+ continue;
1607
+ }
1608
+ const ownerId = (_a = data.ownersByEndpoint.get(alias.endpoint)) === null || _a === void 0 ? void 0 : _a.databaseServerId;
1609
+ if (ownerId) {
1610
+ ownerIds.add(ownerId);
1611
+ }
1612
+ }
1613
+ if (ownerIds.size === 0) {
1614
+ return null;
1615
+ }
1616
+ if (ownerIds.size > 1) {
1617
+ logger.info(`DatabaseServerService: not adopting an existing database for workload ${data.workloadIdentifier} - its endpoints belong to ${ownerIds.size} different databases.`, { projectId: data.projectId.toString() });
1618
+ return null;
1619
+ }
1620
+ const ownerId = Array.from(ownerIds)[0];
1621
+ const candidate = await this.findByIdInProject(data.projectId, new ObjectID(ownerId));
1622
+ if (!candidate || !candidate.id) {
1623
+ return null;
1624
+ }
1625
+ if (candidate.workloadIdentifier) {
1626
+ // Already another workload's database: two workloads, two rows.
1627
+ logger.debug(`DatabaseServerService: not adopting database ${ownerId} for workload ${data.workloadIdentifier} - it already belongs to workload ${candidate.workloadIdentifier}.`, { projectId: data.projectId.toString() });
1628
+ return null;
1629
+ }
1630
+ try {
1631
+ /*
1632
+ * Compare-and-set on the NULL workloadIdentifier: two workloads racing
1633
+ * to adopt the same row cannot both win.
1634
+ */
1635
+ await this.updateColumnsByIdWithoutHooks({
1636
+ id: candidate.id,
1637
+ data: {
1638
+ workloadIdentifier: data.workloadIdentifier,
1639
+ },
1640
+ expectedData: {
1641
+ workloadIdentifier: null,
1642
+ },
1643
+ });
1644
+ }
1645
+ catch (error) {
1646
+ // Most likely a racing run created this workload's row first.
1647
+ logger.debug(`DatabaseServerService: adopting database ${ownerId} for workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`);
1648
+ }
1649
+ /*
1650
+ * Whatever happened, the truth is now in the workload index: our
1651
+ * adopted row, a row a racing run created, or nothing (another workload
1652
+ * adopted the candidate first) - in which case the caller creates.
1653
+ */
1654
+ return await this.findByWorkloadIdentifier(data.projectId, data.workloadIdentifier);
1655
+ }
1656
+ async createWorkloadDatabaseServer(data) {
1657
+ const input = data.data;
1658
+ const now = OneUptimeDate.getCurrentDate();
1659
+ const newRow = new Model();
1660
+ newRow.projectId = input.projectId;
1661
+ newRow.name = truncateShortText((typeof input.displayName === "string" ? input.displayName.trim() : "") ||
1662
+ buildDatabaseServerDisplayName({
1663
+ system: data.dbSystem,
1664
+ namespace: input.kubernetesNamespace,
1665
+ workloadName: input.workloadName,
1666
+ }));
1667
+ newRow.dbSystem = data.dbSystem;
1668
+ // A workload row's identity IS its workload.
1669
+ newRow.databaseIdentifier = data.workloadIdentifier;
1670
+ newRow.workloadIdentifier = data.workloadIdentifier;
1671
+ newRow.discoverySource = input.discoverySource;
1672
+ newRow.instanceCount = toInstanceCount(input.instanceCount);
1673
+ newRow.lastSeenAt = now;
1674
+ newRow.workloadLastSeenAt = now;
1675
+ newRow.memberEntityKeys = mergeDatabaseServerMemberKeys(undefined, Array.isArray(input.memberKeysSeenNow) ? input.memberKeysSeenNow : [], now);
1676
+ const workloadKind = cleanShortText(input.workloadKind);
1677
+ if (workloadKind) {
1678
+ newRow.workloadKind = workloadKind;
1679
+ }
1680
+ const workloadName = cleanLongText(input.workloadName);
1681
+ if (workloadName) {
1682
+ newRow.workloadName = workloadName;
1683
+ }
1684
+ const kubernetesNamespace = cleanShortText(input.kubernetesNamespace);
1685
+ if (kubernetesNamespace) {
1686
+ newRow.kubernetesNamespace = kubernetesNamespace;
1687
+ }
1688
+ if (input.kubernetesClusterId) {
1689
+ newRow.kubernetesClusterId = input.kubernetesClusterId;
1690
+ }
1691
+ if (input.dockerHostId) {
1692
+ newRow.dockerHostId = input.dockerHostId;
1693
+ }
1694
+ if (input.podmanHostId) {
1695
+ newRow.podmanHostId = input.podmanHostId;
1696
+ }
1697
+ const dbVersion = cleanShortText(input.dbVersion);
1698
+ if (dbVersion) {
1699
+ newRow.dbVersion = dbVersion;
1700
+ }
1701
+ // The address the list shows: the first (primary) alias, when there is one.
1702
+ if (data.primaryAlias) {
1703
+ newRow.serverAddress = truncateLongText(data.primaryAlias.parsed.host);
1704
+ if (data.primaryAlias.parsed.port !== null) {
1705
+ newRow.serverPort = data.primaryAlias.parsed.port;
1706
+ }
1707
+ }
1708
+ try {
1709
+ const row = await this.create({
1710
+ data: newRow,
1711
+ props: {
1712
+ isRoot: true,
1713
+ },
1714
+ });
1715
+ return { row: row, createdHere: true };
1716
+ }
1717
+ catch (error) {
1718
+ // A racing run created this workload's row first.
1719
+ const existing = await this.findByWorkloadIdentifier(input.projectId, data.workloadIdentifier);
1720
+ if (!existing) {
1721
+ throw error;
1722
+ }
1723
+ return { row: existing, createdHere: false };
1724
+ }
1725
+ }
1726
+ /*
1727
+ * Reconcile the workload's endpoints with the aliases this run produced:
1728
+ * 1. claim the unowned ones (source "workload");
1729
+ * 2. take over the ones a DISCOVERED endpoint of another row holds when
1730
+ * that row is a workload that is gone (a Deployment moved to a
1731
+ * StatefulSet, an engine reclassified) or an untouched duplicate that
1732
+ * application traces created (the create race) - never a person's
1733
+ * endpoint, never a live workload's, collector's or manual row's;
1734
+ * 3. re-stamp lastMatchedAt on the ones already ours (at most hourly);
1735
+ * 4. release this row's "workload" aliases that the workload has not
1736
+ * produced for WORKLOAD_ALIAS_RELEASE_MINUTES - an unqualified alias
1737
+ * once the project gained a second cluster, a Service that was
1738
+ * renamed. Never its primary endpoint, never an "auto" or "user" one.
1739
+ * Each step is best-effort: one failing never stops the next.
1740
+ */
1741
+ async claimWorkloadAliases(data) {
1742
+ const ownId = data.databaseServerId.toString();
1743
+ const refreshBefore = OneUptimeDate.addRemoveSeconds(data.now, -ENDPOINT_MATCH_REFRESH_SECONDS);
1744
+ const heldByOthers = [];
1745
+ const racedByOthers = [];
1746
+ const ownedToRefresh = [];
1747
+ let primaryClaimed = false;
1748
+ for (const alias of data.aliases) {
1749
+ const stored = data.ownersByEndpoint.get(alias.endpoint);
1750
+ if (stored && stored.databaseServerId === ownId) {
1751
+ if (!stored.lastMatchedAt ||
1752
+ stored.lastMatchedAt.getTime() < refreshBefore.getTime()) {
1753
+ ownedToRefresh.push(alias.endpoint);
1754
+ }
1755
+ continue;
1756
+ }
1757
+ if (stored) {
1758
+ heldByOthers.push({ alias: alias, stored: stored });
1759
+ continue;
1760
+ }
1761
+ try {
1762
+ const isPrimary = alias.endpoint === data.primaryEndpoint;
1763
+ const result = await DatabaseServerEndpointService.claimEndpoint({
1764
+ projectId: data.projectId,
1765
+ databaseServerId: data.databaseServerId,
1766
+ endpoint: alias.endpoint,
1767
+ isPrimary: isPrimary,
1768
+ source: "workload",
1769
+ });
1770
+ if (result === "owned-by-other") {
1771
+ racedByOthers.push(alias.endpoint);
1772
+ }
1773
+ else if (result === "claimed" && isPrimary) {
1774
+ primaryClaimed = true;
1775
+ }
1776
+ }
1777
+ catch (error) {
1778
+ logger.warn(`DatabaseServerService: claiming endpoint ${alias.endpoint} for workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`, { projectId: data.projectId.toString() });
1779
+ }
1780
+ }
1781
+ let notTaken = heldByOthers.map((held) => {
1782
+ return held.alias.endpoint;
1783
+ });
1784
+ try {
1785
+ notTaken = await this.takeOverWorkloadAliases({
1786
+ projectId: data.projectId,
1787
+ databaseServerId: data.databaseServerId,
1788
+ workloadIdentifier: data.workloadIdentifier,
1789
+ heldByOthers: heldByOthers,
1790
+ /*
1791
+ * A row created in this run whose first alias could not be claimed
1792
+ * has no primary endpoint yet; any other row is asked (lazily).
1793
+ */
1794
+ needsPrimary: data.primaryEndpoint ? !primaryClaimed : undefined,
1795
+ now: data.now,
1796
+ });
1797
+ }
1798
+ catch (error) {
1799
+ logger.warn(`DatabaseServerService: taking over endpoints for workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`, { projectId: data.projectId.toString() });
1800
+ }
1801
+ if (ownedToRefresh.length > 0) {
1802
+ try {
1803
+ await DatabaseServerEndpointService.refreshMatchedEndpoints({
1804
+ projectId: data.projectId,
1805
+ databaseServerId: data.databaseServerId,
1806
+ endpoints: ownedToRefresh,
1807
+ now: data.now,
1808
+ staleBefore: refreshBefore,
1809
+ });
1810
+ }
1811
+ catch (error) {
1812
+ logger.warn(`DatabaseServerService: refreshing endpoints of workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`, { projectId: data.projectId.toString() });
1813
+ }
1814
+ }
1815
+ // Only Kubernetes workloads produce aliases, so only they can drop one.
1816
+ if (data.discoverySource === DatabaseServerDiscoverySource.Kubernetes) {
1817
+ try {
1818
+ const released = await DatabaseServerEndpointService.releaseUnproducedWorkloadEndpoints({
1819
+ projectId: data.projectId,
1820
+ databaseServerId: data.databaseServerId,
1821
+ keepEndpoints: data.aliases.map((alias) => {
1822
+ return alias.endpoint;
1823
+ }),
1824
+ staleBefore: OneUptimeDate.addRemoveMinutes(data.now, -WORKLOAD_ALIAS_RELEASE_MINUTES),
1825
+ });
1826
+ if (released.length > 0) {
1827
+ logger.info(`DatabaseServerService: released ${released.length} endpoint(s) workload ${data.workloadIdentifier} no longer serves: ${released.join(", ")}`, { projectId: data.projectId.toString() });
1828
+ }
1829
+ }
1830
+ catch (error) {
1831
+ logger.warn(`DatabaseServerService: releasing endpoints of workload ${data.workloadIdentifier} failed: ${error instanceof Error ? error.message : String(error)}`, { projectId: data.projectId.toString() });
1832
+ }
1833
+ }
1834
+ const ownedByOthers = [...notTaken, ...racedByOthers];
1835
+ if (ownedByOthers.length > 0) {
1836
+ /*
1837
+ * Expected and stable run to run (another database already lists the
1838
+ * endpoint), so debug, not info: this runs every five minutes.
1839
+ */
1840
+ logger.debug(`DatabaseServerService: skipped ${ownedByOthers.length} endpoint(s) of workload ${data.workloadIdentifier} that belong to other databases: ${ownedByOthers.join(", ")}`, { projectId: data.projectId.toString() });
1841
+ }
1842
+ }
1843
+ /*
1844
+ * Step 2 of claimWorkloadAliases. Returns the aliases left with their
1845
+ * current owners.
1846
+ */
1847
+ async takeOverWorkloadAliases(data) {
1848
+ const notTaken = [];
1849
+ const candidates = [];
1850
+ for (const held of data.heldByOthers) {
1851
+ if (held.stored.source === "user" || !held.stored.endpointId) {
1852
+ notTaken.push(held.alias.endpoint);
1853
+ }
1854
+ else {
1855
+ candidates.push(held);
1856
+ }
1857
+ }
1858
+ if (candidates.length === 0) {
1859
+ return notTaken;
1860
+ }
1861
+ const ownerIds = Array.from(new Set(candidates.map((held) => {
1862
+ return held.stored.databaseServerId;
1863
+ })));
1864
+ const owners = await this.findBy({
1865
+ query: {
1866
+ projectId: data.projectId,
1867
+ _id: QueryHelper.any(ownerIds),
1868
+ },
1869
+ select: {
1870
+ _id: true,
1871
+ workloadIdentifier: true,
1872
+ workloadLastSeenAt: true,
1873
+ discoverySource: true,
1874
+ },
1875
+ limit: ownerIds.length,
1876
+ skip: 0,
1877
+ props: {
1878
+ isRoot: true,
1879
+ },
1880
+ });
1881
+ const ownersById = new Map();
1882
+ for (const owner of owners) {
1883
+ if (owner.id) {
1884
+ ownersById.set(owner.id.toString(), owner);
1885
+ }
1886
+ }
1887
+ const traceDuplicateIds = await this.findUntouchedTraceRows(data.projectId, data.now, owners
1888
+ .filter((owner) => {
1889
+ return (Boolean(owner.id) &&
1890
+ !owner.workloadIdentifier &&
1891
+ owner.discoverySource === DatabaseServerDiscoverySource.ClientSpans);
1892
+ })
1893
+ .map((owner) => {
1894
+ return owner.id.toString();
1895
+ }));
1896
+ let needsPrimary = data.needsPrimary;
1897
+ for (const held of candidates) {
1898
+ const owner = ownersById.get(held.stored.databaseServerId);
1899
+ const goneWorkload = Boolean(owner &&
1900
+ owner.workloadIdentifier &&
1901
+ owner.workloadIdentifier !== data.workloadIdentifier &&
1902
+ this.isWorkloadGone(owner, data.now));
1903
+ const traceDuplicate = traceDuplicateIds.has(held.stored.databaseServerId.toLowerCase());
1904
+ if (!goneWorkload && !traceDuplicate) {
1905
+ notTaken.push(held.alias.endpoint);
1906
+ continue;
1907
+ }
1908
+ if (needsPrimary === undefined) {
1909
+ needsPrimary = !(await DatabaseServerEndpointService.hasPrimaryEndpoint({
1910
+ projectId: data.projectId,
1911
+ databaseServerId: data.databaseServerId,
1912
+ }));
1913
+ }
1914
+ const moved = await DatabaseServerEndpointService.transferEndpoint({
1915
+ projectId: data.projectId,
1916
+ endpointId: held.stored.endpointId,
1917
+ fromDatabaseServerId: new ObjectID(held.stored.databaseServerId),
1918
+ toDatabaseServerId: data.databaseServerId,
1919
+ isPrimary: needsPrimary,
1920
+ now: data.now,
1921
+ });
1922
+ if (!moved) {
1923
+ notTaken.push(held.alias.endpoint);
1924
+ continue;
1925
+ }
1926
+ needsPrimary = false;
1927
+ logger.info(`DatabaseServerService: moved endpoint ${held.alias.endpoint} from database ${held.stored.databaseServerId} (${goneWorkload
1928
+ ? "its workload is gone"
1929
+ : "an untouched duplicate application traces created"}) to workload ${data.workloadIdentifier}.`, { projectId: data.projectId.toString() });
1930
+ }
1931
+ return notTaken;
1932
+ }
1933
+ /*
1934
+ * Of `databaseServerIds`, the trace-discovered rows nobody invested in:
1935
+ * duplicates of a workload whose endpoints the workload may take over.
1936
+ * Taking them empties the row, which the sweep then archives, so beyond
1937
+ * UNTOUCHED_DATABASE_SERVER_PREDICATE a person's Restore within the grace
1938
+ * the sweep gives it, a description and a name that is not the one
1939
+ * discovery generated (hasDiscoveryGeneratedName) all count as investment.
1940
+ */
1941
+ async findUntouchedTraceRows(projectId, now, databaseServerIds) {
1942
+ if (databaseServerIds.length === 0) {
1943
+ return new Set();
1944
+ }
1945
+ const restoreGraceCutoff = OneUptimeDate.addRemoveDays(now, -Math.max(MANUAL_RESTORE_GRACE_DAYS, this.getAutoArchiveDays()));
1946
+ const rows = await this.getRepository().manager.query(`SELECT ds."_id" AS "_id", ds."name" AS "name", ds."dbSystem" AS "dbSystem",
1947
+ ds."serverAddress" AS "serverAddress", ds."serverPort" AS "serverPort"
1948
+ FROM "DatabaseServer" ds
1949
+ WHERE ds."projectId" = $1
1950
+ AND ds."_id" = ANY($2::uuid[])
1951
+ AND ds."deletedAt" IS NULL
1952
+ AND ds."workloadIdentifier" IS NULL
1953
+ AND ds."discoverySource" = $3
1954
+ AND (ds."manuallyRestoredAt" IS NULL OR ds."manuallyRestoredAt" < $4)
1955
+ AND COALESCE(BTRIM(ds."description"), '') = ''
1956
+ AND ${UNTOUCHED_DATABASE_SERVER_PREDICATE}`, [
1957
+ projectId.toString(),
1958
+ databaseServerIds,
1959
+ DatabaseServerDiscoverySource.ClientSpans,
1960
+ restoreGraceCutoff,
1961
+ ]);
1962
+ const ids = new Set();
1963
+ for (const row of Array.isArray(rows) ? rows : []) {
1964
+ const found = (row || {});
1965
+ if (!found._id ||
1966
+ !hasDiscoveryGeneratedName({
1967
+ name: typeof found.name === "string" ? found.name : undefined,
1968
+ dbSystem: typeof found.dbSystem === "string" ? found.dbSystem : undefined,
1969
+ serverAddress: typeof found.serverAddress === "string"
1970
+ ? found.serverAddress
1971
+ : undefined,
1972
+ serverPort: found.serverPort === null || found.serverPort === undefined
1973
+ ? undefined
1974
+ : Number(found.serverPort),
1975
+ })) {
1976
+ continue;
1977
+ }
1978
+ ids.add(String(found._id).toLowerCase());
1979
+ }
1980
+ return ids;
1981
+ }
1982
+ async createEndpointDatabaseServer(data) {
1983
+ var _a, _b;
1984
+ const now = OneUptimeDate.getCurrentDate();
1985
+ const databaseIdentifier = truncateLongText(buildDatabaseServerIdentifier(data.dbSystem, data.endpoint));
1986
+ const isCollector = data.discoverySource === DatabaseServerDiscoverySource.Collector;
1987
+ const newRow = new Model();
1988
+ newRow.projectId = data.projectId;
1989
+ newRow.name = truncateShortText((typeof data.displayName === "string" ? data.displayName.trim() : "") ||
1990
+ buildDatabaseServerDisplayName({
1991
+ system: data.dbSystem,
1992
+ endpoint: data.endpoint,
1993
+ }));
1994
+ newRow.dbSystem = data.dbSystem;
1995
+ newRow.serverAddress = truncateLongText(data.endpoint.host);
1996
+ if (data.endpoint.port !== null) {
1997
+ newRow.serverPort = data.endpoint.port;
1998
+ }
1999
+ newRow.databaseIdentifier = databaseIdentifier;
2000
+ newRow.discoverySource = data.discoverySource;
2001
+ newRow.lastSeenAt = now;
2002
+ /*
2003
+ * A collector-created row is connected from its first moment; every
2004
+ * other source leaves otelCollectorStatus empty - no collector has
2005
+ * reported, so none has "disconnected" either (the column has no
2006
+ * default).
2007
+ *
2008
+ * It also carries the agent and engine versions the creating batch
2009
+ * reported, so it never depends on the heartbeat right after it: that
2010
+ * write is FOR UPDATE SKIP LOCKED, the create's own Created feed item
2011
+ * (its foreign key share-locks the row) and rule runs are still in
2012
+ * flight, and a skipped heartbeat waits out the whole ingest maintenance
2013
+ * fence - forever, if the agent stops before it expires.
2014
+ */
2015
+ if (isCollector) {
2016
+ newRow.otelCollectorStatus = "connected";
2017
+ newRow.collectorLastSeenAt = now;
2018
+ const agentVersion = cleanShortText((_a = data.collector) === null || _a === void 0 ? void 0 : _a.agentVersion);
2019
+ if (agentVersion) {
2020
+ newRow.agentVersion = agentVersion;
2021
+ }
2022
+ const dbVersion = cleanShortText((_b = data.collector) === null || _b === void 0 ? void 0 : _b.dbVersion);
2023
+ if (dbVersion) {
2024
+ newRow.dbVersion = dbVersion;
2025
+ }
2026
+ }
2027
+ let row;
2028
+ let createdHere = false;
2029
+ try {
2030
+ /*
2031
+ * Without hooks: the Created feed item and the rule engines run only
2032
+ * once this row has actually won its endpoint (below), so an orphan
2033
+ * that loses the claim leaves no trace behind.
2034
+ */
2035
+ row = await this.create({
2036
+ data: newRow,
2037
+ props: {
2038
+ isRoot: true,
2039
+ ignoreHooks: true,
2040
+ },
2041
+ });
2042
+ createdHere = true;
2043
+ }
2044
+ catch (error) {
2045
+ // Same engine family + endpoint created by a racing writer.
2046
+ const existing = await this.findOneBy({
2047
+ query: {
2048
+ projectId: data.projectId,
2049
+ databaseIdentifier: databaseIdentifier,
2050
+ },
2051
+ select: DISCOVERY_SELECT,
2052
+ props: {
2053
+ isRoot: true,
2054
+ },
2055
+ });
2056
+ if (!existing) {
2057
+ throw error;
2058
+ }
2059
+ // Neither claimed for nor restored by a trace (isRetiredWorkloadRow).
2060
+ if (this.isRetiredFor(existing, data.discoverySource)) {
2061
+ return null;
2062
+ }
2063
+ row = existing;
2064
+ }
2065
+ if (!row.id) {
2066
+ return null;
2067
+ }
2068
+ let claim;
2069
+ try {
2070
+ claim = await DatabaseServerEndpointService.claimEndpoint({
2071
+ projectId: data.projectId,
2072
+ databaseServerId: row.id,
2073
+ endpoint: data.formattedEndpoint,
2074
+ isPrimary: true,
2075
+ source: "auto",
2076
+ });
2077
+ }
2078
+ catch (error) {
2079
+ if (createdHere) {
2080
+ await this.deleteOrphanedDatabaseServer(data.projectId, row.id);
2081
+ }
2082
+ throw error;
2083
+ }
2084
+ if (claim === "owned-by-other") {
2085
+ if (createdHere) {
2086
+ await this.deleteOrphanedDatabaseServer(data.projectId, row.id);
2087
+ }
2088
+ const owner = await DatabaseServerEndpointService.findOwnerByEndpoint(data.projectId, data.formattedEndpoint);
2089
+ if (!owner) {
2090
+ return null;
2091
+ }
2092
+ const ownerRow = await this.findByIdInProject(data.projectId, owner.databaseServerId);
2093
+ // The same sighting findOrCreateByEndpoint gives an owner it finds first.
2094
+ if (ownerRow && this.isRetiredFor(ownerRow, data.discoverySource)) {
2095
+ return null;
2096
+ }
2097
+ if (ownerRow) {
2098
+ await this.recordOwnerSighting(ownerRow, data);
2099
+ }
2100
+ return ownerRow;
2101
+ }
2102
+ if (createdHere) {
2103
+ this.noteAutoCreated(data.projectId);
2104
+ this.runCreatedSideEffects(row, undefined, {
2105
+ endpoint: data.formattedEndpoint,
2106
+ collector: isCollector ? data.collector : undefined,
2107
+ });
2108
+ }
2109
+ else {
2110
+ await this.restoreIfAutoArchived(row);
2111
+ }
2112
+ return row;
2113
+ }
2114
+ /*
2115
+ * The collector path's budget gate (see findOrCreateByEndpoint): false
2116
+ * when the project has reached its auto-create budget - warned about once
2117
+ * per project per ten minutes, since ingest would otherwise repeat it for
2118
+ * every batch - or when the count cannot be read (fail closed, like the
2119
+ * workers' AutoCreateBudget).
2120
+ */
2121
+ async allowsCollectorAutoCreate(projectId, formattedEndpoint) {
2122
+ let underBudget = false;
2123
+ try {
2124
+ underBudget = await this.isUnderAutoCreateBudgetCached(projectId);
2125
+ }
2126
+ catch (error) {
2127
+ logger.error(`DatabaseServerService: auto-create budget check failed for project ${projectId.toString()}; not creating a database for collector endpoint ${formattedEndpoint}: ${error instanceof Error ? error.message : String(error)}`, { projectId: projectId.toString() });
2128
+ return false;
2129
+ }
2130
+ if (!underBudget) {
2131
+ const key = projectId.toString();
2132
+ if (!this.autoCreateBudgetWarningMemo.get(key)) {
2133
+ this.autoCreateBudgetWarningMemo.set(key, true);
2134
+ logger.warn(`DatabaseServerService: project ${key} reached its auto-create budget (DATABASE_SERVER_AUTO_CREATE_BUDGET=${this.getAutoCreateBudget()}); not creating a database for collector endpoint ${formattedEndpoint} (and further new collector endpoints for the next ten minutes). Raise the budget, or add the database manually and link its agent with DATABASE_SERVER_ID.`, { projectId: key });
2135
+ }
2136
+ }
2137
+ return underBudget;
2138
+ }
2139
+ // A row was auto-created: count it against the cached budget straight away.
2140
+ noteAutoCreated(projectId) {
2141
+ const key = projectId.toString();
2142
+ const cached = this.autoCreateCountMemo.get(key);
2143
+ if (!cached) {
2144
+ return;
2145
+ }
2146
+ const remainingMs = cached.readAtMs + AUTO_CREATE_BUDGET_CACHE_MS - Date.now();
2147
+ if (remainingMs <= 0) {
2148
+ this.autoCreateCountMemo.delete(key);
2149
+ return;
2150
+ }
2151
+ this.autoCreateCountMemo.set(key, { count: cached.count + 1, readAtMs: cached.readAtMs }, remainingMs);
2152
+ }
2153
+ async deleteOrphanedDatabaseServer(projectId, databaseServerId) {
2154
+ try {
2155
+ await this.deleteBy({
2156
+ query: {
2157
+ _id: databaseServerId.toString(),
2158
+ projectId: projectId,
2159
+ },
2160
+ limit: 1,
2161
+ skip: 0,
2162
+ props: {
2163
+ isRoot: true,
2164
+ ignoreHooks: true,
2165
+ },
2166
+ });
2167
+ }
2168
+ catch (error) {
2169
+ logger.warn(`DatabaseServerService: could not delete orphaned database ${databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`, { projectId: projectId.toString() });
2170
+ }
2171
+ }
2172
+ /*
2173
+ * A manual create inserts the row first and claims its endpoint second
2174
+ * (the claim needs the row's id). onBeforeCreate already refused an owned
2175
+ * endpoint, so losing here means a concurrent writer claimed it in
2176
+ * between: the new row is removed and the person gets the same message
2177
+ * they would have got a moment later.
2178
+ */
2179
+ async claimManualPrimaryEndpoint(data) {
2180
+ let claim;
2181
+ try {
2182
+ claim = await DatabaseServerEndpointService.claimEndpoint({
2183
+ projectId: data.projectId,
2184
+ databaseServerId: data.databaseServerId,
2185
+ endpoint: data.endpoint,
2186
+ isPrimary: true,
2187
+ source: "user",
2188
+ });
2189
+ }
2190
+ catch (error) {
2191
+ await this.deleteOrphanedDatabaseServer(data.projectId, data.databaseServerId);
2192
+ throw error;
2193
+ }
2194
+ if (claim !== "owned-by-other") {
2195
+ return;
2196
+ }
2197
+ await this.deleteOrphanedDatabaseServer(data.projectId, data.databaseServerId);
2198
+ const owner = await DatabaseServerEndpointService.findOwnerByEndpoint(data.projectId, data.endpoint);
2199
+ throw new BadDataException(owner
2200
+ ? await getOwnedByOtherDatabaseMessage(data.endpoint, owner, data.props)
2201
+ : `${data.endpoint} already belongs to another database. An endpoint can belong to only one database in a project.`);
2202
+ }
2203
+ /*
2204
+ * A Kubernetes Service name typed without its cluster
2205
+ * (`pg.shop.svc.cluster.local`, no `@<cluster>`) in a project that has
2206
+ * Kubernetes clusters. Pods whose telemetry goes through the Kubernetes
2207
+ * agent report their cluster, so their calls are keyed
2208
+ * `<endpoint>@<cluster>`: a row holding only the unqualified name would
2209
+ * stay empty while discovery creates a second row for the qualified one.
2210
+ * The person is asked to name the cluster instead, from the cluster names
2211
+ * they may read - a cluster is its own resource, with its own read
2212
+ * permission and label scope, so a caller who may read none gets the
2213
+ * generic `<endpoint>@<cluster>` advice.
2214
+ *
2215
+ * Only Service names: a private IP or a private-zone name (`.internal`,
2216
+ * `.local`) is also what virtual machines and the Database Agent report,
2217
+ * unqualified, so it stays a valid manual endpoint. And only in a project
2218
+ * with clusters - without one, nothing could report the qualified form.
2219
+ * If the clusters cannot be read the create goes ahead: the advice is not
2220
+ * worth failing the create for.
2221
+ */
2222
+ async refuseUnqualifiedKubernetesServiceName(data) {
2223
+ const endpoint = data.manual.endpoint;
2224
+ if (!endpoint ||
2225
+ !data.manual.clusterQualifierHint ||
2226
+ !isKubernetesServiceDnsHost(endpoint.host)) {
2227
+ return;
2228
+ }
2229
+ let hasClusters = false;
2230
+ try {
2231
+ hasClusters = await this.hasKubernetesClusters(data.projectId);
2232
+ }
2233
+ catch (error) {
2234
+ logger.warn(`DatabaseServerService: could not read the Kubernetes clusters of project ${data.projectId.toString()}; adding ${formatDatabaseEndpoint(endpoint)} unqualified: ${error instanceof Error ? error.message : String(error)}`, { projectId: data.projectId.toString() });
2235
+ return;
2236
+ }
2237
+ if (!hasClusters) {
2238
+ return;
2239
+ }
2240
+ const clusterNames = await this.findReadableKubernetesClusterNames(data.projectId, data.props);
2241
+ // The same value again, now with the cluster names the advice lists.
2242
+ const advice = clusterNames.length > 0
2243
+ ? parseManualDatabaseEndpoint(data.serverAddress, {
2244
+ system: data.dbSystem,
2245
+ port: data.serverPort,
2246
+ knownClusterNames: clusterNames,
2247
+ }).clusterQualifierHint
2248
+ : null;
2249
+ throw new BadDataException(`${advice || data.manual.clusterQualifierHint} To also match a Database Agent or applications that do not report their cluster, add ${formatDatabaseEndpoint(endpoint)} as an endpoint on the database's Endpoints tab once it is created.`);
2250
+ }
2251
+ /*
2252
+ * True when the project has a live Kubernetes cluster with a name
2253
+ * (k8s.cluster.name). Read as root: whether a cluster could report the
2254
+ * qualified form decides the refusal, whoever asks - but no name is read.
2255
+ */
2256
+ async hasKubernetesClusters(projectId) {
2257
+ const rows = await this.getRepository().manager.query(`SELECT 1 AS "found"
2258
+ FROM "KubernetesCluster" kc
2259
+ WHERE kc."projectId" = $1
2260
+ AND kc."deletedAt" IS NULL
2261
+ AND kc."clusterIdentifier" IS NOT NULL
2262
+ AND kc."clusterIdentifier" <> ''
2263
+ LIMIT 1`, [projectId.toString()]);
2264
+ return Array.isArray(rows) && rows.length > 0;
2265
+ }
2266
+ /*
2267
+ * A few of the project's cluster names, sorted - only those the caller may
2268
+ * read, through their own KubernetesCluster permissions (label scope
2269
+ * included). A caller refused the read gets none. Never throws.
2270
+ */
2271
+ async findReadableKubernetesClusterNames(projectId, props) {
2272
+ let clusters = [];
2273
+ try {
2274
+ clusters = await KubernetesClusterService.findBy({
2275
+ query: {
2276
+ projectId: projectId,
2277
+ },
2278
+ select: {
2279
+ clusterIdentifier: true,
2280
+ },
2281
+ sort: {
2282
+ clusterIdentifier: SortOrder.Ascending,
2283
+ },
2284
+ limit: MAX_LISTED_CLUSTER_NAMES,
2285
+ skip: 0,
2286
+ props: props,
2287
+ });
2288
+ }
2289
+ catch (error) {
2290
+ logger.debug(`DatabaseServerService: no Kubernetes cluster names readable for the manual create in project ${projectId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
2291
+ return [];
2292
+ }
2293
+ const names = [];
2294
+ for (const cluster of clusters) {
2295
+ const name = typeof cluster.clusterIdentifier === "string"
2296
+ ? cluster.clusterIdentifier.trim()
2297
+ : "";
2298
+ if (name && !names.includes(name)) {
2299
+ names.push(name);
2300
+ }
2301
+ }
2302
+ return names;
2303
+ }
2304
+ /*
2305
+ * Rules run once, on creation only - exact parity with the other resource
2306
+ * types. Label engine first: it syncs the in-memory labels so the owner
2307
+ * engine can match rule-added labels. Then the Created feed item. Both are
2308
+ * fire and forget: neither may fail the create it follows.
2309
+ */
2310
+ runCreatedSideEffects(createdItem, createdByUserId, context) {
2311
+ if (createdItem.projectId && createdItem.id) {
2312
+ Promise.resolve()
2313
+ .then(async () => {
2314
+ await DatabaseServerLabelRuleEngineService.applyRulesToDatabaseServer(createdItem);
2315
+ })
2316
+ .then(async () => {
2317
+ await DatabaseServerOwnerRuleEngineService.applyRulesToDatabaseServer(createdItem);
2318
+ })
2319
+ .catch((error) => {
2320
+ var _a, _b;
2321
+ logger.error(`Error applying database rules in DatabaseServerService.onCreateSuccess: ${error}`, {
2322
+ projectId: (_a = createdItem.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
2323
+ databaseServerId: (_b = createdItem.id) === null || _b === void 0 ? void 0 : _b.toString(),
2324
+ });
2325
+ });
2326
+ }
2327
+ this.writeDatabaseServerCreatedFeed(createdItem, createdByUserId, context).catch((error) => {
2328
+ logger.error(error);
2329
+ });
2330
+ }
2331
+ async writeDatabaseServerCreatedFeed(createdItem, createdByUserId, context) {
2332
+ const projectId = createdItem.projectId;
2333
+ const databaseServerId = createdItem.id || undefined;
2334
+ if (!projectId || !databaseServerId) {
2335
+ return;
2336
+ }
2337
+ const resourceMarkdownLink = await this.getDatabaseServerMarkdownLink(projectId, databaseServerId);
2338
+ const discoveredFrom = `**Discovered from**: ${getDatabaseServerDiscoverySourceLabel(createdItem.discoverySource)}`;
2339
+ let markdown;
2340
+ if (createdByUserId) {
2341
+ const created = await ResourceFeedUtil.getCreatedFeedMarkdown({
2342
+ resourceTypeName: "database",
2343
+ resourceMarkdownLink: resourceMarkdownLink,
2344
+ projectId: projectId,
2345
+ createdByUserId: createdByUserId,
2346
+ identifierName: "Database identifier",
2347
+ identifierValue: createdItem.databaseIdentifier,
2348
+ description: createdItem.description,
2349
+ });
2350
+ markdown = {
2351
+ feedInfoInMarkdown: created.feedInfoInMarkdown,
2352
+ moreInformationInMarkdown: `${created.moreInformationInMarkdown}\n\n${discoveredFrom}`,
2353
+ };
2354
+ }
2355
+ else {
2356
+ /*
2357
+ * Five discovery paths create rows on their own, and "the first time
2358
+ * telemetry for it arrived" is true of none of the workload ones: say
2359
+ * what found this database, where.
2360
+ */
2361
+ const origin = await this.describeCreationOrigin(createdItem, context);
2362
+ const details = [];
2363
+ if (createdItem.databaseIdentifier) {
2364
+ details.push(`**Database identifier**: \`${createdItem.databaseIdentifier}\``);
2365
+ }
2366
+ if (createdItem.description) {
2367
+ details.push(`**Description**: ${createdItem.description}`);
2368
+ }
2369
+ markdown = {
2370
+ feedInfoInMarkdown: `${origin.emoji} ${resourceMarkdownLink} ${origin.summary}.`,
2371
+ moreInformationInMarkdown: [
2372
+ origin.createdBy,
2373
+ `**How it was created**: ${origin.explanation}`,
2374
+ discoveredFrom,
2375
+ ...details,
2376
+ ].join("\n\n"),
2377
+ };
2378
+ }
2379
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
2380
+ databaseServerId: databaseServerId,
2381
+ projectId: projectId,
2382
+ databaseServerFeedEventType: DatabaseServerFeedEventType.DatabaseServerCreated,
2383
+ displayColor: Green500,
2384
+ feedInfoInMarkdown: markdown.feedInfoInMarkdown,
2385
+ moreInformationInMarkdown: markdown.moreInformationInMarkdown,
2386
+ userId: createdByUserId,
2387
+ });
2388
+ }
2389
+ /*
2390
+ * What the Created feed item says about a row no person added: which
2391
+ * discovery path found it, and where - the workload and its cluster or
2392
+ * host, the endpoint applications called, the agent or collector that
2393
+ * reported it. A name that cannot be read is left out, never guessed.
2394
+ */
2395
+ async describeCreationOrigin(row, context) {
2396
+ const automatic = "**Created by**: No user. OneUptime created this database on its own.";
2397
+ const endpoint = describeCreationEndpoint(row, context);
2398
+ switch (row.discoverySource) {
2399
+ case DatabaseServerDiscoverySource.Kubernetes: {
2400
+ const clusterName = await this.readParentName("kubernetes", row.kubernetesClusterId);
2401
+ const workload = describeKubernetesWorkload(row);
2402
+ return {
2403
+ emoji: "🤖",
2404
+ summary: `was created automatically: detected from ${workload}${clusterName ? ` on Kubernetes cluster ${clusterName}` : ""}`,
2405
+ createdBy: automatic,
2406
+ explanation: "The Kubernetes agent reports the pods of the cluster. OneUptime found a database engine running in this workload's pods and registered the workload as one database; the Kubernetes Services in front of it became its endpoints.",
2407
+ };
2408
+ }
2409
+ case DatabaseServerDiscoverySource.Docker:
2410
+ case DatabaseServerDiscoverySource.Podman: {
2411
+ const isDocker = row.discoverySource === DatabaseServerDiscoverySource.Docker;
2412
+ const platform = isDocker ? "Docker" : "Podman";
2413
+ const hostName = isDocker
2414
+ ? await this.readParentName("docker", row.dockerHostId)
2415
+ : await this.readParentName("podman", row.podmanHostId);
2416
+ const workload = describeContainerWorkload(row);
2417
+ const service = containerServiceKind(row);
2418
+ return {
2419
+ emoji: "🤖",
2420
+ summary: `was created automatically: detected from ${workload}${hostName
2421
+ ? ` on ${platform} host ${hostName}`
2422
+ : ` on a ${platform} host`}`,
2423
+ createdBy: automatic,
2424
+ explanation: service
2425
+ ? `The ${platform} agent reports the containers of the host. OneUptime found a database engine running in the containers of this ${service} and registered the service as one database, its containers as members.`
2426
+ : `The ${platform} agent reports the containers of the host. OneUptime found a database engine running in this container and registered it as a database.`,
2427
+ };
2428
+ }
2429
+ case DatabaseServerDiscoverySource.ClientSpans:
2430
+ return {
2431
+ emoji: "🤖",
2432
+ summary: `was created automatically: detected from application traces${endpoint ? ` calling ${endpoint}` : ""}`,
2433
+ createdBy: automatic,
2434
+ explanation: "Instrumented applications sent database CLIENT spans (OpenTelemetry) that call this server, often enough in a 15-minute window, so OneUptime registered the server they call.",
2435
+ };
2436
+ case DatabaseServerDiscoverySource.Collector: {
2437
+ const report = context === null || context === void 0 ? void 0 : context.collector;
2438
+ const agentVersion = cleanShortText(report === null || report === void 0 ? void 0 : report.agentVersion);
2439
+ const reporter = (report === null || report === void 0 ? void 0 : report.reportedByDatabaseAgent) === true
2440
+ ? `the Database Agent${agentVersion ? ` ${agentVersion}` : ""}`
2441
+ : "an OpenTelemetry Collector database receiver";
2442
+ return {
2443
+ emoji: "🤖",
2444
+ summary: `was created automatically: reported by ${reporter}${endpoint ? `, which sent engine metrics for ${endpoint}` : ""}`,
2445
+ createdBy: automatic,
2446
+ explanation: `${(report === null || report === void 0 ? void 0 : report.reportedByDatabaseAgent) === true
2447
+ ? "The OneUptime Database Agent"
2448
+ : "An OpenTelemetry Collector with a database receiver"} sent engine metrics for a database this project did not have yet, so OneUptime registered it so the metrics had somewhere to land.`,
2449
+ };
2450
+ }
2451
+ case DatabaseServerDiscoverySource.Manual:
2452
+ return {
2453
+ emoji: "🚀",
2454
+ summary: "was added manually",
2455
+ createdBy: "**Created by**: An API key or integration, not a signed-in user.",
2456
+ explanation: "Added from the OneUptime dashboard or through the OneUptime API.",
2457
+ };
2458
+ default:
2459
+ return {
2460
+ emoji: "🤖",
2461
+ summary: "was created automatically by OneUptime",
2462
+ createdBy: automatic,
2463
+ explanation: "A OneUptime discovery path reported a database this project did not have yet, so OneUptime registered it.",
2464
+ };
2465
+ }
2466
+ }
2467
+ // The name of a database's cluster or host, for its feed; null when unreadable.
2468
+ async readParentName(parent, id) {
2469
+ if (!id) {
2470
+ return null;
2471
+ }
2472
+ try {
2473
+ let name = undefined;
2474
+ if (parent === "kubernetes") {
2475
+ const cluster = await KubernetesClusterService.findOneById({
2476
+ id: id,
2477
+ select: { name: true },
2478
+ props: { isRoot: true },
2479
+ });
2480
+ name = cluster === null || cluster === void 0 ? void 0 : cluster.name;
2481
+ }
2482
+ else if (parent === "docker") {
2483
+ const host = await DockerHostService.findOneById({
2484
+ id: id,
2485
+ select: { name: true },
2486
+ props: { isRoot: true },
2487
+ });
2488
+ name = host === null || host === void 0 ? void 0 : host.name;
2489
+ }
2490
+ else {
2491
+ const host = await PodmanHostService.findOneById({
2492
+ id: id,
2493
+ select: { name: true },
2494
+ props: { isRoot: true },
2495
+ });
2496
+ name = host === null || host === void 0 ? void 0 : host.name;
2497
+ }
2498
+ return cleanShortText(name);
2499
+ }
2500
+ catch (error) {
2501
+ logger.warn(`DatabaseServerService: could not read the name of ${id.toString()} for a Created feed item: ${error instanceof Error ? error.message : String(error)}`);
2502
+ return null;
2503
+ }
2504
+ }
2505
+ async writeDatabaseServerUpdatedFeed(onUpdate, updatedItemIds) {
2506
+ const updateData = onUpdate.updateBy
2507
+ .data;
2508
+ /*
2509
+ * Heartbeats and discovery write lastSeenAt / otelCollectorStatus /
2510
+ * member keys constantly - and hook-free, so they never get here. Only
2511
+ * the columns a person would recognise as a change earn a feed item -
2512
+ * see MEANINGFUL_UPDATE_COLUMNS.
2513
+ */
2514
+ const changedColumns = ResourceFeedUtil.getUpdatedColumnsWorthRecording(updateData);
2515
+ if (changedColumns.length === 0 || updatedItemIds.length === 0) {
2516
+ return;
2517
+ }
2518
+ const isArchiveChange = ResourceFeedUtil.isArchiveChange(updateData);
2519
+ const isArchived = Boolean(updateData["isArchived"]);
2520
+ const otherColumns = changedColumns.filter((column) => {
2521
+ return column !== "isArchived";
2522
+ });
2523
+ const updatedByUserId = onUpdate.updateBy.props.userId || undefined;
2524
+ for (const databaseServerId of updatedItemIds) {
2525
+ const databaseServer = await this.findOneById({
2526
+ id: databaseServerId,
2527
+ select: {
2528
+ projectId: true,
2529
+ },
2530
+ props: {
2531
+ isRoot: true,
2532
+ },
2533
+ });
2534
+ const projectId = databaseServer === null || databaseServer === void 0 ? void 0 : databaseServer.projectId;
2535
+ if (!projectId) {
2536
+ continue;
2537
+ }
2538
+ const resourceMarkdownLink = await this.getDatabaseServerMarkdownLink(projectId, databaseServerId);
2539
+ if (isArchiveChange) {
2540
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
2541
+ databaseServerId: databaseServerId,
2542
+ projectId: projectId,
2543
+ databaseServerFeedEventType: isArchived
2544
+ ? DatabaseServerFeedEventType.DatabaseServerArchived
2545
+ : DatabaseServerFeedEventType.DatabaseServerRestored,
2546
+ displayColor: isArchived ? Yellow500 : Blue500,
2547
+ feedInfoInMarkdown: isArchived
2548
+ ? `🗄️ ${resourceMarkdownLink} was archived.`
2549
+ : `♻️ ${resourceMarkdownLink} was restored from the archive.`,
2550
+ userId: updatedByUserId,
2551
+ });
2552
+ }
2553
+ if (otherColumns.length > 0) {
2554
+ const markdown = ResourceFeedUtil.getUpdatedFeedMarkdown({
2555
+ resourceMarkdownLink: resourceMarkdownLink,
2556
+ columns: otherColumns,
2557
+ });
2558
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
2559
+ databaseServerId: databaseServerId,
2560
+ projectId: projectId,
2561
+ databaseServerFeedEventType: DatabaseServerFeedEventType.DatabaseServerUpdated,
2562
+ displayColor: Gray500,
2563
+ feedInfoInMarkdown: markdown.feedInfoInMarkdown,
2564
+ moreInformationInMarkdown: markdown.moreInformationInMarkdown,
2565
+ userId: updatedByUserId,
2566
+ });
2567
+ }
2568
+ }
2569
+ }
2570
+ /*
2571
+ * A person archived (restored = false) or restored a row: it is no longer
2572
+ * discovery's archive (autoArchivedAt cleared), and a restore is stamped
2573
+ * so the sweep leaves the row alone until it is seen again or the grace
2574
+ * period passes (manuallyRestoredAt; cleared again on an archive).
2575
+ */
2576
+ async recordArchiveDecisionByPerson(databaseServerIds, restored) {
2577
+ const now = OneUptimeDate.getCurrentDate();
2578
+ for (const databaseServerId of databaseServerIds) {
2579
+ try {
2580
+ await this.updateColumnsByIdWithoutHooks({
2581
+ id: databaseServerId,
2582
+ data: {
2583
+ autoArchivedAt: null,
2584
+ manuallyRestoredAt: restored ? now : null,
2585
+ },
2586
+ skipUpdateDateColumn: true,
2587
+ });
2588
+ }
2589
+ catch (error) {
2590
+ logger.warn(`DatabaseServerService: could not record the ${restored ? "restore" : "archive"} of database ${databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
2591
+ }
2592
+ }
2593
+ }
2594
+ async recordDatabaseSystemSetByPerson(databaseServerIds) {
2595
+ for (const databaseServerId of databaseServerIds) {
2596
+ try {
2597
+ await this.updateColumnsByIdWithoutHooks({
2598
+ id: databaseServerId,
2599
+ data: {
2600
+ dbSystemSource: DatabaseSystemEvidence.Manual,
2601
+ },
2602
+ skipUpdateDateColumn: true,
2603
+ });
2604
+ }
2605
+ catch (error) {
2606
+ logger.warn(`DatabaseServerService: could not record the engine a person set on database ${databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
2607
+ }
2608
+ }
2609
+ }
2610
+ /*
2611
+ * Restore a row the auto-archive sweep archived, now that it was seen
2612
+ * again. The conditions live in the UPDATE itself, so a row a person
2613
+ * archived (autoArchivedAt cleared - see onUpdateSuccess) or a row a person
2614
+ * restored in the meantime is never touched, and two writers restoring at
2615
+ * once record one feed item. Never throws: a restore must not fail the
2616
+ * discovery that triggered it.
2617
+ */
2618
+ async restoreIfAutoArchived(row) {
2619
+ if (!row.isArchived || !row.autoArchivedAt || !row.id || !row.projectId) {
2620
+ return false;
2621
+ }
2622
+ try {
2623
+ const restored = await this.getRepository().manager.query(`WITH "restored" AS (
2624
+ UPDATE "DatabaseServer"
2625
+ SET "isArchived" = false,
2626
+ "archivedAt" = NULL,
2627
+ "archivedByUserId" = NULL,
2628
+ "autoArchivedAt" = NULL,
2629
+ "updatedAt" = CURRENT_TIMESTAMP
2630
+ WHERE "_id" = $1
2631
+ AND "projectId" = $2
2632
+ AND "isArchived" = true
2633
+ AND "autoArchivedAt" IS NOT NULL
2634
+ AND "deletedAt" IS NULL
2635
+ RETURNING "_id"
2636
+ )
2637
+ SELECT "_id" FROM "restored"`, [row.id.toString(), row.projectId.toString()]);
2638
+ /*
2639
+ * Nothing matched: a person restored it, or archived it themselves, in
2640
+ * the meantime. Their state stands - the in-memory row is left as read.
2641
+ */
2642
+ if (!Array.isArray(restored) || restored.length === 0) {
2643
+ return false;
2644
+ }
2645
+ row.isArchived = false;
2646
+ delete row.autoArchivedAt;
2647
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
2648
+ databaseServerId: row.id,
2649
+ projectId: row.projectId,
2650
+ databaseServerFeedEventType: DatabaseServerFeedEventType.DatabaseServerRestored,
2651
+ displayColor: Blue500,
2652
+ feedInfoInMarkdown: `♻️ ${await this.getDatabaseServerMarkdownLink(row.projectId, row.id)} was restored from the archive automatically - it was seen again after being archived for inactivity.`,
2653
+ });
2654
+ return true;
2655
+ }
2656
+ catch (error) {
2657
+ logger.warn(`DatabaseServerService: could not restore auto-archived database ${row.id.toString()}: ${error instanceof Error ? error.message : String(error)}`, { projectId: row.projectId.toString() });
2658
+ return false;
2659
+ }
2660
+ }
2661
+ /*
2662
+ * The throttled part of a collector heartbeat (see recordCollectorHeartbeat):
2663
+ * one primary-key read per database per window, fleet-wide, then the
2664
+ * auto-archive restore and the engine evidence.
2665
+ */
2666
+ async reconcileAfterHeartbeat(databaseServerId, reportedSystem) {
2667
+ const key = databaseServerId.toString();
2668
+ if (this.autoRestoreCheckMemo.get(key)) {
2669
+ return;
2670
+ }
2671
+ this.autoRestoreCheckMemo.set(key, true);
2672
+ let claimed = false;
2673
+ try {
2674
+ claimed = await GlobalCache.setStringIfNotExists(AUTO_RESTORE_CHECK_CACHE_NAMESPACE, key, "1", {
2675
+ expiresInSeconds: GlobalCache.withJitter(AUTO_RESTORE_CHECK_WINDOW_SECONDS),
2676
+ });
2677
+ }
2678
+ catch (_a) {
2679
+ /*
2680
+ * Fail closed: a missed restore is retried next window, while failing
2681
+ * open would turn a Redis outage into a read per heartbeat.
2682
+ */
2683
+ claimed = false;
2684
+ }
2685
+ if (!claimed) {
2686
+ return;
2687
+ }
2688
+ try {
2689
+ const row = await this.findOneById({
2690
+ id: databaseServerId,
2691
+ select: DISCOVERY_SELECT,
2692
+ props: {
2693
+ isRoot: true,
2694
+ },
2695
+ });
2696
+ if (row) {
2697
+ if (reportedSystem) {
2698
+ await this.applyDatabaseSystemEvidence(row, {
2699
+ system: reportedSystem,
2700
+ evidence: DatabaseSystemEvidence.Collector,
2701
+ });
2702
+ }
2703
+ await this.restoreIfAutoArchived(row);
2704
+ }
2705
+ }
2706
+ catch (error) {
2707
+ logger.warn(`DatabaseServerService: auto-archive check failed for database ${key}: ${error instanceof Error ? error.message : String(error)}`);
2708
+ }
2709
+ }
2710
+ /*
2711
+ * The archive feed item. A workload's row says what brings it back: its
2712
+ * workload or a collector, never application traces (see
2713
+ * isRetiredWorkloadRow).
2714
+ */
2715
+ async writeAutoArchiveFeed(data) {
2716
+ const restoredBy = data.isWorkload
2717
+ ? "It is restored automatically when its Kubernetes, Docker or Podman workload, or a collector, reports it again - application traces alone do not restore it."
2718
+ : "It is restored automatically as soon as it is seen again.";
2719
+ try {
2720
+ await DatabaseServerFeedService.createDatabaseServerFeedItem({
2721
+ databaseServerId: data.databaseServerId,
2722
+ projectId: data.projectId,
2723
+ databaseServerFeedEventType: DatabaseServerFeedEventType.DatabaseServerArchived,
2724
+ displayColor: Yellow500,
2725
+ feedInfoInMarkdown: `🗄️ ${await this.getDatabaseServerMarkdownLink(data.projectId, data.databaseServerId)} was automatically archived - not seen for ${data.days} ${data.days === 1 ? "day" : "days"}.`,
2726
+ moreInformationInMarkdown: `No collector, application trace or container inventory has reported this database for ${data.days} ${data.days === 1 ? "day" : "days"} while the Kubernetes cluster or container host it was found on (if any) kept reporting, and nobody has labelled it, owned it, linked it to an incident, alert or scheduled maintenance, added an endpoint to it, changed its retention or recently restored it from the archive. Labels and owners that rules or telemetry attached on their own do not count. ${restoredBy}`,
2727
+ });
2728
+ }
2729
+ catch (error) {
2730
+ logger.warn(`DatabaseServerService: could not record the auto-archive of database ${data.databaseServerId.toString()}: ${error instanceof Error ? error.message : String(error)}`);
2731
+ }
2732
+ }
2733
+ }
2734
+ __decorate([
2735
+ CaptureSpan(),
2736
+ __metadata("design:type", Function),
2737
+ __metadata("design:paramtypes", [Object]),
2738
+ __metadata("design:returntype", Promise)
2739
+ ], Service.prototype, "onBeforeCreate", null);
2740
+ __decorate([
2741
+ CaptureSpan(),
2742
+ __metadata("design:type", Function),
2743
+ __metadata("design:paramtypes", [Object, Model]),
2744
+ __metadata("design:returntype", Promise)
2745
+ ], Service.prototype, "onCreateSuccess", null);
2746
+ __decorate([
2747
+ CaptureSpan(),
2748
+ __metadata("design:type", Function),
2749
+ __metadata("design:paramtypes", [Object, Array]),
2750
+ __metadata("design:returntype", Promise)
2751
+ ], Service.prototype, "onUpdateSuccess", null);
2752
+ __decorate([
2753
+ CaptureSpan(),
2754
+ __metadata("design:type", Function),
2755
+ __metadata("design:paramtypes", [Object]),
2756
+ __metadata("design:returntype", Promise)
2757
+ ], Service.prototype, "findOrCreateByEndpoint", null);
2758
+ __decorate([
2759
+ CaptureSpan(),
2760
+ __metadata("design:type", Function),
2761
+ __metadata("design:paramtypes", [ObjectID,
2762
+ ObjectID]),
2763
+ __metadata("design:returntype", Promise)
2764
+ ], Service.prototype, "findByIdInProject", null);
2765
+ __decorate([
2766
+ CaptureSpan(),
2767
+ __metadata("design:type", Function),
2768
+ __metadata("design:paramtypes", [Object]),
2769
+ __metadata("design:returntype", Promise)
2770
+ ], Service.prototype, "upsertWorkloadDatabase", null);
2771
+ __decorate([
2772
+ CaptureSpan(),
2773
+ __metadata("design:type", Function),
2774
+ __metadata("design:paramtypes", [ObjectID, Object]),
2775
+ __metadata("design:returntype", Promise)
2776
+ ], Service.prototype, "recordCollectorHeartbeat", null);
2777
+ __decorate([
2778
+ CaptureSpan(),
2779
+ __metadata("design:type", Function),
2780
+ __metadata("design:paramtypes", [ObjectID]),
2781
+ __metadata("design:returntype", Promise)
2782
+ ], Service.prototype, "recordSighting", null);
2783
+ __decorate([
2784
+ CaptureSpan(),
2785
+ __metadata("design:type", Function),
2786
+ __metadata("design:paramtypes", [Object]),
2787
+ __metadata("design:returntype", Promise)
2788
+ ], Service.prototype, "attachLabels", null);
2789
+ __decorate([
2790
+ CaptureSpan(),
2791
+ __metadata("design:type", Function),
2792
+ __metadata("design:paramtypes", []),
2793
+ __metadata("design:returntype", Promise)
2794
+ ], Service.prototype, "markDisconnectedDatabaseServers", null);
2795
+ __decorate([
2796
+ CaptureSpan(),
2797
+ __metadata("design:type", Function),
2798
+ __metadata("design:paramtypes", []),
2799
+ __metadata("design:returntype", Promise)
2800
+ ], Service.prototype, "autoArchiveStaleDatabaseServers", null);
2801
+ __decorate([
2802
+ CaptureSpan(),
2803
+ __metadata("design:type", Function),
2804
+ __metadata("design:paramtypes", [ObjectID]),
2805
+ __metadata("design:returntype", Promise)
2806
+ ], Service.prototype, "isUnderAutoCreateBudget", null);
2807
+ __decorate([
2808
+ CaptureSpan(),
2809
+ __metadata("design:type", Function),
2810
+ __metadata("design:paramtypes", [ObjectID]),
2811
+ __metadata("design:returntype", Promise)
2812
+ ], Service.prototype, "isUnderAutoCreateBudgetCached", null);
2813
+ __decorate([
2814
+ CaptureSpan(),
2815
+ __metadata("design:type", Function),
2816
+ __metadata("design:paramtypes", [ObjectID]),
2817
+ __metadata("design:returntype", Promise)
2818
+ ], Service.prototype, "countAutoCreatedDatabaseServers", null);
2819
+ __decorate([
2820
+ CaptureSpan(),
2821
+ __metadata("design:type", Function),
2822
+ __metadata("design:paramtypes", [Object]),
2823
+ __metadata("design:returntype", Promise)
2824
+ ], Service.prototype, "getDatabaseServerName", null);
2825
+ __decorate([
2826
+ CaptureSpan(),
2827
+ __metadata("design:type", Function),
2828
+ __metadata("design:paramtypes", [Object]),
2829
+ __metadata("design:returntype", Promise)
2830
+ ], Service.prototype, "getDatabaseServerNameIfReadable", null);
2831
+ __decorate([
2832
+ CaptureSpan(),
2833
+ __metadata("design:type", Function),
2834
+ __metadata("design:paramtypes", [Object]),
2835
+ __metadata("design:returntype", Promise)
2836
+ ], Service.prototype, "assertDatabaseServerReferenceInProject", null);
2837
+ __decorate([
2838
+ CaptureSpan(),
2839
+ __metadata("design:type", Function),
2840
+ __metadata("design:paramtypes", [Object]),
2841
+ __metadata("design:returntype", Promise)
2842
+ ], Service.prototype, "recordAutomaticAssignments", null);
2843
+ __decorate([
2844
+ CaptureSpan(),
2845
+ __metadata("design:type", Function),
2846
+ __metadata("design:paramtypes", [Object]),
2847
+ __metadata("design:returntype", Promise)
2848
+ ], Service.prototype, "forgetAutomaticAssignments", null);
2849
+ __decorate([
2850
+ CaptureSpan(),
2851
+ __metadata("design:type", Function),
2852
+ __metadata("design:paramtypes", [ObjectID,
2853
+ ObjectID]),
2854
+ __metadata("design:returntype", Promise)
2855
+ ], Service.prototype, "getDatabaseServerLinkInDashboard", null);
2856
+ __decorate([
2857
+ CaptureSpan(),
2858
+ __metadata("design:type", Function),
2859
+ __metadata("design:paramtypes", [ObjectID,
2860
+ ObjectID]),
2861
+ __metadata("design:returntype", Promise)
2862
+ ], Service.prototype, "getDatabaseServerMarkdownLink", null);
2863
+ /*
2864
+ * The endpoint a person typed into the create form, read by
2865
+ * parseManualDatabaseEndpoint: the port field, when filled, replaces any
2866
+ * port in the address (and a SQL Server `\instance`), the engine's default
2867
+ * port fills a missing one, and a value that cannot be used is refused with
2868
+ * a message saying what to change. The result always has an endpoint.
2869
+ */
2870
+ function parseManualEndpoint(data) {
2871
+ const manual = parseManualDatabaseEndpoint(data.serverAddress, {
2872
+ system: data.dbSystem,
2873
+ port: data.serverPort,
2874
+ });
2875
+ if (!manual.endpoint) {
2876
+ throw new BadDataException(manual.error ||
2877
+ "Enter the host name or IP address applications use to reach this database, for example orders-db.example.com:5432.");
2878
+ }
2879
+ return manual;
2880
+ }
2881
+ /*
2882
+ * True when writing `update` (the workload path's columns) would change the
2883
+ * row beyond its liveness: a member key added or aged out, the instance
2884
+ * count, the version, the workload's kind / name / namespace / parent, or
2885
+ * the engine. lastSeenAt / workloadLastSeenAt and the member keys' own
2886
+ * timestamps are liveness, refreshed at least every
2887
+ * WORKLOAD_REFRESH_MINUTES anyway.
2888
+ */
2889
+ export function workloadColumnsChanged(row, update) {
2890
+ for (const [column, value] of Object.entries(update)) {
2891
+ if (column === "lastSeenAt" || column === "workloadLastSeenAt") {
2892
+ continue;
2893
+ }
2894
+ if (column === "memberEntityKeys") {
2895
+ if (!hasSameMemberKeys(row.memberEntityKeys, value)) {
2896
+ return true;
2897
+ }
2898
+ continue;
2899
+ }
2900
+ if (!isSameColumnValue(row[column], value)) {
2901
+ return true;
2902
+ }
2903
+ }
2904
+ return false;
2905
+ }
2906
+ // An id compares by its text (ObjectID or string), anything else strictly.
2907
+ function isSameColumnValue(current, next) {
2908
+ if (current instanceof ObjectID || next instanceof ObjectID) {
2909
+ return (current !== null &&
2910
+ current !== undefined &&
2911
+ next !== null &&
2912
+ next !== undefined &&
2913
+ String(current).toLowerCase() === String(next).toLowerCase());
2914
+ }
2915
+ return current === next;
2916
+ }
2917
+ // The same member keys, whatever their timestamps.
2918
+ function hasSameMemberKeys(stored, merged) {
2919
+ const keysOf = (value) => {
2920
+ return value && typeof value === "object" && !Array.isArray(value)
2921
+ ? Object.keys(value).sort()
2922
+ : [];
2923
+ };
2924
+ const storedKeys = keysOf(stored);
2925
+ const mergedKeys = keysOf(merged);
2926
+ return (storedKeys.length === mergedKeys.length &&
2927
+ storedKeys.every((key, index) => {
2928
+ return key === mergedKeys[index];
2929
+ }));
2930
+ }
2931
+ /*
2932
+ * The worker's aliases, re-canonicalized (a stored or hand-built alias must
2933
+ * compare byte for byte with what ingest keys), deduped, in order, capped.
2934
+ */
2935
+ function canonicalizeWorkloadAliases(aliases, dbSystem) {
2936
+ const result = [];
2937
+ const seen = new Set();
2938
+ for (const alias of Array.isArray(aliases) ? aliases : []) {
2939
+ if (result.length >= MAX_WORKLOAD_ALIASES) {
2940
+ break;
2941
+ }
2942
+ const parsed = parseDatabaseEndpointString(alias, {
2943
+ system: dbSystem,
2944
+ });
2945
+ if (!parsed) {
2946
+ continue;
2947
+ }
2948
+ const endpoint = formatDatabaseEndpoint(parsed);
2949
+ if (!endpoint || seen.has(endpoint)) {
2950
+ continue;
2951
+ }
2952
+ seen.add(endpoint);
2953
+ result.push({
2954
+ endpoint: endpoint,
2955
+ scope: getDatabaseEndpointScope(parsed),
2956
+ parsed: parsed,
2957
+ });
2958
+ }
2959
+ return result;
2960
+ }
2961
+ function cleanShortText(value) {
2962
+ const trimmed = typeof value === "string" ? value.trim() : "";
2963
+ return trimmed ? truncateShortText(trimmed) : null;
2964
+ }
2965
+ function cleanLongText(value) {
2966
+ const trimmed = typeof value === "string" ? value.trim() : "";
2967
+ return trimmed ? truncateLongText(trimmed) : null;
2968
+ }
2969
+ /*
2970
+ * "the pods of StatefulSet data/postgres", "pod data/pg-debug" - the
2971
+ * workload a Kubernetes row was detected from, for its Created feed item.
2972
+ */
2973
+ function describeKubernetesWorkload(row) {
2974
+ const name = [
2975
+ cleanShortText(row.kubernetesNamespace),
2976
+ cleanLongText(row.workloadName),
2977
+ ]
2978
+ .filter((part) => {
2979
+ return Boolean(part);
2980
+ })
2981
+ .join("/");
2982
+ const kind = cleanShortText(row.workloadKind);
2983
+ if (!name) {
2984
+ return "a Kubernetes workload's pods";
2985
+ }
2986
+ if (kind === "Pod") {
2987
+ return `pod ${name}`;
2988
+ }
2989
+ return `the pods of ${kind ? `${kind} ` : ""}${name}`;
2990
+ }
2991
+ /*
2992
+ * "Compose service" / "Swarm service" when a Docker or Podman row's
2993
+ * containers were grouped by one; null for a container of its own.
2994
+ */
2995
+ function containerServiceKind(row) {
2996
+ const kind = cleanShortText(row.workloadKind);
2997
+ return kind === ContainerWorkloadKind.ComposeService ||
2998
+ kind === ContainerWorkloadKind.SwarmService
2999
+ ? kind
3000
+ : null;
3001
+ }
3002
+ /*
3003
+ * "container e2e-receivers-mariadb", "Compose service shop-postgres",
3004
+ * "Swarm service mystack_db" - the workload a Docker or Podman row was
3005
+ * detected from, for its Created feed item.
3006
+ */
3007
+ function describeContainerWorkload(row) {
3008
+ const name = cleanLongText(row.workloadName);
3009
+ const service = containerServiceKind(row);
3010
+ if (service) {
3011
+ return name ? `${service} ${name}` : `a ${service}`;
3012
+ }
3013
+ return name ? `container ${name}` : "a container";
3014
+ }
3015
+ // The endpoint a row was created for: what discovery saw, else its own columns.
3016
+ function describeCreationEndpoint(row, context) {
3017
+ const seen = cleanLongText(context === null || context === void 0 ? void 0 : context.endpoint);
3018
+ if (seen) {
3019
+ return seen;
3020
+ }
3021
+ const host = cleanLongText(row.serverAddress);
3022
+ if (!host) {
3023
+ return "";
3024
+ }
3025
+ return typeof row.serverPort === "number"
3026
+ ? formatDatabaseEndpoint({ host: host, port: row.serverPort })
3027
+ : host;
3028
+ }
3029
+ function toInstanceCount(value) {
3030
+ return typeof value === "number" && Number.isFinite(value) && value > 0
3031
+ ? Math.floor(value)
3032
+ : 0;
3033
+ }
3034
+ /*
3035
+ * A positive-integer env setting: unset or unparseable → the default, below
3036
+ * the floor → the floor.
3037
+ */
3038
+ function readIntegerEnv(data) {
3039
+ const raw = process.env[data.name];
3040
+ if (raw === undefined || raw.trim() === "") {
3041
+ return data.defaultValue;
3042
+ }
3043
+ const parsed = Number(raw.trim());
3044
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
3045
+ return data.defaultValue;
3046
+ }
3047
+ return Math.max(parsed, data.min);
3048
+ }
3049
+ function readCount(rows) {
3050
+ var _a;
3051
+ if (!Array.isArray(rows) || rows.length === 0) {
3052
+ return 0;
3053
+ }
3054
+ const count = Number((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.count);
3055
+ return Number.isFinite(count) ? count : 0;
3056
+ }
3057
+ /**
3058
+ * The evidence a discovery source's own engine report carries: what the
3059
+ * collector path, the trace path and the container paths each know. A row's
3060
+ * discoverySource read through this is the evidence its engine was first
3061
+ * set from.
3062
+ */
3063
+ export function getDatabaseSystemEvidenceForSource(source) {
3064
+ switch (source) {
3065
+ case DatabaseServerDiscoverySource.Manual:
3066
+ return DatabaseSystemEvidence.Manual;
3067
+ case DatabaseServerDiscoverySource.Collector:
3068
+ return DatabaseSystemEvidence.Collector;
3069
+ case DatabaseServerDiscoverySource.Kubernetes:
3070
+ case DatabaseServerDiscoverySource.Docker:
3071
+ case DatabaseServerDiscoverySource.Podman:
3072
+ return DatabaseSystemEvidence.Container;
3073
+ case DatabaseServerDiscoverySource.ClientSpans:
3074
+ return DatabaseSystemEvidence.ClientSpans;
3075
+ default:
3076
+ return null;
3077
+ }
3078
+ }
3079
+ /*
3080
+ * The evidence behind a row's current engine: dbSystemSource once anything
3081
+ * refined it, else the source that created the row.
3082
+ */
3083
+ export function getStoredDatabaseSystemEvidence(row) {
3084
+ const stored = row.dbSystemSource;
3085
+ if (stored &&
3086
+ Object.values(DatabaseSystemEvidence).includes(stored)) {
3087
+ return stored;
3088
+ }
3089
+ return getDatabaseSystemEvidenceForSource(row.discoverySource);
3090
+ }
3091
+ /**
3092
+ * The engine (and the evidence for it) a row should carry once `incoming`
3093
+ * evidence arrives, or null to leave the row as it is. The evidence stored
3094
+ * is always the evidence that named the engine the row then shows.
3095
+ * - an engine a person chose is never changed;
3096
+ * - within one family, a fork refines the family's own engine (mysql ->
3097
+ * mariadb, postgresql -> cockroachdb) on evidence at least as strong as
3098
+ * what named the current engine, or from a container image, which names
3099
+ * the exact engine where a family receiver cannot (the redis receiver
3100
+ * says "redis" for Valkey). Client spans alone never refine what an
3101
+ * image or a collector determined: a client names the protocol it
3102
+ * speaks, and MariaDB Connector/J says "mariadb" to a MySQL server;
3103
+ * - the family's engine undoes a fork only when an image names it and
3104
+ * nothing stronger than client spans named the fork. A collector or a
3105
+ * client never undoes one, because they cannot tell them apart (the
3106
+ * mysql receiver says "mysql" for MariaDB);
3107
+ * - any other different engine - another family, a sibling fork, an
3108
+ * unknown engine - replaces the current one only on strictly stronger
3109
+ * evidence (the image says CockroachDB where a client said MySQL);
3110
+ * equal or weaker evidence never flips it back and forth;
3111
+ * - the same engine on stronger evidence keeps the engine but records the
3112
+ * stronger evidence, so weaker evidence cannot move it later.
3113
+ * A row with no engine takes the incoming one. Pure.
3114
+ */
3115
+ export function decideDatabaseSystem(data) {
3116
+ const incomingSystem = cleanShortText(normalizeDatabaseSystem(data.incoming.system));
3117
+ if (!incomingSystem) {
3118
+ return null;
3119
+ }
3120
+ const incoming = {
3121
+ system: incomingSystem,
3122
+ evidence: data.incoming.evidence,
3123
+ };
3124
+ const currentSystem = normalizeDatabaseSystem(data.current.system);
3125
+ if (!currentSystem) {
3126
+ return incoming;
3127
+ }
3128
+ if (data.current.evidence === DatabaseSystemEvidence.Manual) {
3129
+ return null;
3130
+ }
3131
+ const currentRank = data.current.evidence
3132
+ ? DATABASE_SYSTEM_EVIDENCE_RANK[data.current.evidence]
3133
+ : 0;
3134
+ const incomingRank = DATABASE_SYSTEM_EVIDENCE_RANK[incoming.evidence];
3135
+ if (incomingSystem === currentSystem) {
3136
+ return incomingRank > currentRank
3137
+ ? { system: currentSystem, evidence: incoming.evidence }
3138
+ : null;
3139
+ }
3140
+ const fromImage = incoming.evidence === DatabaseSystemEvidence.Container;
3141
+ // The incoming engine is a fork of the current one: a refinement.
3142
+ if (getMoreSpecificDatabaseSystem(currentSystem, incomingSystem) ===
3143
+ incomingSystem) {
3144
+ return incomingRank >= currentRank || fromImage ? incoming : null;
3145
+ }
3146
+ /*
3147
+ * The current engine is a fork of the incoming one: only an image undoes
3148
+ * a fork, and only one no more than client spans named.
3149
+ */
3150
+ if (getMoreSpecificDatabaseSystem(incomingSystem, currentSystem) ===
3151
+ currentSystem) {
3152
+ return fromImage && incomingRank > currentRank ? incoming : null;
3153
+ }
3154
+ return incomingRank > currentRank ? incoming : null;
3155
+ }
3156
+ /**
3157
+ * The row's name for a new engine - but only when the row still carries the
3158
+ * name discovery generated for its old engine (endpoint form "PostgreSQL
3159
+ * db:5432" or workload form "PostgreSQL ns/name"). A name a person chose is
3160
+ * kept: null.
3161
+ */
3162
+ export function renameForDatabaseSystem(row, newSystem) {
3163
+ const form = findGeneratedNameForm(row);
3164
+ if (!form) {
3165
+ return null;
3166
+ }
3167
+ const renamed = truncateShortText(buildDatabaseServerDisplayName(Object.assign({ system: newSystem }, form)));
3168
+ return renamed !== (row.name || "").trim() ? renamed : null;
3169
+ }
3170
+ /**
3171
+ * True while the row carries the name discovery generated for it and its
3172
+ * engine (see renameForDatabaseSystem): a name no generated form produces
3173
+ * is one a person chose.
3174
+ */
3175
+ export function hasDiscoveryGeneratedName(row) {
3176
+ return findGeneratedNameForm(row) !== null;
3177
+ }
3178
+ // The generated form the row's name matches, or null when a person named it.
3179
+ function findGeneratedNameForm(row) {
3180
+ const name = typeof row.name === "string" ? row.name.trim() : "";
3181
+ if (!name || !row.dbSystem) {
3182
+ return null;
3183
+ }
3184
+ const forms = [];
3185
+ if (row.serverAddress) {
3186
+ forms.push({
3187
+ endpoint: {
3188
+ host: row.serverAddress,
3189
+ port: typeof row.serverPort === "number" && row.serverPort > 0
3190
+ ? row.serverPort
3191
+ : null,
3192
+ },
3193
+ });
3194
+ }
3195
+ if (row.workloadName) {
3196
+ forms.push({
3197
+ namespace: row.kubernetesNamespace,
3198
+ workloadName: row.workloadName,
3199
+ });
3200
+ }
3201
+ for (const form of forms) {
3202
+ const generated = truncateShortText(buildDatabaseServerDisplayName(Object.assign({ system: row.dbSystem }, form)));
3203
+ if (generated === name) {
3204
+ return form;
3205
+ }
3206
+ }
3207
+ return null;
3208
+ }
3209
+ /*
3210
+ * Every catalogued engine of `system`'s family, the family's own engine
3211
+ * first ("valkey" -> redis, valkey, keydb, ...). An engine outside the
3212
+ * catalog is a family of one.
3213
+ */
3214
+ export function getDatabaseSystemsOfFamily(system) {
3215
+ const family = getDatabaseSystemFamily(system);
3216
+ if (!family) {
3217
+ return [];
3218
+ }
3219
+ const systems = [family];
3220
+ for (const descriptor of DATABASE_SYSTEMS) {
3221
+ if ((descriptor.family || descriptor.system) === family &&
3222
+ !systems.includes(descriptor.system)) {
3223
+ systems.push(descriptor.system);
3224
+ }
3225
+ }
3226
+ return systems;
3227
+ }
3228
+ /*
3229
+ * The ids in a relation payload (an update's `labels`): model instances,
3230
+ * `{ _id }` / `{ id }` objects, ObjectIDs or id strings. Anything else is
3231
+ * skipped.
3232
+ */
3233
+ function readRelationIds(value) {
3234
+ const ids = [];
3235
+ for (const item of Array.isArray(value) ? value : []) {
3236
+ let id = item;
3237
+ if (item && typeof item === "object" && !(item instanceof ObjectID)) {
3238
+ id =
3239
+ item._id ||
3240
+ item.id;
3241
+ }
3242
+ const text = id instanceof ObjectID || typeof id === "string" ? id.toString() : "";
3243
+ if (text) {
3244
+ ids.push(text);
3245
+ }
3246
+ }
3247
+ return ids;
3248
+ }
3249
+ /*
3250
+ * Assignment ids as automaticAssignments stores them: lowercase UUID strings
3251
+ * (what Postgres prints for a uuid column), deduped, invalid ones dropped.
3252
+ */
3253
+ function normalizeAssignmentIds(ids) {
3254
+ const result = [];
3255
+ for (const id of Array.isArray(ids) ? ids : []) {
3256
+ const text = (id ? id.toString() : "").trim().toLowerCase();
3257
+ if (text && ObjectID.isValidUUID(text) && !result.includes(text)) {
3258
+ result.push(text);
3259
+ }
3260
+ }
3261
+ return result;
3262
+ }
3263
+ function fingerprintLabelIds(labelIds) {
3264
+ const sorted = labelIds
3265
+ .map((id) => {
3266
+ return id.toString();
3267
+ })
3268
+ .sort();
3269
+ return crypto.createHash("sha256").update(sorted.join(",")).digest("hex");
3270
+ }
3271
+ export default new Service();
3272
+ //# sourceMappingURL=DatabaseServerService.js.map