@oneuptime/common 10.2.16 → 10.2.18
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/Server/Services/DockerHostService.ts +91 -0
- package/Server/Services/KubernetesClusterService.ts +92 -0
- package/Types/Dashboard/DashboardComponents/DashboardValueComponent.ts +15 -0
- package/Types/Dashboard/DashboardTemplates.ts +33 -2
- package/UI/Components/Charts/Utils/DataPoint.ts +0 -0
- package/Utils/Dashboard/Components/DashboardValueComponent.ts +36 -2
- package/Utils/ValueFormatter.ts +36 -0
- package/build/dist/Server/Services/DockerHostService.js +73 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +74 -0
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardValueComponent.js +14 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardValueComponent.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +26 -2
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +0 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js +31 -1
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +31 -0
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/DockerHost";
|
|
3
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
3
4
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
6
7
|
import OneUptimeDate from "../../Types/Date";
|
|
7
8
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
9
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
10
|
+
import logger from "../Utils/Logger";
|
|
9
11
|
import crypto from "crypto";
|
|
10
12
|
|
|
11
13
|
const LAST_SEEN_CACHE_NAMESPACE: string = "docker-host-last-seen";
|
|
12
14
|
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
13
15
|
|
|
16
|
+
const LABELS_APPLIED_CACHE_NAMESPACE: string = "docker-host-labels-applied";
|
|
17
|
+
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
18
|
+
|
|
14
19
|
export class Service extends DatabaseService<Model> {
|
|
15
20
|
public constructor() {
|
|
16
21
|
super(Model);
|
|
@@ -145,6 +150,83 @@ export class Service extends DatabaseService<Model> {
|
|
|
145
150
|
});
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Additively attach labels to a Docker host. Existing labels are
|
|
155
|
+
* never removed — manual labels set via the UI survive ingest. The
|
|
156
|
+
* set of labelIds passed in is fingerprinted and cached for 60s so
|
|
157
|
+
* the common case (steady-state collector pushing the same label
|
|
158
|
+
* set every batch) costs one in-memory lookup, not a join-table
|
|
159
|
+
* scan.
|
|
160
|
+
*/
|
|
161
|
+
@CaptureSpan()
|
|
162
|
+
public async attachLabels(data: {
|
|
163
|
+
dockerHostId: ObjectID;
|
|
164
|
+
labelIds: Array<ObjectID>;
|
|
165
|
+
}): Promise<void> {
|
|
166
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cacheKey: string = data.dockerHostId.toString();
|
|
171
|
+
const fingerprint: string = fingerprintLabelIds(data.labelIds);
|
|
172
|
+
const cached: string | null = await GlobalCache.getString(
|
|
173
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
174
|
+
cacheKey,
|
|
175
|
+
);
|
|
176
|
+
if (cached === fingerprint) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const dockerHostIdStr: string = data.dockerHostId.toString();
|
|
182
|
+
const existingLabels: Array<Label> = await this.getRepository()
|
|
183
|
+
.createQueryBuilder()
|
|
184
|
+
.relation(Model, "labels")
|
|
185
|
+
.of(dockerHostIdStr)
|
|
186
|
+
.loadMany();
|
|
187
|
+
|
|
188
|
+
const existingIds: Set<string> = new Set();
|
|
189
|
+
for (const lbl of existingLabels) {
|
|
190
|
+
const idStr: string | undefined = lbl._id?.toString();
|
|
191
|
+
if (idStr) {
|
|
192
|
+
existingIds.add(idStr);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const toAddIds: Array<string> = [];
|
|
197
|
+
const seen: Set<string> = new Set();
|
|
198
|
+
for (const id of data.labelIds) {
|
|
199
|
+
const idStr: string = id.toString();
|
|
200
|
+
if (existingIds.has(idStr) || seen.has(idStr)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
seen.add(idStr);
|
|
204
|
+
toAddIds.push(idStr);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (toAddIds.length > 0) {
|
|
208
|
+
await this.getRepository()
|
|
209
|
+
.createQueryBuilder()
|
|
210
|
+
.relation(Model, "labels")
|
|
211
|
+
.of(dockerHostIdStr)
|
|
212
|
+
.add(toAddIds);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await GlobalCache.setString(
|
|
216
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
217
|
+
cacheKey,
|
|
218
|
+
fingerprint,
|
|
219
|
+
{ expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS },
|
|
220
|
+
);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
logger.warn(
|
|
223
|
+
`DockerHostService.attachLabels failed for docker host ${data.dockerHostId.toString()}: ${
|
|
224
|
+
err instanceof Error ? err.message : String(err)
|
|
225
|
+
}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
148
230
|
@CaptureSpan()
|
|
149
231
|
public async markDisconnectedHosts(): Promise<void> {
|
|
150
232
|
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
|
@@ -183,4 +265,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
183
265
|
}
|
|
184
266
|
}
|
|
185
267
|
|
|
268
|
+
function fingerprintLabelIds(labelIds: Array<ObjectID>): string {
|
|
269
|
+
const sorted: Array<string> = labelIds
|
|
270
|
+
.map((id: ObjectID) => {
|
|
271
|
+
return id.toString();
|
|
272
|
+
})
|
|
273
|
+
.sort();
|
|
274
|
+
return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
|
|
275
|
+
}
|
|
276
|
+
|
|
186
277
|
export default new Service();
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/KubernetesCluster";
|
|
3
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
3
4
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
4
5
|
import ObjectID from "../../Types/ObjectID";
|
|
5
6
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
6
7
|
import OneUptimeDate from "../../Types/Date";
|
|
7
8
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
9
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
10
|
+
import logger from "../Utils/Logger";
|
|
11
|
+
import crypto from "crypto";
|
|
9
12
|
|
|
10
13
|
const LAST_SEEN_CACHE_NAMESPACE: string = "k8s-cluster-last-seen";
|
|
11
14
|
const LAST_SEEN_THROTTLE_SECONDS: number = 60;
|
|
12
15
|
|
|
16
|
+
const LABELS_APPLIED_CACHE_NAMESPACE: string = "k8s-cluster-labels-applied";
|
|
17
|
+
const LABELS_APPLIED_CACHE_TTL_SECONDS: number = 60;
|
|
18
|
+
|
|
13
19
|
export class Service extends DatabaseService<Model> {
|
|
14
20
|
public constructor() {
|
|
15
21
|
super(Model);
|
|
@@ -116,6 +122,83 @@ export class Service extends DatabaseService<Model> {
|
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Additively attach labels to a Kubernetes cluster. Existing labels
|
|
127
|
+
* are never removed — manual labels set via the UI survive ingest.
|
|
128
|
+
* The set of labelIds passed in is fingerprinted and cached for 60s
|
|
129
|
+
* so the common case (steady-state collector pushing the same label
|
|
130
|
+
* set every batch) costs one in-memory lookup, not a join-table
|
|
131
|
+
* scan.
|
|
132
|
+
*/
|
|
133
|
+
@CaptureSpan()
|
|
134
|
+
public async attachLabels(data: {
|
|
135
|
+
kubernetesClusterId: ObjectID;
|
|
136
|
+
labelIds: Array<ObjectID>;
|
|
137
|
+
}): Promise<void> {
|
|
138
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const cacheKey: string = data.kubernetesClusterId.toString();
|
|
143
|
+
const fingerprint: string = fingerprintLabelIds(data.labelIds);
|
|
144
|
+
const cached: string | null = await GlobalCache.getString(
|
|
145
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
146
|
+
cacheKey,
|
|
147
|
+
);
|
|
148
|
+
if (cached === fingerprint) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const clusterIdStr: string = data.kubernetesClusterId.toString();
|
|
154
|
+
const existingLabels: Array<Label> = await this.getRepository()
|
|
155
|
+
.createQueryBuilder()
|
|
156
|
+
.relation(Model, "labels")
|
|
157
|
+
.of(clusterIdStr)
|
|
158
|
+
.loadMany();
|
|
159
|
+
|
|
160
|
+
const existingIds: Set<string> = new Set();
|
|
161
|
+
for (const lbl of existingLabels) {
|
|
162
|
+
const idStr: string | undefined = lbl._id?.toString();
|
|
163
|
+
if (idStr) {
|
|
164
|
+
existingIds.add(idStr);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const toAddIds: Array<string> = [];
|
|
169
|
+
const seen: Set<string> = new Set();
|
|
170
|
+
for (const id of data.labelIds) {
|
|
171
|
+
const idStr: string = id.toString();
|
|
172
|
+
if (existingIds.has(idStr) || seen.has(idStr)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seen.add(idStr);
|
|
176
|
+
toAddIds.push(idStr);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (toAddIds.length > 0) {
|
|
180
|
+
await this.getRepository()
|
|
181
|
+
.createQueryBuilder()
|
|
182
|
+
.relation(Model, "labels")
|
|
183
|
+
.of(clusterIdStr)
|
|
184
|
+
.add(toAddIds);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await GlobalCache.setString(
|
|
188
|
+
LABELS_APPLIED_CACHE_NAMESPACE,
|
|
189
|
+
cacheKey,
|
|
190
|
+
fingerprint,
|
|
191
|
+
{ expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS },
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.warn(
|
|
195
|
+
`KubernetesClusterService.attachLabels failed for cluster ${data.kubernetesClusterId.toString()}: ${
|
|
196
|
+
err instanceof Error ? err.message : String(err)
|
|
197
|
+
}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
119
202
|
@CaptureSpan()
|
|
120
203
|
public async markDisconnectedClusters(): Promise<void> {
|
|
121
204
|
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
|
@@ -154,4 +237,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
154
237
|
}
|
|
155
238
|
}
|
|
156
239
|
|
|
240
|
+
function fingerprintLabelIds(labelIds: Array<ObjectID>): string {
|
|
241
|
+
const sorted: Array<string> = labelIds
|
|
242
|
+
.map((id: ObjectID) => {
|
|
243
|
+
return id.toString();
|
|
244
|
+
})
|
|
245
|
+
.sort();
|
|
246
|
+
return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
|
|
247
|
+
}
|
|
248
|
+
|
|
157
249
|
export default new Service();
|
|
@@ -3,6 +3,20 @@ import ObjectID from "../../ObjectID";
|
|
|
3
3
|
import DashboardComponentType from "../DashboardComponentType";
|
|
4
4
|
import BaseComponent from "./DashboardBaseComponent";
|
|
5
5
|
|
|
6
|
+
/*
|
|
7
|
+
* "Auto" defers to a metric-name heuristic in ValueFormatter
|
|
8
|
+
* (`isHigherWorseMetric`) — that lets templates skip the field unless
|
|
9
|
+
* they need to override. "HigherIsBetter" forces ↑ = green / ↓ = red
|
|
10
|
+
* (e.g. uptime, throughput, success rate). "HigherIsWorse" inverts so a
|
|
11
|
+
* rising incident count, error rate, latency, or restart count is shown
|
|
12
|
+
* in red.
|
|
13
|
+
*/
|
|
14
|
+
export enum DashboardValueTrendDirection {
|
|
15
|
+
Auto = "Auto",
|
|
16
|
+
HigherIsBetter = "HigherIsBetter",
|
|
17
|
+
HigherIsWorse = "HigherIsWorse",
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export default interface DashboardValueComponent extends BaseComponent {
|
|
7
21
|
componentType: DashboardComponentType.Value;
|
|
8
22
|
componentId: ObjectID;
|
|
@@ -11,5 +25,6 @@ export default interface DashboardValueComponent extends BaseComponent {
|
|
|
11
25
|
title: string;
|
|
12
26
|
warningThreshold?: number | undefined;
|
|
13
27
|
criticalThreshold?: number | undefined;
|
|
28
|
+
trendDirection?: DashboardValueTrendDirection | undefined;
|
|
14
29
|
};
|
|
15
30
|
}
|
|
@@ -10,6 +10,7 @@ import MetricsAggregationType from "../Metrics/MetricsAggregationType";
|
|
|
10
10
|
import IncidentMetricType from "../Incident/IncidentMetricType";
|
|
11
11
|
import MonitorMetricType from "../Monitor/MonitorMetricType";
|
|
12
12
|
import MetricDashboardMetricType from "../Metrics/MetricDashboardMetricType";
|
|
13
|
+
import { DashboardValueTrendDirection } from "./DashboardComponents/DashboardValueComponent";
|
|
13
14
|
|
|
14
15
|
/*
|
|
15
16
|
* Trace / Exception / Profiles entries are intentionally not in this
|
|
@@ -147,6 +148,13 @@ function createValueComponent(data: {
|
|
|
147
148
|
left: number;
|
|
148
149
|
width: number;
|
|
149
150
|
metricConfig?: MetricConfig;
|
|
151
|
+
/*
|
|
152
|
+
* Per-widget override for the trend-arrow colour. Leave `undefined` to
|
|
153
|
+
* let the renderer apply its metric-name heuristic (incident counts,
|
|
154
|
+
* error rates, latency, CPU/memory usage flip the colour); set
|
|
155
|
+
* explicitly when the heuristic would guess wrong.
|
|
156
|
+
*/
|
|
157
|
+
trendDirection?: DashboardValueTrendDirection;
|
|
150
158
|
}): DashboardBaseComponent {
|
|
151
159
|
return {
|
|
152
160
|
_type: ObjectType.DashboardComponent,
|
|
@@ -168,6 +176,7 @@ function createValueComponent(data: {
|
|
|
168
176
|
groupBy: undefined,
|
|
169
177
|
},
|
|
170
178
|
},
|
|
179
|
+
trendDirection: data.trendDirection,
|
|
171
180
|
},
|
|
172
181
|
};
|
|
173
182
|
}
|
|
@@ -390,6 +399,7 @@ function createMonitorDashboardConfig(): DashboardViewConfig {
|
|
|
390
399
|
aggregationType: MetricsAggregationType.Avg,
|
|
391
400
|
legendUnit: "ms",
|
|
392
401
|
},
|
|
402
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
393
403
|
}),
|
|
394
404
|
/*
|
|
395
405
|
* IsOnline is emitted as 0/1 with unit "" by MonitorMetricUtil, so
|
|
@@ -407,6 +417,7 @@ function createMonitorDashboardConfig(): DashboardViewConfig {
|
|
|
407
417
|
metricName: MonitorMetricType.IsOnline,
|
|
408
418
|
aggregationType: MetricsAggregationType.Avg,
|
|
409
419
|
},
|
|
420
|
+
trendDirection: DashboardValueTrendDirection.HigherIsBetter,
|
|
410
421
|
}),
|
|
411
422
|
/*
|
|
412
423
|
* ResponseStatusCode is the literal HTTP status code (200, 404,
|
|
@@ -424,6 +435,7 @@ function createMonitorDashboardConfig(): DashboardViewConfig {
|
|
|
424
435
|
metricName: MonitorMetricType.ResponseStatusCode,
|
|
425
436
|
aggregationType: MetricsAggregationType.Count,
|
|
426
437
|
},
|
|
438
|
+
trendDirection: DashboardValueTrendDirection.HigherIsBetter,
|
|
427
439
|
}),
|
|
428
440
|
createValueComponent({
|
|
429
441
|
title: "Execution Time",
|
|
@@ -435,6 +447,7 @@ function createMonitorDashboardConfig(): DashboardViewConfig {
|
|
|
435
447
|
aggregationType: MetricsAggregationType.Avg,
|
|
436
448
|
legendUnit: "ms",
|
|
437
449
|
},
|
|
450
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
438
451
|
}),
|
|
439
452
|
|
|
440
453
|
// Row 2-4: Charts
|
|
@@ -586,7 +599,7 @@ function createIncidentDashboardConfig(): DashboardViewConfig {
|
|
|
586
599
|
isBold: true,
|
|
587
600
|
}),
|
|
588
601
|
|
|
589
|
-
// Row 1: Key incident metrics
|
|
602
|
+
// Row 1: Key incident metrics — every one is "higher = worse".
|
|
590
603
|
createValueComponent({
|
|
591
604
|
title: "Incident Count",
|
|
592
605
|
top: 1,
|
|
@@ -596,6 +609,7 @@ function createIncidentDashboardConfig(): DashboardViewConfig {
|
|
|
596
609
|
metricName: IncidentMetricType.IncidentCount,
|
|
597
610
|
aggregationType: MetricsAggregationType.Sum,
|
|
598
611
|
},
|
|
612
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
599
613
|
}),
|
|
600
614
|
createValueComponent({
|
|
601
615
|
title: "MTTR",
|
|
@@ -606,6 +620,7 @@ function createIncidentDashboardConfig(): DashboardViewConfig {
|
|
|
606
620
|
metricName: IncidentMetricType.TimeToResolve,
|
|
607
621
|
aggregationType: MetricsAggregationType.Avg,
|
|
608
622
|
},
|
|
623
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
609
624
|
}),
|
|
610
625
|
createValueComponent({
|
|
611
626
|
title: "MTTA",
|
|
@@ -616,6 +631,7 @@ function createIncidentDashboardConfig(): DashboardViewConfig {
|
|
|
616
631
|
metricName: IncidentMetricType.TimeToAcknowledge,
|
|
617
632
|
aggregationType: MetricsAggregationType.Avg,
|
|
618
633
|
},
|
|
634
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
619
635
|
}),
|
|
620
636
|
createValueComponent({
|
|
621
637
|
title: "Avg Duration",
|
|
@@ -626,6 +642,7 @@ function createIncidentDashboardConfig(): DashboardViewConfig {
|
|
|
626
642
|
metricName: IncidentMetricType.IncidentDuration,
|
|
627
643
|
aggregationType: MetricsAggregationType.Avg,
|
|
628
644
|
},
|
|
645
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
629
646
|
}),
|
|
630
647
|
|
|
631
648
|
// Row 2-4: Incident trends
|
|
@@ -843,6 +860,7 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
843
860
|
/*
|
|
844
861
|
* Row 1: Key cluster metrics — averages render with proper units via
|
|
845
862
|
* ValueFormatter (CPU utilization → "%", memory.usage → "MB"/"GB").
|
|
863
|
+
* All four are "higher = worse" (closer to capacity = bad).
|
|
846
864
|
*/
|
|
847
865
|
createValueComponent({
|
|
848
866
|
title: "Pod CPU (avg)",
|
|
@@ -853,6 +871,7 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
853
871
|
metricName: "k8s.pod.cpu.utilization",
|
|
854
872
|
aggregationType: MetricsAggregationType.Avg,
|
|
855
873
|
},
|
|
874
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
856
875
|
}),
|
|
857
876
|
createValueComponent({
|
|
858
877
|
title: "Pod Memory (avg)",
|
|
@@ -863,6 +882,7 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
863
882
|
metricName: "k8s.pod.memory.usage",
|
|
864
883
|
aggregationType: MetricsAggregationType.Avg,
|
|
865
884
|
},
|
|
885
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
866
886
|
}),
|
|
867
887
|
createValueComponent({
|
|
868
888
|
title: "Node CPU (avg)",
|
|
@@ -873,6 +893,7 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
873
893
|
metricName: "k8s.node.cpu.utilization",
|
|
874
894
|
aggregationType: MetricsAggregationType.Avg,
|
|
875
895
|
},
|
|
896
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
876
897
|
}),
|
|
877
898
|
createValueComponent({
|
|
878
899
|
title: "Node Memory (avg)",
|
|
@@ -883,6 +904,7 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
883
904
|
metricName: "k8s.node.memory.usage",
|
|
884
905
|
aggregationType: MetricsAggregationType.Avg,
|
|
885
906
|
},
|
|
907
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
886
908
|
}),
|
|
887
909
|
|
|
888
910
|
// Row 2-4: Resource usage charts
|
|
@@ -1075,7 +1097,11 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1075
1097
|
isBold: true,
|
|
1076
1098
|
}),
|
|
1077
1099
|
|
|
1078
|
-
|
|
1100
|
+
/*
|
|
1101
|
+
* Row 1: Key HTTP metrics. Request volume rising is generally a
|
|
1102
|
+
* sign of activity (good); latency, errors, and active in-flight
|
|
1103
|
+
* requests rising signal saturation or trouble (bad).
|
|
1104
|
+
*/
|
|
1079
1105
|
createValueComponent({
|
|
1080
1106
|
title: "Request Rate",
|
|
1081
1107
|
top: 1,
|
|
@@ -1086,6 +1112,7 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1086
1112
|
aggregationType: MetricsAggregationType.Sum,
|
|
1087
1113
|
legendUnit: "req/s",
|
|
1088
1114
|
},
|
|
1115
|
+
trendDirection: DashboardValueTrendDirection.HigherIsBetter,
|
|
1089
1116
|
}),
|
|
1090
1117
|
createValueComponent({
|
|
1091
1118
|
title: "Avg Latency",
|
|
@@ -1097,6 +1124,7 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1097
1124
|
aggregationType: MetricsAggregationType.Avg,
|
|
1098
1125
|
legendUnit: "ms",
|
|
1099
1126
|
},
|
|
1127
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
1100
1128
|
}),
|
|
1101
1129
|
createValueComponent({
|
|
1102
1130
|
title: "Error Rate",
|
|
@@ -1108,6 +1136,7 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1108
1136
|
aggregationType: MetricsAggregationType.Avg,
|
|
1109
1137
|
legendUnit: "%",
|
|
1110
1138
|
},
|
|
1139
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
1111
1140
|
}),
|
|
1112
1141
|
createValueComponent({
|
|
1113
1142
|
title: "Active Requests",
|
|
@@ -1118,6 +1147,7 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1118
1147
|
metricName: MetricDashboardMetricType.HttpActiveRequests,
|
|
1119
1148
|
aggregationType: MetricsAggregationType.Avg,
|
|
1120
1149
|
},
|
|
1150
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
1121
1151
|
}),
|
|
1122
1152
|
|
|
1123
1153
|
// Row 2-4: HTTP request charts
|
|
@@ -1230,6 +1260,7 @@ function createMetricsDashboardConfig(): DashboardViewConfig {
|
|
|
1230
1260
|
metricName: MetricDashboardMetricType.SystemMemoryUsage,
|
|
1231
1261
|
aggregationType: MetricsAggregationType.Avg,
|
|
1232
1262
|
},
|
|
1263
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
1233
1264
|
}),
|
|
1234
1265
|
createChartComponent({
|
|
1235
1266
|
title: "CPU Usage Over Time",
|
|
Binary file
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
ComponentArgumentSection,
|
|
4
4
|
ComponentInputType,
|
|
5
5
|
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
|
6
|
-
import DashboardValueComponent
|
|
6
|
+
import DashboardValueComponent, {
|
|
7
|
+
DashboardValueTrendDirection,
|
|
8
|
+
} from "../../../Types/Dashboard/DashboardComponents/DashboardValueComponent";
|
|
7
9
|
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
|
8
10
|
import { ObjectType } from "../../../Types/JSON";
|
|
9
11
|
import ObjectID from "../../../Types/ObjectID";
|
|
@@ -15,10 +17,17 @@ const DataSourceSection: ComponentArgumentSection = {
|
|
|
15
17
|
order: 1,
|
|
16
18
|
};
|
|
17
19
|
|
|
20
|
+
const DisplaySection: ComponentArgumentSection = {
|
|
21
|
+
name: "Display",
|
|
22
|
+
description: "Tune how the trend arrow is coloured",
|
|
23
|
+
order: 2,
|
|
24
|
+
defaultCollapsed: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
const ThresholdsSection: ComponentArgumentSection = {
|
|
19
28
|
name: "Thresholds",
|
|
20
29
|
description: "Set warning and critical levels",
|
|
21
|
-
order:
|
|
30
|
+
order: 3,
|
|
22
31
|
defaultCollapsed: true,
|
|
23
32
|
};
|
|
24
33
|
|
|
@@ -71,6 +80,31 @@ export default class DashboardValueComponentUtil extends DashboardBaseComponentU
|
|
|
71
80
|
section: DataSourceSection,
|
|
72
81
|
});
|
|
73
82
|
|
|
83
|
+
componentArguments.push({
|
|
84
|
+
name: "Trend Direction",
|
|
85
|
+
description:
|
|
86
|
+
"How the trend arrow is coloured. Auto reads the metric name (e.g. 'error_count' and 'latency' are treated as 'higher is worse'). Pick a value to override.",
|
|
87
|
+
required: false,
|
|
88
|
+
type: ComponentInputType.Dropdown,
|
|
89
|
+
id: "trendDirection",
|
|
90
|
+
isAdvanced: true,
|
|
91
|
+
section: DisplaySection,
|
|
92
|
+
dropdownOptions: [
|
|
93
|
+
{
|
|
94
|
+
label: "Auto (detect from metric name)",
|
|
95
|
+
value: DashboardValueTrendDirection.Auto,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: "Higher is better (↑ green)",
|
|
99
|
+
value: DashboardValueTrendDirection.HigherIsBetter,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: "Higher is worse (↑ red)",
|
|
103
|
+
value: DashboardValueTrendDirection.HigherIsWorse,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
74
108
|
componentArguments.push({
|
|
75
109
|
name: "Warning Threshold",
|
|
76
110
|
description: "Yellow background when value exceeds this",
|
package/Utils/ValueFormatter.ts
CHANGED
|
@@ -435,6 +435,42 @@ export default class ValueFormatter {
|
|
|
435
435
|
return fractionMetricSuffixRegex.test(metricName);
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/*
|
|
439
|
+
* Direction-of-goodness heuristic for trend coloring. Returns true when a
|
|
440
|
+
* rising value on this metric should be shown as bad (red ↑) and a falling
|
|
441
|
+
* value as good (green ↓) — the opposite of the default "up = good".
|
|
442
|
+
*
|
|
443
|
+
* Caller is responsible for the inversion; this method only classifies.
|
|
444
|
+
* Conservative by default: when the metric name carries no signal we
|
|
445
|
+
* return `false` so generic counters (request rate, network I/O, span
|
|
446
|
+
* count) keep the up = good colour scheme. Explicit "higher is better"
|
|
447
|
+
* tokens (uptime/availability/online/ready/healthy/available/success/
|
|
448
|
+
* passed) take precedence — without that allowlist `oneuptime.monitor.
|
|
449
|
+
* online` would match nothing and incorrectly fall through; the suffix
|
|
450
|
+
* test then catches incident counts, error rates, MTTR/MTTA, response
|
|
451
|
+
* times, CPU/memory usage, restarts, pressure, queue backlogs, etc.
|
|
452
|
+
*
|
|
453
|
+
* Adding a metric here is a single regex edit. If a dashboard widget
|
|
454
|
+
* really needs to override this for a specific case, callers can set
|
|
455
|
+
* `higherIsBetter` explicitly on the trend display.
|
|
456
|
+
*/
|
|
457
|
+
public static isHigherWorseMetric(metricName: string | undefined): boolean {
|
|
458
|
+
if (!metricName) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const lower: string = metricName.toLowerCase();
|
|
462
|
+
|
|
463
|
+
const higherIsBetter: RegExp =
|
|
464
|
+
/\b(uptime|availability|online|ready|healthy|available|success|passed|ok|alive|up)\b/;
|
|
465
|
+
if (higherIsBetter.test(lower)) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const higherIsWorse: RegExp =
|
|
470
|
+
/(error|fail(?:ure|ed)?|incident|exception|crash|restart|timeout|dropped|aborted?|reject(?:ed)?|mttr|mtta|latenc(?:y|ies)|duration|[._-]time\b|time[._-]to[._-]|usage|utilization|pressure|unresolved|pending|backlog|lag|delay|saturation|throttl|stall|missed?)/;
|
|
471
|
+
return higherIsWorse.test(lower);
|
|
472
|
+
}
|
|
473
|
+
|
|
438
474
|
// Check if a unit is one we can auto-scale (bytes, seconds, etc.)
|
|
439
475
|
public static isScalableUnit(unit: string): boolean {
|
|
440
476
|
if (!unit || unit.trim() === "") {
|
|
@@ -15,9 +15,12 @@ import QueryHelper from "../Types/Database/QueryHelper";
|
|
|
15
15
|
import OneUptimeDate from "../../Types/Date";
|
|
16
16
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
17
17
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
18
|
+
import logger from "../Utils/Logger";
|
|
18
19
|
import crypto from "crypto";
|
|
19
20
|
const LAST_SEEN_CACHE_NAMESPACE = "docker-host-last-seen";
|
|
20
21
|
const LAST_SEEN_THROTTLE_SECONDS = 60;
|
|
22
|
+
const LABELS_APPLIED_CACHE_NAMESPACE = "docker-host-labels-applied";
|
|
23
|
+
const LABELS_APPLIED_CACHE_TTL_SECONDS = 60;
|
|
21
24
|
export class Service extends DatabaseService {
|
|
22
25
|
constructor() {
|
|
23
26
|
super(Model);
|
|
@@ -116,6 +119,62 @@ export class Service extends DatabaseService {
|
|
|
116
119
|
},
|
|
117
120
|
});
|
|
118
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Additively attach labels to a Docker host. Existing labels are
|
|
124
|
+
* never removed — manual labels set via the UI survive ingest. The
|
|
125
|
+
* set of labelIds passed in is fingerprinted and cached for 60s so
|
|
126
|
+
* the common case (steady-state collector pushing the same label
|
|
127
|
+
* set every batch) costs one in-memory lookup, not a join-table
|
|
128
|
+
* scan.
|
|
129
|
+
*/
|
|
130
|
+
async attachLabels(data) {
|
|
131
|
+
var _a;
|
|
132
|
+
if (!data.labelIds || data.labelIds.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const cacheKey = data.dockerHostId.toString();
|
|
136
|
+
const fingerprint = fingerprintLabelIds(data.labelIds);
|
|
137
|
+
const cached = await GlobalCache.getString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey);
|
|
138
|
+
if (cached === fingerprint) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const dockerHostIdStr = data.dockerHostId.toString();
|
|
143
|
+
const existingLabels = await this.getRepository()
|
|
144
|
+
.createQueryBuilder()
|
|
145
|
+
.relation(Model, "labels")
|
|
146
|
+
.of(dockerHostIdStr)
|
|
147
|
+
.loadMany();
|
|
148
|
+
const existingIds = new Set();
|
|
149
|
+
for (const lbl of existingLabels) {
|
|
150
|
+
const idStr = (_a = lbl._id) === null || _a === void 0 ? void 0 : _a.toString();
|
|
151
|
+
if (idStr) {
|
|
152
|
+
existingIds.add(idStr);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const toAddIds = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const id of data.labelIds) {
|
|
158
|
+
const idStr = id.toString();
|
|
159
|
+
if (existingIds.has(idStr) || seen.has(idStr)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
seen.add(idStr);
|
|
163
|
+
toAddIds.push(idStr);
|
|
164
|
+
}
|
|
165
|
+
if (toAddIds.length > 0) {
|
|
166
|
+
await this.getRepository()
|
|
167
|
+
.createQueryBuilder()
|
|
168
|
+
.relation(Model, "labels")
|
|
169
|
+
.of(dockerHostIdStr)
|
|
170
|
+
.add(toAddIds);
|
|
171
|
+
}
|
|
172
|
+
await GlobalCache.setString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey, fingerprint, { expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS });
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.warn(`DockerHostService.attachLabels failed for docker host ${data.dockerHostId.toString()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
119
178
|
async markDisconnectedHosts() {
|
|
120
179
|
const fiveMinutesAgo = OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), -5);
|
|
121
180
|
const connectedHosts = await this.findBy({
|
|
@@ -159,11 +218,25 @@ __decorate([
|
|
|
159
218
|
__metadata("design:paramtypes", [ObjectID, Object]),
|
|
160
219
|
__metadata("design:returntype", Promise)
|
|
161
220
|
], Service.prototype, "updateLastSeen", null);
|
|
221
|
+
__decorate([
|
|
222
|
+
CaptureSpan(),
|
|
223
|
+
__metadata("design:type", Function),
|
|
224
|
+
__metadata("design:paramtypes", [Object]),
|
|
225
|
+
__metadata("design:returntype", Promise)
|
|
226
|
+
], Service.prototype, "attachLabels", null);
|
|
162
227
|
__decorate([
|
|
163
228
|
CaptureSpan(),
|
|
164
229
|
__metadata("design:type", Function),
|
|
165
230
|
__metadata("design:paramtypes", []),
|
|
166
231
|
__metadata("design:returntype", Promise)
|
|
167
232
|
], Service.prototype, "markDisconnectedHosts", null);
|
|
233
|
+
function fingerprintLabelIds(labelIds) {
|
|
234
|
+
const sorted = labelIds
|
|
235
|
+
.map((id) => {
|
|
236
|
+
return id.toString();
|
|
237
|
+
})
|
|
238
|
+
.sort();
|
|
239
|
+
return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
|
|
240
|
+
}
|
|
168
241
|
export default new Service();
|
|
169
242
|
//# sourceMappingURL=DockerHostService.js.map
|