@oneuptime/common 10.0.93 → 10.0.94

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.
Files changed (136) hide show
  1. package/Models/DatabaseModels/DockerResource.ts +497 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/Monitor.ts +83 -0
  4. package/Server/API/MonitorTemplateAPI.ts +182 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1777933061000-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1777972687018-MigrationName.ts +69 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Services/BillingInvoiceService.ts +18 -29
  9. package/Server/Services/DockerResourceService.ts +310 -0
  10. package/Server/Services/MonitorTemplateService.ts +359 -0
  11. package/Types/Dashboard/DashboardComponentType.ts +13 -0
  12. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +16 -0
  13. package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +4 -0
  14. package/Types/Dashboard/DashboardComponents/DashboardDockerContainerListComponent.ts +15 -0
  15. package/Types/Dashboard/DashboardComponents/DashboardDockerHostListComponent.ts +14 -0
  16. package/Types/Dashboard/DashboardComponents/DashboardDockerImageListComponent.ts +15 -0
  17. package/Types/Dashboard/DashboardComponents/DashboardDockerNetworkListComponent.ts +14 -0
  18. package/Types/Dashboard/DashboardComponents/DashboardDockerVolumeListComponent.ts +14 -0
  19. package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +4 -0
  20. package/Types/Dashboard/DashboardComponents/DashboardKubernetesCronJobListComponent.ts +15 -0
  21. package/Types/Dashboard/DashboardComponents/DashboardKubernetesDaemonSetListComponent.ts +15 -0
  22. package/Types/Dashboard/DashboardComponents/DashboardKubernetesDeploymentListComponent.ts +15 -0
  23. package/Types/Dashboard/DashboardComponents/DashboardKubernetesJobListComponent.ts +15 -0
  24. package/Types/Dashboard/DashboardComponents/DashboardKubernetesNamespaceListComponent.ts +14 -0
  25. package/Types/Dashboard/DashboardComponents/DashboardKubernetesNodeListComponent.ts +15 -0
  26. package/Types/Dashboard/DashboardComponents/DashboardKubernetesPodListComponent.ts +16 -0
  27. package/Types/Dashboard/DashboardComponents/DashboardKubernetesStatefulSetListComponent.ts +15 -0
  28. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +3 -0
  29. package/Types/Docker/DockerInventoryExtractor.ts +343 -0
  30. package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +47 -2
  31. package/Utils/Dashboard/Components/DashboardDockerContainerListComponent.ts +93 -0
  32. package/Utils/Dashboard/Components/DashboardDockerHostListComponent.ts +83 -0
  33. package/Utils/Dashboard/Components/DashboardDockerImageListComponent.ts +92 -0
  34. package/Utils/Dashboard/Components/DashboardDockerNetworkListComponent.ts +82 -0
  35. package/Utils/Dashboard/Components/DashboardDockerVolumeListComponent.ts +82 -0
  36. package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +47 -2
  37. package/Utils/Dashboard/Components/DashboardKubernetesCronJobListComponent.ts +36 -0
  38. package/Utils/Dashboard/Components/DashboardKubernetesDaemonSetListComponent.ts +36 -0
  39. package/Utils/Dashboard/Components/DashboardKubernetesDeploymentListComponent.ts +36 -0
  40. package/Utils/Dashboard/Components/DashboardKubernetesJobListComponent.ts +34 -0
  41. package/Utils/Dashboard/Components/DashboardKubernetesNamespaceListComponent.ts +36 -0
  42. package/Utils/Dashboard/Components/DashboardKubernetesNodeListComponent.ts +57 -0
  43. package/Utils/Dashboard/Components/DashboardKubernetesPodListComponent.ts +60 -0
  44. package/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.ts +75 -0
  45. package/Utils/Dashboard/Components/DashboardKubernetesStatefulSetListComponent.ts +36 -0
  46. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +59 -2
  47. package/Utils/Dashboard/Components/Index.ts +102 -0
  48. package/build/dist/Models/DatabaseModels/DockerResource.js +525 -0
  49. package/build/dist/Models/DatabaseModels/DockerResource.js.map +1 -0
  50. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  51. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/Monitor.js +82 -0
  53. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  54. package/build/dist/Server/API/MonitorTemplateAPI.js +108 -0
  55. package/build/dist/Server/API/MonitorTemplateAPI.js.map +1 -0
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777933061000-MigrationName.js +20 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777933061000-MigrationName.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777972687018-MigrationName.js +30 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777972687018-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  62. package/build/dist/Server/Services/BillingInvoiceService.js +22 -25
  63. package/build/dist/Server/Services/BillingInvoiceService.js.map +1 -1
  64. package/build/dist/Server/Services/DockerResourceService.js +196 -0
  65. package/build/dist/Server/Services/DockerResourceService.js.map +1 -0
  66. package/build/dist/Server/Services/MonitorTemplateService.js +290 -0
  67. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  68. package/build/dist/Types/Dashboard/DashboardComponentType.js +13 -0
  69. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  70. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +15 -0
  71. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  72. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerContainerListComponent.js +2 -0
  73. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerContainerListComponent.js.map +1 -0
  74. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerHostListComponent.js +2 -0
  75. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerHostListComponent.js.map +1 -0
  76. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerImageListComponent.js +2 -0
  77. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerImageListComponent.js.map +1 -0
  78. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerNetworkListComponent.js +2 -0
  79. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerNetworkListComponent.js.map +1 -0
  80. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerVolumeListComponent.js +2 -0
  81. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDockerVolumeListComponent.js.map +1 -0
  82. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesCronJobListComponent.js +2 -0
  83. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesCronJobListComponent.js.map +1 -0
  84. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesDaemonSetListComponent.js +2 -0
  85. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesDaemonSetListComponent.js.map +1 -0
  86. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesDeploymentListComponent.js +2 -0
  87. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesDeploymentListComponent.js.map +1 -0
  88. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesJobListComponent.js +2 -0
  89. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesJobListComponent.js.map +1 -0
  90. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesNamespaceListComponent.js +2 -0
  91. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesNamespaceListComponent.js.map +1 -0
  92. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesNodeListComponent.js +2 -0
  93. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesNodeListComponent.js.map +1 -0
  94. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesPodListComponent.js +2 -0
  95. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesPodListComponent.js.map +1 -0
  96. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesStatefulSetListComponent.js +2 -0
  97. package/build/dist/Types/Dashboard/DashboardComponents/DashboardKubernetesStatefulSetListComponent.js.map +1 -0
  98. package/build/dist/Types/Docker/DockerInventoryExtractor.js +293 -0
  99. package/build/dist/Types/Docker/DockerInventoryExtractor.js.map +1 -0
  100. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +43 -3
  101. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -1
  102. package/build/dist/Utils/Dashboard/Components/DashboardDockerContainerListComponent.js +75 -0
  103. package/build/dist/Utils/Dashboard/Components/DashboardDockerContainerListComponent.js.map +1 -0
  104. package/build/dist/Utils/Dashboard/Components/DashboardDockerHostListComponent.js +69 -0
  105. package/build/dist/Utils/Dashboard/Components/DashboardDockerHostListComponent.js.map +1 -0
  106. package/build/dist/Utils/Dashboard/Components/DashboardDockerImageListComponent.js +75 -0
  107. package/build/dist/Utils/Dashboard/Components/DashboardDockerImageListComponent.js.map +1 -0
  108. package/build/dist/Utils/Dashboard/Components/DashboardDockerNetworkListComponent.js +66 -0
  109. package/build/dist/Utils/Dashboard/Components/DashboardDockerNetworkListComponent.js.map +1 -0
  110. package/build/dist/Utils/Dashboard/Components/DashboardDockerVolumeListComponent.js +66 -0
  111. package/build/dist/Utils/Dashboard/Components/DashboardDockerVolumeListComponent.js.map +1 -0
  112. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +43 -3
  113. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -1
  114. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesCronJobListComponent.js +29 -0
  115. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesCronJobListComponent.js.map +1 -0
  116. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesDaemonSetListComponent.js +29 -0
  117. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesDaemonSetListComponent.js.map +1 -0
  118. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesDeploymentListComponent.js +29 -0
  119. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesDeploymentListComponent.js.map +1 -0
  120. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesJobListComponent.js +29 -0
  121. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesJobListComponent.js.map +1 -0
  122. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesNamespaceListComponent.js +29 -0
  123. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesNamespaceListComponent.js.map +1 -0
  124. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesNodeListComponent.js +44 -0
  125. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesNodeListComponent.js.map +1 -0
  126. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesPodListComponent.js +47 -0
  127. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesPodListComponent.js.map +1 -0
  128. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.js +55 -0
  129. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.js.map +1 -0
  130. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesStatefulSetListComponent.js +29 -0
  131. package/build/dist/Utils/Dashboard/Components/DashboardKubernetesStatefulSetListComponent.js.map +1 -0
  132. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +46 -3
  133. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  134. package/build/dist/Utils/Dashboard/Components/Index.js +53 -0
  135. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  136. package/package.json +1 -1
