@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
|
@@ -243,11 +243,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
246
|
-
const
|
|
246
|
+
const liveness: any = {
|
|
247
247
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
248
248
|
otelCollectorStatus: "connected",
|
|
249
249
|
};
|
|
250
250
|
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
252
|
+
const data: any = { ...liveness };
|
|
253
|
+
|
|
251
254
|
if (extra?.osType) {
|
|
252
255
|
data.osType = extra.osType;
|
|
253
256
|
}
|
|
@@ -313,11 +316,51 @@ export class Service extends DatabaseService<Model> {
|
|
|
313
316
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
314
317
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
315
318
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
319
|
+
*
|
|
320
|
+
* Every optional value above comes from an OpenTelemetry resource
|
|
321
|
+
* attribute, so its length is whatever the collector felt like sending.
|
|
322
|
+
* updateColumnsByIdWithoutHooks clamps them to their declared column
|
|
323
|
+
* widths for us — see truncateStringColumnsToMaxLength.
|
|
316
324
|
*/
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
try {
|
|
326
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
327
|
+
id: hostId,
|
|
328
|
+
data: data,
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
/*
|
|
332
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
333
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside a dozen optional,
|
|
334
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
335
|
+
* reject the statement, the host silently stops advancing
|
|
336
|
+
* lastSeenAt and MarkDisconnectedHosts flips a perfectly healthy
|
|
337
|
+
* host to "disconnected" 15 minutes later — with telemetry still
|
|
338
|
+
* arriving the whole time. That was issue #3006 (a host.ip list
|
|
339
|
+
* longer than the then-varchar(500) column). Retry with liveness
|
|
340
|
+
* only so the enrichment is what gets dropped, never the heartbeat.
|
|
341
|
+
*/
|
|
342
|
+
logger.warn(
|
|
343
|
+
`HostService.updateLastSeen: metadata write failed for host ${hostId.toString()}, falling back to a liveness-only update: ${
|
|
344
|
+
err instanceof Error ? err.message : String(err)
|
|
345
|
+
}`,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
* The throttle fingerprint was already stored, so without this the
|
|
350
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
351
|
+
* for the rest of the window.
|
|
352
|
+
*/
|
|
353
|
+
try {
|
|
354
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
355
|
+
} catch {
|
|
356
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
360
|
+
id: hostId,
|
|
361
|
+
data: liveness,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
321
364
|
}
|
|
322
365
|
|
|
323
366
|
private fingerprintExtras(extra?: {
|
|
@@ -5,6 +5,7 @@ import ColumnLength from "../../Types/Database/ColumnLength";
|
|
|
5
5
|
import ObjectID from "../../Types/ObjectID";
|
|
6
6
|
import OneUptimeDate from "../../Types/Date";
|
|
7
7
|
import { clampIoTTimestamp } from "../Utils/Telemetry/IoTSnapshotScan";
|
|
8
|
+
import { truncateShortText } from "../Utils/Database/TruncateColumnValue";
|
|
8
9
|
import logger from "../Utils/Logger";
|
|
9
10
|
|
|
10
11
|
/*
|
|
@@ -59,23 +60,6 @@ export interface IoTInventorySummary {
|
|
|
59
60
|
const UPSERT_BATCH_SIZE: number = 500;
|
|
60
61
|
const STALE_DELETE_WARN_THRESHOLD: number = 100;
|
|
61
62
|
|
|
62
|
-
/*
|
|
63
|
-
* The identity/text columns are ShortText (100 chars). A single value
|
|
64
|
-
* over the column limit fails the whole multi-row INSERT chunk it rides
|
|
65
|
-
* in — truncate per value instead so one malformed device can never
|
|
66
|
-
* drop the other rows of its chunk.
|
|
67
|
-
*/
|
|
68
|
-
function truncateShortText(value: string): string;
|
|
69
|
-
function truncateShortText(value: string | null): string | null;
|
|
70
|
-
function truncateShortText(value: string | null): string | null {
|
|
71
|
-
if (value === null) {
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
return value.length > ColumnLength.ShortText
|
|
75
|
-
? value.substring(0, ColumnLength.ShortText)
|
|
76
|
-
: value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
63
|
/*
|
|
80
64
|
* Column order used by bulkUpsert() and its generated parameter tuples.
|
|
81
65
|
* Keep this and the INSERT column list in perfect sync.
|
|
@@ -214,11 +214,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
|
-
const
|
|
217
|
+
const liveness: any = {
|
|
218
218
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
219
219
|
otelCollectorStatus: "connected",
|
|
220
220
|
};
|
|
221
221
|
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
|
+
const data: any = { ...liveness };
|
|
224
|
+
|
|
222
225
|
if (extra?.agentVersion) {
|
|
223
226
|
data.agentVersion = extra.agentVersion;
|
|
224
227
|
}
|
|
@@ -234,11 +237,50 @@ export class Service extends DatabaseService<Model> {
|
|
|
234
237
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
235
238
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
236
239
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
240
|
+
*
|
|
241
|
+
* Collector-supplied strings are clamped to their declared column
|
|
242
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
243
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
237
244
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
245
|
+
try {
|
|
246
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
247
|
+
id: fleetId,
|
|
248
|
+
data: data,
|
|
249
|
+
});
|
|
250
|
+
} catch (err) {
|
|
251
|
+
/*
|
|
252
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
253
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
254
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
255
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
256
|
+
* markDisconnectedFleets flips a perfectly healthy IoT fleet to
|
|
257
|
+
* "disconnected" 15 minutes later — with telemetry still arriving the
|
|
258
|
+
* whole time. That was issue #3006 on the Host path (a host.ip list
|
|
259
|
+
* longer than the then-varchar(500) column). Retry with liveness only
|
|
260
|
+
* so the enrichment is what gets dropped, never the heartbeat.
|
|
261
|
+
*/
|
|
262
|
+
logger.warn(
|
|
263
|
+
`IoTFleetService.updateLastSeen: metadata write failed for IoT fleet ${fleetId.toString()}, falling back to a liveness-only update: ${
|
|
264
|
+
err instanceof Error ? err.message : String(err)
|
|
265
|
+
}`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* The throttle fingerprint was already stored, so without this the
|
|
270
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
271
|
+
* for the rest of the window.
|
|
272
|
+
*/
|
|
273
|
+
try {
|
|
274
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
275
|
+
} catch {
|
|
276
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
280
|
+
id: fleetId,
|
|
281
|
+
data: liveness,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
242
284
|
}
|
|
243
285
|
|
|
244
286
|
/**
|
|
@@ -184,11 +184,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
-
const
|
|
187
|
+
const liveness: any = {
|
|
188
188
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
189
189
|
otelCollectorStatus: "connected",
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
+
const data: any = { ...liveness };
|
|
194
|
+
|
|
192
195
|
if (extra?.agentVersion) {
|
|
193
196
|
data.agentVersion = extra.agentVersion;
|
|
194
197
|
}
|
|
@@ -197,11 +200,51 @@ export class Service extends DatabaseService<Model> {
|
|
|
197
200
|
* Heartbeat write: a single-statement UPDATE with no hooks and no
|
|
198
201
|
* `version` bump, avoiding the hot-row Postgres lock convoy that the
|
|
199
202
|
* full updateOneById pipeline causes. See ServiceService.updateLastSeen.
|
|
203
|
+
*
|
|
204
|
+
* Collector-supplied strings are clamped to their declared column
|
|
205
|
+
* widths inside updateColumnsByIdWithoutHooks — see
|
|
206
|
+
* DatabaseService.truncateStringColumnsToMaxLength.
|
|
200
207
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
try {
|
|
209
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
210
|
+
id: clusterId,
|
|
211
|
+
data: data,
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
/*
|
|
215
|
+
* Liveness must survive bad metadata. The enriched write carries
|
|
216
|
+
* `lastSeenAt` and `otelCollectorStatus` alongside optional,
|
|
217
|
+
* collector-supplied columns; if any one of them makes Postgres
|
|
218
|
+
* reject the statement, lastSeenAt silently stops advancing and
|
|
219
|
+
* markDisconnectedClusters flips a perfectly healthy Kubernetes
|
|
220
|
+
* cluster to "disconnected" 15 minutes later — with telemetry still
|
|
221
|
+
* arriving the whole time. That was issue #3006 on the Host path (a
|
|
222
|
+
* host.ip list longer than the then-varchar(500) column). Retry with
|
|
223
|
+
* liveness only so the enrichment is what gets dropped, never the
|
|
224
|
+
* heartbeat.
|
|
225
|
+
*/
|
|
226
|
+
logger.warn(
|
|
227
|
+
`KubernetesClusterService.updateLastSeen: metadata write failed for Kubernetes cluster ${clusterId.toString()}, falling back to a liveness-only update: ${
|
|
228
|
+
err instanceof Error ? err.message : String(err)
|
|
229
|
+
}`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* The throttle fingerprint was already stored, so without this the
|
|
234
|
+
* next batch would short-circuit on a cache hit and skip the retry
|
|
235
|
+
* for the rest of the window.
|
|
236
|
+
*/
|
|
237
|
+
try {
|
|
238
|
+
await GlobalCache.deleteKey(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
239
|
+
} catch {
|
|
240
|
+
// Best-effort; the fallback write below is what actually matters.
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await this.updateColumnsByIdWithoutHooks({
|
|
244
|
+
id: clusterId,
|
|
245
|
+
data: liveness,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
205
248
|
}
|
|
206
249
|
|
|
207
250
|
/**
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/KubernetesContainer";
|
|
3
3
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
6
|
+
import {
|
|
7
|
+
truncateLongText,
|
|
8
|
+
truncateShortText,
|
|
9
|
+
} from "../Utils/Database/TruncateColumnValue";
|
|
5
10
|
import logger from "../Utils/Logger";
|
|
6
11
|
|
|
7
12
|
/*
|
|
@@ -61,6 +66,29 @@ const UPSERT_COLUMNS: Array<string> = [
|
|
|
61
66
|
"version",
|
|
62
67
|
];
|
|
63
68
|
|
|
69
|
+
/*
|
|
70
|
+
* KubernetesContainer's text columns are bounded varchars — podName and
|
|
71
|
+
* image at 500 chars, podNamespaceKey / name / state / reason at 100.
|
|
72
|
+
* The bulk paths below go through manager.query(), which skips the
|
|
73
|
+
* length validation DatabaseService applies to ordinary writes, so a
|
|
74
|
+
* single oversized value aborts the whole 500-row INSERT chunk it rides
|
|
75
|
+
* in. Clamp per value instead, so one pathological pod can never drop
|
|
76
|
+
* the rest of the snapshot.
|
|
77
|
+
*/
|
|
78
|
+
function sanitizeContainer(
|
|
79
|
+
c: ParsedKubernetesContainer,
|
|
80
|
+
): ParsedKubernetesContainer {
|
|
81
|
+
return {
|
|
82
|
+
...c,
|
|
83
|
+
podNamespaceKey: truncateShortText(c.podNamespaceKey),
|
|
84
|
+
podName: truncateLongText(c.podName),
|
|
85
|
+
name: truncateShortText(c.name),
|
|
86
|
+
image: truncateLongText(c.image),
|
|
87
|
+
state: truncateShortText(c.state),
|
|
88
|
+
reason: truncateShortText(c.reason),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
export class Service extends DatabaseService<Model> {
|
|
65
93
|
public constructor() {
|
|
66
94
|
super(Model);
|
|
@@ -86,12 +114,20 @@ export class Service extends DatabaseService<Model> {
|
|
|
86
114
|
return;
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
const containers: Array<ParsedKubernetesContainer> = data.containers.map(
|
|
118
|
+
(c: ParsedKubernetesContainer) => {
|
|
119
|
+
const sanitized: ParsedKubernetesContainer = sanitizeContainer(c);
|
|
120
|
+
if (sanitized.podName !== c.podName) {
|
|
121
|
+
logger.warn(
|
|
122
|
+
`KubernetesContainer podName exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.podName}" (cluster ${data.kubernetesClusterId.toString()}).`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return sanitized;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
for (let i: number = 0; i < containers.length; i += UPSERT_BATCH_SIZE) {
|
|
130
|
+
const chunk: Array<ParsedKubernetesContainer> = containers.slice(
|
|
95
131
|
i,
|
|
96
132
|
i + UPSERT_BATCH_SIZE,
|
|
97
133
|
);
|
|
@@ -224,10 +260,15 @@ export class Service extends DatabaseService<Model> {
|
|
|
224
260
|
valueFragments.push(
|
|
225
261
|
`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}::numeric, $${paramIndex++}::bigint, $${paramIndex++}::timestamptz)`,
|
|
226
262
|
);
|
|
263
|
+
/*
|
|
264
|
+
* The identity clamps MUST match bulkUpsert's, or this mirror
|
|
265
|
+
* UPDATE would silently miss the row the upsert just wrote
|
|
266
|
+
* under the truncated podName.
|
|
267
|
+
*/
|
|
227
268
|
params.push(
|
|
228
|
-
m.podNamespaceKey,
|
|
229
|
-
m.podName,
|
|
230
|
-
m.name,
|
|
269
|
+
truncateShortText(m.podNamespaceKey),
|
|
270
|
+
truncateLongText(m.podName),
|
|
271
|
+
truncateShortText(m.name),
|
|
231
272
|
m.cpuPercent !== null && m.cpuPercent !== undefined
|
|
232
273
|
? m.cpuPercent
|
|
233
274
|
: null,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/KubernetesResource";
|
|
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 { ParsedKubernetesResource } from "../../Types/Kubernetes/KubernetesInventoryExtractor";
|
|
8
|
+
import {
|
|
9
|
+
truncateLongText,
|
|
10
|
+
truncateShortText,
|
|
11
|
+
} from "../Utils/Database/TruncateColumnValue";
|
|
7
12
|
import logger from "../Utils/Logger";
|
|
8
13
|
|
|
9
14
|
/*
|
|
@@ -317,6 +322,28 @@ const UPSERT_COLUMNS: Array<keyof ParsedKubernetesResource | string> = [
|
|
|
317
322
|
"version",
|
|
318
323
|
];
|
|
319
324
|
|
|
325
|
+
/*
|
|
326
|
+
* KubernetesResource's text columns are bounded varchars — name and
|
|
327
|
+
* controllerDeploymentName at 500 chars, kind / namespaceKey / uid /
|
|
328
|
+
* phase at 100. The bulk paths below go through manager.query(), which
|
|
329
|
+
* skips the length validation DatabaseService applies to ordinary
|
|
330
|
+
* writes, so a single oversized value aborts the whole 500-row INSERT
|
|
331
|
+
* chunk it rides in. Clamp per value instead, so one pathological
|
|
332
|
+
* object can never drop the rest of the snapshot.
|
|
333
|
+
*/
|
|
334
|
+
function sanitizeResource(
|
|
335
|
+
r: ParsedKubernetesResource,
|
|
336
|
+
): ParsedKubernetesResource {
|
|
337
|
+
return {
|
|
338
|
+
...r,
|
|
339
|
+
kind: truncateShortText(r.kind),
|
|
340
|
+
namespaceKey: truncateShortText(r.namespaceKey),
|
|
341
|
+
name: truncateLongText(r.name),
|
|
342
|
+
uid: truncateShortText(r.uid),
|
|
343
|
+
phase: truncateShortText(r.phase),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
320
347
|
export class Service extends DatabaseService<Model> {
|
|
321
348
|
public constructor() {
|
|
322
349
|
super(Model);
|
|
@@ -338,9 +365,21 @@ export class Service extends DatabaseService<Model> {
|
|
|
338
365
|
return;
|
|
339
366
|
}
|
|
340
367
|
|
|
368
|
+
const resources: Array<ParsedKubernetesResource> = data.resources.map(
|
|
369
|
+
(r: ParsedKubernetesResource) => {
|
|
370
|
+
const sanitized: ParsedKubernetesResource = sanitizeResource(r);
|
|
371
|
+
if (sanitized.name !== r.name) {
|
|
372
|
+
logger.warn(
|
|
373
|
+
`KubernetesResource name exceeds ${ColumnLength.LongText} chars; truncated to "${sanitized.name}" (cluster ${data.kubernetesClusterId.toString()}).`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return sanitized;
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
|
|
341
380
|
// Chunk to keep individual statement parameter counts reasonable.
|
|
342
|
-
for (let i: number = 0; i <
|
|
343
|
-
const chunk: Array<ParsedKubernetesResource> =
|
|
381
|
+
for (let i: number = 0; i < resources.length; i += UPSERT_BATCH_SIZE) {
|
|
382
|
+
const chunk: Array<ParsedKubernetesResource> = resources.slice(
|
|
344
383
|
i,
|
|
345
384
|
i + UPSERT_BATCH_SIZE,
|
|
346
385
|
);
|
|
@@ -450,10 +489,17 @@ export class Service extends DatabaseService<Model> {
|
|
|
450
489
|
valueFragments.push(
|
|
451
490
|
`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}::numeric, $${paramIndex++}::bigint, $${paramIndex++}::numeric, $${paramIndex++}::timestamptz, $${paramIndex++}, $${paramIndex++})`,
|
|
452
491
|
);
|
|
492
|
+
/*
|
|
493
|
+
* The identity clamps MUST match bulkUpsert's, or this mirror
|
|
494
|
+
* UPDATE would silently miss the row the upsert just wrote
|
|
495
|
+
* under the truncated name. controllerDeploymentName /
|
|
496
|
+
* controllerCronJobName are assigned columns, so an oversized
|
|
497
|
+
* value there aborts the chunk exactly like the INSERT path.
|
|
498
|
+
*/
|
|
453
499
|
params.push(
|
|
454
|
-
m.kind,
|
|
455
|
-
m.namespaceKey,
|
|
456
|
-
m.name,
|
|
500
|
+
truncateShortText(m.kind),
|
|
501
|
+
truncateShortText(m.namespaceKey),
|
|
502
|
+
truncateLongText(m.name),
|
|
457
503
|
m.cpuPercent !== null && m.cpuPercent !== undefined
|
|
458
504
|
? m.cpuPercent
|
|
459
505
|
: null,
|
|
@@ -464,8 +510,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
464
510
|
? m.memoryPercent
|
|
465
511
|
: null,
|
|
466
512
|
m.observedAt,
|
|
467
|
-
m.controllerDeploymentName ?? null,
|
|
468
|
-
m.controllerCronJobName ?? null,
|
|
513
|
+
truncateLongText(m.controllerDeploymentName ?? null),
|
|
514
|
+
truncateShortText(m.controllerCronJobName ?? null),
|
|
469
515
|
);
|
|
470
516
|
}
|
|
471
517
|
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import CreateBy from "../Types/Database/CreateBy";
|
|
2
|
-
import
|
|
2
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
3
|
+
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
|
3
4
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
4
5
|
import DatabaseService from "./DatabaseService";
|
|
6
|
+
import ServiceLevelObjectiveMonitorRuleEngineService from "./ServiceLevelObjectiveMonitorRuleEngineService";
|
|
7
|
+
import ServiceLevelObjectiveService from "./ServiceLevelObjectiveService";
|
|
8
|
+
import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
|
|
5
9
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
10
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
6
11
|
import ObjectID from "../../Types/ObjectID";
|
|
7
12
|
import Color from "../../Types/Color";
|
|
8
13
|
import { BrightColors } from "../../Types/BrandColors";
|
|
9
14
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
10
15
|
import Model from "../../Models/DatabaseModels/Label";
|
|
11
16
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
17
|
+
import logger from "../Utils/Logger";
|
|
12
18
|
|
|
13
19
|
const AUTO_LABEL_ID_CACHE_NAMESPACE: string = "auto-label-id";
|
|
14
20
|
const AUTO_LABEL_ID_CACHE_TTL_SECONDS: number = 24 * 60 * 60;
|
|
@@ -57,6 +63,105 @@ export class Service extends DatabaseService<Model> {
|
|
|
57
63
|
return Promise.resolve({ createBy, carryForward: null });
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/*
|
|
67
|
+
* Deleting a label cascades its join rows away at the database level, so no
|
|
68
|
+
* service hook ever fires for the SLO label rules that referenced it. Those
|
|
69
|
+
* SLOs would keep the monitors the rule attached, with nothing left to
|
|
70
|
+
* explain why. Note the ids down while the label still exists, and re-run
|
|
71
|
+
* the (now smaller) rules once it is gone.
|
|
72
|
+
*/
|
|
73
|
+
@CaptureSpan()
|
|
74
|
+
protected override async onBeforeDelete(
|
|
75
|
+
deleteBy: DeleteBy<Model>,
|
|
76
|
+
): Promise<OnDelete<Model>> {
|
|
77
|
+
let serviceLevelObjectiveIds: Array<ObjectID> = [];
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const labels: Array<Model> = await this.findBy({
|
|
81
|
+
query: deleteBy.query,
|
|
82
|
+
select: {
|
|
83
|
+
_id: true,
|
|
84
|
+
},
|
|
85
|
+
limit: LIMIT_MAX,
|
|
86
|
+
skip: 0,
|
|
87
|
+
props: {
|
|
88
|
+
isRoot: true,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const labelIds: Array<ObjectID> = labels
|
|
93
|
+
.map((label: Model) => {
|
|
94
|
+
return label.id;
|
|
95
|
+
})
|
|
96
|
+
.filter((id: ObjectID | null): id is ObjectID => {
|
|
97
|
+
return Boolean(id);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (labelIds.length > 0) {
|
|
101
|
+
const slos: Array<ServiceLevelObjective> =
|
|
102
|
+
await ServiceLevelObjectiveService.findBy({
|
|
103
|
+
query: {
|
|
104
|
+
monitorLabels: labelIds,
|
|
105
|
+
},
|
|
106
|
+
select: {
|
|
107
|
+
_id: true,
|
|
108
|
+
},
|
|
109
|
+
limit: LIMIT_MAX,
|
|
110
|
+
skip: 0,
|
|
111
|
+
props: {
|
|
112
|
+
isRoot: true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
serviceLevelObjectiveIds = slos
|
|
117
|
+
.map((slo: ServiceLevelObjective) => {
|
|
118
|
+
return slo.id;
|
|
119
|
+
})
|
|
120
|
+
.filter((id: ObjectID | null): id is ObjectID => {
|
|
121
|
+
return Boolean(id);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error(
|
|
126
|
+
`Error collecting SLO label rules affected by a label delete: ${err}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
deleteBy,
|
|
132
|
+
carryForward: {
|
|
133
|
+
serviceLevelObjectiveIds: serviceLevelObjectiveIds,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@CaptureSpan()
|
|
139
|
+
protected override async onDeleteSuccess(
|
|
140
|
+
onDelete: OnDelete<Model>,
|
|
141
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
142
|
+
): Promise<OnDelete<Model>> {
|
|
143
|
+
const serviceLevelObjectiveIds: Array<ObjectID> =
|
|
144
|
+
(
|
|
145
|
+
onDelete.carryForward as {
|
|
146
|
+
serviceLevelObjectiveIds?: Array<ObjectID>;
|
|
147
|
+
} | null
|
|
148
|
+
)?.serviceLevelObjectiveIds || [];
|
|
149
|
+
|
|
150
|
+
for (const serviceLevelObjectiveId of serviceLevelObjectiveIds) {
|
|
151
|
+
try {
|
|
152
|
+
await ServiceLevelObjectiveMonitorRuleEngineService.syncMonitorsForSlo({
|
|
153
|
+
serviceLevelObjectiveId: serviceLevelObjectiveId,
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
logger.error(
|
|
157
|
+
`Error re-applying the monitor label rule for SLO ${serviceLevelObjectiveId.toString()} after a label delete: ${err}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return onDelete;
|
|
163
|
+
}
|
|
164
|
+
|
|
60
165
|
/**
|
|
61
166
|
* Find existing labels by name (case-insensitive) or auto-create
|
|
62
167
|
* them, returning the ObjectIDs in the same order as the input.
|
|
@@ -265,6 +265,68 @@ export class Service extends DatabaseService<Model> {
|
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/*
|
|
269
|
+
* THE rule for "may this project use this provider?": a global provider is
|
|
270
|
+
* shared with everyone, and a project-owned provider belongs only to its
|
|
271
|
+
* own project. Every caller that honours a caller-supplied llmProviderId
|
|
272
|
+
* routes through this one predicate so the checks cannot drift apart —
|
|
273
|
+
* a second, subtly different copy of this comparison is how one entry point
|
|
274
|
+
* ends up accepting a provider another one rejects.
|
|
275
|
+
*/
|
|
276
|
+
private isProviderUsableBy(provider: Model, projectId: ObjectID): boolean {
|
|
277
|
+
if (provider.isGlobalLlm === true) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return Boolean(
|
|
282
|
+
provider.projectId &&
|
|
283
|
+
provider.projectId.toString() === projectId.toString(),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* Can this project run against this provider id? Answers the question
|
|
289
|
+
* without loading secrets — callers that only need a yes/no (the runbook AI
|
|
290
|
+
* step validating a pinned provider before it runs) must not pull an apiKey
|
|
291
|
+
* into memory to get it. A provider that does not exist, or belongs to
|
|
292
|
+
* another project, is not usable.
|
|
293
|
+
*/
|
|
294
|
+
@CaptureSpan()
|
|
295
|
+
public async isProviderUsableByProject(data: {
|
|
296
|
+
projectId: ObjectID;
|
|
297
|
+
llmProviderId: ObjectID;
|
|
298
|
+
}): Promise<boolean> {
|
|
299
|
+
/*
|
|
300
|
+
* _id is a uuid column: querying it with a non-uuid string is a Postgres
|
|
301
|
+
* cast error, not an empty result. A caller-supplied id reaches us
|
|
302
|
+
* straight from an unvalidated JSON config, so shape-check before the
|
|
303
|
+
* query and report "not usable" rather than throwing a driver error.
|
|
304
|
+
*/
|
|
305
|
+
if (!ObjectID.isValidUUID(data.llmProviderId.toString())) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const provider: Model | null = await this.findOneBy({
|
|
310
|
+
query: {
|
|
311
|
+
_id: data.llmProviderId.toString(),
|
|
312
|
+
},
|
|
313
|
+
select: {
|
|
314
|
+
_id: true,
|
|
315
|
+
projectId: true,
|
|
316
|
+
isGlobalLlm: true,
|
|
317
|
+
},
|
|
318
|
+
props: {
|
|
319
|
+
isRoot: true,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!provider) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return this.isProviderUsableBy(provider, data.projectId);
|
|
328
|
+
}
|
|
329
|
+
|
|
268
330
|
/*
|
|
269
331
|
* Resolve the provider to use for a chat turn. When the user has explicitly
|
|
270
332
|
* chosen a provider (llmProviderId), use it — but only if it is actually
|
|
@@ -278,7 +340,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
278
340
|
projectId: ObjectID;
|
|
279
341
|
llmProviderId?: ObjectID | undefined;
|
|
280
342
|
}): Promise<Model | null> {
|
|
281
|
-
if (
|
|
343
|
+
if (
|
|
344
|
+
data.llmProviderId &&
|
|
345
|
+
// A non-uuid id would be a Postgres cast error, not a miss. Fall back.
|
|
346
|
+
ObjectID.isValidUUID(data.llmProviderId.toString())
|
|
347
|
+
) {
|
|
282
348
|
const provider: Model | null = await this.findOneBy({
|
|
283
349
|
query: {
|
|
284
350
|
_id: data.llmProviderId.toString(),
|
|
@@ -300,14 +366,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
300
366
|
},
|
|
301
367
|
});
|
|
302
368
|
|
|
303
|
-
if (provider) {
|
|
304
|
-
|
|
305
|
-
const belongsToProject: boolean =
|
|
306
|
-
provider.projectId?.toString() === data.projectId.toString();
|
|
307
|
-
|
|
308
|
-
if (isGlobal || belongsToProject) {
|
|
309
|
-
return provider;
|
|
310
|
-
}
|
|
369
|
+
if (provider && this.isProviderUsableBy(provider, data.projectId)) {
|
|
370
|
+
return provider;
|
|
311
371
|
}
|
|
312
372
|
// Fall through to default resolution when the id is invalid/inaccessible.
|
|
313
373
|
}
|