@oneuptime/common 14.0.2 → 14.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (765) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  4. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  5. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  6. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  9. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  10. package/Models/DatabaseModels/Incident.ts +55 -0
  11. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  12. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  13. package/Models/DatabaseModels/Index.ts +18 -0
  14. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  15. package/Models/DatabaseModels/Project.ts +60 -0
  16. package/Models/DatabaseModels/RumApplication.ts +52 -1
  17. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  18. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  19. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  25. package/Server/Middleware/MultipartFormData.ts +1 -1
  26. package/Server/Middleware/TelemetryIngest.ts +15 -8
  27. package/Server/Services/AlertReminderRuleService.ts +100 -24
  28. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  29. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  30. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  31. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  32. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  33. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  34. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  35. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  36. package/Server/Services/DatabaseServerService.ts +4546 -0
  37. package/Server/Services/DatabaseService.ts +14 -8
  38. package/Server/Services/DockerResourceService.ts +43 -2
  39. package/Server/Services/IncidentAlertService.ts +1811 -0
  40. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  41. package/Server/Services/IncidentService.ts +210 -10
  42. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  43. package/Server/Services/Index.ts +18 -0
  44. package/Server/Services/InventoryItemService.ts +113 -5
  45. package/Server/Services/IoTDeviceService.ts +145 -37
  46. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  47. package/Server/Services/PodmanResourceService.ts +43 -2
  48. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  49. package/Server/Services/ServiceService.ts +1 -1
  50. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  51. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  52. package/Server/Services/UserService.ts +14 -1
  53. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  54. package/Server/Types/Database/QueryHelper.ts +30 -1
  55. package/Server/Utils/AI/Toolbox/ResourceTools.ts +69 -1
  56. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  57. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +188 -6
  58. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  59. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2262 -755
  60. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  61. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  62. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  63. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  64. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  65. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  66. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  67. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  69. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  70. package/Server/Utils/StartServer.ts +26 -6
  71. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  72. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  73. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  74. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  75. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  76. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  77. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  78. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  79. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  80. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  81. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  82. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  83. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  84. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  85. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  86. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  87. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  88. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  89. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  90. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  91. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  92. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  93. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  94. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  95. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  96. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  97. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  98. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  99. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  100. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  101. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  102. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  103. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  104. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  105. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  106. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  107. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  108. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  109. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  110. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  111. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  112. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  113. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  114. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  115. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  116. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  117. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  118. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  119. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  120. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  121. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  122. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  123. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  124. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  125. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  126. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  127. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  128. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  129. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  130. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  131. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  132. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  133. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  134. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  135. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  136. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  137. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  138. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  139. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  140. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  141. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  142. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  143. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  144. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  145. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  146. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  147. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  148. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  149. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  150. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  151. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  152. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  153. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  154. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  155. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  156. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  157. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  158. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  159. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  160. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  161. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  162. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  163. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  164. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  165. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  166. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  167. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  168. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  169. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  170. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  171. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  172. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  173. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  174. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  175. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  176. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  177. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  178. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  179. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  180. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  181. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  182. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  183. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  184. package/Tests/ClassStrings.ts +178 -0
  185. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  186. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  187. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  188. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  189. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  190. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  191. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  192. package/Tests/RenderedMarkup.ts +518 -0
  193. package/Tests/ResponsiveVisibility.test.ts +490 -4
  194. package/Tests/ResponsiveVisibility.ts +505 -52
  195. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  196. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  197. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  198. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  199. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  200. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  201. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  202. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  203. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  204. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  205. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  206. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  207. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  208. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  209. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  210. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  211. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  212. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  213. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  214. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  215. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  216. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  217. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  218. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  219. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  220. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  221. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  222. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  223. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  224. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  225. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  226. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  227. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  228. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  229. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  230. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  231. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  232. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  233. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  234. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  235. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  236. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  237. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  238. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  239. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  240. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  241. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  242. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  243. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  244. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  245. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  246. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  247. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  248. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  249. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  250. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  251. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  252. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1719 -0
  253. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  254. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  255. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  256. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  257. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  258. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  259. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  260. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  261. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  262. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  263. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  264. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  265. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  266. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  267. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  268. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  269. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  270. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  271. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  272. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  273. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  274. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  275. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  276. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  277. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  278. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  279. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  280. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  281. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  282. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  283. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  284. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  285. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  286. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  287. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  288. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  289. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  290. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  291. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  292. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  293. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +298 -0
  294. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +271 -0
  295. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  296. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  297. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  298. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  299. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  300. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  301. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +181 -0
  302. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  303. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  304. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  305. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  306. package/Tests/UI/Components/Card.test.tsx +93 -2
  307. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  308. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  309. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  310. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  311. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  312. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  313. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  314. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  315. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  316. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  317. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  318. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  319. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  320. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  321. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  322. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  323. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  324. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  325. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  326. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  327. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  328. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  329. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  330. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  331. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  332. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  333. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  334. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  335. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  336. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  337. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  338. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  339. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  340. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  341. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  342. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  343. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +80 -0
  344. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +23 -0
  345. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  346. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  347. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  348. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  349. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  350. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  351. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  352. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  353. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  354. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  355. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  356. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  357. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  358. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  359. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  360. package/Tests/tsconfig.json +16 -0
  361. package/Types/AI/AIResourceContext.ts +7 -0
  362. package/Types/Dashboard/DashboardTemplates.ts +2 -1
  363. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  364. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  365. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  366. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  367. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  368. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  369. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  370. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  371. package/Types/Email/EmailTemplateType.ts +1 -0
  372. package/Types/Incident/IncidentAlertLink.ts +38 -0
  373. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  374. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  375. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  376. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  377. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  378. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  379. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  380. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  381. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  382. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  383. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  384. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  385. package/Types/Permission.ts +315 -0
  386. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  387. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  388. package/Types/Rules/RuleRun.ts +12 -0
  389. package/Types/Rum/SessionReplay.ts +25 -6
  390. package/Types/Rum/WebVitals.ts +52 -0
  391. package/Types/Telemetry/EntityType.ts +23 -0
  392. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  393. package/Types/Telemetry/ServiceType.ts +8 -0
  394. package/UI/Components/Banner/Banner.tsx +1 -1
  395. package/UI/Components/Card/Card.tsx +1 -1
  396. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  397. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  398. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  399. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  400. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  401. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  402. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  403. package/UI/Components/Forms/BasicForm.tsx +1 -1
  404. package/UI/Components/Header/Header.tsx +1 -1
  405. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  406. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  407. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  408. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  409. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  410. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  411. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  412. package/UI/Components/ModelTable/BaseModelTable.tsx +99 -21
  413. package/UI/Components/ModelTable/Column.ts +5 -0
  414. package/UI/Components/Navbar/NavBar.tsx +9 -2
  415. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  416. package/UI/Components/Page/Page.tsx +1 -1
  417. package/UI/Components/Pagination/Pagination.tsx +3 -3
  418. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  419. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  420. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  421. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  422. package/UI/Components/Table/TableHeader.tsx +47 -2
  423. package/UI/Components/Table/Types/Column.ts +7 -0
  424. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  425. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  426. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  427. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  428. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  429. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  430. package/UI/Styles/Theme.css +8 -1
  431. package/UI/Utils/Navigation.ts +89 -42
  432. package/UI/Utils/PermissionGate.ts +11 -2
  433. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  434. package/UI/Utils/TelemetryService.ts +4 -3
  435. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  436. package/Utils/LabelRuleImportExport.ts +2 -0
  437. package/Utils/MetricUnitUtil.ts +103 -11
  438. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  439. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  440. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  441. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  442. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  443. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  444. package/Utils/Rum/SessionTraceState.ts +212 -0
  445. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  446. package/Utils/Telemetry/EntityKey.ts +176 -0
  447. package/Utils/ValueFormatter.ts +16 -0
  448. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  449. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  451. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  453. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  454. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  455. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  456. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  457. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  458. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  459. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  460. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  461. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  462. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  463. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  464. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  465. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  466. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  467. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  469. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  470. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  471. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/Index.js +18 -0
  473. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  476. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  478. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  480. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  482. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  484. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  495. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  496. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  497. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  498. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  499. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  500. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  501. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  502. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  503. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  504. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  505. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  506. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  507. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  508. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  509. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  510. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  511. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  512. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  513. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  514. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  515. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  516. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  517. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  518. package/build/dist/Server/Services/DatabaseService.js +8 -4
  519. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  520. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  521. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  522. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  523. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  524. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  525. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  526. package/build/dist/Server/Services/IncidentService.js +164 -12
  527. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  528. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  529. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  530. package/build/dist/Server/Services/Index.js +18 -0
  531. package/build/dist/Server/Services/Index.js.map +1 -1
  532. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  533. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  534. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  535. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  536. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  537. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  538. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  539. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  540. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  541. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  542. package/build/dist/Server/Services/ServiceService.js +1 -1
  543. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  544. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  545. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  546. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  547. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  548. package/build/dist/Server/Services/UserService.js +13 -1
  549. package/build/dist/Server/Services/UserService.js.map +1 -1
  550. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  551. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  552. package/build/dist/Server/Types/Database/QueryHelper.js +26 -1
  553. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  554. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +46 -0
  555. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  556. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  557. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  558. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +134 -13
  559. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  560. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  561. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  562. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  563. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  564. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  565. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  566. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  567. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  568. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  569. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  570. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  571. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  572. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  573. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  574. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  575. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  576. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  577. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  578. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  579. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  580. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  581. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  582. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  583. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  584. package/build/dist/Server/Utils/StartServer.js +20 -4
  585. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  586. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  587. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  588. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  589. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  590. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  591. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  592. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  593. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  594. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  595. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  596. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  597. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  598. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  599. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  600. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  601. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  602. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  603. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  604. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  605. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  606. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  607. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  608. package/build/dist/Types/Dashboard/DashboardTemplates.js +2 -1
  609. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  610. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  611. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  612. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  613. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  614. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  615. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  616. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  617. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  618. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  619. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  620. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  621. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  622. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  623. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  624. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  625. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  626. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  627. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  628. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  629. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  630. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  631. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  632. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  633. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  634. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  635. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  636. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  637. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  638. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  639. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  640. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  641. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  642. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  643. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  644. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  645. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  646. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  647. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  648. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  649. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  650. package/build/dist/Types/Permission.js +276 -0
  651. package/build/dist/Types/Permission.js.map +1 -1
  652. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  653. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  654. package/build/dist/Types/Rules/RuleRun.js +4 -0
  655. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  656. package/build/dist/Types/Rum/WebVitals.js +25 -0
  657. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  658. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  659. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  660. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  661. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  662. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  663. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  664. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  665. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  666. package/build/dist/UI/Components/Card/Card.js +1 -1
  667. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  668. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  669. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  670. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  671. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  672. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  673. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  674. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  675. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  676. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  677. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  678. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  679. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  680. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  681. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  682. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  683. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  684. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  685. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  686. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  687. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  688. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  689. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  690. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  692. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  694. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  695. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  696. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  697. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  698. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +70 -18
  699. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  700. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  701. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  702. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  703. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  704. package/build/dist/UI/Components/Page/Page.js +1 -1
  705. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  706. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  707. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  708. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  709. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  710. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  711. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  712. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  713. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  714. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  715. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  716. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  717. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  718. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  719. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  720. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  721. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  722. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  723. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  724. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  725. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  727. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  728. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  729. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  730. package/build/dist/UI/Utils/Navigation.js +66 -33
  731. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  732. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  733. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  734. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  735. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  736. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  737. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  738. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  739. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  740. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  741. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  742. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  743. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  744. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  745. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  746. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  747. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  748. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  749. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  750. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  751. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  752. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  753. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  754. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  755. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  756. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  757. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  758. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  759. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  760. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  761. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  762. package/build/dist/Utils/ValueFormatter.js +15 -0
  763. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  764. package/package.json +1 -1
  765. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2657 @@
