@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -41,6 +41,7 @@ import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData"
41
41
  import MetricFormulaConfigData from "../../../Types/Metrics/MetricFormulaConfigData";
42
42
  import MetricsViewConfig from "../../../Types/Metrics/MetricsViewConfig";
43
43
  import MetricUnitUtil from "../../../Utils/MetricUnitUtil";
44
+ import MetricValueFormatter from "../../../Utils/Monitor/MetricValueFormatter";
44
45
 
45
46
  export default class MonitorCriteriaObservationBuilder {
46
47
  public static describeFilterObservation(input: {
@@ -1220,18 +1221,33 @@ export default class MonitorCriteriaObservationBuilder {
1220
1221
  * that unit alongside the numbers so the message reads "latest 0.06 sec"
1221
1222
  * instead of the unitless "latest 0.06".
1222
1223
  */
1223
- const displayUnit: string | undefined =
1224
- MonitorCriteriaObservationBuilder.resolveMetricUnits({
1225
- criteriaFilter: input.criteriaFilter,
1226
- dataToProcess: input.dataToProcess,
1227
- monitorStep: input.monitorStep,
1228
- alias: metricValues.alias,
1229
- }).displayUnit;
1224
+ const resolvedUnits: {
1225
+ displayUnit: string | undefined;
1226
+ metricName: string | undefined;
1227
+ } = MonitorCriteriaObservationBuilder.resolveMetricUnits({
1228
+ criteriaFilter: input.criteriaFilter,
1229
+ dataToProcess: input.dataToProcess,
1230
+ monitorStep: input.monitorStep,
1231
+ alias: metricValues.alias,
1232
+ });
1230
1233
 
1234
+ /*
1235
+ * The same renderer the fired-alert email uses. Before this, a breach
1236
+ * that CompareCriteria wrote into the email as "1.07 GB" appeared in
1237
+ * the monitor's own evaluation log as "1073741824.00 By" — one sample,
1238
+ * two descriptions, on two screens a reader compares.
1239
+ */
1231
1240
  const summary: string | null =
1232
1241
  MonitorCriteriaMessageFormatter.summarizeNumericSeries(
1233
1242
  displayValues,
1234
- displayUnit,
1243
+ resolvedUnits.displayUnit,
1244
+ (value: number): string => {
1245
+ return MetricValueFormatter.format({
1246
+ value: value,
1247
+ unit: resolvedUnits.displayUnit,
1248
+ metricName: resolvedUnits.metricName,
1249
+ });
1250
+ },
1235
1251
  );
1236
1252
 
1237
1253
  if (!summary) {
@@ -1304,6 +1320,18 @@ export default class MonitorCriteriaObservationBuilder {
1304
1320
  sampleUnit: string | undefined;
1305
1321
  thresholdUnit: string | undefined;
1306
1322
  displayUnit: string | undefined;
1323
+ /**
1324
+ * The metric NAME, for the two decisions MetricValueFormatter cannot
1325
+ * make from a unit alone: whether a "1"-unit value is a fraction to
1326
+ * render as a percent, and whether a "seconds"-unit value is really an
1327
+ * epoch that must skip the duration ladder.
1328
+ *
1329
+ * Undefined for a formula. A formula's "name" is its expression, and an
1330
+ * expression ending in `_ratio` would trip the fraction heuristic into
1331
+ * reporting every value at 100x — the same guard
1332
+ * MonitorCriteriaEvaluator.metricNameForUnitHeuristics applies.
1333
+ */
1334
+ metricName: string | undefined;
1307
1335
  } {
1308
1336
  const thresholdUnit: string | undefined =
1309
1337
  input.criteriaFilter.metricMonitorOptions?.thresholdUnit || undefined;
@@ -1317,6 +1345,7 @@ export default class MonitorCriteriaObservationBuilder {
1317
1345
  sampleUnit: undefined,
1318
1346
  thresholdUnit,
1319
1347
  displayUnit: thresholdUnit,
1348
+ metricName: undefined,
1320
1349
  };
1321
1350
  }
1322
1351
 
@@ -1355,35 +1384,42 @@ export default class MonitorCriteriaObservationBuilder {
1355
1384
  nativeUnitFromMap ||
1356
1385
  undefined;
1357
1386
 
1387
+ const displayUnit: string | undefined = thresholdUnit || sampleUnit;
1388
+
1358
1389
  return {
1359
1390
  sampleUnit,
1360
1391
  thresholdUnit,
1361
1392
  displayUnit: MonitorCriteriaObservationBuilder.normalizeDisplayUnit(
1362
- thresholdUnit || sampleUnit,
1393
+ displayUnit,
1394
+ rawMetricName,
1363
1395
  ),
1396
+ metricName: rawMetricName,
1364
1397
  };
1365
1398
  }
1366
1399
 
1367
1400
  /*
1368
- * Suppress units that would read as noise next to a raw number. OTel's
1369
- * dimensionless "1" marks ratio metrics whose samples are fractions in
1370
- * [0, 1]; rendering "0.06 1" is both ugly and misleading (it is not 1% —
1371
- * it is 6%). Returning undefined leaves the number unlabelled, matching
1372
- * the pre-unit behavior for that specific case. Any real unit passes
1373
- * through unchanged.
1401
+ * Suppress units that name no dimension, so they never reach a reader
1402
+ * next to a number.
1403
+ *
1404
+ * This used to be a hand-rolled rule that dropped only the literal "1",
1405
+ * and it drifted from the one CompareCriteria applies to the very same
1406
+ * sample: a ratio metric's evaluation log read a bare "latest 0.06"
1407
+ * while the alert email for the identical value said "6.00%". It also
1408
+ * let UCUM's "{restarts}" and the platform catalogs' "count" / "ratio"
1409
+ * through, none of which mean anything to a reader.
1410
+ *
1411
+ * MetricValueFormatter owns that rule now. Note it is name-AWARE where
1412
+ * the old one was not: "1" on a `.utilization` metric is a real
1413
+ * dimension (a fraction to be shown as a percent), so it survives here
1414
+ * and the formatter turns it into "%".
1374
1415
  */
1375
1416
  private static normalizeDisplayUnit(
1376
1417
  unit: string | undefined,
1418
+ metricName?: string | undefined,
1377
1419
  ): string | undefined {
1378
- if (!unit || !unit.trim()) {
1379
- return undefined;
1380
- }
1381
-
1382
- if (unit.trim() === "1") {
1383
- return undefined;
1384
- }
1385
-
1386
- return unit;
1420
+ return MetricValueFormatter.hasDisplayableUnit(unit, metricName)
1421
+ ? unit
1422
+ : undefined;
1387
1423
  }
1388
1424
 
1389
1425
  /*
@@ -1399,6 +1435,24 @@ export default class MonitorCriteriaObservationBuilder {
1399
1435
  dataToProcess: DataToProcess;
1400
1436
  monitorStep: MonitorStep;
1401
1437
  }): string | undefined {
1438
+ return MonitorCriteriaObservationBuilder.getMetricValueDisplayContext(input)
1439
+ .unit;
1440
+ }
1441
+
1442
+ /**
1443
+ * The unit AND the metric name a criteria's values are displayed in.
1444
+ *
1445
+ * The expectation clause needs both, not just the unit: it renders the
1446
+ * threshold through the same MetricValueFormatter as the observation, and
1447
+ * the formatter's fraction and epoch-timestamp rules are keyed on the
1448
+ * name. Without it, "recorded latest 6.00%" would be followed by
1449
+ * "(expected to be greater than 0.9)".
1450
+ */
1451
+ public static getMetricValueDisplayContext(input: {
1452
+ criteriaFilter: CriteriaFilter;
1453
+ dataToProcess: DataToProcess;
1454
+ monitorStep: MonitorStep;
1455
+ }): { unit: string | undefined; metricName: string | undefined } {
1402
1456
  /*
1403
1457
  * Database Health thresholds are typed in the metric's own unit, which
1404
1458
  * only the catalog knows - without this the expectation clause reads
@@ -1412,13 +1466,17 @@ export default class MonitorCriteriaObservationBuilder {
1412
1466
  ? getDatabaseMetricByMetricType(metricType)
1413
1467
  : null;
1414
1468
 
1415
- return MonitorCriteriaObservationBuilder.normalizeDisplayUnit(
1416
- definition?.unit,
1417
- );
1469
+ return {
1470
+ unit: MonitorCriteriaObservationBuilder.normalizeDisplayUnit(
1471
+ definition?.unit,
1472
+ ),
1473
+ // Catalog units are explicit; no name-based heuristic is wanted.
1474
+ metricName: undefined,
1475
+ };
1418
1476
  }
1419
1477
 
1420
1478
  if (input.criteriaFilter.checkOn !== CheckOn.MetricValue) {
1421
- return undefined;
1479
+ return { unit: undefined, metricName: undefined };
1422
1480
  }
1423
1481
 
1424
1482
  const metricValues: {
@@ -1430,12 +1488,17 @@ export default class MonitorCriteriaObservationBuilder {
1430
1488
  monitorStep: input.monitorStep,
1431
1489
  });
1432
1490
 
1433
- return MonitorCriteriaObservationBuilder.resolveMetricUnits({
1491
+ const resolved: {
1492
+ displayUnit: string | undefined;
1493
+ metricName: string | undefined;
1494
+ } = MonitorCriteriaObservationBuilder.resolveMetricUnits({
1434
1495
  criteriaFilter: input.criteriaFilter,
1435
1496
  dataToProcess: input.dataToProcess,
1436
1497
  monitorStep: input.monitorStep,
1437
1498
  alias: metricValues?.alias ?? null,
1438
- }).displayUnit;
1499
+ });
1500
+
1501
+ return { unit: resolved.displayUnit, metricName: resolved.metricName };
1439
1502
  }
1440
1503
 
1441
1504
  private static getSnmpResponse(input: {
@@ -1743,28 +1806,24 @@ export default class MonitorCriteriaObservationBuilder {
1743
1806
  }
1744
1807
  }
1745
1808
 
1809
+ /*
1810
+ * A Database Health metric carries an explicit catalog unit, and
1811
+ * DatabaseMonitorCriteria already hands that same unit to CompareCriteria
1812
+ * for the fired-alert email. Rendering it here through anything else made
1813
+ * one byte count read three ways in one product: the email said
1814
+ * "1.07 GB" (decimal, via MetricValueFormatter), this observation said
1815
+ * "1.00 GB" (formatBytes divides by 1024 but labels the rungs SI), and
1816
+ * the expectation clause beside it said "1000000000 bytes".
1817
+ *
1818
+ * formatBytes stays for the server-monitor memory and disk observations,
1819
+ * where the values come from BasicMetrics with no unit metadata at all
1820
+ * and 1024 is the right convention for RAM and volumes.
1821
+ */
1746
1822
  private static formatDatabaseMetricValue(
1747
1823
  value: number,
1748
1824
  unit: string,
1749
1825
  ): string {
1750
- if (unit === "%") {
1751
- return (
1752
- MonitorCriteriaMessageFormatter.formatPercentage(value) ?? String(value)
1753
- );
1754
- }
1755
-
1756
- if (unit === "bytes") {
1757
- return (
1758
- MonitorCriteriaMessageFormatter.formatBytes(value) ?? String(value)
1759
- );
1760
- }
1761
-
1762
- const formatted: string =
1763
- MonitorCriteriaMessageFormatter.formatNumber(value, {
1764
- maximumFractionDigits: 2,
1765
- }) ?? String(value);
1766
-
1767
- return unit ? `${formatted} ${unit}` : formatted;
1826
+ return MetricValueFormatter.format({ value: value, unit: unit });
1768
1827
  }
1769
1828
 
1770
1829
  private static describeDatabaseCollectionErrorObservation(input: {
@@ -0,0 +1,158 @@
1
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
2
+ import NetworkDeviceService from "../../Services/NetworkDeviceService";
3
+ import QueryHelper from "../../Types/Database/QueryHelper";
4
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
5
+ import ObjectID from "../../../Types/ObjectID";
6
+ import DeviceMacLearningUtil, {
7
+ MacLearningCandidateDevice,
8
+ MacLearningIpBinding,
9
+ MacLearningPlanEntry,
10
+ } from "../../../Utils/Monitor/DeviceMacLearningUtil";
11
+ import logger from "../Logger";
12
+
13
+ /*
14
+ * Fills NetworkDevice.macAddress from a walked router's ARP table.
15
+ *
16
+ * Runs once per successful walk that carried ARP entries, after the
17
+ * endpoint inventory has been written. The decision of WHICH devices to
18
+ * stamp is DeviceMacLearningUtil.planMacLearning (pure, tested without a
19
+ * database); this only fetches the candidates and applies the plan.
20
+ *
21
+ * The write is hook-free and compare-and-set: a row is stamped only while
22
+ * its MAC column still holds what the plan saw (NULL for a first learn, the
23
+ * previously learned MAC for a correction), so an operator who types a MAC
24
+ * between the read and the write keeps theirs, and two walks racing on one
25
+ * row cannot both land. A stable site plans nothing - every device already
26
+ * holds what the table says - so it costs one indexed read per walk and no
27
+ * writes, which is what keeps this affordable at fleet scale.
28
+ *
29
+ * The write cannot tell the caller whether the compare-and-set matched a
30
+ * row, so the result counts what was PLANNED, not what landed.
31
+ */
32
+
33
+ // Same chunking NetworkDeviceService.getDevicesByHostnames uses.
34
+ const HOSTNAME_LOOKUP_CHUNK_SIZE: number = 500;
35
+
36
+ export interface MacLearningResult {
37
+ candidateDeviceCount: number;
38
+ // Writes issued. A compare-and-set that matched nothing is not subtracted.
39
+ plannedCount: number;
40
+ }
41
+
42
+ export default class NetworkDeviceMacLearningUtil {
43
+ public static async learnFromArpBindings(data: {
44
+ projectId: ObjectID;
45
+ // The router (or L3 switch) whose ARP table this is.
46
+ observingDeviceId: ObjectID;
47
+ observingSiteId: ObjectID | undefined;
48
+ ipBindings: Array<MacLearningIpBinding>;
49
+ }): Promise<MacLearningResult> {
50
+ const addresses: Array<string> = Array.from(
51
+ new Set<string>(
52
+ data.ipBindings
53
+ .map((binding: MacLearningIpBinding) => {
54
+ return (binding.ipAddress || "").trim();
55
+ })
56
+ .filter((ip: string) => {
57
+ return ip.length > 0;
58
+ }),
59
+ ),
60
+ );
61
+
62
+ if (addresses.length === 0) {
63
+ return { candidateDeviceCount: 0, plannedCount: 0 };
64
+ }
65
+
66
+ /*
67
+ * Every live device in the project registered at one of the bound
68
+ * addresses. Not filtered by site or by an empty MAC column in SQL:
69
+ * the planner needs to SEE a second device at the same address, or a
70
+ * device that already has a MAC, to refuse correctly - a query that
71
+ * hid them would make "exactly one" true by accident.
72
+ */
73
+ const candidates: Array<MacLearningCandidateDevice> = [];
74
+
75
+ for (
76
+ let offset: number = 0;
77
+ offset < addresses.length;
78
+ offset += HOSTNAME_LOOKUP_CHUNK_SIZE
79
+ ) {
80
+ const chunk: Array<string> = addresses.slice(
81
+ offset,
82
+ offset + HOSTNAME_LOOKUP_CHUNK_SIZE,
83
+ );
84
+
85
+ const devices: Array<NetworkDevice> = await NetworkDeviceService.findBy({
86
+ query: {
87
+ projectId: data.projectId,
88
+ hostname: QueryHelper.any(chunk),
89
+ isArchived: false,
90
+ },
91
+ select: {
92
+ _id: true,
93
+ hostname: true,
94
+ siteId: true,
95
+ macAddress: true,
96
+ isMacAddressLearned: true,
97
+ },
98
+ limit: LIMIT_MAX,
99
+ skip: 0,
100
+ props: {
101
+ isRoot: true,
102
+ },
103
+ });
104
+
105
+ for (const device of devices) {
106
+ if (!device.id) {
107
+ continue;
108
+ }
109
+ candidates.push({
110
+ id: device.id.toString(),
111
+ hostname: device.hostname,
112
+ siteId: device.siteId?.toString(),
113
+ macAddress: device.macAddress,
114
+ isMacAddressLearned: device.isMacAddressLearned,
115
+ });
116
+ }
117
+ }
118
+
119
+ const plan: Array<MacLearningPlanEntry> =
120
+ DeviceMacLearningUtil.planMacLearning({
121
+ ipBindings: data.ipBindings,
122
+ devices: candidates,
123
+ observingSiteId: data.observingSiteId?.toString(),
124
+ });
125
+
126
+ let plannedCount: number = 0;
127
+
128
+ for (const entry of plan) {
129
+ await NetworkDeviceService.updateColumnsByIdWithoutHooks({
130
+ id: new ObjectID(entry.deviceId),
131
+ data: {
132
+ macAddress: entry.macAddress,
133
+ isMacAddressLearned: true,
134
+ } as any,
135
+ /*
136
+ * Only while the column still holds what the plan saw - empty, or
137
+ * the learned value being corrected - and never a row that was
138
+ * deleted between the read and this write.
139
+ */
140
+ expectedData: {
141
+ macAddress: entry.previousMacAddress,
142
+ deletedAt: null,
143
+ } as any,
144
+ });
145
+ plannedCount++;
146
+
147
+ logger.debug(
148
+ `Planned MAC ${entry.macAddress} for network device ${entry.deviceId} (${entry.ipAddress}) from the ARP table of device ${data.observingDeviceId.toString()}${
149
+ entry.previousMacAddress
150
+ ? `, correcting the learned ${entry.previousMacAddress}`
151
+ : ""
152
+ }; skipped if the column changed meanwhile.`,
153
+ );
154
+ }
155
+
156
+ return { candidateDeviceCount: candidates.length, plannedCount };
157
+ }
158
+ }
@@ -1,6 +1,7 @@
1
1
  import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
2
2
  import NetworkDeviceService from "../../Services/NetworkDeviceService";
3
3
  import NetworkEndpointService from "../../Services/NetworkEndpointService";
4
+ import NetworkDeviceMacLearningUtil from "./NetworkDeviceMacLearningUtil";
4
5
  import NetworkInterfaceService, {
5
6
  InterfaceWalkUpsertResult,
6
7
  } from "../../Services/NetworkInterfaceService";
@@ -445,6 +446,32 @@ export default class NetworkInventoryUtil {
445
446
  now: now,
446
447
  });
447
448
  }
449
+
450
+ /*
451
+ * --- Device MAC learning (ARP) ---
452
+ * The ARP table also names the MACs of managed devices registered
453
+ * at the addresses it binds - the ping-only registers and handsets
454
+ * that the switches' forwarding tables can put on a port, but only
455
+ * once something has said which MAC is theirs. Stamped on the
456
+ * device, once, while its MAC column is empty. Its own try/catch:
457
+ * the endpoint inventory above is already written, and a failure
458
+ * here must not read as one there.
459
+ */
460
+ if (endpointResult.ipBindings.length > 0) {
461
+ try {
462
+ await NetworkDeviceMacLearningUtil.learnFromArpBindings({
463
+ projectId: data.projectId,
464
+ observingDeviceId: deviceId,
465
+ observingSiteId: ownedDevice.siteId,
466
+ ipBindings: endpointResult.ipBindings,
467
+ });
468
+ } catch (err) {
469
+ logger.error(
470
+ `Failed to learn device MAC addresses from the ARP table of network device ${deviceId.toString()}:`,
471
+ );
472
+ logger.error(err);
473
+ }
474
+ }
448
475
  }
449
476
  } catch (err) {
450
477
  // Inventory bookkeeping must never fail the walk pipeline.
@@ -217,7 +217,17 @@ export async function reconcileEntityRegistryThrottled(data: {
217
217
  try {
218
218
  const promoted: Array<ExtractedEntity> = data.entities.filter(
219
219
  (entity: ExtractedEntity) => {
220
- return REGISTRY_PROMOTED_TYPES.has(entity.entityType);
220
+ /*
221
+ * Two gates, and they answer different questions. The type gate asks
222
+ * "does this KIND of thing belong in the registry"; the per-entity
223
+ * flag asks "is this particular observation the resource's entity of
224
+ * that type, or a duplicate the producer sent alongside it". See
225
+ * `ExtractedEntity.membershipOnly`.
226
+ */
227
+ return (
228
+ !entity.membershipOnly &&
229
+ REGISTRY_PROMOTED_TYPES.has(entity.entityType)
230
+ );
221
231
  },
222
232
  );
223
233
  const retiredHosts: Array<RetiredEntityIdentity> = (
@@ -82,6 +82,20 @@ export interface ExtractedEntity {
82
82
  * the registry attaches them via the existing project label system.
83
83
  */
84
84
  labels?: Array<string>;
85
+ /**
86
+ * When true this entity contributes its key to `entityKeys` on signals but
87
+ * must never mint a registry row.
88
+ *
89
+ * One OTLP resource describes ONE object of any given type — it is a single
90
+ * pod, on a single node, in a single cluster — and the rest of the pipeline
91
+ * relies on that: `scalarEntityKeysFromEntities` keeps the first key per
92
+ * type, and the heuristic resolvers emit at most one entity per type by
93
+ * construction. A producer can nonetheless send two `entity_refs` of the
94
+ * same type on one resource, and promoting both put two rows in Inventory
95
+ * from a single batch. The extra refs stay queryable as membership keys and
96
+ * stop there.
97
+ */
98
+ membershipOnly?: boolean;
85
99
  }
86
100
 
87
101
  /**
@@ -302,6 +316,12 @@ export default class InventoryItem {
302
316
  }): Array<ExtractedEntity> {
303
317
  const out: Array<ExtractedEntity> = [];
304
318
  const seen: Set<string> = new Set<string>();
319
+ /*
320
+ * First usable ref of a type is the one that gets a registry row; any
321
+ * later ref of that same type is demoted to membership-only. See
322
+ * `ExtractedEntity.membershipOnly`.
323
+ */
324
+ const promotedTypes: Set<EntityType> = new Set<EntityType>();
305
325
 
306
326
  for (const ref of data.entityRefs) {
307
327
  const refType: string =
@@ -381,6 +401,15 @@ export default class InventoryItem {
381
401
  }
382
402
  }
383
403
 
404
+ const alreadyPromoted: boolean = promotedTypes.has(entityType);
405
+ if (alreadyPromoted) {
406
+ logger.debug(
407
+ `OTLP resource declares more than one entity_ref of type "${refType}"; keeping the first as the entity and the rest as membership keys only.`,
408
+ );
409
+ } else {
410
+ promotedTypes.add(entityType);
411
+ }
412
+
384
413
  out.push({
385
414
  entityType,
386
415
  entityKey,
@@ -388,6 +417,7 @@ export default class InventoryItem {
388
417
  ...(Object.keys(descriptiveAttributes).length > 0
389
418
  ? { descriptiveAttributes }
390
419
  : {}),
420
+ ...(alreadyPromoted ? { membershipOnly: true } : {}),
391
421
  });
392
422
  }
393
423
 
@@ -570,7 +600,32 @@ export default class InventoryItem {
570
600
  return { entityType: EntityType.KubernetesNamespace, id };
571
601
  },
572
602
 
573
- // k8s.node — cluster + k8s.node.uid/k8s.node.name.
603
+ /*
604
+ * k8s.node — cluster + k8s.node.name, falling back to k8s.node.uid only
605
+ * when no name is available.
606
+ *
607
+ * The name is preferred BECAUSE the uid is not universally reported and
608
+ * identity must not depend on which optional attributes a particular
609
+ * producer happens to carry. The shipped Kubernetes agent proves the
610
+ * point: its Deployment collector's `k8s_cluster` receiver reports node
611
+ * metrics with uid AND name, while its DaemonSet collector
612
+ * (kubeletstats / hostmetrics / cAdvisor / filelog) only ever has the
613
+ * name — the kubelet summary API carries no node uid and `k8sattributes`
614
+ * is not asked to extract one. A uid-first rule therefore hashed the same
615
+ * node two ways and put two rows in Inventory, both `discovered`, both
616
+ * displaying the same node name (`k8s.node.name` is a descriptive
617
+ * attribute on the uid-keyed row, so the two were indistinguishable in
618
+ * the list). Neither row aged out, because both pipelines keep reporting.
619
+ *
620
+ * Name-keying is also what the rest of OneUptime already assumes: the
621
+ * chart's own comment at configmap-daemonset.yaml says "OneUptime keys the
622
+ * k8s.node entity on k8s.cluster.name + k8s.node.name", and cluster and
623
+ * host identity are name-based for the same reason (see
624
+ * k8sClusterIdentity and the host resolver above).
625
+ *
626
+ * The uid stays available as a descriptive attribute, and remains the
627
+ * identity for the one resource that genuinely has nothing else.
628
+ */
574
629
  (attrs: EntityAttributes) => {
575
630
  const nodeUid: string | null = InventoryItem.str(attrs, "k8s.node.uid");
576
631
  const nodeName: string | null = InventoryItem.str(attrs, "k8s.node.name");
@@ -580,15 +635,27 @@ export default class InventoryItem {
580
635
  const id: Dictionary<string> = {
581
636
  ...(InventoryItem.k8sClusterIdentity(attrs) || {}),
582
637
  };
583
- if (nodeUid) {
584
- id["k8s.node.uid"] = nodeUid;
585
- } else if (nodeName) {
638
+ if (nodeName) {
586
639
  id["k8s.node.name"] = nodeName;
640
+ } else {
641
+ id["k8s.node.uid"] = nodeUid!;
587
642
  }
588
643
  return { entityType: EntityType.KubernetesNode, id };
589
644
  },
590
645
 
591
- // k8s.pod — cluster + namespace + k8s.pod.uid/k8s.pod.name.
646
+ /*
647
+ * k8s.pod — cluster + namespace + k8s.pod.name, falling back to
648
+ * k8s.pod.uid only when no name is available. Name-preferring for the
649
+ * same reason as k8s.node above: a pod observed by one pipeline that
650
+ * resolves its uid and another that does not is one pod, not two.
651
+ *
652
+ * A pod name is unique within a namespace at any instant, so this also
653
+ * makes a restarted StatefulSet pod (same name, new uid) stay one
654
+ * inventory row instead of minting a fresh one per incarnation — which
655
+ * is the behaviour an inventory wants. Incarnations remain separable on
656
+ * the signals themselves, which still carry `k8s.pod.uid` as a resource
657
+ * attribute, and the 24h pod TTL still reaps a name that stops reporting.
658
+ */
592
659
  (attrs: EntityAttributes) => {
593
660
  const podUid: string | null = InventoryItem.str(attrs, "k8s.pod.uid");
594
661
  const podName: string | null = InventoryItem.str(attrs, "k8s.pod.name");
@@ -599,10 +666,10 @@ export default class InventoryItem {
599
666
  ...(InventoryItem.k8sClusterIdentity(attrs) || {}),
600
667
  };
601
668
  InventoryItem.addIfPresent(id, attrs, "k8s.namespace.name");
602
- if (podUid) {
603
- id["k8s.pod.uid"] = podUid;
604
- } else if (podName) {
669
+ if (podName) {
605
670
  id["k8s.pod.name"] = podName;
671
+ } else {
672
+ id["k8s.pod.uid"] = podUid!;
606
673
  }
607
674
  return { entityType: EntityType.KubernetesPod, id };
608
675
  },
@@ -820,11 +887,18 @@ export default class InventoryItem {
820
887
  "telemetry.sdk.name",
821
888
  "telemetry.sdk.version",
822
889
  ],
890
+ /*
891
+ * The uids are descriptive rather than identifying (see the node / pod
892
+ * resolvers): keeping them here means the Inventory row still shows the
893
+ * uid of the object it is currently tracking, without that uid being able
894
+ * to fork the row's identity.
895
+ */
823
896
  [EntityType.KubernetesNode]: [
824
897
  "k8s.node.name",
898
+ "k8s.node.uid",
825
899
  "node.kubernetes.io/instance-type",
826
900
  ],
827
- [EntityType.KubernetesPod]: ["k8s.pod.name"],
901
+ [EntityType.KubernetesPod]: ["k8s.pod.name", "k8s.pod.uid"],
828
902
  [EntityType.Container]: [
829
903
  "container.image.name",
830
904
  "container.image.tag",
@@ -530,3 +530,35 @@ describe("the Add to Monitoring dialog", () => {
530
530
  }
531
531
  });
532
532
  });
533
+
534
+ /*
535
+ * The MAC the map already knows for a neighbour rides into the create form,
536
+ * so a device adopted from the drawer keeps its switch port from its first
537
+ * poll rather than waiting for a router's ARP table to say it again.
538
+ */
539
+ describe("the MAC Address the map knows is pre-filled", () => {
540
+ beforeEach(() => {
541
+ captured = null;
542
+ neighborQueries = [];
543
+ neighborRows = [{ _id: "switch-1", probeId: "probe-a", siteId: "site-a" }];
544
+ });
545
+
546
+ afterEach(() => {
547
+ cleanup();
548
+ });
549
+
550
+ test("a neighbour with a known MAC opens with it filled in", async () => {
551
+ const props: CapturedModalProps = await openDialog({
552
+ ...PHONE_NODE,
553
+ macAddress: "aa:bb:cc:dd:ee:01",
554
+ });
555
+
556
+ expect(props.initialValues["macAddress"]).toBe("aa:bb:cc:dd:ee:01");
557
+ });
558
+
559
+ test("a neighbour with no known MAC opens with the field empty", async () => {
560
+ const props: CapturedModalProps = await openDialog(PHONE_NODE);
561
+
562
+ expect(props.initialValues["macAddress"]).toBeUndefined();
563
+ });
564
+ });