@oneuptime/common 14.0.2 → 14.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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.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/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -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/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
expectReadableDescriptionRecord,
|
|
6
|
+
expectTitleExplained,
|
|
7
|
+
} from "./MetricDescriptionRules";
|
|
8
|
+
import {
|
|
9
|
+
DOCKER_SWARM_INSIGHTS_CHART_DESCRIPTIONS,
|
|
10
|
+
DOCKER_SWARM_METRIC_DESCRIPTIONS,
|
|
11
|
+
DockerSwarmInsightsChart,
|
|
12
|
+
DockerSwarmMetric,
|
|
13
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/DockerSwarmMetricDescriptions";
|
|
14
|
+
import { extractDockerSwarmInventoryResource } from "../../../Types/DockerSwarm/DockerSwarmInventoryExtractor";
|
|
15
|
+
import {
|
|
16
|
+
METRIC_STALE_MS,
|
|
17
|
+
toInfrastructureResource,
|
|
18
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/DockerSwarm/Utils/DockerSwarmResourceUtils";
|
|
19
|
+
import DockerSwarmResourceModel from "../../../Models/DatabaseModels/DockerSwarmResource";
|
|
20
|
+
import {
|
|
21
|
+
getStatusBadgeClass,
|
|
22
|
+
InfrastructureResource,
|
|
23
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Infrastructure/ResourceTable";
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* The plain-English texts behind every (i) on the Docker Swarm cluster pages
|
|
27
|
+
* (and the line under each Insights chart title).
|
|
28
|
+
*
|
|
29
|
+
* The rules (length, finished sentences, no placeholders) are checked for
|
|
30
|
+
* every module by MetricDescriptionsCatalog.test.ts; this file adds the
|
|
31
|
+
* accuracy anchors - each claim a text makes is checked against the code
|
|
32
|
+
* that produces the number, so the words cannot drift from the fetch:
|
|
33
|
+
*
|
|
34
|
+
* - counts come from the agent's inventory snapshot (inventory-snapshot.sh
|
|
35
|
+
* and DockerSwarmInventoryExtractor), not from a time range;
|
|
36
|
+
* - only Task rows are given CPU/memory at ingest, so the Nodes and
|
|
37
|
+
* Services columns are always N/A;
|
|
38
|
+
* - the lists hide readings older than METRIC_STALE_MS, the task page not;
|
|
39
|
+
* - the Insights charts read docker_stats' own scale (100% = one core).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const REPO_ROOT: string = path.join(__dirname, "..", "..", "..", "..", "..");
|
|
43
|
+
|
|
44
|
+
function readRepoFile(...segments: Array<string>): string {
|
|
45
|
+
return fs.readFileSync(path.join(REPO_ROOT, ...segments), "utf8");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const AGENT_SNAPSHOT_SCRIPT: string = readRepoFile(
|
|
49
|
+
"agents",
|
|
50
|
+
"DockerSwarmAgent",
|
|
51
|
+
"inventory-snapshot.sh",
|
|
52
|
+
);
|
|
53
|
+
const AGENT_COLLECTOR_CONFIG: string = readRepoFile(
|
|
54
|
+
"agents",
|
|
55
|
+
"DockerSwarmAgent",
|
|
56
|
+
"otel-collector-config.yaml",
|
|
57
|
+
);
|
|
58
|
+
const METRICS_INGEST: string = readRepoFile(
|
|
59
|
+
"packages",
|
|
60
|
+
"App",
|
|
61
|
+
"FeatureSet",
|
|
62
|
+
"Telemetry",
|
|
63
|
+
"Services",
|
|
64
|
+
"OtelMetricsIngestService.ts",
|
|
65
|
+
);
|
|
66
|
+
const INSIGHTS_PAGE: string = readRepoFile(
|
|
67
|
+
"packages",
|
|
68
|
+
"App",
|
|
69
|
+
"FeatureSet",
|
|
70
|
+
"Dashboard",
|
|
71
|
+
"src",
|
|
72
|
+
"Pages",
|
|
73
|
+
"DockerSwarm",
|
|
74
|
+
"View",
|
|
75
|
+
"Insights.tsx",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const T: Record<DockerSwarmMetric, string> = DOCKER_SWARM_METRIC_DESCRIPTIONS;
|
|
79
|
+
const CHART: Record<DockerSwarmInsightsChart, string> =
|
|
80
|
+
DOCKER_SWARM_INSIGHTS_CHART_DESCRIPTIONS;
|
|
81
|
+
|
|
82
|
+
// The source between two markers, for checking one function at a time.
|
|
83
|
+
function between(source: string, from: string, to: string): string {
|
|
84
|
+
const start: number = source.indexOf(from);
|
|
85
|
+
|
|
86
|
+
expect(start).toBeGreaterThan(-1);
|
|
87
|
+
|
|
88
|
+
const end: number = source.indexOf(to, start + from.length);
|
|
89
|
+
|
|
90
|
+
expect(end).toBeGreaterThan(start);
|
|
91
|
+
|
|
92
|
+
return source.slice(start, end);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function serviceReadiness(replicas: string): boolean | null {
|
|
96
|
+
const parsed: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
97
|
+
extractDockerSwarmInventoryResource({
|
|
98
|
+
kind: "Service",
|
|
99
|
+
logBody: JSON.stringify({
|
|
100
|
+
data: {
|
|
101
|
+
ID: "svc1",
|
|
102
|
+
Name: "web",
|
|
103
|
+
Mode: "replicated",
|
|
104
|
+
Replicas: replicas,
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
lastSeenAt: new Date(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(parsed).not.toBeNull();
|
|
111
|
+
|
|
112
|
+
return parsed!.resource.isReady;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function taskRow(minutesSinceReading: number): DockerSwarmResourceModel {
|
|
116
|
+
const row: DockerSwarmResourceModel = new DockerSwarmResourceModel();
|
|
117
|
+
row.kind = "Task";
|
|
118
|
+
row.externalId = "task/abc";
|
|
119
|
+
row.name = "web.1";
|
|
120
|
+
row.latestCpuPercent = 12.5;
|
|
121
|
+
row.latestMemoryBytes = 64 * 1024 * 1024;
|
|
122
|
+
row.metricsUpdatedAt = new Date(Date.now() - minutesSinceReading * 60_000);
|
|
123
|
+
|
|
124
|
+
return row;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Title shown on the page -> the description it is paired with.
|
|
128
|
+
const TITLED: Array<[string, string]> = [
|
|
129
|
+
["Nodes", T.nodes],
|
|
130
|
+
["Nodes ready", T.nodes],
|
|
131
|
+
["Managers", T.managers],
|
|
132
|
+
["Services", T.services],
|
|
133
|
+
["Tasks", T.tasks],
|
|
134
|
+
["Tasks running", T.tasks],
|
|
135
|
+
["Stacks", T.stacks],
|
|
136
|
+
["Networks", T.networks],
|
|
137
|
+
["Volumes", T.volumes],
|
|
138
|
+
["Status", T.serviceStatus],
|
|
139
|
+
["Status", T.serviceStatusColumn],
|
|
140
|
+
["Replicas", T.replicas],
|
|
141
|
+
["CPU", T.taskCpu],
|
|
142
|
+
["Memory", T.taskMemory],
|
|
143
|
+
["CPU", T.taskCpuColumn],
|
|
144
|
+
["Memory", T.taskMemoryColumn],
|
|
145
|
+
["CPU", T.nodeUsageColumns],
|
|
146
|
+
["Memory", T.nodeUsageColumns],
|
|
147
|
+
["CPU", T.serviceUsageColumns],
|
|
148
|
+
["Memory", T.serviceUsageColumns],
|
|
149
|
+
["Services", T.stackServices],
|
|
150
|
+
["Status", T.stackStatusColumn],
|
|
151
|
+
["Nodes", T.clusterListNodes],
|
|
152
|
+
["Services", T.clusterListServices],
|
|
153
|
+
["Tasks", T.clusterListTasks],
|
|
154
|
+
["Cluster CPU Utilization", CHART.clusterCpu],
|
|
155
|
+
["Cluster Memory Utilization", CHART.clusterMemoryPercent],
|
|
156
|
+
["Task Memory Usage", CHART.taskMemory],
|
|
157
|
+
["Top Tasks by CPU", CHART.topTasksCpu],
|
|
158
|
+
["Top Tasks by Memory", CHART.topTasksMemory],
|
|
159
|
+
["Task Process Count", CHART.taskProcesses],
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const RAW_METRIC_NAME: RegExp = /\bcontainer\.[a-z_.]+/;
|
|
163
|
+
const WHITESPACE: RegExp = /\s+/g;
|
|
164
|
+
const CUTOFF_ANCHORED_TO_CLUSTER: RegExp =
|
|
165
|
+
/getStaleThresholdDate\(\s*cluster\.lastSeenAt/;
|
|
166
|
+
const EVERY_CONTAINER_CLAIM: RegExp = /every container is drawn/i;
|
|
167
|
+
const RANKED_BY_MAX: RegExp = /rankBy: "max" as const/;
|
|
168
|
+
const TASKS_WANTED_FIRST: RegExp = /^Tasks Swarm wants running/;
|
|
169
|
+
|
|
170
|
+
describe("Docker Swarm metric descriptions read well", () => {
|
|
171
|
+
test("every tooltip text passes the shared rules and none repeats another", () => {
|
|
172
|
+
expectReadableDescriptionRecord(
|
|
173
|
+
DOCKER_SWARM_METRIC_DESCRIPTIONS,
|
|
174
|
+
"DOCKER_SWARM_METRIC_DESCRIPTIONS",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("every Insights chart line passes the shared rules and none repeats another", () => {
|
|
179
|
+
expectReadableDescriptionRecord(
|
|
180
|
+
DOCKER_SWARM_INSIGHTS_CHART_DESCRIPTIONS,
|
|
181
|
+
"DOCKER_SWARM_INSIGHTS_CHART_DESCRIPTIONS",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test.each(TITLED)(
|
|
186
|
+
"%s is explained by its text",
|
|
187
|
+
(title: string, text: string) => {
|
|
188
|
+
expectTitleExplained(title, text);
|
|
189
|
+
expect(text.length).toBeLessThanOrEqual(260);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
test("no text leans on a raw metric name - the chart's own (i) already shows that", () => {
|
|
194
|
+
for (const text of [...Object.values(T), ...Object.values(CHART)]) {
|
|
195
|
+
expect(text).not.toMatch(RAW_METRIC_NAME);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("the chart lines are not also tooltip texts", () => {
|
|
200
|
+
for (const text of Object.values(CHART)) {
|
|
201
|
+
expect(Object.values(T)).not.toContain(text);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("overview counts describe the inventory snapshot, not a time range", () => {
|
|
207
|
+
const INVENTORY_COUNTS: Array<DockerSwarmMetric> = [
|
|
208
|
+
"nodes",
|
|
209
|
+
"managers",
|
|
210
|
+
"services",
|
|
211
|
+
"tasks",
|
|
212
|
+
"stacks",
|
|
213
|
+
"networks",
|
|
214
|
+
"volumes",
|
|
215
|
+
"serviceStatus",
|
|
216
|
+
"serviceStatusColumn",
|
|
217
|
+
"replicas",
|
|
218
|
+
"stackServices",
|
|
219
|
+
"stackStatusColumn",
|
|
220
|
+
"clusterListNodes",
|
|
221
|
+
"clusterListServices",
|
|
222
|
+
"clusterListTasks",
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
test.each(INVENTORY_COUNTS)(
|
|
226
|
+
"%s never claims a selected range",
|
|
227
|
+
(key: DockerSwarmMetric) => {
|
|
228
|
+
expect(T[key]).not.toMatch(/selected range|time range|past hour/i);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
test("the snapshot cadence the texts quote is the agent's default", () => {
|
|
233
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
234
|
+
'INTERVAL="${DOCKER_INVENTORY_INTERVAL_SECONDS:-300}"',
|
|
235
|
+
);
|
|
236
|
+
expect(T.nodes).toContain("latest inventory snapshot");
|
|
237
|
+
expect(T.nodes).toContain("every 5 minutes by default");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("Nodes explains Ready as the node state the managers report", () => {
|
|
241
|
+
const ready: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
242
|
+
extractDockerSwarmInventoryResource({
|
|
243
|
+
kind: "Node",
|
|
244
|
+
logBody: JSON.stringify({
|
|
245
|
+
data: { ID: "n1", Hostname: "a", Status: "down", ManagerStatus: "" },
|
|
246
|
+
}),
|
|
247
|
+
lastSeenAt: new Date(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(ready!.resource.isReady).toBe(false);
|
|
251
|
+
expect(T.nodes).toMatch(/Ready counts/);
|
|
252
|
+
expect(T.nodes).toMatch(/Down cannot run tasks/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("Managers are the nodes with a manager status, and the text does not say they never run tasks", () => {
|
|
256
|
+
const manager: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
257
|
+
extractDockerSwarmInventoryResource({
|
|
258
|
+
kind: "Node",
|
|
259
|
+
logBody: JSON.stringify({
|
|
260
|
+
data: {
|
|
261
|
+
ID: "n1",
|
|
262
|
+
Hostname: "a",
|
|
263
|
+
Status: "ready",
|
|
264
|
+
ManagerStatus: "Leader",
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
lastSeenAt: new Date(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(manager!.resource.role).toBe("manager");
|
|
271
|
+
expect(T.managers).toContain("can run tasks too");
|
|
272
|
+
expect(T.managers).toContain("worker");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("Services: converged means running >= desired AND desired > 0, so a scaled-to-zero service is not converged", () => {
|
|
276
|
+
expect(serviceReadiness("3/3")).toBe(true);
|
|
277
|
+
expect(serviceReadiness("4/3")).toBe(true);
|
|
278
|
+
expect(serviceReadiness("2/3")).toBe(false);
|
|
279
|
+
expect(serviceReadiness("0/0")).toBe(false);
|
|
280
|
+
|
|
281
|
+
expect(T.services).toMatch(/Converged counts/);
|
|
282
|
+
expect(T.services).toContain("scaled to 0 never counts as converged");
|
|
283
|
+
expect(T.serviceStatus).toContain("scaled to 0");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("Service status names its colours the way ServiceDetail picks them", () => {
|
|
287
|
+
const serviceDetail: string = readRepoFile(
|
|
288
|
+
"packages",
|
|
289
|
+
"App",
|
|
290
|
+
"FeatureSet",
|
|
291
|
+
"Dashboard",
|
|
292
|
+
"src",
|
|
293
|
+
"Pages",
|
|
294
|
+
"DockerSwarm",
|
|
295
|
+
"View",
|
|
296
|
+
"ServiceDetail.tsx",
|
|
297
|
+
).replace(WHITESPACE, " ");
|
|
298
|
+
const badge: string = readRepoFile(
|
|
299
|
+
"packages",
|
|
300
|
+
"Common",
|
|
301
|
+
"UI",
|
|
302
|
+
"Components",
|
|
303
|
+
"StatusBadge",
|
|
304
|
+
"StatusBadge.tsx",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Not converged (isReady false, which includes 0/0) is the amber badge.
|
|
308
|
+
expect(serviceDetail).toContain(
|
|
309
|
+
"row.isReady === false ? StatusBadgeType.Warning : StatusBadgeType.Success",
|
|
310
|
+
);
|
|
311
|
+
expect(between(badge, "[StatusBadgeType.Warning]:", ",")).toContain(
|
|
312
|
+
"amber",
|
|
313
|
+
);
|
|
314
|
+
expect(between(badge, "[StatusBadgeType.Success]:", ",")).toContain(
|
|
315
|
+
"emerald",
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(T.serviceStatus).toContain("2/3");
|
|
319
|
+
expect(T.serviceStatus).toMatch(/Green when every wanted task is running/);
|
|
320
|
+
expect(T.serviceStatus).toMatch(/amber when some are missing/);
|
|
321
|
+
// On the list the badge is plain - no colours are promised there.
|
|
322
|
+
expect(T.serviceStatusColumn).not.toMatch(/green|amber|red/i);
|
|
323
|
+
expect(T.serviceStatusColumn).toContain("same figure as Replicas");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("Replicas: the agent always sends running/desired, and a global service wants a copy per eligible node", () => {
|
|
327
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
328
|
+
'Replicas: (((.ServiceStatus.RunningTasks // 0)|tostring) + "/" + ((.ServiceStatus.DesiredTasks',
|
|
329
|
+
);
|
|
330
|
+
expect(T.replicas).toContain("2/3 means one is missing");
|
|
331
|
+
expect(T.replicas).toContain("global service wants one copy on every");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("Tasks: only the tasks Swarm wants running are inventoried", () => {
|
|
335
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
336
|
+
'select(.DesiredState == "running")',
|
|
337
|
+
);
|
|
338
|
+
expect(T.tasks).toMatch(/^Tasks Swarm wants running/);
|
|
339
|
+
expect(T.tasks).toMatch(/one task is one container of a service/);
|
|
340
|
+
// The page counts only state "running"; everything else is the rest.
|
|
341
|
+
expect(T.tasks).toContain("running counts those actually running");
|
|
342
|
+
expect(T.tasks).toContain("the rest are starting, stopped or failed");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("Tasks: a replaced task lingers until the stale-row cleanup, which the text owns up to", () => {
|
|
346
|
+
/*
|
|
347
|
+
* A row is pruned once it is 15 minutes older than the CLUSTER's
|
|
348
|
+
* lastSeenAt (not wall-clock now), and that lastSeenAt is itself only
|
|
349
|
+
* refreshed through a 5-minute ingest fence; the sweep runs every 5
|
|
350
|
+
* minutes. Worst case 15 + 5 + 5 = about 25 minutes, so "about 20"
|
|
351
|
+
* would understate it.
|
|
352
|
+
*/
|
|
353
|
+
const ingestBase: string = readRepoFile(
|
|
354
|
+
"packages",
|
|
355
|
+
"App",
|
|
356
|
+
"FeatureSet",
|
|
357
|
+
"Telemetry",
|
|
358
|
+
"Services",
|
|
359
|
+
"OtelIngestBaseService.ts",
|
|
360
|
+
);
|
|
361
|
+
const service: string = readRepoFile(
|
|
362
|
+
"packages",
|
|
363
|
+
"Common",
|
|
364
|
+
"Server",
|
|
365
|
+
"Services",
|
|
366
|
+
"DockerSwarmResourceService.ts",
|
|
367
|
+
);
|
|
368
|
+
const cleanup: string = readRepoFile(
|
|
369
|
+
"packages",
|
|
370
|
+
"App",
|
|
371
|
+
"FeatureSet",
|
|
372
|
+
"Workers",
|
|
373
|
+
"Jobs",
|
|
374
|
+
"DockerSwarm",
|
|
375
|
+
"CleanupStaleResources.ts",
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(service).toContain("return 15;");
|
|
379
|
+
expect(cleanup).toContain("schedule: EVERY_FIVE_MINUTE");
|
|
380
|
+
expect(cleanup).toMatch(CUTOFF_ANCHORED_TO_CLUSTER);
|
|
381
|
+
expect(ingestBase).toContain(
|
|
382
|
+
"MAINTENANCE_FENCE_TTL_SECONDS: number = 5 * 60;",
|
|
383
|
+
);
|
|
384
|
+
expect(T.tasks).toContain("up to about 25 minutes");
|
|
385
|
+
expect(T.tasks).not.toContain("20 minutes");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("Stacks are grouped from the stack label on services", () => {
|
|
389
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain('"com.docker.stack.namespace"');
|
|
390
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain("group_by(.)");
|
|
391
|
+
expect(T.stacks).toContain("stack name Docker puts on each service");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("Networks: only swarm-scoped networks are counted", () => {
|
|
395
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain('select(.Scope == "swarm")');
|
|
396
|
+
expect(T.networks).toMatch(/single node only.*are not counted/);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("Volumes: listed on the poller's own node only, and the overview has no other source", () => {
|
|
400
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain('fetch "/volumes"');
|
|
401
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain("Node: $node");
|
|
402
|
+
expect(T.volumes).toContain(
|
|
403
|
+
"the node where the OneUptime inventory poller",
|
|
404
|
+
);
|
|
405
|
+
expect(T.volumes).toContain(
|
|
406
|
+
"volumes on nodes without the poller are not counted",
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("Volumes: the text does not promise a single node - the poller ships in the same compose file as the collector", () => {
|
|
411
|
+
/*
|
|
412
|
+
* The collector config invites running the agent on every node, and
|
|
413
|
+
* docker-compose.yml starts the inventory poller alongside it. /volumes
|
|
414
|
+
* answers on a worker too (only the swarm endpoints are manager-only),
|
|
415
|
+
* so each node that runs the compose file adds its own volumes.
|
|
416
|
+
*/
|
|
417
|
+
const compose: string = readRepoFile(
|
|
418
|
+
"agents",
|
|
419
|
+
"DockerSwarmAgent",
|
|
420
|
+
"docker-compose.yml",
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(compose).toContain("oneuptime-docker-swarm-agent:");
|
|
424
|
+
expect(compose).toContain("oneuptime-docker-swarm-inventory:");
|
|
425
|
+
expect(AGENT_COLLECTOR_CONFIG).toContain("run the agent on every node");
|
|
426
|
+
expect(T.volumes).not.toMatch(/\bthe one manager\b/);
|
|
427
|
+
expect(T.volumes).toContain("normally one manager");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("the Stacks list: a service count, not a health check", () => {
|
|
432
|
+
function stackRow(): DockerSwarmResourceModel {
|
|
433
|
+
const parsed: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
434
|
+
extractDockerSwarmInventoryResource({
|
|
435
|
+
kind: "Stack",
|
|
436
|
+
logBody: JSON.stringify({ data: { Name: "shop", Services: "3" } }),
|
|
437
|
+
lastSeenAt: new Date(),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(parsed).not.toBeNull();
|
|
441
|
+
|
|
442
|
+
const row: DockerSwarmResourceModel = new DockerSwarmResourceModel();
|
|
443
|
+
Object.assign(row, parsed!.resource);
|
|
444
|
+
|
|
445
|
+
return row;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
test("the agent counts every service carrying the stack label, whatever its replicas", () => {
|
|
449
|
+
// All services are fetched, with no filter on their state.
|
|
450
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
451
|
+
'fetch "/services?status=true" > "${SERVICES_JSON}"',
|
|
452
|
+
);
|
|
453
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
454
|
+
'[.[] | .Spec.Labels["com.docker.stack.namespace"] // empty]',
|
|
455
|
+
);
|
|
456
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain("Services: (length|tostring)");
|
|
457
|
+
|
|
458
|
+
expect(T.stackServices).toContain("carry this stack's name");
|
|
459
|
+
expect(T.stackServices).toContain("whatever their state");
|
|
460
|
+
expect(T.stackServices).toContain("including ones scaled to 0");
|
|
461
|
+
expect(T.stackServices).toContain("every 5 minutes by default");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("the Services column and the Status column show the same number", () => {
|
|
465
|
+
const resource: InfrastructureResource =
|
|
466
|
+
toInfrastructureResource(stackRow());
|
|
467
|
+
|
|
468
|
+
expect(resource.additionalAttributes["serviceCount"]).toBe("3");
|
|
469
|
+
expect(resource.status).toBe("3 services");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("the Status badge for a stack is grey, never green or red, as a count should be", () => {
|
|
473
|
+
const resource: InfrastructureResource =
|
|
474
|
+
toInfrastructureResource(stackRow());
|
|
475
|
+
|
|
476
|
+
expect(getStatusBadgeClass(resource.status)).toBe(
|
|
477
|
+
"bg-gray-50 text-gray-700",
|
|
478
|
+
);
|
|
479
|
+
expect(T.stackStatusColumn).toContain("only repeats its service count");
|
|
480
|
+
expect(T.stackStatusColumn).toContain("it is not a health check");
|
|
481
|
+
expect(T.stackStatusColumn).toContain("Services list");
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
describe("the Clusters list reads the counts cached on the cluster row", () => {
|
|
486
|
+
const LOGS_INGEST: string = readRepoFile(
|
|
487
|
+
"packages",
|
|
488
|
+
"App",
|
|
489
|
+
"FeatureSet",
|
|
490
|
+
"Telemetry",
|
|
491
|
+
"Services",
|
|
492
|
+
"OtelLogsIngestService.ts",
|
|
493
|
+
).replace(WHITESPACE, " ");
|
|
494
|
+
const CLUSTER_SERVICE: string = readRepoFile(
|
|
495
|
+
"packages",
|
|
496
|
+
"Common",
|
|
497
|
+
"Server",
|
|
498
|
+
"Services",
|
|
499
|
+
"DockerSwarmClusterService.ts",
|
|
500
|
+
).replace(WHITESPACE, " ");
|
|
501
|
+
|
|
502
|
+
// The counts are derived from one inventory batch, right after its upsert.
|
|
503
|
+
const counts: string = between(
|
|
504
|
+
LOGS_INGEST,
|
|
505
|
+
"const sawKind: Set<string> = new Set(",
|
|
506
|
+
"await DockerSwarmClusterService.updateLastSeen(",
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
function taskIsRunning(currentState: string): boolean | null {
|
|
510
|
+
const parsed: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
511
|
+
extractDockerSwarmInventoryResource({
|
|
512
|
+
kind: "Task",
|
|
513
|
+
logBody: JSON.stringify({
|
|
514
|
+
data: { ID: "t1", Name: "web.1", CurrentState: currentState },
|
|
515
|
+
}),
|
|
516
|
+
lastSeenAt: new Date(),
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return parsed!.resource.isReady;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
test("each count is the snapshot's own rows of that kind", () => {
|
|
523
|
+
expect(counts).toContain('extras.nodeCount = countOf("Node");');
|
|
524
|
+
expect(counts).toContain('return r.kind === "Node" && r.isReady === true;');
|
|
525
|
+
expect(counts).toContain('extras.serviceCount = countOf("Service");');
|
|
526
|
+
expect(counts).toContain('extras.taskCount = countOf("Task");');
|
|
527
|
+
expect(counts).toContain('return r.kind === "Task" && r.isReady === true;');
|
|
528
|
+
|
|
529
|
+
for (const text of [
|
|
530
|
+
T.clusterListNodes,
|
|
531
|
+
T.clusterListServices,
|
|
532
|
+
T.clusterListTasks,
|
|
533
|
+
]) {
|
|
534
|
+
expect(text).toContain("latest inventory snapshot");
|
|
535
|
+
// Rewritten from each snapshot, so nothing lingers until pruning.
|
|
536
|
+
expect(text).not.toContain("25 minutes");
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("a change in any count is written at once, not held back by the heartbeat throttle", () => {
|
|
541
|
+
for (const field of [
|
|
542
|
+
"nodeCount",
|
|
543
|
+
"readyNodeCount",
|
|
544
|
+
"serviceCount",
|
|
545
|
+
"taskCount",
|
|
546
|
+
"runningTaskCount",
|
|
547
|
+
]) {
|
|
548
|
+
expect(CLUSTER_SERVICE).toContain(`${field}: extra?.${field} ?? null,`);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("Nodes: ready out of total, where ready is the state the managers report", () => {
|
|
553
|
+
const readyNode: ReturnType<typeof extractDockerSwarmInventoryResource> =
|
|
554
|
+
extractDockerSwarmInventoryResource({
|
|
555
|
+
kind: "Node",
|
|
556
|
+
logBody: JSON.stringify({
|
|
557
|
+
data: { ID: "n1", Hostname: "a", Status: "ready", ManagerStatus: "" },
|
|
558
|
+
}),
|
|
559
|
+
lastSeenAt: new Date(),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
expect(readyNode!.resource.isReady).toBe(true);
|
|
563
|
+
expect(T.clusterListNodes).toContain("shown as ready out of total");
|
|
564
|
+
expect(T.clusterListNodes).toContain("the managers see the node as up");
|
|
565
|
+
expect(T.clusterListNodes).toContain(
|
|
566
|
+
"turns red when any node is not ready",
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("Services: every service in the snapshot, converged or not", () => {
|
|
571
|
+
expect(T.clusterListServices).toContain("whatever their state");
|
|
572
|
+
expect(T.clusterListServices).toContain("including ones scaled to 0");
|
|
573
|
+
// Converged is on the overview, not on this list.
|
|
574
|
+
expect(T.clusterListServices).toContain("overview");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("Tasks: running out of the tasks Swarm wants running", () => {
|
|
578
|
+
expect(AGENT_SNAPSHOT_SCRIPT).toContain(
|
|
579
|
+
'select(.DesiredState == "running")',
|
|
580
|
+
);
|
|
581
|
+
expect(taskIsRunning("running")).toBe(true);
|
|
582
|
+
expect(taskIsRunning("starting")).toBe(false);
|
|
583
|
+
expect(taskIsRunning("failed")).toBe(false);
|
|
584
|
+
|
|
585
|
+
expect(T.clusterListTasks).toMatch(TASKS_WANTED_FIRST);
|
|
586
|
+
expect(T.clusterListTasks).toContain("one task is one container");
|
|
587
|
+
expect(T.clusterListTasks).toContain("how many are actually running");
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("CPU and memory texts match what the rows actually carry", () => {
|
|
592
|
+
const flush: string = between(
|
|
593
|
+
METRICS_INGEST,
|
|
594
|
+
"private static async flushDockerSwarmTaskMetrics",
|
|
595
|
+
"private static bufferDockerSnapshotMetric",
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
test("ingest mirrors CPU/memory onto Task rows only, so Nodes and Services always read N/A", () => {
|
|
599
|
+
expect(flush).toContain('kind: "Task"');
|
|
600
|
+
expect(flush).not.toMatch(/kind: "(Node|Service)"/);
|
|
601
|
+
|
|
602
|
+
for (const text of [T.nodeUsageColumns, T.serviceUsageColumns]) {
|
|
603
|
+
expect(text).toContain("collected per task container");
|
|
604
|
+
expect(text).toContain("show N/A");
|
|
605
|
+
expect(text).toContain("Tasks list");
|
|
606
|
+
}
|
|
607
|
+
expect(T.nodeUsageColumns).toContain("not per node");
|
|
608
|
+
expect(T.serviceUsageColumns).toContain("not per service");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("the Tasks list hides readings older than 15 minutes, and its texts say so", () => {
|
|
612
|
+
expect(METRIC_STALE_MS).toBe(15 * 60 * 1000);
|
|
613
|
+
|
|
614
|
+
const fresh: InfrastructureResource = toInfrastructureResource(taskRow(1));
|
|
615
|
+
const stale: InfrastructureResource = toInfrastructureResource(taskRow(16));
|
|
616
|
+
|
|
617
|
+
expect(fresh.cpuUtilization).toBe(12.5);
|
|
618
|
+
expect(fresh.memoryUsageBytes).toBe(64 * 1024 * 1024);
|
|
619
|
+
expect(stale.cpuUtilization).toBeNull();
|
|
620
|
+
expect(stale.memoryUsageBytes).toBeNull();
|
|
621
|
+
|
|
622
|
+
expect(T.taskCpuColumn).toContain("only when that reading is under 15");
|
|
623
|
+
expect(T.taskMemoryColumn).toContain("only when that reading is under 15");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("the task page applies no such cutoff, and its texts say that instead", () => {
|
|
627
|
+
const taskDetail: string = readRepoFile(
|
|
628
|
+
"packages",
|
|
629
|
+
"App",
|
|
630
|
+
"FeatureSet",
|
|
631
|
+
"Dashboard",
|
|
632
|
+
"src",
|
|
633
|
+
"Pages",
|
|
634
|
+
"DockerSwarm",
|
|
635
|
+
"View",
|
|
636
|
+
"TaskDetail.tsx",
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
expect(taskDetail).not.toContain("METRIC_STALE_MS");
|
|
640
|
+
expect(taskDetail).not.toContain("metricsUpdatedAt");
|
|
641
|
+
expect(T.taskCpu).toContain("more than 15 minutes old");
|
|
642
|
+
expect(T.taskMemory).toContain("more than 15 minutes old");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("docker_stats reads only the local daemon, so tasks on nodes without the agent have no reading", () => {
|
|
646
|
+
expect(AGENT_COLLECTOR_CONFIG).toContain(
|
|
647
|
+
"endpoint: unix:///var/run/docker.sock",
|
|
648
|
+
);
|
|
649
|
+
expect(AGENT_COLLECTOR_CONFIG).toContain("collection_interval: 30s");
|
|
650
|
+
expect(T.taskCpuColumn).toMatch(/nodes where the agent does not run/);
|
|
651
|
+
expect(T.taskMemoryColumn).toMatch(/nodes without the agent/);
|
|
652
|
+
expect(T.taskCpu).toContain("every 30 seconds");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("memory texts say file cache is left out (docker_stats usage.total excludes it)", () => {
|
|
656
|
+
for (const text of [T.taskMemory, T.taskMemoryColumn, CHART.taskMemory]) {
|
|
657
|
+
expect(text).toMatch(/file cache/);
|
|
658
|
+
/*
|
|
659
|
+
* docker_stats subtracts only INACTIVE file cache (inactive_file), the
|
|
660
|
+
* way docker stats does - the same metric and the same words as the
|
|
661
|
+
* Docker and Podman host pages. Recently used cache is still counted,
|
|
662
|
+
* so "cache the system can reclaim" would overstate what is left out.
|
|
663
|
+
*/
|
|
664
|
+
expect(text).toContain("file cache the system has not used recently");
|
|
665
|
+
expect(text).not.toContain("reclaim");
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("task CPU names a scale only once ingest stops scaling the mirrored value by 100 a second time", () => {
|
|
670
|
+
const cpuToPercent: string = between(
|
|
671
|
+
METRICS_INGEST,
|
|
672
|
+
"private static cpuValueToPercent(",
|
|
673
|
+
"\n }\n",
|
|
674
|
+
);
|
|
675
|
+
const stillMultiplies: boolean = cpuToPercent.includes(
|
|
676
|
+
"return rawValue * 100;",
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
for (const text of [T.taskCpu, T.taskCpuColumn]) {
|
|
680
|
+
if (stillMultiplies) {
|
|
681
|
+
/*
|
|
682
|
+
* docker_stats already reports a percent (100 = one core) under
|
|
683
|
+
* unit "1", so the Task rows hold 100x that. Promising "100% is one
|
|
684
|
+
* core" would be false until the ingest is fixed.
|
|
685
|
+
*/
|
|
686
|
+
expect(text).not.toMatch(/core/i);
|
|
687
|
+
} else {
|
|
688
|
+
expect(text).toMatch(/100% is one full CPU core/);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe("Insights chart lines match their queries", () => {
|
|
695
|
+
type Spec = { title: string; aggregation: string; key: string };
|
|
696
|
+
|
|
697
|
+
const SPEC_PATTERN: RegExp =
|
|
698
|
+
/title: "([^"]+)",\s*description:\s*DOCKER_SWARM_INSIGHTS_CHART_DESCRIPTIONS\.(\w+),[\s\S]*?aggregation: AggregationType\.(\w+)/g;
|
|
699
|
+
|
|
700
|
+
const specs: Array<Spec> = Array.from(
|
|
701
|
+
INSIGHTS_PAGE.matchAll(SPEC_PATTERN),
|
|
702
|
+
).map((match: RegExpMatchArray): Spec => {
|
|
703
|
+
return { title: match[1]!, key: match[2]!, aggregation: match[3]! };
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("every chart on the page takes its line from the record, once each", () => {
|
|
707
|
+
expect(
|
|
708
|
+
specs
|
|
709
|
+
.map((spec: Spec) => {
|
|
710
|
+
return spec.key;
|
|
711
|
+
})
|
|
712
|
+
.sort(),
|
|
713
|
+
).toEqual(Object.keys(CHART).sort());
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("averaged charts say averaged; max charts say highest and that only the top 10 are drawn by default", () => {
|
|
717
|
+
for (const spec of specs) {
|
|
718
|
+
const text: string = CHART[spec.key as DockerSwarmInsightsChart];
|
|
719
|
+
|
|
720
|
+
if (spec.aggregation === "Avg") {
|
|
721
|
+
expect(text).toContain("averaged per interval");
|
|
722
|
+
expect(text).not.toMatch(/highest/i);
|
|
723
|
+
} else {
|
|
724
|
+
expect(spec.aggregation).toBe("Max");
|
|
725
|
+
expect(text).toMatch(/^The highest/);
|
|
726
|
+
expect(text).toContain(
|
|
727
|
+
"By default only the 10 containers that peaked highest are drawn",
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("no chart line claims every container is drawn - MetricView caps grouped charts at the top 10 by peak", () => {
|
|
734
|
+
/*
|
|
735
|
+
* EmbeddedMetricCard renders MetricView, which opts in to the default
|
|
736
|
+
* server-side Top-N (10 series, ranked by each series' max) and shows a
|
|
737
|
+
* "Showing top k of N" banner with a Show all control.
|
|
738
|
+
*/
|
|
739
|
+
const metricsDir: Array<string> = [
|
|
740
|
+
"packages",
|
|
741
|
+
"App",
|
|
742
|
+
"FeatureSet",
|
|
743
|
+
"Dashboard",
|
|
744
|
+
"src",
|
|
745
|
+
"Components",
|
|
746
|
+
"Metrics",
|
|
747
|
+
];
|
|
748
|
+
const metricUtils: string = readRepoFile(
|
|
749
|
+
...metricsDir,
|
|
750
|
+
"Utils",
|
|
751
|
+
"Metrics.ts",
|
|
752
|
+
);
|
|
753
|
+
const metricView: string = readRepoFile(...metricsDir, "MetricView.tsx");
|
|
754
|
+
const embedded: string = readRepoFile(
|
|
755
|
+
...metricsDir,
|
|
756
|
+
"EmbeddedMetricCard.tsx",
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
expect(INSIGHTS_PAGE).toContain("<EmbeddedMetricCard");
|
|
760
|
+
expect(embedded).toContain("<MetricView");
|
|
761
|
+
expect(metricView).toContain("defaultTopN: true");
|
|
762
|
+
expect(metricUtils).toContain(
|
|
763
|
+
"export const DEFAULT_TOP_N_SERIES: number = 10;",
|
|
764
|
+
);
|
|
765
|
+
expect(metricUtils).toMatch(RANKED_BY_MAX);
|
|
766
|
+
|
|
767
|
+
for (const text of Object.values(CHART)) {
|
|
768
|
+
expect(text).not.toMatch(EVERY_CONTAINER_CLAIM);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("CPU charts read docker_stats' own scale, where 100% is one core, and say so", () => {
|
|
773
|
+
expect(INSIGHTS_PAGE).not.toContain("host-CPU");
|
|
774
|
+
expect(CHART.clusterCpu).toMatch(/^100% is one full CPU core/);
|
|
775
|
+
expect(CHART.clusterCpu).toContain("can go above 100%");
|
|
776
|
+
// The same words as the Docker and Podman host pages, which read the same metric.
|
|
777
|
+
expect(CHART.topTasksCpu).toContain("100% is one full CPU core");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("the memory percent line does not promise a limit the service may not set", () => {
|
|
781
|
+
expect(CHART.clusterMemoryPercent).toContain("memory limit");
|
|
782
|
+
expect(CHART.clusterMemoryPercent).toContain(
|
|
783
|
+
"node's total memory when the service sets no limit",
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test("the process line counts threads too (the pids controller counts both)", () => {
|
|
788
|
+
expect(CHART.taskProcesses).toMatch(/^Processes and threads/);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("chart lines put the containers the agent can see, not the whole swarm", () => {
|
|
792
|
+
expect(CHART.clusterCpu).toContain(
|
|
793
|
+
"on the nodes where the OneUptime agent runs",
|
|
794
|
+
);
|
|
795
|
+
});
|
|
796
|
+
});
|