@@ -0,0 +1,310 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/DockerResource";
3
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
4
+ import ObjectID from "../../Types/ObjectID";
5
+ import OneUptimeDate from "../../Types/Date";
6
+ import { JSONObject } from "../../Types/JSON";
7
+ import logger from "../Utils/Logger";
8
+
9
+ /*
10
+ * ------------------------------------------------------------------
11
+ * DockerResourceService
12
+ *
13
+ * Writes and reads the Docker inventory table populated by the
14
+ * telemetry ingest path. Container rows are upserted from the
15
+ * docker_stats receiver metric stream — every container.* metric
16
+ * carries container.id / container.name / container.image.name
17
+ * resource attributes, which is enough to maintain a live inventory
18
+ * of running containers per host without any agent-side change.
19
+ *
20
+ * Image / Network / Volume kinds are reserved for a follow-up agent
21
+ * change that adds a snapshot poller; the schema is ready for them.
22
+ *
23
+ * Rows are hard-deleted once lastSeenAt falls behind the staleness
24
+ * threshold (default: 15 min) so containers that stopped emitting
25
+ * metrics fall off the list automatically.
26
+ * ------------------------------------------------------------------
27
+ */
28
+
29
+ export interface ParsedDockerContainer {
30
+ containerName: string;
31
+ containerId: string | null;
32
+ imageName: string | null;
33
+ state: string;
34
+ cpuPercent: number | null;
35
+ memoryBytes: number | null;
36
+ observedAt: Date;
37
+ }
38
+
39
+ export interface ParsedDockerResource {
40
+ kind: string;
41
+ name: string;
42
+ containerId: string | null;
43
+ imageName: string | null;
44
+ state: string | null;
45
+ labels: JSONObject | null;
46
+ resourceCreationTimestamp: Date | null;
47
+ lastSeenAt: Date;
48
+ }
49
+
50
+ export interface DockerHostInventoryCounts {
51
+ containersRunning: number;
52
+ containersStopped: number;
53
+ containersPaused: number;
54
+ }
55
+
56
+ const UPSERT_BATCH_SIZE: number = 500;
57
+ const STALE_DELETE_WARN_THRESHOLD: number = 100;
58
+
59
+ export class Service extends DatabaseService<Model> {
60
+ public constructor() {
61
+ super(Model);
62
+ }
63
+
64
+ /**
65
+ * Upsert a batch of Container rows for a single (project, host)
66
+ * pair. Uses ON CONFLICT on the UNIQUE (projectId, dockerHostId,
67
+ * kind, name) index with a dominance guard on lastSeenAt so
68
+ * out-of-order ingest never regresses a newer observation.
69
+ *
70
+ * Containers are upserted from metric snapshots (no separate
71
+ * inventory snapshot path), so this also writes
72
+ * latestCpuPercent / latestMemoryBytes / metricsUpdatedAt in the
73
+ * same statement — saves a round trip vs. the K8s pattern of
74
+ * upsert-then-update.
75
+ */
76
+ @CaptureSpan()
77
+ public async bulkUpsertContainers(data: {
78
+ projectId: ObjectID;
79
+ dockerHostId: ObjectID;
80
+ containers: Array<ParsedDockerContainer>;
81
+ }): Promise<void> {
82
+ if (data.containers.length === 0) {
83
+ return;
84
+ }
85
+
86
+ for (
87
+ let i: number = 0;
88
+ i < data.containers.length;
89
+ i += UPSERT_BATCH_SIZE
90
+ ) {
91
+ const chunk: Array<ParsedDockerContainer> = data.containers.slice(
92
+ i,
93
+ i + UPSERT_BATCH_SIZE,
94
+ );
95
+
96
+ const valueFragments: Array<string> = [];
97
+ const params: Array<unknown> = [];
98
+ let p: number = 1;
99
+
100
+ for (const c of chunk) {
101
+ valueFragments.push(
102
+ `($${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}::numeric, $${p++}::bigint, $${p++}::timestamptz, $${p++}::timestamptz, $${p++})`,
103
+ );
104
+ params.push(
105
+ data.projectId.toString(),
106
+ data.dockerHostId.toString(),
107
+ "Container",
108
+ c.containerName,
109
+ c.containerId,
110
+ c.imageName,
111
+ c.state,
112
+ c.cpuPercent !== null && c.cpuPercent !== undefined
113
+ ? c.cpuPercent
114
+ : null,
115
+ c.memoryBytes !== null && c.memoryBytes !== undefined
116
+ ? Math.trunc(c.memoryBytes).toString()
117
+ : null,
118
+ c.observedAt,
119
+ c.observedAt,
120
+ 0, // version (BaseModel @VersionColumn)
121
+ );
122
+ }
123
+
124
+ const sql: string = `
125
+ INSERT INTO "DockerResource" (
126
+ "projectId", "dockerHostId", "kind", "name",
127
+ "containerId", "imageName", "state",
128
+ "latestCpuPercent", "latestMemoryBytes",
129
+ "metricsUpdatedAt", "lastSeenAt", "version"
130
+ )
131
+ VALUES ${valueFragments.join(", ")}
132
+ ON CONFLICT ("projectId", "dockerHostId", "kind", "name")
133
+ DO UPDATE SET
134
+ "containerId" = COALESCE(EXCLUDED."containerId", "DockerResource"."containerId"),
135
+ "imageName" = COALESCE(EXCLUDED."imageName", "DockerResource"."imageName"),
136
+ "state" = EXCLUDED."state",
137
+ "latestCpuPercent" = COALESCE(EXCLUDED."latestCpuPercent", "DockerResource"."latestCpuPercent"),
138
+ "latestMemoryBytes" = COALESCE(EXCLUDED."latestMemoryBytes", "DockerResource"."latestMemoryBytes"),
139
+ "metricsUpdatedAt" = EXCLUDED."metricsUpdatedAt",
140
+ "lastSeenAt" = EXCLUDED."lastSeenAt",
141
+ "updatedAt" = now()
142
+ WHERE EXCLUDED."lastSeenAt" >= "DockerResource"."lastSeenAt"
143
+ `;
144
+
145
+ await this.getRepository().manager.query(sql, params);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Upsert a batch of resources for any kind. Used by the snapshot
151
+ * ingest path (Container / Image / Network / Volume rows from the
152
+ * Docker agent's inventory poller). Container rows from this path
153
+ * carry full state (running / exited / paused / restarting / dead /
154
+ * created), unlike the metric-derived path which only sees running
155
+ * containers.
156
+ */
157
+ @CaptureSpan()
158
+ public async bulkUpsert(data: {
159
+ projectId: ObjectID;
160
+ dockerHostId: ObjectID;
161
+ resources: Array<ParsedDockerResource>;
162
+ }): Promise<void> {
163
+ if (data.resources.length === 0) {
164
+ return;
165
+ }
166
+
167
+ for (let i: number = 0; i < data.resources.length; i += UPSERT_BATCH_SIZE) {
168
+ const chunk: Array<ParsedDockerResource> = data.resources.slice(
169
+ i,
170
+ i + UPSERT_BATCH_SIZE,
171
+ );
172
+
173
+ const valueFragments: Array<string> = [];
174
+ const params: Array<unknown> = [];
175
+ let p: number = 1;
176
+
177
+ for (const r of chunk) {
178
+ valueFragments.push(
179
+ `($${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}::timestamptz, $${p++}::timestamptz, $${p++})`,
180
+ );
181
+ params.push(
182
+ data.projectId.toString(),
183
+ data.dockerHostId.toString(),
184
+ r.kind,
185
+ r.name,
186
+ r.containerId,
187
+ r.imageName,
188
+ r.state,
189
+ r.labels ? JSON.stringify(r.labels) : null,
190
+ r.resourceCreationTimestamp,
191
+ r.lastSeenAt,
192
+ 0, // version
193
+ );
194
+ }
195
+
196
+ const sql: string = `
197
+ INSERT INTO "DockerResource" (
198
+ "projectId", "dockerHostId", "kind", "name",
199
+ "containerId", "imageName", "state", "labels",
200
+ "resourceCreationTimestamp", "lastSeenAt", "version"
201
+ )
202
+ VALUES ${valueFragments.join(", ")}
203
+ ON CONFLICT ("projectId", "dockerHostId", "kind", "name")
204
+ DO UPDATE SET
205
+ "containerId" = COALESCE(EXCLUDED."containerId", "DockerResource"."containerId"),
206
+ "imageName" = COALESCE(EXCLUDED."imageName", "DockerResource"."imageName"),
207
+ "state" = COALESCE(EXCLUDED."state", "DockerResource"."state"),
208
+ "labels" = COALESCE(EXCLUDED."labels", "DockerResource"."labels"),
209
+ "resourceCreationTimestamp" = COALESCE(EXCLUDED."resourceCreationTimestamp", "DockerResource"."resourceCreationTimestamp"),
210
+ "lastSeenAt" = EXCLUDED."lastSeenAt",
211
+ "updatedAt" = now()
212
+ WHERE EXCLUDED."lastSeenAt" >= "DockerResource"."lastSeenAt"
213
+ `;
214
+
215
+ await this.getRepository().manager.query(sql, params);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Hard-delete all resources on a host whose last observation is
221
+ * older than olderThan. Returns the number of deleted rows.
222
+ */
223
+ @CaptureSpan()
224
+ public async deleteStaleForHost(data: {
225
+ dockerHostId: ObjectID;
226
+ olderThan: Date;
227
+ }): Promise<number> {
228
+ const result: Array<{ affected?: number }> | { affected?: number } =
229
+ await this.getRepository().manager.query(
230
+ `DELETE FROM "DockerResource" WHERE "dockerHostId" = $1 AND "lastSeenAt" < $2`,
231
+ [data.dockerHostId.toString(), data.olderThan],
232
+ );
233
+
234
+ let affected: number = 0;
235
+ if (Array.isArray(result) && result.length >= 2) {
236
+ const second: unknown = (result as Array<unknown>)[1];
237
+ if (typeof second === "number") {
238
+ affected = second;
239
+ }
240
+ }
241
+
242
+ if (affected > STALE_DELETE_WARN_THRESHOLD) {
243
+ logger.warn(
244
+ `DockerResource cleanup deleted ${affected} stale rows for host ${data.dockerHostId.toString()} — larger than expected; investigate agent health.`,
245
+ );
246
+ }
247
+
248
+ return affected;
249
+ }
250
+
251
+ /**
252
+ * Compute container state breakdowns for a single host from the
253
+ * inventory table. Used by the cleanup worker to refresh the cached
254
+ * counts on DockerHost so the Hosts page / dashboard widget shows
255
+ * accurate numbers without needing a SQL aggregation per render.
256
+ */
257
+ @CaptureSpan()
258
+ public async getContainerCountsForHost(data: {
259
+ projectId: ObjectID;
260
+ dockerHostId: ObjectID;
261
+ }): Promise<DockerHostInventoryCounts> {
262
+ const rows: Array<{
263
+ running: string;
264
+ stopped: string;
265
+ paused: string;
266
+ }> = await this.getRepository().manager.query(
267
+ `SELECT
268
+ COUNT(*) FILTER (WHERE LOWER("state") = 'running')::text AS "running",
269
+ COUNT(*) FILTER (WHERE LOWER("state") IN ('exited', 'dead', 'created'))::text AS "stopped",
270
+ COUNT(*) FILTER (WHERE LOWER("state") = 'paused')::text AS "paused"
271
+ FROM "DockerResource"
272
+ WHERE "projectId" = $1
273
+ AND "dockerHostId" = $2
274
+ AND "kind" = 'Container'
275
+ AND "deletedAt" IS NULL`,
276
+ [data.projectId.toString(), data.dockerHostId.toString()],
277
+ );
278
+
279
+ const row:
280
+ | { running: string; stopped: string; paused: string }
281
+ | undefined = rows[0];
282
+ return {
283
+ containersRunning: row ? parseInt(row.running, 10) || 0 : 0,
284
+ containersStopped: row ? parseInt(row.stopped, 10) || 0 : 0,
285
+ containersPaused: row ? parseInt(row.paused, 10) || 0 : 0,
286
+ };
287
+ }
288
+
289
+ public getStaleThresholdDate(nowOverride?: Date): Date {
290
+ const minutes: number = this.getStaleThresholdMinutes();
291
+ return OneUptimeDate.addRemoveMinutes(
292
+ nowOverride || OneUptimeDate.getCurrentDate(),
293
+ -minutes,
294
+ );
295
+ }
296
+
297
+ public getStaleThresholdMinutes(): number {
298
+ const raw: string | undefined =
299
+ process.env["DOCKER_INVENTORY_STALE_MINUTES"];
300
+ if (raw) {
301
+ const parsed: number = parseInt(raw, 10);
302
+ if (!isNaN(parsed) && parsed >= 5) {
303
+ return parsed;
304
+ }
305
+ }
306
+ return 15;
307
+ }
308
+ }
309
+
310
+ export default new Service();
@@ -1,9 +1,368 @@
1
1
  import DatabaseService from "./DatabaseService";
2
+ import MonitorService from "./MonitorService";
3
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
4
+ import BadDataException from "../../Types/Exception/BadDataException";
5
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
6
+ import ObjectID from "../../Types/ObjectID";
7
+ import PositiveNumber from "../../Types/PositiveNumber";
2
8
  import Model from "../../Models/DatabaseModels/MonitorTemplate";
9
+ import Monitor from "../../Models/DatabaseModels/Monitor";
10
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
11
+
12
+ export interface SyncLinkedMonitorsResult {
13
+ totalLinkedMonitors: number;
14
+ syncedMonitors: number;
15
+ }
16
+
17
+ /**
18
+ * Subset of Monitor fields that a template push can overwrite. Anything
19
+ * outside this set (name, description, labels, monitorType, etc.) is
20
+ * intentionally never touched by sync — those are per-monitor concerns.
21
+ */
22
+ export type SyncableTemplateField =
23
+ | "monitorSteps"
24
+ | "monitoringInterval"
25
+ | "minimumProbeAgreement";
26
+
27
+ const ALL_SYNCABLE_FIELDS: ReadonlyArray<SyncableTemplateField> = [
28
+ "monitorSteps",
29
+ "monitoringInterval",
30
+ "minimumProbeAgreement",
31
+ ];
3
32
 
4
33
  export class Service extends DatabaseService<Model> {
5
34
  public constructor() {
6
35
  super(Model);
7
36
  }
37
+
38
+ /**
39
+ * Count monitors created from this template.
40
+ * Caller must already have read access on the template via the API layer.
41
+ */
42
+ @CaptureSpan()
43
+ public async countLinkedMonitors(data: {
44
+ monitorTemplateId: ObjectID;
45
+ projectId: ObjectID;
46
+ }): Promise<number> {
47
+ const count: PositiveNumber = await MonitorService.countBy({
48
+ query: {
49
+ monitorTemplateId: data.monitorTemplateId,
50
+ projectId: data.projectId,
51
+ },
52
+ props: {
53
+ isRoot: true,
54
+ },
55
+ });
56
+
57
+ return count.toNumber();
58
+ }
59
+
60
+ /**
61
+ * Validate and narrow a list of field names to the syncable subset.
62
+ * Anything not in the whitelist throws — we never silently drop a field the
63
+ * caller asked for, that would mask UI bugs.
64
+ */
65
+ private validateSyncableFields(
66
+ fields: Array<string> | undefined,
67
+ ): Array<SyncableTemplateField> {
68
+ if (!fields || fields.length === 0) {
69
+ return [...ALL_SYNCABLE_FIELDS];
70
+ }
71
+
72
+ const allowed: Set<string> = new Set(ALL_SYNCABLE_FIELDS);
73
+ for (const field of fields) {
74
+ if (!allowed.has(field)) {
75
+ throw new BadDataException(
76
+ `Field "${field}" is not syncable from a monitor template`,
77
+ );
78
+ }
79
+ }
80
+ return fields as Array<SyncableTemplateField>;
81
+ }
82
+
83
+ private buildUpdateData(
84
+ template: Model,
85
+ fields: Array<SyncableTemplateField>,
86
+ ): Partial<Monitor> {
87
+ const updateData: Partial<Monitor> = {};
88
+
89
+ for (const field of fields) {
90
+ const value: unknown = (template as unknown as Record<string, unknown>)[
91
+ field
92
+ ];
93
+ if (value === undefined) {
94
+ continue;
95
+ }
96
+ (updateData as unknown as Record<string, unknown>)[field] = value;
97
+ }
98
+
99
+ return updateData;
100
+ }
101
+
102
+ /**
103
+ * Push the template's current configuration onto every monitor that was
104
+ * created from it. Sync is intentionally explicit (button-triggered) so a
105
+ * config tweak doesn't silently re-deploy across the whole fleet.
106
+ *
107
+ * Pass `fields` to scope the sync — e.g. `["monitorSteps"]` to push only the
108
+ * criteria. If omitted, every syncable field is pushed.
109
+ */
110
+ @CaptureSpan()
111
+ public async syncLinkedMonitors(data: {
112
+ monitorTemplateId: ObjectID;
113
+ props: DatabaseCommonInteractionProps;
114
+ fields?: Array<string>;
115
+ }): Promise<SyncLinkedMonitorsResult> {
116
+ const fields: Array<SyncableTemplateField> = this.validateSyncableFields(
117
+ data.fields,
118
+ );
119
+
120
+ const template: Model | null = await this.findOneById({
121
+ id: data.monitorTemplateId,
122
+ select: {
123
+ _id: true,
124
+ projectId: true,
125
+ monitorSteps: true,
126
+ monitoringInterval: true,
127
+ minimumProbeAgreement: true,
128
+ },
129
+ props: data.props,
130
+ });
131
+
132
+ if (!template) {
133
+ throw new BadDataException("Monitor template not found");
134
+ }
135
+
136
+ if (!template.projectId) {
137
+ throw new BadDataException("Monitor template is missing projectId");
138
+ }
139
+
140
+ const totalLinkedMonitors: number = await this.countLinkedMonitors({
141
+ monitorTemplateId: template.id!,
142
+ projectId: template.projectId,
143
+ });
144
+
145
+ if (totalLinkedMonitors === 0) {
146
+ return {
147
+ totalLinkedMonitors: 0,
148
+ syncedMonitors: 0,
149
+ };
150
+ }
151
+
152
+ const updateData: Partial<Monitor> = this.buildUpdateData(template, fields);
153
+
154
+ if (Object.keys(updateData).length === 0) {
155
+ return {
156
+ totalLinkedMonitors,
157
+ syncedMonitors: 0,
158
+ };
159
+ }
160
+
161
+ const syncedMonitors: number = await MonitorService.updateBy({
162
+ query: {
163
+ monitorTemplateId: template.id!,
164
+ projectId: template.projectId,
165
+ },
166
+ data: updateData as any,
167
+ limit: LIMIT_MAX,
168
+ skip: 0,
169
+ props: data.props,
170
+ });
171
+
172
+ return {
173
+ totalLinkedMonitors,
174
+ syncedMonitors,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Sync the template's current configuration onto a single monitor that was
180
+ * created from it. The monitor must be linked to this template — passing an
181
+ * arbitrary monitor ID is rejected so the endpoint can't be tricked into
182
+ * pushing config to an unrelated monitor.
183
+ *
184
+ * Pass `fields` to scope the sync; if omitted, every syncable field is
185
+ * pushed.
186
+ */
187
+ @CaptureSpan()
188
+ public async syncToMonitor(data: {
189
+ monitorTemplateId: ObjectID;
190
+ monitorId: ObjectID;
191
+ props: DatabaseCommonInteractionProps;
192
+ fields?: Array<string>;
193
+ }): Promise<void> {
194
+ const fields: Array<SyncableTemplateField> = this.validateSyncableFields(
195
+ data.fields,
196
+ );
197
+
198
+ const template: Model | null = await this.findOneById({
199
+ id: data.monitorTemplateId,
200
+ select: {
201
+ _id: true,
202
+ projectId: true,
203
+ monitorSteps: true,
204
+ monitoringInterval: true,
205
+ minimumProbeAgreement: true,
206
+ },
207
+ props: data.props,
208
+ });
209
+
210
+ if (!template) {
211
+ throw new BadDataException("Monitor template not found");
212
+ }
213
+
214
+ if (!template.projectId) {
215
+ throw new BadDataException("Monitor template is missing projectId");
216
+ }
217
+
218
+ const monitor: Monitor | null = await MonitorService.findOneById({
219
+ id: data.monitorId,
220
+ select: {
221
+ _id: true,
222
+ projectId: true,
223
+ monitorTemplateId: true,
224
+ },
225
+ props: { isRoot: true },
226
+ });
227
+
228
+ if (!monitor) {
229
+ throw new BadDataException("Monitor not found");
230
+ }
231
+
232
+ if (
233
+ !monitor.monitorTemplateId ||
234
+ monitor.monitorTemplateId.toString() !== template.id!.toString()
235
+ ) {
236
+ throw new BadDataException("Monitor is not linked to this template");
237
+ }
238
+
239
+ if (
240
+ !monitor.projectId ||
241
+ monitor.projectId.toString() !== template.projectId.toString()
242
+ ) {
243
+ throw new BadDataException(
244
+ "Monitor and template belong to different projects",
245
+ );
246
+ }
247
+
248
+ const updateData: Partial<Monitor> = this.buildUpdateData(template, fields);
249
+
250
+ if (Object.keys(updateData).length === 0) {
251
+ return;
252
+ }
253
+
254
+ await MonitorService.updateOneById({
255
+ id: data.monitorId,
256
+ data: updateData as any,
257
+ props: data.props,
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Link an existing monitor to this template. The monitor must be in the same
263
+ * project AND have the same monitorType as the template — anything else is
264
+ * rejected, so a user can't (e.g.) link an API monitor to a Server-monitor
265
+ * template and then sync incompatible criteria onto it.
266
+ */
267
+ @CaptureSpan()
268
+ public async linkMonitor(data: {
269
+ monitorTemplateId: ObjectID;
270
+ monitorId: ObjectID;
271
+ props: DatabaseCommonInteractionProps;
272
+ }): Promise<void> {
273
+ const template: Model | null = await this.findOneById({
274
+ id: data.monitorTemplateId,
275
+ select: {
276
+ _id: true,
277
+ projectId: true,
278
+ monitorType: true,
279
+ },
280
+ props: data.props,
281
+ });
282
+
283
+ if (!template) {
284
+ throw new BadDataException("Monitor template not found");
285
+ }
286
+ if (!template.projectId) {
287
+ throw new BadDataException("Monitor template is missing projectId");
288
+ }
289
+ if (!template.monitorType) {
290
+ throw new BadDataException("Monitor template is missing monitorType");
291
+ }
292
+
293
+ const monitor: Monitor | null = await MonitorService.findOneById({
294
+ id: data.monitorId,
295
+ select: {
296
+ _id: true,
297
+ projectId: true,
298
+ monitorType: true,
299
+ },
300
+ props: data.props,
301
+ });
302
+
303
+ if (!monitor) {
304
+ throw new BadDataException("Monitor not found");
305
+ }
306
+ if (
307
+ !monitor.projectId ||
308
+ monitor.projectId.toString() !== template.projectId.toString()
309
+ ) {
310
+ throw new BadDataException(
311
+ "Monitor and template belong to different projects",
312
+ );
313
+ }
314
+ if (monitor.monitorType !== template.monitorType) {
315
+ throw new BadDataException(
316
+ `Monitor type "${monitor.monitorType}" does not match template type "${template.monitorType}"`,
317
+ );
318
+ }
319
+
320
+ await MonitorService.updateOneById({
321
+ id: data.monitorId,
322
+ data: {
323
+ monitorTemplateId: template.id!,
324
+ } as any,
325
+ props: data.props,
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Detach a monitor from this template. The monitor must currently be linked
331
+ * to *this* template — passing a monitor linked elsewhere (or unlinked) is
332
+ * rejected so a stale UI can't accidentally clear someone else's link.
333
+ */
334
+ @CaptureSpan()
335
+ public async unlinkMonitor(data: {
336
+ monitorTemplateId: ObjectID;
337
+ monitorId: ObjectID;
338
+ props: DatabaseCommonInteractionProps;
339
+ }): Promise<void> {
340
+ const monitor: Monitor | null = await MonitorService.findOneById({
341
+ id: data.monitorId,
342
+ select: {
343
+ _id: true,
344
+ monitorTemplateId: true,
345
+ },
346
+ props: data.props,
347
+ });
348
+
349
+ if (!monitor) {
350
+ throw new BadDataException("Monitor not found");
351
+ }
352
+ if (
353
+ !monitor.monitorTemplateId ||
354
+ monitor.monitorTemplateId.toString() !== data.monitorTemplateId.toString()
355
+ ) {
356
+ throw new BadDataException("Monitor is not linked to this template");
357
+ }
358
+
359
+ await MonitorService.updateOneById({
360
+ id: data.monitorId,
361
+ data: {
362
+ monitorTemplateId: null,
363
+ } as any,
364
+ props: data.props,
365
+ });
366
+ }
8
367
  }
9
368
  export default new Service();
@@ -9,6 +9,19 @@ enum DashboardComponentType {
9
9
  IncidentList = `IncidentList`,
10
10
  AlertList = `AlertList`,
11
11
  MonitorList = `MonitorList`,
12
+ KubernetesPodList = `KubernetesPodList`,
13
+ KubernetesNodeList = `KubernetesNodeList`,
14
+ KubernetesNamespaceList = `KubernetesNamespaceList`,
15
+ KubernetesDeploymentList = `KubernetesDeploymentList`,
16
+ KubernetesStatefulSetList = `KubernetesStatefulSetList`,
17
+ KubernetesDaemonSetList = `KubernetesDaemonSetList`,
18
+ KubernetesJobList = `KubernetesJobList`,
19
+ KubernetesCronJobList = `KubernetesCronJobList`,
20
+ DockerHostList = `DockerHostList`,
21
+ DockerContainerList = `DockerContainerList`,
22
+ DockerImageList = `DockerImageList`,
23
+ DockerNetworkList = `DockerNetworkList`,
24
+ DockerVolumeList = `DockerVolumeList`,
12
25
  }
13
26
 
14
27
  export default DashboardComponentType;