@oneuptime/common 12.0.0 → 12.0.2
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/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/LlmProviderService.ts +69 -9
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/RunnerService.ts +1 -7
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
- package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
- package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
- package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
- package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/Runbook/RunbookStep.ts +15 -0
- package/Types/Runner/RunnerLiveStatus.ts +114 -0
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/Detail/Detail.tsx +155 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +61 -8
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -6
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
- package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
- package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +97 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -229,11 +229,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
-
const
|
|
232
|
+
const liveness: any = {
|
|
233
233
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
234
234
|
otelCollectorStatus: "connected",
|
|
235
235
|
};
|
|
236
236
|
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
+
const data: any = { ...liveness };
|
|
239
|
+
|
|
237
240
|
if (extra?.cephVersion) {
|
|
238
241
|
data.cephVersion = extra.cephVersion;
|
|
239
242
|
}
|
|
@@ -273,11 +276,50 @@ export class Service extends DatabaseService<Model> {
|
|
|
273
276
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
274
277
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
275
278
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
279
|
+
*
|
|
280
|
+
* Collector-supplied strings are clamped to their declared column
|
|
281
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
282
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
276
283
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
284
|
+
try {
|
|
285
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
286
|
+
id: clusterId,
|
|
287
|
+
data: data,
|
|
288
|
+
});
|
|
289
|
+
} catch (err) {
|
|
290
|
+
/*
|
|
291
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
292
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
293
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
294
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
295
|
+
* markDisconnectedClusters flips a perfectly healthy Ceph cluster to
|
|
296
|
+
* "disconnected" 15 minutes later — with telemetry still arriving the
|
|
297
|
+
* whole time. That was issue #3006 on the Host path (a host.ip list
|
|
298
|
+
* longer than the then-varchar(500) column). Retry with liveness only
|
|
299
|
+
* so the enrichment is what gets dropped, never the heartbeat.
|
|
300
|
+
*/
|
|
301
|
+
logger.warn(
|
|
302
|
+
`CephClusterService.updateLastSeen: metadata write failed for Ceph cluster ${clusterId.toString()}, falling back to a liveness-only update: ${
|
|
303
|
+
err instanceof Error ? err.message : String(err)
|
|
304
|
+
}`,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
/*
|
|
308
|
+
* The throttle fingerprint was already stored, so without this the
|
|
309
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
310
|
+
* for the rest of the window.
|
|
311
|
+
*/
|
|
312
|
+
try {
|
|
313
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
314
|
+
} catch {
|
|
315
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
319
|
+
id: clusterId,
|
|
320
|
+
data: liveness,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
281
323
|
}
|
|
282
324
|
|
|
283
325
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/CephResource";
|
|
3
3
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import OneUptimeDate from "../../Types/Date";
|
|
7
|
+
import { truncateShortText } from "../Utils/Database/TruncateColumnValue";
|
|
6
8
|
import logger from "../Utils/Logger";
|
|
7
9
|
|
|
8
10
|
/*
|
|
@@ -86,6 +88,26 @@ const UPSERT_COLUMNS: Array<string> = [
|
|
|
86
88
|
"version",
|
|
87
89
|
];
|
|
88
90
|
|
|
91
|
+
/*
|
|
92
|
+
* CephResource's text columns are all ShortText (100 chars). The bulk
|
|
93
|
+
* paths below go through manager.query(), which skips the length
|
|
94
|
+
* validation DatabaseService applies to ordinary writes, so a single
|
|
95
|
+
* oversized value aborts the whole 500-row INSERT chunk it rides in.
|
|
96
|
+
* Clamp per value instead, so one pathological daemon can never drop
|
|
97
|
+
* the rest of the scrape.
|
|
98
|
+
*/
|
|
99
|
+
function sanitizeResource(r: ParsedCephResource): ParsedCephResource {
|
|
100
|
+
return {
|
|
101
|
+
...r,
|
|
102
|
+
kind: truncateShortText(r.kind),
|
|
103
|
+
externalId: truncateShortText(r.externalId),
|
|
104
|
+
name: truncateShortText(r.name),
|
|
105
|
+
hostname: truncateShortText(r.hostname),
|
|
106
|
+
daemonVersion: truncateShortText(r.daemonVersion),
|
|
107
|
+
deviceClass: truncateShortText(r.deviceClass),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
export class Service extends DatabaseService<Model> {
|
|
90
112
|
public constructor() {
|
|
91
113
|
super(Model);
|
|
@@ -112,9 +134,21 @@ export class Service extends DatabaseService<Model> {
|
|
|
112
134
|
return;
|
|
113
135
|
}
|
|
114
136
|
|
|
137
|
+
const resources: Array<ParsedCephResource> = data.resources.map(
|
|
138
|
+
(r: ParsedCephResource) => {
|
|
139
|
+
const sanitized: ParsedCephResource = sanitizeResource(r);
|
|
140
|
+
if (sanitized.externalId !== r.externalId) {
|
|
141
|
+
logger.warn(
|
|
142
|
+
`CephResource externalId exceeds ${ColumnLength.ShortText} chars; truncated to "${sanitized.externalId}" (cluster ${data.cephClusterId.toString()}).`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return sanitized;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
115
149
|
// Chunk to keep individual statement parameter counts reasonable.
|
|
116
|
-
for (let i: number = 0; i <
|
|
117
|
-
const chunk: Array<ParsedCephResource> =
|
|
150
|
+
for (let i: number = 0; i < resources.length; i += UPSERT_BATCH_SIZE) {
|
|
151
|
+
const chunk: Array<ParsedCephResource> = resources.slice(
|
|
118
152
|
i,
|
|
119
153
|
i + UPSERT_BATCH_SIZE,
|
|
120
154
|
);
|
|
@@ -218,9 +252,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
218
252
|
valueFragments.push(
|
|
219
253
|
`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::numeric, $${paramIndex++}::numeric, $${paramIndex++}::integer, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::timestamptz)`,
|
|
220
254
|
);
|
|
255
|
+
/*
|
|
256
|
+
* The identity clamps MUST match bulkUpsert's, or this mirror
|
|
257
|
+
* UPDATE would silently miss the row the upsert just wrote
|
|
258
|
+
* under the truncated externalId.
|
|
259
|
+
*/
|
|
221
260
|
params.push(
|
|
222
|
-
m.kind,
|
|
223
|
-
m.externalId,
|
|
261
|
+
truncateShortText(m.kind),
|
|
262
|
+
truncateShortText(m.externalId),
|
|
224
263
|
bigintOrNull(m.statBytes),
|
|
225
264
|
bigintOrNull(m.statBytesUsed),
|
|
226
265
|
m.applyLatencyMs !== null && m.applyLatencyMs !== undefined
|
|
@@ -199,11 +199,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
-
const
|
|
202
|
+
const liveness: any = {
|
|
203
203
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
204
204
|
otelCollectorStatus: "connected",
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
const data: any = { ...liveness };
|
|
209
|
+
|
|
207
210
|
if (extra?.agentVersion) {
|
|
208
211
|
data.agentVersion = extra.agentVersion;
|
|
209
212
|
}
|
|
@@ -230,11 +233,50 @@ export class Service extends DatabaseService<Model> {
|
|
|
230
233
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
231
234
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
232
235
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
236
|
+
*
|
|
237
|
+
* Collector-supplied strings are clamped to their declared column
|
|
238
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
239
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
233
240
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
try {
|
|
242
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
243
|
+
id: cloudResourceId,
|
|
244
|
+
data: data,
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
/*
|
|
248
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
249
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
250
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
251
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
252
|
+
* markDisconnectedResources flips a perfectly healthy cloud resource
|
|
253
|
+
* to "disconnected" 15 minutes later — with telemetry still arriving
|
|
254
|
+
* the whole time. That was issue #3006 on the Host path (a host.ip
|
|
255
|
+
* list longer than the then-varchar(500) column). Retry with liveness
|
|
256
|
+
* only so the enrichment is what gets dropped, never the heartbeat.
|
|
257
|
+
*/
|
|
258
|
+
logger.warn(
|
|
259
|
+
`CloudResourceService.updateLastSeen: metadata write failed for cloud resource ${cloudResourceId.toString()}, falling back to a liveness-only update: ${
|
|
260
|
+
err instanceof Error ? err.message : String(err)
|
|
261
|
+
}`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
/*
|
|
265
|
+
* The throttle fingerprint was already stored, so without this the
|
|
266
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
267
|
+
* for the rest of the window.
|
|
268
|
+
*/
|
|
269
|
+
try {
|
|
270
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
271
|
+
} catch {
|
|
272
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
276
|
+
id: cloudResourceId,
|
|
277
|
+
data: liveness,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
@CaptureSpan()
|
|
@@ -1046,6 +1046,61 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
1046
1046
|
return data;
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
+
/*
|
|
1050
|
+
* Clamp string values to the max length their column declares, instead of
|
|
1051
|
+
* rejecting the whole write the way checkMaxLengthOfFields does.
|
|
1052
|
+
*
|
|
1053
|
+
* For machine-stamped metadata harvested from OpenTelemetry resource
|
|
1054
|
+
* attributes there is no user to show a validation error to, and the
|
|
1055
|
+
* write these values ride along with also carries liveness columns
|
|
1056
|
+
* (lastSeenAt / otelCollectorStatus). Letting one oversized optional
|
|
1057
|
+
* attribute abort that statement strands a healthy resource as
|
|
1058
|
+
* "disconnected" — the failure mode in issue #3006. A clipped display
|
|
1059
|
+
* string is strictly better than a lost heartbeat.
|
|
1060
|
+
*
|
|
1061
|
+
* Only string values on columns with a declared max length are touched;
|
|
1062
|
+
* `text` columns (VeryLongText and friends) declare none and pass
|
|
1063
|
+
* through untouched, as do numbers, dates and ObjectIDs.
|
|
1064
|
+
*
|
|
1065
|
+
* The raw, hook-free write paths (updateColumnsByIdWithoutHooks and
|
|
1066
|
+
* atomicAddToColumnsByIdWithoutHooks) call this themselves, so callers do
|
|
1067
|
+
* NOT need to — and must not rely on being able to skip it by writing a
|
|
1068
|
+
* new raw path of their own. It is NOT a substitute for sizing a column
|
|
1069
|
+
* correctly: if a field is legitimately long, widen the column.
|
|
1070
|
+
*/
|
|
1071
|
+
public truncateStringColumnsToMaxLength(
|
|
1072
|
+
data: Record<string, unknown>,
|
|
1073
|
+
): Record<string, unknown> {
|
|
1074
|
+
for (const column of Object.keys(data)) {
|
|
1075
|
+
const value: unknown = data[column];
|
|
1076
|
+
|
|
1077
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/*
|
|
1082
|
+
* Returns undefined for an unmapped column name despite its type —
|
|
1083
|
+
* leave those alone and let the write path complain about them.
|
|
1084
|
+
*/
|
|
1085
|
+
const metadata: TableColumnMetadata | undefined =
|
|
1086
|
+
this.model.getTableColumnMetadata(column);
|
|
1087
|
+
|
|
1088
|
+
if (!metadata?.type) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const maxLength: number | undefined = getMaxLengthFromTableColumnType(
|
|
1093
|
+
metadata.type,
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
if (maxLength !== undefined && value.length > maxLength) {
|
|
1097
|
+
data[column] = value.substring(0, maxLength);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return data;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1049
1104
|
private async checkTotalItemsBy(
|
|
1050
1105
|
createdBy: CreateBy<TBaseModel>,
|
|
1051
1106
|
): Promise<void> {
|
|
@@ -2248,6 +2303,17 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
2248
2303
|
* the caller), and every value is bound as a parameter, so this is not an
|
|
2249
2304
|
* injection surface. Use ONLY for trusted, internal, side-effect-free
|
|
2250
2305
|
* column writes where the full pipeline is pure overhead.
|
|
2306
|
+
*
|
|
2307
|
+
* Oversized strings are CLAMPED here (truncateStringColumnsToMaxLength)
|
|
2308
|
+
* rather than rejected. Skipping the hooks also skips
|
|
2309
|
+
* checkMaxLengthOfFields, so before this an over-long value reached
|
|
2310
|
+
* Postgres raw and failed the ENTIRE statement — including whatever else
|
|
2311
|
+
* rode along with it. On the telemetry liveness writes that is fatal: one
|
|
2312
|
+
* over-long OpenTelemetry resource attribute took `lastSeenAt` down with
|
|
2313
|
+
* it and markDisconnected* stranded a healthy resource as "disconnected"
|
|
2314
|
+
* 15 minutes later while its data kept arriving (issue #3006). Clamping
|
|
2315
|
+
* in here, at the choke point, is what makes that unforgettable — every
|
|
2316
|
+
* caller of this path gets it, including ones not written yet.
|
|
2251
2317
|
*/
|
|
2252
2318
|
@CaptureSpan()
|
|
2253
2319
|
public async updateColumnsByIdWithoutHooks(input: {
|
|
@@ -2273,9 +2339,16 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
2273
2339
|
const setClauses: Array<string> = [];
|
|
2274
2340
|
const params: Array<unknown> = [];
|
|
2275
2341
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2342
|
+
/*
|
|
2343
|
+
* Clamp on a shallow COPY: callers must not have their own object
|
|
2344
|
+
* mutated behind their back (a liveness-only retry, for instance,
|
|
2345
|
+
* rebuilds its payload from the same source values).
|
|
2346
|
+
*/
|
|
2347
|
+
const data: ObjectLiteral = this.truncateStringColumnsToMaxLength({
|
|
2348
|
+
...(input.data as ObjectLiteral),
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
for (const [propertyName, value] of Object.entries(data)) {
|
|
2279
2352
|
const column: ColumnMetadata | undefined =
|
|
2280
2353
|
metadata.findColumnWithPropertyName(propertyName);
|
|
2281
2354
|
if (!column) {
|
|
@@ -2347,6 +2420,11 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
2347
2420
|
* caller) and every value is bound as a parameter, so this is not an
|
|
2348
2421
|
* injection surface. Use ONLY for trusted, internal, side-effect-free
|
|
2349
2422
|
* column writes.
|
|
2423
|
+
*
|
|
2424
|
+
* `set` values are clamped to their column widths for the same reason
|
|
2425
|
+
* updateColumnsByIdWithoutHooks clamps: this path also skips
|
|
2426
|
+
* checkMaxLengthOfFields, and an over-long string here would abort the
|
|
2427
|
+
* counter increments riding in the same statement.
|
|
2350
2428
|
*/
|
|
2351
2429
|
@CaptureSpan()
|
|
2352
2430
|
public async atomicAddToColumnsByIdWithoutHooks(input: {
|
|
@@ -2393,9 +2471,12 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
2393
2471
|
setClauses.push(`${quoted} = COALESCE(${quoted}, 0) + $${params.length}`);
|
|
2394
2472
|
}
|
|
2395
2473
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2474
|
+
// Shallow copy — never mutate the caller's object. See the clamp note above.
|
|
2475
|
+
const set: ObjectLiteral = this.truncateStringColumnsToMaxLength({
|
|
2476
|
+
...((input.set || {}) as ObjectLiteral),
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
for (const [propertyName, value] of Object.entries(set)) {
|
|
2399
2480
|
if (typeof value === "function") {
|
|
2400
2481
|
throw new BadDataException(
|
|
2401
2482
|
`atomicAddToColumnsByIdWithoutHooks: SQL-expression values are not supported (column "${propertyName}"); pass a literal value.`,
|
|
@@ -221,11 +221,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
-
const
|
|
224
|
+
const liveness: any = {
|
|
225
225
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
226
226
|
otelCollectorStatus: "connected",
|
|
227
227
|
};
|
|
228
228
|
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
const data: any = { ...liveness };
|
|
231
|
+
|
|
229
232
|
if (extra?.osType) {
|
|
230
233
|
data.osType = extra.osType;
|
|
231
234
|
}
|
|
@@ -240,11 +243,50 @@ export class Service extends DatabaseService<Model> {
|
|
|
240
243
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
241
244
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
242
245
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
246
|
+
*
|
|
247
|
+
* Collector-supplied strings are clamped to their declared column
|
|
248
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
249
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
243
250
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
try {
|
|
252
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
253
|
+
id: hostId,
|
|
254
|
+
data: data,
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
/*
|
|
258
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
259
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
260
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
261
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
262
|
+
* markDisconnectedHosts flips a perfectly healthy Docker host to
|
|
263
|
+
* "disconnected" 15 minutes later — with telemetry still arriving the
|
|
264
|
+
* whole time. That was issue #3006 on the Host path (a host.ip list
|
|
265
|
+
* longer than the then-varchar(500) column). Retry with liveness only
|
|
266
|
+
* so the enrichment is what gets dropped, never the heartbeat.
|
|
267
|
+
*/
|
|
268
|
+
logger.warn(
|
|
269
|
+
`DockerHostService.updateLastSeen: metadata write failed for Docker host ${hostId.toString()}, falling back to a liveness-only update: ${
|
|
270
|
+
err instanceof Error ? err.message : String(err)
|
|
271
|
+
}`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* The throttle fingerprint was already stored, so without this the
|
|
276
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
277
|
+
* for the rest of the window.
|
|
278
|
+
*/
|
|
279
|
+
try {
|
|
280
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
281
|
+
} catch {
|
|
282
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
286
|
+
id: hostId,
|
|
287
|
+
data: liveness,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
248
290
|
}
|
|
249
291
|
|
|
250
292
|
/**
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/DockerResource";
|
|
3
3
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import OneUptimeDate from "../../Types/Date";
|
|
6
7
|
import { JSONObject } from "../../Types/JSON";
|
|
8
|
+
import {
|
|
9
|
+
truncateLongText,
|
|
10
|
+
truncateShortText,
|
|
11
|
+
} from "../Utils/Database/TruncateColumnValue";
|
|
7
12
|
import logger from "../Utils/Logger";
|
|
8
13
|
|
|
9
14
|
/*
|
|
@@ -56,6 +61,35 @@ export interface DockerHostInventoryCounts {
|
|
|
56
61
|
const UPSERT_BATCH_SIZE: number = 500;
|
|
57
62
|
const STALE_DELETE_WARN_THRESHOLD: number = 100;
|
|
58
63
|
|
|
64
|
+
/*
|
|
65
|
+
* DockerResource's text columns are bounded varchars — name / imageName
|
|
66
|
+
* at 500 chars, kind / containerId / state at 100. These bulk paths go
|
|
67
|
+
* through manager.query(), which skips the length validation
|
|
68
|
+
* DatabaseService applies to ordinary writes, so a single oversized
|
|
69
|
+
* value aborts the whole 500-row INSERT chunk it rides in. Clamp per
|
|
70
|
+
* value instead, so one pathological container can never drop the rest.
|
|
71
|
+
*/
|
|
72
|
+
function sanitizeContainer(c: ParsedDockerContainer): ParsedDockerContainer {
|
|
73
|
+
return {
|
|
74
|
+
...c,
|
|
75
|
+
containerName: truncateLongText(c.containerName),
|
|
76
|
+
containerId: truncateShortText(c.containerId),
|
|
77
|
+
imageName: truncateLongText(c.imageName),
|
|
78
|
+
state: truncateShortText(c.state),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sanitizeResource(r: ParsedDockerResource): ParsedDockerResource {
|
|
83
|
+
return {
|
|
84
|
+
...r,
|
|
85
|
+
kind: truncateShortText(r.kind),
|
|
86
|
+
name: truncateLongText(r.name),
|
|
87
|
+
containerId: truncateShortText(r.containerId),
|
|
88
|
+
imageName: truncateLongText(r.imageName),
|
|
89
|
+
state: truncateShortText(r.state),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
59
93
|
export class Service extends DatabaseService<Model> {
|
|
60
94
|
public constructor() {
|
|
61
95
|
super(Model);
|
|
@@ -83,12 +117,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
83
117
|
return;
|
|
84
118
|
}
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
const containers: Array<ParsedDockerContainer> = data.containers.map(
|
|
121
|
+
(c: ParsedDockerContainer) => {
|
|
122
|
+
const sanitized: ParsedDockerContainer = sanitizeContainer(c);
|
|
123
|
+
if (sanitized.containerName !== c.containerName) {
|
|
124
|
+
logger.warn(
|
|
125
|
+
`DockerResource container name exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.containerName}" (host ${data.dockerHostId.toString()}).`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return sanitized;
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
for (let i: number = 0; i < containers.length; i += UPSERT_BATCH_SIZE) {
|
|
133
|
+
const chunk: Array<ParsedDockerContainer> = containers.slice(
|
|
92
134
|
i,
|
|
93
135
|
i + UPSERT_BATCH_SIZE,
|
|
94
136
|
);
|
|
@@ -164,8 +206,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
164
206
|
return;
|
|
165
207
|
}
|
|
166
208
|
|
|
167
|
-
|
|
168
|
-
|
|
209
|
+
const resources: Array<ParsedDockerResource> = data.resources.map(
|
|
210
|
+
(r: ParsedDockerResource) => {
|
|
211
|
+
const sanitized: ParsedDockerResource = sanitizeResource(r);
|
|
212
|
+
if (sanitized.name !== r.name) {
|
|
213
|
+
logger.warn(
|
|
214
|
+
`DockerResource name exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.name}" (host ${data.dockerHostId.toString()}).`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return sanitized;
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
for (let i: number = 0; i < resources.length; i += UPSERT_BATCH_SIZE) {
|
|
222
|
+
const chunk: Array<ParsedDockerResource> = resources.slice(
|
|
169
223
|
i,
|
|
170
224
|
i + UPSERT_BATCH_SIZE,
|
|
171
225
|
);
|
|
@@ -231,11 +231,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
234
|
-
const
|
|
234
|
+
const liveness: any = {
|
|
235
235
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
236
236
|
otelCollectorStatus: "connected",
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
240
|
+
const data: any = { ...liveness };
|
|
241
|
+
|
|
239
242
|
if (extra?.dockerVersion) {
|
|
240
243
|
data.dockerVersion = extra.dockerVersion;
|
|
241
244
|
}
|
|
@@ -275,11 +278,51 @@ export class Service extends DatabaseService<Model> {
|
|
|
275
278
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
276
279
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
277
280
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
281
|
+
*
|
|
282
|
+
* Collector-supplied strings are clamped to their declared column
|
|
283
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
284
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
278
285
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
try {
|
|
287
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
288
|
+
id: clusterId,
|
|
289
|
+
data: data,
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
/*
|
|
293
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
294
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
295
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
296
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
297
|
+
* markDisconnectedClusters flips a perfectly healthy Docker Swarm
|
|
298
|
+
* cluster to "disconnected" 15 minutes later — with telemetry still
|
|
299
|
+
* arriving the whole time. That was issue #3006 on the Host path (a
|
|
300
|
+
* host.ip list longer than the then-varchar(500) column). Retry with
|
|
301
|
+
* liveness only so the enrichment is what gets dropped, never the
|
|
302
|
+
* heartbeat.
|
|
303
|
+
*/
|
|
304
|
+
logger.warn(
|
|
305
|
+
`DockerSwarmClusterService.updateLastSeen: metadata write failed for Docker Swarm cluster ${clusterId.toString()}, falling back to a liveness-only update: ${
|
|
306
|
+
err instanceof Error ? err.message : String(err)
|
|
307
|
+
}`,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
* The throttle fingerprint was already stored, so without this the
|
|
312
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
313
|
+
* for the rest of the window.
|
|
314
|
+
*/
|
|
315
|
+
try {
|
|
316
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
317
|
+
} catch {
|
|
318
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
322
|
+
id: clusterId,
|
|
323
|
+
data: liveness,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
283
326
|
}
|
|
284
327
|
|
|
285
328
|
/**
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/DockerSwarmResource";
|
|
3
3
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import OneUptimeDate from "../../Types/Date";
|
|
6
7
|
import { JSONObject } from "../../Types/JSON";
|
|
8
|
+
import {
|
|
9
|
+
truncateLongText,
|
|
10
|
+
truncateShortText,
|
|
11
|
+
} from "../Utils/Database/TruncateColumnValue";
|
|
7
12
|
import logger from "../Utils/Logger";
|
|
8
13
|
|
|
9
14
|
/*
|
|
@@ -95,6 +100,33 @@ const UPSERT_COLUMNS: Array<string> = [
|
|
|
95
100
|
"version",
|
|
96
101
|
];
|
|
97
102
|
|
|
103
|
+
/*
|
|
104
|
+
* DockerSwarmResource's text columns are bounded varchars — image at
|
|
105
|
+
* 500 chars, everything else at 100. The bulk paths below go through
|
|
106
|
+
* manager.query(), which skips the length validation DatabaseService
|
|
107
|
+
* applies to ordinary writes, so a single oversized value aborts the
|
|
108
|
+
* whole 500-row INSERT chunk it rides in. Clamp per value instead, so
|
|
109
|
+
* one pathological task can never drop the rest of the snapshot.
|
|
110
|
+
*/
|
|
111
|
+
function sanitizeResource(
|
|
112
|
+
r: ParsedDockerSwarmResource,
|
|
113
|
+
): ParsedDockerSwarmResource {
|
|
114
|
+
return {
|
|
115
|
+
...r,
|
|
116
|
+
kind: truncateShortText(r.kind),
|
|
117
|
+
externalId: truncateShortText(r.externalId),
|
|
118
|
+
name: truncateShortText(r.name),
|
|
119
|
+
state: truncateShortText(r.state),
|
|
120
|
+
role: truncateShortText(r.role),
|
|
121
|
+
serviceMode: truncateShortText(r.serviceMode),
|
|
122
|
+
image: truncateLongText(r.image),
|
|
123
|
+
stackName: truncateShortText(r.stackName),
|
|
124
|
+
serviceName: truncateShortText(r.serviceName),
|
|
125
|
+
nodeHostname: truncateShortText(r.nodeHostname),
|
|
126
|
+
driver: truncateShortText(r.driver),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
export class Service extends DatabaseService<Model> {
|
|
99
131
|
public constructor() {
|
|
100
132
|
super(Model);
|
|
@@ -120,8 +152,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
120
152
|
return;
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
const resources: Array<ParsedDockerSwarmResource> = data.resources.map(
|
|
156
|
+
(r: ParsedDockerSwarmResource) => {
|
|
157
|
+
const sanitized: ParsedDockerSwarmResource = sanitizeResource(r);
|
|
158
|
+
if (sanitized.externalId !== r.externalId) {
|
|
159
|
+
logger.warn(
|
|
160
|
+
`DockerSwarmResource externalId exceeds ${ColumnLength.ShortText} chars; truncated to "${sanitized.externalId}" (cluster ${data.dockerSwarmClusterId.toString()}).`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return sanitized;
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
for (let i: number = 0; i < resources.length; i += UPSERT_BATCH_SIZE) {
|
|
168
|
+
const chunk: Array<ParsedDockerSwarmResource> = resources.slice(
|
|
125
169
|
i,
|
|
126
170
|
i + UPSERT_BATCH_SIZE,
|
|
127
171
|
);
|
|
@@ -231,9 +275,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
231
275
|
valueFragments.push(
|
|
232
276
|
`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}::numeric, $${paramIndex++}::bigint, $${paramIndex++}::bigint, $${paramIndex++}::numeric, $${paramIndex++}::timestamptz)`,
|
|
233
277
|
);
|
|
278
|
+
/*
|
|
279
|
+
* The identity clamps MUST match bulkUpsert's, or this mirror
|
|
280
|
+
* UPDATE would silently miss the row the upsert just wrote
|
|
281
|
+
* under the truncated externalId.
|
|
282
|
+
*/
|
|
234
283
|
params.push(
|
|
235
|
-
m.kind,
|
|
236
|
-
m.externalId,
|
|
284
|
+
truncateShortText(m.kind),
|
|
285
|
+
truncateShortText(m.externalId),
|
|
237
286
|
m.cpuPercent !== null && m.cpuPercent !== undefined
|
|
238
287
|
? m.cpuPercent
|
|
239
288
|
: null,
|