@oneuptime/common 12.0.1 → 12.0.3
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/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +27 -45
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +24 -42
- package/Server/Services/DatabaseService.ts +198 -6
- package/Server/Services/DockerHostService.ts +20 -38
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +28 -46
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +47 -57
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +20 -38
- package/Server/Services/KubernetesClusterService.ts +18 -36
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +153 -1
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +146 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +20 -38
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +24 -42
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +20 -38
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +25 -43
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +658 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +636 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Date.ts +112 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Events/Recurring.ts +99 -1
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.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/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -39
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -36
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +155 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -32
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -40
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -64
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -32
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -30
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +127 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +128 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -32
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -36
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -32
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -37
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
|
|
2
|
+
import ServiceService from "../../../Server/Services/ServiceService";
|
|
3
|
+
import SingleFlight from "../../../Server/Utils/SingleFlight";
|
|
4
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
5
|
+
import {
|
|
6
|
+
afterEach,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
jest,
|
|
11
|
+
test,
|
|
12
|
+
} from "@jest/globals";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Regression suite for the row-lock convoy outage.
|
|
16
|
+
*
|
|
17
|
+
* `Service.lastSeenAt` is the hottest UPDATE in the product: every
|
|
18
|
+
* metric/log/trace/profile batch re-resolves the same serviceId, and every
|
|
19
|
+
* OTLP resource inside a batch does it again. It was throttled — and the
|
|
20
|
+
* throttle did not work.
|
|
21
|
+
*
|
|
22
|
+
* The throttle keyed on the serviceId but STORED a fingerprint of the
|
|
23
|
+
* collector-supplied metadata, and skipped only when the stored fingerprint
|
|
24
|
+
* matched. That is fine while one producer owns a row. It collapses the moment
|
|
25
|
+
* two producers of the SAME row disagree, and `Service` is unique on
|
|
26
|
+
* (projectId, name) alone — so the same `service.name` running in dev and prod
|
|
27
|
+
* inside one project is ONE row with two permanently disagreeing fingerprints.
|
|
28
|
+
* A writes fp_A; B reads fp_A, sees a mismatch, writes fp_B; A reads fp_B,
|
|
29
|
+
* sees a mismatch, writes fp_A. Forever. For those rows the throttle admitted
|
|
30
|
+
* not one write per minute but one write per BATCH.
|
|
31
|
+
*
|
|
32
|
+
* At 100 worker pods and ~25K concurrent jobs that was thousands of
|
|
33
|
+
* simultaneous UPDATEs onto ~2,300 rows. Postgres queues row-lock waiters
|
|
34
|
+
* strictly, so they did not collide and retry — they lined up: 1,017 active
|
|
35
|
+
* connections, 892 parked on locks, the tail waiting 3.7 hours. The database
|
|
36
|
+
* had capacity the entire time.
|
|
37
|
+
*
|
|
38
|
+
* What is pinned here, in order of how badly it hurt:
|
|
39
|
+
*
|
|
40
|
+
* 1. LIVENESS IS GATED ON PRESENCE, NEVER ON METADATA. No payload, however
|
|
41
|
+
* it flaps, can make a second heartbeat happen inside one window. This is
|
|
42
|
+
* the assertion that would have caught the outage.
|
|
43
|
+
* 2. ENRICHMENT IS RATE-LIMITED SEPARATELY. A real change still lands
|
|
44
|
+
* promptly, but a permanently-flapping service cannot buy more than one
|
|
45
|
+
* enrichment write per window with it.
|
|
46
|
+
* 3. THE CLAIMS ARE ATOMIC. Check-then-act admits every concurrent caller;
|
|
47
|
+
* SET NX admits exactly one.
|
|
48
|
+
* 4. THE WRITE IS NON-BLOCKING. SKIP LOCKED, so contention can never queue,
|
|
49
|
+
* including when everything above fails open.
|
|
50
|
+
* 5. LIVENESS SURVIVES EVERYTHING ELSE FAILING — a Redis outage, a bad
|
|
51
|
+
* metadata column, a lost race. A suppressed heartbeat strands a healthy
|
|
52
|
+
* service as "disconnected" while its telemetry is still arriving, which
|
|
53
|
+
* is the failure this must never trade away for efficiency.
|
|
54
|
+
*
|
|
55
|
+
* Everything external is mocked — no Postgres, no Redis.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const SERVICE_ID: ObjectID = ObjectID.generate();
|
|
59
|
+
|
|
60
|
+
const LIVENESS_NAMESPACE: string = "service-last-seen";
|
|
61
|
+
const FINGERPRINT_NAMESPACE: string = "service-metadata-fingerprint";
|
|
62
|
+
const WRITE_WINDOW_NAMESPACE: string = "service-metadata-write-window";
|
|
63
|
+
|
|
64
|
+
type WriteCall = { id: ObjectID; data: Record<string, unknown> };
|
|
65
|
+
|
|
66
|
+
let writes: Array<WriteCall>;
|
|
67
|
+
let cache: Map<string, string>;
|
|
68
|
+
let deletedKeys: Array<string>;
|
|
69
|
+
let ttls: Map<string, number>;
|
|
70
|
+
/** Set to false to simulate every row being locked by another writer. */
|
|
71
|
+
let writeLands: boolean;
|
|
72
|
+
|
|
73
|
+
function cacheKey(namespace: string, key: string): string {
|
|
74
|
+
return `${namespace}:${key}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A faithful in-memory Redis: SET NX only succeeds on an absent key, and the
|
|
79
|
+
* compare-and-claim only succeeds when the stored value differs. Getting
|
|
80
|
+
* either of these wrong here would make the suite agree with a broken
|
|
81
|
+
* implementation, so they are modelled rather than stubbed.
|
|
82
|
+
*/
|
|
83
|
+
function mockCache(): void {
|
|
84
|
+
jest
|
|
85
|
+
.spyOn(GlobalCache, "setStringIfNotExists")
|
|
86
|
+
.mockImplementation(
|
|
87
|
+
async (
|
|
88
|
+
namespace: string,
|
|
89
|
+
key: string,
|
|
90
|
+
value: string,
|
|
91
|
+
options?: { expiresInSeconds: number },
|
|
92
|
+
) => {
|
|
93
|
+
const full: string = cacheKey(namespace, key);
|
|
94
|
+
if (cache.has(full)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
cache.set(full, value);
|
|
98
|
+
ttls.set(full, options?.expiresInSeconds ?? 0);
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
jest
|
|
104
|
+
.spyOn(GlobalCache, "setStringIfChanged")
|
|
105
|
+
.mockImplementation(
|
|
106
|
+
async (
|
|
107
|
+
namespace: string,
|
|
108
|
+
key: string,
|
|
109
|
+
value: string,
|
|
110
|
+
options?: { expiresInSeconds: number },
|
|
111
|
+
) => {
|
|
112
|
+
const full: string = cacheKey(namespace, key);
|
|
113
|
+
if (cache.get(full) === value) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
cache.set(full, value);
|
|
117
|
+
ttls.set(full, options?.expiresInSeconds ?? 0);
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
jest
|
|
123
|
+
.spyOn(GlobalCache, "deleteKey")
|
|
124
|
+
.mockImplementation(async (namespace: string, key: string) => {
|
|
125
|
+
const full: string = cacheKey(namespace, key);
|
|
126
|
+
deletedKeys.push(full);
|
|
127
|
+
cache.delete(full);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mockWritesSucceeding(): void {
|
|
132
|
+
jest
|
|
133
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
134
|
+
.mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
|
|
135
|
+
writes.push({
|
|
136
|
+
id: input.id,
|
|
137
|
+
data: { ...(input.data as Record<string, unknown>) },
|
|
138
|
+
});
|
|
139
|
+
return writeLands;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function lastWrite(): Record<string, unknown> {
|
|
144
|
+
return writes[writes.length - 1]!.data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Columns written, sorted — `lastSeenAt` alone means liveness-only. */
|
|
148
|
+
function columnsOf(write: WriteCall): Array<string> {
|
|
149
|
+
return Object.keys(write.data).sort();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isLivenessOnly(write: WriteCall): boolean {
|
|
153
|
+
return columnsOf(write).join(",") === "lastSeenAt";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
writes = [];
|
|
158
|
+
cache = new Map<string, string>();
|
|
159
|
+
deletedKeys = [];
|
|
160
|
+
ttls = new Map<string, number>();
|
|
161
|
+
writeLands = true;
|
|
162
|
+
SingleFlight.clear();
|
|
163
|
+
mockCache();
|
|
164
|
+
mockWritesSucceeding();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
jest.restoreAllMocks();
|
|
169
|
+
SingleFlight.clear();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("ServiceService.updateLastSeen — liveness is gated on presence", () => {
|
|
173
|
+
test("writes lastSeenAt on the first call", async () => {
|
|
174
|
+
await ServiceService.updateLastSeen(SERVICE_ID);
|
|
175
|
+
|
|
176
|
+
expect(writes).toHaveLength(1);
|
|
177
|
+
expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
|
|
178
|
+
expect(writes[0]!.id.toString()).toBe(SERVICE_ID.toString());
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("skips the second identical call inside the window", async () => {
|
|
182
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
183
|
+
serviceVersion: "1.0.0",
|
|
184
|
+
});
|
|
185
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
186
|
+
serviceVersion: "1.0.0",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(writes).toHaveLength(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* THE OUTAGE, reproduced. Two producers of one Service row that disagree on
|
|
194
|
+
* a fingerprinted attribute — dev and prod under a single service.name, or
|
|
195
|
+
* an active-active pair differing only in cloud.region. Under the old
|
|
196
|
+
* throttle each call busted the other's key and every one of these 20 calls
|
|
197
|
+
* issued its own UPDATE.
|
|
198
|
+
*
|
|
199
|
+
* The liveness heartbeat must now happen exactly ONCE regardless.
|
|
200
|
+
*/
|
|
201
|
+
test("flapping producers cannot force more than one heartbeat per window", async () => {
|
|
202
|
+
for (let i: number = 0; i < 10; i++) {
|
|
203
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
204
|
+
deploymentEnvironment: "production",
|
|
205
|
+
});
|
|
206
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
207
|
+
deploymentEnvironment: "development",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const heartbeats: Array<WriteCall> = writes.filter((write: WriteCall) => {
|
|
212
|
+
return write.data["lastSeenAt"] !== undefined;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* Every write refreshes lastSeenAt, so the bound that matters is the total
|
|
217
|
+
* number of statements: one liveness claim plus at most one enrichment
|
|
218
|
+
* claim per window. Twenty flapping batches used to mean twenty UPDATEs.
|
|
219
|
+
*/
|
|
220
|
+
expect(heartbeats.length).toBeLessThanOrEqual(2);
|
|
221
|
+
expect(writes.length).toBeLessThanOrEqual(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("flapping across many distinct payloads is bounded just the same", async () => {
|
|
225
|
+
for (let i: number = 0; i < 50; i++) {
|
|
226
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
227
|
+
serviceVersion: `1.0.${i}`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(writes.length).toBeLessThanOrEqual(2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a different service is throttled independently", async () => {
|
|
235
|
+
const other: ObjectID = ObjectID.generate();
|
|
236
|
+
|
|
237
|
+
await ServiceService.updateLastSeen(SERVICE_ID);
|
|
238
|
+
await ServiceService.updateLastSeen(other);
|
|
239
|
+
|
|
240
|
+
expect(writes).toHaveLength(2);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("claims the liveness gate atomically, never read-then-write", async () => {
|
|
244
|
+
jest.spyOn(GlobalCache, "getString");
|
|
245
|
+
jest.spyOn(GlobalCache, "setString");
|
|
246
|
+
|
|
247
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
248
|
+
serviceVersion: "1.0.0",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(GlobalCache.setStringIfNotExists).toHaveBeenCalledWith(
|
|
252
|
+
LIVENESS_NAMESPACE,
|
|
253
|
+
SERVICE_ID.toString(),
|
|
254
|
+
"1",
|
|
255
|
+
expect.anything(),
|
|
256
|
+
);
|
|
257
|
+
// A read here would be the check-then-act race coming back.
|
|
258
|
+
expect(GlobalCache.getString).not.toHaveBeenCalled();
|
|
259
|
+
expect(GlobalCache.setString).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* The liveness key must hold a constant. Storing anything attribute-derived
|
|
264
|
+
* is what let the payload bust the gate in the first place — the exact
|
|
265
|
+
* regression this whole file exists to prevent.
|
|
266
|
+
*/
|
|
267
|
+
test("stores a constant in the liveness key, never anything payload-derived", async () => {
|
|
268
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
269
|
+
serviceVersion: "1.2.3",
|
|
270
|
+
cloudRegion: "us-east-1",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(cache.get(cacheKey(LIVENESS_NAMESPACE, SERVICE_ID.toString()))).toBe(
|
|
274
|
+
"1",
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("jitters the liveness TTL so windows cannot re-synchronise fleet-wide", async () => {
|
|
279
|
+
await ServiceService.updateLastSeen(SERVICE_ID);
|
|
280
|
+
|
|
281
|
+
const ttl: number = ttls.get(
|
|
282
|
+
cacheKey(LIVENESS_NAMESPACE, SERVICE_ID.toString()),
|
|
283
|
+
)!;
|
|
284
|
+
|
|
285
|
+
expect(ttl).toBeGreaterThanOrEqual(60);
|
|
286
|
+
expect(ttl).toBeLessThanOrEqual(75);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("ServiceService.updateLastSeen — enrichment is gated separately", () => {
|
|
291
|
+
test("writes the descriptive columns on first contact", async () => {
|
|
292
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
293
|
+
serviceVersion: "1.2.3",
|
|
294
|
+
deploymentEnvironment: "production",
|
|
295
|
+
cloudRegion: "us-east-1",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(lastWrite()["serviceVersion"]).toBe("1.2.3");
|
|
299
|
+
expect(lastWrite()["deploymentEnvironment"]).toBe("production");
|
|
300
|
+
expect(lastWrite()["cloudRegion"]).toBe("us-east-1");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* The fingerprint gate must OPEN for a changed attribute. This is why it is
|
|
305
|
+
* compare-and-claim rather than plain set-if-absent: plain NX fails whenever
|
|
306
|
+
* the key exists, including when it holds a stale fingerprint, so a new
|
|
307
|
+
* service.version would be suppressed and then never written at all.
|
|
308
|
+
*/
|
|
309
|
+
test("the fingerprint gate opens for a genuinely changed attribute", async () => {
|
|
310
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
311
|
+
serviceVersion: "1.0.0",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Only the rate limiter is holding it back, not change detection.
|
|
315
|
+
cache.delete(cacheKey(WRITE_WINDOW_NAMESPACE, SERVICE_ID.toString()));
|
|
316
|
+
SingleFlight.clear();
|
|
317
|
+
|
|
318
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
319
|
+
serviceVersion: "2.0.0",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(writes).toHaveLength(2);
|
|
323
|
+
expect(lastWrite()["serviceVersion"]).toBe("2.0.0");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/*
|
|
327
|
+
* The deliberate trade, pinned so nobody "fixes" it by accident.
|
|
328
|
+
*
|
|
329
|
+
* Change detection alone cannot distinguish a real deploy from two
|
|
330
|
+
* producers that disagree forever — both present as "it changed" on every
|
|
331
|
+
* batch. Since the flapping case is unbounded and the deploy case is not,
|
|
332
|
+
* the rate limiter applies to both, and the cost is that a new
|
|
333
|
+
* service.version can lag by up to one enrichment window.
|
|
334
|
+
*
|
|
335
|
+
* That is a descriptive column, not an operational one: nothing is
|
|
336
|
+
* mis-reported by showing the previous version for a few minutes, and
|
|
337
|
+
* `lastSeenAt` — the value anything actually alerts on — is unaffected
|
|
338
|
+
* because it rides the separate liveness gate.
|
|
339
|
+
*/
|
|
340
|
+
test("a changed attribute waits for the enrichment rate-limit window", async () => {
|
|
341
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
342
|
+
serviceVersion: "1.0.0",
|
|
343
|
+
});
|
|
344
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
345
|
+
serviceVersion: "2.0.0",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(writes).toHaveLength(1);
|
|
349
|
+
expect(lastWrite()["serviceVersion"]).toBe("1.0.0");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("an unchanged payload claims nothing on the second call", async () => {
|
|
353
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
354
|
+
serviceVersion: "1.0.0",
|
|
355
|
+
});
|
|
356
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
357
|
+
serviceVersion: "1.0.0",
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(writes).toHaveLength(1);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* The fingerprint gate alone cannot tell a real deploy from a permanently
|
|
365
|
+
* flapping pair of producers — both look like "it changed". The rate limiter
|
|
366
|
+
* is what bounds the second case, and without it a longer fingerprint TTL
|
|
367
|
+
* just makes every flap more expensive.
|
|
368
|
+
*/
|
|
369
|
+
test("the rate limiter bounds enrichment for a permanently flapping service", async () => {
|
|
370
|
+
for (let i: number = 0; i < 25; i++) {
|
|
371
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
372
|
+
cloudRegion: "us-east-1",
|
|
373
|
+
});
|
|
374
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
375
|
+
cloudRegion: "eu-west-1",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const enrichmentWrites: Array<WriteCall> = writes.filter(
|
|
380
|
+
(write: WriteCall) => {
|
|
381
|
+
return !isLivenessOnly(write);
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expect(enrichmentWrites).toHaveLength(1);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("holds the enrichment window key for longer than the liveness window", async () => {
|
|
389
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
390
|
+
serviceVersion: "1.0.0",
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const livenessTtl: number = ttls.get(
|
|
394
|
+
cacheKey(LIVENESS_NAMESPACE, SERVICE_ID.toString()),
|
|
395
|
+
)!;
|
|
396
|
+
const windowTtl: number = ttls.get(
|
|
397
|
+
cacheKey(WRITE_WINDOW_NAMESPACE, SERVICE_ID.toString()),
|
|
398
|
+
)!;
|
|
399
|
+
|
|
400
|
+
expect(windowTtl).toBeGreaterThan(livenessTtl);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("only lastSeenAt is written once the enrichment window is claimed", async () => {
|
|
404
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
405
|
+
serviceVersion: "1.0.0",
|
|
406
|
+
});
|
|
407
|
+
cache.delete(cacheKey(LIVENESS_NAMESPACE, SERVICE_ID.toString()));
|
|
408
|
+
|
|
409
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
410
|
+
serviceVersion: "9.9.9",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(writes).toHaveLength(2);
|
|
414
|
+
expect(isLivenessOnly(writes[1]!)).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
/*
|
|
418
|
+
* Enrichment is descriptive; liveness is operational. Opening the
|
|
419
|
+
* enrichment gate during a Redis outage would restore the per-batch write
|
|
420
|
+
* storm, and nothing is mis-reported by writing a cloud region a minute
|
|
421
|
+
* later. Liveness gets the opposite treatment — see below.
|
|
422
|
+
*/
|
|
423
|
+
test("fails CLOSED for enrichment when the cache is unavailable", async () => {
|
|
424
|
+
jest
|
|
425
|
+
.spyOn(GlobalCache, "setStringIfChanged")
|
|
426
|
+
.mockRejectedValue(new Error("redis down"));
|
|
427
|
+
|
|
428
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
429
|
+
serviceVersion: "1.2.3",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(writes).toHaveLength(1);
|
|
433
|
+
expect(isLivenessOnly(writes[0]!)).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("ServiceService.updateLastSeen — liveness survives everything else", () => {
|
|
438
|
+
/*
|
|
439
|
+
* A cache outage must never suppress the heartbeat. The disconnection sweep
|
|
440
|
+
* runs 15 minutes after lastSeenAt stops advancing, so a fail-closed gate
|
|
441
|
+
* would show healthy services as down while their telemetry kept arriving.
|
|
442
|
+
* Affordable only because the write itself cannot queue.
|
|
443
|
+
*/
|
|
444
|
+
test("fails OPEN for liveness when the cache is unavailable", async () => {
|
|
445
|
+
jest
|
|
446
|
+
.spyOn(GlobalCache, "setStringIfNotExists")
|
|
447
|
+
.mockRejectedValue(new Error("redis down"));
|
|
448
|
+
|
|
449
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
450
|
+
serviceVersion: "1.0.0",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(writes).toHaveLength(1);
|
|
454
|
+
expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("keeps writing liveness on every call while the cache stays down", async () => {
|
|
458
|
+
jest
|
|
459
|
+
.spyOn(GlobalCache, "setStringIfNotExists")
|
|
460
|
+
.mockRejectedValue(new Error("redis down"));
|
|
461
|
+
|
|
462
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
463
|
+
serviceVersion: "1.0.0",
|
|
464
|
+
});
|
|
465
|
+
SingleFlight.clear();
|
|
466
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
467
|
+
serviceVersion: "1.0.0",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(writes).toHaveLength(2);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
/*
|
|
474
|
+
* Issue #3006's shape: one bad collector-supplied column must not take the
|
|
475
|
+
* heartbeat down with it.
|
|
476
|
+
*/
|
|
477
|
+
test("retries with liveness only when the enriched write fails", async () => {
|
|
478
|
+
let attempt: number = 0;
|
|
479
|
+
jest
|
|
480
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
481
|
+
.mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
|
|
482
|
+
attempt++;
|
|
483
|
+
const data: Record<string, unknown> = {
|
|
484
|
+
...(input.data as Record<string, unknown>),
|
|
485
|
+
};
|
|
486
|
+
writes.push({ id: input.id, data: data });
|
|
487
|
+
if (attempt === 1) {
|
|
488
|
+
throw new Error("value too long for type character varying(100)");
|
|
489
|
+
}
|
|
490
|
+
return true;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
494
|
+
cloudRegion: "x".repeat(5000),
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(writes).toHaveLength(2);
|
|
498
|
+
expect(columnsOf(writes[1]!)).toEqual(["lastSeenAt"]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("does not throw when the enriched write fails — ingest keeps going", async () => {
|
|
502
|
+
let attempt: number = 0;
|
|
503
|
+
jest
|
|
504
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
505
|
+
.mockImplementation(async () => {
|
|
506
|
+
attempt++;
|
|
507
|
+
if (attempt === 1) {
|
|
508
|
+
throw new Error("value too long");
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await expect(
|
|
514
|
+
ServiceService.updateLastSeen(SERVICE_ID, { cloudRegion: "x" }),
|
|
515
|
+
).resolves.toBeUndefined();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("re-opens the enrichment gates after a failed write so the next batch retries", async () => {
|
|
519
|
+
jest
|
|
520
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
521
|
+
.mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
|
|
522
|
+
const data: Record<string, unknown> = {
|
|
523
|
+
...(input.data as Record<string, unknown>),
|
|
524
|
+
};
|
|
525
|
+
writes.push({ id: input.id, data: data });
|
|
526
|
+
if (Object.keys(data).length > 1) {
|
|
527
|
+
throw new Error("value too long");
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
await ServiceService.updateLastSeen(SERVICE_ID, { cloudRegion: "x" });
|
|
533
|
+
|
|
534
|
+
expect(deletedKeys).toContain(
|
|
535
|
+
cacheKey(FINGERPRINT_NAMESPACE, SERVICE_ID.toString()),
|
|
536
|
+
);
|
|
537
|
+
expect(deletedKeys).toContain(
|
|
538
|
+
cacheKey(WRITE_WINDOW_NAMESPACE, SERVICE_ID.toString()),
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("surfaces a genuinely broken database rather than hiding it", async () => {
|
|
543
|
+
jest
|
|
544
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
545
|
+
.mockRejectedValue(new Error("connection terminated"));
|
|
546
|
+
|
|
547
|
+
await expect(
|
|
548
|
+
ServiceService.updateLastSeen(SERVICE_ID, { serviceVersion: "1.0.0" }),
|
|
549
|
+
).rejects.toThrow("connection terminated");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("a failing cache delete does not stop the liveness retry", async () => {
|
|
553
|
+
jest
|
|
554
|
+
.spyOn(GlobalCache, "deleteKey")
|
|
555
|
+
.mockRejectedValue(new Error("redis down"));
|
|
556
|
+
|
|
557
|
+
let attempt: number = 0;
|
|
558
|
+
jest
|
|
559
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
560
|
+
.mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
|
|
561
|
+
attempt++;
|
|
562
|
+
writes.push({
|
|
563
|
+
id: input.id,
|
|
564
|
+
data: { ...(input.data as Record<string, unknown>) },
|
|
565
|
+
});
|
|
566
|
+
if (attempt === 1) {
|
|
567
|
+
throw new Error("value too long");
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await ServiceService.updateLastSeen(SERVICE_ID, { cloudRegion: "x" });
|
|
573
|
+
|
|
574
|
+
expect(columnsOf(writes[1]!)).toEqual(["lastSeenAt"]);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe("ServiceService.updateLastSeen — contention is yielded, not queued", () => {
|
|
579
|
+
test("uses the non-blocking write primitive", async () => {
|
|
580
|
+
await ServiceService.updateLastSeen(SERVICE_ID);
|
|
581
|
+
|
|
582
|
+
expect(
|
|
583
|
+
ServiceService.updateColumnsByIdIfUnlockedWithoutHooks,
|
|
584
|
+
).toHaveBeenCalled();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
/*
|
|
588
|
+
* A skipped liveness bump is a non-event: whoever holds the row lock is
|
|
589
|
+
* writing the same timestamp. Re-opening the window for it would generate
|
|
590
|
+
* pointless retries against a row that is demonstrably being written.
|
|
591
|
+
*/
|
|
592
|
+
test("a skipped liveness-only write is dropped silently", async () => {
|
|
593
|
+
writeLands = false;
|
|
594
|
+
|
|
595
|
+
await ServiceService.updateLastSeen(SERVICE_ID);
|
|
596
|
+
|
|
597
|
+
expect(deletedKeys).toHaveLength(0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
/*
|
|
601
|
+
* Enrichment is different: the writer holding the lock may be storing an
|
|
602
|
+
* OLDER payload, and the gates this call just claimed would suppress the
|
|
603
|
+
* newer one for the rest of the window. It has to be retried.
|
|
604
|
+
*/
|
|
605
|
+
test("a skipped enrichment write re-opens the gates for the next batch", async () => {
|
|
606
|
+
writeLands = false;
|
|
607
|
+
|
|
608
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
609
|
+
serviceVersion: "1.2.3",
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(deletedKeys).toContain(
|
|
613
|
+
cacheKey(FINGERPRINT_NAMESPACE, SERVICE_ID.toString()),
|
|
614
|
+
);
|
|
615
|
+
expect(deletedKeys).toContain(
|
|
616
|
+
cacheKey(WRITE_WINDOW_NAMESPACE, SERVICE_ID.toString()),
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("the re-opened gates actually let the next batch through", async () => {
|
|
621
|
+
writeLands = false;
|
|
622
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
623
|
+
serviceVersion: "1.2.3",
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
writeLands = true;
|
|
627
|
+
SingleFlight.clear();
|
|
628
|
+
await ServiceService.updateLastSeen(SERVICE_ID, {
|
|
629
|
+
serviceVersion: "1.2.3",
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(writes).toHaveLength(2);
|
|
633
|
+
expect(lastWrite()["serviceVersion"]).toBe("1.2.3");
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("ServiceService.updateLastSeen — duplicate in-process callers collapse", () => {
|
|
638
|
+
/*
|
|
639
|
+
* One OTLP batch resolves the same service once per resource, and the
|
|
640
|
+
* collector config OneUptime itself ships emits hundreds of resources per
|
|
641
|
+
* batch. Without in-process coalescing each one independently asks Redis the
|
|
642
|
+
* same question — correct, but it multiplies the round trips by the resource
|
|
643
|
+
* count for no information gained.
|
|
644
|
+
*/
|
|
645
|
+
test("concurrent identical calls issue a single write", async () => {
|
|
646
|
+
await Promise.all(
|
|
647
|
+
Array.from({ length: 200 }, () => {
|
|
648
|
+
return ServiceService.updateLastSeen(SERVICE_ID, {
|
|
649
|
+
serviceVersion: "1.0.0",
|
|
650
|
+
});
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
expect(writes).toHaveLength(1);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("concurrent identical calls make a single fence claim", async () => {
|
|
658
|
+
await Promise.all(
|
|
659
|
+
Array.from({ length: 200 }, () => {
|
|
660
|
+
return ServiceService.updateLastSeen(SERVICE_ID, {
|
|
661
|
+
serviceVersion: "1.0.0",
|
|
662
|
+
});
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
expect(GlobalCache.setStringIfNotExists).toHaveBeenCalledTimes(2);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
/*
|
|
670
|
+
* Coalescing must not merge callers carrying DIFFERENT metadata — that
|
|
671
|
+
* would silently drop one producer's payload rather than throttling it.
|
|
672
|
+
*/
|
|
673
|
+
test("concurrent calls carrying different metadata are not merged", async () => {
|
|
674
|
+
await Promise.all([
|
|
675
|
+
ServiceService.updateLastSeen(SERVICE_ID, { serviceVersion: "1.0.0" }),
|
|
676
|
+
ServiceService.updateLastSeen(SERVICE_ID, { serviceVersion: "2.0.0" }),
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
// Both were considered; the gates, not the coalescer, decide what lands.
|
|
680
|
+
expect(GlobalCache.setStringIfChanged).toHaveBeenCalledTimes(2);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("concurrent calls for different services are not merged", async () => {
|
|
684
|
+
const other: ObjectID = ObjectID.generate();
|
|
685
|
+
|
|
686
|
+
await Promise.all([
|
|
687
|
+
ServiceService.updateLastSeen(SERVICE_ID),
|
|
688
|
+
ServiceService.updateLastSeen(other),
|
|
689
|
+
]);
|
|
690
|
+
|
|
691
|
+
expect(writes).toHaveLength(2);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("a rejected shared call does not strand later callers", async () => {
|
|
695
|
+
jest
|
|
696
|
+
.spyOn(ServiceService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
697
|
+
.mockRejectedValue(new Error("connection terminated"));
|
|
698
|
+
|
|
699
|
+
await expect(
|
|
700
|
+
ServiceService.updateLastSeen(SERVICE_ID, { serviceVersion: "1.0.0" }),
|
|
701
|
+
).rejects.toThrow("connection terminated");
|
|
702
|
+
|
|
703
|
+
expect(SingleFlight.inflightCount()).toBe(0);
|
|
704
|
+
});
|
|
705
|
+
});
|