@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
|
@@ -13,6 +13,7 @@ import Model from "../../Models/DatabaseModels/Service";
|
|
|
13
13
|
import Label from "../../Models/DatabaseModels/Label";
|
|
14
14
|
import Project from "../../Models/DatabaseModels/Project";
|
|
15
15
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
16
|
+
import SingleFlight from "../Utils/SingleFlight";
|
|
16
17
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
17
18
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
18
19
|
import crypto from "crypto";
|
|
@@ -22,6 +23,20 @@ const DEFAULT_TELEMETRY_RETENTION_IN_DAYS: number = 15;
|
|
|
22
23
|
const LAST_SEEN_CACHE_NAMESPACE: string = "service-last-seen";
|
|
23
24
|
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
24
25
|
|
|
26
|
+
/*
|
|
27
|
+
* Enrichment (the descriptive, collector-supplied columns) is gated
|
|
28
|
+
* separately from liveness — see the long note on updateLastSeen. The
|
|
29
|
+
* fingerprint key detects a genuine change; the write-window key bounds how
|
|
30
|
+
* often a service whose producers permanently disagree can act on one.
|
|
31
|
+
*/
|
|
32
|
+
const METADATA_FINGERPRINT_CACHE_NAMESPACE: string =
|
|
33
|
+
"service-metadata-fingerprint";
|
|
34
|
+
const METADATA_FINGERPRINT_TTL_SECONDS: number = 60 * 60;
|
|
35
|
+
|
|
36
|
+
const METADATA_WRITE_WINDOW_CACHE_NAMESPACE: string =
|
|
37
|
+
"service-metadata-write-window";
|
|
38
|
+
const METADATA_WRITE_THROTTLE_SECONDS: number = 5 * 60;
|
|
39
|
+
|
|
25
40
|
const LABELS_APPLIED_CACHE_NAMESPACE: string = "service-labels-applied";
|
|
26
41
|
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
27
42
|
|
|
@@ -119,10 +134,53 @@ export class Service extends DatabaseService<Model> {
|
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
/*
|
|
122
|
-
* Refresh `lastSeenAt`
|
|
123
|
-
*
|
|
124
|
-
* re-resolves the same serviceId
|
|
125
|
-
*
|
|
137
|
+
* Refresh `lastSeenAt` (and, occasionally, the descriptive columns) for a
|
|
138
|
+
* service. This is the single hottest UPDATE in the system: every
|
|
139
|
+
* metric/log/trace/profile batch re-resolves the same serviceId, and every
|
|
140
|
+
* OTLP resource inside a batch does it again.
|
|
141
|
+
*
|
|
142
|
+
* It is also the write that took production down, so the throttling here is
|
|
143
|
+
* deliberate in a way worth reading before changing.
|
|
144
|
+
*
|
|
145
|
+
* WHAT WENT WRONG. The throttle used to key on the serviceId but STORE a
|
|
146
|
+
* fingerprint of the metadata, and skip only when the stored fingerprint
|
|
147
|
+
* matched. That works when one producer owns a row. It collapses when two
|
|
148
|
+
* producers of the SAME row disagree, because `Service` is unique on
|
|
149
|
+
* (projectId, name) alone: the same service.name running in dev and prod
|
|
150
|
+
* inside one project is ONE row with two permanently disagreeing
|
|
151
|
+
* fingerprints. A writes fp_A, B reads fp_A, sees a mismatch, writes fp_B, A
|
|
152
|
+
* reads fp_B, sees a mismatch... forever. The same holds for active-active
|
|
153
|
+
* deployments differing in cloud.region or cloud.account.id, and transiently
|
|
154
|
+
* for every rolling deploy (service.version). For those rows the throttle
|
|
155
|
+
* admitted not one write per minute but one write per BATCH — and with the
|
|
156
|
+
* autoscaler at 100 pods (~25K concurrent jobs) that was thousands of
|
|
157
|
+
* simultaneous UPDATEs onto ~2,300 rows. Postgres queues row-lock waiters
|
|
158
|
+
* strictly, so they did not collide and retry, they lined up: 1,017 active
|
|
159
|
+
* connections, 892 parked on locks, the tail waiting 3.7 hours. The database
|
|
160
|
+
* was never short of capacity.
|
|
161
|
+
*
|
|
162
|
+
* The shape below separates the two things that were tangled together:
|
|
163
|
+
*
|
|
164
|
+
* LIVENESS (`lastSeenAt`) is gated on key PRESENCE, never on metadata. One
|
|
165
|
+
* heartbeat per service per window, full stop — no payload can bust it, so
|
|
166
|
+
* disagreeing producers cannot flap it.
|
|
167
|
+
*
|
|
168
|
+
* ENRICHMENT (the descriptive columns) is gated on TWO keys: a fingerprint
|
|
169
|
+
* claim so a genuinely changed attribute still lands promptly, plus a
|
|
170
|
+
* presence-only rate limiter so a permanently-flapping service costs one
|
|
171
|
+
* enrichment write per rate-limit window instead of one per batch. The
|
|
172
|
+
* fingerprint key alone does not fix flapping — a longer TTL just makes
|
|
173
|
+
* each flap more expensive.
|
|
174
|
+
*
|
|
175
|
+
* Both claims are ATOMIC (SET NX / compare-and-claim in one round trip).
|
|
176
|
+
* The old read-then-write was check-then-act, and at this concurrency the
|
|
177
|
+
* losers of that race do not politely retry — they all proceed.
|
|
178
|
+
*
|
|
179
|
+
* Underneath both, the write itself is non-blocking (SKIP LOCKED), so a
|
|
180
|
+
* convoy cannot form even if every gate above fails open at once — which is
|
|
181
|
+
* exactly what a Redis outage does. That layering is what makes fail-open
|
|
182
|
+
* affordable here, and fail-open is required: a suppressed heartbeat strands
|
|
183
|
+
* a healthy service as "disconnected" while its telemetry keeps arriving.
|
|
126
184
|
*/
|
|
127
185
|
@CaptureSpan()
|
|
128
186
|
public async updateLastSeen(
|
|
@@ -140,90 +198,258 @@ export class Service extends DatabaseService<Model> {
|
|
|
140
198
|
cloudAccountId?: string | undefined;
|
|
141
199
|
},
|
|
142
200
|
): Promise<void> {
|
|
201
|
+
const cacheKey: string = serviceId.toString();
|
|
202
|
+
|
|
143
203
|
/*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* immediately. With no extras the fingerprint is constant, preserving
|
|
149
|
-
* the original "refresh lastSeenAt at most once per window" behaviour.
|
|
204
|
+
* Collapse duplicate concurrent callers inside THIS process first. One
|
|
205
|
+
* OTLP batch resolves the same service once per resource — the shipped
|
|
206
|
+
* host-metrics collector config produces hundreds per batch — and without
|
|
207
|
+
* this every one of them independently asks Redis the same question.
|
|
150
208
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
209
|
+
await SingleFlight.run(
|
|
210
|
+
`${LAST_SEEN_CACHE_NAMESPACE}:${cacheKey}:${this.fingerprintLastSeenExtras(
|
|
211
|
+
extra,
|
|
212
|
+
)}`,
|
|
213
|
+
async () => {
|
|
214
|
+
await this.writeLastSeen(serviceId, cacheKey, extra);
|
|
215
|
+
},
|
|
156
216
|
);
|
|
217
|
+
}
|
|
157
218
|
|
|
158
|
-
|
|
159
|
-
|
|
219
|
+
private async writeLastSeen(
|
|
220
|
+
serviceId: ObjectID,
|
|
221
|
+
cacheKey: string,
|
|
222
|
+
extra?: {
|
|
223
|
+
serviceVersion?: string | undefined;
|
|
224
|
+
deploymentEnvironment?: string | undefined;
|
|
225
|
+
serviceNamespace?: string | undefined;
|
|
226
|
+
runtimeName?: string | undefined;
|
|
227
|
+
runtimeVersion?: string | undefined;
|
|
228
|
+
telemetrySdkLanguage?: string | undefined;
|
|
229
|
+
cloudProvider?: string | undefined;
|
|
230
|
+
cloudPlatform?: string | undefined;
|
|
231
|
+
cloudRegion?: string | undefined;
|
|
232
|
+
cloudAccountId?: string | undefined;
|
|
233
|
+
},
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
const fingerprint: string = this.fingerprintLastSeenExtras(extra);
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* Liveness gate. Presence-only and jittered: a fleet-wide restart would
|
|
239
|
+
* otherwise re-synchronise every service's window and rebuild the herd on
|
|
240
|
+
* a one-minute period.
|
|
241
|
+
*/
|
|
242
|
+
let writeLiveness: boolean = true;
|
|
243
|
+
try {
|
|
244
|
+
writeLiveness = await GlobalCache.setStringIfNotExists(
|
|
245
|
+
LAST_SEEN_CACHE_NAMESPACE,
|
|
246
|
+
cacheKey,
|
|
247
|
+
"1",
|
|
248
|
+
{
|
|
249
|
+
expiresInSeconds: GlobalCache.withJitter(LAST_SEEN_THROTTLE_SECONDS),
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
/*
|
|
254
|
+
* Cache unavailable — fail OPEN and refresh liveness anyway. Suppressing
|
|
255
|
+
* the heartbeat during a Redis outage is how a healthy service gets
|
|
256
|
+
* shown as disconnected while its data keeps arriving (issue #3006's
|
|
257
|
+
* failure mode). Affordable only because the write below cannot queue.
|
|
258
|
+
*/
|
|
259
|
+
writeLiveness = true;
|
|
160
260
|
}
|
|
161
261
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
262
|
+
/*
|
|
263
|
+
* Enrichment gate 1: has the payload actually changed? Compare-and-claim,
|
|
264
|
+
* so a deploy's new service.version is written promptly rather than
|
|
265
|
+
* waiting out a window.
|
|
266
|
+
*/
|
|
267
|
+
let metadataChanged: boolean = false;
|
|
268
|
+
try {
|
|
269
|
+
metadataChanged = await GlobalCache.setStringIfChanged(
|
|
270
|
+
METADATA_FINGERPRINT_CACHE_NAMESPACE,
|
|
271
|
+
cacheKey,
|
|
272
|
+
fingerprint,
|
|
273
|
+
{
|
|
274
|
+
expiresInSeconds: GlobalCache.withJitter(
|
|
275
|
+
METADATA_FINGERPRINT_TTL_SECONDS,
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
} catch {
|
|
280
|
+
/*
|
|
281
|
+
* Fail CLOSED for enrichment (unlike liveness). These columns are
|
|
282
|
+
* descriptive, not operational — nothing is mis-reported by writing them
|
|
283
|
+
* a little later, and letting a Redis outage open this gate would
|
|
284
|
+
* restore the per-batch write storm this method exists to prevent.
|
|
285
|
+
*/
|
|
286
|
+
metadataChanged = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
* Enrichment gate 2: rate limiter. A service whose producers permanently
|
|
291
|
+
* disagree (dev + prod under one name, active-active regions) reports a
|
|
292
|
+
* "change" on every batch forever. Gate 1 cannot tell that apart from a
|
|
293
|
+
* real deploy, so this bounds it regardless.
|
|
294
|
+
*/
|
|
295
|
+
let writeMetadata: boolean = false;
|
|
296
|
+
if (metadataChanged) {
|
|
297
|
+
try {
|
|
298
|
+
writeMetadata = await GlobalCache.setStringIfNotExists(
|
|
299
|
+
METADATA_WRITE_WINDOW_CACHE_NAMESPACE,
|
|
300
|
+
cacheKey,
|
|
301
|
+
"1",
|
|
302
|
+
{
|
|
303
|
+
expiresInSeconds: GlobalCache.withJitter(
|
|
304
|
+
METADATA_WRITE_THROTTLE_SECONDS,
|
|
305
|
+
),
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
} catch {
|
|
309
|
+
writeMetadata = false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!writeLiveness && !writeMetadata) {
|
|
314
|
+
// Nothing to say. Issue no statement at all.
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
170
317
|
|
|
171
318
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
-
const
|
|
319
|
+
const liveness: any = {
|
|
173
320
|
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
|
174
321
|
};
|
|
175
322
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
|
+
const metadata: any = {};
|
|
325
|
+
|
|
326
|
+
if (writeMetadata) {
|
|
327
|
+
if (extra?.serviceVersion) {
|
|
328
|
+
metadata.serviceVersion = extra.serviceVersion;
|
|
329
|
+
}
|
|
330
|
+
if (extra?.deploymentEnvironment) {
|
|
331
|
+
metadata.deploymentEnvironment = extra.deploymentEnvironment;
|
|
332
|
+
}
|
|
333
|
+
if (extra?.serviceNamespace) {
|
|
334
|
+
metadata.serviceNamespace = extra.serviceNamespace;
|
|
335
|
+
}
|
|
336
|
+
if (extra?.runtimeName) {
|
|
337
|
+
metadata.runtimeName = extra.runtimeName;
|
|
338
|
+
}
|
|
339
|
+
if (extra?.runtimeVersion) {
|
|
340
|
+
metadata.runtimeVersion = extra.runtimeVersion;
|
|
341
|
+
}
|
|
342
|
+
if (extra?.telemetrySdkLanguage) {
|
|
343
|
+
metadata.telemetrySdkLanguage = extra.telemetrySdkLanguage;
|
|
344
|
+
}
|
|
345
|
+
if (extra?.cloudProvider) {
|
|
346
|
+
metadata.cloudProvider = extra.cloudProvider;
|
|
347
|
+
}
|
|
348
|
+
if (extra?.cloudPlatform) {
|
|
349
|
+
metadata.cloudPlatform = extra.cloudPlatform;
|
|
350
|
+
}
|
|
351
|
+
if (extra?.cloudRegion) {
|
|
352
|
+
metadata.cloudRegion = extra.cloudRegion;
|
|
353
|
+
}
|
|
354
|
+
if (extra?.cloudAccountId) {
|
|
355
|
+
metadata.cloudAccountId = extra.cloudAccountId;
|
|
356
|
+
}
|
|
205
357
|
}
|
|
206
358
|
|
|
359
|
+
const hasMetadata: boolean = Object.keys(metadata).length > 0;
|
|
360
|
+
|
|
207
361
|
/*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* workflow/realtime/audit hooks). That pipeline holds the Service row's
|
|
213
|
-
* write lock across several round trips and, when an event loop stalls
|
|
214
|
-
* mid-`save()` transaction, leaves it open as the head of a lock convoy
|
|
215
|
-
* that starves the Postgres connection pool. lastSeenAt is an internal
|
|
216
|
-
* liveness timestamp with no onBeforeUpdate/onUpdateSuccess hooks on this
|
|
217
|
-
* model (connected / disconnected status is derived from it at read
|
|
218
|
-
* time), so a bare single-statement UPDATE is both correct and far
|
|
219
|
-
* cheaper. This also intentionally drops the per-update workflow trigger
|
|
220
|
-
* Service's @EnableWorkflow({ update: true }) fired on every heartbeat —
|
|
221
|
-
* a liveness ping should not trigger user workflows.
|
|
362
|
+
* `lastSeenAt` rides along even on an enrichment-only write: refreshing it
|
|
363
|
+
* early is harmless (it is a liveness floor, not a sample), and splitting
|
|
364
|
+
* the two into separate statements would double the writes we just went to
|
|
365
|
+
* such lengths to halve.
|
|
222
366
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
367
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
368
|
+
const data: any = { ...liveness, ...metadata };
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* Heartbeat write. Deliberately NOT the full updateOneById pipeline
|
|
372
|
+
* (permission SELECT -> _findBy SELECT -> save() with `version = version +
|
|
373
|
+
* 1` -> workflow/realtime/audit hooks): that pipeline holds the row's
|
|
374
|
+
* write lock across several round trips inside an open transaction, which
|
|
375
|
+
* is precisely how a stalled event loop becomes the head of a lock convoy.
|
|
376
|
+
* It also intentionally drops the per-update workflow trigger that
|
|
377
|
+
* Service's @EnableWorkflow({ update: true }) fired on every heartbeat — a
|
|
378
|
+
* liveness ping should not trigger user workflows.
|
|
379
|
+
*
|
|
380
|
+
* And it is the SKIP LOCKED variant: contended writers yield instead of
|
|
381
|
+
* queueing, capping backends blocked on any one Service row at one.
|
|
382
|
+
*/
|
|
383
|
+
let wrote: boolean = false;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
wrote = await this.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
387
|
+
id: serviceId,
|
|
388
|
+
data: data,
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
/*
|
|
392
|
+
* Liveness must survive bad metadata. Every enrichment value is a
|
|
393
|
+
* collector-supplied resource attribute, so if one of them makes
|
|
394
|
+
* Postgres reject the statement it takes `lastSeenAt` down with it and
|
|
395
|
+
* the service silently stops looking alive. Retry with liveness only, so
|
|
396
|
+
* enrichment is what gets dropped, never the heartbeat. (Issue #3006 was
|
|
397
|
+
* this exact failure on Host.)
|
|
398
|
+
*/
|
|
399
|
+
logger.warn(
|
|
400
|
+
`ServiceService.updateLastSeen: metadata write failed for service ${serviceId.toString()}, falling back to a liveness-only update: ${
|
|
401
|
+
err instanceof Error ? err.message : String(err)
|
|
402
|
+
}`,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
await this.clearLastSeenGates(cacheKey, hasMetadata);
|
|
406
|
+
|
|
407
|
+
await this.updateColumnsByIdIfUnlockedWithoutHooks({
|
|
408
|
+
id: serviceId,
|
|
409
|
+
data: liveness,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!wrote && hasMetadata) {
|
|
416
|
+
/*
|
|
417
|
+
* The row was locked, so this write was SKIPPED rather than applied. For
|
|
418
|
+
* liveness that is a non-event — whoever holds the lock is storing the
|
|
419
|
+
* same timestamp. Enrichment is different: the holder may be writing an
|
|
420
|
+
* older payload, and the gates we just claimed would suppress ours for
|
|
421
|
+
* the rest of the window. Release them so the next batch re-attempts.
|
|
422
|
+
*
|
|
423
|
+
* This cannot become a retry storm — each attempt is a non-blocking
|
|
424
|
+
* statement that returns immediately when contended.
|
|
425
|
+
*/
|
|
426
|
+
await this.clearLastSeenGates(cacheKey, true);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Re-open the gates so the next batch retries. Liveness is only re-opened
|
|
432
|
+
* when there was no enrichment at stake; otherwise the enrichment gates are
|
|
433
|
+
* the ones that must not stay claimed after a write that never happened.
|
|
434
|
+
*/
|
|
435
|
+
private async clearLastSeenGates(
|
|
436
|
+
cacheKey: string,
|
|
437
|
+
hadMetadata: boolean,
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
const namespaces: Array<string> = hadMetadata
|
|
440
|
+
? [
|
|
441
|
+
METADATA_FINGERPRINT_CACHE_NAMESPACE,
|
|
442
|
+
METADATA_WRITE_WINDOW_CACHE_NAMESPACE,
|
|
443
|
+
]
|
|
444
|
+
: [LAST_SEEN_CACHE_NAMESPACE];
|
|
445
|
+
|
|
446
|
+
for (const namespace of namespaces) {
|
|
447
|
+
try {
|
|
448
|
+
await GlobalCache.deleteKey(namespace, cacheKey);
|
|
449
|
+
} catch {
|
|
450
|
+
// Best effort — the retry below is what actually matters.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
227
453
|
}
|
|
228
454
|
|
|
229
455
|
private fingerprintLastSeenExtras(extra?: {
|