@oneuptime/common 12.0.2 → 12.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/DatabaseService.ts +111 -0
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +86 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
deriveRelationships,
|
|
15
15
|
EntityRelationshipEdge,
|
|
16
16
|
} from "../../../Utils/Telemetry/EntityRelationship";
|
|
17
|
+
import crypto from "crypto";
|
|
17
18
|
|
|
18
19
|
/*
|
|
19
20
|
* Shared entity-registry reconciliation machinery (phases 2 + 5 of the
|
|
@@ -87,28 +88,81 @@ export const REGISTRY_PROMOTED_TYPES: ReadonlySet<EntityType> =
|
|
|
87
88
|
*/
|
|
88
89
|
const FENCE_NAMESPACE: string = "otel-maintenance-fence";
|
|
89
90
|
const FENCE_SCOPE: string = "entity-reconcile";
|
|
91
|
+
const ROW_FENCE_SCOPE: string = "entity-reconcile-row";
|
|
90
92
|
const FENCE_TTL_SECONDS: number = 5 * 60; // 5 minutes
|
|
91
93
|
|
|
94
|
+
/*
|
|
95
|
+
* Atomic claim. A read-then-write here admits every worker that reads the
|
|
96
|
+
* absent key in the same instant, which at ingest concurrency is all of them —
|
|
97
|
+
* the defect behind the row-lock convoy documented in
|
|
98
|
+
* DatabaseService.updateColumnsByIdIfUnlockedWithoutHooks. TTLs are jittered
|
|
99
|
+
* so a fleet-wide restart cannot re-synchronise the windows and rebuild the
|
|
100
|
+
* herd on a fixed period.
|
|
101
|
+
*/
|
|
92
102
|
async function shouldReconcile(fenceId: string): Promise<boolean> {
|
|
93
103
|
try {
|
|
94
|
-
|
|
95
|
-
const seen: string | null = await GlobalCache.getString(
|
|
104
|
+
return await GlobalCache.setStringIfNotExists(
|
|
96
105
|
FENCE_NAMESPACE,
|
|
97
|
-
|
|
106
|
+
`${FENCE_SCOPE}:${hashFenceId(fenceId)}`,
|
|
107
|
+
"1",
|
|
108
|
+
{ expiresInSeconds: GlobalCache.withJitter(FENCE_TTL_SECONDS) },
|
|
98
109
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
await GlobalCache.setString(FENCE_NAMESPACE, key, "1", {
|
|
103
|
-
expiresInSeconds: FENCE_TTL_SECONDS,
|
|
104
|
-
});
|
|
110
|
+
} catch {
|
|
111
|
+
// If the cache is down, default to running the reconcile.
|
|
105
112
|
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* The set-level fence id concatenates every promoted entity key, so it is
|
|
118
|
+
* unbounded in length — a pod with many entities would otherwise produce a
|
|
119
|
+
* multi-kilobyte Redis key. Hashing keeps it fixed-width without changing which
|
|
120
|
+
* sets collide.
|
|
121
|
+
*/
|
|
122
|
+
function hashFenceId(fenceId: string): string {
|
|
123
|
+
return crypto.createHash("sha1").update(fenceId).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* Per-ROW fence, sitting inside the set-level one.
|
|
128
|
+
*
|
|
129
|
+
* The set-level fence keys on (project + the whole promoted entity set), which
|
|
130
|
+
* in practice is unique per POD — a pod's own key is in the set. But the writes
|
|
131
|
+
* it gates are per ROW: the single TelemetryEntity row for a Kubernetes cluster
|
|
132
|
+
* takes one UPDATE per pod in that cluster per window, the namespace row one
|
|
133
|
+
* per pod in the namespace, and so on. Throttle granularity finer than write
|
|
134
|
+
* granularity means the throttle does not bound the writes at all, and the
|
|
135
|
+
* mismatch factor is the pod count — the same shape of defect as the Service
|
|
136
|
+
* heartbeat, one layer down.
|
|
137
|
+
*
|
|
138
|
+
* Claiming per row before the lookup also removes the redundant SELECT, not
|
|
139
|
+
* just the redundant UPDATE.
|
|
140
|
+
*/
|
|
141
|
+
async function shouldReconcileRow(rowFenceId: string): Promise<boolean> {
|
|
142
|
+
try {
|
|
143
|
+
return await GlobalCache.setStringIfNotExists(
|
|
144
|
+
FENCE_NAMESPACE,
|
|
145
|
+
`${ROW_FENCE_SCOPE}:${hashFenceId(rowFenceId)}`,
|
|
146
|
+
"1",
|
|
147
|
+
{ expiresInSeconds: GlobalCache.withJitter(FENCE_TTL_SECONDS) },
|
|
148
|
+
);
|
|
106
149
|
} catch {
|
|
107
150
|
// If the cache is down, default to running the reconcile.
|
|
108
151
|
return true;
|
|
109
152
|
}
|
|
110
153
|
}
|
|
111
154
|
|
|
155
|
+
async function releaseRowFence(rowFenceId: string): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
await GlobalCache.deleteKey(
|
|
158
|
+
FENCE_NAMESPACE,
|
|
159
|
+
`${ROW_FENCE_SCOPE}:${hashFenceId(rowFenceId)}`,
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
// Best effort — losing the release costs one window of staleness.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
112
166
|
/*
|
|
113
167
|
* Over-budget skips happen per row, but the warn log is fenced to once per
|
|
114
168
|
* (project, entityType) per fence window — an over-budget project would
|
|
@@ -119,20 +173,12 @@ export async function shouldWarnEntityBudgetOnce(data: {
|
|
|
119
173
|
entityType: EntityType;
|
|
120
174
|
}): Promise<boolean> {
|
|
121
175
|
try {
|
|
122
|
-
|
|
123
|
-
data.entityType
|
|
124
|
-
}`;
|
|
125
|
-
const seen: string | null = await GlobalCache.getString(
|
|
176
|
+
return await GlobalCache.setStringIfNotExists(
|
|
126
177
|
FENCE_NAMESPACE,
|
|
127
|
-
|
|
178
|
+
`entity-budget-warn:${data.projectId.toString()}:${data.entityType}`,
|
|
179
|
+
"1",
|
|
180
|
+
{ expiresInSeconds: GlobalCache.withJitter(FENCE_TTL_SECONDS) },
|
|
128
181
|
);
|
|
129
|
-
if (seen) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
await GlobalCache.setString(FENCE_NAMESPACE, key, "1", {
|
|
133
|
-
expiresInSeconds: FENCE_TTL_SECONDS,
|
|
134
|
-
});
|
|
135
|
-
return true;
|
|
136
182
|
} catch {
|
|
137
183
|
// If the cache is down, default to warning (visibility over silence).
|
|
138
184
|
return true;
|
|
@@ -230,6 +276,13 @@ export async function reconcileByNaturalKey<
|
|
|
230
276
|
lastSeenAt: Date;
|
|
231
277
|
/** Human-readable row identity for log lines, e.g. "entity host/abc123". */
|
|
232
278
|
describe: string;
|
|
279
|
+
/**
|
|
280
|
+
* Stable identity of the ROW being reconciled, e.g.
|
|
281
|
+
* `${projectId}:${entityType}:${entityKey}`. Claimed atomically before the
|
|
282
|
+
* lookup so one writer per row per window does the work — see
|
|
283
|
+
* shouldReconcileRow for why the set-level fence above is not enough.
|
|
284
|
+
*/
|
|
285
|
+
rowFenceId: string;
|
|
233
286
|
/** Extra columns to select on the existing row, for `buildUpdate` diffing. */
|
|
234
287
|
select?: Select<TBaseModel> | undefined;
|
|
235
288
|
/**
|
|
@@ -264,6 +317,10 @@ export async function reconcileByNaturalKey<
|
|
|
264
317
|
} as unknown as QueryDeepPartialEntity<TBaseModel>;
|
|
265
318
|
};
|
|
266
319
|
|
|
320
|
+
if (!(await shouldReconcileRow(data.rowFenceId))) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
267
324
|
const existing: TBaseModel | null = await data.service.findOneBy({
|
|
268
325
|
query: data.query,
|
|
269
326
|
select,
|
|
@@ -278,15 +335,32 @@ export async function reconcileByNaturalKey<
|
|
|
278
335
|
* audit). buildBump returns only plain values — lastSeenAt plus, at most,
|
|
279
336
|
* the descriptiveAttributes / labels JSON columns — which the primitive
|
|
280
337
|
* persists via the driver transformer path. See ServiceService.updateLastSeen.
|
|
338
|
+
*
|
|
339
|
+
* SKIP LOCKED: a contended bump yields rather than queueing behind the
|
|
340
|
+
* writer that already holds the row. Registry rows for shared entities (a
|
|
341
|
+
* cluster, a namespace) are written by every pod that reports them, so
|
|
342
|
+
* this is exactly the hot-row shape that convoyed on Service.
|
|
281
343
|
*/
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
344
|
+
const wrote: boolean =
|
|
345
|
+
await data.service.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
346
|
+
id: existing.id!,
|
|
347
|
+
data: buildBump(existing),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (!wrote) {
|
|
351
|
+
// Skipped under contention — re-open the window so the next batch retries.
|
|
352
|
+
await releaseRowFence(data.rowFenceId);
|
|
353
|
+
}
|
|
286
354
|
return;
|
|
287
355
|
}
|
|
288
356
|
|
|
289
357
|
if (data.beforeCreate && !(await data.beforeCreate())) {
|
|
358
|
+
/*
|
|
359
|
+
* Over budget: release the row fence so the gate is re-evaluated next
|
|
360
|
+
* batch rather than being suppressed for a full window by a decision that
|
|
361
|
+
* wrote nothing.
|
|
362
|
+
*/
|
|
363
|
+
await releaseRowFence(data.rowFenceId);
|
|
290
364
|
return;
|
|
291
365
|
}
|
|
292
366
|
|
|
@@ -312,13 +386,20 @@ export async function reconcileByNaturalKey<
|
|
|
312
386
|
logger.debug(
|
|
313
387
|
`EntityRegistry: create raced for ${data.describe} (concurrent insert); bumping lastSeenAt on the winning row.`,
|
|
314
388
|
);
|
|
315
|
-
await data.service.
|
|
389
|
+
await data.service.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
316
390
|
id: winner.id!,
|
|
317
391
|
data: buildBump(winner),
|
|
318
392
|
});
|
|
319
393
|
return;
|
|
320
394
|
}
|
|
321
395
|
|
|
396
|
+
/*
|
|
397
|
+
* The insert itself was invalid. Re-open the row window so the next batch
|
|
398
|
+
* retries rather than being silently suppressed for the TTL — a genuinely
|
|
399
|
+
* broken row should keep surfacing this warning, not go quiet.
|
|
400
|
+
*/
|
|
401
|
+
await releaseRowFence(data.rowFenceId);
|
|
402
|
+
|
|
322
403
|
logger.warn(`EntityRegistry: create failed for ${data.describe}:`);
|
|
323
404
|
logger.warn(err as Error);
|
|
324
405
|
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import QueryDeepPartialEntity from "../../../Types/Database/PartialEntity";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import DatabaseService from "../../Services/DatabaseService";
|
|
5
|
+
import GlobalCache from "../../Infrastructure/GlobalCache";
|
|
6
|
+
import SingleFlight from "../SingleFlight";
|
|
7
|
+
import logger from "../Logger";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The one place a telemetry resource's liveness heartbeat gets written.
|
|
11
|
+
*
|
|
12
|
+
* Twelve services (Service, Host, DockerHost, PodmanHost, KubernetesCluster,
|
|
13
|
+
* ProxmoxCluster, CephCluster, IoTFleet, CloudResource, ServerlessFunction,
|
|
14
|
+
* RumApplication, DockerSwarmCluster) each carried a hand-copied version of
|
|
15
|
+
* this: read a throttle key, compare a fingerprint, write it back, UPDATE the
|
|
16
|
+
* row. Copying it twelve times copied its defects twelve times, and one of
|
|
17
|
+
* those copies took production down.
|
|
18
|
+
*
|
|
19
|
+
* THE DEFECT. The throttle keyed on the row id but STORED a fingerprint of the
|
|
20
|
+
* incoming metadata, skipping only when the stored fingerprint matched. That
|
|
21
|
+
* works while one producer owns a row. It provides ZERO throttling the moment
|
|
22
|
+
* the fingerprinted payload changes as fast as the batches arrive — and on
|
|
23
|
+
* these paths it does, in two different ways:
|
|
24
|
+
*
|
|
25
|
+
* - Two producers of the same row disagreeing forever. `Service` is unique
|
|
26
|
+
* on (projectId, name) alone, so one service.name in dev and prod inside a
|
|
27
|
+
* project is one row with two fingerprints, each busting the other's key.
|
|
28
|
+
* - A single producer whose payload is a per-scrape GAUGE. The cluster and
|
|
29
|
+
* fleet snapshots hash things like `runningTaskCount`, `osdUpCount`,
|
|
30
|
+
* `onlineDeviceCount` and `capacityUsedPercent` into the fingerprint.
|
|
31
|
+
* A float utilisation percentage changes on literally every scrape, so
|
|
32
|
+
* the fingerprint never repeats and the window never engages.
|
|
33
|
+
*
|
|
34
|
+
* Either way the throttle admitted one write per BATCH rather than one per
|
|
35
|
+
* window, against rows shared by every node/pod/device reporting into them.
|
|
36
|
+
* With the autoscaler at 100 pods (~25K concurrent jobs) that was thousands of
|
|
37
|
+
* simultaneous UPDATEs onto ~2,300 rows. Postgres queues row-lock waiters
|
|
38
|
+
* strictly, so they did not collide and retry — they lined up: 1,017 active
|
|
39
|
+
* connections, 892 parked on locks, the tail waiting 3.7 hours. The database
|
|
40
|
+
* was never short of capacity.
|
|
41
|
+
*
|
|
42
|
+
* THE SHAPE. Liveness and enrichment are gated separately, because they are
|
|
43
|
+
* different kinds of fact:
|
|
44
|
+
*
|
|
45
|
+
* LIVENESS (`lastSeenAt`, collector status) is gated on key PRESENCE, never
|
|
46
|
+
* on the payload. One heartbeat per resource per window, full stop — no
|
|
47
|
+
* metadata, however fast it churns, can bust it.
|
|
48
|
+
*
|
|
49
|
+
* ENRICHMENT (everything harvested from the payload) is gated on TWO keys: a
|
|
50
|
+
* compare-and-claim so a genuinely changed value still lands promptly, plus
|
|
51
|
+
* a presence-only rate limiter so a permanently-churning value costs one
|
|
52
|
+
* write per window instead of one per batch. The fingerprint alone does not
|
|
53
|
+
* bound churn — it is exactly what failed.
|
|
54
|
+
*
|
|
55
|
+
* Both claims are ATOMIC. The old read-then-write was check-then-act, and at
|
|
56
|
+
* ingest concurrency the losers of that race do not politely retry: they all
|
|
57
|
+
* proceed.
|
|
58
|
+
*
|
|
59
|
+
* Underneath both, the write is `FOR UPDATE SKIP LOCKED`, so a contended
|
|
60
|
+
* writer yields instead of queueing. That layer holds even when every gate
|
|
61
|
+
* above fails open at once — which is exactly what a Redis outage does — and
|
|
62
|
+
* it is what makes failing open affordable. Failing open is required:
|
|
63
|
+
* suppressing a heartbeat strands a healthy resource as "disconnected" while
|
|
64
|
+
* its telemetry is still arriving (issue #3006's failure mode).
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export interface ResourceHeartbeatWrite<TBaseModel extends BaseModel> {
|
|
68
|
+
service: DatabaseService<TBaseModel>;
|
|
69
|
+
id: ObjectID;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Redis namespace for this model's liveness key. The two enrichment keys are
|
|
73
|
+
* derived from it, so one namespace per model keeps all three together.
|
|
74
|
+
*/
|
|
75
|
+
cacheNamespace: string;
|
|
76
|
+
|
|
77
|
+
/** Liveness window in seconds, before jitter. */
|
|
78
|
+
throttleInSeconds: number;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Enrichment rate-limit window in seconds, before jitter. Defaults to the
|
|
82
|
+
* liveness window: enrichment then lands no later than it used to, while
|
|
83
|
+
* the number of writes is bounded for the first time. Raise it only for
|
|
84
|
+
* genuinely static metadata (a service version, a cloud region), never for
|
|
85
|
+
* values a user watches change.
|
|
86
|
+
*/
|
|
87
|
+
enrichmentThrottleInSeconds?: number | undefined;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Columns that must always be written and are safe to lose to contention —
|
|
91
|
+
* a concurrent writer is storing the same `now()`.
|
|
92
|
+
*/
|
|
93
|
+
liveness: QueryDeepPartialEntity<TBaseModel>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Columns harvested from the payload. Unlike liveness these can carry
|
|
97
|
+
* information a concurrent writer does not have, so a contended write of
|
|
98
|
+
* them is retried rather than dropped.
|
|
99
|
+
*/
|
|
100
|
+
metadata?: QueryDeepPartialEntity<TBaseModel> | undefined;
|
|
101
|
+
|
|
102
|
+
/** Stable fingerprint of `metadata`, for change detection. */
|
|
103
|
+
fingerprint: string;
|
|
104
|
+
|
|
105
|
+
/** Human-readable row identity for log lines, e.g. "host abc123". */
|
|
106
|
+
describe: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Derived from the model's liveness namespace — see cacheNamespace. */
|
|
110
|
+
function fingerprintNamespace(cacheNamespace: string): string {
|
|
111
|
+
return `${cacheNamespace}-fingerprint`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeWindowNamespace(cacheNamespace: string): string {
|
|
115
|
+
return `${cacheNamespace}-write-window`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default class ResourceHeartbeat {
|
|
119
|
+
public static async write<TBaseModel extends BaseModel>(
|
|
120
|
+
input: ResourceHeartbeatWrite<TBaseModel>,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const cacheKey: string = input.id.toString();
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* Collapse duplicate concurrent callers inside THIS process first. One
|
|
126
|
+
* OTLP batch resolves the same resource once per contained resource — the
|
|
127
|
+
* host-metrics collector config OneUptime ships emits hundreds per batch —
|
|
128
|
+
* and without this every one of them independently asks Redis the same
|
|
129
|
+
* question and throws the answer away.
|
|
130
|
+
*/
|
|
131
|
+
await SingleFlight.run(
|
|
132
|
+
`${input.cacheNamespace}:${cacheKey}:${input.fingerprint}`,
|
|
133
|
+
async () => {
|
|
134
|
+
await this.writeOnce(input, cacheKey);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static async writeOnce<TBaseModel extends BaseModel>(
|
|
140
|
+
input: ResourceHeartbeatWrite<TBaseModel>,
|
|
141
|
+
cacheKey: string,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
/*
|
|
144
|
+
* Liveness gate. Presence-only and jittered: a fleet-wide restart would
|
|
145
|
+
* otherwise re-synchronise every resource's window and rebuild the herd on
|
|
146
|
+
* a fixed period.
|
|
147
|
+
*/
|
|
148
|
+
let writeLiveness: boolean = true;
|
|
149
|
+
try {
|
|
150
|
+
writeLiveness = await GlobalCache.setStringIfNotExists(
|
|
151
|
+
input.cacheNamespace,
|
|
152
|
+
cacheKey,
|
|
153
|
+
"1",
|
|
154
|
+
{
|
|
155
|
+
expiresInSeconds: GlobalCache.withJitter(input.throttleInSeconds),
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
/*
|
|
160
|
+
* Cache unavailable — fail OPEN and refresh liveness anyway. Suppressing
|
|
161
|
+
* the heartbeat during a Redis outage is how a healthy resource is shown
|
|
162
|
+
* as disconnected while its data keeps arriving. Affordable only because
|
|
163
|
+
* the write below cannot queue.
|
|
164
|
+
*/
|
|
165
|
+
writeLiveness = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hasMetadata: boolean =
|
|
169
|
+
Boolean(input.metadata) && Object.keys(input.metadata!).length > 0;
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
* Enrichment gate 1: has the payload actually changed? Compare-and-claim,
|
|
173
|
+
* so a genuinely new value is written promptly rather than waiting out a
|
|
174
|
+
* window.
|
|
175
|
+
*/
|
|
176
|
+
let metadataChanged: boolean = false;
|
|
177
|
+
if (hasMetadata) {
|
|
178
|
+
try {
|
|
179
|
+
metadataChanged = await GlobalCache.setStringIfChanged(
|
|
180
|
+
fingerprintNamespace(input.cacheNamespace),
|
|
181
|
+
cacheKey,
|
|
182
|
+
input.fingerprint,
|
|
183
|
+
{
|
|
184
|
+
expiresInSeconds: GlobalCache.withJitter(
|
|
185
|
+
input.enrichmentThrottleInSeconds ?? input.throttleInSeconds,
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
/*
|
|
191
|
+
* Fail CLOSED for enrichment, unlike liveness. These columns are
|
|
192
|
+
* descriptive; nothing is mis-reported by writing them a little later,
|
|
193
|
+
* and letting a Redis outage open this gate restores the per-batch
|
|
194
|
+
* write storm this exists to prevent.
|
|
195
|
+
*/
|
|
196
|
+
metadataChanged = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/*
|
|
201
|
+
* Enrichment gate 2: the rate limiter. Change detection cannot tell a
|
|
202
|
+
* meaningful update apart from a gauge that moves every scrape — both
|
|
203
|
+
* present as "it changed". Since the churning case is unbounded, this
|
|
204
|
+
* bounds both.
|
|
205
|
+
*/
|
|
206
|
+
let writeMetadata: boolean = false;
|
|
207
|
+
if (metadataChanged) {
|
|
208
|
+
try {
|
|
209
|
+
writeMetadata = await GlobalCache.setStringIfNotExists(
|
|
210
|
+
writeWindowNamespace(input.cacheNamespace),
|
|
211
|
+
cacheKey,
|
|
212
|
+
"1",
|
|
213
|
+
{
|
|
214
|
+
expiresInSeconds: GlobalCache.withJitter(
|
|
215
|
+
input.enrichmentThrottleInSeconds ?? input.throttleInSeconds,
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
} catch {
|
|
220
|
+
writeMetadata = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!writeLiveness && !writeMetadata) {
|
|
225
|
+
// Nothing to say. Issue no statement at all.
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* Liveness rides along even on an enrichment-only write: refreshing it
|
|
231
|
+
* early is harmless (it is a floor, not a sample), and splitting the two
|
|
232
|
+
* into separate statements would double the writes we just halved.
|
|
233
|
+
*/
|
|
234
|
+
const data: QueryDeepPartialEntity<TBaseModel> = {
|
|
235
|
+
...(input.liveness as Record<string, unknown>),
|
|
236
|
+
...(writeMetadata
|
|
237
|
+
? ((input.metadata || {}) as Record<string, unknown>)
|
|
238
|
+
: {}),
|
|
239
|
+
} as QueryDeepPartialEntity<TBaseModel>;
|
|
240
|
+
|
|
241
|
+
let wrote: boolean = false;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
wrote = await input.service.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
245
|
+
id: input.id,
|
|
246
|
+
data: data,
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
/*
|
|
250
|
+
* Liveness must survive bad metadata. Every enrichment value is a
|
|
251
|
+
* collector-supplied attribute, so if one of them makes Postgres reject
|
|
252
|
+
* the statement it takes `lastSeenAt` down with it and the resource
|
|
253
|
+
* silently stops looking alive — the disconnection sweep then flips a
|
|
254
|
+
* perfectly healthy resource 15 minutes later with telemetry arriving
|
|
255
|
+
* the whole time. That was issue #3006. Retry with liveness only, so
|
|
256
|
+
* enrichment is what gets dropped, never the heartbeat.
|
|
257
|
+
*/
|
|
258
|
+
logger.warn(
|
|
259
|
+
`ResourceHeartbeat: metadata write failed for ${input.describe}, falling back to a liveness-only update: ${
|
|
260
|
+
err instanceof Error ? err.message : String(err)
|
|
261
|
+
}`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await this.releaseGates(input.cacheNamespace, cacheKey, writeMetadata);
|
|
265
|
+
|
|
266
|
+
await input.service.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
267
|
+
id: input.id,
|
|
268
|
+
data: input.liveness,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!wrote && writeMetadata) {
|
|
275
|
+
/*
|
|
276
|
+
* The row was locked, so this write was SKIPPED rather than applied. For
|
|
277
|
+
* liveness that is a non-event — whoever holds the lock is storing the
|
|
278
|
+
* same timestamp. Enrichment is different: the holder may be writing an
|
|
279
|
+
* older payload, and the gates just claimed would suppress ours for the
|
|
280
|
+
* rest of the window. Release them so the next batch re-attempts.
|
|
281
|
+
*
|
|
282
|
+
* This cannot become a retry storm: each attempt is a non-blocking
|
|
283
|
+
* statement that returns immediately when contended.
|
|
284
|
+
*/
|
|
285
|
+
await this.releaseGates(input.cacheNamespace, cacheKey, true);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Re-open the gates so the next batch retries. When enrichment was at stake
|
|
291
|
+
* those are the gates that must not stay claimed after a write that never
|
|
292
|
+
* happened; otherwise it is the liveness gate.
|
|
293
|
+
*/
|
|
294
|
+
private static async releaseGates(
|
|
295
|
+
cacheNamespace: string,
|
|
296
|
+
cacheKey: string,
|
|
297
|
+
hadMetadata: boolean,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const namespaces: Array<string> = hadMetadata
|
|
300
|
+
? [
|
|
301
|
+
fingerprintNamespace(cacheNamespace),
|
|
302
|
+
writeWindowNamespace(cacheNamespace),
|
|
303
|
+
]
|
|
304
|
+
: [cacheNamespace];
|
|
305
|
+
|
|
306
|
+
for (const namespace of namespaces) {
|
|
307
|
+
try {
|
|
308
|
+
await GlobalCache.deleteKey(namespace, cacheKey);
|
|
309
|
+
} catch {
|
|
310
|
+
// Best effort — the retry is what actually matters.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|