@oneuptime/common 11.1.2 → 11.2.1
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/Alert.ts +55 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/Index.ts +12 -0
- package/Models/DatabaseModels/IoTDevice.ts +690 -0
- package/Models/DatabaseModels/IoTFleet.ts +862 -0
- package/Models/DatabaseModels/IoTFleetLabelRule.ts +514 -0
- package/Models/DatabaseModels/IoTFleetOwnerRule.ts +596 -0
- package/Models/DatabaseModels/IoTFleetOwnerTeam.ts +485 -0
- package/Models/DatabaseModels/IoTFleetOwnerUser.ts +484 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
- package/Server/API/IoTDeviceAPI.ts +128 -0
- package/Server/API/TelemetryAPI.ts +104 -35
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782900000000-AddIoTFleetAndDeviceTables.ts +399 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783000000000-AddIoTFleetActivityJoinTables.ts +97 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783010000000-MigrationName.ts +979 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/AlertService.ts +24 -3
- package/Server/Services/IncidentService.ts +24 -3
- package/Server/Services/IoTDeviceService.ts +356 -0
- package/Server/Services/IoTFleetLabelRuleEngineService.ts +198 -0
- package/Server/Services/IoTFleetLabelRuleService.ts +14 -0
- package/Server/Services/IoTFleetOwnerRuleEngineService.ts +216 -0
- package/Server/Services/IoTFleetOwnerRuleService.ts +14 -0
- package/Server/Services/IoTFleetOwnerTeamService.ts +10 -0
- package/Server/Services/IoTFleetOwnerUserService.ts +10 -0
- package/Server/Services/IoTFleetService.ts +376 -0
- package/Server/Services/OpenTelemetryIngestService.ts +16 -0
- package/Server/Services/StatusPageService.ts +69 -26
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +36 -0
- package/Server/Services/TelemetryUsageBillingService.ts +18 -0
- package/Server/Services/TraceAggregationService.ts +174 -0
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorAlert.ts +24 -4
- package/Server/Utils/Monitor/MonitorClusterContext.ts +26 -2
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +4 -2
- package/Server/Utils/Monitor/MonitorIncident.ts +39 -5
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +14 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +22 -0
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +408 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +36 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/Monitor/IotAlertTemplates.ts +593 -0
- package/Types/Monitor/IotMetricCatalog.ts +146 -0
- package/Types/Monitor/MonitorStep.ts +31 -0
- package/Types/Monitor/MonitorStepIoTMonitor.ts +65 -0
- package/Types/Monitor/MonitorType.ts +12 -1
- package/Types/Permission.ts +230 -0
- package/Types/StatusPage/StatusPageSubscriberNotificationEventType.ts +3 -0
- package/Types/Telemetry/ServiceType.ts +1 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
- package/UI/Components/Icon/Icon.tsx +18 -2
- package/UI/Components/Page/Page.tsx +1 -2
- package/build/dist/Models/DatabaseModels/Alert.js +54 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +54 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +12 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IoTDevice.js +722 -0
- package/build/dist/Models/DatabaseModels/IoTDevice.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IoTFleet.js +882 -0
- package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IoTFleetLabelRule.js +522 -0
- package/build/dist/Models/DatabaseModels/IoTFleetLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerRule.js +603 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerTeam.js +503 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerUser.js +502 -0
- package/build/dist/Models/DatabaseModels/IoTFleetOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/API/IoTDeviceAPI.js +92 -0
- package/build/dist/Server/API/IoTDeviceAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +76 -27
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782900000000-AddIoTFleetAndDeviceTables.js +187 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782900000000-AddIoTFleetAndDeviceTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783000000000-AddIoTFleetActivityJoinTables.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783000000000-AddIoTFleetActivityJoinTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783010000000-MigrationName.js +340 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783010000000-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +22 -3
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +22 -3
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +249 -0
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js +166 -0
- package/build/dist/Server/Services/IoTFleetLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetLabelRuleService.js +13 -0
- package/build/dist/Server/Services/IoTFleetLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js +186 -0
- package/build/dist/Server/Services/IoTFleetOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/IoTFleetOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetOwnerTeamService.js +9 -0
- package/build/dist/Server/Services/IoTFleetOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetOwnerUserService.js +9 -0
- package/build/dist/Server/Services/IoTFleetOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/IoTFleetService.js +340 -0
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -0
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +26 -11
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +57 -23
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +34 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +13 -0
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +136 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +15 -15
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +37 -19
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +27 -4
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +4 -2
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +39 -11
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +17 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +300 -0
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -0
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Monitor/IotAlertTemplates.js +467 -0
- package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/IotMetricCatalog.js +108 -0
- package/build/dist/Types/Monitor/IotMetricCatalog.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +23 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepIoTMonitor.js +32 -0
- package/build/dist/Types/Monitor/MonitorStepIoTMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +11 -1
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Permission.js +204 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js +2 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +3 -3
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +12 -2
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +1 -2
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/package.json +1 -1
|
@@ -31,6 +31,12 @@ export interface MaintainedResourceKeys {
|
|
|
31
31
|
*/
|
|
32
32
|
proxmoxClusters: ResourceKeySet;
|
|
33
33
|
cephClusters: ResourceKeySet;
|
|
34
|
+
/*
|
|
35
|
+
* IoT fleets, like Proxmox/Ceph clusters, have no `oneuptime.*.id`
|
|
36
|
+
* label stamp, so only the name set is ever matched; the id set
|
|
37
|
+
* exists for shape parity.
|
|
38
|
+
*/
|
|
39
|
+
iotFleets: ResourceKeySet;
|
|
34
40
|
services: ResourceKeySet;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -134,6 +140,7 @@ export default class MonitorMaintenanceSuppression {
|
|
|
134
140
|
refs.cephClusterNames,
|
|
135
141
|
input.maintained.cephClusters.names,
|
|
136
142
|
) ||
|
|
143
|
+
this.intersects(refs.iotFleetNames, input.maintained.iotFleets.names) ||
|
|
137
144
|
this.intersects(refs.serviceIds, input.maintained.services.ids) ||
|
|
138
145
|
this.intersects(refs.serviceNames, input.maintained.services.names);
|
|
139
146
|
|
|
@@ -170,6 +177,8 @@ export default class MonitorMaintenanceSuppression {
|
|
|
170
177
|
maintained.proxmoxClusters.names.size > 0 ||
|
|
171
178
|
maintained.cephClusters.ids.size > 0 ||
|
|
172
179
|
maintained.cephClusters.names.size > 0 ||
|
|
180
|
+
maintained.iotFleets.ids.size > 0 ||
|
|
181
|
+
maintained.iotFleets.names.size > 0 ||
|
|
173
182
|
maintained.services.ids.size > 0 ||
|
|
174
183
|
maintained.services.names.size > 0
|
|
175
184
|
);
|
|
@@ -193,6 +202,7 @@ export default class MonitorMaintenanceSuppression {
|
|
|
193
202
|
kubernetesClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
194
203
|
proxmoxClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
195
204
|
cephClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
205
|
+
iotFleets: { ids: new Set<string>(), names: new Set<string>() },
|
|
196
206
|
services: { ids: new Set<string>(), names: new Set<string>() },
|
|
197
207
|
};
|
|
198
208
|
|
|
@@ -212,6 +222,7 @@ export default class MonitorMaintenanceSuppression {
|
|
|
212
222
|
kubernetesClusters: { _id: true, clusterIdentifier: true },
|
|
213
223
|
proxmoxClusters: { _id: true, name: true },
|
|
214
224
|
cephClusters: { _id: true, name: true },
|
|
225
|
+
iotFleets: { _id: true, name: true },
|
|
215
226
|
services: { _id: true, name: true },
|
|
216
227
|
},
|
|
217
228
|
skip: 0,
|
|
@@ -256,6 +267,9 @@ export default class MonitorMaintenanceSuppression {
|
|
|
256
267
|
for (const cephCluster of event.cephClusters || []) {
|
|
257
268
|
this.addKey(maintained.cephClusters, cephCluster._id, cephCluster.name);
|
|
258
269
|
}
|
|
270
|
+
for (const iotFleet of event.iotFleets || []) {
|
|
271
|
+
this.addKey(maintained.iotFleets, iotFleet._id, iotFleet.name);
|
|
272
|
+
}
|
|
259
273
|
for (const service of event.services || []) {
|
|
260
274
|
this.addKey(maintained.services, service._id, service.name);
|
|
261
275
|
}
|
|
@@ -99,6 +99,23 @@ export const CephClusterNameLabelKeys: ReadonlyArray<string> = [
|
|
|
99
99
|
"ceph.cluster.name",
|
|
100
100
|
];
|
|
101
101
|
|
|
102
|
+
/*
|
|
103
|
+
* IoT fleet identity rides the agent-stamped resource attribute
|
|
104
|
+
* (`iot.fleet.name`) and its ClickHouse `resource.`-prefixed twin.
|
|
105
|
+
* Ingest keys fleet rows by name only — there is no `oneuptime.*.id`
|
|
106
|
+
* stamp for fleets — so only name keys exist. The name maps to the
|
|
107
|
+
* IoTFleet model's `name` column. Like Proxmox/Ceph, the shipped IoT
|
|
108
|
+
* alert templates group by the datapoint label `device.id`, so their
|
|
109
|
+
* series labels do NOT carry these keys; the deterministic fleet link
|
|
110
|
+
* for those monitors comes from the monitor step config instead (see
|
|
111
|
+
* MonitorClusterContext). These keys cover user-built monitors that
|
|
112
|
+
* group by the fleet attribute.
|
|
113
|
+
*/
|
|
114
|
+
export const IoTFleetNameLabelKeys: ReadonlyArray<string> = [
|
|
115
|
+
"resource.iot.fleet.name",
|
|
116
|
+
"iot.fleet.name",
|
|
117
|
+
];
|
|
118
|
+
|
|
102
119
|
/*
|
|
103
120
|
* Services come from OTel-ingested telemetry. The ingest pipeline
|
|
104
121
|
* auto-creates a Service row keyed by `service.name`, so any series
|
|
@@ -134,6 +151,7 @@ export interface SeriesResourceRefs {
|
|
|
134
151
|
kubernetesClusterNames: Array<string>;
|
|
135
152
|
proxmoxClusterNames: Array<string>;
|
|
136
153
|
cephClusterNames: Array<string>;
|
|
154
|
+
iotFleetNames: Array<string>;
|
|
137
155
|
serviceIds: Array<string>;
|
|
138
156
|
serviceNames: Array<string>;
|
|
139
157
|
}
|
|
@@ -208,6 +226,10 @@ export default class SeriesResourceLabels {
|
|
|
208
226
|
seriesLabels,
|
|
209
227
|
CephClusterNameLabelKeys,
|
|
210
228
|
),
|
|
229
|
+
iotFleetNames: this.collectLabelValues(
|
|
230
|
+
seriesLabels,
|
|
231
|
+
IoTFleetNameLabelKeys,
|
|
232
|
+
),
|
|
211
233
|
serviceIds: this.collectLabelValues(seriesLabels, ServiceIdLabelKeys),
|
|
212
234
|
serviceNames: this.collectLabelValues(seriesLabels, ServiceNameLabelKeys),
|
|
213
235
|
};
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
2
|
+
import { JSONArray, JSONObject, JSONValue } from "../../../Types/JSON";
|
|
3
|
+
import logger from "../Logger";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* ------------------------------------------------------------------
|
|
7
|
+
* IoT snapshot scan — pure fold & derive helpers
|
|
8
|
+
* ------------------------------------------------------------------
|
|
9
|
+
*
|
|
10
|
+
* Cloned from ProxmoxCephSnapshotScan.ts (the Proxmox half). The
|
|
11
|
+
* ingest service owns the I/O: it walks the OTLP payload, calls
|
|
12
|
+
* bufferIoTSnapshotMetric per datapoint, and at flush time maps the
|
|
13
|
+
* folded buffers through deriveIoTFleetSnapshotExtras before handing
|
|
14
|
+
* the results to IoTDeviceService / IoTFleetService. Everything in
|
|
15
|
+
* this module is pure (Map/object mutation only — no DB, no network),
|
|
16
|
+
* which is what makes the snapshot-scan semantics unit-testable:
|
|
17
|
+
*
|
|
18
|
+
* - identity labels fold first-non-null-wins; status/metric fields
|
|
19
|
+
* fold newest-observedAt-wins,
|
|
20
|
+
* - count columns are only derived when the batch carried the
|
|
21
|
+
* matching identity series (never zero a count on a partial
|
|
22
|
+
* batch — the COALESCE-per-column contract),
|
|
23
|
+
* - non-allow-listed metric names are skipped via the exported
|
|
24
|
+
* IOT_SNAPSHOT_METRIC_NAMES set.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* IoT snapshot metrics — emitted by IoT devices / gateways pushing
|
|
29
|
+
* OTLP, identity in the `device.id` datapoint label plus the
|
|
30
|
+
* iot.device.kind / iot.device.type / iot.device.firmware attributes
|
|
31
|
+
* the device's SDK stamps. Unlike K8s there is no separate object
|
|
32
|
+
* stream: identity, status AND the latest-metric mirror all arrive on
|
|
33
|
+
* every scrape, so the same scan feeds the IoTDevice inventory upsert
|
|
34
|
+
* and the IoTFleet count snapshot columns (single source — the
|
|
35
|
+
* list-page counts and the sidebar badges can never drift).
|
|
36
|
+
*
|
|
37
|
+
* iot_cpu_usage_ratio is already a true 0..1 ratio — no allocatable-
|
|
38
|
+
* denominator cache is needed, unlike K8s cpuCoresToPercent.
|
|
39
|
+
*/
|
|
40
|
+
export const IOT_SNAPSHOT_METRIC_NAMES: ReadonlySet<string> = new Set([
|
|
41
|
+
// Identity / status (fleet counts derive from these)
|
|
42
|
+
"iot_device_up",
|
|
43
|
+
"iot_device_info",
|
|
44
|
+
// Latest-metric mirror (inventory columns)
|
|
45
|
+
"iot_battery_percent",
|
|
46
|
+
"iot_signal_strength_dbm",
|
|
47
|
+
"iot_temperature_celsius",
|
|
48
|
+
"iot_cpu_usage_ratio",
|
|
49
|
+
"iot_memory_usage_bytes",
|
|
50
|
+
"iot_memory_size_bytes",
|
|
51
|
+
"iot_uptime_seconds",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* One IoT device folded across a batch. Identity labels are
|
|
56
|
+
* first-non-null-wins (stable for the lifetime of the device);
|
|
57
|
+
* status / metric fields are newest-observedAt-wins.
|
|
58
|
+
*/
|
|
59
|
+
export interface IoTDeviceBufferEntry {
|
|
60
|
+
kind: string; // device class — Device | Sensor | Gateway
|
|
61
|
+
externalId: string; // raw `device.id` label
|
|
62
|
+
name: string | null;
|
|
63
|
+
deviceType: string | null;
|
|
64
|
+
firmwareVersion: string | null;
|
|
65
|
+
isUp: boolean | null;
|
|
66
|
+
uptimeSeconds: number | null;
|
|
67
|
+
latestCpuPercent: number | null;
|
|
68
|
+
latestMemoryBytes: number | null;
|
|
69
|
+
maxMemoryBytes: number | null;
|
|
70
|
+
latestBatteryPercent: number | null;
|
|
71
|
+
latestSignalStrengthDbm: number | null;
|
|
72
|
+
latestTemperatureCelsius: number | null;
|
|
73
|
+
observedAt: Date;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Per-fleet IoT snapshot state. The saw* flags implement the
|
|
78
|
+
* never-zero-a-count-on-a-partial-batch contract: a count column is
|
|
79
|
+
* only written when the batch carried the matching identity series.
|
|
80
|
+
*/
|
|
81
|
+
export interface IoTFleetSnapshotBufferEntry {
|
|
82
|
+
sawDeviceIdentity: boolean; // iot_device_info, or iot_device_up
|
|
83
|
+
sawDeviceUp: boolean; // iot_device_up
|
|
84
|
+
agentVersion?: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The IoTFleet snapshot columns derived from one folded batch.
|
|
88
|
+
export interface IoTFleetSnapshotExtras {
|
|
89
|
+
agentVersion?: string | undefined;
|
|
90
|
+
deviceCount?: number | undefined;
|
|
91
|
+
onlineDeviceCount?: number | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Same finite-or-null coercion contract as the ingest service's
|
|
96
|
+
* toNumberOrNull (NaN / ±Infinity fold to null so a malformed
|
|
97
|
+
* datapoint is skipped rather than poisoning a snapshot column).
|
|
98
|
+
*/
|
|
99
|
+
function toNumberOrNull(value: unknown): number | null {
|
|
100
|
+
if (typeof value === "number") {
|
|
101
|
+
return Number.isFinite(value) ? value : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
const parsed: number = Number.parseFloat(value);
|
|
106
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Same fall-back-to-now parse contract as the ingest service's
|
|
114
|
+
* safeParseUnixNano — only the Date is needed on the snapshot path.
|
|
115
|
+
*/
|
|
116
|
+
function parseUnixNanoToDate(
|
|
117
|
+
value: string | number | undefined,
|
|
118
|
+
context: string,
|
|
119
|
+
): Date {
|
|
120
|
+
let numericValue: number = OneUptimeDate.getCurrentDateAsUnixNano();
|
|
121
|
+
|
|
122
|
+
if (value !== undefined && value !== null) {
|
|
123
|
+
try {
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
const parsed: number = Number.parseFloat(value);
|
|
126
|
+
if (isNaN(parsed)) {
|
|
127
|
+
throw new Error(`Invalid timestamp string: ${value}`);
|
|
128
|
+
}
|
|
129
|
+
numericValue = parsed;
|
|
130
|
+
} else if (typeof value === "number") {
|
|
131
|
+
if (!Number.isFinite(value)) {
|
|
132
|
+
throw new Error(`Invalid timestamp number: ${value}`);
|
|
133
|
+
}
|
|
134
|
+
numericValue = value;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.warn(
|
|
138
|
+
`Error processing ${context}: ${error instanceof Error ? error.message : String(error)}, using current time`,
|
|
139
|
+
);
|
|
140
|
+
numericValue = OneUptimeDate.getCurrentDateAsUnixNano();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return OneUptimeDate.fromUnixNano(numericValue);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Same trim-or-null read contract as OtelIngestBaseService.getStringAttribute.
|
|
148
|
+
function getStringAttribute(attributes: JSONArray, key: string): string | null {
|
|
149
|
+
for (const attribute of attributes) {
|
|
150
|
+
if (
|
|
151
|
+
attribute["key"] === key &&
|
|
152
|
+
attribute["value"] &&
|
|
153
|
+
(attribute["value"] as JSONObject)["stringValue"]
|
|
154
|
+
) {
|
|
155
|
+
const value: JSONValue = (attribute["value"] as JSONObject)[
|
|
156
|
+
"stringValue"
|
|
157
|
+
];
|
|
158
|
+
if (typeof value === "string" && value.trim()) {
|
|
159
|
+
return value.trim();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getOrCreateIoTFleetSnapshot(
|
|
167
|
+
buffer: Map<string, IoTFleetSnapshotBufferEntry>,
|
|
168
|
+
fleetIdStr: string,
|
|
169
|
+
): IoTFleetSnapshotBufferEntry {
|
|
170
|
+
let entry: IoTFleetSnapshotBufferEntry | undefined = buffer.get(fleetIdStr);
|
|
171
|
+
if (!entry) {
|
|
172
|
+
entry = {
|
|
173
|
+
sawDeviceIdentity: false,
|
|
174
|
+
sawDeviceUp: false,
|
|
175
|
+
};
|
|
176
|
+
buffer.set(fleetIdStr, entry);
|
|
177
|
+
}
|
|
178
|
+
return entry;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* Fold one iot_* datapoint into the per-fleet buffers. Identity lives
|
|
183
|
+
* in the `device.id` DATAPOINT label, so this reads the raw datapoint
|
|
184
|
+
* attribute array — not the merged resource-prefixed map the K8s scan
|
|
185
|
+
* uses.
|
|
186
|
+
*/
|
|
187
|
+
export function bufferIoTSnapshotMetric(data: {
|
|
188
|
+
fleetIdStr: string;
|
|
189
|
+
metricName: string;
|
|
190
|
+
datapoint: JSONObject;
|
|
191
|
+
resourceBuffer: Map<string, Map<string, IoTDeviceBufferEntry>>;
|
|
192
|
+
fleetBuffer: Map<string, IoTFleetSnapshotBufferEntry>;
|
|
193
|
+
}): void {
|
|
194
|
+
const valueFromInt: number | null = toNumberOrNull(data.datapoint["asInt"]);
|
|
195
|
+
const valueFromDouble: number | null = toNumberOrNull(
|
|
196
|
+
data.datapoint["asDouble"],
|
|
197
|
+
);
|
|
198
|
+
const rawValue: number | null = valueFromDouble ?? valueFromInt;
|
|
199
|
+
if (rawValue === null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const observedAt: Date = parseUnixNanoToDate(
|
|
204
|
+
data.datapoint["timeUnixNano"] as string | number | undefined,
|
|
205
|
+
"iot snapshot timeUnixNano",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const dpAttributes: JSONArray =
|
|
209
|
+
(data.datapoint["attributes"] as JSONArray) || [];
|
|
210
|
+
|
|
211
|
+
const fleet: IoTFleetSnapshotBufferEntry = getOrCreateIoTFleetSnapshot(
|
|
212
|
+
data.fleetBuffer,
|
|
213
|
+
data.fleetIdStr,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Identity lives in the `device.id` datapoint label.
|
|
217
|
+
const externalId: string | null = getStringAttribute(
|
|
218
|
+
dpAttributes,
|
|
219
|
+
"device.id",
|
|
220
|
+
);
|
|
221
|
+
if (!externalId) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* Kind/type/firmware: read from the iot.device.* attributes the
|
|
227
|
+
* device SDK stamps; kind falls back to "Device" so inventory still
|
|
228
|
+
* populates without an explicit class label.
|
|
229
|
+
*/
|
|
230
|
+
const kind: string =
|
|
231
|
+
getStringAttribute(dpAttributes, "iot.device.kind") || "Device";
|
|
232
|
+
|
|
233
|
+
const patch: IoTDeviceBufferEntry = {
|
|
234
|
+
kind,
|
|
235
|
+
externalId,
|
|
236
|
+
name: null,
|
|
237
|
+
deviceType: null,
|
|
238
|
+
firmwareVersion: null,
|
|
239
|
+
isUp: null,
|
|
240
|
+
uptimeSeconds: null,
|
|
241
|
+
latestCpuPercent: null,
|
|
242
|
+
latestMemoryBytes: null,
|
|
243
|
+
maxMemoryBytes: null,
|
|
244
|
+
latestBatteryPercent: null,
|
|
245
|
+
latestSignalStrengthDbm: null,
|
|
246
|
+
latestTemperatureCelsius: null,
|
|
247
|
+
observedAt,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// deviceType / firmware ride every series the device emits.
|
|
251
|
+
patch.deviceType = getStringAttribute(dpAttributes, "iot.device.type");
|
|
252
|
+
patch.firmwareVersion = getStringAttribute(
|
|
253
|
+
dpAttributes,
|
|
254
|
+
"iot.device.firmware",
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
switch (data.metricName) {
|
|
258
|
+
case "iot_device_up": {
|
|
259
|
+
patch.isUp = rawValue >= 1;
|
|
260
|
+
fleet.sawDeviceIdentity = true;
|
|
261
|
+
fleet.sawDeviceUp = true;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "iot_device_info": {
|
|
265
|
+
// Identity-only series — carries the human-readable device name.
|
|
266
|
+
patch.name = getStringAttribute(dpAttributes, "name");
|
|
267
|
+
fleet.sawDeviceIdentity = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "iot_battery_percent": {
|
|
271
|
+
patch.latestBatteryPercent = rawValue;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "iot_signal_strength_dbm": {
|
|
275
|
+
patch.latestSignalStrengthDbm = rawValue;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "iot_temperature_celsius": {
|
|
279
|
+
patch.latestTemperatureCelsius = rawValue;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "iot_cpu_usage_ratio": {
|
|
283
|
+
/*
|
|
284
|
+
* Already a true 0..1 ratio — no allocatable-denominator cache
|
|
285
|
+
* needed, unlike K8s cpuCoresToPercent.
|
|
286
|
+
*/
|
|
287
|
+
patch.latestCpuPercent = rawValue * 100;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "iot_memory_usage_bytes": {
|
|
291
|
+
patch.latestMemoryBytes = Math.max(0, Math.trunc(rawValue));
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "iot_memory_size_bytes": {
|
|
295
|
+
patch.maxMemoryBytes = Math.max(0, Math.trunc(rawValue));
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "iot_uptime_seconds": {
|
|
299
|
+
patch.uptimeSeconds = Math.max(0, Math.trunc(rawValue));
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
default: {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
foldIoTDeviceSnapshot({
|
|
308
|
+
buffer: data.resourceBuffer,
|
|
309
|
+
fleetIdStr: data.fleetIdStr,
|
|
310
|
+
patch,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/*
|
|
315
|
+
* Merge a patch into the per-fleet IoT buffer: identity labels are
|
|
316
|
+
* first-non-null-wins (stable, and a batch missing an info series
|
|
317
|
+
* must not blank them), status/metric fields are
|
|
318
|
+
* newest-observedAt-wins (K8s buffer semantics).
|
|
319
|
+
*/
|
|
320
|
+
export function foldIoTDeviceSnapshot(data: {
|
|
321
|
+
buffer: Map<string, Map<string, IoTDeviceBufferEntry>>;
|
|
322
|
+
fleetIdStr: string;
|
|
323
|
+
patch: IoTDeviceBufferEntry;
|
|
324
|
+
}): void {
|
|
325
|
+
let perFleet: Map<string, IoTDeviceBufferEntry> | undefined = data.buffer.get(
|
|
326
|
+
data.fleetIdStr,
|
|
327
|
+
);
|
|
328
|
+
if (!perFleet) {
|
|
329
|
+
perFleet = new Map();
|
|
330
|
+
data.buffer.set(data.fleetIdStr, perFleet);
|
|
331
|
+
}
|
|
332
|
+
const key: string = `${data.patch.kind}|${data.patch.externalId}`;
|
|
333
|
+
const existing: IoTDeviceBufferEntry | undefined = perFleet.get(key);
|
|
334
|
+
if (!existing) {
|
|
335
|
+
perFleet.set(key, data.patch);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const patch: IoTDeviceBufferEntry = data.patch;
|
|
340
|
+
const newer: boolean = patch.observedAt >= existing.observedAt;
|
|
341
|
+
|
|
342
|
+
// Identity: first-non-null wins.
|
|
343
|
+
if (existing.name === null && patch.name !== null) {
|
|
344
|
+
existing.name = patch.name;
|
|
345
|
+
}
|
|
346
|
+
if (existing.deviceType === null && patch.deviceType !== null) {
|
|
347
|
+
existing.deviceType = patch.deviceType;
|
|
348
|
+
}
|
|
349
|
+
if (existing.firmwareVersion === null && patch.firmwareVersion !== null) {
|
|
350
|
+
existing.firmwareVersion = patch.firmwareVersion;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Status / metrics: newest observation wins.
|
|
354
|
+
if (patch.isUp !== null && newer) {
|
|
355
|
+
existing.isUp = patch.isUp;
|
|
356
|
+
}
|
|
357
|
+
if (patch.uptimeSeconds !== null && newer) {
|
|
358
|
+
existing.uptimeSeconds = patch.uptimeSeconds;
|
|
359
|
+
}
|
|
360
|
+
if (patch.latestCpuPercent !== null && newer) {
|
|
361
|
+
existing.latestCpuPercent = patch.latestCpuPercent;
|
|
362
|
+
}
|
|
363
|
+
if (patch.latestMemoryBytes !== null && newer) {
|
|
364
|
+
existing.latestMemoryBytes = patch.latestMemoryBytes;
|
|
365
|
+
}
|
|
366
|
+
if (patch.maxMemoryBytes !== null && newer) {
|
|
367
|
+
existing.maxMemoryBytes = patch.maxMemoryBytes;
|
|
368
|
+
}
|
|
369
|
+
if (patch.latestBatteryPercent !== null && newer) {
|
|
370
|
+
existing.latestBatteryPercent = patch.latestBatteryPercent;
|
|
371
|
+
}
|
|
372
|
+
if (patch.latestSignalStrengthDbm !== null && newer) {
|
|
373
|
+
existing.latestSignalStrengthDbm = patch.latestSignalStrengthDbm;
|
|
374
|
+
}
|
|
375
|
+
if (patch.latestTemperatureCelsius !== null && newer) {
|
|
376
|
+
existing.latestTemperatureCelsius = patch.latestTemperatureCelsius;
|
|
377
|
+
}
|
|
378
|
+
if (patch.observedAt > existing.observedAt) {
|
|
379
|
+
existing.observedAt = patch.observedAt;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/*
|
|
384
|
+
* Derive the IoTFleet snapshot columns from one folded batch. Counts
|
|
385
|
+
* are only set when the batch carried the matching identity series —
|
|
386
|
+
* never zero a count on a partial batch. Returns an object whose keys
|
|
387
|
+
* are exactly the columns to write (empty object = nothing to write).
|
|
388
|
+
*/
|
|
389
|
+
export function deriveIoTFleetSnapshotExtras(
|
|
390
|
+
entries: Array<IoTDeviceBufferEntry>,
|
|
391
|
+
snap: IoTFleetSnapshotBufferEntry | undefined,
|
|
392
|
+
): IoTFleetSnapshotExtras {
|
|
393
|
+
const extras: IoTFleetSnapshotExtras = {};
|
|
394
|
+
|
|
395
|
+
if (snap?.sawDeviceIdentity) {
|
|
396
|
+
extras.deviceCount = entries.length;
|
|
397
|
+
}
|
|
398
|
+
if (snap?.sawDeviceUp) {
|
|
399
|
+
extras.onlineDeviceCount = entries.filter((e: IoTDeviceBufferEntry) => {
|
|
400
|
+
return e.isUp === true;
|
|
401
|
+
}).length;
|
|
402
|
+
}
|
|
403
|
+
if (snap?.agentVersion) {
|
|
404
|
+
extras.agentVersion = snap.agentVersion;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return extras;
|
|
408
|
+
}
|
|
@@ -15,6 +15,7 @@ function emptyMaintained(): MaintainedResourceKeys {
|
|
|
15
15
|
kubernetesClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
16
16
|
proxmoxClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
17
17
|
cephClusters: { ids: new Set<string>(), names: new Set<string>() },
|
|
18
|
+
iotFleets: { ids: new Set<string>(), names: new Set<string>() },
|
|
18
19
|
services: { ids: new Set<string>(), names: new Set<string>() },
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -105,6 +106,21 @@ describe("SeriesResourceLabels", () => {
|
|
|
105
106
|
expect(refs.proxmoxClusterNames).toEqual(["pve-1"]);
|
|
106
107
|
expect(refs.cephClusterNames).toEqual(["ceph-1"]);
|
|
107
108
|
});
|
|
109
|
+
|
|
110
|
+
it("maps iot fleet name keys (prefixed and unprefixed)", () => {
|
|
111
|
+
const refs: SeriesResourceRefs = SeriesResourceLabels.extractResourceRefs(
|
|
112
|
+
{
|
|
113
|
+
"resource.iot.fleet.name": "fleet-1",
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
expect(refs.iotFleetNames).toEqual(["fleet-1"]);
|
|
117
|
+
|
|
118
|
+
const refsUnprefixed: SeriesResourceRefs =
|
|
119
|
+
SeriesResourceLabels.extractResourceRefs({
|
|
120
|
+
"iot.fleet.name": "fleet-2",
|
|
121
|
+
});
|
|
122
|
+
expect(refsUnprefixed.iotFleetNames).toEqual(["fleet-2"]);
|
|
123
|
+
});
|
|
108
124
|
});
|
|
109
125
|
});
|
|
110
126
|
|
|
@@ -171,6 +187,26 @@ describe("MonitorMaintenanceSuppression.getSuppressedFingerprintsForMaintainedRe
|
|
|
171
187
|
]);
|
|
172
188
|
});
|
|
173
189
|
|
|
190
|
+
it("suppresses an IoT series whose fleet is under maintenance", () => {
|
|
191
|
+
const maintained: MaintainedResourceKeys = emptyMaintained();
|
|
192
|
+
maintained.iotFleets.names.add("warehouse-sensors");
|
|
193
|
+
|
|
194
|
+
const result: Set<string> =
|
|
195
|
+
MonitorMaintenanceSuppression.getSuppressedFingerprintsForMaintainedResources(
|
|
196
|
+
{
|
|
197
|
+
matchesPerSeries: [
|
|
198
|
+
series("fpFleet", {
|
|
199
|
+
"resource.iot.fleet.name": "warehouse-sensors",
|
|
200
|
+
}),
|
|
201
|
+
series("fpClear", { "resource.iot.fleet.name": "office-sensors" }),
|
|
202
|
+
],
|
|
203
|
+
maintained,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(Array.from(result)).toEqual(["fpFleet"]);
|
|
208
|
+
});
|
|
209
|
+
|
|
174
210
|
it("does not cross-match resource types that happen to share a name", () => {
|
|
175
211
|
/*
|
|
176
212
|
* A service named the same string as the breaching host must not
|