@oneuptime/common 12.0.34 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The cache and BullMQ queue backend runs Valkey, the BSD-licensed fork of
|
|
8
|
+
* Redis 7.2 that most of the original Redis contributors moved to after Redis
|
|
9
|
+
* 7.4 left the BSD licence. The engine, the compose service, the settings and
|
|
10
|
+
* the Kubernetes objects are all named for it.
|
|
11
|
+
*
|
|
12
|
+
* What these tests guard is the half of that rename nobody sees until an
|
|
13
|
+
* upgrade goes wrong: every OLD name still has to resolve. A self-hoster who
|
|
14
|
+
* never edits config.env, and an operator whose values.yaml still says `redis:`,
|
|
15
|
+
* must both keep running. So the compose file reads `${VALKEY_X:-${REDIS_X}}`,
|
|
16
|
+
* keeps `redis` as a network alias, and emits the deprecated REDIS_* mirrors for
|
|
17
|
+
* an app image pinned to a pre-rename release.
|
|
18
|
+
*
|
|
19
|
+
* The app-side half of the fallback is covered by
|
|
20
|
+
* Common/Tests/Server/EnvironmentConfigValkey.test.ts, the Helm-side half by
|
|
21
|
+
* HelmChart/Public/oneuptime/tests/valkey-legacy-values_test.yaml, and the
|
|
22
|
+
* config.env upgrade path by Tests/Ops/MergeEnvTemplateRenames.test.js.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT: string = path.resolve(__dirname, "../../../..");
|
|
26
|
+
const BASE_COMPOSE_PATH: string = path.join(
|
|
27
|
+
REPO_ROOT,
|
|
28
|
+
"docker-compose.base.yml",
|
|
29
|
+
);
|
|
30
|
+
const EXAMPLE_ENV_PATH: string = path.join(REPO_ROOT, "config.example.env");
|
|
31
|
+
const HELM_VALUES_PATH: string = path.join(
|
|
32
|
+
REPO_ROOT,
|
|
33
|
+
"HelmChart",
|
|
34
|
+
"Public",
|
|
35
|
+
"oneuptime",
|
|
36
|
+
"values.yaml",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// The three compose files an operator actually invokes; each extends the base.
|
|
40
|
+
const OVERLAY_COMPOSE_FILES: Array<string> = [
|
|
41
|
+
"docker-compose.yml",
|
|
42
|
+
"docker-compose.dev.yml",
|
|
43
|
+
"docker-compose.billing.yml",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Every cache setting, by the suffix the two spellings share.
|
|
47
|
+
const SETTING_SUFFIXES: Array<string> = [
|
|
48
|
+
"USERNAME",
|
|
49
|
+
"PASSWORD",
|
|
50
|
+
"HOST",
|
|
51
|
+
"PORT",
|
|
52
|
+
"DB",
|
|
53
|
+
"IP_FAMILY",
|
|
54
|
+
"TLS_CA",
|
|
55
|
+
"TLS_SENTINEL_MODE",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
interface ComposeService {
|
|
59
|
+
image?: string;
|
|
60
|
+
command?: string;
|
|
61
|
+
healthcheck?: { test?: Array<string> };
|
|
62
|
+
networks?: Record<string, { aliases?: Array<string> } | null>;
|
|
63
|
+
extends?: { file?: string; service?: string };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ComposeFile {
|
|
67
|
+
services: Record<string, ComposeService>;
|
|
68
|
+
"x-common-runtime-variables"?: Record<string, string>;
|
|
69
|
+
"x-common-depends-on"?: Record<string, { condition?: string }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readCompose(filePath: string): ComposeFile {
|
|
73
|
+
return yaml.load(fs.readFileSync(filePath, "utf8")) as ComposeFile;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readExampleEnvValue(name: string): string | undefined {
|
|
77
|
+
const source: string = fs.readFileSync(EXAMPLE_ENV_PATH, "utf8");
|
|
78
|
+
const match: RegExpMatchArray | null = source.match(
|
|
79
|
+
new RegExp(`^${name}=(.*)$`, "m"),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return match?.[1];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* `${VALKEY_X:-${REDIS_X}}` -- the current name, falling back to the old one. */
|
|
86
|
+
function fallbackExpression(suffix: string): string {
|
|
87
|
+
return `\${VALKEY_${suffix}:-\${REDIS_${suffix}}}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* A bare `REDIS_SOMETHING=` line in config.example.env. */
|
|
91
|
+
const LEGACY_ENV_ASSIGNMENT: RegExp = /^REDIS_[A-Z_]+=/;
|
|
92
|
+
|
|
93
|
+
describe("valkey cache and queue backend", () => {
|
|
94
|
+
const base: ComposeFile = readCompose(BASE_COMPOSE_PATH);
|
|
95
|
+
|
|
96
|
+
test("docker compose runs the valkey image and not redis", () => {
|
|
97
|
+
const service: ComposeService | undefined = base.services["valkey"];
|
|
98
|
+
|
|
99
|
+
expect(service).toBeDefined();
|
|
100
|
+
expect(service!.image).toMatch(/^valkey\/valkey:/);
|
|
101
|
+
expect(base.services["redis"]).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* The valkey image ships redis-server/redis-cli symlinks, so leaving the old
|
|
106
|
+
* binary names in place would keep working today and break silently the day
|
|
107
|
+
* those symlinks go. Both the entrypoint and the healthcheck name the real
|
|
108
|
+
* binaries.
|
|
109
|
+
*/
|
|
110
|
+
test("docker compose starts and probes valkey by its own binary names", () => {
|
|
111
|
+
const service: ComposeService = base.services["valkey"]!;
|
|
112
|
+
|
|
113
|
+
expect(service.command).toMatch(/^valkey-server /);
|
|
114
|
+
expect(service.command).not.toMatch(/redis-server/);
|
|
115
|
+
expect(service.healthcheck?.test?.[1]).toBe("valkey-cli");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
* The server, the healthcheck and the app must all resolve to ONE password.
|
|
120
|
+
* If any of the three read a different expression, the stack still starts --
|
|
121
|
+
* it just cannot authenticate, or worse, starts on a different secret than
|
|
122
|
+
* the one the operator set.
|
|
123
|
+
*/
|
|
124
|
+
test("the server, its healthcheck and the app share one password expression", () => {
|
|
125
|
+
const service: ComposeService = base.services["valkey"]!;
|
|
126
|
+
const expression: string = fallbackExpression("PASSWORD");
|
|
127
|
+
|
|
128
|
+
expect(service.command).toContain(`--requirepass "${expression}"`);
|
|
129
|
+
expect(service.healthcheck?.test).toEqual([
|
|
130
|
+
"CMD",
|
|
131
|
+
"valkey-cli",
|
|
132
|
+
"-a",
|
|
133
|
+
expression,
|
|
134
|
+
"ping",
|
|
135
|
+
]);
|
|
136
|
+
expect(base["x-common-runtime-variables"]!["VALKEY_PASSWORD"]).toBe(
|
|
137
|
+
expression,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* The upgrade case: an existing config.env holds only REDIS_*, and
|
|
143
|
+
* `npm run update` deliberately does not rewrite it (see
|
|
144
|
+
* Tests/Ops/MergeEnvTemplateRenames.test.js). The `:-` fallback in the compose
|
|
145
|
+
* file is the only thing that carries those values onto the new names.
|
|
146
|
+
*/
|
|
147
|
+
test.each(SETTING_SUFFIXES)(
|
|
148
|
+
"VALKEY_%s falls back to the deprecated REDIS_ name",
|
|
149
|
+
(suffix: string) => {
|
|
150
|
+
expect(base["x-common-runtime-variables"]![`VALKEY_${suffix}`]).toBe(
|
|
151
|
+
fallbackExpression(suffix),
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
* And the mirrors going the other way: APP_TAG lets you pin an image from
|
|
158
|
+
* before the rename against this compose file, and such an image reads
|
|
159
|
+
* REDIS_* only. Both names must carry the identical expression -- a second
|
|
160
|
+
* source of truth here is how the app and the server end up on different
|
|
161
|
+
* credentials.
|
|
162
|
+
*/
|
|
163
|
+
test.each(SETTING_SUFFIXES)(
|
|
164
|
+
"the deprecated REDIS_%s mirror carries the identical value",
|
|
165
|
+
(suffix: string) => {
|
|
166
|
+
const common: Record<string, string> =
|
|
167
|
+
base["x-common-runtime-variables"]!;
|
|
168
|
+
|
|
169
|
+
expect(common[`REDIS_${suffix}`]).toBe(common[`VALKEY_${suffix}`]);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* Renaming the service moved the hostname. Every config.env written before
|
|
175
|
+
* this change still says REDIS_HOST=redis, and those files are not rewritten
|
|
176
|
+
* on upgrade -- the network alias is the only thing keeping them resolving.
|
|
177
|
+
*/
|
|
178
|
+
test("the old redis hostname still resolves to the valkey service", () => {
|
|
179
|
+
const networks: Record<string, { aliases?: Array<string> } | null> =
|
|
180
|
+
base.services["valkey"]!.networks!;
|
|
181
|
+
|
|
182
|
+
expect(networks["oneuptime"]?.aliases).toContain("redis");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("config.example.env ships the current names only", () => {
|
|
186
|
+
expect(readExampleEnvValue("VALKEY_HOST")).toBe("valkey");
|
|
187
|
+
expect(readExampleEnvValue("VALKEY_PORT")).toBe("6379");
|
|
188
|
+
expect(readExampleEnvValue("VALKEY_USERNAME")).toBe("default");
|
|
189
|
+
expect(readExampleEnvValue("VALKEY_DB")).toBe("0");
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* A leftover REDIS_* line would be actively harmful, not just untidy:
|
|
193
|
+
* Home/Scripts/Install.sh replaces each `please-change-this-to-random-value`
|
|
194
|
+
* occurrence with a SEPARATE freshly generated value, so a duplicated
|
|
195
|
+
* password line hands the server one secret and the app another.
|
|
196
|
+
*/
|
|
197
|
+
const example: string = fs.readFileSync(EXAMPLE_ENV_PATH, "utf8");
|
|
198
|
+
const legacyLines: Array<string> = example
|
|
199
|
+
.split("\n")
|
|
200
|
+
.filter((line: string) => {
|
|
201
|
+
return LEGACY_ENV_ASSIGNMENT.test(line);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(legacyLines).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test.each(OVERLAY_COMPOSE_FILES)(
|
|
208
|
+
"%s extends and waits on the valkey service",
|
|
209
|
+
(fileName: string) => {
|
|
210
|
+
const overlay: ComposeFile = readCompose(path.join(REPO_ROOT, fileName));
|
|
211
|
+
|
|
212
|
+
expect(overlay.services["valkey"]?.extends?.service).toBe("valkey");
|
|
213
|
+
expect(overlay.services["redis"]).toBeUndefined();
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* Every app container gates its start on this block, so a stale service
|
|
217
|
+
* name here means compose refuses to start the stack at all.
|
|
218
|
+
*/
|
|
219
|
+
const dependsOn: Record<string, { condition?: string }> =
|
|
220
|
+
overlay["x-common-depends-on"]!;
|
|
221
|
+
|
|
222
|
+
expect(dependsOn["valkey"]?.condition).toBe("service_healthy");
|
|
223
|
+
expect(dependsOn["redis"]).toBeUndefined();
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* Compose and Helm claim in their comments to run the same engine version.
|
|
229
|
+
* Nothing enforces that but this: the two drift apart the first time only one
|
|
230
|
+
* of them is bumped, and the bug surfaces as behaviour that reproduces in
|
|
231
|
+
* compose but not in Kubernetes (or the reverse).
|
|
232
|
+
*/
|
|
233
|
+
test("helm runs the same pinned valkey image as compose", () => {
|
|
234
|
+
const values: {
|
|
235
|
+
valkey: { image: { repository: string; tag: string } };
|
|
236
|
+
redis: Record<string, unknown>;
|
|
237
|
+
} = yaml.load(fs.readFileSync(HELM_VALUES_PATH, "utf8")) as {
|
|
238
|
+
valkey: { image: { repository: string; tag: string } };
|
|
239
|
+
redis: Record<string, unknown>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect(values.valkey.image.repository).toBe("valkey/valkey");
|
|
243
|
+
expect(`${values.valkey.image.repository}:${values.valkey.image.tag}`).toBe(
|
|
244
|
+
base.services["valkey"]!.image,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
* The deprecated `redis:` alias must stay EMPTY. The merge in
|
|
249
|
+
* `oneuptime.valkey` layers it on top of the real defaults, so anything
|
|
250
|
+
* shipped here would be impossible for an operator to override.
|
|
251
|
+
*/
|
|
252
|
+
expect(values.redis).toEqual({});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/*
|
|
256
|
+
* A floating tag would let a major engine version arrive on an unrelated
|
|
257
|
+
* `docker compose pull` or `helm upgrade`. The pin is to a minor line, so
|
|
258
|
+
* patch releases (including CVE fixes) still flow in.
|
|
259
|
+
*/
|
|
260
|
+
test("the valkey image tag is pinned to a version line", () => {
|
|
261
|
+
expect(base.services["valkey"]!.image).toMatch(
|
|
262
|
+
/^valkey\/valkey:\d+\.\d+(\.\d+)?-alpine$/,
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import HttpMetricsMiddleware from "../../../Server/Middleware/HttpMetricsMiddleware";
|
|
3
|
+
import AppMetrics from "../../../Server/Utils/Telemetry/AppMetrics";
|
|
4
|
+
import { ExpressRequest, ExpressResponse } from "../../../Server/Utils/Express";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Every HTTP request the server handles passes through here, so two things
|
|
8
|
+
* about it are disproportionately important.
|
|
9
|
+
*
|
|
10
|
+
* THE IN-FLIGHT GAUGE MUST BALANCE. It is incremented on the way in and
|
|
11
|
+
* decremented on the way out, and Express fires BOTH `finish` and `close` for
|
|
12
|
+
* most responses. A recorder that ran twice would decrement twice, and because
|
|
13
|
+
* the gauge is cumulative the drift never washes out - it accumulates for the
|
|
14
|
+
* life of the process until "active requests" is a large negative number and
|
|
15
|
+
* every dashboard and alert built on it is worthless. `recorded` is the latch
|
|
16
|
+
* that prevents it, and nothing else checks that the latch works.
|
|
17
|
+
*
|
|
18
|
+
* THE ROUTE LABEL MUST STAY LOW-CARDINALITY. It is a metric dimension, so a
|
|
19
|
+
* raw URL would mint a new time series per distinct path - one scan of
|
|
20
|
+
* /admin/<random> is enough to blow up the metric store. Express only
|
|
21
|
+
* populates req.route once something MATCHED, so the unmatched case (which is
|
|
22
|
+
* exactly the scanning case) has to fall back to a constant.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface RecordedCall {
|
|
26
|
+
value: number;
|
|
27
|
+
attributes: Record<string, string | number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Recorder {
|
|
31
|
+
counter: Array<RecordedCall>;
|
|
32
|
+
duration: Array<RecordedCall>;
|
|
33
|
+
inFlight: Array<RecordedCall>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type InstallRecorderFunction = () => Recorder;
|
|
37
|
+
|
|
38
|
+
const installRecorder: InstallRecorderFunction = (): Recorder => {
|
|
39
|
+
const recorder: Recorder = { counter: [], duration: [], inFlight: [] };
|
|
40
|
+
|
|
41
|
+
jest.spyOn(AppMetrics, "getHttpRequestCounter").mockReturnValue({
|
|
42
|
+
add: (value: number, attributes: Record<string, string | number>): void => {
|
|
43
|
+
recorder.counter.push({ value: value, attributes: attributes });
|
|
44
|
+
},
|
|
45
|
+
} as never);
|
|
46
|
+
|
|
47
|
+
jest.spyOn(AppMetrics, "getHttpRequestDuration").mockReturnValue({
|
|
48
|
+
record: (
|
|
49
|
+
value: number,
|
|
50
|
+
attributes: Record<string, string | number>,
|
|
51
|
+
): void => {
|
|
52
|
+
recorder.duration.push({ value: value, attributes: attributes });
|
|
53
|
+
},
|
|
54
|
+
} as never);
|
|
55
|
+
|
|
56
|
+
jest.spyOn(AppMetrics, "getHttpRequestsInFlight").mockReturnValue({
|
|
57
|
+
add: (value: number, attributes: Record<string, string | number>): void => {
|
|
58
|
+
recorder.inFlight.push({ value: value, attributes: attributes });
|
|
59
|
+
},
|
|
60
|
+
} as never);
|
|
61
|
+
|
|
62
|
+
return recorder;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface FakeResponse {
|
|
66
|
+
statusCode: number;
|
|
67
|
+
listeners: Record<string, Array<() => void>>;
|
|
68
|
+
emit: (event: string) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type BuildResponseFunction = (statusCode: number) => FakeResponse;
|
|
72
|
+
|
|
73
|
+
const buildResponse: BuildResponseFunction = (
|
|
74
|
+
statusCode: number,
|
|
75
|
+
): FakeResponse => {
|
|
76
|
+
const listeners: Record<string, Array<() => void>> = {};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
statusCode: statusCode,
|
|
80
|
+
listeners: listeners,
|
|
81
|
+
emit: (event: string): void => {
|
|
82
|
+
for (const listener of listeners[event] ?? []) {
|
|
83
|
+
listener();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
on: (event: string, listener: () => void): void => {
|
|
87
|
+
listeners[event] = listeners[event] ?? [];
|
|
88
|
+
listeners[event]!.push(listener);
|
|
89
|
+
},
|
|
90
|
+
} as unknown as FakeResponse;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type RunFunction = (data: {
|
|
94
|
+
method?: string | undefined;
|
|
95
|
+
route?: { path?: string } | undefined;
|
|
96
|
+
baseUrl?: string;
|
|
97
|
+
statusCode?: number;
|
|
98
|
+
emit?: Array<string>;
|
|
99
|
+
}) => { recorder: Recorder; nextCalled: number };
|
|
100
|
+
|
|
101
|
+
const run: RunFunction = (data: {
|
|
102
|
+
method?: string | undefined;
|
|
103
|
+
route?: { path?: string } | undefined;
|
|
104
|
+
baseUrl?: string;
|
|
105
|
+
statusCode?: number;
|
|
106
|
+
emit?: Array<string>;
|
|
107
|
+
}): { recorder: Recorder; nextCalled: number } => {
|
|
108
|
+
const recorder: Recorder = installRecorder();
|
|
109
|
+
const res: FakeResponse = buildResponse(data.statusCode ?? 200);
|
|
110
|
+
|
|
111
|
+
let nextCalled: number = 0;
|
|
112
|
+
|
|
113
|
+
const req: Record<string, unknown> = {
|
|
114
|
+
method: data.method,
|
|
115
|
+
route: data.route,
|
|
116
|
+
baseUrl: data.baseUrl,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
HttpMetricsMiddleware(
|
|
120
|
+
req as unknown as ExpressRequest,
|
|
121
|
+
res as unknown as ExpressResponse,
|
|
122
|
+
((): void => {
|
|
123
|
+
nextCalled++;
|
|
124
|
+
}) as never,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
for (const event of data.emit ?? ["finish"]) {
|
|
128
|
+
res.emit(event);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { recorder: recorder, nextCalled: nextCalled };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
describe("the middleware stays out of the way of the request", () => {
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
jest.restoreAllMocks();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("calls next exactly once, immediately", () => {
|
|
140
|
+
expect(run({ method: "GET" }).nextCalled).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("counts the request as in flight before the handler runs", () => {
|
|
144
|
+
const recorder: Recorder = installRecorder();
|
|
145
|
+
const res: FakeResponse = buildResponse(200);
|
|
146
|
+
|
|
147
|
+
HttpMetricsMiddleware(
|
|
148
|
+
{ method: "GET" } as unknown as ExpressRequest,
|
|
149
|
+
res as unknown as ExpressResponse,
|
|
150
|
+
((): void => {}) as never,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Nothing has finished yet, so the only entry is the increment.
|
|
154
|
+
expect(recorder.inFlight).toEqual([
|
|
155
|
+
{ value: 1, attributes: { "http.request.method": "GET" } },
|
|
156
|
+
]);
|
|
157
|
+
expect(recorder.counter).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("the in-flight gauge balances", () => {
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
jest.restoreAllMocks();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("adds one on the way in and takes one back on the way out", () => {
|
|
167
|
+
const recorder: Recorder = run({ method: "GET" }).recorder;
|
|
168
|
+
|
|
169
|
+
expect(
|
|
170
|
+
recorder.inFlight.map((call: RecordedCall) => {
|
|
171
|
+
return call.value;
|
|
172
|
+
}),
|
|
173
|
+
).toEqual([1, -1]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Express fires both events for an ordinary response. Recording on each
|
|
178
|
+
* would decrement twice, and because the gauge is cumulative that error
|
|
179
|
+
* never washes out - it accumulates for the life of the process.
|
|
180
|
+
*/
|
|
181
|
+
test("records once even though both finish and close fire", () => {
|
|
182
|
+
const recorder: Recorder = run({
|
|
183
|
+
method: "GET",
|
|
184
|
+
emit: ["finish", "close"],
|
|
185
|
+
}).recorder;
|
|
186
|
+
|
|
187
|
+
expect(recorder.counter.length).toBe(1);
|
|
188
|
+
expect(recorder.duration.length).toBe(1);
|
|
189
|
+
expect(
|
|
190
|
+
recorder.inFlight.reduce((total: number, call: RecordedCall) => {
|
|
191
|
+
return total + call.value;
|
|
192
|
+
}, 0),
|
|
193
|
+
).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("records once when only close fires - a client that hung up", () => {
|
|
197
|
+
const recorder: Recorder = run({ method: "GET", emit: ["close"] }).recorder;
|
|
198
|
+
|
|
199
|
+
expect(recorder.counter.length).toBe(1);
|
|
200
|
+
expect(
|
|
201
|
+
recorder.inFlight.reduce((total: number, call: RecordedCall) => {
|
|
202
|
+
return total + call.value;
|
|
203
|
+
}, 0),
|
|
204
|
+
).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("stays balanced however many times the events are re-emitted", () => {
|
|
208
|
+
const recorder: Recorder = run({
|
|
209
|
+
method: "GET",
|
|
210
|
+
emit: ["finish", "close", "finish", "close"],
|
|
211
|
+
}).recorder;
|
|
212
|
+
|
|
213
|
+
expect(recorder.counter.length).toBe(1);
|
|
214
|
+
expect(
|
|
215
|
+
recorder.inFlight.reduce((total: number, call: RecordedCall) => {
|
|
216
|
+
return total + call.value;
|
|
217
|
+
}, 0),
|
|
218
|
+
).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("the route label stays low-cardinality", () => {
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
jest.restoreAllMocks();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("uses the route TEMPLATE, never the resolved path", () => {
|
|
228
|
+
const recorder: Recorder = run({
|
|
229
|
+
method: "GET",
|
|
230
|
+
route: { path: "/users/:id" },
|
|
231
|
+
}).recorder;
|
|
232
|
+
|
|
233
|
+
expect(recorder.counter[0]!.attributes["http.route"]).toBe("/users/:id");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("prefixes the router's mount point so two routers do not collide", () => {
|
|
237
|
+
const recorder: Recorder = run({
|
|
238
|
+
method: "GET",
|
|
239
|
+
route: { path: "/users/:id" },
|
|
240
|
+
baseUrl: "/api",
|
|
241
|
+
}).recorder;
|
|
242
|
+
|
|
243
|
+
expect(recorder.counter[0]!.attributes["http.route"]).toBe(
|
|
244
|
+
"/api/users/:id",
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* The unmatched case IS the scanning case. Labelling it with the raw URL
|
|
250
|
+
* would mint one time series per probe - which is the fastest way to lose a
|
|
251
|
+
* metric store to somebody else's port scanner.
|
|
252
|
+
*/
|
|
253
|
+
test("labels an unmatched request with a constant, not its URL", () => {
|
|
254
|
+
const recorder: Recorder = run({ method: "GET" }).recorder;
|
|
255
|
+
|
|
256
|
+
expect(recorder.counter[0]!.attributes["http.route"]).toBe("unmatched");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("labels a route object with no path as unmatched too", () => {
|
|
260
|
+
const recorder: Recorder = run({ method: "GET", route: {} }).recorder;
|
|
261
|
+
|
|
262
|
+
expect(recorder.counter[0]!.attributes["http.route"]).toBe("unmatched");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("carries no high-cardinality identifiers at all", () => {
|
|
266
|
+
const recorder: Recorder = run({
|
|
267
|
+
method: "GET",
|
|
268
|
+
route: { path: "/users/:id" },
|
|
269
|
+
}).recorder;
|
|
270
|
+
|
|
271
|
+
expect(Object.keys(recorder.counter[0]!.attributes).sort()).toEqual([
|
|
272
|
+
"http.request.method",
|
|
273
|
+
"http.response.status_code",
|
|
274
|
+
"http.route",
|
|
275
|
+
"status_class",
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("the labels it records", () => {
|
|
281
|
+
afterEach(() => {
|
|
282
|
+
jest.restoreAllMocks();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("upper-cases the method", () => {
|
|
286
|
+
const recorder: Recorder = run({ method: "post" }).recorder;
|
|
287
|
+
|
|
288
|
+
expect(recorder.counter[0]!.attributes["http.request.method"]).toBe("POST");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("labels a request with no method as UNKNOWN rather than blank", () => {
|
|
292
|
+
const recorder: Recorder = run({ method: undefined }).recorder;
|
|
293
|
+
|
|
294
|
+
expect(recorder.counter[0]!.attributes["http.request.method"]).toBe(
|
|
295
|
+
"UNKNOWN",
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("records the exact status code alongside its class", () => {
|
|
300
|
+
const recorder: Recorder = run({
|
|
301
|
+
method: "GET",
|
|
302
|
+
statusCode: 404,
|
|
303
|
+
}).recorder;
|
|
304
|
+
|
|
305
|
+
expect(recorder.counter[0]!.attributes["http.response.status_code"]).toBe(
|
|
306
|
+
404,
|
|
307
|
+
);
|
|
308
|
+
expect(recorder.counter[0]!.attributes["status_class"]).toBe("4xx");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const STATUS_CLASSES: Array<[number, string]> = [
|
|
312
|
+
[200, "2xx"],
|
|
313
|
+
[201, "2xx"],
|
|
314
|
+
[301, "3xx"],
|
|
315
|
+
[400, "4xx"],
|
|
316
|
+
[401, "4xx"],
|
|
317
|
+
[500, "5xx"],
|
|
318
|
+
[503, "5xx"],
|
|
319
|
+
[100, "1xx"],
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
test.each(STATUS_CLASSES)(
|
|
323
|
+
"buckets %i as %s",
|
|
324
|
+
(statusCode: number, expected: string) => {
|
|
325
|
+
const recorder: Recorder = run({
|
|
326
|
+
method: "GET",
|
|
327
|
+
statusCode: statusCode,
|
|
328
|
+
}).recorder;
|
|
329
|
+
|
|
330
|
+
expect(recorder.counter[0]!.attributes["status_class"]).toBe(expected);
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* A response that was never given a status - a socket that died before
|
|
336
|
+
* anything was written - must not produce a "0xx" bucket nobody can
|
|
337
|
+
* interpret.
|
|
338
|
+
*/
|
|
339
|
+
test("buckets a status that never got set as unknown", () => {
|
|
340
|
+
const recorder: Recorder = run({ method: "GET", statusCode: 0 }).recorder;
|
|
341
|
+
|
|
342
|
+
expect(recorder.counter[0]!.attributes["status_class"]).toBe("unknown");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("buckets an out-of-range status as unknown", () => {
|
|
346
|
+
const recorder: Recorder = run({ method: "GET", statusCode: 999 }).recorder;
|
|
347
|
+
|
|
348
|
+
expect(recorder.counter[0]!.attributes["status_class"]).toBe("unknown");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("the duration and the count carry identical labels", () => {
|
|
352
|
+
const recorder: Recorder = run({
|
|
353
|
+
method: "GET",
|
|
354
|
+
route: { path: "/users/:id" },
|
|
355
|
+
statusCode: 201,
|
|
356
|
+
}).recorder;
|
|
357
|
+
|
|
358
|
+
expect(recorder.duration[0]!.attributes).toEqual(
|
|
359
|
+
recorder.counter[0]!.attributes,
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("records a duration in milliseconds, not nanoseconds", () => {
|
|
364
|
+
const recorder: Recorder = run({ method: "GET" }).recorder;
|
|
365
|
+
const durationMs: number = recorder.duration[0]!.value;
|
|
366
|
+
|
|
367
|
+
expect(durationMs).toBeGreaterThanOrEqual(0);
|
|
368
|
+
// A synchronous test cannot plausibly take a second.
|
|
369
|
+
expect(durationMs).toBeLessThan(1000);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("the in-flight gauge is labelled by method only", () => {
|
|
373
|
+
const recorder: Recorder = run({
|
|
374
|
+
method: "GET",
|
|
375
|
+
route: { path: "/users/:id" },
|
|
376
|
+
}).recorder;
|
|
377
|
+
|
|
378
|
+
for (const call of recorder.inFlight) {
|
|
379
|
+
expect(Object.keys(call.attributes)).toEqual(["http.request.method"]);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|