@oneuptime/common 14.0.7 → 14.0.9
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/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +207 -25
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1788 -142
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +805 -19
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +778 -2
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +828 -17
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +21 -9
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1251 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +178 -92
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +582 -20
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2852 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROXMOX_INFERRED_ATTRIBUTE,
|
|
3
|
+
PROXMOX_INFERRED_NOT_REPORTING,
|
|
4
|
+
PROXMOX_NATIVE_PUSH_SERVICE_NAME,
|
|
5
|
+
PROXMOX_SIBLING_REPORT_SCOPE_NAME,
|
|
6
|
+
appendProxmoxSiblingReportsInPlace,
|
|
7
|
+
isProxmoxNativePushResource,
|
|
8
|
+
isProxmoxSiblingReportScope,
|
|
9
|
+
normalizeProxmoxNativePushInPlace,
|
|
10
|
+
readProxmoxNativeNodeStatus,
|
|
11
|
+
resolveProxmoxNativePushClusterName,
|
|
12
|
+
splitProxmoxSiblingInfoWeights,
|
|
13
|
+
} from "../../../../Server/Utils/Telemetry/ProxmoxNativePush";
|
|
14
|
+
import {
|
|
15
|
+
PVE_SNAPSHOT_METRIC_NAMES,
|
|
16
|
+
ProxmoxClusterSnapshotBufferEntry,
|
|
17
|
+
ProxmoxResourceBufferEntry,
|
|
18
|
+
bufferProxmoxSnapshotMetric,
|
|
19
|
+
deriveProxmoxClusterSnapshotExtras,
|
|
20
|
+
} from "../../../../Server/Utils/Telemetry/ProxmoxCephSnapshotScan";
|
|
21
|
+
import {
|
|
22
|
+
ProxmoxAlertTemplate,
|
|
23
|
+
getAllProxmoxAlertTemplates,
|
|
24
|
+
} from "../../../../Types/Monitor/ProxmoxAlertTemplates";
|
|
25
|
+
import { getProxmoxMetricByMetricName } from "../../../../Types/Monitor/ProxmoxMetricCatalog";
|
|
26
|
+
import MonitorStepProxmoxMonitor from "../../../../Types/Monitor/MonitorStepProxmoxMonitor";
|
|
27
|
+
import MetricsAggregationType from "../../../../Types/Metrics/MetricsAggregationType";
|
|
28
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
29
|
+
import { JSONArray, JSONObject, JSONValue } from "../../../../Types/JSON";
|
|
30
|
+
import { describe, expect, test } from "@jest/globals";
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Proxmox VE 9+ native OpenTelemetry push → the pve_* model.
|
|
34
|
+
*
|
|
35
|
+
* The payloads below are built exactly the way PVE::Status::OpenTelemetry
|
|
36
|
+
* (pve-manager) builds them: one request per pvestatd status pass (node,
|
|
37
|
+
* qemu, lxc, storage are flushed separately), resource attributes
|
|
38
|
+
* service.name=proxmox-ve / service.version / proxmox.cluster /
|
|
39
|
+
* proxmox.node, metric names `${prefix}_${key}` from the PVE status hash
|
|
40
|
+
* (counters get `_total`), identity in `node` / `vmid` / `name` / `type` /
|
|
41
|
+
* `storage` datapoint attributes, timeUnixNano = ctime * 1e9 as a JSON
|
|
42
|
+
* number, integers as asInt, fractions as asDouble.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const CTIME_S: number = 1700000000;
|
|
46
|
+
const TIME_NANO: number = CTIME_S * 1_000_000_000;
|
|
47
|
+
|
|
48
|
+
function attrs(values: Record<string, string>): JSONArray {
|
|
49
|
+
return Object.entries(values).map(([key, value]: [string, string]) => {
|
|
50
|
+
return { key, value: { stringValue: value } };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dp(value: number, labels: Record<string, string>): JSONObject {
|
|
55
|
+
const point: JSONObject = {
|
|
56
|
+
timeUnixNano: TIME_NANO,
|
|
57
|
+
attributes: attrs(labels),
|
|
58
|
+
};
|
|
59
|
+
if (Number.isInteger(value)) {
|
|
60
|
+
point["asInt"] = value;
|
|
61
|
+
} else {
|
|
62
|
+
point["asDouble"] = value;
|
|
63
|
+
}
|
|
64
|
+
return point;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function gauge(
|
|
68
|
+
name: string,
|
|
69
|
+
value: number,
|
|
70
|
+
labels: Record<string, string>,
|
|
71
|
+
): JSONObject {
|
|
72
|
+
return { name, unit: "1", gauge: { dataPoints: [dp(value, labels)] } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function counter(
|
|
76
|
+
name: string,
|
|
77
|
+
value: number,
|
|
78
|
+
labels: Record<string, string>,
|
|
79
|
+
): JSONObject {
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
unit: "bytes",
|
|
83
|
+
sum: {
|
|
84
|
+
dataPoints: [dp(value, labels)],
|
|
85
|
+
aggregationTemporality: 2,
|
|
86
|
+
isMonotonic: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pveResource(
|
|
92
|
+
metrics: Array<JSONObject>,
|
|
93
|
+
overrides: {
|
|
94
|
+
cluster?: string;
|
|
95
|
+
node?: string;
|
|
96
|
+
extra?: Record<string, string>;
|
|
97
|
+
} = {},
|
|
98
|
+
): JSONObject {
|
|
99
|
+
return {
|
|
100
|
+
resource: {
|
|
101
|
+
attributes: attrs({
|
|
102
|
+
"service.name": PROXMOX_NATIVE_PUSH_SERVICE_NAME,
|
|
103
|
+
"service.version": "9.0.10/deb1ca707ec72a89",
|
|
104
|
+
"proxmox.cluster": overrides.cluster ?? "homelab",
|
|
105
|
+
"proxmox.node": overrides.node ?? "pve1",
|
|
106
|
+
...(overrides.extra || {}),
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
scopeMetrics: [{ scope: {}, metrics }],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const GIB: number = 1024 * 1024 * 1024;
|
|
114
|
+
|
|
115
|
+
// update_node_status: uptime, cpustat, memory, blockstat, nics.
|
|
116
|
+
function nodePush(node: string = "pve1"): JSONObject {
|
|
117
|
+
const n: Record<string, string> = { node };
|
|
118
|
+
return pveResource(
|
|
119
|
+
[
|
|
120
|
+
gauge("proxmox_node_uptime", 86400, n),
|
|
121
|
+
gauge("proxmox_node_cpustat_cpu", 0.25, n),
|
|
122
|
+
gauge("proxmox_node_cpustat_cpus", 16, n),
|
|
123
|
+
gauge("proxmox_node_cpustat_avg1", 0.5, n),
|
|
124
|
+
counter("proxmox_node_cpustat_user_total", 123456, n),
|
|
125
|
+
gauge("proxmox_node_memory_memtotal", 64 * GIB, n),
|
|
126
|
+
gauge("proxmox_node_memory_memused", 16 * GIB, n),
|
|
127
|
+
gauge("proxmox_node_memory_swaptotal", 8 * GIB, n),
|
|
128
|
+
gauge("proxmox_node_blockstat_total", 100 * GIB, n),
|
|
129
|
+
gauge("proxmox_node_blockstat_used", 40 * GIB, n),
|
|
130
|
+
gauge("proxmox_node_blockstat_avail", 60 * GIB, n),
|
|
131
|
+
counter("proxmox_node_network_receive_total", 999, {
|
|
132
|
+
node,
|
|
133
|
+
device: "vmbr0",
|
|
134
|
+
}),
|
|
135
|
+
],
|
|
136
|
+
{ node },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function guestLabels(data: {
|
|
141
|
+
vmid: string;
|
|
142
|
+
type: string;
|
|
143
|
+
name: string;
|
|
144
|
+
node?: string;
|
|
145
|
+
}): Record<string, string> {
|
|
146
|
+
return {
|
|
147
|
+
vmid: data.vmid,
|
|
148
|
+
node: data.node ?? "pve1",
|
|
149
|
+
name: data.name,
|
|
150
|
+
type: data.type,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// update_qemu_status: every VM on the node, running or stopped.
|
|
155
|
+
function qemuPush(): JSONObject {
|
|
156
|
+
const running: Record<string, string> = guestLabels({
|
|
157
|
+
vmid: "100",
|
|
158
|
+
type: "qemu",
|
|
159
|
+
name: "web",
|
|
160
|
+
});
|
|
161
|
+
const stopped: Record<string, string> = guestLabels({
|
|
162
|
+
vmid: "101",
|
|
163
|
+
type: "qemu",
|
|
164
|
+
name: "old-db",
|
|
165
|
+
});
|
|
166
|
+
return pveResource([
|
|
167
|
+
gauge("proxmox_vm_uptime", 3600, running),
|
|
168
|
+
gauge("proxmox_vm_cpu", 0.5, running),
|
|
169
|
+
gauge("proxmox_vm_cpus", 4, running),
|
|
170
|
+
gauge("proxmox_vm_mem", 2 * GIB, running),
|
|
171
|
+
gauge("proxmox_vm_maxmem", 8 * GIB, running),
|
|
172
|
+
gauge("proxmox_vm_disk", 0, running),
|
|
173
|
+
gauge("proxmox_vm_maxdisk", 32 * GIB, running),
|
|
174
|
+
counter("proxmox_vm_netin_total", 1000, running),
|
|
175
|
+
counter("proxmox_vm_netout_total", 2000, running),
|
|
176
|
+
counter("proxmox_vm_diskread_total", 3000, running),
|
|
177
|
+
counter("proxmox_vm_diskwrite_total", 4000, running),
|
|
178
|
+
gauge("proxmox_vm_pressurecpusome", 0.1, running),
|
|
179
|
+
gauge("proxmox_vm_uptime", 0, stopped),
|
|
180
|
+
gauge("proxmox_vm_cpu", 0, stopped),
|
|
181
|
+
gauge("proxmox_vm_cpus", 2, stopped),
|
|
182
|
+
gauge("proxmox_vm_mem", 0, stopped),
|
|
183
|
+
gauge("proxmox_vm_maxmem", 4 * GIB, stopped),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// update_lxc_status.
|
|
188
|
+
function lxcPush(): JSONObject {
|
|
189
|
+
const ct: Record<string, string> = guestLabels({
|
|
190
|
+
vmid: "200",
|
|
191
|
+
type: "lxc",
|
|
192
|
+
name: "dns",
|
|
193
|
+
});
|
|
194
|
+
return pveResource([
|
|
195
|
+
gauge("proxmox_vm_uptime", 7200, ct),
|
|
196
|
+
gauge("proxmox_vm_cpu", 0.05, ct),
|
|
197
|
+
gauge("proxmox_vm_cpus", 1, ct),
|
|
198
|
+
gauge("proxmox_vm_mem", 256 * 1024 * 1024, ct),
|
|
199
|
+
gauge("proxmox_vm_maxmem", 512 * 1024 * 1024, ct),
|
|
200
|
+
gauge("proxmox_vm_disk", 3 * GIB, ct),
|
|
201
|
+
gauge("proxmox_vm_maxdisk", 8 * GIB, ct),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// update_storage_status: only active storages are pushed.
|
|
206
|
+
function storagePush(): JSONObject {
|
|
207
|
+
const local: Record<string, string> = { node: "pve1", storage: "local" };
|
|
208
|
+
const zfs: Record<string, string> = { node: "pve1", storage: "local-zfs" };
|
|
209
|
+
return pveResource([
|
|
210
|
+
gauge("proxmox_storage_total", 100 * GIB, local),
|
|
211
|
+
gauge("proxmox_storage_used", 40 * GIB, local),
|
|
212
|
+
gauge("proxmox_storage_avail", 60 * GIB, local),
|
|
213
|
+
gauge("proxmox_storage_active", 1, local),
|
|
214
|
+
gauge("proxmox_storage_enabled", 1, local),
|
|
215
|
+
gauge("proxmox_storage_shared", 0, local),
|
|
216
|
+
gauge("proxmox_storage_total", 500 * GIB, zfs),
|
|
217
|
+
gauge("proxmox_storage_used", 450 * GIB, zfs),
|
|
218
|
+
gauge("proxmox_storage_active", 1, zfs),
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- read helpers ----------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function allMetrics(envelope: JSONObject): Array<JSONObject> {
|
|
225
|
+
const out: Array<JSONObject> = [];
|
|
226
|
+
for (const sm of envelope["scopeMetrics"] as JSONArray) {
|
|
227
|
+
out.push(...((sm as JSONObject)["metrics"] as Array<JSONObject>));
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function pointsOf(metric: JSONObject): Array<JSONObject> {
|
|
233
|
+
const holder: JSONObject = (metric["gauge"] || metric["sum"]) as JSONObject;
|
|
234
|
+
return holder["dataPoints"] as Array<JSONObject>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function labelsOf(point: JSONObject): Record<string, string> {
|
|
238
|
+
const out: Record<string, string> = {};
|
|
239
|
+
for (const a of point["attributes"] as Array<JSONObject>) {
|
|
240
|
+
out[a["key"] as string] = (a["value"] as JSONObject)[
|
|
241
|
+
"stringValue"
|
|
242
|
+
] as string;
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface Series {
|
|
248
|
+
name: string;
|
|
249
|
+
value: JSONValue;
|
|
250
|
+
labels: Record<string, string>;
|
|
251
|
+
point: JSONObject;
|
|
252
|
+
metric: JSONObject;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function pveSeries(envelope: JSONObject): Array<Series> {
|
|
256
|
+
const out: Array<Series> = [];
|
|
257
|
+
for (const metric of allMetrics(envelope)) {
|
|
258
|
+
const name: string = metric["name"] as string;
|
|
259
|
+
if (!name.startsWith("pve_")) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
for (const point of pointsOf(metric)) {
|
|
263
|
+
out.push({
|
|
264
|
+
name,
|
|
265
|
+
value: point["asDouble"] ?? point["asInt"],
|
|
266
|
+
labels: labelsOf(point),
|
|
267
|
+
point,
|
|
268
|
+
metric,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function find(
|
|
276
|
+
series: Array<Series>,
|
|
277
|
+
name: string,
|
|
278
|
+
id: string | undefined,
|
|
279
|
+
): Series {
|
|
280
|
+
const match: Array<Series> = series.filter((s: Series) => {
|
|
281
|
+
return s.name === name && s.labels["id"] === id;
|
|
282
|
+
});
|
|
283
|
+
if (match.length !== 1) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`expected exactly one ${name}{id=${id}}, found ${match.length}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return match[0]!;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resourceAttr(envelope: JSONObject, key: string): Array<string> {
|
|
292
|
+
return (
|
|
293
|
+
(envelope["resource"] as JSONObject)["attributes"] as Array<JSONObject>
|
|
294
|
+
)
|
|
295
|
+
.filter((a: JSONObject) => {
|
|
296
|
+
return a["key"] === key;
|
|
297
|
+
})
|
|
298
|
+
.map((a: JSONObject) => {
|
|
299
|
+
return (a["value"] as JSONObject)["stringValue"] as string;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalized(envelope: JSONObject): JSONObject {
|
|
304
|
+
normalizeProxmoxNativePushInPlace([envelope]);
|
|
305
|
+
return envelope;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
type NodeStatus = ReturnType<typeof readProxmoxNativeNodeStatus>;
|
|
309
|
+
|
|
310
|
+
interface SiblingWeight {
|
|
311
|
+
nodeName: string;
|
|
312
|
+
weight: number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Restamp every datapoint of a push (undefined drops the timestamp).
|
|
316
|
+
function withTime(
|
|
317
|
+
envelope: JSONObject,
|
|
318
|
+
timeUnixNano: JSONValue | undefined,
|
|
319
|
+
): JSONObject {
|
|
320
|
+
for (const metric of allMetrics(envelope)) {
|
|
321
|
+
for (const point of pointsOf(metric)) {
|
|
322
|
+
if (timeUnixNano === undefined) {
|
|
323
|
+
delete point["timeUnixNano"];
|
|
324
|
+
} else {
|
|
325
|
+
point["timeUnixNano"] = timeUnixNano;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return envelope;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// The scopeMetrics entries holding sibling reports.
|
|
333
|
+
function reportScopesOf(envelope: JSONObject): Array<JSONObject> {
|
|
334
|
+
return (envelope["scopeMetrics"] as Array<JSONObject>).filter(
|
|
335
|
+
(scopeMetric: JSONObject) => {
|
|
336
|
+
return isProxmoxSiblingReportScope(scopeMetric);
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The one sibling-report scope of a push (fails when there is not one).
|
|
342
|
+
function reportScopeOf(envelope: JSONObject): JSONObject {
|
|
343
|
+
const scopes: Array<JSONObject> = reportScopesOf(envelope);
|
|
344
|
+
if (scopes.length !== 1) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`expected one sibling-report scope, found ${scopes.length}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return scopes[0]!;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The datapoints of one metric of the sibling-report scope.
|
|
353
|
+
function reportPointsOf(
|
|
354
|
+
envelope: JSONObject,
|
|
355
|
+
metricName: string,
|
|
356
|
+
): Array<JSONObject> {
|
|
357
|
+
const metrics: Array<JSONObject> = reportScopeOf(envelope)[
|
|
358
|
+
"metrics"
|
|
359
|
+
] as Array<JSONObject>;
|
|
360
|
+
return metrics
|
|
361
|
+
.filter((metric: JSONObject) => {
|
|
362
|
+
return metric["name"] === metricName;
|
|
363
|
+
})
|
|
364
|
+
.flatMap((metric: JSONObject) => {
|
|
365
|
+
return pointsOf(metric);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function weightsOf(
|
|
370
|
+
silentCount: number,
|
|
371
|
+
reporterCount: number,
|
|
372
|
+
): Array<SiblingWeight> {
|
|
373
|
+
const silentNodes: Array<string> = [];
|
|
374
|
+
for (let i: number = 0; i < silentCount; i++) {
|
|
375
|
+
silentNodes.push(`dead${i + 1}`);
|
|
376
|
+
}
|
|
377
|
+
return splitProxmoxSiblingInfoWeights(silentNodes, reporterCount);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function sumOf(values: Array<number>): number {
|
|
381
|
+
let total: number = 0;
|
|
382
|
+
for (const value of values) {
|
|
383
|
+
total += value;
|
|
384
|
+
}
|
|
385
|
+
return total;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---- tests -----------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
describe("isProxmoxNativePushResource", () => {
|
|
391
|
+
test("recognises the resource PVE stamps on every push", () => {
|
|
392
|
+
expect(
|
|
393
|
+
isProxmoxNativePushResource(
|
|
394
|
+
(nodePush()["resource"] as JSONObject)["attributes"] as JSONArray,
|
|
395
|
+
),
|
|
396
|
+
).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("still recognises the block after service.name was dropped", () => {
|
|
400
|
+
const envelope: JSONObject = normalized(nodePush());
|
|
401
|
+
expect(
|
|
402
|
+
isProxmoxNativePushResource(
|
|
403
|
+
(envelope["resource"] as JSONObject)["attributes"] as JSONArray,
|
|
404
|
+
),
|
|
405
|
+
).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("the Proxmox Agent's resource is not a native push", () => {
|
|
409
|
+
expect(
|
|
410
|
+
isProxmoxNativePushResource(attrs({ "proxmox.cluster.name": "homelab" })),
|
|
411
|
+
).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("needs both proxmox.cluster and proxmox.node", () => {
|
|
415
|
+
expect(
|
|
416
|
+
isProxmoxNativePushResource(attrs({ "proxmox.cluster": "homelab" })),
|
|
417
|
+
).toBe(false);
|
|
418
|
+
expect(isProxmoxNativePushResource(attrs({ "proxmox.node": "pve1" }))).toBe(
|
|
419
|
+
false,
|
|
420
|
+
);
|
|
421
|
+
expect(
|
|
422
|
+
isProxmoxNativePushResource(
|
|
423
|
+
attrs({ "proxmox.cluster": " ", "proxmox.node": "pve1" }),
|
|
424
|
+
),
|
|
425
|
+
).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("tolerates missing or malformed attribute arrays", () => {
|
|
429
|
+
expect(isProxmoxNativePushResource(undefined)).toBe(false);
|
|
430
|
+
expect(
|
|
431
|
+
isProxmoxNativePushResource([
|
|
432
|
+
null,
|
|
433
|
+
"x",
|
|
434
|
+
{ key: "proxmox.cluster" },
|
|
435
|
+
] as unknown as JSONArray),
|
|
436
|
+
).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("resolveProxmoxNativePushClusterName", () => {
|
|
441
|
+
test("uses the PVE cluster name", () => {
|
|
442
|
+
expect(
|
|
443
|
+
resolveProxmoxNativePushClusterName(
|
|
444
|
+
attrs({ "proxmox.cluster": "homelab", "proxmox.node": "pve1" }),
|
|
445
|
+
),
|
|
446
|
+
).toBe("homelab");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("an explicit proxmox.cluster.name wins, so existing clusters keep their identity", () => {
|
|
450
|
+
expect(
|
|
451
|
+
resolveProxmoxNativePushClusterName(
|
|
452
|
+
attrs({
|
|
453
|
+
"proxmox.cluster": "homelab",
|
|
454
|
+
"proxmox.node": "pve1",
|
|
455
|
+
"proxmox.cluster.name": "prod-cluster",
|
|
456
|
+
}),
|
|
457
|
+
),
|
|
458
|
+
).toBe("prod-cluster");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("a standalone node is its own cluster, not a shared 'single-node' bucket", () => {
|
|
462
|
+
expect(
|
|
463
|
+
resolveProxmoxNativePushClusterName(
|
|
464
|
+
attrs({ "proxmox.cluster": "single-node", "proxmox.node": "nas" }),
|
|
465
|
+
),
|
|
466
|
+
).toBe("nas");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("returns null when nothing identifies the cluster", () => {
|
|
470
|
+
expect(resolveProxmoxNativePushClusterName(attrs({}))).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("normalizeProxmoxNativePushInPlace — resource identity", () => {
|
|
475
|
+
test("stamps proxmox.cluster.name from PVE's own proxmox.cluster", () => {
|
|
476
|
+
const envelope: JSONObject = normalized(nodePush());
|
|
477
|
+
expect(resourceAttr(envelope, "proxmox.cluster.name")).toEqual(["homelab"]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("keeps a user-supplied proxmox.cluster.name as the only one", () => {
|
|
481
|
+
const envelope: JSONObject = normalized(
|
|
482
|
+
pveResource([gauge("proxmox_node_uptime", 1, { node: "pve1" })], {
|
|
483
|
+
extra: { "proxmox.cluster.name": "prod-cluster" },
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
expect(resourceAttr(envelope, "proxmox.cluster.name")).toEqual([
|
|
487
|
+
"prod-cluster",
|
|
488
|
+
]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("drops service.name=proxmox-ve so the batch routes to the cluster, not a phantom Service", () => {
|
|
492
|
+
const envelope: JSONObject = normalized(nodePush());
|
|
493
|
+
expect(resourceAttr(envelope, "service.name")).toEqual([]);
|
|
494
|
+
// The rest of what PVE sent is kept.
|
|
495
|
+
expect(resourceAttr(envelope, "service.version")).toEqual([
|
|
496
|
+
"9.0.10/deb1ca707ec72a89",
|
|
497
|
+
]);
|
|
498
|
+
expect(resourceAttr(envelope, "proxmox.cluster")).toEqual(["homelab"]);
|
|
499
|
+
expect(resourceAttr(envelope, "proxmox.node")).toEqual(["pve1"]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("keeps a service.name the user chose deliberately", () => {
|
|
503
|
+
const envelope: JSONObject = pveResource([]);
|
|
504
|
+
((envelope["resource"] as JSONObject)["attributes"] as JSONArray).push({
|
|
505
|
+
key: "service.name",
|
|
506
|
+
value: { stringValue: "my-proxmox" },
|
|
507
|
+
});
|
|
508
|
+
normalized(envelope);
|
|
509
|
+
expect(resourceAttr(envelope, "service.name")).toEqual(["my-proxmox"]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("leaves the Proxmox Agent's batches untouched", () => {
|
|
513
|
+
const agent: JSONObject = {
|
|
514
|
+
resource: { attributes: attrs({ "proxmox.cluster.name": "homelab" }) },
|
|
515
|
+
scopeMetrics: [
|
|
516
|
+
{
|
|
517
|
+
scope: {},
|
|
518
|
+
metrics: [gauge("pve_up", 1, { id: "node/pve1" })],
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
};
|
|
522
|
+
const before: string = JSON.stringify(agent);
|
|
523
|
+
normalizeProxmoxNativePushInPlace([agent]);
|
|
524
|
+
expect(JSON.stringify(agent)).toBe(before);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("leaves unrelated OTLP resources untouched", () => {
|
|
528
|
+
const other: JSONObject = {
|
|
529
|
+
resource: { attributes: attrs({ "service.name": "checkout" }) },
|
|
530
|
+
scopeMetrics: [
|
|
531
|
+
{
|
|
532
|
+
scope: {},
|
|
533
|
+
metrics: [gauge("proxmox_vm_cpu", 0.5, { vmid: "1", type: "qemu" })],
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
};
|
|
537
|
+
const before: string = JSON.stringify(other);
|
|
538
|
+
normalizeProxmoxNativePushInPlace([other]);
|
|
539
|
+
expect(JSON.stringify(other)).toBe(before);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("survives malformed envelopes without throwing", () => {
|
|
543
|
+
expect(() => {
|
|
544
|
+
normalizeProxmoxNativePushInPlace([
|
|
545
|
+
null,
|
|
546
|
+
1,
|
|
547
|
+
"x",
|
|
548
|
+
{},
|
|
549
|
+
{ resource: {} },
|
|
550
|
+
{ resource: { attributes: "nope" } },
|
|
551
|
+
{
|
|
552
|
+
resource: {
|
|
553
|
+
attributes: attrs({
|
|
554
|
+
"proxmox.cluster": "c",
|
|
555
|
+
"proxmox.node": "n",
|
|
556
|
+
}),
|
|
557
|
+
},
|
|
558
|
+
scopeMetrics: "nope",
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
resource: {
|
|
562
|
+
attributes: attrs({
|
|
563
|
+
"proxmox.cluster": "c",
|
|
564
|
+
"proxmox.node": "n",
|
|
565
|
+
}),
|
|
566
|
+
},
|
|
567
|
+
scopeMetrics: [
|
|
568
|
+
null,
|
|
569
|
+
{ metrics: "nope" },
|
|
570
|
+
{ metrics: [null, { name: 5 }, { name: "proxmox_vm_cpu" }] },
|
|
571
|
+
{
|
|
572
|
+
metrics: [
|
|
573
|
+
{ name: "proxmox_vm_cpu", gauge: { dataPoints: [null, {}] } },
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
},
|
|
578
|
+
] as unknown as JSONArray);
|
|
579
|
+
}).not.toThrow();
|
|
580
|
+
expect(() => {
|
|
581
|
+
normalizeProxmoxNativePushInPlace(undefined as unknown as JSONArray);
|
|
582
|
+
}).not.toThrow();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe("normalizeProxmoxNativePushInPlace — node push", () => {
|
|
587
|
+
const series: Array<Series> = pveSeries(normalized(nodePush()));
|
|
588
|
+
const id: string = "node/pve1";
|
|
589
|
+
|
|
590
|
+
test("maps every node series the product reads", () => {
|
|
591
|
+
expect(find(series, "pve_uptime_seconds", id).value).toBe(86400);
|
|
592
|
+
expect(find(series, "pve_cpu_usage_ratio", id).value).toBe(0.25);
|
|
593
|
+
expect(find(series, "pve_cpu_usage_limit", id).value).toBe(16);
|
|
594
|
+
expect(find(series, "pve_memory_usage_bytes", id).value).toBe(16 * GIB);
|
|
595
|
+
expect(find(series, "pve_memory_size_bytes", id).value).toBe(64 * GIB);
|
|
596
|
+
expect(find(series, "pve_disk_usage_bytes", id).value).toBe(40 * GIB);
|
|
597
|
+
expect(find(series, "pve_disk_size_bytes", id).value).toBe(100 * GIB);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("a node that pushes is up, and gets its identity series", () => {
|
|
601
|
+
expect(find(series, "pve_up", id).value).toBe("1");
|
|
602
|
+
const info: Series = find(series, "pve_node_info", id);
|
|
603
|
+
expect(info.value).toBe("1");
|
|
604
|
+
expect(info.labels["name"]).toBe("pve1");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("the PVE version is split into pve_version_info's version and repoid labels", () => {
|
|
608
|
+
const version: Series = find(series, "pve_version_info", undefined);
|
|
609
|
+
expect(version.labels).toEqual({
|
|
610
|
+
version: "9.0.10",
|
|
611
|
+
repoid: "deb1ca707ec72a89",
|
|
612
|
+
});
|
|
613
|
+
expect(version.point["timeUnixNano"]).toBe(TIME_NANO);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("every data series carries the agent transform's identity attributes", () => {
|
|
617
|
+
for (const s of series) {
|
|
618
|
+
if (s.name === "pve_version_info") {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
expect(s.labels).toMatchObject({
|
|
622
|
+
id,
|
|
623
|
+
"pve.scope": "node",
|
|
624
|
+
"pve.type": "node",
|
|
625
|
+
"pve.id": "pve1",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("derived series are gauges stamped with the push's timestamp", () => {
|
|
631
|
+
for (const s of series) {
|
|
632
|
+
expect(s.metric["gauge"]).toBeDefined();
|
|
633
|
+
expect(s.metric["sum"]).toBeUndefined();
|
|
634
|
+
expect(s.point["timeUnixNano"]).toBe(TIME_NANO);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("series with no pve-exporter counterpart are not invented", () => {
|
|
639
|
+
const names: Set<string> = new Set(
|
|
640
|
+
series.map((s: Series) => {
|
|
641
|
+
return s.name;
|
|
642
|
+
}),
|
|
643
|
+
);
|
|
644
|
+
expect([...names].sort()).toEqual(
|
|
645
|
+
[
|
|
646
|
+
"pve_cpu_usage_limit",
|
|
647
|
+
"pve_cpu_usage_ratio",
|
|
648
|
+
"pve_disk_size_bytes",
|
|
649
|
+
"pve_disk_usage_bytes",
|
|
650
|
+
"pve_memory_size_bytes",
|
|
651
|
+
"pve_memory_usage_bytes",
|
|
652
|
+
"pve_node_info",
|
|
653
|
+
"pve_up",
|
|
654
|
+
"pve_uptime_seconds",
|
|
655
|
+
"pve_version_info",
|
|
656
|
+
].sort(),
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("the original proxmox_* series stay exactly as PVE sent them", () => {
|
|
661
|
+
const original: JSONObject = nodePush();
|
|
662
|
+
const envelope: JSONObject = normalized(nodePush());
|
|
663
|
+
const originals: Array<JSONObject> = allMetrics(envelope).filter(
|
|
664
|
+
(m: JSONObject) => {
|
|
665
|
+
return (m["name"] as string).startsWith("proxmox_");
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
expect(originals).toEqual(allMetrics(original));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("no version series when PVE sent no service.version", () => {
|
|
672
|
+
const envelope: JSONObject = nodePush();
|
|
673
|
+
const attributes: JSONArray = (envelope["resource"] as JSONObject)[
|
|
674
|
+
"attributes"
|
|
675
|
+
] as JSONArray;
|
|
676
|
+
(envelope["resource"] as JSONObject)["attributes"] = attributes.filter(
|
|
677
|
+
(a: JSONValue) => {
|
|
678
|
+
return (a as JSONObject)["key"] !== "service.version";
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
const names: Array<string> = pveSeries(normalized(envelope)).map(
|
|
682
|
+
(s: Series) => {
|
|
683
|
+
return s.name;
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
expect(names).not.toContain("pve_version_info");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("a version without a repoid still reports the version", () => {
|
|
690
|
+
const envelope: JSONObject = pveResource(
|
|
691
|
+
[gauge("proxmox_node_uptime", 5, { node: "pve1" })],
|
|
692
|
+
{ extra: {} },
|
|
693
|
+
);
|
|
694
|
+
const attributes: Array<JSONObject> = (envelope["resource"] as JSONObject)[
|
|
695
|
+
"attributes"
|
|
696
|
+
] as Array<JSONObject>;
|
|
697
|
+
for (const a of attributes) {
|
|
698
|
+
if (a["key"] === "service.version") {
|
|
699
|
+
a["value"] = { stringValue: "9.1.2" };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
expect(
|
|
703
|
+
find(pveSeries(normalized(envelope)), "pve_version_info", undefined)
|
|
704
|
+
.labels,
|
|
705
|
+
).toEqual({ version: "9.1.2" });
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("falls back to the resource's proxmox.node when a datapoint has no node label", () => {
|
|
709
|
+
const envelope: JSONObject = pveResource(
|
|
710
|
+
[gauge("proxmox_node_uptime", 42, {})],
|
|
711
|
+
{ node: "pve7" },
|
|
712
|
+
);
|
|
713
|
+
expect(
|
|
714
|
+
find(pveSeries(normalized(envelope)), "pve_uptime_seconds", "node/pve7")
|
|
715
|
+
.value,
|
|
716
|
+
).toBe(42);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe("normalizeProxmoxNativePushInPlace — guest pushes", () => {
|
|
721
|
+
const qemu: Array<Series> = pveSeries(normalized(qemuPush()));
|
|
722
|
+
const lxc: Array<Series> = pveSeries(normalized(lxcPush()));
|
|
723
|
+
|
|
724
|
+
test("a VM maps onto qemu/<vmid> with every guest series", () => {
|
|
725
|
+
const id: string = "qemu/100";
|
|
726
|
+
expect(find(qemu, "pve_uptime_seconds", id).value).toBe(3600);
|
|
727
|
+
expect(find(qemu, "pve_cpu_usage_ratio", id).value).toBe(0.5);
|
|
728
|
+
expect(find(qemu, "pve_cpu_usage_limit", id).value).toBe(4);
|
|
729
|
+
expect(find(qemu, "pve_memory_usage_bytes", id).value).toBe(2 * GIB);
|
|
730
|
+
expect(find(qemu, "pve_memory_size_bytes", id).value).toBe(8 * GIB);
|
|
731
|
+
expect(find(qemu, "pve_disk_usage_bytes", id).value).toBe(0);
|
|
732
|
+
expect(find(qemu, "pve_disk_size_bytes", id).value).toBe(32 * GIB);
|
|
733
|
+
expect(find(qemu, "pve_network_receive_bytes", id).value).toBe(1000);
|
|
734
|
+
expect(find(qemu, "pve_network_transmit_bytes", id).value).toBe(2000);
|
|
735
|
+
expect(find(qemu, "pve_disk_read_bytes", id).value).toBe(3000);
|
|
736
|
+
expect(find(qemu, "pve_disk_write_bytes", id).value).toBe(4000);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("a running guest is up, a stopped one is down (uptime is PVE's only run-state signal)", () => {
|
|
740
|
+
expect(find(qemu, "pve_up", "qemu/100").value).toBe("1");
|
|
741
|
+
expect(find(qemu, "pve_up", "qemu/101").value).toBe("0");
|
|
742
|
+
expect(find(lxc, "pve_up", "lxc/200").value).toBe("1");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("pve_guest_info carries the name, node and type labels the inventory reads", () => {
|
|
746
|
+
expect(find(qemu, "pve_guest_info", "qemu/100").labels).toEqual({
|
|
747
|
+
id: "qemu/100",
|
|
748
|
+
"pve.scope": "guest",
|
|
749
|
+
"pve.type": "qemu",
|
|
750
|
+
"pve.id": "100",
|
|
751
|
+
type: "qemu",
|
|
752
|
+
name: "web",
|
|
753
|
+
node: "pve1",
|
|
754
|
+
});
|
|
755
|
+
expect(find(lxc, "pve_guest_info", "lxc/200").labels).toMatchObject({
|
|
756
|
+
"pve.type": "lxc",
|
|
757
|
+
name: "dns",
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("one info series per guest, however many series the guest pushed", () => {
|
|
762
|
+
expect(
|
|
763
|
+
qemu.filter((s: Series) => {
|
|
764
|
+
return s.name === "pve_guest_info";
|
|
765
|
+
}),
|
|
766
|
+
).toHaveLength(2);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("an LXC container lands on the lxc/<vmid> id with the lxc type", () => {
|
|
770
|
+
for (const s of lxc) {
|
|
771
|
+
expect(s.labels).toMatchObject({
|
|
772
|
+
id: "lxc/200",
|
|
773
|
+
"pve.scope": "guest",
|
|
774
|
+
"pve.type": "lxc",
|
|
775
|
+
"pve.id": "200",
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
expect(find(lxc, "pve_disk_usage_bytes", "lxc/200").value).toBe(3 * GIB);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("counters pushed without the _total suffix map too", () => {
|
|
782
|
+
const labels: Record<string, string> = guestLabels({
|
|
783
|
+
vmid: "102",
|
|
784
|
+
type: "qemu",
|
|
785
|
+
name: "x",
|
|
786
|
+
});
|
|
787
|
+
const series: Array<Series> = pveSeries(
|
|
788
|
+
normalized(
|
|
789
|
+
pveResource([
|
|
790
|
+
counter("proxmox_vm_netin", 1, labels),
|
|
791
|
+
counter("proxmox_vm_netout", 2, labels),
|
|
792
|
+
counter("proxmox_vm_diskread", 3, labels),
|
|
793
|
+
counter("proxmox_vm_diskwrite", 4, labels),
|
|
794
|
+
]),
|
|
795
|
+
),
|
|
796
|
+
);
|
|
797
|
+
expect(find(series, "pve_network_receive_bytes", "qemu/102").value).toBe(1);
|
|
798
|
+
expect(find(series, "pve_network_transmit_bytes", "qemu/102").value).toBe(
|
|
799
|
+
2,
|
|
800
|
+
);
|
|
801
|
+
expect(find(series, "pve_disk_read_bytes", "qemu/102").value).toBe(3);
|
|
802
|
+
expect(find(series, "pve_disk_write_bytes", "qemu/102").value).toBe(4);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("a guest series without a usable vmid/type is skipped, not guessed", () => {
|
|
806
|
+
const series: Array<Series> = pveSeries(
|
|
807
|
+
normalized(
|
|
808
|
+
pveResource([
|
|
809
|
+
gauge("proxmox_vm_cpu", 0.5, { node: "pve1", type: "qemu" }),
|
|
810
|
+
gauge("proxmox_vm_cpu", 0.5, { vmid: "5", node: "pve1" }),
|
|
811
|
+
gauge("proxmox_vm_cpu", 0.5, {
|
|
812
|
+
vmid: "6",
|
|
813
|
+
node: "pve1",
|
|
814
|
+
type: "openvz",
|
|
815
|
+
}),
|
|
816
|
+
]),
|
|
817
|
+
),
|
|
818
|
+
);
|
|
819
|
+
expect(series).toEqual([]);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("an integer vmid attribute is accepted", () => {
|
|
823
|
+
const envelope: JSONObject = pveResource([
|
|
824
|
+
{
|
|
825
|
+
name: "proxmox_vm_cpu",
|
|
826
|
+
gauge: {
|
|
827
|
+
dataPoints: [
|
|
828
|
+
{
|
|
829
|
+
timeUnixNano: TIME_NANO,
|
|
830
|
+
asDouble: 0.3,
|
|
831
|
+
attributes: [
|
|
832
|
+
{ key: "vmid", value: { intValue: 300 } },
|
|
833
|
+
{ key: "type", value: { stringValue: "qemu" } },
|
|
834
|
+
],
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
]);
|
|
840
|
+
expect(
|
|
841
|
+
find(pveSeries(normalized(envelope)), "pve_cpu_usage_ratio", "qemu/300")
|
|
842
|
+
.value,
|
|
843
|
+
).toBe(0.3);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
test("a non-numeric datapoint is skipped", () => {
|
|
847
|
+
const labels: Record<string, string> = guestLabels({
|
|
848
|
+
vmid: "100",
|
|
849
|
+
type: "qemu",
|
|
850
|
+
name: "web",
|
|
851
|
+
});
|
|
852
|
+
const envelope: JSONObject = pveResource([
|
|
853
|
+
{
|
|
854
|
+
name: "proxmox_vm_cpu",
|
|
855
|
+
gauge: {
|
|
856
|
+
dataPoints: [
|
|
857
|
+
{ timeUnixNano: TIME_NANO, attributes: attrs(labels) },
|
|
858
|
+
{
|
|
859
|
+
timeUnixNano: TIME_NANO,
|
|
860
|
+
asDouble: "NaN",
|
|
861
|
+
attributes: attrs(labels),
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
]);
|
|
867
|
+
expect(pveSeries(normalized(envelope))).toEqual([]);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("int64 values sent as strings (protobuf-decoded) are carried over", () => {
|
|
871
|
+
const labels: Record<string, string> = guestLabels({
|
|
872
|
+
vmid: "100",
|
|
873
|
+
type: "qemu",
|
|
874
|
+
name: "web",
|
|
875
|
+
});
|
|
876
|
+
const envelope: JSONObject = pveResource([
|
|
877
|
+
{
|
|
878
|
+
name: "proxmox_vm_maxmem",
|
|
879
|
+
gauge: {
|
|
880
|
+
dataPoints: [
|
|
881
|
+
{
|
|
882
|
+
timeUnixNano: String(TIME_NANO),
|
|
883
|
+
asInt: "8589934592",
|
|
884
|
+
attributes: attrs(labels),
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
]);
|
|
890
|
+
const s: Series = find(
|
|
891
|
+
pveSeries(normalized(envelope)),
|
|
892
|
+
"pve_memory_size_bytes",
|
|
893
|
+
"qemu/100",
|
|
894
|
+
);
|
|
895
|
+
expect(s.value).toBe("8589934592");
|
|
896
|
+
expect(s.point["timeUnixNano"]).toBe(String(TIME_NANO));
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test("guest pushes carry no node or version series", () => {
|
|
900
|
+
for (const s of [...qemu, ...lxc]) {
|
|
901
|
+
expect(["pve_node_info", "pve_version_info"]).not.toContain(s.name);
|
|
902
|
+
expect(s.labels["pve.scope"]).toBe("guest");
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
describe("normalizeProxmoxNativePushInPlace — storage push", () => {
|
|
908
|
+
const series: Array<Series> = pveSeries(normalized(storagePush()));
|
|
909
|
+
|
|
910
|
+
test("storage maps onto storage/<node>/<storage>, pve-exporter's id format", () => {
|
|
911
|
+
expect(
|
|
912
|
+
find(series, "pve_disk_usage_bytes", "storage/pve1/local").value,
|
|
913
|
+
).toBe(40 * GIB);
|
|
914
|
+
expect(
|
|
915
|
+
find(series, "pve_disk_size_bytes", "storage/pve1/local").value,
|
|
916
|
+
).toBe(100 * GIB);
|
|
917
|
+
expect(
|
|
918
|
+
find(series, "pve_disk_usage_bytes", "storage/pve1/local-zfs").value,
|
|
919
|
+
).toBe(450 * GIB);
|
|
920
|
+
expect(find(series, "pve_up", "storage/pve1/local").value).toBe(1);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test("pve_storage_info carries the node and storage labels", () => {
|
|
924
|
+
expect(
|
|
925
|
+
find(series, "pve_storage_info", "storage/pve1/local-zfs").labels,
|
|
926
|
+
).toEqual({
|
|
927
|
+
id: "storage/pve1/local-zfs",
|
|
928
|
+
"pve.scope": "storage",
|
|
929
|
+
"pve.type": "storage",
|
|
930
|
+
"pve.id": "pve1/local-zfs",
|
|
931
|
+
node: "pve1",
|
|
932
|
+
storage: "local-zfs",
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test("a storage series without a storage label is skipped", () => {
|
|
937
|
+
expect(
|
|
938
|
+
pveSeries(
|
|
939
|
+
normalized(
|
|
940
|
+
pveResource([gauge("proxmox_storage_used", 1, { node: "pve1" })]),
|
|
941
|
+
),
|
|
942
|
+
),
|
|
943
|
+
).toEqual([]);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
describe("normalizeProxmoxNativePushInPlace — idempotency", () => {
|
|
948
|
+
test("running it twice derives nothing new", () => {
|
|
949
|
+
const envelope: JSONObject = normalized(qemuPush());
|
|
950
|
+
const once: string = JSON.stringify(envelope);
|
|
951
|
+
normalizeProxmoxNativePushInPlace([envelope]);
|
|
952
|
+
expect(JSON.stringify(envelope)).toBe(once);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("a push already translated upstream (e.g. by a collector) is not doubled", () => {
|
|
956
|
+
const envelope: JSONObject = pveResource([
|
|
957
|
+
gauge(
|
|
958
|
+
"proxmox_vm_cpu",
|
|
959
|
+
0.5,
|
|
960
|
+
guestLabels({
|
|
961
|
+
vmid: "100",
|
|
962
|
+
type: "qemu",
|
|
963
|
+
name: "web",
|
|
964
|
+
}),
|
|
965
|
+
),
|
|
966
|
+
gauge("pve_cpu_usage_ratio", 0.5, { id: "qemu/100" }),
|
|
967
|
+
]);
|
|
968
|
+
const series: Array<Series> = pveSeries(normalized(envelope));
|
|
969
|
+
expect(series).toHaveLength(1);
|
|
970
|
+
// The cluster identity is still filled in.
|
|
971
|
+
expect(resourceAttr(envelope, "proxmox.cluster.name")).toEqual(["homelab"]);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("a multi-resource payload is rewritten block by block", () => {
|
|
975
|
+
const payload: JSONArray = [nodePush("pve1"), nodePush("pve2")];
|
|
976
|
+
normalizeProxmoxNativePushInPlace(payload);
|
|
977
|
+
expect(
|
|
978
|
+
find(pveSeries(payload[0] as JSONObject), "pve_up", "node/pve1").value,
|
|
979
|
+
).toBe("1");
|
|
980
|
+
expect(
|
|
981
|
+
find(pveSeries(payload[1] as JSONObject), "pve_up", "node/pve2").value,
|
|
982
|
+
).toBe("1");
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
/*
|
|
987
|
+
* The translated series must be exactly what the real consumer — the
|
|
988
|
+
* inventory / cluster snapshot scan — understands. Feed every derived
|
|
989
|
+
* pve_* datapoint through bufferProxmoxSnapshotMetric the way the ingest
|
|
990
|
+
* loop does.
|
|
991
|
+
*/
|
|
992
|
+
describe("translated pushes feed the Proxmox inventory", () => {
|
|
993
|
+
function scan(envelopes: Array<JSONObject>): {
|
|
994
|
+
rows: Array<ProxmoxResourceBufferEntry>;
|
|
995
|
+
snap: ProxmoxClusterSnapshotBufferEntry;
|
|
996
|
+
} {
|
|
997
|
+
const resourceBuffer: Map<
|
|
998
|
+
string,
|
|
999
|
+
Map<string, ProxmoxResourceBufferEntry>
|
|
1000
|
+
> = new Map();
|
|
1001
|
+
const clusterBuffer: Map<string, ProxmoxClusterSnapshotBufferEntry> =
|
|
1002
|
+
new Map();
|
|
1003
|
+
normalizeProxmoxNativePushInPlace(envelopes as JSONArray);
|
|
1004
|
+
for (const envelope of envelopes) {
|
|
1005
|
+
const native: boolean = isProxmoxNativePushResource(
|
|
1006
|
+
(envelope["resource"] as JSONObject)["attributes"] as JSONArray,
|
|
1007
|
+
);
|
|
1008
|
+
for (const metric of allMetrics(envelope)) {
|
|
1009
|
+
const metricName: string = metric["name"] as string;
|
|
1010
|
+
if (!PVE_SNAPSHOT_METRIC_NAMES.has(metricName)) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
for (const datapoint of pointsOf(metric)) {
|
|
1014
|
+
bufferProxmoxSnapshotMetric({
|
|
1015
|
+
clusterIdStr: "c1",
|
|
1016
|
+
metricName,
|
|
1017
|
+
datapoint,
|
|
1018
|
+
resourceBuffer,
|
|
1019
|
+
clusterBuffer,
|
|
1020
|
+
countsFromInventory: native,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const rows: Array<ProxmoxResourceBufferEntry> = [
|
|
1026
|
+
...(resourceBuffer.get("c1")?.values() || []),
|
|
1027
|
+
].sort((a: ProxmoxResourceBufferEntry, b: ProxmoxResourceBufferEntry) => {
|
|
1028
|
+
return a.externalId.localeCompare(b.externalId);
|
|
1029
|
+
});
|
|
1030
|
+
return { rows, snap: clusterBuffer.get("c1")! };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function row(
|
|
1034
|
+
rows: Array<ProxmoxResourceBufferEntry>,
|
|
1035
|
+
externalId: string,
|
|
1036
|
+
): ProxmoxResourceBufferEntry {
|
|
1037
|
+
const found: ProxmoxResourceBufferEntry | undefined = rows.find(
|
|
1038
|
+
(r: ProxmoxResourceBufferEntry) => {
|
|
1039
|
+
return r.externalId === externalId;
|
|
1040
|
+
},
|
|
1041
|
+
);
|
|
1042
|
+
if (!found) {
|
|
1043
|
+
throw new Error(`no inventory row ${externalId}`);
|
|
1044
|
+
}
|
|
1045
|
+
return found;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const { rows, snap } = scan([
|
|
1049
|
+
nodePush(),
|
|
1050
|
+
qemuPush(),
|
|
1051
|
+
lxcPush(),
|
|
1052
|
+
storagePush(),
|
|
1053
|
+
]);
|
|
1054
|
+
|
|
1055
|
+
test("every node, guest and storage becomes an inventory row", () => {
|
|
1056
|
+
expect(
|
|
1057
|
+
rows.map((r: ProxmoxResourceBufferEntry) => {
|
|
1058
|
+
return `${r.kind} ${r.externalId}`;
|
|
1059
|
+
}),
|
|
1060
|
+
).toEqual([
|
|
1061
|
+
"Guest lxc/200",
|
|
1062
|
+
"Node node/pve1",
|
|
1063
|
+
"Guest qemu/100",
|
|
1064
|
+
"Guest qemu/101",
|
|
1065
|
+
"Storage storage/pve1/local",
|
|
1066
|
+
"Storage storage/pve1/local-zfs",
|
|
1067
|
+
]);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test("the node row carries its name, state and latest metrics", () => {
|
|
1071
|
+
expect(row(rows, "node/pve1")).toMatchObject({
|
|
1072
|
+
kind: "Node",
|
|
1073
|
+
name: "pve1",
|
|
1074
|
+
isUp: true,
|
|
1075
|
+
uptimeSeconds: 86400,
|
|
1076
|
+
latestCpuPercent: 25,
|
|
1077
|
+
latestMemoryBytes: 16 * GIB,
|
|
1078
|
+
maxMemoryBytes: 64 * GIB,
|
|
1079
|
+
latestDiskBytes: 40 * GIB,
|
|
1080
|
+
maxDiskBytes: 100 * GIB,
|
|
1081
|
+
observedAt: new Date(CTIME_S * 1000),
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("guest rows carry vmid, type, parent node and run state", () => {
|
|
1086
|
+
expect(row(rows, "qemu/100")).toMatchObject({
|
|
1087
|
+
kind: "Guest",
|
|
1088
|
+
name: "web",
|
|
1089
|
+
vmid: 100,
|
|
1090
|
+
guestType: "qemu",
|
|
1091
|
+
parentNodeName: "pve1",
|
|
1092
|
+
isUp: true,
|
|
1093
|
+
latestCpuPercent: 50,
|
|
1094
|
+
maxMemoryBytes: 8 * GIB,
|
|
1095
|
+
});
|
|
1096
|
+
expect(row(rows, "qemu/101")).toMatchObject({
|
|
1097
|
+
name: "old-db",
|
|
1098
|
+
isUp: false,
|
|
1099
|
+
uptimeSeconds: 0,
|
|
1100
|
+
});
|
|
1101
|
+
expect(row(rows, "lxc/200")).toMatchObject({
|
|
1102
|
+
name: "dns",
|
|
1103
|
+
vmid: 200,
|
|
1104
|
+
guestType: "lxc",
|
|
1105
|
+
isUp: true,
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
test("storage rows carry name, parent node and capacity", () => {
|
|
1110
|
+
expect(row(rows, "storage/pve1/local-zfs")).toMatchObject({
|
|
1111
|
+
kind: "Storage",
|
|
1112
|
+
name: "local-zfs",
|
|
1113
|
+
parentNodeName: "pve1",
|
|
1114
|
+
isUp: true,
|
|
1115
|
+
latestDiskBytes: 450 * GIB,
|
|
1116
|
+
maxDiskBytes: 500 * GIB,
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test("the cluster learns its PVE version", () => {
|
|
1121
|
+
expect(snap.pveVersion).toBe("9.0.10");
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("a native push never derives cluster counts from one request", () => {
|
|
1125
|
+
expect(snap.countsFromInventory).toBe(true);
|
|
1126
|
+
expect(deriveProxmoxClusterSnapshotExtras(rows, snap)).toEqual({
|
|
1127
|
+
pveVersion: "9.0.10",
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
/*
|
|
1133
|
+
* ------------------------------------------------------------------
|
|
1134
|
+
* Reporting the nodes that have stopped reporting
|
|
1135
|
+
* ------------------------------------------------------------------
|
|
1136
|
+
*
|
|
1137
|
+
* A native-push node that dies goes quiet, so the live nodes' own status
|
|
1138
|
+
* pushes carry pve_up = 0 (and a pve_node_info share) for it. WHICH
|
|
1139
|
+
* siblings are silent is decided in ProxmoxNativeNodeLiveness; these are
|
|
1140
|
+
* the payload side: finding a node's own status push, splitting the
|
|
1141
|
+
* pve_node_info weight, and appending the reports.
|
|
1142
|
+
*/
|
|
1143
|
+
|
|
1144
|
+
describe("isProxmoxSiblingReportScope", () => {
|
|
1145
|
+
test("recognises the scope the reports are appended under", () => {
|
|
1146
|
+
expect(PROXMOX_SIBLING_REPORT_SCOPE_NAME).toBe(
|
|
1147
|
+
"oneuptime.proxmox.sibling-report",
|
|
1148
|
+
);
|
|
1149
|
+
expect(
|
|
1150
|
+
isProxmoxSiblingReportScope({
|
|
1151
|
+
scope: { name: PROXMOX_SIBLING_REPORT_SCOPE_NAME, version: "1" },
|
|
1152
|
+
metrics: [],
|
|
1153
|
+
}),
|
|
1154
|
+
).toBe(true);
|
|
1155
|
+
expect(
|
|
1156
|
+
isProxmoxSiblingReportScope({
|
|
1157
|
+
scope: { name: PROXMOX_SIBLING_REPORT_SCOPE_NAME },
|
|
1158
|
+
}),
|
|
1159
|
+
).toBe(true);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test("any other scope — PVE's own is empty — is not a report", () => {
|
|
1163
|
+
expect(isProxmoxSiblingReportScope({ scope: {}, metrics: [] })).toBe(false);
|
|
1164
|
+
expect(isProxmoxSiblingReportScope({ scope: { name: "pve" } })).toBe(false);
|
|
1165
|
+
expect(isProxmoxSiblingReportScope({ metrics: [] })).toBe(false);
|
|
1166
|
+
// The name has to sit in scope.name.
|
|
1167
|
+
expect(
|
|
1168
|
+
isProxmoxSiblingReportScope({
|
|
1169
|
+
scope: PROXMOX_SIBLING_REPORT_SCOPE_NAME,
|
|
1170
|
+
}),
|
|
1171
|
+
).toBe(false);
|
|
1172
|
+
expect(
|
|
1173
|
+
isProxmoxSiblingReportScope({ name: PROXMOX_SIBLING_REPORT_SCOPE_NAME }),
|
|
1174
|
+
).toBe(false);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("tolerates malformed input", () => {
|
|
1178
|
+
const inputs: Array<JSONValue | undefined> = [
|
|
1179
|
+
undefined,
|
|
1180
|
+
null,
|
|
1181
|
+
"x",
|
|
1182
|
+
5,
|
|
1183
|
+
true,
|
|
1184
|
+
[],
|
|
1185
|
+
{ scope: null },
|
|
1186
|
+
];
|
|
1187
|
+
for (const input of inputs) {
|
|
1188
|
+
expect(isProxmoxSiblingReportScope(input)).toBe(false);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
describe("readProxmoxNativeNodeStatus", () => {
|
|
1194
|
+
const NATIVE_PVE1: JSONArray = attrs({
|
|
1195
|
+
"proxmox.cluster": "homelab",
|
|
1196
|
+
"proxmox.node": "pve1",
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("a node's own status push yields the node and the timestamp of its own pve_up", () => {
|
|
1200
|
+
expect(readProxmoxNativeNodeStatus(normalized(nodePush("pve2")))).toEqual({
|
|
1201
|
+
nodeName: "pve2",
|
|
1202
|
+
timeUnixNanos: [TIME_NANO],
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("reads the block after the rewrite: a raw push carries no pve_up yet", () => {
|
|
1207
|
+
expect(readProxmoxNativeNodeStatus(nodePush())).toBeNull();
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
test("a guest or storage push is no node-status push, although it carries pve_up", () => {
|
|
1211
|
+
for (const push of [qemuPush(), lxcPush(), storagePush()]) {
|
|
1212
|
+
const envelope: JSONObject = normalized(push);
|
|
1213
|
+
expect(
|
|
1214
|
+
pveSeries(envelope).some((s: Series) => {
|
|
1215
|
+
return s.name === "pve_up";
|
|
1216
|
+
}),
|
|
1217
|
+
).toBe(true);
|
|
1218
|
+
expect(readProxmoxNativeNodeStatus(envelope)).toBeNull();
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test("the Proxmox Agent's blocks are never read as a native node push", () => {
|
|
1223
|
+
const scopeMetrics: JSONArray = [
|
|
1224
|
+
{
|
|
1225
|
+
scope: {},
|
|
1226
|
+
metrics: [
|
|
1227
|
+
gauge("pve_up", 1, { id: "node/pve1", "pve.scope": "node" }),
|
|
1228
|
+
gauge("pve_node_info", 1, { id: "node/pve1", name: "pve1" }),
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
];
|
|
1232
|
+
expect(
|
|
1233
|
+
readProxmoxNativeNodeStatus({
|
|
1234
|
+
resource: { attributes: attrs({ "proxmox.cluster.name": "homelab" }) },
|
|
1235
|
+
scopeMetrics,
|
|
1236
|
+
}),
|
|
1237
|
+
).toBeNull();
|
|
1238
|
+
// Not even with a proxmox.node stamp — PVE's proxmox.cluster is missing.
|
|
1239
|
+
expect(
|
|
1240
|
+
readProxmoxNativeNodeStatus({
|
|
1241
|
+
resource: {
|
|
1242
|
+
attributes: attrs({
|
|
1243
|
+
"proxmox.cluster.name": "homelab",
|
|
1244
|
+
"proxmox.node": "pve1",
|
|
1245
|
+
}),
|
|
1246
|
+
},
|
|
1247
|
+
scopeMetrics,
|
|
1248
|
+
}),
|
|
1249
|
+
).toBeNull();
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test("other telemetry is ignored", () => {
|
|
1253
|
+
expect(
|
|
1254
|
+
readProxmoxNativeNodeStatus({
|
|
1255
|
+
resource: { attributes: attrs({ "service.name": "checkout" }) },
|
|
1256
|
+
scopeMetrics: [
|
|
1257
|
+
{ scope: {}, metrics: [gauge("pve_up", 1, { id: "node/pve1" })] },
|
|
1258
|
+
],
|
|
1259
|
+
}),
|
|
1260
|
+
).toBeNull();
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
test("malformed input reads as null and never throws", () => {
|
|
1264
|
+
const inputs: Array<JSONValue | undefined> = [
|
|
1265
|
+
undefined,
|
|
1266
|
+
null,
|
|
1267
|
+
"x",
|
|
1268
|
+
5,
|
|
1269
|
+
[],
|
|
1270
|
+
{},
|
|
1271
|
+
{ resource: null },
|
|
1272
|
+
{ resource: {} },
|
|
1273
|
+
{ resource: { attributes: "nope" } },
|
|
1274
|
+
{ resource: { attributes: NATIVE_PVE1 } },
|
|
1275
|
+
{ resource: { attributes: NATIVE_PVE1 }, scopeMetrics: "nope" },
|
|
1276
|
+
{ resource: { attributes: NATIVE_PVE1 }, scopeMetrics: [] },
|
|
1277
|
+
{
|
|
1278
|
+
resource: { attributes: NATIVE_PVE1 },
|
|
1279
|
+
scopeMetrics: [
|
|
1280
|
+
null,
|
|
1281
|
+
"x",
|
|
1282
|
+
{},
|
|
1283
|
+
{ metrics: "nope" },
|
|
1284
|
+
{
|
|
1285
|
+
metrics: [
|
|
1286
|
+
null,
|
|
1287
|
+
"x",
|
|
1288
|
+
{ name: 5 },
|
|
1289
|
+
{ name: "pve_up" },
|
|
1290
|
+
{ name: "pve_up", gauge: null },
|
|
1291
|
+
{ name: "pve_up", gauge: { dataPoints: "nope" } },
|
|
1292
|
+
{
|
|
1293
|
+
name: "pve_up",
|
|
1294
|
+
gauge: {
|
|
1295
|
+
dataPoints: [
|
|
1296
|
+
null,
|
|
1297
|
+
"x",
|
|
1298
|
+
{},
|
|
1299
|
+
{ attributes: "nope" },
|
|
1300
|
+
{ attributes: [null, { key: "id" }] },
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
},
|
|
1306
|
+
],
|
|
1307
|
+
},
|
|
1308
|
+
];
|
|
1309
|
+
for (const input of inputs) {
|
|
1310
|
+
expect(readProxmoxNativeNodeStatus(input)).toBeNull();
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
test("points of the sibling-report scope never count as the node's own", () => {
|
|
1315
|
+
const reportAboutPve1: JSONObject = {
|
|
1316
|
+
scope: { name: PROXMOX_SIBLING_REPORT_SCOPE_NAME, version: "1" },
|
|
1317
|
+
metrics: [
|
|
1318
|
+
{
|
|
1319
|
+
name: "pve_up",
|
|
1320
|
+
gauge: {
|
|
1321
|
+
dataPoints: [
|
|
1322
|
+
{
|
|
1323
|
+
asInt: "0",
|
|
1324
|
+
timeUnixNano: TIME_NANO + 1_000_000_000,
|
|
1325
|
+
attributes: attrs({ id: "node/pve1", "pve.scope": "node" }),
|
|
1326
|
+
},
|
|
1327
|
+
],
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
],
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
1334
|
+
(envelope["scopeMetrics"] as JSONArray).push(reportAboutPve1);
|
|
1335
|
+
expect(readProxmoxNativeNodeStatus(envelope)).toEqual({
|
|
1336
|
+
nodeName: "pve1",
|
|
1337
|
+
timeUnixNanos: [TIME_NANO],
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// A block whose only node/pve1 pve_up is a report is no status push.
|
|
1341
|
+
expect(
|
|
1342
|
+
readProxmoxNativeNodeStatus({
|
|
1343
|
+
resource: { attributes: NATIVE_PVE1 },
|
|
1344
|
+
scopeMetrics: [reportAboutPve1],
|
|
1345
|
+
}),
|
|
1346
|
+
).toBeNull();
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
test("pve_up of any other id is not the node's own", () => {
|
|
1350
|
+
// A block of pve1, translated upstream, carrying only other ids.
|
|
1351
|
+
expect(
|
|
1352
|
+
readProxmoxNativeNodeStatus(
|
|
1353
|
+
normalized(
|
|
1354
|
+
pveResource(
|
|
1355
|
+
[
|
|
1356
|
+
gauge("pve_up", 1, { id: "node/pve2", "pve.scope": "node" }),
|
|
1357
|
+
gauge("pve_up", 1, { id: "qemu/100", "pve.scope": "guest" }),
|
|
1358
|
+
gauge("pve_up", 1, {
|
|
1359
|
+
id: "storage/pve1/local",
|
|
1360
|
+
"pve.scope": "storage",
|
|
1361
|
+
}),
|
|
1362
|
+
// pve1's id, but not on pve_up.
|
|
1363
|
+
gauge("pve_node_info", 1, { id: "node/pve1", name: "pve1" }),
|
|
1364
|
+
],
|
|
1365
|
+
{ node: "pve1" },
|
|
1366
|
+
),
|
|
1367
|
+
),
|
|
1368
|
+
),
|
|
1369
|
+
).toBeNull();
|
|
1370
|
+
|
|
1371
|
+
// A node series labelled with another node derives that node's id.
|
|
1372
|
+
expect(
|
|
1373
|
+
readProxmoxNativeNodeStatus(
|
|
1374
|
+
normalized(
|
|
1375
|
+
pveResource([gauge("proxmox_node_uptime", 5, { node: "pve9" })], {
|
|
1376
|
+
node: "pve1",
|
|
1377
|
+
}),
|
|
1378
|
+
),
|
|
1379
|
+
),
|
|
1380
|
+
).toBeNull();
|
|
1381
|
+
|
|
1382
|
+
// Among other ids, only the node's own point is read.
|
|
1383
|
+
expect(
|
|
1384
|
+
readProxmoxNativeNodeStatus(
|
|
1385
|
+
normalized(
|
|
1386
|
+
pveResource(
|
|
1387
|
+
[
|
|
1388
|
+
gauge("pve_up", 1, { id: "node/pve2" }),
|
|
1389
|
+
{
|
|
1390
|
+
name: "pve_up",
|
|
1391
|
+
gauge: {
|
|
1392
|
+
dataPoints: [
|
|
1393
|
+
{
|
|
1394
|
+
asInt: 1,
|
|
1395
|
+
timeUnixNano: TIME_NANO + 5_000_000_000,
|
|
1396
|
+
attributes: attrs({ id: "node/pve1" }),
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1399
|
+
},
|
|
1400
|
+
},
|
|
1401
|
+
],
|
|
1402
|
+
{ node: "pve1" },
|
|
1403
|
+
),
|
|
1404
|
+
),
|
|
1405
|
+
),
|
|
1406
|
+
).toEqual({ nodeName: "pve1", timeUnixNanos: [TIME_NANO + 5_000_000_000] });
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
test("every own pve_up point is read in order, its timestamp exactly as sent", () => {
|
|
1410
|
+
// Two node-status scopes in one block, the second one protobuf-decoded.
|
|
1411
|
+
const later: string = String(TIME_NANO + 10_000_000_000);
|
|
1412
|
+
const envelope: JSONObject = nodePush("pve1");
|
|
1413
|
+
(envelope["scopeMetrics"] as JSONArray).push(
|
|
1414
|
+
...(withTime(nodePush("pve1"), later)["scopeMetrics"] as JSONArray),
|
|
1415
|
+
);
|
|
1416
|
+
expect(readProxmoxNativeNodeStatus(normalized(envelope))).toEqual({
|
|
1417
|
+
nodeName: "pve1",
|
|
1418
|
+
timeUnixNanos: [TIME_NANO, later],
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
test("an own pve_up without a timestamp reads as a null timestamp", () => {
|
|
1423
|
+
expect(
|
|
1424
|
+
readProxmoxNativeNodeStatus(
|
|
1425
|
+
normalized(withTime(nodePush("pve1"), undefined)),
|
|
1426
|
+
),
|
|
1427
|
+
).toEqual({ nodeName: "pve1", timeUnixNanos: [null] });
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
test("reads the same once the node's own reports are appended", () => {
|
|
1431
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
1432
|
+
const before: NodeStatus = readProxmoxNativeNodeStatus(envelope);
|
|
1433
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
1434
|
+
silentNodes: ["pve2", "pve3"],
|
|
1435
|
+
reporterCount: 1,
|
|
1436
|
+
timeUnixNanos: before!.timeUnixNanos,
|
|
1437
|
+
});
|
|
1438
|
+
expect(reportScopesOf(envelope)).toHaveLength(1);
|
|
1439
|
+
expect(readProxmoxNativeNodeStatus(envelope)).toEqual(before);
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
/*
|
|
1444
|
+
* D is the silent nodes, L the reporterCount — the live nodes, every one
|
|
1445
|
+
* of which reports (ProxmoxNativeNodeLiveness), so "reporters" below are
|
|
1446
|
+
* simply the live nodes.
|
|
1447
|
+
*/
|
|
1448
|
+
describe("splitProxmoxSiblingInfoWeights", () => {
|
|
1449
|
+
const UNITS_PER_NODE: number = 65536; // 2^16
|
|
1450
|
+
|
|
1451
|
+
function unitsOf(weights: Array<SiblingWeight>): Array<number> {
|
|
1452
|
+
return weights.map((w: SiblingWeight) => {
|
|
1453
|
+
return w.weight * UNITS_PER_NODE;
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function valuesOf(weights: Array<SiblingWeight>): Array<number> {
|
|
1458
|
+
return weights.map((w: SiblingWeight) => {
|
|
1459
|
+
return w.weight;
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
test("nothing to split without silent nodes or reporters", () => {
|
|
1464
|
+
expect(splitProxmoxSiblingInfoWeights([], 3)).toEqual([]);
|
|
1465
|
+
expect(splitProxmoxSiblingInfoWeights([], 0)).toEqual([]);
|
|
1466
|
+
expect(splitProxmoxSiblingInfoWeights(["pve3"], 0)).toEqual([]);
|
|
1467
|
+
expect(splitProxmoxSiblingInfoWeights(["pve3"], -1)).toEqual([]);
|
|
1468
|
+
expect(splitProxmoxSiblingInfoWeights(["pve3"], 0.5)).toEqual([]);
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
test("one reporter, one silent node: the report weighs one node", () => {
|
|
1472
|
+
expect(splitProxmoxSiblingInfoWeights(["pve2"], 1)).toEqual([
|
|
1473
|
+
{ nodeName: "pve2", weight: 1 },
|
|
1474
|
+
]);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
test("simple splits", () => {
|
|
1478
|
+
expect(splitProxmoxSiblingInfoWeights(["pve3"], 2)).toEqual([
|
|
1479
|
+
{ nodeName: "pve3", weight: 0.5 },
|
|
1480
|
+
]);
|
|
1481
|
+
expect(splitProxmoxSiblingInfoWeights(["pve2", "pve3"], 1)).toEqual([
|
|
1482
|
+
{ nodeName: "pve2", weight: 1 },
|
|
1483
|
+
{ nodeName: "pve3", weight: 1 },
|
|
1484
|
+
]);
|
|
1485
|
+
expect(splitProxmoxSiblingInfoWeights(["pve3", "pve4"], 4)).toEqual([
|
|
1486
|
+
{ nodeName: "pve3", weight: 0.25 },
|
|
1487
|
+
{ nodeName: "pve4", weight: 0.25 },
|
|
1488
|
+
]);
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
test("keeps the silent nodes' names and order, and leaves the input alone", () => {
|
|
1492
|
+
const silentNodes: Array<string> = ["pve9", "pve3", "pve5"];
|
|
1493
|
+
expect(
|
|
1494
|
+
splitProxmoxSiblingInfoWeights(silentNodes, 2).map((w: SiblingWeight) => {
|
|
1495
|
+
return w.nodeName;
|
|
1496
|
+
}),
|
|
1497
|
+
).toEqual(["pve9", "pve3", "pve5"]);
|
|
1498
|
+
expect(silentNodes).toEqual(["pve9", "pve3", "pve5"]);
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
test("L = D: one report's weights add up to exactly 1, in any order", () => {
|
|
1502
|
+
for (let silent: number = 1; silent <= 32; silent++) {
|
|
1503
|
+
const weights: Array<number> = valuesOf(weightsOf(silent, silent));
|
|
1504
|
+
expect(sumOf(weights)).toBe(1);
|
|
1505
|
+
expect(sumOf([...weights].reverse())).toBe(1);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
test("the remainder goes to the first nodes", () => {
|
|
1510
|
+
// 5 × 65536 ÷ 3 = 109226.67… → 109227 units: 2 × 21846 + 3 × 21845.
|
|
1511
|
+
expect(
|
|
1512
|
+
unitsOf(splitProxmoxSiblingInfoWeights(["a", "b", "c", "d", "e"], 3)),
|
|
1513
|
+
).toEqual([21846, 21846, 21845, 21845, 21845]);
|
|
1514
|
+
// 2 × 65536 ÷ 3 = 43690.67… → 43691 units: 21846 + 21845.
|
|
1515
|
+
expect(unitsOf(splitProxmoxSiblingInfoWeights(["a", "b"], 3))).toEqual([
|
|
1516
|
+
21846, 21845,
|
|
1517
|
+
]);
|
|
1518
|
+
// L = D = 3: 65536 units, 21846 + 21845 + 21845 — still exactly 1.
|
|
1519
|
+
expect(unitsOf(splitProxmoxSiblingInfoWeights(["a", "b", "c"], 3))).toEqual(
|
|
1520
|
+
[21846, 21845, 21845],
|
|
1521
|
+
);
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
test("every weight is a whole number of 2^-16 units, the total within one unit of D ÷ L, split as evenly as possible", () => {
|
|
1525
|
+
const failures: Array<string> = [];
|
|
1526
|
+
for (let silent: number = 1; silent <= 32; silent++) {
|
|
1527
|
+
for (let reporters: number = 1; reporters <= 32; reporters++) {
|
|
1528
|
+
const weights: Array<SiblingWeight> = weightsOf(silent, reporters);
|
|
1529
|
+
const units: Array<number> = unitsOf(weights);
|
|
1530
|
+
const label: string = `D=${silent} L=${reporters} units=${units.join(",")}`;
|
|
1531
|
+
if (weights.length !== silent) {
|
|
1532
|
+
failures.push(`${label}: ${weights.length} weights`);
|
|
1533
|
+
}
|
|
1534
|
+
if (!units.every(Number.isInteger)) {
|
|
1535
|
+
failures.push(`${label}: not whole units`);
|
|
1536
|
+
}
|
|
1537
|
+
if (
|
|
1538
|
+
Math.abs(sumOf(units) - (silent * UNITS_PER_NODE) / reporters) >= 1
|
|
1539
|
+
) {
|
|
1540
|
+
failures.push(`${label}: total is not within one unit of D ÷ L`);
|
|
1541
|
+
}
|
|
1542
|
+
if (Math.max(...units) - Math.min(...units) > 1) {
|
|
1543
|
+
failures.push(`${label}: uneven split`);
|
|
1544
|
+
}
|
|
1545
|
+
for (let i: number = 1; i < units.length; i++) {
|
|
1546
|
+
if ((units[i] as number) > (units[i - 1] as number)) {
|
|
1547
|
+
failures.push(`${label}: a later node got the remainder`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
expect(failures).toEqual([]);
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
test("L > D weighs less than one node per report, L < D more — only L = D weighs exactly one", () => {
|
|
1556
|
+
const failures: Array<string> = [];
|
|
1557
|
+
for (let silent: number = 1; silent <= 32; silent++) {
|
|
1558
|
+
for (let reporters: number = 1; reporters <= 32; reporters++) {
|
|
1559
|
+
const total: number = sumOf(valuesOf(weightsOf(silent, reporters)));
|
|
1560
|
+
const expected: boolean =
|
|
1561
|
+
reporters > silent
|
|
1562
|
+
? total < 1
|
|
1563
|
+
: reporters < silent
|
|
1564
|
+
? total > 1
|
|
1565
|
+
: total === 1;
|
|
1566
|
+
if (!expected) {
|
|
1567
|
+
failures.push(`D=${silent} L=${reporters}: total ${total}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
expect(failures).toEqual([]);
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
/*
|
|
1576
|
+
* Cluster Quorum at Risk is Σ pve_up ÷ Σ pve_node_info over every
|
|
1577
|
+
* pve.scope=node row of a minute, compared with <= 50 %. On a native push
|
|
1578
|
+
* each live node adds pve_up 1 and pve_node_info 1 per push, and — every
|
|
1579
|
+
* live node being a reporter (ProxmoxNativeNodeLiveness: reporterCount is
|
|
1580
|
+
* the live count L) — each of those pushes also adds the report: pve_up 0
|
|
1581
|
+
* and a weight per silent node, D ÷ L in all. The weights must keep L live
|
|
1582
|
+
* nodes over D silent ones at L ÷ (L + D), and L = D at exactly 50 % —
|
|
1583
|
+
* whatever order ClickHouse adds the rows in.
|
|
1584
|
+
*/
|
|
1585
|
+
describe("sibling-report weights keep Quorum at Risk exact at 50 %", () => {
|
|
1586
|
+
type Random = () => number;
|
|
1587
|
+
|
|
1588
|
+
const PUSH_INTERVAL_MS: number = 10_000;
|
|
1589
|
+
const MINUTE_MS: number = 60_000;
|
|
1590
|
+
|
|
1591
|
+
interface MinuteBucket {
|
|
1592
|
+
pushesPerNode: Array<number>;
|
|
1593
|
+
upValues: Array<number>; // every pve_up{pve.scope=node} row
|
|
1594
|
+
infoValues: Array<number>; // every pve_node_info{pve.scope=node} row
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/*
|
|
1598
|
+
* mulberry32: a small seeded PRNG, so every run explores the same
|
|
1599
|
+
* cases and a failure reproduces.
|
|
1600
|
+
*/
|
|
1601
|
+
function seededRandom(seed: number): Random {
|
|
1602
|
+
let state: number = seed >>> 0;
|
|
1603
|
+
return (): number => {
|
|
1604
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
1605
|
+
let t: number = state;
|
|
1606
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
1607
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
1608
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function shuffled<T>(items: Array<T>, random: Random): Array<T> {
|
|
1613
|
+
const out: Array<T> = [...items];
|
|
1614
|
+
for (let i: number = out.length - 1; i > 0; i--) {
|
|
1615
|
+
const j: number = Math.floor(random() * (i + 1));
|
|
1616
|
+
const swap: T = out[i] as T;
|
|
1617
|
+
out[i] = out[j] as T;
|
|
1618
|
+
out[j] = swap;
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/*
|
|
1624
|
+
* Sum the way ClickHouse does: the rows arrive in no particular order
|
|
1625
|
+
* and are spread over several threads that each keep a partial sum;
|
|
1626
|
+
* the partial sums are then added together, in no particular order
|
|
1627
|
+
* either.
|
|
1628
|
+
*/
|
|
1629
|
+
function parallelSum(values: Array<number>, random: Random): number {
|
|
1630
|
+
const partials: Array<number> = new Array<number>(
|
|
1631
|
+
1 + Math.floor(random() * 8),
|
|
1632
|
+
).fill(0);
|
|
1633
|
+
for (const value of shuffled(values, random)) {
|
|
1634
|
+
const index: number = Math.floor(random() * partials.length);
|
|
1635
|
+
partials[index] = (partials[index] as number) + value;
|
|
1636
|
+
}
|
|
1637
|
+
return sumOf(shuffled(partials, random));
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/*
|
|
1641
|
+
* `liveNodes` nodes — every one of them reporting, as every live node
|
|
1642
|
+
* does — push every ~10 s (pvestatd sleeps 10 s after a pass that
|
|
1643
|
+
* itself takes a moment) from a random phase, with jitter. A push is
|
|
1644
|
+
* the node's own pve_up = 1 and pve_node_info = 1, plus its report:
|
|
1645
|
+
* pve_up = 0 and the report's weight for every silent node.
|
|
1646
|
+
*/
|
|
1647
|
+
function simulateMinuteBuckets(data: {
|
|
1648
|
+
liveNodes: number;
|
|
1649
|
+
reportWeights: Array<number>;
|
|
1650
|
+
minutes: number;
|
|
1651
|
+
random: Random;
|
|
1652
|
+
}): Array<MinuteBucket> {
|
|
1653
|
+
const buckets: Array<MinuteBucket> = [];
|
|
1654
|
+
for (let minute: number = 0; minute < data.minutes; minute++) {
|
|
1655
|
+
buckets.push({
|
|
1656
|
+
pushesPerNode: new Array<number>(data.liveNodes).fill(0),
|
|
1657
|
+
upValues: [],
|
|
1658
|
+
infoValues: [],
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
for (let node: number = 0; node < data.liveNodes; node++) {
|
|
1662
|
+
let pushAtMs: number = data.random() * PUSH_INTERVAL_MS;
|
|
1663
|
+
while (pushAtMs < data.minutes * MINUTE_MS) {
|
|
1664
|
+
const bucket: MinuteBucket = buckets[
|
|
1665
|
+
Math.floor(pushAtMs / MINUTE_MS)
|
|
1666
|
+
] as MinuteBucket;
|
|
1667
|
+
bucket.pushesPerNode[node] = (bucket.pushesPerNode[node] as number) + 1;
|
|
1668
|
+
bucket.upValues.push(1);
|
|
1669
|
+
bucket.infoValues.push(1);
|
|
1670
|
+
for (const weight of data.reportWeights) {
|
|
1671
|
+
bucket.upValues.push(0);
|
|
1672
|
+
bucket.infoValues.push(weight);
|
|
1673
|
+
}
|
|
1674
|
+
// 9.7 s .. 10.7 s to the next push.
|
|
1675
|
+
pushAtMs += PUSH_INTERVAL_MS - 300 + data.random() * 1000;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return buckets;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// The template's formula: (nodes_online / nodes_total) * 100.
|
|
1682
|
+
function quorumPercent(bucket: MinuteBucket, random: Random): number {
|
|
1683
|
+
const online: number = parallelSum(bucket.upValues, random);
|
|
1684
|
+
const total: number = parallelSum(bucket.infoValues, random);
|
|
1685
|
+
return (online / total) * 100;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function boundaryFailure(data: {
|
|
1689
|
+
bucket: MinuteBucket;
|
|
1690
|
+
live: number;
|
|
1691
|
+
silent: number;
|
|
1692
|
+
random: Random;
|
|
1693
|
+
}): string | null {
|
|
1694
|
+
const label: string = `L=${data.live} D=${data.silent} pushes=${data.bucket.pushesPerNode.join(",")}`;
|
|
1695
|
+
for (const pushes of data.bucket.pushesPerNode) {
|
|
1696
|
+
if (pushes < 5 || pushes > 7) {
|
|
1697
|
+
return `${label}: a node pushed ${pushes} times in the minute`;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return ratioFailure({
|
|
1701
|
+
label,
|
|
1702
|
+
percent: quorumPercent(data.bucket, data.random),
|
|
1703
|
+
live: data.live,
|
|
1704
|
+
silent: data.silent,
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/*
|
|
1709
|
+
* A minute's Quorum at Risk percentage against the truth for L live
|
|
1710
|
+
* nodes over D silent ones: at or below 50 % when L <= D (exactly 50
|
|
1711
|
+
* when L = D, below when L < D), above when L > D, and L ÷ (L + D)
|
|
1712
|
+
* within the 2^-16 rounding of the weights.
|
|
1713
|
+
*/
|
|
1714
|
+
function ratioFailure(data: {
|
|
1715
|
+
label: string;
|
|
1716
|
+
percent: number;
|
|
1717
|
+
live: number;
|
|
1718
|
+
silent: number;
|
|
1719
|
+
}): string | null {
|
|
1720
|
+
const label: string = data.label;
|
|
1721
|
+
const percent: number = data.percent;
|
|
1722
|
+
if (data.live <= data.silent && !(percent <= 50)) {
|
|
1723
|
+
return `${label}: ${percent} % does not fire (<= 50 %)`;
|
|
1724
|
+
}
|
|
1725
|
+
if (data.live === data.silent && percent !== 50) {
|
|
1726
|
+
return `${label}: ${percent} % is not exactly 50 %`;
|
|
1727
|
+
}
|
|
1728
|
+
if (data.live < data.silent && !(percent < 50)) {
|
|
1729
|
+
return `${label}: ${percent} % is not below 50 %`;
|
|
1730
|
+
}
|
|
1731
|
+
if (data.live > data.silent && !(percent > 50)) {
|
|
1732
|
+
return `${label}: ${percent} % fires (<= 50 %) with a majority up`;
|
|
1733
|
+
}
|
|
1734
|
+
const exact: number = (100 * data.live) / (data.live + data.silent);
|
|
1735
|
+
// Within the 2^-16 rounding of the weights.
|
|
1736
|
+
if (Math.abs(percent - exact) > 0.002) {
|
|
1737
|
+
return `${label}: ${percent} % is not L ÷ (L + D) = ${exact} %`;
|
|
1738
|
+
}
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
test("2..32 nodes, every D: exactly 50 % when L = D, below when L < D, above when L > D — any push phase, any summation order", () => {
|
|
1743
|
+
const random: Random = seededRandom(0x5eed);
|
|
1744
|
+
const failures: Array<string> = [];
|
|
1745
|
+
let evenBuckets: number = 0;
|
|
1746
|
+
let unevenBuckets: number = 0;
|
|
1747
|
+
|
|
1748
|
+
for (let nodes: number = 2; nodes <= 32; nodes++) {
|
|
1749
|
+
for (let silent: number = 1; silent < nodes; silent++) {
|
|
1750
|
+
const live: number = nodes - silent;
|
|
1751
|
+
const reportWeights: Array<number> = weightsOf(silent, live).map(
|
|
1752
|
+
(w: SiblingWeight) => {
|
|
1753
|
+
return w.weight;
|
|
1754
|
+
},
|
|
1755
|
+
);
|
|
1756
|
+
for (let trial: number = 0; trial < 2; trial++) {
|
|
1757
|
+
for (const bucket of simulateMinuteBuckets({
|
|
1758
|
+
liveNodes: live,
|
|
1759
|
+
reportWeights,
|
|
1760
|
+
minutes: 3,
|
|
1761
|
+
random,
|
|
1762
|
+
})) {
|
|
1763
|
+
/*
|
|
1764
|
+
* Every live node reports, so the claim is not limited to a
|
|
1765
|
+
* minute in which every live node pushed the same number of
|
|
1766
|
+
* times: each push adds its own 1 and its report's D ÷ L
|
|
1767
|
+
* alike. Both kinds of minute are checked.
|
|
1768
|
+
*/
|
|
1769
|
+
if (new Set(bucket.pushesPerNode).size === 1) {
|
|
1770
|
+
evenBuckets++;
|
|
1771
|
+
} else {
|
|
1772
|
+
unevenBuckets++;
|
|
1773
|
+
}
|
|
1774
|
+
const failure: string | null = boundaryFailure({
|
|
1775
|
+
bucket,
|
|
1776
|
+
live,
|
|
1777
|
+
silent,
|
|
1778
|
+
random,
|
|
1779
|
+
});
|
|
1780
|
+
if (failure) {
|
|
1781
|
+
failures.push(failure);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
expect(failures).toEqual([]);
|
|
1789
|
+
// Both kinds of minute were exercised.
|
|
1790
|
+
expect(evenBuckets).toBeGreaterThan(100);
|
|
1791
|
+
expect(unevenBuckets).toBeGreaterThan(0);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
test("naive 1 ÷ L float weights do not: for some L half the cluster is down and the ratio reads above 50 %", () => {
|
|
1795
|
+
const random: Random = seededRandom(0x5eed);
|
|
1796
|
+
const missedAt: Set<number> = new Set();
|
|
1797
|
+
|
|
1798
|
+
// L = D, so 2..32 nodes.
|
|
1799
|
+
for (let live: number = 1; live <= 16; live++) {
|
|
1800
|
+
const naiveWeights: Array<number> = new Array<number>(live).fill(
|
|
1801
|
+
1 / live,
|
|
1802
|
+
);
|
|
1803
|
+
for (let trial: number = 0; trial < 4; trial++) {
|
|
1804
|
+
for (const bucket of simulateMinuteBuckets({
|
|
1805
|
+
liveNodes: live,
|
|
1806
|
+
reportWeights: naiveWeights,
|
|
1807
|
+
minutes: 3,
|
|
1808
|
+
random,
|
|
1809
|
+
})) {
|
|
1810
|
+
if (quorumPercent(bucket, random) > 50) {
|
|
1811
|
+
missedAt.add(live);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
// Quorum at Risk (<= 50 %) would stay quiet for these clusters.
|
|
1817
|
+
expect(missedAt.size).toBeGreaterThan(0);
|
|
1818
|
+
|
|
1819
|
+
/*
|
|
1820
|
+
* The plainest case: three live nodes each reporting the three dead
|
|
1821
|
+
* ones, six pushes each, summed in push order.
|
|
1822
|
+
*/
|
|
1823
|
+
function inPushOrder(reportWeights: Array<number>): number {
|
|
1824
|
+
let online: number = 0;
|
|
1825
|
+
let total: number = 0;
|
|
1826
|
+
for (let push: number = 0; push < 3 * 6; push++) {
|
|
1827
|
+
online += 1;
|
|
1828
|
+
total += 1;
|
|
1829
|
+
for (const weight of reportWeights) {
|
|
1830
|
+
total += weight;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return (online / total) * 100;
|
|
1834
|
+
}
|
|
1835
|
+
expect(inPushOrder([1 / 3, 1 / 3, 1 / 3])).toBeGreaterThan(50);
|
|
1836
|
+
expect(
|
|
1837
|
+
inPushOrder(
|
|
1838
|
+
weightsOf(3, 3).map((w: SiblingWeight) => {
|
|
1839
|
+
return w.weight;
|
|
1840
|
+
}),
|
|
1841
|
+
),
|
|
1842
|
+
).toBe(50);
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
/*
|
|
1846
|
+
* A minute of node-status pushes: live node i pushed pushesPerNode[i]
|
|
1847
|
+
* times, each push its own pve_up = 1 and pve_node_info = 1 plus its
|
|
1848
|
+
* report — pve_up = 0 and the report's weight for every silent node —
|
|
1849
|
+
* except the first unreportedPerNode[i] of node i's pushes, which went
|
|
1850
|
+
* through without a report.
|
|
1851
|
+
*/
|
|
1852
|
+
function minuteOf(data: {
|
|
1853
|
+
pushesPerNode: Array<number>;
|
|
1854
|
+
unreportedPerNode: Array<number>;
|
|
1855
|
+
reportWeights: Array<number>;
|
|
1856
|
+
}): MinuteBucket {
|
|
1857
|
+
const bucket: MinuteBucket = {
|
|
1858
|
+
pushesPerNode: [...data.pushesPerNode],
|
|
1859
|
+
upValues: [],
|
|
1860
|
+
infoValues: [],
|
|
1861
|
+
};
|
|
1862
|
+
data.pushesPerNode.forEach((pushes: number, node: number) => {
|
|
1863
|
+
const unreported: number = data.unreportedPerNode[node] ?? 0;
|
|
1864
|
+
for (let push: number = 0; push < pushes; push++) {
|
|
1865
|
+
bucket.upValues.push(1);
|
|
1866
|
+
bucket.infoValues.push(1);
|
|
1867
|
+
if (push < unreported) {
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
for (const weight of data.reportWeights) {
|
|
1871
|
+
bucket.upValues.push(0);
|
|
1872
|
+
bucket.infoValues.push(weight);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
return bucket;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Each live node's report weights: D ÷ L in all, L the live nodes.
|
|
1880
|
+
function reportWeightsOf(silent: number, live: number): Array<number> {
|
|
1881
|
+
return weightsOf(silent, live).map((w: SiblingWeight) => {
|
|
1882
|
+
return w.weight;
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/*
|
|
1887
|
+
* Every live node reports, and every report splits D ÷ L — L being all
|
|
1888
|
+
* the live nodes (reporterCount is the live count), not only the
|
|
1889
|
+
* established ones: the established gate is the cluster's, not the
|
|
1890
|
+
* reporter's, so whenever the cluster reports a silent node, a node in
|
|
1891
|
+
* its first two minutes of pushing reports it too, from its first push.
|
|
1892
|
+
* Each push of a minute therefore adds 1 to Σ pve_up and 1 + W to
|
|
1893
|
+
* Σ pve_node_info, W being one report's total, and the minute reads
|
|
1894
|
+
* 1 ÷ (1 + W) — L ÷ (L + D) — whichever node pushed how often.
|
|
1895
|
+
* pvestatd's passes are not evenly spaced: a slow pass, a push held up
|
|
1896
|
+
* in transit and processed late, a node that lands one push in a
|
|
1897
|
+
* minute and a dozen in the next.
|
|
1898
|
+
*
|
|
1899
|
+
* Every value is a whole number of 2^-16 units and the sums stay far
|
|
1900
|
+
* below 2^37, so ClickHouse's sums are exact in any order, and
|
|
1901
|
+
* P ÷ (P × (1 + W)) rounds to the very same double as 1 ÷ (1 + W): an
|
|
1902
|
+
* uneven minute reads bit for bit what one push from each node reads.
|
|
1903
|
+
*/
|
|
1904
|
+
test("every live node reports D ÷ L: the ratio is L ÷ (L + D) however unevenly the pushes fall — exactly 50 % at L = D", () => {
|
|
1905
|
+
const random: Random = seededRandom(0x5eed);
|
|
1906
|
+
const failures: Array<string> = [];
|
|
1907
|
+
|
|
1908
|
+
for (let nodes: number = 2; nodes <= 32; nodes++) {
|
|
1909
|
+
for (let silent: number = 1; silent < nodes; silent++) {
|
|
1910
|
+
const live: number = nodes - silent;
|
|
1911
|
+
const reportWeights: Array<number> = reportWeightsOf(silent, live);
|
|
1912
|
+
const noneUnreported: Array<number> = new Array<number>(live).fill(0);
|
|
1913
|
+
const onePushEach: number = quorumPercent(
|
|
1914
|
+
minuteOf({
|
|
1915
|
+
pushesPerNode: new Array<number>(live).fill(1),
|
|
1916
|
+
unreportedPerNode: noneUnreported,
|
|
1917
|
+
reportWeights,
|
|
1918
|
+
}),
|
|
1919
|
+
random,
|
|
1920
|
+
);
|
|
1921
|
+
|
|
1922
|
+
const pushCounts: Array<Array<number>> = [
|
|
1923
|
+
// One node barely pushed and the rest a dozen times, and back.
|
|
1924
|
+
[1, ...new Array<number>(live - 1).fill(12)],
|
|
1925
|
+
[12, ...new Array<number>(live - 1).fill(1)],
|
|
1926
|
+
];
|
|
1927
|
+
for (let trial: number = 0; trial < 3; trial++) {
|
|
1928
|
+
pushCounts.push(
|
|
1929
|
+
Array.from({ length: live }, (): number => {
|
|
1930
|
+
return 1 + Math.floor(random() * 12);
|
|
1931
|
+
}),
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
for (const pushesPerNode of pushCounts) {
|
|
1936
|
+
const label: string = `L=${live} D=${silent} pushes=${pushesPerNode.join(",")}`;
|
|
1937
|
+
const percent: number = quorumPercent(
|
|
1938
|
+
minuteOf({
|
|
1939
|
+
pushesPerNode,
|
|
1940
|
+
unreportedPerNode: noneUnreported,
|
|
1941
|
+
reportWeights,
|
|
1942
|
+
}),
|
|
1943
|
+
random,
|
|
1944
|
+
);
|
|
1945
|
+
if (percent !== onePushEach) {
|
|
1946
|
+
failures.push(
|
|
1947
|
+
`${label}: ${percent} % is not what one push each reads, ${onePushEach} %`,
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
const failure: string | null = ratioFailure({
|
|
1951
|
+
label,
|
|
1952
|
+
percent,
|
|
1953
|
+
live,
|
|
1954
|
+
silent,
|
|
1955
|
+
});
|
|
1956
|
+
if (failure) {
|
|
1957
|
+
failures.push(failure);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
expect(failures).toEqual([]);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
/*
|
|
1967
|
+
* Rounding. A report's weights add up to D ÷ L rounded UP to whole
|
|
1968
|
+
* 2^-16 units — W — so one round of the L live nodes' reports weighs at
|
|
1969
|
+
* least the D silent nodes: L × W ≥ D. That is the direction that keeps
|
|
1970
|
+
* the ratio at or below the truth — 1 ÷ (1 + W) ≤ L ÷ (L + D) exactly
|
|
1971
|
+
* when L × W ≥ D — so rounding can never lift a minute above
|
|
1972
|
+
* L ÷ (L + D), however slightly; and W is the smallest such weight, so
|
|
1973
|
+
* it overshoots by less than one unit a report. At the 50 % boundary,
|
|
1974
|
+
* L = D, L × W must reach D exactly, and does: W is 1, with nothing to
|
|
1975
|
+
* round. (That a majority up still reads above 50 % — W < 1 when
|
|
1976
|
+
* L > D — is pinned under splitProxmoxSiblingInfoWeights.) Rounding to
|
|
1977
|
+
* the nearest unit instead falls short whenever the fraction is under
|
|
1978
|
+
* half a unit: 3 live nodes over 4 silent would add 3.99998 a round
|
|
1979
|
+
* and read above 3 ÷ 7.
|
|
1980
|
+
*/
|
|
1981
|
+
test("rounding D ÷ L up: L × W reaches D for every L and D — exactly D at L = D, the 50 % boundary — so rounding never lifts the ratio above L ÷ (L + D)", () => {
|
|
1982
|
+
const UNITS_PER_NODE: number = 65536; // 2^16
|
|
1983
|
+
const failures: Array<string> = [];
|
|
1984
|
+
|
|
1985
|
+
for (let silent: number = 1; silent <= 64; silent++) {
|
|
1986
|
+
for (let live: number = 1; live <= 64; live++) {
|
|
1987
|
+
const units: number = sumOf(
|
|
1988
|
+
reportWeightsOf(silent, live).map((weight: number) => {
|
|
1989
|
+
return weight * UNITS_PER_NODE;
|
|
1990
|
+
}),
|
|
1991
|
+
);
|
|
1992
|
+
const label: string = `L=${live} D=${silent}: W = ${units} units`;
|
|
1993
|
+
if (live * units < silent * UNITS_PER_NODE) {
|
|
1994
|
+
failures.push(`${label}: L × W falls short of D`);
|
|
1995
|
+
}
|
|
1996
|
+
if (live * (units - 1) >= silent * UNITS_PER_NODE) {
|
|
1997
|
+
failures.push(`${label}: not the smallest W with L × W ≥ D`);
|
|
1998
|
+
}
|
|
1999
|
+
if (live === silent && units !== UNITS_PER_NODE) {
|
|
2000
|
+
failures.push(`${label}: L = D does not weigh exactly one node`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
expect(failures).toEqual([]);
|
|
2005
|
+
|
|
2006
|
+
// 3 live over 4 silent: 4 × 65536 ÷ 3 = 87381.33… units.
|
|
2007
|
+
const roundedUp: Array<number> = reportWeightsOf(4, 3);
|
|
2008
|
+
expect(sumOf(roundedUp) * UNITS_PER_NODE).toBe(87382);
|
|
2009
|
+
const nearestUnits: number = Math.round((4 * UNITS_PER_NODE) / 3);
|
|
2010
|
+
expect(nearestUnits).toBe(87381);
|
|
2011
|
+
expect(3 * nearestUnits).toBe(4 * UNITS_PER_NODE - 1); // 3.99998 a round
|
|
2012
|
+
// The same even split, of 87381 units instead of 87382.
|
|
2013
|
+
const nearest: Array<number> = [21846, 21845, 21845, 21845].map(
|
|
2014
|
+
(units: number) => {
|
|
2015
|
+
return units / UNITS_PER_NODE;
|
|
2016
|
+
},
|
|
2017
|
+
);
|
|
2018
|
+
expect(sumOf(nearest) * UNITS_PER_NODE).toBe(nearestUnits);
|
|
2019
|
+
|
|
2020
|
+
const random: Random = seededRandom(0x5eed);
|
|
2021
|
+
function percentWith(reportWeights: Array<number>): number {
|
|
2022
|
+
return quorumPercent(
|
|
2023
|
+
minuteOf({
|
|
2024
|
+
pushesPerNode: [6, 6, 6],
|
|
2025
|
+
unreportedPerNode: [0, 0, 0],
|
|
2026
|
+
reportWeights,
|
|
2027
|
+
}),
|
|
2028
|
+
random,
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const truth: number = (100 * 3) / 7;
|
|
2032
|
+
expect(percentWith(roundedUp)).toBeLessThan(truth);
|
|
2033
|
+
expect(percentWith(nearest)).toBeGreaterThan(truth);
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
/*
|
|
2037
|
+
* The known limit. The ratio is L ÷ (L + D) only while EVERY push of
|
|
2038
|
+
* every live node carries its report. A live node whose push goes
|
|
2039
|
+
* through without one — its liveness check failed closed on a Redis
|
|
2040
|
+
* error, say — still adds its own 1 to both sums but not the silent
|
|
2041
|
+
* nodes' D ÷ L, so the minute holding that push reads above
|
|
2042
|
+
* L ÷ (L + D): at L = D above 50 %, and Quorum at Risk does not fire on
|
|
2043
|
+
* it. (Node Offline is not affected as long as another push of the
|
|
2044
|
+
* minute still says pve_up = 0 for the silent node.) Nothing here
|
|
2045
|
+
* corrects for it; this pins the limit so that a change to it is
|
|
2046
|
+
* deliberate.
|
|
2047
|
+
*/
|
|
2048
|
+
test("known limit: a live node that pushes without reporting (a fail-closed Redis error) lifts the minute above L ÷ (L + D) — at L = D, above 50 %", () => {
|
|
2049
|
+
const random: Random = seededRandom(0x5eed);
|
|
2050
|
+
const failures: Array<string> = [];
|
|
2051
|
+
|
|
2052
|
+
for (let nodes: number = 2; nodes <= 32; nodes++) {
|
|
2053
|
+
for (let silent: number = 1; silent < nodes; silent++) {
|
|
2054
|
+
const live: number = nodes - silent;
|
|
2055
|
+
// Six pushes each; one push of one node carries no report.
|
|
2056
|
+
const unreportedPerNode: Array<number> = new Array<number>(live).fill(
|
|
2057
|
+
0,
|
|
2058
|
+
);
|
|
2059
|
+
unreportedPerNode[0] = 1;
|
|
2060
|
+
const percent: number = quorumPercent(
|
|
2061
|
+
minuteOf({
|
|
2062
|
+
pushesPerNode: new Array<number>(live).fill(6),
|
|
2063
|
+
unreportedPerNode,
|
|
2064
|
+
reportWeights: reportWeightsOf(silent, live),
|
|
2065
|
+
}),
|
|
2066
|
+
random,
|
|
2067
|
+
);
|
|
2068
|
+
const truth: number = (100 * live) / (live + silent);
|
|
2069
|
+
const label: string = `L=${live} D=${silent}: ${percent} %`;
|
|
2070
|
+
if (!(percent > truth)) {
|
|
2071
|
+
failures.push(`${label} is not above L ÷ (L + D) = ${truth} %`);
|
|
2072
|
+
}
|
|
2073
|
+
if (live === silent && !(percent > 50)) {
|
|
2074
|
+
failures.push(`${label} is not above 50 %`);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
expect(failures).toEqual([]);
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
describe("appendProxmoxSiblingReportsInPlace", () => {
|
|
2084
|
+
type SiblingReport = Parameters<typeof appendProxmoxSiblingReportsInPlace>[1];
|
|
2085
|
+
|
|
2086
|
+
const INFERRED: JSONObject = {
|
|
2087
|
+
key: PROXMOX_INFERRED_ATTRIBUTE,
|
|
2088
|
+
value: { stringValue: PROXMOX_INFERRED_NOT_REPORTING },
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
// pve1's own status push, rewritten, with its reports appended.
|
|
2092
|
+
function reportingPush(report: {
|
|
2093
|
+
silentNodes: Array<string>;
|
|
2094
|
+
reporterCount: number;
|
|
2095
|
+
timeUnixNanos?: Array<JSONValue> | undefined;
|
|
2096
|
+
}): JSONObject {
|
|
2097
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
2098
|
+
const status: NodeStatus = readProxmoxNativeNodeStatus(envelope);
|
|
2099
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2100
|
+
silentNodes: report.silentNodes,
|
|
2101
|
+
reporterCount: report.reporterCount,
|
|
2102
|
+
timeUnixNanos: report.timeUnixNanos ?? status!.timeUnixNanos,
|
|
2103
|
+
});
|
|
2104
|
+
return envelope;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// The attributes a node's OWN push gives one of its own series.
|
|
2108
|
+
function ownAttributes(node: string, metricName: string): JSONArray {
|
|
2109
|
+
return find(
|
|
2110
|
+
pveSeries(normalized(nodePush(node))),
|
|
2111
|
+
metricName,
|
|
2112
|
+
`node/${node}`,
|
|
2113
|
+
).point["attributes"] as JSONArray;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function pointsFor(points: Array<JSONObject>, id: string): Array<JSONObject> {
|
|
2117
|
+
return points.filter((point: JSONObject) => {
|
|
2118
|
+
return labelsOf(point)["id"] === id;
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function idsOf(points: Array<JSONObject>): Array<string | undefined> {
|
|
2123
|
+
return points.map((point: JSONObject) => {
|
|
2124
|
+
return labelsOf(point)["id"];
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
test("marks the reports with the documented attribute", () => {
|
|
2129
|
+
expect(PROXMOX_INFERRED_ATTRIBUTE).toBe("oneuptime.proxmox.inferred");
|
|
2130
|
+
expect(PROXMOX_INFERRED_NOT_REPORTING).toBe("not-reporting");
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
test("appends exactly one scopeMetrics entry, in its own named and versioned scope", () => {
|
|
2134
|
+
const own: number = (
|
|
2135
|
+
normalized(nodePush("pve1"))["scopeMetrics"] as JSONArray
|
|
2136
|
+
).length;
|
|
2137
|
+
const envelope: JSONObject = reportingPush({
|
|
2138
|
+
silentNodes: ["pve2", "pve3"],
|
|
2139
|
+
reporterCount: 2,
|
|
2140
|
+
});
|
|
2141
|
+
const scopeMetrics: Array<JSONObject> = envelope[
|
|
2142
|
+
"scopeMetrics"
|
|
2143
|
+
] as Array<JSONObject>;
|
|
2144
|
+
expect(scopeMetrics).toHaveLength(own + 1);
|
|
2145
|
+
|
|
2146
|
+
const report: JSONObject = scopeMetrics[scopeMetrics.length - 1]!;
|
|
2147
|
+
expect(report["scope"]).toEqual({
|
|
2148
|
+
name: PROXMOX_SIBLING_REPORT_SCOPE_NAME,
|
|
2149
|
+
version: "1",
|
|
2150
|
+
});
|
|
2151
|
+
expect(isProxmoxSiblingReportScope(report)).toBe(true);
|
|
2152
|
+
expect(report["metrics"]).toEqual([
|
|
2153
|
+
{ name: "pve_up", gauge: { dataPoints: expect.any(Array) } },
|
|
2154
|
+
{ name: "pve_node_info", gauge: { dataPoints: expect.any(Array) } },
|
|
2155
|
+
]);
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
test("pve_up = 0, as an int, for every silent node at the push's own timestamp", () => {
|
|
2159
|
+
const up: Array<JSONObject> = reportPointsOf(
|
|
2160
|
+
reportingPush({ silentNodes: ["pve2", "pve3"], reporterCount: 2 }),
|
|
2161
|
+
"pve_up",
|
|
2162
|
+
);
|
|
2163
|
+
expect(idsOf(up)).toEqual(["node/pve2", "node/pve3"]);
|
|
2164
|
+
for (const point of up) {
|
|
2165
|
+
expect(point["asInt"]).toBe("0");
|
|
2166
|
+
expect(point["asDouble"]).toBeUndefined();
|
|
2167
|
+
expect(point["timeUnixNano"]).toBe(TIME_NANO);
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test("pve_node_info carries each silent node's weight as a double — D ÷ L in all", () => {
|
|
2172
|
+
// Two silent, three live (each reports): 2 × 65536 ÷ 3 → 21846 + 21845.
|
|
2173
|
+
const info: Array<JSONObject> = reportPointsOf(
|
|
2174
|
+
reportingPush({ silentNodes: ["pve4", "pve5"], reporterCount: 3 }),
|
|
2175
|
+
"pve_node_info",
|
|
2176
|
+
);
|
|
2177
|
+
expect(
|
|
2178
|
+
info.map((point: JSONObject) => {
|
|
2179
|
+
return [labelsOf(point)["id"], point["asDouble"]];
|
|
2180
|
+
}),
|
|
2181
|
+
).toEqual([
|
|
2182
|
+
["node/pve4", 21846 / 65536],
|
|
2183
|
+
["node/pve5", 21845 / 65536],
|
|
2184
|
+
]);
|
|
2185
|
+
for (const point of info) {
|
|
2186
|
+
expect(point["asInt"]).toBeUndefined();
|
|
2187
|
+
expect(point["timeUnixNano"]).toBe(TIME_NANO);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// L = D: one report weighs exactly one node.
|
|
2191
|
+
const even: Array<JSONObject> = reportPointsOf(
|
|
2192
|
+
reportingPush({ silentNodes: ["pve2", "pve3"], reporterCount: 2 }),
|
|
2193
|
+
"pve_node_info",
|
|
2194
|
+
);
|
|
2195
|
+
expect(
|
|
2196
|
+
sumOf(
|
|
2197
|
+
even.map((point: JSONObject) => {
|
|
2198
|
+
return point["asDouble"] as number;
|
|
2199
|
+
}),
|
|
2200
|
+
),
|
|
2201
|
+
).toBe(1);
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
test("the labels are exactly those the silent node's own push carries, plus the inferred marker", () => {
|
|
2205
|
+
const envelope: JSONObject = reportingPush({
|
|
2206
|
+
silentNodes: ["pve2", "pve3"],
|
|
2207
|
+
reporterCount: 2,
|
|
2208
|
+
});
|
|
2209
|
+
for (const node of ["pve2", "pve3"]) {
|
|
2210
|
+
const up: Array<JSONObject> = pointsFor(
|
|
2211
|
+
reportPointsOf(envelope, "pve_up"),
|
|
2212
|
+
`node/${node}`,
|
|
2213
|
+
);
|
|
2214
|
+
const info: Array<JSONObject> = pointsFor(
|
|
2215
|
+
reportPointsOf(envelope, "pve_node_info"),
|
|
2216
|
+
`node/${node}`,
|
|
2217
|
+
);
|
|
2218
|
+
expect(up).toHaveLength(1);
|
|
2219
|
+
expect(info).toHaveLength(1);
|
|
2220
|
+
expect(up[0]!["attributes"]).toEqual([
|
|
2221
|
+
...ownAttributes(node, "pve_up"),
|
|
2222
|
+
INFERRED,
|
|
2223
|
+
]);
|
|
2224
|
+
expect(info[0]!["attributes"]).toEqual([
|
|
2225
|
+
...ownAttributes(node, "pve_node_info"),
|
|
2226
|
+
INFERRED,
|
|
2227
|
+
]);
|
|
2228
|
+
expect(labelsOf(info[0]!)).toEqual({
|
|
2229
|
+
id: `node/${node}`,
|
|
2230
|
+
"pve.scope": "node",
|
|
2231
|
+
"pve.type": "node",
|
|
2232
|
+
"pve.id": node,
|
|
2233
|
+
name: node,
|
|
2234
|
+
[PROXMOX_INFERRED_ATTRIBUTE]: PROXMOX_INFERRED_NOT_REPORTING,
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
test("one point per silent node per own timestamp, each timestamp kept exactly as sent", () => {
|
|
2240
|
+
const later: string = String(TIME_NANO + 10_000_000_000);
|
|
2241
|
+
const envelope: JSONObject = reportingPush({
|
|
2242
|
+
silentNodes: ["pve2", "pve3", "pve4"],
|
|
2243
|
+
reporterCount: 1,
|
|
2244
|
+
timeUnixNanos: [TIME_NANO, later],
|
|
2245
|
+
});
|
|
2246
|
+
for (const metricName of ["pve_up", "pve_node_info"]) {
|
|
2247
|
+
const points: Array<JSONObject> = reportPointsOf(envelope, metricName);
|
|
2248
|
+
expect(points).toHaveLength(6);
|
|
2249
|
+
for (const node of ["pve2", "pve3", "pve4"]) {
|
|
2250
|
+
expect(
|
|
2251
|
+
pointsFor(points, `node/${node}`).map((point: JSONObject) => {
|
|
2252
|
+
return point["timeUnixNano"];
|
|
2253
|
+
}),
|
|
2254
|
+
).toEqual([TIME_NANO, later]);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
test("an own point without a timestamp gives reports without one", () => {
|
|
2260
|
+
const envelope: JSONObject = reportingPush({
|
|
2261
|
+
silentNodes: ["pve2"],
|
|
2262
|
+
reporterCount: 1,
|
|
2263
|
+
timeUnixNanos: [null],
|
|
2264
|
+
});
|
|
2265
|
+
for (const metricName of ["pve_up", "pve_node_info"]) {
|
|
2266
|
+
const points: Array<JSONObject> = reportPointsOf(envelope, metricName);
|
|
2267
|
+
expect(points).toHaveLength(1);
|
|
2268
|
+
expect(points[0]).not.toHaveProperty("timeUnixNano");
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
test("reports in the given order, and leaves the caller's arrays alone", () => {
|
|
2273
|
+
const silentNodes: Array<string> = ["pve3", "pve2"];
|
|
2274
|
+
const timeUnixNanos: Array<JSONValue> = [TIME_NANO];
|
|
2275
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
2276
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2277
|
+
silentNodes,
|
|
2278
|
+
reporterCount: 3,
|
|
2279
|
+
timeUnixNanos,
|
|
2280
|
+
});
|
|
2281
|
+
expect(silentNodes).toEqual(["pve3", "pve2"]);
|
|
2282
|
+
expect(timeUnixNanos).toEqual([TIME_NANO]);
|
|
2283
|
+
expect(idsOf(reportPointsOf(envelope, "pve_up"))).toEqual([
|
|
2284
|
+
"node/pve3",
|
|
2285
|
+
"node/pve2",
|
|
2286
|
+
]);
|
|
2287
|
+
// The remainder unit goes to the first node given.
|
|
2288
|
+
expect(
|
|
2289
|
+
reportPointsOf(envelope, "pve_node_info").map((point: JSONObject) => {
|
|
2290
|
+
return (point["asDouble"] as number) * 65536;
|
|
2291
|
+
}),
|
|
2292
|
+
).toEqual([21846, 21845]);
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
test("idempotent: a second append changes nothing, whatever it reports", () => {
|
|
2296
|
+
const envelope: JSONObject = reportingPush({
|
|
2297
|
+
silentNodes: ["pve2"],
|
|
2298
|
+
reporterCount: 1,
|
|
2299
|
+
});
|
|
2300
|
+
const once: string = JSON.stringify(envelope);
|
|
2301
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2302
|
+
silentNodes: ["pve2"],
|
|
2303
|
+
reporterCount: 1,
|
|
2304
|
+
timeUnixNanos: [TIME_NANO],
|
|
2305
|
+
});
|
|
2306
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2307
|
+
silentNodes: ["pve3", "pve4"],
|
|
2308
|
+
reporterCount: 2,
|
|
2309
|
+
timeUnixNanos: [TIME_NANO + 1_000_000_000],
|
|
2310
|
+
});
|
|
2311
|
+
expect(JSON.stringify(envelope)).toBe(once);
|
|
2312
|
+
expect(reportScopesOf(envelope)).toHaveLength(1);
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
test("the node's own series and its resource are left exactly as they were", () => {
|
|
2316
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
2317
|
+
const before: JSONObject = JSON.parse(
|
|
2318
|
+
JSON.stringify(envelope),
|
|
2319
|
+
) as JSONObject;
|
|
2320
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2321
|
+
silentNodes: ["pve2", "pve3"],
|
|
2322
|
+
reporterCount: 2,
|
|
2323
|
+
timeUnixNanos: [TIME_NANO],
|
|
2324
|
+
});
|
|
2325
|
+
expect((envelope["scopeMetrics"] as JSONArray).slice(0, -1)).toEqual(
|
|
2326
|
+
before["scopeMetrics"],
|
|
2327
|
+
);
|
|
2328
|
+
expect(envelope["resource"]).toEqual(before["resource"]);
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
test("the reports survive a second rewrite pass untouched", () => {
|
|
2332
|
+
const envelope: JSONObject = reportingPush({
|
|
2333
|
+
silentNodes: ["pve2", "pve3"],
|
|
2334
|
+
reporterCount: 2,
|
|
2335
|
+
});
|
|
2336
|
+
const once: string = JSON.stringify(envelope);
|
|
2337
|
+
normalizeProxmoxNativePushInPlace([envelope]);
|
|
2338
|
+
expect(JSON.stringify(envelope)).toBe(once);
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
test("appends nothing without silent nodes, timestamps or reporters", () => {
|
|
2342
|
+
const reports: Array<SiblingReport> = [
|
|
2343
|
+
{ silentNodes: [], reporterCount: 2, timeUnixNanos: [TIME_NANO] },
|
|
2344
|
+
{ silentNodes: ["pve2"], reporterCount: 2, timeUnixNanos: [] },
|
|
2345
|
+
{ silentNodes: ["pve2"], reporterCount: 0, timeUnixNanos: [TIME_NANO] },
|
|
2346
|
+
// No usable node name: there is no id to report.
|
|
2347
|
+
{ silentNodes: [""], reporterCount: 1, timeUnixNanos: [TIME_NANO] },
|
|
2348
|
+
];
|
|
2349
|
+
for (const report of reports) {
|
|
2350
|
+
const envelope: JSONObject = normalized(nodePush("pve1"));
|
|
2351
|
+
const before: string = JSON.stringify(envelope);
|
|
2352
|
+
appendProxmoxSiblingReportsInPlace(envelope, report);
|
|
2353
|
+
expect(JSON.stringify(envelope)).toBe(before);
|
|
2354
|
+
expect(reportScopesOf(envelope)).toEqual([]);
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
|
|
2358
|
+
test("appends nothing to a block without a scopeMetrics array, and never throws", () => {
|
|
2359
|
+
const values: Array<JSONValue | undefined> = [undefined, null, "nope", {}];
|
|
2360
|
+
for (const scopeMetrics of values) {
|
|
2361
|
+
const envelope: JSONObject = {
|
|
2362
|
+
resource: {
|
|
2363
|
+
attributes: attrs({
|
|
2364
|
+
"proxmox.cluster": "homelab",
|
|
2365
|
+
"proxmox.node": "pve1",
|
|
2366
|
+
}),
|
|
2367
|
+
},
|
|
2368
|
+
};
|
|
2369
|
+
if (scopeMetrics !== undefined) {
|
|
2370
|
+
envelope["scopeMetrics"] = scopeMetrics;
|
|
2371
|
+
}
|
|
2372
|
+
const before: string = JSON.stringify(envelope);
|
|
2373
|
+
expect(() => {
|
|
2374
|
+
appendProxmoxSiblingReportsInPlace(envelope, {
|
|
2375
|
+
silentNodes: ["pve2"],
|
|
2376
|
+
reporterCount: 1,
|
|
2377
|
+
timeUnixNanos: [TIME_NANO],
|
|
2378
|
+
});
|
|
2379
|
+
}).not.toThrow();
|
|
2380
|
+
expect(JSON.stringify(envelope)).toBe(before);
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
test("every inferred point sits in the report scope, and the report scope holds nothing else", () => {
|
|
2385
|
+
const envelope: JSONObject = reportingPush({
|
|
2386
|
+
silentNodes: ["pve2", "pve3"],
|
|
2387
|
+
reporterCount: 2,
|
|
2388
|
+
});
|
|
2389
|
+
let inferredPoints: number = 0;
|
|
2390
|
+
for (const scopeMetric of envelope["scopeMetrics"] as Array<JSONObject>) {
|
|
2391
|
+
const isReport: boolean = isProxmoxSiblingReportScope(scopeMetric);
|
|
2392
|
+
for (const metric of scopeMetric["metrics"] as Array<JSONObject>) {
|
|
2393
|
+
for (const point of pointsOf(metric)) {
|
|
2394
|
+
const inferred: boolean =
|
|
2395
|
+
labelsOf(point)[PROXMOX_INFERRED_ATTRIBUTE] ===
|
|
2396
|
+
PROXMOX_INFERRED_NOT_REPORTING;
|
|
2397
|
+
expect(inferred).toBe(isReport);
|
|
2398
|
+
if (inferred) {
|
|
2399
|
+
inferredPoints++;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
expect(inferredPoints).toBe(4);
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
/*
|
|
2408
|
+
* The ingest's inventory fold skips the report scope: a report must
|
|
2409
|
+
* never refresh the silent node's row. Folding the push as the ingest
|
|
2410
|
+
* does leaves only the reporter; folding the report scope too would
|
|
2411
|
+
* give each silent node a row stamped with the reporter's time.
|
|
2412
|
+
*/
|
|
2413
|
+
test("with the report scope skipped, as the ingest does, the inventory fold sees only the reporter", () => {
|
|
2414
|
+
function fold(
|
|
2415
|
+
envelope: JSONObject,
|
|
2416
|
+
skipReports: boolean,
|
|
2417
|
+
): Array<ProxmoxResourceBufferEntry> {
|
|
2418
|
+
const resourceBuffer: Map<
|
|
2419
|
+
string,
|
|
2420
|
+
Map<string, ProxmoxResourceBufferEntry>
|
|
2421
|
+
> = new Map();
|
|
2422
|
+
const clusterBuffer: Map<string, ProxmoxClusterSnapshotBufferEntry> =
|
|
2423
|
+
new Map();
|
|
2424
|
+
for (const scopeMetric of envelope["scopeMetrics"] as Array<JSONObject>) {
|
|
2425
|
+
if (skipReports && isProxmoxSiblingReportScope(scopeMetric)) {
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
for (const metric of scopeMetric["metrics"] as Array<JSONObject>) {
|
|
2429
|
+
const metricName: string = metric["name"] as string;
|
|
2430
|
+
if (!PVE_SNAPSHOT_METRIC_NAMES.has(metricName)) {
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
for (const datapoint of pointsOf(metric)) {
|
|
2434
|
+
bufferProxmoxSnapshotMetric({
|
|
2435
|
+
clusterIdStr: "c1",
|
|
2436
|
+
metricName,
|
|
2437
|
+
datapoint,
|
|
2438
|
+
resourceBuffer,
|
|
2439
|
+
clusterBuffer,
|
|
2440
|
+
countsFromInventory: true,
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return [...(resourceBuffer.get("c1")?.values() || [])].sort(
|
|
2446
|
+
(a: ProxmoxResourceBufferEntry, b: ProxmoxResourceBufferEntry) => {
|
|
2447
|
+
return a.externalId.localeCompare(b.externalId);
|
|
2448
|
+
},
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function externalIdsOf(
|
|
2453
|
+
rows: Array<ProxmoxResourceBufferEntry>,
|
|
2454
|
+
): Array<string> {
|
|
2455
|
+
return rows.map((row: ProxmoxResourceBufferEntry) => {
|
|
2456
|
+
return row.externalId;
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
const envelope: JSONObject = reportingPush({
|
|
2461
|
+
silentNodes: ["pve2", "pve3"],
|
|
2462
|
+
reporterCount: 2,
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
const rows: Array<ProxmoxResourceBufferEntry> = fold(envelope, true);
|
|
2466
|
+
expect(externalIdsOf(rows)).toEqual(["node/pve1"]);
|
|
2467
|
+
expect(rows[0]).toMatchObject({ kind: "Node", name: "pve1", isUp: true });
|
|
2468
|
+
|
|
2469
|
+
expect(externalIdsOf(fold(envelope, false))).toEqual([
|
|
2470
|
+
"node/pve1",
|
|
2471
|
+
"node/pve2",
|
|
2472
|
+
"node/pve3",
|
|
2473
|
+
]);
|
|
2474
|
+
});
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
/*
|
|
2478
|
+
* Which built-in Proxmox alert templates a native push can drive. Every
|
|
2479
|
+
* query of a served template must find a translated series with its
|
|
2480
|
+
* metric name, its attribute filters and its group-by label. A template
|
|
2481
|
+
* added later fails here until it is classified — and the docs, which
|
|
2482
|
+
* list the unserved ones, are updated with it.
|
|
2483
|
+
*
|
|
2484
|
+
* The payload is a three-node cluster whose third node, pve3, has died:
|
|
2485
|
+
* pve1 and pve2 still push their own status (pve1 its guests and storage
|
|
2486
|
+
* too), and each live node's status push carries the report the ingest
|
|
2487
|
+
* appends for the silent sibling — every live node reports, with
|
|
2488
|
+
* reporterCount the number of live nodes.
|
|
2489
|
+
*/
|
|
2490
|
+
describe("built-in alert templates on a native push", () => {
|
|
2491
|
+
const SERVED: Array<string> = [
|
|
2492
|
+
"pve-node-high-cpu",
|
|
2493
|
+
"pve-node-high-memory",
|
|
2494
|
+
"pve-guest-high-cpu",
|
|
2495
|
+
"pve-storage-near-full",
|
|
2496
|
+
"pve-lxc-disk-near-full",
|
|
2497
|
+
];
|
|
2498
|
+
/*
|
|
2499
|
+
* Each node pushes only its own status, so a node that dies goes quiet
|
|
2500
|
+
* instead of reporting pve_up = 0. These two see it anyway: the live
|
|
2501
|
+
* nodes' status pushes carry pve_up = 0 for it — and its pve_node_info
|
|
2502
|
+
* share — with the labels its own push would carry.
|
|
2503
|
+
*/
|
|
2504
|
+
const SERVED_BY_SIBLING_REPORTS: Array<string> = [
|
|
2505
|
+
"pve-node-offline",
|
|
2506
|
+
"pve-quorum-risk",
|
|
2507
|
+
];
|
|
2508
|
+
// Data the native push does not carry at all.
|
|
2509
|
+
const NOT_SERVED: Array<string> = [
|
|
2510
|
+
"pve-guest-down", // needs pve_onboot_status
|
|
2511
|
+
"pve-ha-state-error", // needs pve_ha_state
|
|
2512
|
+
"pve-guest-not-backed-up", // needs the backup-info collector
|
|
2513
|
+
"pve-replication-failing", // needs the replication collector
|
|
2514
|
+
];
|
|
2515
|
+
|
|
2516
|
+
const DEAD_NODE_ID: string = "node/pve3";
|
|
2517
|
+
|
|
2518
|
+
interface TemplateQuery {
|
|
2519
|
+
metricName: string;
|
|
2520
|
+
attributes: Record<string, string>;
|
|
2521
|
+
groupBy: Array<string>;
|
|
2522
|
+
aggregation: MetricsAggregationType;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
interface QuorumCase {
|
|
2526
|
+
title: string;
|
|
2527
|
+
live: Array<string>;
|
|
2528
|
+
dead: Array<string>;
|
|
2529
|
+
percent: number;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
/*
|
|
2533
|
+
* The node-status pushes of a native-push cluster as the ingest emits
|
|
2534
|
+
* them: rewritten, each live node reporting the dead ones.
|
|
2535
|
+
*/
|
|
2536
|
+
function clusterNodePushes(data: {
|
|
2537
|
+
live: Array<string>;
|
|
2538
|
+
dead: Array<string>;
|
|
2539
|
+
}): Array<JSONObject> {
|
|
2540
|
+
const pushes: Array<JSONObject> = data.live.map((node: string) => {
|
|
2541
|
+
return normalized(nodePush(node));
|
|
2542
|
+
});
|
|
2543
|
+
for (const push of pushes) {
|
|
2544
|
+
const status: NodeStatus = readProxmoxNativeNodeStatus(push);
|
|
2545
|
+
if (!status) {
|
|
2546
|
+
throw new Error("not a node-status push");
|
|
2547
|
+
}
|
|
2548
|
+
appendProxmoxSiblingReportsInPlace(push, {
|
|
2549
|
+
silentNodes: data.dead,
|
|
2550
|
+
reporterCount: data.live.length,
|
|
2551
|
+
timeUnixNanos: status.timeUnixNanos,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
return pushes;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function seriesOf(envelopes: Array<JSONObject>): Array<Series> {
|
|
2558
|
+
return envelopes.flatMap((envelope: JSONObject) => {
|
|
2559
|
+
return pveSeries(envelope);
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
const series: Array<Series> = seriesOf([
|
|
2564
|
+
...clusterNodePushes({ live: ["pve1", "pve2"], dead: ["pve3"] }),
|
|
2565
|
+
...[qemuPush(), lxcPush(), storagePush()].map((envelope: JSONObject) => {
|
|
2566
|
+
return normalized(envelope);
|
|
2567
|
+
}),
|
|
2568
|
+
]);
|
|
2569
|
+
|
|
2570
|
+
function templateById(id: string): ProxmoxAlertTemplate {
|
|
2571
|
+
const template: ProxmoxAlertTemplate | undefined =
|
|
2572
|
+
getAllProxmoxAlertTemplates().find((t: ProxmoxAlertTemplate) => {
|
|
2573
|
+
return t.id === id;
|
|
2574
|
+
});
|
|
2575
|
+
if (!template) {
|
|
2576
|
+
throw new Error(`no template ${id}`);
|
|
2577
|
+
}
|
|
2578
|
+
return template;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function queriesOf(template: ProxmoxAlertTemplate): Array<TemplateQuery> {
|
|
2582
|
+
const monitor: MonitorStepProxmoxMonitor = template.getMonitorStep({
|
|
2583
|
+
clusterIdentifier: "homelab",
|
|
2584
|
+
onlineMonitorStatusId: ObjectID.generate(),
|
|
2585
|
+
offlineMonitorStatusId: ObjectID.generate(),
|
|
2586
|
+
defaultIncidentSeverityId: ObjectID.generate(),
|
|
2587
|
+
defaultAlertSeverityId: ObjectID.generate(),
|
|
2588
|
+
monitorName: "m",
|
|
2589
|
+
}).data!.proxmoxMonitor!;
|
|
2590
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2591
|
+
return monitor.metricViewConfig.queryConfigs.map((q: any) => {
|
|
2592
|
+
return {
|
|
2593
|
+
metricName: q.metricQueryData.filterData.metricName as string,
|
|
2594
|
+
attributes: (q.metricQueryData.filterData.attributes || {}) as Record<
|
|
2595
|
+
string,
|
|
2596
|
+
string
|
|
2597
|
+
>,
|
|
2598
|
+
groupBy: (q.metricQueryData.groupByAttributeKeys ||
|
|
2599
|
+
[]) as Array<string>,
|
|
2600
|
+
aggregation: q.metricQueryData.filterData
|
|
2601
|
+
.aggegationType as MetricsAggregationType,
|
|
2602
|
+
};
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function matching(query: TemplateQuery, from: Array<Series>): Array<Series> {
|
|
2607
|
+
return from.filter((s: Series) => {
|
|
2608
|
+
return (
|
|
2609
|
+
s.name === query.metricName &&
|
|
2610
|
+
Object.entries(query.attributes).every(
|
|
2611
|
+
([key, value]: [string, string]) => {
|
|
2612
|
+
return s.labels[key] === value;
|
|
2613
|
+
},
|
|
2614
|
+
) &&
|
|
2615
|
+
query.groupBy.every((key: string) => {
|
|
2616
|
+
return s.labels[key] !== undefined;
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function served(template: ProxmoxAlertTemplate): boolean {
|
|
2623
|
+
return queriesOf(template).every((query: TemplateQuery) => {
|
|
2624
|
+
return matching(query, series).length > 0;
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
/*
|
|
2629
|
+
* One query over one window, as the monitor evaluates it: the matching
|
|
2630
|
+
* rows grouped by the group-by labels ("" when there are none), each
|
|
2631
|
+
* group aggregated.
|
|
2632
|
+
*/
|
|
2633
|
+
function evaluate(
|
|
2634
|
+
query: TemplateQuery,
|
|
2635
|
+
from: Array<Series>,
|
|
2636
|
+
): Record<string, number> {
|
|
2637
|
+
const groups: Map<string, Array<number>> = new Map();
|
|
2638
|
+
for (const s of matching(query, from)) {
|
|
2639
|
+
const key: string = query.groupBy
|
|
2640
|
+
.map((label: string) => {
|
|
2641
|
+
return s.labels[label];
|
|
2642
|
+
})
|
|
2643
|
+
.join("|");
|
|
2644
|
+
groups.set(key, [...(groups.get(key) || []), Number(s.value)]);
|
|
2645
|
+
}
|
|
2646
|
+
const out: Record<string, number> = {};
|
|
2647
|
+
for (const [key, values] of groups) {
|
|
2648
|
+
if (query.aggregation === MetricsAggregationType.Min) {
|
|
2649
|
+
out[key] = Math.min(...values);
|
|
2650
|
+
} else if (query.aggregation === MetricsAggregationType.Sum) {
|
|
2651
|
+
out[key] = sumOf(values);
|
|
2652
|
+
} else {
|
|
2653
|
+
throw new Error(`unexpected aggregation ${query.aggregation}`);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return out;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// Node Offline: pve_up Min per node id; fires below 1.
|
|
2660
|
+
function nodeOfflineMins(from: Array<Series>): Record<string, number> {
|
|
2661
|
+
const queries: Array<TemplateQuery> = queriesOf(
|
|
2662
|
+
templateById("pve-node-offline"),
|
|
2663
|
+
);
|
|
2664
|
+
expect(queries).toHaveLength(1);
|
|
2665
|
+
return evaluate(queries[0]!, from);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
function firingNodeIds(from: Array<Series>): Array<string> {
|
|
2669
|
+
const mins: Record<string, number> = nodeOfflineMins(from);
|
|
2670
|
+
return Object.keys(mins)
|
|
2671
|
+
.filter((id: string) => {
|
|
2672
|
+
return (mins[id] as number) < 1;
|
|
2673
|
+
})
|
|
2674
|
+
.sort();
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// Cluster Quorum at Risk: (Σ pve_up ÷ Σ pve_node_info) × 100; fires <= 50.
|
|
2678
|
+
function quorumAtRiskPercent(from: Array<Series>): number {
|
|
2679
|
+
const queries: Array<TemplateQuery> = queriesOf(
|
|
2680
|
+
templateById("pve-quorum-risk"),
|
|
2681
|
+
);
|
|
2682
|
+
expect(
|
|
2683
|
+
queries.map((query: TemplateQuery) => {
|
|
2684
|
+
return query.metricName;
|
|
2685
|
+
}),
|
|
2686
|
+
).toEqual(["pve_up", "pve_node_info"]);
|
|
2687
|
+
const online: number = evaluate(queries[0]!, from)[""] as number;
|
|
2688
|
+
const total: number = evaluate(queries[1]!, from)[""] as number;
|
|
2689
|
+
return (online / total) * 100;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
test("every template is classified", () => {
|
|
2693
|
+
expect(
|
|
2694
|
+
getAllProxmoxAlertTemplates()
|
|
2695
|
+
.map((t: ProxmoxAlertTemplate) => {
|
|
2696
|
+
return t.id;
|
|
2697
|
+
})
|
|
2698
|
+
.sort(),
|
|
2699
|
+
).toEqual([...SERVED, ...SERVED_BY_SIBLING_REPORTS, ...NOT_SERVED].sort());
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
test.each([...SERVED, ...SERVED_BY_SIBLING_REPORTS])(
|
|
2703
|
+
"%s finds every series it queries",
|
|
2704
|
+
(id: string) => {
|
|
2705
|
+
expect(served(templateById(id))).toBe(true);
|
|
2706
|
+
},
|
|
2707
|
+
);
|
|
2708
|
+
|
|
2709
|
+
test.each(SERVED_BY_SIBLING_REPORTS)(
|
|
2710
|
+
"%s finds pve_up = 0 for the dead node, under its own id and pve.scope = node",
|
|
2711
|
+
(id: string) => {
|
|
2712
|
+
const upQueries: Array<TemplateQuery> = queriesOf(
|
|
2713
|
+
templateById(id),
|
|
2714
|
+
).filter((query: TemplateQuery) => {
|
|
2715
|
+
return query.metricName === "pve_up";
|
|
2716
|
+
});
|
|
2717
|
+
expect(upQueries).toHaveLength(1);
|
|
2718
|
+
expect(upQueries[0]!.attributes).toEqual({ "pve.scope": "node" });
|
|
2719
|
+
|
|
2720
|
+
const dead: Array<Series> = matching(upQueries[0]!, series).filter(
|
|
2721
|
+
(s: Series) => {
|
|
2722
|
+
return s.labels["id"] === DEAD_NODE_ID;
|
|
2723
|
+
},
|
|
2724
|
+
);
|
|
2725
|
+
// One report on each live node's push.
|
|
2726
|
+
expect(dead).toHaveLength(2);
|
|
2727
|
+
for (const s of dead) {
|
|
2728
|
+
expect(s.value).toBe("0");
|
|
2729
|
+
expect(s.labels).toEqual({
|
|
2730
|
+
id: DEAD_NODE_ID,
|
|
2731
|
+
"pve.scope": "node",
|
|
2732
|
+
"pve.type": "node",
|
|
2733
|
+
"pve.id": "pve3",
|
|
2734
|
+
[PROXMOX_INFERRED_ATTRIBUTE]: PROXMOX_INFERRED_NOT_REPORTING,
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
},
|
|
2738
|
+
);
|
|
2739
|
+
|
|
2740
|
+
test("Node Offline fires for the dead node, and only for it", () => {
|
|
2741
|
+
expect(nodeOfflineMins(series)).toEqual({
|
|
2742
|
+
"node/pve1": 1,
|
|
2743
|
+
"node/pve2": 1,
|
|
2744
|
+
[DEAD_NODE_ID]: 0,
|
|
2745
|
+
});
|
|
2746
|
+
expect(firingNodeIds(series)).toEqual([DEAD_NODE_ID]);
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
test("Quorum at Risk counts the dead node: 2 of 3 online is not at risk", () => {
|
|
2750
|
+
const percent: number = quorumAtRiskPercent(series);
|
|
2751
|
+
expect(percent).toBeCloseTo((2 / 3) * 100, 10);
|
|
2752
|
+
expect(percent).toBeGreaterThan(50);
|
|
2753
|
+
});
|
|
2754
|
+
|
|
2755
|
+
const QUORUM_CASES: Array<QuorumCase> = [
|
|
2756
|
+
{
|
|
2757
|
+
title: "3 live, none dead",
|
|
2758
|
+
live: ["pve1", "pve2", "pve3"],
|
|
2759
|
+
dead: [],
|
|
2760
|
+
percent: 100,
|
|
2761
|
+
},
|
|
2762
|
+
{
|
|
2763
|
+
title: "3 live, 1 dead",
|
|
2764
|
+
live: ["pve1", "pve2", "pve3"],
|
|
2765
|
+
dead: ["pve4"],
|
|
2766
|
+
percent: 75,
|
|
2767
|
+
},
|
|
2768
|
+
{
|
|
2769
|
+
title: "3 live, 2 dead",
|
|
2770
|
+
live: ["pve1", "pve2", "pve3"],
|
|
2771
|
+
dead: ["pve4", "pve5"],
|
|
2772
|
+
percent: 60,
|
|
2773
|
+
},
|
|
2774
|
+
{
|
|
2775
|
+
title: "2 live, 2 dead",
|
|
2776
|
+
live: ["pve1", "pve2"],
|
|
2777
|
+
dead: ["pve3", "pve4"],
|
|
2778
|
+
percent: 50,
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
title: "3 live, 3 dead",
|
|
2782
|
+
live: ["pve1", "pve2", "pve3"],
|
|
2783
|
+
dead: ["pve4", "pve5", "pve6"],
|
|
2784
|
+
percent: 50,
|
|
2785
|
+
},
|
|
2786
|
+
{
|
|
2787
|
+
title: "1 live, 2 dead",
|
|
2788
|
+
live: ["pve1"],
|
|
2789
|
+
dead: ["pve2", "pve3"],
|
|
2790
|
+
percent: 100 / 3,
|
|
2791
|
+
},
|
|
2792
|
+
];
|
|
2793
|
+
|
|
2794
|
+
test.each(QUORUM_CASES)(
|
|
2795
|
+
"$title: Quorum at Risk reads live ÷ all nodes, Node Offline fires for each dead node",
|
|
2796
|
+
(quorumCase: QuorumCase) => {
|
|
2797
|
+
const from: Array<Series> = seriesOf(
|
|
2798
|
+
clusterNodePushes({ live: quorumCase.live, dead: quorumCase.dead }),
|
|
2799
|
+
);
|
|
2800
|
+
const percent: number = quorumAtRiskPercent(from);
|
|
2801
|
+
if (quorumCase.percent === 50) {
|
|
2802
|
+
// The boundary is exact: half the cluster down always fires.
|
|
2803
|
+
expect(percent).toBe(50);
|
|
2804
|
+
} else {
|
|
2805
|
+
expect(percent).toBeCloseTo(quorumCase.percent, 2);
|
|
2806
|
+
expect(percent <= 50).toBe(quorumCase.percent <= 50);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
expect(firingNodeIds(from)).toEqual(
|
|
2810
|
+
quorumCase.dead
|
|
2811
|
+
.map((node: string) => {
|
|
2812
|
+
return `node/${node}`;
|
|
2813
|
+
})
|
|
2814
|
+
.sort(),
|
|
2815
|
+
);
|
|
2816
|
+
},
|
|
2817
|
+
);
|
|
2818
|
+
|
|
2819
|
+
test("without the sibling reports a dead node is invisible to both", () => {
|
|
2820
|
+
const quiet: Array<Series> = seriesOf(
|
|
2821
|
+
clusterNodePushes({ live: ["pve1", "pve2"], dead: [] }),
|
|
2822
|
+
);
|
|
2823
|
+
expect(nodeOfflineMins(quiet)).toEqual({ "node/pve1": 1, "node/pve2": 1 });
|
|
2824
|
+
expect(quorumAtRiskPercent(quiet)).toBe(100);
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
test("only what is said about the dead node is marked inferred", () => {
|
|
2828
|
+
for (const s of series) {
|
|
2829
|
+
expect(s.labels[PROXMOX_INFERRED_ATTRIBUTE] !== undefined).toBe(
|
|
2830
|
+
s.labels["id"] === DEAD_NODE_ID,
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
test.each(NOT_SERVED)(
|
|
2836
|
+
"%s needs data the native push does not send",
|
|
2837
|
+
(id: string) => {
|
|
2838
|
+
expect(served(templateById(id))).toBe(false);
|
|
2839
|
+
},
|
|
2840
|
+
);
|
|
2841
|
+
|
|
2842
|
+
test("every translated series is a catalog metric", () => {
|
|
2843
|
+
const names: Set<string> = new Set(
|
|
2844
|
+
series.map((s: Series) => {
|
|
2845
|
+
return s.name;
|
|
2846
|
+
}),
|
|
2847
|
+
);
|
|
2848
|
+
for (const name of names) {
|
|
2849
|
+
expect(getProxmoxMetricByMetricName(name)).toBeDefined();
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
});
|