@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.
- package/Models/DatabaseModels/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { normalizeMac } from "./EndpointAttachmentUtil";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Pure planner for learning a managed device's MAC address from a walked
|
|
5
|
+
* router's ARP table.
|
|
6
|
+
*
|
|
7
|
+
* WHY. A ping-only device is put on its switch port by the switch's
|
|
8
|
+
* forwarding table, which knows MACs and nothing else - and a ping-only
|
|
9
|
+
* device is exactly the device nothing ever asked for its MAC. A router's
|
|
10
|
+
* ARP table is the bridge: it binds the address the device is registered at
|
|
11
|
+
* to the MAC the switches learned. Writing that MAC onto the device once is
|
|
12
|
+
* what keeps the device on its port after the ARP entry ages out, and after
|
|
13
|
+
* the address changes.
|
|
14
|
+
*
|
|
15
|
+
* Deterministic and side-effect free, like EndpointAttachmentUtil: the
|
|
16
|
+
* server util (NetworkDeviceMacLearningUtil) fetches the candidate devices,
|
|
17
|
+
* hands them here, and applies whatever comes back.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface MacLearningIpBinding {
|
|
21
|
+
macAddress: string;
|
|
22
|
+
ipAddress: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One NetworkDevice row, reduced to what the plan needs.
|
|
26
|
+
export interface MacLearningCandidateDevice {
|
|
27
|
+
id: string;
|
|
28
|
+
hostname?: string | undefined;
|
|
29
|
+
// A string, never an ObjectID: the plan keys on it.
|
|
30
|
+
siteId?: string | undefined;
|
|
31
|
+
macAddress?: string | undefined;
|
|
32
|
+
/*
|
|
33
|
+
* True when the stored MAC was written by an earlier pass rather than
|
|
34
|
+
* typed. Only such a value may be corrected; a typed one is never touched.
|
|
35
|
+
*/
|
|
36
|
+
isMacAddressLearned?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MacLearningPlanEntry {
|
|
40
|
+
deviceId: string;
|
|
41
|
+
macAddress: string;
|
|
42
|
+
ipAddress: string;
|
|
43
|
+
/*
|
|
44
|
+
* What the column held when the plan was made: null for an empty column,
|
|
45
|
+
* the previously learned MAC for a correction. The writer compares-and-
|
|
46
|
+
* sets against it, so a value that changed under the plan is left alone.
|
|
47
|
+
*/
|
|
48
|
+
previousMacAddress: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* null, undefined, "" and whitespace all read as the same "no site" key -
|
|
53
|
+
* the convention NetworkDeviceLinkRuleUtil and the topology builder use.
|
|
54
|
+
*/
|
|
55
|
+
export function siteKeyOf(siteId: string | null | undefined): string {
|
|
56
|
+
return (siteId || "").trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default class DeviceMacLearningUtil {
|
|
60
|
+
/*
|
|
61
|
+
* Which devices get which MAC. The rules, every one of them a refusal:
|
|
62
|
+
*
|
|
63
|
+
* - Only a device in the SAME site as the router that reported the
|
|
64
|
+
* binding. Every branch has a 10.0.0.5; a project-wide match would
|
|
65
|
+
* stamp one store's register with another store's MAC.
|
|
66
|
+
* - Only a device whose MAC column is EMPTY, or holds a MAC an earlier
|
|
67
|
+
* pass LEARNED that this table now contradicts. What an operator typed
|
|
68
|
+
* is theirs and is never touched. A learned value is the pass's own
|
|
69
|
+
* answer, and an ARP table is exactly the evidence that can be stale
|
|
70
|
+
* (an address re-leased the day before the register was plugged in
|
|
71
|
+
* stamps the previous holder's MAC), so the pass corrects itself when
|
|
72
|
+
* the address is later bound to a different MAC.
|
|
73
|
+
* - Only an address exactly one candidate device answers at. Two
|
|
74
|
+
* devices registered at one address is a contradiction, and a wrong
|
|
75
|
+
* MAC drawn as a cable is worse than none.
|
|
76
|
+
* - Only an address the ARP table binds to exactly one MAC. Proxy ARP
|
|
77
|
+
* and stale duplicates put two MACs behind one address; neither can
|
|
78
|
+
* be trusted.
|
|
79
|
+
*
|
|
80
|
+
* Output is sorted by device id, so a given input always plans the same
|
|
81
|
+
* writes in the same order.
|
|
82
|
+
*/
|
|
83
|
+
public static planMacLearning(data: {
|
|
84
|
+
ipBindings: Array<MacLearningIpBinding>;
|
|
85
|
+
devices: Array<MacLearningCandidateDevice>;
|
|
86
|
+
observingSiteId?: string | null | undefined;
|
|
87
|
+
}): Array<MacLearningPlanEntry> {
|
|
88
|
+
const macsByIp: Map<string, Set<string>> = new Map<string, Set<string>>();
|
|
89
|
+
for (const binding of data.ipBindings) {
|
|
90
|
+
const mac: string | undefined = normalizeMac(binding.macAddress);
|
|
91
|
+
const ip: string = (binding.ipAddress || "").trim();
|
|
92
|
+
if (!mac || !ip) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const macs: Set<string> | undefined = macsByIp.get(ip);
|
|
96
|
+
if (macs) {
|
|
97
|
+
macs.add(mac);
|
|
98
|
+
} else {
|
|
99
|
+
macsByIp.set(ip, new Set<string>([mac]));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const siteKey: string = siteKeyOf(data.observingSiteId);
|
|
104
|
+
|
|
105
|
+
const devicesByIp: Map<string, Array<MacLearningCandidateDevice>> = new Map<
|
|
106
|
+
string,
|
|
107
|
+
Array<MacLearningCandidateDevice>
|
|
108
|
+
>();
|
|
109
|
+
for (const device of data.devices) {
|
|
110
|
+
const hostname: string = (device.hostname || "").trim();
|
|
111
|
+
if (!hostname || !macsByIp.has(hostname)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (siteKeyOf(device.siteId) !== siteKey) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const bucket: Array<MacLearningCandidateDevice> | undefined =
|
|
118
|
+
devicesByIp.get(hostname);
|
|
119
|
+
if (bucket) {
|
|
120
|
+
bucket.push(device);
|
|
121
|
+
} else {
|
|
122
|
+
devicesByIp.set(hostname, [device]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const plan: Array<MacLearningPlanEntry> = [];
|
|
127
|
+
|
|
128
|
+
for (const [ip, candidates] of devicesByIp) {
|
|
129
|
+
// Two devices at one address: neither can be the one.
|
|
130
|
+
if (candidates.length !== 1) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const macs: Set<string> = macsByIp.get(ip)!;
|
|
134
|
+
// Two MACs behind one address: neither can be trusted.
|
|
135
|
+
if (macs.size !== 1) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const device: MacLearningCandidateDevice = candidates[0]!;
|
|
139
|
+
const learnedMac: string = Array.from(macs)[0]!;
|
|
140
|
+
const storedMac: string | undefined = normalizeMac(device.macAddress);
|
|
141
|
+
const hasStoredValue: boolean =
|
|
142
|
+
(device.macAddress || "").trim().length > 0;
|
|
143
|
+
|
|
144
|
+
if (!hasStoredValue) {
|
|
145
|
+
plan.push({
|
|
146
|
+
deviceId: device.id,
|
|
147
|
+
macAddress: learnedMac,
|
|
148
|
+
ipAddress: ip,
|
|
149
|
+
previousMacAddress: null,
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Typed by somebody, or already what the table says: leave it.
|
|
155
|
+
if (device.isMacAddressLearned !== true || storedMac === learnedMac) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
plan.push({
|
|
160
|
+
deviceId: device.id,
|
|
161
|
+
macAddress: learnedMac,
|
|
162
|
+
ipAddress: ip,
|
|
163
|
+
/*
|
|
164
|
+
* The raw stored value, not the normalised one: the writer's
|
|
165
|
+
* compare-and-set has to match what the column actually holds.
|
|
166
|
+
*/
|
|
167
|
+
previousMacAddress: device.macAddress || null,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
plan.sort((a: MacLearningPlanEntry, b: MacLearningPlanEntry) => {
|
|
172
|
+
return a.deviceId < b.deviceId ? -1 : a.deviceId > b.deviceId ? 1 : 0;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return plan;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import MetricUnitUtil from "../MetricUnitUtil";
|
|
2
|
+
import ValueFormatter from "../ValueFormatter";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* How a metric sample is written into alert / incident notification text.
|
|
6
|
+
*
|
|
7
|
+
* The dashboard has always scaled metric values before showing them —
|
|
8
|
+
* `ValueFormatter` turns 1073741824 bytes into "1.07 GB" for every chart,
|
|
9
|
+
* gauge and value tile. The notification path never did. The same memory
|
|
10
|
+
* breach that reads "1.07 GB" on the monitor page arrived in the on-call
|
|
11
|
+
* engineer's inbox as the bare digits "1073741824 By", and in the
|
|
12
|
+
* "Affected Resources" tables as a Value column with no unit at all — a
|
|
13
|
+
* column of ten-digit numbers the reader has to divide by 2^30 in their
|
|
14
|
+
* head at 3am to find out whether the pod is anywhere near its limit.
|
|
15
|
+
*
|
|
16
|
+
* This module is the single place that decides how a (value, unit) pair
|
|
17
|
+
* is rendered for a notification, so the breaching-samples table, the
|
|
18
|
+
* "Filter Conditions Met" sentence, the observation summary and the
|
|
19
|
+
* per-platform resource tables cannot drift apart inside one email.
|
|
20
|
+
*
|
|
21
|
+
* WHY IT WRAPS ValueFormatter RATHER THAN BEING A CALL TO IT.
|
|
22
|
+
*
|
|
23
|
+
* Three things differ between a chart axis and an alert body:
|
|
24
|
+
*
|
|
25
|
+
* 1. ABBREVIATION. `ValueFormatter` shortens any number over 1000 to
|
|
26
|
+
* "1.5K" — right for a tick label, which has no horizontal room, and
|
|
27
|
+
* wrong for an alert, where "restarted 1.5K times" hides the
|
|
28
|
+
* difference between 1500 and 1549 that the reader opened the table
|
|
29
|
+
* to find. Here, only a unit that names an actual SCALE LADDER
|
|
30
|
+
* (bytes → KB → MB, seconds → min → hours) may rescale a number.
|
|
31
|
+
* Everything else keeps its digits and simply gets its unit spelled
|
|
32
|
+
* out: "1234567 cores", not "1.23M cores".
|
|
33
|
+
*
|
|
34
|
+
* 2. PREFIXED UNITS. A user who typed a threshold in "GB" makes every
|
|
35
|
+
* sample arrive carrying the unit "GB", and ValueFormatter has a
|
|
36
|
+
* ladder for "bytes" but not for "GB" — so 2500 of them rendered
|
|
37
|
+
* "2.5K GB" instead of "2.5 TB". Anything in a MetricUnitUtil family
|
|
38
|
+
* is converted to that family's base before the ladder runs.
|
|
39
|
+
*
|
|
40
|
+
* 3. UNITS WORTH HIDING. OTel's dimensionless "1" and UCUM's
|
|
41
|
+
* annotation-only units ("{restarts}", "{packets}") are not
|
|
42
|
+
* dimensions, and printing them produces the "0.06 1" that
|
|
43
|
+
* CompareCriteria and MonitorCriteriaObservationBuilder each grew
|
|
44
|
+
* their own guard against. That rule lives here now.
|
|
45
|
+
*/
|
|
46
|
+
export default class MetricValueFormatter {
|
|
47
|
+
/*
|
|
48
|
+
* UCUM annotation-only units — "{thread}", "{packets}", "{errors}".
|
|
49
|
+
* The braces describe what is being counted; the value is a plain
|
|
50
|
+
* count and the text inside them is not a unit a reader should see.
|
|
51
|
+
*/
|
|
52
|
+
private static readonly annotationOnlyUnitPattern: RegExp = /^\{[^{}]*\}$/;
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* The per-platform metric catalogs (Kubernetes, Ceph, Proxmox, Docker
|
|
56
|
+
* Swarm) were written for humans reading a metric picker, so they spell
|
|
57
|
+
* two dimensionless concepts in English rather than in UCUM. "count"
|
|
58
|
+
* appears on 43 catalog entries and means exactly what UCUM's "1" means
|
|
59
|
+
* on a counter — printing it gives "15 count", which reads as a typo.
|
|
60
|
+
* "ratio" means what UCUM's "1" means on a fraction metric: a value in
|
|
61
|
+
* [0, 1] that a reader wants as a percentage.
|
|
62
|
+
*
|
|
63
|
+
* Rewriting them here rather than in each catalog keeps the catalogs
|
|
64
|
+
* legible to the people who edit them and still gives every consumer of
|
|
65
|
+
* this formatter one vocabulary.
|
|
66
|
+
*/
|
|
67
|
+
private static readonly informalUnitAliases: Record<string, string> = {
|
|
68
|
+
count: "",
|
|
69
|
+
counts: "",
|
|
70
|
+
ratio: "1",
|
|
71
|
+
fraction: "1",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Metrics whose unit says "seconds" but whose VALUE is a POINT in time
|
|
76
|
+
* rather than a length of one.
|
|
77
|
+
*
|
|
78
|
+
* Proxmox has three: pve_replication_last_sync_timestamp_seconds,
|
|
79
|
+
* _last_try_ and _next_sync_ — all carrying `unit: "seconds"`, all
|
|
80
|
+
* holding a Unix epoch. Sent through the duration ladder, an epoch of
|
|
81
|
+
* 1750000000 renders "20.25K days", which is not merely unhelpful: it
|
|
82
|
+
* READS as a duration, so a replication job that synced a minute ago
|
|
83
|
+
* looks 55 years stale. The bare number at least cannot be misread as
|
|
84
|
+
* something it is not.
|
|
85
|
+
*
|
|
86
|
+
* Same shape as ValueFormatter's kubeletstats exclusion: a narrow,
|
|
87
|
+
* name-anchored carve-out for a unit the exporter declared wrongly.
|
|
88
|
+
*/
|
|
89
|
+
private static readonly absoluteTimestampMetricPattern: RegExp =
|
|
90
|
+
/(^|[._])timestamps?([._]seconds?)?$/i;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether `unit` names a real dimension for `metricName` — i.e. whether
|
|
94
|
+
* it is worth attaching to a number at all. False for the empty unit,
|
|
95
|
+
* for the dimensionless "1" on a non-fraction metric, and for
|
|
96
|
+
* annotation-only units.
|
|
97
|
+
*/
|
|
98
|
+
public static hasDisplayableUnit(
|
|
99
|
+
unit: string | null | undefined,
|
|
100
|
+
metricName?: string | null | undefined,
|
|
101
|
+
): boolean {
|
|
102
|
+
const trimmed: string = MetricValueFormatter.canonicalizeUnit(unit);
|
|
103
|
+
|
|
104
|
+
if (!trimmed) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* "1" is OTel's dimensionless marker. On a `.utilization` / `.ratio`
|
|
110
|
+
* metric it means "this is a [0, 1] fraction", which IS a scale (× 100
|
|
111
|
+
* into a percent) and is why the fraction case is excluded here rather
|
|
112
|
+
* than folded in. On anything else it is noise: "0.06 1" is not just
|
|
113
|
+
* ugly, it reads as 1% when the value is 6%.
|
|
114
|
+
*/
|
|
115
|
+
if (trimmed === "1") {
|
|
116
|
+
return ValueFormatter.isFractionMetric(metricName || undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return !MetricValueFormatter.annotationOnlyUnitPattern.test(trimmed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format one metric sample for a notification.
|
|
124
|
+
*
|
|
125
|
+
* Units with a scale ladder are rescaled, exactly as the dashboard does:
|
|
126
|
+
* format({ value: 1073741824, unit: "By" }) → "1.07 GB"
|
|
127
|
+
* format({ value: 2500, unit: "GB" }) → "2.5 TB"
|
|
128
|
+
* format({ value: 1500, unit: "ms" }) → "1.5 sec"
|
|
129
|
+
* format({ value: 3661, unit: "seconds" }) → "1.02 hours"
|
|
130
|
+
* format({ value: 1500000, unit: "By/s" }) → "1.5 MB/s"
|
|
131
|
+
* format({ value: 0.2534, unit: "1",
|
|
132
|
+
* metricName: "system.cpu.utilization" }) → "25.34%"
|
|
133
|
+
* format({ value: 87.5, unit: "%" }) → "87.50%"
|
|
134
|
+
*
|
|
135
|
+
* Units without one keep every digit and are merely labelled:
|
|
136
|
+
* format({ value: 1234567, unit: "cores" }) → "1234567 cores"
|
|
137
|
+
* format({ value: 95, unit: "Cel" }) → "95 Celsius"
|
|
138
|
+
*
|
|
139
|
+
* A value with no dimension to report is left bare:
|
|
140
|
+
* format({ value: 1500 }) → "1500"
|
|
141
|
+
* format({ value: 1500, unit: "{restarts}" }) → "1500"
|
|
142
|
+
* format({ value: 3.14159 }) → "3.14"
|
|
143
|
+
*/
|
|
144
|
+
public static format(input: {
|
|
145
|
+
value: number;
|
|
146
|
+
unit?: string | null | undefined;
|
|
147
|
+
metricName?: string | null | undefined;
|
|
148
|
+
}): string {
|
|
149
|
+
if (!Number.isFinite(input.value)) {
|
|
150
|
+
return String(input.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unit: string = MetricValueFormatter.canonicalizeUnit(input.unit);
|
|
154
|
+
const metricName: string | undefined = input.metricName || undefined;
|
|
155
|
+
|
|
156
|
+
if (!MetricValueFormatter.hasDisplayableUnit(unit, metricName)) {
|
|
157
|
+
return MetricValueFormatter.formatBareNumber(input.value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const formatOptions: { metricName?: string } = metricName
|
|
161
|
+
? { metricName: metricName }
|
|
162
|
+
: {};
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* Percent first. ValueFormatter renders "%" and the fraction-metric
|
|
166
|
+
* "1" as a two-decimal percentage, and neither is reachable through
|
|
167
|
+
* the ladder path below (there is no percent threshold table, and the
|
|
168
|
+
* percent family's base unit "%" has no ladder either).
|
|
169
|
+
*/
|
|
170
|
+
if (
|
|
171
|
+
ValueFormatter.isPercentUnit(unit) ||
|
|
172
|
+
(unit === "1" && ValueFormatter.isFractionMetric(metricName))
|
|
173
|
+
) {
|
|
174
|
+
return ValueFormatter.formatValue(input.value, unit, formatOptions);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const scalable: { value: number; unit: string } | null =
|
|
178
|
+
MetricValueFormatter.isAbsoluteTimestampMetric(metricName)
|
|
179
|
+
? null
|
|
180
|
+
: MetricValueFormatter.toScalableUnit(input.value, unit);
|
|
181
|
+
|
|
182
|
+
if (scalable) {
|
|
183
|
+
return ValueFormatter.formatValue(
|
|
184
|
+
scalable.value,
|
|
185
|
+
scalable.unit,
|
|
186
|
+
formatOptions,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* No ladder for this unit: label the number, leave its digits alone.
|
|
192
|
+
*
|
|
193
|
+
* The SYMBOL rather than the spelled-out name, because this suffix
|
|
194
|
+
* sits directly against a number, where the ladder path also emits
|
|
195
|
+
* symbols ("1.07 GB", "1.5 sec"). getReadableUnit is for the "Unit:"
|
|
196
|
+
* label line, where a name reads better than a code — and it is
|
|
197
|
+
* actively wrong here for compound units, which it rewrites: "m/s2"
|
|
198
|
+
* becomes "m per s2" and "10*3/uL" becomes "10*3 per uL", both longer
|
|
199
|
+
* and less recognisable than what the exporter wrote. getCompactUnit
|
|
200
|
+
* keeps the slash and still expands what is worth expanding
|
|
201
|
+
* ("Cel" → "°C", "KiBy" → "KiB").
|
|
202
|
+
*/
|
|
203
|
+
const unitSymbol: string =
|
|
204
|
+
ValueFormatter.getCompactUnit(unit, formatOptions) || unit;
|
|
205
|
+
|
|
206
|
+
return `${MetricValueFormatter.formatBareNumber(input.value)} ${unitSymbol}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The spelled-out name of a unit, for the "Unit:" line of a root cause —
|
|
211
|
+
* "Bytes" rather than the UCUM "By" the exporter wrote. Returns null when
|
|
212
|
+
* the unit names no dimension, so the caller can drop the line entirely
|
|
213
|
+
* instead of printing the noise "Unit: 1".
|
|
214
|
+
*/
|
|
215
|
+
public static getReadableUnit(
|
|
216
|
+
unit: string | null | undefined,
|
|
217
|
+
metricName?: string | null | undefined,
|
|
218
|
+
): string | null {
|
|
219
|
+
if (!MetricValueFormatter.hasDisplayableUnit(unit, metricName)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const readable: string = ValueFormatter.getReadableUnit(
|
|
224
|
+
MetricValueFormatter.canonicalizeUnit(unit),
|
|
225
|
+
metricName ? { metricName: metricName } : {},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return readable.trim() ? readable : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/*
|
|
232
|
+
* Trim the unit and rewrite the catalogs' English spellings of "no
|
|
233
|
+
* dimension" into the UCUM ones the rest of the pipeline understands.
|
|
234
|
+
* Anything else is returned untouched — including its casing, which
|
|
235
|
+
* matters because an unrecognised unit is echoed verbatim next to the
|
|
236
|
+
* value ("5 widgets", not "5 Widgets").
|
|
237
|
+
*/
|
|
238
|
+
private static canonicalizeUnit(unit: string | null | undefined): string {
|
|
239
|
+
const trimmed: string = (unit || "").trim();
|
|
240
|
+
|
|
241
|
+
if (!trimmed) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const alias: string | undefined =
|
|
246
|
+
MetricValueFormatter.informalUnitAliases[trimmed.toLowerCase()];
|
|
247
|
+
|
|
248
|
+
return alias === undefined ? trimmed : alias;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Whether this metric's value is a point in time rather than a quantity,
|
|
253
|
+
* and so must never be run through a scale ladder. See
|
|
254
|
+
* absoluteTimestampMetricPattern.
|
|
255
|
+
*/
|
|
256
|
+
public static isAbsoluteTimestampMetric(
|
|
257
|
+
metricName: string | null | undefined,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (!metricName) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return MetricValueFormatter.absoluteTimestampMetricPattern.test(
|
|
264
|
+
metricName.trim(),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* Restate (value, unit) in a unit ValueFormatter has a scale ladder for,
|
|
270
|
+
* or return null when no ladder can apply.
|
|
271
|
+
*
|
|
272
|
+
* Three ways a unit gets there:
|
|
273
|
+
* - it already has one ("bytes", "ms", "s", "ns");
|
|
274
|
+
* - it is a rate whose NUMERATOR does ("By/s" → scale the bytes, keep
|
|
275
|
+
* the "/s"), which is how 1500000 By/s reads "1.5 MB/s";
|
|
276
|
+
* - it is a prefixed member of a MetricUnitUtil family ("GB", "hours"),
|
|
277
|
+
* in which case the value is converted into that family's base first.
|
|
278
|
+
*/
|
|
279
|
+
private static toScalableUnit(
|
|
280
|
+
value: number,
|
|
281
|
+
unit: string,
|
|
282
|
+
): { value: number; unit: string } | null {
|
|
283
|
+
if (!unit) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (ValueFormatter.isScalableUnit(unit)) {
|
|
288
|
+
return { value: value, unit: unit };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (unit.includes("/")) {
|
|
292
|
+
const [numerator, ...denominatorParts] = unit.split("/");
|
|
293
|
+
const denominator: string = denominatorParts.join("/").trim();
|
|
294
|
+
|
|
295
|
+
if (!numerator || !numerator.trim() || !denominator) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const scaledNumerator: { value: number; unit: string } | null =
|
|
300
|
+
MetricValueFormatter.toScalableUnit(value, numerator.trim());
|
|
301
|
+
|
|
302
|
+
if (!scaledNumerator) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
value: scaledNumerator.value,
|
|
308
|
+
unit: `${scaledNumerator.unit}/${denominator}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const baseUnit: string | null = MetricUnitUtil.getFamilyBaseUnit(unit);
|
|
313
|
+
|
|
314
|
+
if (!baseUnit || !ValueFormatter.isScalableUnit(baseUnit)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
value: MetricUnitUtil.convertToMetricUnit({
|
|
320
|
+
value: value,
|
|
321
|
+
fromUnit: unit,
|
|
322
|
+
metricUnit: baseUnit,
|
|
323
|
+
}),
|
|
324
|
+
unit: baseUnit,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
* The unscaled rendering: integers verbatim, everything else rounded to
|
|
330
|
+
* two decimals with trailing zeros dropped (1.50 → "1.5"). This is the
|
|
331
|
+
* behaviour the root-cause tables had before units arrived, and every
|
|
332
|
+
* value without a scale ladder keeps it exactly, so a counter's digits
|
|
333
|
+
* survive the trip to the inbox.
|
|
334
|
+
*/
|
|
335
|
+
private static formatBareNumber(value: number): string {
|
|
336
|
+
if (Number.isInteger(value)) {
|
|
337
|
+
return value.toString();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return Number(value.toFixed(2)).toString();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { JSONObject, JSONValue } from "../../Types/JSON";
|
|
2
|
+
import JSONFunctions from "../../Types/JSONFunctions";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* WHAT A MONITOR TEMPLATE'S CUSTOM FIELDS MEAN, in one place.
|
|
6
|
+
*
|
|
7
|
+
* A template's `customFields` bag is a set of DEFAULTS for the monitors made
|
|
8
|
+
* from it — the answer to issue #3548, where an auto-import rule turned a
|
|
9
|
+
* thousand discovered devices into a thousand monitors whose Vendor, Services
|
|
10
|
+
* and Configuration Item fields were all empty and could only be filled in one
|
|
11
|
+
* monitor at a time.
|
|
12
|
+
*
|
|
13
|
+
* Two callers need the same answer and would drift apart if each wrote its
|
|
14
|
+
* own:
|
|
15
|
+
*
|
|
16
|
+
* - PROVISIONING (NetworkDeviceMonitorTemplateUtil.buildMonitor, reached
|
|
17
|
+
* from the auto-import engine and the alert-policy engine) starts from a
|
|
18
|
+
* monitor with no values at all, so it takes the whole bag.
|
|
19
|
+
* - SYNC (MonitorTemplateService) pushes the template onto monitors that
|
|
20
|
+
* already exist and may already carry values an operator typed in, so it
|
|
21
|
+
* OVERLAYS instead: every field the template actually gives a default for
|
|
22
|
+
* wins, and every other field on the monitor is left exactly as it is.
|
|
23
|
+
*
|
|
24
|
+
* Both go through a deep clone, because a jsonb bag handed to several monitors
|
|
25
|
+
* by reference is one nested edit away from leaking across the fleet — and
|
|
26
|
+
* back onto the template itself.
|
|
27
|
+
*/
|
|
28
|
+
export default class MonitorTemplateCustomFieldUtil {
|
|
29
|
+
/*
|
|
30
|
+
* A defaults bag is only worth writing when there is a value in it, so
|
|
31
|
+
* "template has no defaults" and "template has an empty bag" are the same
|
|
32
|
+
* answer to every caller here. This is what keeps a sync scoped to custom
|
|
33
|
+
* fields alone from reporting that it wrote to a fleet it did not touch.
|
|
34
|
+
*/
|
|
35
|
+
public static hasDefaults(
|
|
36
|
+
customFields: JSONObject | undefined | null,
|
|
37
|
+
): boolean {
|
|
38
|
+
return Object.keys(this.getDefaults(customFields)).length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* The template's defaults, deep-cloned, with the fields it does not actually
|
|
43
|
+
* default DROPPED.
|
|
44
|
+
*
|
|
45
|
+
* A template carries a key for every custom field the project has defined
|
|
46
|
+
* the moment somebody saves the edit form — the form submits its whole
|
|
47
|
+
* values object — so most keys in a freshly saved bag hold nothing. Left in,
|
|
48
|
+
* they would make a sync overwrite real per-monitor values with blanks: a
|
|
49
|
+
* template that only defaults Vendor would silently clear Configuration Item
|
|
50
|
+
* on every monitor in the fleet.
|
|
51
|
+
*
|
|
52
|
+
* `false` and `0` are values, not blanks — a Boolean custom field defaulting
|
|
53
|
+
* to false and a Number defaulting to 0 are both things an operator means.
|
|
54
|
+
*/
|
|
55
|
+
public static getDefaults(
|
|
56
|
+
customFields: JSONObject | undefined | null,
|
|
57
|
+
): JSONObject {
|
|
58
|
+
if (!customFields) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const defaults: JSONObject = {};
|
|
63
|
+
|
|
64
|
+
for (const key of Object.keys(customFields)) {
|
|
65
|
+
const value: JSONValue = customFields[key] as JSONValue;
|
|
66
|
+
|
|
67
|
+
if (this.isBlank(value)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
defaults[key] = value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.clone(defaults);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Overlay the template's defaults onto the values a monitor already has.
|
|
79
|
+
* Returns a new bag; neither input is mutated.
|
|
80
|
+
*/
|
|
81
|
+
public static applyDefaults(data: {
|
|
82
|
+
templateCustomFields: JSONObject | undefined | null;
|
|
83
|
+
monitorCustomFields: JSONObject | undefined | null;
|
|
84
|
+
}): JSONObject {
|
|
85
|
+
return {
|
|
86
|
+
...this.clone(data.monitorCustomFields || {}),
|
|
87
|
+
...this.getDefaults(data.templateCustomFields),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* The whole bag, verbatim apart from being deep-cloned — blanks included.
|
|
93
|
+
*
|
|
94
|
+
* Provisioning uses this rather than getDefaults because there is nothing
|
|
95
|
+
* underneath to protect: a monitor being created has no values of its own,
|
|
96
|
+
* so a blank default and an absent one look identical on the monitor, and
|
|
97
|
+
* copying the bag as written keeps "what the template says" and "what the
|
|
98
|
+
* new monitor got" literally the same object.
|
|
99
|
+
*/
|
|
100
|
+
public static clone(customFields: JSONObject): JSONObject {
|
|
101
|
+
return JSONFunctions.deserialize(JSONFunctions.serialize(customFields));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Nothing an operator would call a default. An empty multi-select arrives as
|
|
106
|
+
* [], a cleared text field as "" (or as spaces, from a form nobody trimmed),
|
|
107
|
+
* and a field never touched as null or undefined.
|
|
108
|
+
*/
|
|
109
|
+
private static isBlank(value: JSONValue): boolean {
|
|
110
|
+
if (value === undefined || value === null) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof value === "string") {
|
|
115
|
+
return value.trim() === "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
return value.length === 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -3,10 +3,10 @@ import MonitorTemplate from "../../Models/DatabaseModels/MonitorTemplate";
|
|
|
3
3
|
import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
|
|
4
4
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
5
5
|
import { JSONObject } from "../../Types/JSON";
|
|
6
|
-
import JSONFunctions from "../../Types/JSONFunctions";
|
|
7
6
|
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
|
|
8
7
|
import MonitorType from "../../Types/Monitor/MonitorType";
|
|
9
8
|
import ObjectID from "../../Types/ObjectID";
|
|
9
|
+
import MonitorTemplateCustomFieldUtil from "./MonitorTemplateCustomFieldUtil";
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
12
|
* Materialises a project MonitorTemplate for one discovered NetworkDevice.
|
|
@@ -379,7 +379,12 @@ export default class NetworkDeviceMonitorTemplateUtil {
|
|
|
379
379
|
return truncated;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
/*
|
|
383
|
+
* Delegated so "what a template's custom fields mean" is written down once
|
|
384
|
+
* — provisioning takes the bag whole, sync overlays it. See
|
|
385
|
+
* MonitorTemplateCustomFieldUtil.
|
|
386
|
+
*/
|
|
382
387
|
private static cloneCustomFields(customFields: JSONObject): JSONObject {
|
|
383
|
-
return
|
|
388
|
+
return MonitorTemplateCustomFieldUtil.clone(customFields);
|
|
384
389
|
}
|
|
385
390
|
}
|