1
+ import {
2
+ buildDatabaseCallerContext,
3
+ buildDatabaseServerDisplayName,
4
+ buildDatabaseServerIdentifier,
5
+ buildKubernetesDatabaseAliases,
6
+ buildWorkloadDatabaseServerIdentifier,
7
+ canonicalizeDatabaseEndpoint,
8
+ DATABASE_INSTANCE_ATTRIBUTES,
9
+ DatabaseCallerContext,
10
+ DatabaseEndpoint,
11
+ DatabaseEndpointPurpose,
12
+ formatDatabaseEndpoint,
13
+ getDatabaseClusterHost,
14
+ getDatabaseEndpointScope,
15
+ isClusterScopedDatabaseHost,
16
+ isEphemeralCaller,
17
+ isHostRelativeDatabaseHost,
18
+ isIpLiteralHost,
19
+ isKubernetesDatabaseCaller,
20
+ isKubernetesServiceDnsHost,
21
+ isLinkLocalIpHost,
22
+ isLoopbackDatabaseHost,
23
+ isPrivateIpHost,
24
+ ManualDatabaseEndpoint,
25
+ NETWORK_SCOPED_NAME_SUFFIXES,
26
+ parseDatabaseEndpointString,
27
+ parseHostAndPort,
28
+ parseHostAndPortList,
29
+ ParsedHostAndPort,
30
+ parseManualDatabaseEndpoint,
31
+ readDatabaseInstanceName,
32
+ splitDatabaseHostInstance,
33
+ } from "../../../Types/DatabaseServer/DatabaseEndpoint";
34
+ import { DATABASE_SYSTEMS } from "../../../Types/DatabaseServer/DatabaseSystem";
35
+ import { keyForDatabaseEndpoint } from "../../../Utils/Telemetry/EntityKey";
36
+ import { describe, expect, test } from "@jest/globals";
37
+
38
+ /*
39
+ * The endpoint rules decide which telemetry lands on which database page.
40
+ * A wrong merge shows one database's queries on another's page; a wrong
41
+ * split leaves a page dark. Every rule of SPEC §2.3 is pinned here.
42
+ */
43
+
44
+ const VM_CALLER: DatabaseCallerContext = {
45
+ hostName: "app-7.corp.example",
46
+ isEphemeral: false,
47
+ };
48
+
49
+ const K8S_CALLER: DatabaseCallerContext = {
50
+ kubernetesNamespace: "shop",
51
+ kubernetesClusterName: "prod-eu",
52
+ hostName: "checkout-7d9f-abcde",
53
+ isEphemeral: true,
54
+ };
55
+
56
+ const NO_CONTEXT: DatabaseCallerContext = { isEphemeral: true };
57
+
58
+ function canonical(
59
+ address: string | null | undefined,
60
+ options: {
61
+ system?: string;
62
+ port?: string | number | null;
63
+ caller?: DatabaseCallerContext;
64
+ purpose?: DatabaseEndpointPurpose;
65
+ } = {},
66
+ ): DatabaseEndpoint | null {
67
+ return canonicalizeDatabaseEndpoint({
68
+ system: options.system ?? "postgresql",
69
+ address,
70
+ port: options.port,
71
+ caller: options.caller ?? NO_CONTEXT,
72
+ purpose: options.purpose ?? "client-call",
73
+ });
74
+ }
75
+
76
+ describe("parseHostAndPort — accepted shapes", () => {
77
+ test.each([
78
+ ["db.prod.example.com", "db.prod.example.com", null],
79
+ ["db.prod.example.com:6543", "db.prod.example.com", 6543],
80
+ [" DB.Prod.Example.COM. ", "db.prod.example.com", null],
81
+ ["db.prod.example.com.:5432", "db.prod.example.com", 5432],
82
+ ["10.0.0.5", "10.0.0.5", null],
83
+ ["10.0.0.5:5432", "10.0.0.5", 5432],
84
+ ["010.000.000.005", "10.0.0.5", null],
85
+ ["pg_primary", "pg_primary", null],
86
+ ["postgres", "postgres", null],
87
+ ])("%s → host %s port %s", (raw: string, host: string, port: unknown) => {
88
+ expect(parseHostAndPort(raw)).toEqual({ host, port });
89
+ });
90
+
91
+ test("URL forms: scheme, userinfo, path and query are stripped", () => {
92
+ expect(
93
+ parseHostAndPort("postgres://app:secret@db.prod:5432/orders"),
94
+ ).toEqual({ host: "db.prod", port: 5432 });
95
+ expect(
96
+ parseHostAndPort("postgresql://db.prod:5432/orders?sslmode=require"),
97
+ ).toEqual({ host: "db.prod", port: 5432 });
98
+ expect(parseHostAndPort("redis://cache.prod#frag")).toEqual({
99
+ host: "cache.prod",
100
+ port: null,
101
+ });
102
+ expect(
103
+ parseHostAndPort("mongodb+srv://cluster0.abc.mongodb.net/app"),
104
+ ).toEqual({ host: "cluster0.abc.mongodb.net", port: null });
105
+ expect(parseHostAndPort("jdbc:postgresql://db.prod:5433/app")).toEqual({
106
+ host: "db.prod",
107
+ port: 5433,
108
+ });
109
+ });
110
+
111
+ test("URL userinfo splits on the LAST @ before the first /", () => {
112
+ // A password containing "@" must not leak into the host.
113
+ expect(parseHostAndPort("mysql://app:p@ss@w0rd@db.prod:3306/x")).toEqual({
114
+ host: "db.prod",
115
+ port: 3306,
116
+ });
117
+ // An "@" after the authority is path, not userinfo.
118
+ expect(parseHostAndPort("mysql://db.prod:3306/x@y")).toEqual({
119
+ host: "db.prod",
120
+ port: 3306,
121
+ });
122
+ });
123
+
124
+ test("userinfo is only recognised in URL forms", () => {
125
+ expect(parseHostAndPort("admin@db:5432")).toBeNull();
126
+ });
127
+
128
+ test("JDBC ;key=value properties are not part of the address", () => {
129
+ expect(
130
+ parseHostAndPort(
131
+ "jdbc:sqlserver://sql.prod:1433;databaseName=orders;encrypt=true",
132
+ ),
133
+ ).toEqual({ host: "sql.prod", port: 1433 });
134
+ expect(parseHostAndPort("sql.prod:1433;encrypt=true")).toEqual({
135
+ host: "sql.prod",
136
+ port: 1433,
137
+ });
138
+ });
139
+
140
+ test("a host list keeps its first host in canonical order", () => {
141
+ expect(
142
+ parseHostAndPort("mongodb://m1.prod:27017,m2.prod:27017/app"),
143
+ ).toEqual({ host: "m1.prod", port: 27017 });
144
+ expect(parseHostAndPort("m1.prod:27018,m2.prod:27017")).toEqual({
145
+ host: "m1.prod",
146
+ port: 27018,
147
+ });
148
+ });
149
+
150
+ test("non-URL values drop a path and query", () => {
151
+ // Oracle's service.instance.id shape: host:port/service.
152
+ expect(parseHostAndPort("ora.prod:1521/ORCLPDB1")).toEqual({
153
+ host: "ora.prod",
154
+ port: 1521,
155
+ });
156
+ expect(parseHostAndPort("db.prod:5432?x=1")).toEqual({
157
+ host: "db.prod",
158
+ port: 5432,
159
+ });
160
+ });
161
+
162
+ test("SQL Server forms", () => {
163
+ expect(parseHostAndPort("tcp:sql.prod,1433")).toEqual({
164
+ host: "sql.prod",
165
+ port: 1433,
166
+ });
167
+ expect(parseHostAndPort("TCP:sql.prod")).toEqual({
168
+ host: "sql.prod",
169
+ port: null,
170
+ });
171
+ expect(parseHostAndPort("sql.prod,14330")).toEqual({
172
+ host: "sql.prod",
173
+ port: 14330,
174
+ });
175
+ // The named instance is returned beside the host, lowercased.
176
+ expect(parseHostAndPort("sql.prod\\SQLEXPRESS")).toEqual({
177
+ host: "sql.prod",
178
+ port: null,
179
+ instance: "sqlexpress",
180
+ });
181
+ expect(parseHostAndPort("sql.prod\\SQLEXPRESS,1434")).toEqual({
182
+ host: "sql.prod",
183
+ port: 1434,
184
+ instance: "sqlexpress",
185
+ });
186
+ expect(parseHostAndPort("sql.prod\\SQLEXPRESS:1435")).toEqual({
187
+ host: "sql.prod",
188
+ port: 1435,
189
+ instance: "sqlexpress",
190
+ });
191
+ // MSSQLSERVER is the default instance: no instance at all.
192
+ expect(parseHostAndPort("sql.prod\\MSSQLSERVER")).toEqual({
193
+ host: "sql.prod",
194
+ port: null,
195
+ });
196
+ expect(parseHostAndPort("sql.prod\\")).toEqual({
197
+ host: "sql.prod",
198
+ port: null,
199
+ });
200
+ });
201
+
202
+ test("a SQL Server instance that is not a valid instance name rejects the value", () => {
203
+ for (const raw of [
204
+ "sql.prod\\[REDACTED]",
205
+ "sql.prod\\SQL EXPRESS",
206
+ "sql.prod\\averyveryverylonginstance",
207
+ "sql.prod\\inst:abc",
208
+ ]) {
209
+ expect(parseHostAndPort(raw)).toBeNull();
210
+ }
211
+ // "#" ends an address (a URL fragment), so it never reaches an instance.
212
+ expect(parseHostAndPort("sql.prod\\inst#1")).toEqual({
213
+ host: "sql.prod",
214
+ port: null,
215
+ instance: "inst",
216
+ });
217
+ });
218
+
219
+ test("a host literally named tcp keeps its port", () => {
220
+ expect(parseHostAndPort("tcp:5432")).toEqual({ host: "tcp", port: 5432 });
221
+ });
222
+
223
+ test("SQL Server local-machine spellings become localhost", () => {
224
+ expect(parseHostAndPort("(local)")).toEqual({
225
+ host: "localhost",
226
+ port: null,
227
+ });
228
+ expect(parseHostAndPort("(LOCAL)\\SQLEXPRESS")).toEqual({
229
+ host: "localhost",
230
+ port: null,
231
+ instance: "sqlexpress",
232
+ });
233
+ expect(parseHostAndPort(".")).toEqual({ host: "localhost", port: null });
234
+ expect(parseHostAndPort(".\\SQLEXPRESS,1433")).toEqual({
235
+ host: "localhost",
236
+ port: 1433,
237
+ instance: "sqlexpress",
238
+ });
239
+ expect(parseHostAndPort("(localdb)\\MSSQLLocalDB")).toEqual({
240
+ host: "localhost",
241
+ port: null,
242
+ instance: "mssqllocaldb",
243
+ });
244
+ });
245
+
246
+ test("IPv6: brackets, bare, zone id, compression, case", () => {
247
+ expect(parseHostAndPort("[fd00::5]:5432")).toEqual({
248
+ host: "fd00::5",
249
+ port: 5432,
250
+ });
251
+ expect(parseHostAndPort("[FD00:0:0:0:0:0:0:5]")).toEqual({
252
+ host: "fd00::5",
253
+ port: null,
254
+ });
255
+ expect(parseHostAndPort("fd00:0000:0000::0005")).toEqual({
256
+ host: "fd00::5",
257
+ port: null,
258
+ });
259
+ expect(parseHostAndPort("fe80::1%eth0")).toEqual({
260
+ host: "fe80::1",
261
+ port: null,
262
+ });
263
+ expect(parseHostAndPort("[fe80::1%25eth0]:6379")).toEqual({
264
+ host: "fe80::1",
265
+ port: 6379,
266
+ });
267
+ expect(parseHostAndPort("2001:db8:0:0:1:0:0:1")).toEqual({
268
+ host: "2001:db8::1:0:0:1",
269
+ port: null,
270
+ });
271
+ expect(parseHostAndPort("::1")).toEqual({ host: "::1", port: null });
272
+ expect(parseHostAndPort("::")).toEqual({ host: "::", port: null });
273
+ });
274
+
275
+ test("IPv4-mapped IPv6 is unwrapped to IPv4 (dotted and hex tails)", () => {
276
+ expect(parseHostAndPort("::ffff:10.0.0.5")).toEqual({
277
+ host: "10.0.0.5",
278
+ port: null,
279
+ });
280
+ expect(parseHostAndPort("[::ffff:10.0.0.5]:5432")).toEqual({
281
+ host: "10.0.0.5",
282
+ port: 5432,
283
+ });
284
+ expect(parseHostAndPort("::FFFF:7f00:1")).toEqual({
285
+ host: "127.0.0.1",
286
+ port: null,
287
+ });
288
+ });
289
+
290
+ test("ports outside 1..65535 are dropped, not the host", () => {
291
+ expect(parseHostAndPort("db.prod:0")).toEqual({
292
+ host: "db.prod",
293
+ port: null,
294
+ });
295
+ expect(parseHostAndPort("db.prod:65536")).toEqual({
296
+ host: "db.prod",
297
+ port: null,
298
+ });
299
+ expect(parseHostAndPort("db.prod:65535")).toEqual({
300
+ host: "db.prod",
301
+ port: 65535,
302
+ });
303
+ expect(parseHostAndPort("db.prod:")).toEqual({
304
+ host: "db.prod",
305
+ port: null,
306
+ });
307
+ expect(parseHostAndPort("[fd00::1]:")).toEqual({
308
+ host: "fd00::1",
309
+ port: null,
310
+ });
311
+ });
312
+ });
313
+
314
+ describe("parseHostAndPort — rejected values", () => {
315
+ test.each([
316
+ // Built-in trace scrub outputs.
317
+ ["[REDACTED]"],
318
+ ["[redacted]:5432"],
319
+ ["[HASHED:ab12cd34]"],
320
+ ["[HASHED:ab]"],
321
+ ["***.***.***.***"],
322
+ ["***.***.***.***:5432"],
323
+ // Garbage.
324
+ ["*"],
325
+ [""],
326
+ [" "],
327
+ ["db prod"],
328
+ ["db\tprod"],
329
+ ["(db)"],
330
+ ["db!.prod"],
331
+ ["hashed:ab12cd"],
332
+ ["db.prod:abc"],
333
+ // Unix socket paths.
334
+ ["/var/run/postgresql"],
335
+ ["/tmp/.s.PGSQL.5432"],
336
+ // Mangled addresses.
337
+ ["999.1.1.1"],
338
+ ["1.2.3"],
339
+ ["12345"],
340
+ ["[fd00::1"],
341
+ ["[fd00::1]5432"],
342
+ ["fd00:::1"],
343
+ ["1:2:3:4:5:6:7:8:9"],
344
+ [":5432"],
345
+ ])("%j → null", (raw: string) => {
346
+ expect(parseHostAndPort(raw)).toBeNull();
347
+ });
348
+
349
+ test("labels and total length follow RFC 1123 limits", () => {
350
+ const label63: string = "a".repeat(63);
351
+ expect(parseHostAndPort(`${label63}.example.com`)?.host).toBe(
352
+ `${label63}.example.com`,
353
+ );
354
+ expect(parseHostAndPort(`${"a".repeat(64)}.example.com`)).toBeNull();
355
+
356
+ const long: string = `${Array(64).fill("abc").join(".")}.com`; // 259 chars
357
+ expect(long.length).toBeGreaterThan(253);
358
+ expect(parseHostAndPort(long)).toBeNull();
359
+ });
360
+
361
+ test("non-strings are null", () => {
362
+ expect(parseHostAndPort(null)).toBeNull();
363
+ expect(parseHostAndPort(undefined)).toBeNull();
364
+ expect(parseHostAndPort(5432)).toBeNull();
365
+ expect(parseHostAndPort({ host: "db" })).toBeNull();
366
+ });
367
+ });
368
+
369
+ describe("host classification", () => {
370
+ test("isIpLiteralHost", () => {
371
+ expect(isIpLiteralHost("10.0.0.5")).toBe(true);
372
+ expect(isIpLiteralHost("fd00::1")).toBe(true);
373
+ expect(isIpLiteralHost("[fd00::1]")).toBe(true);
374
+ expect(isIpLiteralHost("db.prod")).toBe(false);
375
+ expect(isIpLiteralHost("1.2.3")).toBe(false);
376
+ expect(isIpLiteralHost("")).toBe(false);
377
+ });
378
+
379
+ test("isLoopbackDatabaseHost", () => {
380
+ for (const host of [
381
+ "localhost",
382
+ "LOCALHOST",
383
+ "api.localhost",
384
+ "127.0.0.1",
385
+ "127.1.2.3",
386
+ "::1",
387
+ "::",
388
+ "0.0.0.0",
389
+ "::ffff:127.0.0.1",
390
+ "0:0:0:0:0:0:0:1",
391
+ "(local)",
392
+ ".",
393
+ // The /etc/hosts aliases distributions give the loopback address.
394
+ "localhost.localdomain",
395
+ "LOCALHOST.LOCALDOMAIN.",
396
+ "localhost4",
397
+ "localhost6",
398
+ "localhost6.localdomain6",
399
+ "ip6-localhost",
400
+ "ip6-loopback",
401
+ "localhost.",
402
+ ]) {
403
+ expect(isLoopbackDatabaseHost(host)).toBe(true);
404
+ }
405
+ for (const host of ["db.prod", "10.0.0.5", "128.0.0.1", "localhost.corp"]) {
406
+ expect(isLoopbackDatabaseHost(host)).toBe(false);
407
+ }
408
+ });
409
+
410
+ test("isHostRelativeDatabaseHost", () => {
411
+ for (const host of [
412
+ "host.docker.internal",
413
+ "HOST.DOCKER.INTERNAL.",
414
+ "host.containers.internal",
415
+ "gateway.docker.internal",
416
+ "docker.for.mac.localhost",
417
+ "kubernetes.docker.internal",
418
+ ]) {
419
+ expect(isHostRelativeDatabaseHost(host)).toBe(true);
420
+ }
421
+ expect(isHostRelativeDatabaseHost("db.internal")).toBe(false);
422
+ });
423
+
424
+ test("isPrivateIpHost covers RFC 1918, CGNAT and ULA only", () => {
425
+ for (const host of [
426
+ "10.0.0.1",
427
+ "10.255.255.255",
428
+ "172.16.0.1",
429
+ "172.31.255.255",
430
+ "192.168.1.1",
431
+ "100.64.0.1",
432
+ "100.127.255.255",
433
+ "fc00::1",
434
+ "fd12:3456::1",
435
+ "[fd00::1]",
436
+ "::ffff:10.1.2.3",
437
+ ]) {
438
+ expect(isPrivateIpHost(host)).toBe(true);
439
+ }
440
+ for (const host of [
441
+ "172.15.0.1",
442
+ "172.32.0.1",
443
+ "192.169.0.1",
444
+ "100.63.255.255",
445
+ "100.128.0.1",
446
+ "8.8.8.8",
447
+ "2001:db8::1",
448
+ "fe80::1",
449
+ "db.prod",
450
+ "10.0.0",
451
+ ]) {
452
+ expect(isPrivateIpHost(host)).toBe(false);
453
+ }
454
+ });
455
+ });
456
+
457
+ describe("isEphemeralCaller / buildDatabaseCallerContext", () => {
458
+ const VM: Record<string, unknown> = {
459
+ "host.name": "app-7",
460
+ "os.type": "linux",
461
+ };
462
+
463
+ test("a VM with os.type is a stable machine", () => {
464
+ expect(isEphemeralCaller(VM)).toBe(false);
465
+ });
466
+
467
+ test.each([
468
+ ["k8s.pod.name", "checkout-7d9f"],
469
+ ["k8s.namespace.name", "shop"],
470
+ ["k8s.cluster.name", "prod"],
471
+ ["k8s.node.name", "node-1"],
472
+ ["container.id", "0123456789ab"],
473
+ ["container.runtime", "docker"],
474
+ ["faas.name", "resize-image"],
475
+ ["faas.instance", "2021/06/28/[$LATEST]abc"],
476
+ ])("%s marks the caller ephemeral", (key: string, value: string) => {
477
+ expect(isEphemeralCaller({ ...VM, [key]: value })).toBe(true);
478
+ });
479
+
480
+ test.each([
481
+ "aws_ecs",
482
+ "aws_lambda",
483
+ "aws_app_runner",
484
+ "gcp_cloud_run",
485
+ "gcp_cloud_functions",
486
+ "gcp_app_engine",
487
+ "azure_container_apps",
488
+ "azure_container_instances",
489
+ "azure_functions",
490
+ ])("cloud.platform %s marks the caller ephemeral", (platform: string) => {
491
+ expect(isEphemeralCaller({ ...VM, "cloud.platform": platform })).toBe(true);
492
+ });
493
+
494
+ test("an EC2 / GCE VM stays stable", () => {
495
+ expect(isEphemeralCaller({ ...VM, "cloud.platform": "aws_ec2" })).toBe(
496
+ false,
497
+ );
498
+ });
499
+
500
+ test("no os.type → ephemeral (no system detector ran)", () => {
501
+ expect(isEphemeralCaller({ "host.name": "app-7" })).toBe(true);
502
+ expect(isEphemeralCaller({ "host.name": "app-7", "os.type": " " })).toBe(
503
+ true,
504
+ );
505
+ expect(isEphemeralCaller({})).toBe(true);
506
+ });
507
+
508
+ test("blank identity attributes do not count", () => {
509
+ expect(isEphemeralCaller({ ...VM, "k8s.pod.name": " " })).toBe(false);
510
+ expect(isEphemeralCaller({ ...VM, "container.id": "" })).toBe(false);
511
+ });
512
+
513
+ test("the stored resource.-prefixed spelling is honoured", () => {
514
+ expect(
515
+ isEphemeralCaller({
516
+ "resource.host.name": "app-7",
517
+ "resource.os.type": "linux",
518
+ }),
519
+ ).toBe(false);
520
+ expect(
521
+ isEphemeralCaller({
522
+ "resource.os.type": "linux",
523
+ "resource.k8s.pod.name": "p",
524
+ }),
525
+ ).toBe(true);
526
+ expect(
527
+ isEphemeralCaller({
528
+ "resource.os.type": "linux",
529
+ "resource.faas.name": "f",
530
+ }),
531
+ ).toBe(true);
532
+ });
533
+
534
+ test("buildDatabaseCallerContext reads namespace, cluster and host", () => {
535
+ expect(
536
+ buildDatabaseCallerContext({
537
+ "k8s.namespace.name": " shop ",
538
+ "k8s.cluster.name": "prod-eu",
539
+ "host.name": "checkout-7d9f",
540
+ }),
541
+ ).toEqual({
542
+ kubernetesNamespace: "shop",
543
+ kubernetesClusterName: "prod-eu",
544
+ hostName: "checkout-7d9f",
545
+ isEphemeral: true,
546
+ });
547
+ expect(buildDatabaseCallerContext(VM)).toEqual({
548
+ kubernetesNamespace: null,
549
+ kubernetesClusterName: null,
550
+ hostName: "app-7",
551
+ isEphemeral: false,
552
+ });
553
+ });
554
+ });
555
+
556
+ describe("canonicalizeDatabaseEndpoint — rule 1 (parse + port)", () => {
557
+ test("the engine default port is applied when none is given", () => {
558
+ expect(canonical("db.prod")).toEqual({ host: "db.prod", port: 5432 });
559
+ expect(canonical("cache.prod", { system: "redis" })).toEqual({
560
+ host: "cache.prod",
561
+ port: 6379,
562
+ });
563
+ expect(canonical("db.prod", { system: "postgres" })).toEqual({
564
+ host: "db.prod",
565
+ port: 5432,
566
+ });
567
+ });
568
+
569
+ test("an explicit port wins over the address port and the default", () => {
570
+ expect(canonical("db.prod:6543", { port: 5433 })).toEqual({
571
+ host: "db.prod",
572
+ port: 5433,
573
+ });
574
+ expect(canonical("db.prod", { port: "6543" })).toEqual({
575
+ host: "db.prod",
576
+ port: 6543,
577
+ });
578
+ expect(canonical("db.prod:6543")).toEqual({ host: "db.prod", port: 6543 });
579
+ });
580
+
581
+ test("an invalid explicit port falls back", () => {
582
+ expect(canonical("db.prod:6543", { port: "abc" })).toEqual({
583
+ host: "db.prod",
584
+ port: 6543,
585
+ });
586
+ expect(canonical("db.prod", { port: 0 })).toEqual({
587
+ host: "db.prod",
588
+ port: 5432,
589
+ });
590
+ expect(canonical("db.prod", { port: 5432.5 })).toEqual({
591
+ host: "db.prod",
592
+ port: 5432,
593
+ });
594
+ });
595
+
596
+ test("an unknown engine without a port keeps port null", () => {
597
+ expect(canonical("db.prod", { system: "acmedb" })).toEqual({
598
+ host: "db.prod",
599
+ port: null,
600
+ });
601
+ expect(canonical("db.prod", { system: "" })).toEqual({
602
+ host: "db.prod",
603
+ port: null,
604
+ });
605
+ });
606
+
607
+ test("unparseable addresses are null", () => {
608
+ expect(canonical(null)).toBeNull();
609
+ expect(canonical(undefined)).toBeNull();
610
+ expect(canonical("[REDACTED]")).toBeNull();
611
+ expect(canonical("***.***.***.***")).toBeNull();
612
+ });
613
+ });
614
+
615
+ describe("canonicalizeDatabaseEndpoint — rule 2 (loopback + host-relative)", () => {
616
+ const LOOPBACKS: Array<string> = [
617
+ "localhost",
618
+ "localhost:5432",
619
+ "127.0.0.1",
620
+ "::1",
621
+ "[::1]:5432",
622
+ "::",
623
+ "0.0.0.0",
624
+ "::ffff:127.0.0.1",
625
+ "(local)",
626
+ ".",
627
+ "db.localhost",
628
+ "localhost.localdomain",
629
+ "ip6-localhost",
630
+ ];
631
+ const HOST_RELATIVE: Array<string> = [
632
+ "host.docker.internal",
633
+ "host.containers.internal",
634
+ "gateway.docker.internal",
635
+ "docker.for.mac.localhost",
636
+ "kubernetes.docker.internal",
637
+ ];
638
+
639
+ test.each([...LOOPBACKS, ...HOST_RELATIVE])(
640
+ "client-call: %s is never an identity, whoever the caller is",
641
+ (address: string) => {
642
+ expect(canonical(address, { caller: VM_CALLER })).toBeNull();
643
+ expect(canonical(address, { caller: K8S_CALLER })).toBeNull();
644
+ expect(canonical(address, { caller: NO_CONTEXT })).toBeNull();
645
+ },
646
+ );
647
+
648
+ test.each([...LOOPBACKS, ...HOST_RELATIVE])(
649
+ "collector on a stable VM: %s is the VM's own host",
650
+ (address: string) => {
651
+ expect(
652
+ canonical(address, { caller: VM_CALLER, purpose: "collector" }),
653
+ ).toEqual({ host: "app-7.corp.example", port: 5432 });
654
+ },
655
+ );
656
+
657
+ test("collector: the address port survives the rewrite", () => {
658
+ expect(
659
+ canonical("localhost:6543", { caller: VM_CALLER, purpose: "collector" }),
660
+ ).toEqual({ host: "app-7.corp.example", port: 6543 });
661
+ });
662
+
663
+ test("collector: an ephemeral collector's loopback is null", () => {
664
+ expect(
665
+ canonical("localhost", { caller: K8S_CALLER, purpose: "collector" }),
666
+ ).toBeNull();
667
+ expect(
668
+ canonical("127.0.0.1", {
669
+ caller: { hostName: "3f9a1b2c4d5e", isEphemeral: true },
670
+ purpose: "collector",
671
+ }),
672
+ ).toBeNull();
673
+ });
674
+
675
+ test("collector: no host.name, or a loopback host.name, is null", () => {
676
+ expect(
677
+ canonical("localhost", {
678
+ caller: { isEphemeral: false },
679
+ purpose: "collector",
680
+ }),
681
+ ).toBeNull();
682
+ expect(
683
+ canonical("localhost", {
684
+ caller: { hostName: "localhost", isEphemeral: false },
685
+ purpose: "collector",
686
+ }),
687
+ ).toBeNull();
688
+ expect(
689
+ canonical("localhost", {
690
+ caller: { hostName: "[REDACTED]", isEphemeral: false },
691
+ purpose: "collector",
692
+ }),
693
+ ).toBeNull();
694
+ });
695
+
696
+ test("collector: the rewritten host is canonicalized", () => {
697
+ expect(
698
+ canonical("localhost", {
699
+ caller: { hostName: " DB-1.Corp. ", isEphemeral: false },
700
+ purpose: "collector",
701
+ }),
702
+ ).toEqual({ host: "db-1.corp", port: 5432 });
703
+ });
704
+
705
+ test("a non-loopback address is unaffected by the purpose", () => {
706
+ expect(
707
+ canonical("db.prod", { caller: VM_CALLER, purpose: "collector" }),
708
+ ).toEqual(canonical("db.prod", { caller: VM_CALLER }));
709
+ });
710
+ });
711
+
712
+ describe("canonicalizeDatabaseEndpoint — rule 3 (Kubernetes DNS)", () => {
713
+ test.each([
714
+ ["pg.shop.svc", "pg.shop.svc.cluster.local"],
715
+ ["pg.shop.svc.cluster.local", "pg.shop.svc.cluster.local"],
716
+ ["pg.shop.svc.cluster.local.", "pg.shop.svc.cluster.local"],
717
+ ["pg.shop.svc.corp.k8s", "pg.shop.svc.cluster.local"],
718
+ ["PG.Shop.SVC", "pg.shop.svc.cluster.local"],
719
+ // A StatefulSet member behind a headless Service.
720
+ ["pg-0.pg-hl.shop.svc", "pg-0.pg-hl.shop.svc.cluster.local"],
721
+ ["pg-0.pg-hl.shop.svc.corp.k8s", "pg-0.pg-hl.shop.svc.cluster.local"],
722
+ ])("%s → %s", (address: string, host: string) => {
723
+ expect(canonical(address, { caller: NO_CONTEXT })?.host).toBe(host);
724
+ });
725
+
726
+ test("a single-label name expands with the caller's namespace", () => {
727
+ expect(
728
+ canonical("pg", {
729
+ caller: { kubernetesNamespace: "Shop", isEphemeral: true },
730
+ }),
731
+ ).toEqual({ host: "pg.shop.svc.cluster.local", port: 5432 });
732
+ });
733
+
734
+ test("a single-label name without a namespace stays as is", () => {
735
+ expect(canonical("pg", { caller: VM_CALLER })).toEqual({
736
+ host: "pg",
737
+ port: 5432,
738
+ });
739
+ });
740
+
741
+ test("an invalid namespace does not produce a bogus FQDN", () => {
742
+ expect(
743
+ canonical("pg", {
744
+ caller: { kubernetesNamespace: "not a namespace", isEphemeral: true },
745
+ })?.host,
746
+ ).toBe("pg");
747
+ });
748
+
749
+ test("a two-label name from a Kubernetes caller is <service>.<namespace>", () => {
750
+ // A pod's resolver search path turns `pg.shop` into the Service FQDN.
751
+ expect(canonical("pg.shop", { caller: K8S_CALLER })).toEqual({
752
+ host: "pg.shop.svc.cluster.local",
753
+ port: 5432,
754
+ kubernetesClusterName: "prod-eu",
755
+ });
756
+ });
757
+
758
+ test("a two-label name from anything else is a domain and left alone", () => {
759
+ expect(canonical("pg.shop", { caller: VM_CALLER })).toEqual({
760
+ host: "pg.shop",
761
+ port: 5432,
762
+ });
763
+ expect(canonical("pg.shop", { caller: NO_CONTEXT })).toEqual({
764
+ host: "pg.shop",
765
+ port: 5432,
766
+ });
767
+ });
768
+
769
+ test("IP literals are never expanded", () => {
770
+ expect(
771
+ canonical("fd00::1", {
772
+ caller: { kubernetesNamespace: "shop", isEphemeral: true },
773
+ })?.host,
774
+ ).toBe("fd00::1");
775
+ });
776
+
777
+ test("a one-label-before-svc name is not a Service FQDN", () => {
778
+ expect(canonical("pg.svc", { caller: NO_CONTEXT })?.host).toBe("pg.svc");
779
+ });
780
+ });
781
+
782
+ describe("canonicalizeDatabaseEndpoint — rule 4 (cluster qualifier)", () => {
783
+ test("cluster-local names are qualified with the caller's cluster", () => {
784
+ expect(canonical("pg", { caller: K8S_CALLER })).toEqual({
785
+ host: "pg.shop.svc.cluster.local",
786
+ port: 5432,
787
+ kubernetesClusterName: "prod-eu",
788
+ });
789
+ expect(
790
+ canonical("pg.other.svc.cluster.local", { caller: K8S_CALLER }),
791
+ ).toEqual({
792
+ host: "pg.other.svc.cluster.local",
793
+ port: 5432,
794
+ kubernetesClusterName: "prod-eu",
795
+ });
796
+ });
797
+
798
+ test("private IPs are qualified (pod CIDRs overlap across clusters)", () => {
799
+ for (const address of [
800
+ "10.244.3.17",
801
+ "172.20.0.5",
802
+ "192.168.0.10",
803
+ "100.64.1.2",
804
+ "fd00::5",
805
+ "::ffff:10.244.3.17",
806
+ ]) {
807
+ expect(
808
+ canonical(address, { caller: K8S_CALLER })?.kubernetesClusterName,
809
+ ).toBe("prod-eu");
810
+ }
811
+ });
812
+
813
+ test("public names and public IPs are never qualified", () => {
814
+ for (const address of [
815
+ "mydb.abc123.us-east-1.rds.amazonaws.com",
816
+ "8.8.8.8",
817
+ "2001:db8::1",
818
+ ]) {
819
+ expect(
820
+ canonical(address, { caller: K8S_CALLER })?.kubernetesClusterName,
821
+ ).toBeUndefined();
822
+ }
823
+ // A two-label name is only public when the caller is not in Kubernetes.
824
+ expect(
825
+ canonical("pg.shop", {
826
+ caller: { ...VM_CALLER, kubernetesClusterName: null },
827
+ })?.kubernetesClusterName,
828
+ ).toBeUndefined();
829
+ });
830
+
831
+ test("the cluster is canonicalized but otherwise kept raw (ARNs)", () => {
832
+ const arn: string = "arn:aws:eks:us-east-1:123456789012:cluster/Prod-EU";
833
+ expect(
834
+ canonical("10.0.0.5", {
835
+ caller: { kubernetesClusterName: ` ${arn} `, isEphemeral: true },
836
+ })?.kubernetesClusterName,
837
+ ).toBe(arn.toLowerCase());
838
+ });
839
+
840
+ test("no cluster on the caller → no qualifier (and no empty one)", () => {
841
+ const endpoint: DatabaseEndpoint | null = canonical("10.0.0.5", {
842
+ caller: { kubernetesClusterName: " ", isEphemeral: true },
843
+ });
844
+ expect(endpoint).toEqual({ host: "10.0.0.5", port: 5432 });
845
+ expect(Object.keys(endpoint!)).not.toContain("kubernetesClusterName");
846
+ });
847
+ });
848
+
849
+ describe("getDatabaseEndpointScope", () => {
850
+ test.each([
851
+ [{ host: "db.prod.example.com", port: 5432 }, "global"],
852
+ [{ host: "8.8.8.8", port: 5432 }, "global"],
853
+ [{ host: "2001:db8::1", port: 5432 }, "global"],
854
+ [
855
+ {
856
+ host: "pg.shop.svc.cluster.local",
857
+ port: 5432,
858
+ kubernetesClusterName: "prod",
859
+ },
860
+ "global",
861
+ ],
862
+ [{ host: "10.0.0.5", port: 5432, kubernetesClusterName: "prod" }, "global"],
863
+ [{ host: "pg.shop", port: 5432 }, "global"],
864
+ [{ host: "postgres", port: 5432 }, "local"],
865
+ [{ host: "db1", port: 5432 }, "local"],
866
+ [{ host: "pg.shop.svc.cluster.local", port: 5432 }, "local"],
867
+ [
868
+ {
869
+ host: "pg.shop.svc.cluster.local",
870
+ port: 5432,
871
+ kubernetesClusterName: " ",
872
+ },
873
+ "local",
874
+ ],
875
+ [{ host: "10.0.0.5", port: 5432 }, "local"],
876
+ [{ host: "fd00::5", port: 5432 }, "local"],
877
+ [{ host: "100.64.0.1", port: 5432 }, "local"],
878
+ [{ host: "localhost", port: 5432 }, "local"],
879
+ [{ host: "127.0.0.1", port: 5432 }, "local"],
880
+ [{ host: "host.docker.internal", port: 5432 }, "local"],
881
+ [{ host: "", port: 5432 }, "local"],
882
+ ])("%j is %s", (endpoint: DatabaseEndpoint, scope: string) => {
883
+ expect(getDatabaseEndpointScope(endpoint)).toBe(scope);
884
+ });
885
+ });
886
+
887
+ describe("formatDatabaseEndpoint", () => {
888
+ test.each([
889
+ [{ host: "db.prod", port: 5432 }, "db.prod:5432"],
890
+ [{ host: "db.prod", port: null }, "db.prod"],
891
+ [{ host: "DB.Prod", port: 5432 }, "db.prod:5432"],
892
+ [{ host: "fd00::5", port: 5432 }, "[fd00::5]:5432"],
893
+ [{ host: "fd00::5", port: null }, "[fd00::5]"],
894
+ [
895
+ {
896
+ host: "pg.shop.svc.cluster.local",
897
+ port: 5432,
898
+ kubernetesClusterName: " Prod-EU ",
899
+ },
900
+ "pg.shop.svc.cluster.local:5432@prod-eu",
901
+ ],
902
+ [
903
+ { host: "10.0.0.5", port: 5432, kubernetesClusterName: "" },
904
+ "10.0.0.5:5432",
905
+ ],
906
+ [{ host: "db.prod", port: 0 }, "db.prod"],
907
+ ])("%j → %s", (endpoint: DatabaseEndpoint, formatted: string) => {
908
+ expect(formatDatabaseEndpoint(endpoint)).toBe(formatted);
909
+ });
910
+ });
911
+
912
+ describe("parseDatabaseEndpointString", () => {
913
+ const PG: { system: string } = { system: "postgresql" };
914
+
915
+ test("round-trips everything canonicalize produces", () => {
916
+ const endpoints: Array<DatabaseEndpoint | null> = [
917
+ canonical("db.prod.example.com"),
918
+ canonical("db.prod.example.com:6543"),
919
+ canonical("pg", { caller: K8S_CALLER }),
920
+ canonical("10.244.3.17", { caller: K8S_CALLER }),
921
+ canonical("fd00::5", { caller: K8S_CALLER }),
922
+ canonical("2001:db8::1:0:0:1"),
923
+ canonical("db.prod", { system: "acmedb" }),
924
+ canonical("pg.shop.svc.cluster.local"),
925
+ ];
926
+ for (const endpoint of endpoints) {
927
+ expect(endpoint).not.toBeNull();
928
+ expect(
929
+ parseDatabaseEndpointString(formatDatabaseEndpoint(endpoint!), {
930
+ system: endpoint!.port === null ? "acmedb" : "postgresql",
931
+ }),
932
+ ).toEqual(endpoint);
933
+ }
934
+ });
935
+
936
+ test("an ARN-like cluster name round-trips", () => {
937
+ const endpoint: DatabaseEndpoint = {
938
+ host: "pg.shop.svc.cluster.local",
939
+ port: 5432,
940
+ kubernetesClusterName:
941
+ "arn:aws:eks:us-east-1:123456789012:cluster/prod-eu",
942
+ };
943
+ const formatted: string = formatDatabaseEndpoint(endpoint);
944
+ expect(formatted).toBe(
945
+ "pg.shop.svc.cluster.local:5432@arn:aws:eks:us-east-1:123456789012:cluster/prod-eu",
946
+ );
947
+ expect(parseDatabaseEndpointString(formatted, PG)).toEqual(endpoint);
948
+ });
949
+
950
+ test("the cluster splits on the LAST @", () => {
951
+ expect(
952
+ parseDatabaseEndpointString("10.0.0.5:5432@team@prod", PG),
953
+ ).toBeNull();
954
+ expect(
955
+ parseDatabaseEndpointString("10.0.0.5:5432@prod", PG)
956
+ ?.kubernetesClusterName,
957
+ ).toBe("prod");
958
+ });
959
+
960
+ test("a qualifier on a host that resolves the same everywhere is refused, not dropped", () => {
961
+ expect(
962
+ parseDatabaseEndpointString("db.prod.example.com:5432@prod", PG),
963
+ ).toBeNull();
964
+ expect(parseDatabaseEndpointString("8.8.8.8@prod", PG)).toBeNull();
965
+ expect(parseDatabaseEndpointString("2001:db8::1@prod", PG)).toBeNull();
966
+ });
967
+
968
+ test("an empty qualifier is no qualifier", () => {
969
+ expect(parseDatabaseEndpointString("10.0.0.5:5432@", PG)).toEqual({
970
+ host: "10.0.0.5",
971
+ port: 5432,
972
+ });
973
+ });
974
+
975
+ test("URL forms: the @ is userinfo, never a cluster", () => {
976
+ expect(
977
+ parseDatabaseEndpointString(
978
+ "postgres://admin@pg.shop.svc.cluster.local:5432/app",
979
+ { ...PG, kubernetesClusterName: "prod" },
980
+ ),
981
+ ).toEqual({
982
+ host: "pg.shop.svc.cluster.local",
983
+ port: 5432,
984
+ kubernetesClusterName: "prod",
985
+ });
986
+ });
987
+
988
+ test("row context: a bare name on a Kubernetes row expands and qualifies", () => {
989
+ expect(
990
+ parseDatabaseEndpointString("pg:5432", {
991
+ system: "postgresql",
992
+ kubernetesNamespace: "shop",
993
+ kubernetesClusterName: "Prod-EU",
994
+ }),
995
+ ).toEqual({
996
+ host: "pg.shop.svc.cluster.local",
997
+ port: 5432,
998
+ kubernetesClusterName: "prod-eu",
999
+ });
1000
+ });
1001
+
1002
+ test("row context: .svc forms expand, and the row cluster fills in", () => {
1003
+ expect(
1004
+ parseDatabaseEndpointString("pg.shop.svc", {
1005
+ system: "postgresql",
1006
+ kubernetesClusterName: "prod",
1007
+ }),
1008
+ ).toEqual({
1009
+ host: "pg.shop.svc.cluster.local",
1010
+ port: 5432,
1011
+ kubernetesClusterName: "prod",
1012
+ });
1013
+ });
1014
+
1015
+ test("an explicit qualifier wins over the row's cluster", () => {
1016
+ expect(
1017
+ parseDatabaseEndpointString("10.0.0.5@staging", {
1018
+ system: "postgresql",
1019
+ kubernetesClusterName: "prod",
1020
+ })?.kubernetesClusterName,
1021
+ ).toBe("staging");
1022
+ });
1023
+
1024
+ test("a bare name without row context stays single-label", () => {
1025
+ expect(parseDatabaseEndpointString("postgres", PG)).toEqual({
1026
+ host: "postgres",
1027
+ port: 5432,
1028
+ });
1029
+ });
1030
+
1031
+ test("the port defaults from the row's engine", () => {
1032
+ expect(
1033
+ parseDatabaseEndpointString("cache.prod", { system: "redis" }),
1034
+ ).toEqual({ host: "cache.prod", port: 6379 });
1035
+ expect(parseDatabaseEndpointString("cache.prod", { system: "" })).toEqual({
1036
+ host: "cache.prod",
1037
+ port: null,
1038
+ });
1039
+ });
1040
+
1041
+ test("loopback, host-relative and garbage are null", () => {
1042
+ for (const value of [
1043
+ "localhost:5432",
1044
+ "localhost.localdomain",
1045
+ "127.0.0.1",
1046
+ "[::1]:5432",
1047
+ "host.docker.internal",
1048
+ "(local)",
1049
+ "[REDACTED]",
1050
+ "***.***.***.***@prod",
1051
+ "",
1052
+ " ",
1053
+ "@prod",
1054
+ ]) {
1055
+ expect(parseDatabaseEndpointString(value, PG)).toBeNull();
1056
+ }
1057
+ expect(parseDatabaseEndpointString(null, PG)).toBeNull();
1058
+ expect(parseDatabaseEndpointString(42, PG)).toBeNull();
1059
+ });
1060
+
1061
+ test("a non-URL user@host is refused — never read as host@qualifier", () => {
1062
+ // The finding's probes: these used to become hosts "user" and "admin".
1063
+ expect(parseDatabaseEndpointString("user@db.example.com", PG)).toBeNull();
1064
+ expect(parseDatabaseEndpointString("admin@10.0.0.5:5432", PG)).toBeNull();
1065
+ expect(parseDatabaseEndpointString("admin@db:5432", PG)).toBeNull();
1066
+ // Even on a Kubernetes row, whose namespace would expand "admin".
1067
+ expect(
1068
+ parseDatabaseEndpointString("admin@10.0.0.5:5432", {
1069
+ system: "postgresql",
1070
+ kubernetesNamespace: "shop",
1071
+ kubernetesClusterName: "prod",
1072
+ }),
1073
+ ).toBeNull();
1074
+ expect(
1075
+ parseDatabaseEndpointString("admin@db.example.com", {
1076
+ system: "postgresql",
1077
+ kubernetesNamespace: "shop",
1078
+ }),
1079
+ ).toBeNull();
1080
+ // A dotted user name is not read as a <service>.<namespace> either.
1081
+ expect(
1082
+ parseDatabaseEndpointString("john.doe@db.example.com", PG),
1083
+ ).toBeNull();
1084
+ expect(
1085
+ parseDatabaseEndpointString("postgres.data@db.example.com", PG),
1086
+ ).toBeNull();
1087
+ });
1088
+
1089
+ test("an address that takes a qualifier keeps it, whatever the cluster name looks like", () => {
1090
+ for (const [value, host] of [
1091
+ ["10.0.0.5@prod.eu-west-1", "10.0.0.5"],
1092
+ ["pg.shop.svc.cluster.local@prod.eu-west-1", "pg.shop.svc.cluster.local"],
1093
+ ["pg.shop.svc@prod.eu-west-1", "pg.shop.svc.cluster.local"],
1094
+ ["db.internal@prod.eu-west-1", "db.internal"],
1095
+ ["postgres.data:5432@prod.eu-west-1", "postgres.data.svc.cluster.local"],
1096
+ ] as Array<[string, string]>) {
1097
+ const endpoint: DatabaseEndpoint | null = parseDatabaseEndpointString(
1098
+ value,
1099
+ PG,
1100
+ );
1101
+ expect(endpoint?.host).toBe(host);
1102
+ expect(endpoint?.kubernetesClusterName).toBe("prod.eu-west-1");
1103
+ }
1104
+ // An EKS ARN never reads as a server.
1105
+ expect(
1106
+ parseDatabaseEndpointString(
1107
+ "postgres.data@arn:aws:eks:us-east-1:1:cluster/prod",
1108
+ PG,
1109
+ )?.kubernetesClusterName,
1110
+ ).toBe("arn:aws:eks:us-east-1:1:cluster/prod");
1111
+ });
1112
+ });
1113
+
1114
+ describe("buildDatabaseServerIdentifier", () => {
1115
+ test("system|formatted endpoint, system normalized", () => {
1116
+ expect(
1117
+ buildDatabaseServerIdentifier("postgres", {
1118
+ host: "db.prod",
1119
+ port: 5432,
1120
+ }),
1121
+ ).toBe("postgresql|db.prod:5432");
1122
+ expect(
1123
+ buildDatabaseServerIdentifier("postgresql", {
1124
+ host: "pg.shop.svc.cluster.local",
1125
+ port: 5432,
1126
+ kubernetesClusterName: "prod",
1127
+ }),
1128
+ ).toBe("postgresql|pg.shop.svc.cluster.local:5432@prod");
1129
+ });
1130
+
1131
+ test("cluster-qualified and unqualified endpoints are different rows", () => {
1132
+ expect(
1133
+ buildDatabaseServerIdentifier("postgresql", {
1134
+ host: "10.0.0.5",
1135
+ port: 5432,
1136
+ kubernetesClusterName: "prod",
1137
+ }),
1138
+ ).not.toBe(
1139
+ buildDatabaseServerIdentifier("postgresql", {
1140
+ host: "10.0.0.5",
1141
+ port: 5432,
1142
+ }),
1143
+ );
1144
+ });
1145
+
1146
+ /*
1147
+ * Regression (fork-engines-lose-identity): forks are engines of their own,
1148
+ * so an identifier keyed on the engine re-keyed a row the moment a
1149
+ * stronger source refined it ("mysql" → "mariadb") and let a racing
1150
+ * writer that saw the other spelling create a second row for one server.
1151
+ */
1152
+ test("keys on the engine family: a fork and the engine it forks are one row", () => {
1153
+ const endpoint: DatabaseEndpoint = { host: "db.prod", port: 3306 };
1154
+ const mysql: string = buildDatabaseServerIdentifier("mysql", endpoint);
1155
+
1156
+ expect(mysql).toBe("mysql|db.prod:3306");
1157
+ for (const fork of ["mariadb", "MariaDB", " mariadb ", "tidb", "vitess"]) {
1158
+ expect(buildDatabaseServerIdentifier(fork, endpoint)).toBe(mysql);
1159
+ }
1160
+
1161
+ const cache: DatabaseEndpoint = { host: "cache.prod", port: 6379 };
1162
+ for (const fork of ["redis", "valkey", "keydb", "dragonfly"]) {
1163
+ expect(buildDatabaseServerIdentifier(fork, cache)).toBe(
1164
+ "redis|cache.prod:6379",
1165
+ );
1166
+ }
1167
+
1168
+ const pg: DatabaseEndpoint = { host: "crdb.prod", port: 26257 };
1169
+ expect(buildDatabaseServerIdentifier("cockroachdb", pg)).toBe(
1170
+ buildDatabaseServerIdentifier("postgres", pg),
1171
+ );
1172
+ });
1173
+
1174
+ test("engines of different families still key apart on one endpoint", () => {
1175
+ const endpoint: DatabaseEndpoint = { host: "db.prod", port: 5432 };
1176
+ const identifiers: Set<string> = new Set<string>(
1177
+ ["postgresql", "mysql", "redis", "mongodb", "microsoft.sql_server"].map(
1178
+ (system: string): string => {
1179
+ return buildDatabaseServerIdentifier(system, endpoint);
1180
+ },
1181
+ ),
1182
+ );
1183
+ expect(identifiers.size).toBe(5);
1184
+ });
1185
+
1186
+ test("an unknown engine is a family of its own, canonicalized", () => {
1187
+ const endpoint: DatabaseEndpoint = { host: "db.prod", port: 7000 };
1188
+ expect(buildDatabaseServerIdentifier(" AcmeDB ", endpoint)).toBe(
1189
+ "acmedb|db.prod:7000",
1190
+ );
1191
+ expect(buildDatabaseServerIdentifier("", endpoint)).toBe("|db.prod:7000");
1192
+ });
1193
+ });
1194
+
1195
+ describe("buildWorkloadDatabaseServerIdentifier", () => {
1196
+ test("kubernetes: cluster/namespace/kind/name, lowercased", () => {
1197
+ expect(
1198
+ buildWorkloadDatabaseServerIdentifier({
1199
+ system: "postgres",
1200
+ platform: "kubernetes",
1201
+ parentName: "Prod-EU",
1202
+ namespace: "Shop",
1203
+ workloadKind: "Cluster",
1204
+ workloadName: "PG-Main",
1205
+ }),
1206
+ ).toBe("postgresql|kubernetes:prod-eu/shop/cluster/pg-main");
1207
+ });
1208
+
1209
+ test("kubernetes: missing namespace / kind leave empty segments", () => {
1210
+ expect(
1211
+ buildWorkloadDatabaseServerIdentifier({
1212
+ system: "redis",
1213
+ platform: "kubernetes",
1214
+ parentName: "prod",
1215
+ workloadName: "cache",
1216
+ }),
1217
+ ).toBe("redis|kubernetes:prod///cache");
1218
+ });
1219
+
1220
+ test("docker / podman: no kind, so compose labels cannot change identity", () => {
1221
+ const base: {
1222
+ system: string;
1223
+ parentName: string;
1224
+ workloadName: string;
1225
+ } = {
1226
+ system: "mysql",
1227
+ parentName: "Build-Host-1",
1228
+ workloadName: "shop-db",
1229
+ };
1230
+
1231
+ expect(
1232
+ buildWorkloadDatabaseServerIdentifier({ ...base, platform: "docker" }),
1233
+ ).toBe("mysql|docker:build-host-1/shop-db");
1234
+ expect(
1235
+ buildWorkloadDatabaseServerIdentifier({
1236
+ ...base,
1237
+ platform: "docker",
1238
+ workloadKind: "ComposeService",
1239
+ namespace: "shop",
1240
+ }),
1241
+ ).toBe("mysql|docker:build-host-1/shop-db");
1242
+ expect(
1243
+ buildWorkloadDatabaseServerIdentifier({ ...base, platform: "podman" }),
1244
+ ).toBe("mysql|podman:build-host-1/shop-db");
1245
+ });
1246
+
1247
+ /*
1248
+ * Regression (fork-engines-lose-identity): a workload the classifier
1249
+ * re-reads as another engine of its family (the image moved from Redis to
1250
+ * Valkey, a MySQL image turned out to be MariaDB) is the same database; an
1251
+ * engine-keyed identifier turned it into two rows.
1252
+ */
1253
+ test("keys on the engine family: a re-classified workload keeps its identifier", () => {
1254
+ const kubernetes: (system: string) => string = (system: string): string => {
1255
+ return buildWorkloadDatabaseServerIdentifier({
1256
+ system: system,
1257
+ platform: "kubernetes",
1258
+ parentName: "prod",
1259
+ namespace: "cache",
1260
+ workloadKind: "StatefulSet",
1261
+ workloadName: "redis",
1262
+ });
1263
+ };
1264
+
1265
+ expect(kubernetes("redis")).toBe(
1266
+ "redis|kubernetes:prod/cache/statefulset/redis",
1267
+ );
1268
+ for (const fork of ["valkey", "Valkey", "keydb", "dragonfly"]) {
1269
+ expect(kubernetes(fork)).toBe(kubernetes("redis"));
1270
+ }
1271
+
1272
+ for (const platform of ["docker", "podman"] as const) {
1273
+ const container: (system: string) => string = (
1274
+ system: string,
1275
+ ): string => {
1276
+ return buildWorkloadDatabaseServerIdentifier({
1277
+ system: system,
1278
+ platform: platform,
1279
+ parentName: "host-1",
1280
+ workloadName: "shop-db",
1281
+ });
1282
+ };
1283
+ expect(container("mariadb")).toBe(`mysql|${platform}:host-1/shop-db`);
1284
+ expect(container("mysql")).toBe(container("mariadb"));
1285
+ expect(container("postgresql")).not.toBe(container("mariadb"));
1286
+ }
1287
+ });
1288
+
1289
+ test("every catalogued engine and alias keys both identifiers on its family", () => {
1290
+ const endpoint: DatabaseEndpoint = { host: "db.example.com", port: 1 };
1291
+ for (const descriptor of DATABASE_SYSTEMS) {
1292
+ const family: string = descriptor.family || descriptor.system;
1293
+ for (const name of [descriptor.system, ...descriptor.aliases]) {
1294
+ expect(buildDatabaseServerIdentifier(name, endpoint)).toBe(
1295
+ `${family}|db.example.com:1`,
1296
+ );
1297
+ expect(
1298
+ buildWorkloadDatabaseServerIdentifier({
1299
+ system: name,
1300
+ platform: "docker",
1301
+ parentName: "h",
1302
+ workloadName: "w",
1303
+ }),
1304
+ ).toBe(`${family}|docker:h/w`);
1305
+ }
1306
+ }
1307
+ });
1308
+
1309
+ test("workloads of different families stay apart", () => {
1310
+ const identifier: (system: string) => string = (system: string): string => {
1311
+ return buildWorkloadDatabaseServerIdentifier({
1312
+ system: system,
1313
+ platform: "kubernetes",
1314
+ parentName: "prod",
1315
+ namespace: "data",
1316
+ workloadKind: "StatefulSet",
1317
+ workloadName: "db",
1318
+ });
1319
+ };
1320
+ expect(identifier("postgresql")).not.toBe(identifier("mysql"));
1321
+ expect(identifier("cockroachdb")).toBe(identifier("postgresql"));
1322
+ expect(identifier("opensearch")).toBe(
1323
+ "elasticsearch|kubernetes:prod/data/statefulset/db",
1324
+ );
1325
+ });
1326
+ });
1327
+
1328
+ describe("buildDatabaseServerDisplayName", () => {
1329
+ test("engine + host:port, never the cluster qualifier", () => {
1330
+ expect(
1331
+ buildDatabaseServerDisplayName({
1332
+ system: "postgresql",
1333
+ endpoint: {
1334
+ host: "pg.shop.svc.cluster.local",
1335
+ port: 5432,
1336
+ kubernetesClusterName: "prod",
1337
+ },
1338
+ }),
1339
+ ).toBe("PostgreSQL pg.shop.svc.cluster.local:5432");
1340
+ expect(
1341
+ buildDatabaseServerDisplayName({
1342
+ system: "redis",
1343
+ endpoint: { host: "fd00::5", port: 6379 },
1344
+ }),
1345
+ ).toBe("Redis [fd00::5]:6379");
1346
+ });
1347
+
1348
+ test("engine + namespace/workload, or the workload alone", () => {
1349
+ expect(
1350
+ buildDatabaseServerDisplayName({
1351
+ system: "mongodb",
1352
+ namespace: "shop",
1353
+ workloadName: "orders-db",
1354
+ }),
1355
+ ).toBe("MongoDB shop/orders-db");
1356
+ expect(
1357
+ buildDatabaseServerDisplayName({
1358
+ system: "mongodb",
1359
+ workloadName: "orders-db",
1360
+ }),
1361
+ ).toBe("MongoDB orders-db");
1362
+ });
1363
+
1364
+ test("the endpoint wins over the workload", () => {
1365
+ expect(
1366
+ buildDatabaseServerDisplayName({
1367
+ system: "mysql",
1368
+ endpoint: { host: "db.prod", port: 3306 },
1369
+ workloadName: "ignored",
1370
+ }),
1371
+ ).toBe("MySQL db.prod:3306");
1372
+ });
1373
+
1374
+ test("nothing to name → just the engine; unknown engine → raw", () => {
1375
+ expect(buildDatabaseServerDisplayName({ system: "postgresql" })).toBe(
1376
+ "PostgreSQL",
1377
+ );
1378
+ expect(buildDatabaseServerDisplayName({ system: "" })).toBe("Database");
1379
+ expect(
1380
+ buildDatabaseServerDisplayName({
1381
+ system: "acmedb",
1382
+ endpoint: { host: "acme.prod", port: 4000 },
1383
+ }),
1384
+ ).toBe("acmedb acme.prod:4000");
1385
+ });
1386
+
1387
+ test("is capped at 100 characters, keeping the engine", () => {
1388
+ const name: string = buildDatabaseServerDisplayName({
1389
+ system: "postgresql",
1390
+ endpoint: { host: `${"a".repeat(60)}.${"b".repeat(60)}.com`, port: 5432 },
1391
+ });
1392
+ expect(name.length).toBeLessThanOrEqual(100);
1393
+ expect(name.startsWith("PostgreSQL ")).toBe(true);
1394
+ });
1395
+ });
1396
+
1397
+ describe("buildKubernetesDatabaseAliases", () => {
1398
+ test("each Service × port, qualified; the engine default port is always included", () => {
1399
+ expect(
1400
+ buildKubernetesDatabaseAliases({
1401
+ system: "postgresql",
1402
+ namespace: "shop",
1403
+ clusterName: "prod",
1404
+ serviceNames: ["pg-rw", "pg-ro"],
1405
+ ports: [5432],
1406
+ includeUnqualified: false,
1407
+ }),
1408
+ ).toEqual([
1409
+ "pg-rw.shop.svc.cluster.local:5432@prod",
1410
+ "pg-ro.shop.svc.cluster.local:5432@prod",
1411
+ ]);
1412
+ });
1413
+
1414
+ test("the unqualified twin only when asked (single-cluster projects)", () => {
1415
+ expect(
1416
+ buildKubernetesDatabaseAliases({
1417
+ system: "redis",
1418
+ namespace: "cache",
1419
+ clusterName: "Prod",
1420
+ serviceNames: ["redis-master"],
1421
+ ports: [],
1422
+ includeUnqualified: true,
1423
+ }),
1424
+ ).toEqual([
1425
+ "redis-master.cache.svc.cluster.local:6379@prod",
1426
+ "redis-master.cache.svc.cluster.local:6379",
1427
+ ]);
1428
+ });
1429
+
1430
+ test("declared container ports are added after the default, deduped", () => {
1431
+ expect(
1432
+ buildKubernetesDatabaseAliases({
1433
+ system: "postgresql",
1434
+ namespace: "shop",
1435
+ clusterName: "prod",
1436
+ serviceNames: ["pg"],
1437
+ ports: [5432, 6432, 6432, 0, 70000],
1438
+ includeUnqualified: false,
1439
+ }),
1440
+ ).toEqual([
1441
+ "pg.shop.svc.cluster.local:5432@prod",
1442
+ "pg.shop.svc.cluster.local:6432@prod",
1443
+ ]);
1444
+ });
1445
+
1446
+ test("per-member headless DNS for StatefulSet / operator members", () => {
1447
+ expect(
1448
+ buildKubernetesDatabaseAliases({
1449
+ system: "mongodb",
1450
+ namespace: "data",
1451
+ clusterName: "prod",
1452
+ serviceNames: ["mongo"],
1453
+ podServiceNames: [
1454
+ { podName: "mongo-0", serviceName: "mongo-headless" },
1455
+ { podName: "mongo-1", serviceName: "mongo-headless" },
1456
+ ],
1457
+ ports: [27017],
1458
+ includeUnqualified: false,
1459
+ }),
1460
+ ).toEqual([
1461
+ "mongo.data.svc.cluster.local:27017@prod",
1462
+ "mongo-0.mongo-headless.data.svc.cluster.local:27017@prod",
1463
+ "mongo-1.mongo-headless.data.svc.cluster.local:27017@prod",
1464
+ ]);
1465
+ });
1466
+
1467
+ test("never emits two-label svc.ns forms", () => {
1468
+ const aliases: Array<string> = buildKubernetesDatabaseAliases({
1469
+ system: "postgresql",
1470
+ namespace: "shop",
1471
+ clusterName: "prod",
1472
+ serviceNames: ["pg"],
1473
+ ports: [],
1474
+ includeUnqualified: true,
1475
+ });
1476
+ for (const alias of aliases) {
1477
+ expect(alias.startsWith("pg.shop:")).toBe(false);
1478
+ expect(alias).toContain(".svc.cluster.local");
1479
+ }
1480
+ });
1481
+
1482
+ test("invalid names are skipped; duplicates collapse", () => {
1483
+ expect(
1484
+ buildKubernetesDatabaseAliases({
1485
+ system: "postgresql",
1486
+ namespace: "shop",
1487
+ clusterName: "prod",
1488
+ serviceNames: ["PG", "pg", "bad name", "", "-pg"],
1489
+ podServiceNames: [{ podName: "pg-0", serviceName: "" }],
1490
+ ports: [],
1491
+ includeUnqualified: false,
1492
+ }),
1493
+ ).toEqual(["pg.shop.svc.cluster.local:5432@prod"]);
1494
+ });
1495
+
1496
+ test("an invalid namespace produces nothing", () => {
1497
+ expect(
1498
+ buildKubernetesDatabaseAliases({
1499
+ system: "postgresql",
1500
+ namespace: "Not A Namespace",
1501
+ clusterName: "prod",
1502
+ serviceNames: ["pg"],
1503
+ ports: [],
1504
+ includeUnqualified: true,
1505
+ }),
1506
+ ).toEqual([]);
1507
+ });
1508
+
1509
+ test("a blank cluster can only produce the unqualified form", () => {
1510
+ expect(
1511
+ buildKubernetesDatabaseAliases({
1512
+ system: "postgresql",
1513
+ namespace: "shop",
1514
+ clusterName: " ",
1515
+ serviceNames: ["pg"],
1516
+ ports: [],
1517
+ includeUnqualified: false,
1518
+ }),
1519
+ ).toEqual(["pg.shop.svc.cluster.local:5432"]);
1520
+ });
1521
+
1522
+ test("an engine without a default port and no declared port → port-less", () => {
1523
+ expect(
1524
+ buildKubernetesDatabaseAliases({
1525
+ system: "acmedb",
1526
+ namespace: "shop",
1527
+ clusterName: "prod",
1528
+ serviceNames: ["acme"],
1529
+ ports: [],
1530
+ includeUnqualified: false,
1531
+ }),
1532
+ ).toEqual(["acme.shop.svc.cluster.local@prod"]);
1533
+ });
1534
+
1535
+ test("every alias parses back to itself", () => {
1536
+ const aliases: Array<string> = buildKubernetesDatabaseAliases({
1537
+ system: "postgresql",
1538
+ namespace: "shop",
1539
+ clusterName: "arn:aws:eks:us-east-1:1:cluster/prod",
1540
+ serviceNames: ["pg-rw"],
1541
+ podServiceNames: [{ podName: "pg-1", serviceName: "pg" }],
1542
+ ports: [6432],
1543
+ includeUnqualified: true,
1544
+ });
1545
+ expect(aliases.length).toBe(8);
1546
+ for (const alias of aliases) {
1547
+ const parsed: DatabaseEndpoint | null = parseDatabaseEndpointString(
1548
+ alias,
1549
+ { system: "postgresql" },
1550
+ );
1551
+ expect(parsed).not.toBeNull();
1552
+ expect(formatDatabaseEndpoint(parsed!)).toBe(alias);
1553
+ }
1554
+ });
1555
+
1556
+ test("the alias a k8s caller's span canonicalizes to is in the set", () => {
1557
+ const aliases: Array<string> = buildKubernetesDatabaseAliases({
1558
+ system: "postgresql",
1559
+ namespace: "shop",
1560
+ clusterName: "prod-eu",
1561
+ serviceNames: ["pg"],
1562
+ ports: [],
1563
+ includeUnqualified: false,
1564
+ });
1565
+ const fromSpan: DatabaseEndpoint | null = canonical("pg", {
1566
+ caller: K8S_CALLER,
1567
+ });
1568
+ expect(aliases).toContain(formatDatabaseEndpoint(fromSpan!));
1569
+ });
1570
+ });
1571
+
1572
+ describe("parse → canonicalize consistency", () => {
1573
+ test("the parsed host of every accepted shape is itself parseable", () => {
1574
+ for (const raw of [
1575
+ "db.prod:5432",
1576
+ "[fd00::5]:5432",
1577
+ "tcp:sql.prod,1433",
1578
+ "postgres://u:p@db.prod/x",
1579
+ "::ffff:10.0.0.5",
1580
+ ]) {
1581
+ const parsed: ParsedHostAndPort | null = parseHostAndPort(raw);
1582
+ expect(parsed).not.toBeNull();
1583
+ expect(parseHostAndPort(parsed!.host)?.host).toBe(parsed!.host);
1584
+ }
1585
+ });
1586
+ });
1587
+
1588
+ describe("hostile input stays linear", () => {
1589
+ /*
1590
+ * Hosts come straight from telemetry. Each of these inputs made the old
1591
+ * trailing-dot regexes (/\.+$/) backtrack quadratically; the parsers must
1592
+ * answer in well under a second.
1593
+ */
1594
+ test("long runs of dots that do not end the host", () => {
1595
+ const dots: string = ".".repeat(200_000);
1596
+ const started: number = performance.now();
1597
+
1598
+ expect(parseHostAndPort("a" + dots + "b")).toBeNull();
1599
+ expect(isLoopbackDatabaseHost("a" + dots + "b")).toBe(false);
1600
+ expect(isHostRelativeDatabaseHost("a" + dots + "b")).toBe(false);
1601
+ expect(parseHostAndPort("db" + dots + ":5432")).toEqual({
1602
+ host: "db",
1603
+ port: 5432,
1604
+ });
1605
+
1606
+ expect(performance.now() - started).toBeLessThan(1000);
1607
+ });
1608
+
1609
+ test("trailing dots are still dropped", () => {
1610
+ expect(parseHostAndPort("db.prod.:5432")).toEqual({
1611
+ host: "db.prod",
1612
+ port: 5432,
1613
+ });
1614
+ expect(isLoopbackDatabaseHost("localhost.")).toBe(true);
1615
+ expect(isHostRelativeDatabaseHost("host.docker.internal.")).toBe(true);
1616
+ });
1617
+ });
1618
+
1619
+ /*
1620
+ * ---- Audit regressions (lane B1: endpoint identity) -------------------------
1621
+ *
1622
+ * Each block below first reproduces a failure the audit found, then pins the
1623
+ * rule that fixes it.
1624
+ */
1625
+
1626
+ const PG_SYSTEM: string = "postgresql";
1627
+ const MSSQL: string = "microsoft.sql_server";
1628
+ const PROJECT: string = "8a4c5b1e-2b3f-4c1d-9e8f-1a2b3c4d5e6f";
1629
+
1630
+ function key(endpoint: DatabaseEndpoint | null): string | null {
1631
+ return endpoint ? keyForDatabaseEndpoint(PROJECT, endpoint) : null;
1632
+ }
1633
+
1634
+ function pod(
1635
+ namespace: string | null,
1636
+ cluster: string | null,
1637
+ ): DatabaseCallerContext {
1638
+ return {
1639
+ kubernetesNamespace: namespace,
1640
+ kubernetesClusterName: cluster,
1641
+ hostName: "api-7d9f-abcde",
1642
+ isEphemeral: true,
1643
+ };
1644
+ }
1645
+
1646
+ describe("two-label <service>.<namespace> names from Kubernetes callers", () => {
1647
+ test("the finding's probe: staging and production pods calling postgres.data no longer share one global key", () => {
1648
+ const staging: DatabaseEndpoint | null = canonical("postgres.data", {
1649
+ caller: pod("shop", "staging"),
1650
+ });
1651
+ const production: DatabaseEndpoint | null = canonical("postgres.data", {
1652
+ caller: pod("shop", "production"),
1653
+ });
1654
+
1655
+ expect(staging).toEqual({
1656
+ host: "postgres.data.svc.cluster.local",
1657
+ port: 5432,
1658
+ kubernetesClusterName: "staging",
1659
+ });
1660
+ expect(production).toEqual({
1661
+ host: "postgres.data.svc.cluster.local",
1662
+ port: 5432,
1663
+ kubernetesClusterName: "production",
1664
+ });
1665
+ expect(key(staging)).not.toBe(key(production));
1666
+ });
1667
+
1668
+ test("the span key is exactly the Kubernetes workload's Service alias, so the rows merge", () => {
1669
+ const aliases: Array<string> = buildKubernetesDatabaseAliases({
1670
+ system: PG_SYSTEM,
1671
+ namespace: "data",
1672
+ clusterName: "prod",
1673
+ serviceNames: ["postgres"],
1674
+ ports: [],
1675
+ includeUnqualified: true,
1676
+ });
1677
+
1678
+ // A pod that reports its cluster: the qualified alias.
1679
+ expect(aliases).toContain(
1680
+ formatDatabaseEndpoint(
1681
+ canonical("postgres.data", { caller: pod("shop", "prod") })!,
1682
+ ),
1683
+ );
1684
+ // A pod that reports no cluster (single-cluster project): the twin.
1685
+ expect(aliases).toContain(
1686
+ formatDatabaseEndpoint(
1687
+ canonical("postgres.data", { caller: pod("shop", null) })!,
1688
+ ),
1689
+ );
1690
+ // And the alias parses back to the very key the span was stamped with.
1691
+ const alias: DatabaseEndpoint | null = parseDatabaseEndpointString(
1692
+ "postgres.data.svc.cluster.local:5432@prod",
1693
+ { system: PG_SYSTEM },
1694
+ );
1695
+ expect(key(alias)).toBe(
1696
+ key(canonical("postgres.data", { caller: pod("billing", "prod") })),
1697
+ );
1698
+ });
1699
+
1700
+ test("without a cluster the name is cluster-local and LOCAL scope (never auto-created, never global)", () => {
1701
+ const endpoint: DatabaseEndpoint | null = canonical("postgres.data", {
1702
+ caller: pod("shop", null),
1703
+ });
1704
+ expect(endpoint).toEqual({
1705
+ host: "postgres.data.svc.cluster.local",
1706
+ port: 5432,
1707
+ });
1708
+ expect(getDatabaseEndpointScope(endpoint!)).toBe("local");
1709
+ });
1710
+
1711
+ test("a caller with only a cluster (no namespace) is in Kubernetes too", () => {
1712
+ expect(canonical("postgres.data", { caller: pod(null, "prod") })).toEqual({
1713
+ host: "postgres.data.svc.cluster.local",
1714
+ port: 5432,
1715
+ kubernetesClusterName: "prod",
1716
+ });
1717
+ });
1718
+
1719
+ test("the discovery query's flag alone is enough (reduced cron context)", () => {
1720
+ expect(
1721
+ canonical("postgres.data", {
1722
+ caller: { isEphemeral: true, runsInKubernetes: true },
1723
+ }),
1724
+ ).toEqual({ host: "postgres.data.svc.cluster.local", port: 5432 });
1725
+ expect(
1726
+ canonical("postgres.data", {
1727
+ caller: { isEphemeral: true, runsInKubernetes: false },
1728
+ }),
1729
+ ).toEqual({ host: "postgres.data", port: 5432 });
1730
+ });
1731
+
1732
+ test("blank namespace / cluster values do not make a caller Kubernetes", () => {
1733
+ for (const caller of [
1734
+ pod(" ", " "),
1735
+ pod("", ""),
1736
+ { isEphemeral: false, hostName: "vm-1" },
1737
+ ]) {
1738
+ expect(isKubernetesDatabaseCaller(caller)).toBe(false);
1739
+ expect(canonical("postgres.data", { caller })).toEqual({
1740
+ host: "postgres.data",
1741
+ port: 5432,
1742
+ });
1743
+ }
1744
+ expect(isKubernetesDatabaseCaller(null)).toBe(false);
1745
+ expect(isKubernetesDatabaseCaller(pod("shop", null))).toBe(true);
1746
+ expect(isKubernetesDatabaseCaller(pod(null, "prod"))).toBe(true);
1747
+ });
1748
+
1749
+ test("case, trailing dots and ports are canonicalized first", () => {
1750
+ expect(
1751
+ canonical("Postgres.Data.:6543", { caller: pod("shop", "Prod") }),
1752
+ ).toEqual({
1753
+ host: "postgres.data.svc.cluster.local",
1754
+ port: 6543,
1755
+ kubernetesClusterName: "prod",
1756
+ });
1757
+ });
1758
+
1759
+ test("the collector purpose reads the name the same way (a receiver in a pod)", () => {
1760
+ expect(
1761
+ canonical("postgres.data:5432", {
1762
+ caller: pod("monitoring", "prod"),
1763
+ purpose: "collector",
1764
+ }),
1765
+ ).toEqual({
1766
+ host: "postgres.data.svc.cluster.local",
1767
+ port: 5432,
1768
+ kubernetesClusterName: "prod",
1769
+ });
1770
+ });
1771
+
1772
+ test("a StatefulSet member of the caller's own namespace keeps its namespace", () => {
1773
+ // `mongo-0.mongo-headless` from namespace data is that pod, not a Service.
1774
+ const member: DatabaseEndpoint | null = canonical(
1775
+ "mongo-0.mongo-headless:27017",
1776
+ { system: "mongodb", caller: pod("data", "prod") },
1777
+ );
1778
+ expect(member).toEqual({
1779
+ host: "mongo-0.mongo-headless.data.svc.cluster.local",
1780
+ port: 27017,
1781
+ kubernetesClusterName: "prod",
1782
+ });
1783
+ // …which is the member alias the Kubernetes worker claims.
1784
+ expect(
1785
+ buildKubernetesDatabaseAliases({
1786
+ system: "mongodb",
1787
+ namespace: "data",
1788
+ clusterName: "prod",
1789
+ serviceNames: ["mongo"],
1790
+ podServiceNames: [
1791
+ { podName: "mongo-0", serviceName: "mongo-headless" },
1792
+ ],
1793
+ ports: [27017],
1794
+ includeUnqualified: false,
1795
+ }),
1796
+ ).toContain(formatDatabaseEndpoint(member!));
1797
+
1798
+ // Bitnami-style names share only the release word.
1799
+ expect(
1800
+ canonical("redis-node-2.redis-headless", {
1801
+ system: "redis",
1802
+ caller: pod("cache", "prod"),
1803
+ })?.host,
1804
+ ).toBe("redis-node-2.redis-headless.cache.svc.cluster.local");
1805
+ });
1806
+
1807
+ test("a StatefulSet-looking name with no namespace known falls back to the Service reading", () => {
1808
+ expect(
1809
+ canonical("mongo-0.mongo-headless", {
1810
+ system: "mongodb",
1811
+ caller: pod(null, "prod"),
1812
+ })?.host,
1813
+ ).toBe("mongo-0.mongo-headless.svc.cluster.local");
1814
+ });
1815
+
1816
+ test("a Service whose name ends in a number is still a Service in another namespace", () => {
1817
+ expect(
1818
+ canonical("postgres-2.data", { caller: pod("shop", "prod") })?.host,
1819
+ ).toBe("postgres-2.data.svc.cluster.local");
1820
+ });
1821
+
1822
+ test("private-zone second labels are not namespaces, but stay network-scoped", () => {
1823
+ for (const address of ["db.internal", "db.local", "db.localdomain"]) {
1824
+ const endpoint: DatabaseEndpoint | null = canonical(address, {
1825
+ caller: pod("shop", "prod"),
1826
+ });
1827
+ expect(endpoint?.host).toBe(address);
1828
+ }
1829
+ expect(
1830
+ canonical("db.internal", { caller: pod("shop", "prod") })
1831
+ ?.kubernetesClusterName,
1832
+ ).toBe("prod");
1833
+ });
1834
+
1835
+ test("labels Kubernetes would reject are never expanded", () => {
1836
+ expect(canonical("my_db.corp", { caller: pod("shop", "prod") })).toEqual({
1837
+ host: "my_db.corp",
1838
+ port: 5432,
1839
+ });
1840
+ });
1841
+
1842
+ test("three-label public names from pods are never touched", () => {
1843
+ expect(
1844
+ canonical("db.example.com", { caller: pod("shop", "prod") }),
1845
+ ).toEqual({ host: "db.example.com", port: 5432 });
1846
+ });
1847
+
1848
+ test("an alias typed as svc.ns on a Kubernetes row reads like its pods' spans", () => {
1849
+ expect(
1850
+ parseDatabaseEndpointString("postgres.data:5432", {
1851
+ system: PG_SYSTEM,
1852
+ kubernetesNamespace: "data",
1853
+ kubernetesClusterName: "Prod",
1854
+ }),
1855
+ ).toEqual({
1856
+ host: "postgres.data.svc.cluster.local",
1857
+ port: 5432,
1858
+ kubernetesClusterName: "prod",
1859
+ });
1860
+ // …but a stored / VM-seen two-label endpoint parses back to itself.
1861
+ expect(
1862
+ parseDatabaseEndpointString("postgres.data:5432", { system: PG_SYSTEM }),
1863
+ ).toEqual({ host: "postgres.data", port: 5432 });
1864
+ });
1865
+ });
1866
+
1867
+ describe("SQL Server named instances", () => {
1868
+ test("the finding's probe: INST01 and INST02 on one host are two endpoints with two keys", () => {
1869
+ const first: DatabaseEndpoint | null = canonical(
1870
+ "sql1.corp.example.com\\INST01",
1871
+ { system: MSSQL, caller: VM_CALLER },
1872
+ );
1873
+ const second: DatabaseEndpoint | null = canonical(
1874
+ "sql1.corp.example.com\\INST02",
1875
+ { system: MSSQL, caller: VM_CALLER },
1876
+ );
1877
+ expect(first).toEqual({
1878
+ host: "sql1.corp.example.com\\inst01",
1879
+ port: null,
1880
+ });
1881
+ expect(second).toEqual({
1882
+ host: "sql1.corp.example.com\\inst02",
1883
+ port: null,
1884
+ });
1885
+ expect(key(first)).not.toBe(key(second));
1886
+ // Neither takes the default instance's 1433.
1887
+ expect(key(first)).not.toBe(
1888
+ key(canonical("sql1.corp.example.com", { system: MSSQL })),
1889
+ );
1890
+ });
1891
+
1892
+ test("an instance reported beside the address (db.namespace / db.mssql.instance_name) splits the same way", () => {
1893
+ const first: DatabaseEndpoint | null = canonicalizeDatabaseEndpoint({
1894
+ system: MSSQL,
1895
+ address: "sql1.corp.example.com",
1896
+ instance: "INST01",
1897
+ caller: VM_CALLER,
1898
+ purpose: "client-call",
1899
+ });
1900
+ const second: DatabaseEndpoint | null = canonicalizeDatabaseEndpoint({
1901
+ system: MSSQL,
1902
+ address: "sql1.corp.example.com",
1903
+ instance: "INST02",
1904
+ caller: VM_CALLER,
1905
+ purpose: "client-call",
1906
+ });
1907
+ expect(formatDatabaseEndpoint(first!)).toBe(
1908
+ "sql1.corp.example.com\\inst01",
1909
+ );
1910
+ expect(key(first)).not.toBe(key(second));
1911
+ // Written in the address or beside it, it is one endpoint.
1912
+ expect(first).toEqual(
1913
+ canonical("sql1.corp.example.com\\inst01", { system: MSSQL }),
1914
+ );
1915
+ });
1916
+
1917
+ test("the default instance is the plain host on 1433", () => {
1918
+ for (const instance of ["MSSQLSERVER", "mssqlserver", "", " ", null]) {
1919
+ expect(
1920
+ canonicalizeDatabaseEndpoint({
1921
+ system: MSSQL,
1922
+ address: "sql1.corp.example.com",
1923
+ instance: instance,
1924
+ caller: VM_CALLER,
1925
+ purpose: "client-call",
1926
+ }),
1927
+ ).toEqual({ host: "sql1.corp.example.com", port: 1433 });
1928
+ }
1929
+ expect(
1930
+ canonical("sql1.corp.example.com\\MSSQLSERVER", { system: MSSQL }),
1931
+ ).toEqual({ host: "sql1.corp.example.com", port: 1433 });
1932
+ });
1933
+
1934
+ test("a known port already names the instance, so the instance is dropped", () => {
1935
+ const expected: DatabaseEndpoint = {
1936
+ host: "sql1.corp.example.com",
1937
+ port: 1434,
1938
+ };
1939
+ expect(
1940
+ canonical("sql1.corp.example.com\\INST01,1434", { system: MSSQL }),
1941
+ ).toEqual(expected);
1942
+ expect(
1943
+ canonical("sql1.corp.example.com\\INST01:1434", { system: MSSQL }),
1944
+ ).toEqual(expected);
1945
+ expect(
1946
+ canonical("sql1.corp.example.com\\INST01", {
1947
+ system: MSSQL,
1948
+ port: 1434,
1949
+ }),
1950
+ ).toEqual(expected);
1951
+ expect(
1952
+ canonicalizeDatabaseEndpoint({
1953
+ system: MSSQL,
1954
+ address: "sql1.corp.example.com",
1955
+ port: "1434",
1956
+ instance: "INST01",
1957
+ caller: VM_CALLER,
1958
+ purpose: "client-call",
1959
+ }),
1960
+ ).toEqual(expected);
1961
+ // host,port forms on one host stay apart by port.
1962
+ expect(
1963
+ key(canonical("sql1.corp.example.com,1434", { system: MSSQL })),
1964
+ ).not.toBe(key(canonical("sql1.corp.example.com,1435", { system: MSSQL })));
1965
+ });
1966
+
1967
+ test("the address's instance wins over a reported one; an invalid reported one is ignored", () => {
1968
+ expect(
1969
+ canonicalizeDatabaseEndpoint({
1970
+ system: MSSQL,
1971
+ address: "sql1.corp\\INST01",
1972
+ instance: "INST02",
1973
+ caller: VM_CALLER,
1974
+ purpose: "client-call",
1975
+ })?.host,
1976
+ ).toBe("sql1.corp\\inst01");
1977
+ expect(
1978
+ canonicalizeDatabaseEndpoint({
1979
+ system: MSSQL,
1980
+ address: "sql1.corp",
1981
+ instance: "not an instance!",
1982
+ caller: VM_CALLER,
1983
+ purpose: "client-call",
1984
+ }),
1985
+ ).toEqual({ host: "sql1.corp", port: 1433 });
1986
+ });
1987
+
1988
+ test("an instance on a private IP from a pod is qualified, and stays an IP", () => {
1989
+ const endpoint: DatabaseEndpoint | null = canonical("10.0.0.5\\INST01", {
1990
+ system: MSSQL,
1991
+ caller: K8S_CALLER,
1992
+ });
1993
+ expect(endpoint).toEqual({
1994
+ host: "10.0.0.5\\inst01",
1995
+ port: null,
1996
+ kubernetesClusterName: "prod-eu",
1997
+ });
1998
+ expect(isIpLiteralHost(endpoint!.host)).toBe(true);
1999
+ expect(isPrivateIpHost(endpoint!.host)).toBe(true);
2000
+ expect(getDatabaseEndpointScope(endpoint!)).toBe("global");
2001
+ expect(
2002
+ getDatabaseEndpointScope({ host: "10.0.0.5\\inst01", port: null }),
2003
+ ).toBe("local");
2004
+ });
2005
+
2006
+ test("scope ignores the instance: a single-label host is still local", () => {
2007
+ expect(getDatabaseEndpointScope({ host: "sql1\\inst01", port: null })).toBe(
2008
+ "local",
2009
+ );
2010
+ expect(
2011
+ getDatabaseEndpointScope({
2012
+ host: "sql1.corp.example.com\\inst01",
2013
+ port: null,
2014
+ }),
2015
+ ).toBe("global");
2016
+ expect(
2017
+ getDatabaseEndpointScope({ host: "localhost\\sqlexpress", port: null }),
2018
+ ).toBe("local");
2019
+ });
2020
+
2021
+ test("format → parse round-trips, whatever system the reader assumes", () => {
2022
+ const endpoints: Array<DatabaseEndpoint> = [
2023
+ { host: "sql1.corp.example.com\\inst01", port: null },
2024
+ { host: "fd00::5\\inst01", port: null, kubernetesClusterName: "prod" },
2025
+ { host: "10.0.0.5\\inst$1", port: null, kubernetesClusterName: "prod" },
2026
+ ];
2027
+ expect(formatDatabaseEndpoint(endpoints[1]!)).toBe(
2028
+ "[fd00::5]\\inst01@prod",
2029
+ );
2030
+ for (const endpoint of endpoints) {
2031
+ for (const system of [MSSQL, "", PG_SYSTEM]) {
2032
+ expect(
2033
+ parseDatabaseEndpointString(formatDatabaseEndpoint(endpoint), {
2034
+ system,
2035
+ }),
2036
+ ).toEqual(endpoint);
2037
+ }
2038
+ }
2039
+ });
2040
+
2041
+ test("identifiers and display names carry the instance", () => {
2042
+ const endpoint: DatabaseEndpoint = {
2043
+ host: "sql1.corp.example.com\\inst01",
2044
+ port: null,
2045
+ };
2046
+ expect(buildDatabaseServerIdentifier(MSSQL, endpoint)).toBe(
2047
+ "microsoft.sql_server|sql1.corp.example.com\\inst01",
2048
+ );
2049
+ expect(buildDatabaseServerDisplayName({ system: MSSQL, endpoint })).toBe(
2050
+ "SQL Server sql1.corp.example.com\\inst01",
2051
+ );
2052
+ expect(splitDatabaseHostInstance(endpoint.host)).toEqual({
2053
+ host: "sql1.corp.example.com",
2054
+ instance: "inst01",
2055
+ });
2056
+ expect(splitDatabaseHostInstance("db.prod")).toEqual({
2057
+ host: "db.prod",
2058
+ instance: "",
2059
+ });
2060
+ });
2061
+
2062
+ test("loopback with an instance is still loopback", () => {
2063
+ expect(
2064
+ canonical("localhost\\SQLEXPRESS", { system: MSSQL, caller: VM_CALLER }),
2065
+ ).toBeNull();
2066
+ expect(
2067
+ canonical(".\\SQLEXPRESS", {
2068
+ system: MSSQL,
2069
+ caller: VM_CALLER,
2070
+ purpose: "collector",
2071
+ }),
2072
+ ).toEqual({ host: "app-7.corp.example\\sqlexpress", port: null });
2073
+ });
2074
+ });
2075
+
2076
+ describe("readDatabaseInstanceName", () => {
2077
+ function read(
2078
+ system: unknown,
2079
+ attributes: Record<string, unknown>,
2080
+ ): string | null {
2081
+ return readDatabaseInstanceName({
2082
+ system,
2083
+ getAttribute: (name: string): unknown => {
2084
+ return attributes[name];
2085
+ },
2086
+ });
2087
+ }
2088
+
2089
+ test("reads the legacy attribute first, then the instance half of db.namespace", () => {
2090
+ expect(DATABASE_INSTANCE_ATTRIBUTES).toEqual([
2091
+ "db.mssql.instance_name",
2092
+ "db.namespace",
2093
+ ]);
2094
+ expect(
2095
+ read("mssql", {
2096
+ "db.mssql.instance_name": "INST01",
2097
+ "db.namespace": "INST02|orders",
2098
+ }),
2099
+ ).toBe("INST01");
2100
+ expect(read(MSSQL, { "db.namespace": "INST02|orders" })).toBe("INST02");
2101
+ expect(read(MSSQL, { "db.namespace": "a|b|c" })).toBe("a");
2102
+ expect(read(MSSQL, { "db.mssql.instance_name": 7 })).toBe("7");
2103
+ });
2104
+
2105
+ test("an empty legacy attribute falls through; a plain database name is no instance", () => {
2106
+ expect(
2107
+ read(MSSQL, {
2108
+ "db.mssql.instance_name": "",
2109
+ "db.namespace": "INST02|orders",
2110
+ }),
2111
+ ).toBe("INST02");
2112
+ expect(read(MSSQL, { "db.namespace": "orders" })).toBeNull();
2113
+ expect(read(MSSQL, {})).toBeNull();
2114
+ });
2115
+
2116
+ test("only SQL Server reads an instance (other engines' db.namespace means something else)", () => {
2117
+ for (const system of ["postgresql", "redis", "", null]) {
2118
+ expect(
2119
+ read(system, {
2120
+ "db.mssql.instance_name": "INST01",
2121
+ "db.namespace": "a|b",
2122
+ }),
2123
+ ).toBeNull();
2124
+ }
2125
+ });
2126
+
2127
+ test("tolerates junk", () => {
2128
+ expect(
2129
+ readDatabaseInstanceName(
2130
+ null as unknown as {
2131
+ system: unknown;
2132
+ getAttribute: (key: string) => unknown;
2133
+ },
2134
+ ),
2135
+ ).toBeNull();
2136
+ expect(read(MSSQL, { "db.mssql.instance_name": { x: 1 } })).toBeNull();
2137
+ });
2138
+ });
2139
+
2140
+ describe("per-network names are never global identities", () => {
2141
+ test("the finding's probes: dev-tool gateway names are host-relative", () => {
2142
+ for (const host of [
2143
+ "host.minikube.internal",
2144
+ "host.k3d.internal",
2145
+ "host.lima.internal",
2146
+ "host.orb.internal",
2147
+ "host.rancher-desktop.internal",
2148
+ "HOST.MINIKUBE.INTERNAL.",
2149
+ "vm.docker.internal",
2150
+ "host.docker.internal",
2151
+ ]) {
2152
+ expect(isHostRelativeDatabaseHost(host)).toBe(true);
2153
+ expect(canonical(host, { caller: VM_CALLER })).toBeNull();
2154
+ expect(canonical(host, { caller: K8S_CALLER })).toBeNull();
2155
+ // A collector on a stable machine: that machine.
2156
+ expect(
2157
+ canonical(host, { caller: VM_CALLER, purpose: "collector" })?.host,
2158
+ ).toBe("app-7.corp.example");
2159
+ }
2160
+ for (const host of [
2161
+ "db.internal",
2162
+ "host.internal",
2163
+ "a.host.minikube.internal",
2164
+ "host.bad_label.internal",
2165
+ "db.docker.internals",
2166
+ ]) {
2167
+ expect(isHostRelativeDatabaseHost(host)).toBe(false);
2168
+ }
2169
+ });
2170
+
2171
+ test("isLinkLocalIpHost: 169.254/16 and fe80::/10 only", () => {
2172
+ for (const host of [
2173
+ "169.254.1.10",
2174
+ "169.254.0.1",
2175
+ "fe80::1",
2176
+ "fe80::1%eth0",
2177
+ "[fe80::1]",
2178
+ "febf::1",
2179
+ "::ffff:169.254.1.1",
2180
+ ]) {
2181
+ expect(isLinkLocalIpHost(host)).toBe(true);
2182
+ }
2183
+ for (const host of [
2184
+ "169.253.1.1",
2185
+ "170.254.1.1",
2186
+ "fec0::1",
2187
+ "fe7f::1",
2188
+ "10.0.0.1",
2189
+ "db.prod",
2190
+ "",
2191
+ ]) {
2192
+ expect(isLinkLocalIpHost(host)).toBe(false);
2193
+ }
2194
+ });
2195
+
2196
+ test("the finding's probes: link-local addresses are LOCAL, qualified or not", () => {
2197
+ const fromPod: DatabaseEndpoint | null = canonical("169.254.1.10", {
2198
+ caller: K8S_CALLER,
2199
+ });
2200
+ expect(fromPod).toEqual({
2201
+ host: "169.254.1.10",
2202
+ port: 5432,
2203
+ kubernetesClusterName: "prod-eu",
2204
+ });
2205
+ expect(getDatabaseEndpointScope(fromPod!)).toBe("local");
2206
+
2207
+ const zoned: DatabaseEndpoint | null = canonical("fe80::1%eth0", {
2208
+ caller: VM_CALLER,
2209
+ });
2210
+ expect(zoned).toEqual({ host: "fe80::1", port: 5432 });
2211
+ expect(getDatabaseEndpointScope(zoned!)).toBe("local");
2212
+ });
2213
+
2214
+ test("private DNS zones (.internal, .local, .home.arpa) are network-scoped", () => {
2215
+ expect(NETWORK_SCOPED_NAME_SUFFIXES).toEqual([
2216
+ ".local",
2217
+ ".internal",
2218
+ ".home.arpa",
2219
+ ".localdomain",
2220
+ ]);
2221
+ for (const host of [
2222
+ "ip-10-0-0-5.ec2.internal",
2223
+ "db-1.c.my-project.internal",
2224
+ "db.corp.local",
2225
+ "nas.home.arpa",
2226
+ "build-7.localdomain",
2227
+ "pg.shop.svc.cluster.local",
2228
+ ]) {
2229
+ expect(isClusterScopedDatabaseHost(host)).toBe(true);
2230
+
2231
+ // From a machine outside any cluster: kept, but LOCAL.
2232
+ const fromVm: DatabaseEndpoint | null = canonical(host, {
2233
+ caller: VM_CALLER,
2234
+ });
2235
+ expect(fromVm?.kubernetesClusterName).toBeUndefined();
2236
+ expect(getDatabaseEndpointScope(fromVm!)).toBe("local");
2237
+
2238
+ // From a pod: qualified with its cluster, which makes it global.
2239
+ const fromPod: DatabaseEndpoint | null = canonical(host, {
2240
+ caller: K8S_CALLER,
2241
+ });
2242
+ expect(fromPod?.kubernetesClusterName).toBe("prod-eu");
2243
+ expect(getDatabaseEndpointScope(fromPod!)).toBe("global");
2244
+ }
2245
+ for (const host of [
2246
+ "db.example.com",
2247
+ "orders.cjd8.eu-west-1.rds.amazonaws.com",
2248
+ "api.localhost",
2249
+ "db.internalapi.com",
2250
+ "8.8.8.8",
2251
+ "",
2252
+ ]) {
2253
+ expect(isClusterScopedDatabaseHost(host)).toBe(false);
2254
+ }
2255
+ });
2256
+
2257
+ test("two clusters' copies of one EC2 private name are two endpoints", () => {
2258
+ expect(
2259
+ key(canonical("ip-10-0-0-5.ec2.internal", { caller: pod("a", "east") })),
2260
+ ).not.toBe(
2261
+ key(canonical("ip-10-0-0-5.ec2.internal", { caller: pod("a", "west") })),
2262
+ );
2263
+ });
2264
+
2265
+ test("isPrivateIpHost is unchanged: link-local is its own class", () => {
2266
+ expect(isPrivateIpHost("169.254.1.1")).toBe(false);
2267
+ expect(isPrivateIpHost("fe80::1")).toBe(false);
2268
+ });
2269
+ });
2270
+
2271
+ describe("the @cluster qualifier on typed endpoints", () => {
2272
+ const PG: { system: string } = { system: PG_SYSTEM };
2273
+
2274
+ test("names the cluster of every cluster-scoped kind of address", () => {
2275
+ expect(
2276
+ parseDatabaseEndpointString("pg.shop.svc.cluster.local:5432@Prod-EU", PG),
2277
+ ).toEqual({
2278
+ host: "pg.shop.svc.cluster.local",
2279
+ port: 5432,
2280
+ kubernetesClusterName: "prod-eu",
2281
+ });
2282
+ // A <service>.<namespace> short name is read as the pod would read it.
2283
+ expect(parseDatabaseEndpointString("postgres.data@staging", PG)).toEqual({
2284
+ host: "postgres.data.svc.cluster.local",
2285
+ port: 5432,
2286
+ kubernetesClusterName: "staging",
2287
+ });
2288
+ expect(parseDatabaseEndpointString("db.internal:5432@prod", PG)).toEqual({
2289
+ host: "db.internal",
2290
+ port: 5432,
2291
+ kubernetesClusterName: "prod",
2292
+ });
2293
+ expect(
2294
+ parseDatabaseEndpointString("169.254.1.1@prod", PG)
2295
+ ?.kubernetesClusterName,
2296
+ ).toBe("prod");
2297
+ });
2298
+
2299
+ test("a single-label name takes a qualifier only with a namespace to expand it", () => {
2300
+ expect(parseDatabaseEndpointString("pg@prod", PG)).toBeNull();
2301
+ expect(
2302
+ parseDatabaseEndpointString("pg@prod", {
2303
+ ...PG,
2304
+ kubernetesNamespace: "shop",
2305
+ }),
2306
+ ).toEqual({
2307
+ host: "pg.shop.svc.cluster.local",
2308
+ port: 5432,
2309
+ kubernetesClusterName: "prod",
2310
+ });
2311
+ });
2312
+
2313
+ test("stored values stay readable: everything canonicalize can produce round-trips", () => {
2314
+ const produced: Array<DatabaseEndpoint | null> = [
2315
+ canonical("postgres.data", { caller: pod("shop", "staging") }),
2316
+ canonical("postgres.data", { caller: pod("shop", null) }),
2317
+ canonical("mongo-0.mongo-hl", {
2318
+ system: "mongodb",
2319
+ caller: pod("data", "prod"),
2320
+ }),
2321
+ canonical("169.254.1.10", { caller: K8S_CALLER }),
2322
+ canonical("db.internal", { caller: K8S_CALLER }),
2323
+ canonical("db.internal", { caller: VM_CALLER }),
2324
+ canonical("10.0.0.5\\INST01", { system: MSSQL, caller: K8S_CALLER }),
2325
+ canonical("pg.shop", { caller: VM_CALLER }),
2326
+ ];
2327
+ for (const endpoint of produced) {
2328
+ expect(endpoint).not.toBeNull();
2329
+ const system: string = endpoint!.host.includes("\\")
2330
+ ? MSSQL
2331
+ : endpoint!.port === 27017
2332
+ ? "mongodb"
2333
+ : PG_SYSTEM;
2334
+ expect(
2335
+ parseDatabaseEndpointString(formatDatabaseEndpoint(endpoint!), {
2336
+ system,
2337
+ }),
2338
+ ).toEqual(endpoint);
2339
+ }
2340
+ });
2341
+ });
2342
+
2343
+ describe("parseManualDatabaseEndpoint", () => {
2344
+ function manual(
2345
+ value: unknown,
2346
+ context: Partial<{
2347
+ system: string;
2348
+ port: unknown;
2349
+ kubernetesClusterName: string | null;
2350
+ kubernetesNamespace: string | null;
2351
+ knownClusterNames: Array<string> | null;
2352
+ }> = {},
2353
+ ): ManualDatabaseEndpoint {
2354
+ return parseManualDatabaseEndpoint(value, {
2355
+ system: PG_SYSTEM,
2356
+ ...context,
2357
+ });
2358
+ }
2359
+
2360
+ test("accepts what parseDatabaseEndpointString accepts, canonicalized the same way", () => {
2361
+ for (const value of [
2362
+ "Orders-DB.example.com",
2363
+ "orders-db.example.com:6543",
2364
+ "10.0.0.5@prod",
2365
+ "postgres.data@staging",
2366
+ "pg.shop.svc.cluster.local:5432@prod",
2367
+ "[fd00::5]:5432",
2368
+ "postgres://app@db.example.com:5432/orders",
2369
+ ]) {
2370
+ const result: ManualDatabaseEndpoint = manual(value);
2371
+ expect(result.error).toBeNull();
2372
+ expect(result.endpoint).toEqual(
2373
+ parseDatabaseEndpointString(value, { system: PG_SYSTEM }),
2374
+ );
2375
+ }
2376
+ });
2377
+
2378
+ test("user@host is refused with a message that says to drop the user", () => {
2379
+ const result: ManualDatabaseEndpoint = manual("admin@10.0.0.5:5432");
2380
+ expect(result.endpoint).toBeNull();
2381
+ expect(result.error).toContain("looks like user@host");
2382
+ expect(result.error).toContain('Remove "admin@"');
2383
+ expect(manual("user@db.example.com").error).toContain(
2384
+ "looks like user@host",
2385
+ );
2386
+ expect(manual("john.doe@db.example.com").error).toContain(
2387
+ 'Remove "john.doe@"',
2388
+ );
2389
+ // "@" with nothing before it is simply not an endpoint.
2390
+ expect(manual("@db.example.com").error).toContain(
2391
+ "is not a valid host[:port] endpoint",
2392
+ );
2393
+ });
2394
+
2395
+ test("a qualifier on a public name is refused, naming the qualifier to remove", () => {
2396
+ const result: ManualDatabaseEndpoint = manual("db.example.com:5432@prod");
2397
+ expect(result.endpoint).toBeNull();
2398
+ expect(result.error).toContain('Remove "@prod"');
2399
+ expect(result.error).toContain("db.example.com");
2400
+ });
2401
+
2402
+ test("a qualifier on a bare single-label name asks for the namespace", () => {
2403
+ const result: ManualDatabaseEndpoint = manual("pg@prod");
2404
+ expect(result.endpoint).toBeNull();
2405
+ expect(result.error).toContain("pg.<namespace>@prod");
2406
+ });
2407
+
2408
+ test("loopback, garbage and empty values are refused with their own messages", () => {
2409
+ expect(manual("localhost:5432").error).toContain("loopback");
2410
+ expect(manual("host.minikube.internal").error).toContain("loopback");
2411
+ expect(manual("[REDACTED]").error).toContain(
2412
+ "is not a valid host[:port] endpoint",
2413
+ );
2414
+ expect(manual(" ").error).toContain("Server address is required");
2415
+ expect(manual(null).error).toContain("Server address is required");
2416
+ });
2417
+
2418
+ test("an out-of-range port in the address is an error, not silently the default", () => {
2419
+ // parseDatabaseEndpointString would quietly fill in 5432.
2420
+ expect(
2421
+ parseDatabaseEndpointString("db.example.com:70000", {
2422
+ system: PG_SYSTEM,
2423
+ }),
2424
+ ).toEqual({ host: "db.example.com", port: 5432 });
2425
+ const result: ManualDatabaseEndpoint = manual("db.example.com:70000");
2426
+ expect(result.endpoint).toBeNull();
2427
+ expect(result.error).toContain("outside 1-65535");
2428
+ });
2429
+
2430
+ test("the port field replaces the address's port, and must be valid", () => {
2431
+ expect(manual("db.example.com:70000", { port: 6543 }).endpoint).toEqual({
2432
+ host: "db.example.com",
2433
+ port: 6543,
2434
+ });
2435
+ expect(manual("db.example.com:5432", { port: "6543" }).endpoint).toEqual({
2436
+ host: "db.example.com",
2437
+ port: 6543,
2438
+ });
2439
+ expect(manual("db.example.com", { port: "" }).endpoint).toEqual({
2440
+ host: "db.example.com",
2441
+ port: 5432,
2442
+ });
2443
+ for (const port of [0, 70000, "abc", 5432.5]) {
2444
+ expect(manual("db.example.com", { port }).error).toBe(
2445
+ "Server port must be a whole number between 1 and 65535.",
2446
+ );
2447
+ }
2448
+ // A port names a SQL Server instance: the instance is dropped.
2449
+ expect(
2450
+ manual("sql1.corp\\INST01", { system: MSSQL, port: 1434 }).endpoint,
2451
+ ).toEqual({ host: "sql1.corp", port: 1434 });
2452
+ expect(manual("sql1.corp\\INST01", { system: MSSQL }).endpoint).toEqual({
2453
+ host: "sql1.corp\\inst01",
2454
+ port: null,
2455
+ });
2456
+ });
2457
+
2458
+ test("an unqualified cluster-scoped endpoint is accepted with a hint naming the qualified form", () => {
2459
+ const result: ManualDatabaseEndpoint = manual(
2460
+ "postgres.data.svc.cluster.local",
2461
+ { knownClusterNames: ["Staging", "prod", "", "staging"] },
2462
+ );
2463
+ expect(result.endpoint).toEqual({
2464
+ host: "postgres.data.svc.cluster.local",
2465
+ port: 5432,
2466
+ });
2467
+ expect(result.error).toBeNull();
2468
+ expect(result.clusterQualifierHint).toContain(
2469
+ "postgres.data.svc.cluster.local:5432@staging",
2470
+ );
2471
+ expect(result.clusterQualifierHint).toContain(
2472
+ "this project's clusters: staging, prod",
2473
+ );
2474
+ expect(manual("10.0.0.5").clusterQualifierHint).toContain(
2475
+ "10.0.0.5:5432@<cluster name>",
2476
+ );
2477
+ });
2478
+
2479
+ test("no hint for a global or already-qualified endpoint", () => {
2480
+ expect(manual("db.example.com").clusterQualifierHint).toBeNull();
2481
+ expect(manual("10.0.0.5@prod").clusterQualifierHint).toBeNull();
2482
+ // The row's own cluster qualifies it, too.
2483
+ expect(
2484
+ manual("10.0.0.5", { kubernetesClusterName: "prod" }).endpoint,
2485
+ ).toEqual({ host: "10.0.0.5", port: 5432, kubernetesClusterName: "prod" });
2486
+ });
2487
+
2488
+ test("echoes at most 100 characters of a rejected value", () => {
2489
+ const long: string = `${"a".repeat(300)}!`;
2490
+ const error: string = manual(long).error || "";
2491
+ expect(error).toContain(`${"a".repeat(100)}…`);
2492
+ expect(error).not.toContain("a".repeat(101));
2493
+ });
2494
+ });
2495
+
2496
+ describe("host lists name one logical server", () => {
2497
+ test("the member chosen does not depend on the order a client lists them", () => {
2498
+ const orders: Array<string> = [
2499
+ "m1.prod:27017,m2.prod:27017,m3.prod:27017",
2500
+ "m3.prod:27017,m1.prod:27017,m2.prod:27017",
2501
+ "m2.prod:27017,m3.prod:27017,m1.prod:27017",
2502
+ "mongodb://app:pw@m3.prod:27017,m2.prod:27017,m1.prod:27017/orders?replicaSet=rs0",
2503
+ ];
2504
+ for (const raw of orders) {
2505
+ expect(parseHostAndPort(raw)).toEqual({ host: "m1.prod", port: 27017 });
2506
+ expect(
2507
+ key(canonical(raw, { system: "mongodb", caller: VM_CALLER })),
2508
+ ).toBe(key(canonical("m1.prod:27017", { system: "mongodb" })));
2509
+ }
2510
+ });
2511
+
2512
+ test("parseHostAndPortList returns every member once, in canonical order", () => {
2513
+ expect(
2514
+ parseHostAndPortList("M2.prod:27017, m1.prod ,m2.prod:27017,"),
2515
+ ).toEqual([
2516
+ { host: "m1.prod", port: null },
2517
+ { host: "m2.prod", port: 27017 },
2518
+ ]);
2519
+ expect(parseHostAndPortList("db.prod:5432")).toEqual([
2520
+ { host: "db.prod", port: 5432 },
2521
+ ]);
2522
+ expect(parseHostAndPortList("sql.prod,1433")).toEqual([
2523
+ { host: "sql.prod", port: 1433 },
2524
+ ]);
2525
+ });
2526
+
2527
+ test("a list is only read when every member is readable", () => {
2528
+ expect(parseHostAndPort("m1.prod:27017,[REDACTED]")).toBeNull();
2529
+ expect(parseHostAndPortList("m1.prod,***.***.***.***")).toBeNull();
2530
+ expect(parseHostAndPortList(",,,")).toBeNull();
2531
+ expect(parseHostAndPortList(null)).toBeNull();
2532
+ });
2533
+ });
2534
+
2535
+ describe("getDatabaseClusterHost (managed cluster members)", () => {
2536
+ test("MongoDB Atlas members map to their cluster's name", () => {
2537
+ expect(getDatabaseClusterHost("c0-shard-00-00.abcd.mongodb.net")).toBe(
2538
+ "c0.abcd.mongodb.net",
2539
+ );
2540
+ expect(getDatabaseClusterHost("C0-Shard-00-02.ABCD.mongodb.net")).toBe(
2541
+ "c0.abcd.mongodb.net",
2542
+ );
2543
+ expect(
2544
+ getDatabaseClusterHost("ac-x1y2z3-shard-01-02.ab1cd.mongodb.net"),
2545
+ ).toBe("ac-x1y2z3.ab1cd.mongodb.net");
2546
+ expect(
2547
+ getDatabaseClusterHost("my-shard-cluster-shard-00-01.q.mongodb.net"),
2548
+ ).toBe("my-shard-cluster.q.mongodb.net");
2549
+ });
2550
+
2551
+ test("anything else is not a member", () => {
2552
+ for (const host of [
2553
+ "c0.abcd.mongodb.net",
2554
+ "c0-shard-0-1.abcd.mongodb.net",
2555
+ "c0-shard-00-00.abcd.mongodb.com",
2556
+ "a.c0-shard-00-00.abcd.mongodb.net",
2557
+ "-shard-00-00.abcd.mongodb.net",
2558
+ "c0-shard-00-00-pl-0.abcd.mongodb.net",
2559
+ "c0-shard-00-00.ab_cd.mongodb.net",
2560
+ "db.example.com",
2561
+ "",
2562
+ ]) {
2563
+ expect(getDatabaseClusterHost(host)).toBeNull();
2564
+ }
2565
+ expect(getDatabaseClusterHost(null as unknown as string)).toBeNull();
2566
+ });
2567
+ });
2568
+
2569
+ describe("the new parsing paths stay linear on hostile input", () => {
2570
+ test("huge host lists, long labels and repeated markers answer quickly", () => {
2571
+ const started: number = performance.now();
2572
+
2573
+ expect(parseHostAndPort(`${"a,".repeat(100_000)}b`)?.host).toBe("a");
2574
+ expect(
2575
+ getDatabaseClusterHost(
2576
+ `${"x-shard-".repeat(20_000)}00-00.abcd.mongodb.net`,
2577
+ ),
2578
+ ).toBeNull();
2579
+ expect(
2580
+ isHostRelativeDatabaseHost(`host.${"a".repeat(200_000)}.internal`),
2581
+ ).toBe(false);
2582
+ expect(
2583
+ parseDatabaseEndpointString(
2584
+ `${"a".repeat(100_000)}@${"b.".repeat(50_000)}c`,
2585
+ {
2586
+ system: PG_SYSTEM,
2587
+ },
2588
+ ),
2589
+ ).toBeNull();
2590
+ expect(
2591
+ canonical(`${"a-".repeat(50_000)}1.b`, { caller: pod("shop", "prod") }),
2592
+ ).toBeNull();
2593
+
2594
+ expect(performance.now() - started).toBeLessThan(2000);
2595
+ });
2596
+ });
2597
+
2598
+ describe("isKubernetesServiceDnsHost", () => {
2599
+ test.each([
2600
+ "pg.shop.svc.cluster.local",
2601
+ "pg.shop.svc",
2602
+ "pg.shop.svc.corp.k8s",
2603
+ "pg-0.pg-headless.shop.svc.cluster.local",
2604
+ "PG.Shop.SVC.cluster.local.",
2605
+ " pg.shop.svc.cluster.local ",
2606
+ "mssql.data.svc.cluster.local\\inst01",
2607
+ ])("%j is a Kubernetes Service name", (host: string) => {
2608
+ expect(isKubernetesServiceDnsHost(host)).toBe(true);
2609
+ });
2610
+
2611
+ test.each([
2612
+ "pg",
2613
+ "pg.shop",
2614
+ "db.internal",
2615
+ "orders-db.corp.internal",
2616
+ "orders-db.local",
2617
+ "10.0.1.5",
2618
+ "orders-db.example.com",
2619
+ "svc.cluster.local",
2620
+ "a.b.c.d.svc.cluster.local",
2621
+ "pg.shop.svcx.cluster.local",
2622
+ "",
2623
+ null as unknown as string,
2624
+ 42 as unknown as string,
2625
+ ])("%j is not", (host: string) => {
2626
+ expect(isKubernetesServiceDnsHost(host)).toBe(false);
2627
+ });
2628
+
2629
+ test("it is the test the endpoint parser expands Service names with", () => {
2630
+ for (const typed of [
2631
+ "pg.shop.svc",
2632
+ "pg.shop.svc.cluster.local",
2633
+ "pg-0.pg-hl.shop.svc",
2634
+ ]) {
2635
+ const parsed: ManualDatabaseEndpoint = parseManualDatabaseEndpoint(
2636
+ typed,
2637
+ { system: PG_SYSTEM },
2638
+ );
2639
+ expect(parsed.endpoint!.host.endsWith(".svc.cluster.local")).toBe(true);
2640
+ expect(isKubernetesServiceDnsHost(parsed.endpoint!.host)).toBe(true);
2641
+ }
2642
+ });
2643
+
2644
+ test("stays linear on hostile input", () => {
2645
+ const started: number = performance.now();
2646
+
2647
+ expect(isKubernetesServiceDnsHost("a".repeat(200_000))).toBe(false);
2648
+ expect(isKubernetesServiceDnsHost(`${"a.".repeat(100_000)}svc`)).toBe(
2649
+ false,
2650
+ );
2651
+ expect(isKubernetesServiceDnsHost(`a.b${".".repeat(200_000)}x`)).toBe(
2652
+ false,
2653
+ );
2654
+
2655
+ expect(performance.now() - started).toBeLessThan(1000);
2656
+ });
2657
+ });