Skip to content

Commit b537225

Browse files
elinoljoshk
andauthored
Very basic health status report for device (#1805)
A very very simple module for getting device health status based on latest metric set, together with tests. Not sure if it's even worth merging? Might just be a start for further work on health statuses. Note: default thresholds are currently just placeholders and should probably be updated. --------- Co-authored-by: Josh Kalderimis <[email protected]>
1 parent 07f22b4 commit b537225

File tree

24 files changed

+2158
-5409
lines changed

24 files changed

+2158
-5409
lines changed

assets/package-lock.json

Lines changed: 1592 additions & 5348 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"check_formatting": "prettier -c js/**/*.js"
1212
},
1313
"dependencies": {
14+
"@floating-ui/dom": "^1.6.13",
1415
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
1516
"bootstrap": "^4.6.0",
1617
"chart.js": "^4.4.4",

assets/ui-rework/app.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,18 @@
4444
@apply pl-6;
4545
}
4646

47+
.listing tr th:nth-child(2) {
48+
@apply px-0;
49+
}
50+
4751
.listing tr td:first-child {
4852
@apply pl-6;
4953
}
5054

55+
.listing tr td:nth-child(2) {
56+
@apply px-0;
57+
}
58+
5159
.listing .checkbox {
5260
@apply h-11 w-16 py-3 px-6;
5361
}

assets/ui-rework/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Chart from "./hooks/chart.js"
1414
import HighlightCode from "./hooks/highlightCode.js"
1515
import UpdatingTimeAgo from "./hooks/updatingTimeAgo.js"
1616
import LocalTime from "./hooks/localTime.js"
17+
import ToolTip from "./hooks/toolTip.js"
1718
import SimpleDate from "./hooks/simpleDate.js"
1819
import WorldMap from "./hooks/worldMap.js"
1920
import DeviceLocationMap from "./hooks/deviceLocationMap.js"
@@ -35,6 +36,7 @@ let liveSocket = new LiveSocket("/live", Socket, {
3536
HighlightCode,
3637
UpdatingTimeAgo,
3738
LocalTime,
39+
ToolTip,
3840
SimpleDate,
3941
WorldMap,
4042
DeviceLocationMap,

assets/ui-rework/hooks/toolTip.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { computePosition, offset, arrow } from "@floating-ui/dom"
2+
3+
export default {
4+
mounted() {
5+
this.updated()
6+
},
7+
8+
updateTooltip() {
9+
computePosition(this.el, this.content, {
10+
placement: this.placement,
11+
middleware: [offset(15), arrow({ element: this.arrow })]
12+
}).then(({ x, y, middlewareData }) => {
13+
Object.assign(this.content.style, {
14+
top: `${y}px`,
15+
left: `${x}px`
16+
})
17+
18+
if (middlewareData.arrow) {
19+
const { x } = middlewareData.arrow
20+
21+
Object.assign(this.arrow.style, {
22+
left: `${x}px`,
23+
top: `${-this.arrow.offsetHeight / 2}px`
24+
})
25+
}
26+
})
27+
},
28+
29+
showTooltip() {
30+
this.content.style.display = "block"
31+
this.updateTooltip()
32+
},
33+
34+
hideTooltip() {
35+
this.content.style.display = ""
36+
},
37+
38+
updated() {
39+
this.content = this.el.getElementsByClassName("tooltip-content")[0]
40+
this.arrow = this.el.getElementsByClassName("tooltip-arrow")[0]
41+
this.placement = this.el.dataset.placement || "bottom"
42+
43+
this.el.addEventListener("mouseover", this.showTooltip.bind(this))
44+
this.el.addEventListener("mouseout", this.hideTooltip.bind(this))
45+
}
46+
}

config/runtime.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ config :nerves_hub,
2323
System.get_env("DEVICE_ENDPOINT_REDIRECT", "https://docs.nerves-hub.org/"),
2424
device_health_days_to_retain:
2525
String.to_integer(System.get_env("HEALTH_CHECK_DAYS_TO_RETAIN", "7")),
26+
device_health_delete_limit:
27+
String.to_integer(System.get_env("DEVICE_HEALTH_DELETE_LIMIT", "100000")),
2628
device_deployment_change_jitter_seconds:
2729
String.to_integer(System.get_env("DEVICE_DEPLOYMENT_CHANGE_JITTER_SECONDS", "10")),
2830
device_last_seen_update_interval_minutes:

lib/nerves_hub/devices.ex

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,16 @@ defmodule NervesHub.Devices do
7777
|> join(:left, [d, o, p], dp in assoc(d, :deployment))
7878
|> join(:left, [d, o, p, dp], f in assoc(dp, :firmware))
7979
|> join(:left, [d, o, p, dp, f], lc in assoc(d, :latest_connection), as: :latest_connection)
80+
|> join(:left, [d, o, p, dp, f, lc], lh in assoc(d, :latest_health), as: :latest_health)
8081
|> Repo.exclude_deleted()
8182
|> sort_devices(sorting)
8283
|> Filtering.build_filters(filters)
83-
|> preload([d, o, p, dp, f, latest_connection: lc],
84+
|> preload([d, o, p, dp, f, latest_connection: lc, latest_health: lh],
8485
org: o,
8586
product: p,
8687
deployment: {dp, firmware: f},
87-
latest_connection: lc
88+
latest_connection: lc,
89+
latest_health: lh
8890
)
8991
|> Flop.run(flop)
9092
end
@@ -120,7 +122,9 @@ defmodule NervesHub.Devices do
120122
|> where([d], d.product_id == ^product.id)
121123
|> Repo.exclude_deleted()
122124
|> join(:left, [d], dc in assoc(d, :latest_connection), as: :latest_connection)
125+
|> join(:left, [d, dc], dh in assoc(d, :latest_health), as: :latest_health)
123126
|> preload([latest_connection: lc], latest_connection: lc)
127+
|> preload([latest_health: lh], latest_health: lh)
124128
|> Filtering.build_filters(filters)
125129
|> sort_devices(sorting)
126130
|> Flop.run(flop)
@@ -243,6 +247,12 @@ defmodule NervesHub.Devices do
243247
|> preload([d, o, dp], org: o, deployment: dp)
244248
end
245249

250+
defp join_and_preload(query, assocs) when is_list(assocs) do
251+
Enum.reduce(assocs, query, fn assoc, q ->
252+
join_and_preload(q, assoc)
253+
end)
254+
end
255+
246256
defp join_and_preload(query, nil), do: query
247257

248258
defp join_and_preload(query, :device_certificates) do
@@ -257,6 +267,12 @@ defmodule NervesHub.Devices do
257267
|> preload([latest_connection: lc], latest_connection: lc)
258268
end
259269

270+
defp join_and_preload(query, :latest_health) do
271+
query
272+
|> join(:left, [d], dh in assoc(d, :latest_health), as: :latest_health)
273+
|> preload([latest_health: lh], latest_health: lh)
274+
end
275+
260276
def get_device_by_x509(cert) do
261277
fingerprint = NervesHub.Certificate.fingerprint(cert)
262278

@@ -1145,23 +1161,52 @@ defmodule NervesHub.Devices do
11451161
end
11461162

11471163
def save_device_health(device_status) do
1148-
device_status
1149-
|> DeviceHealth.save()
1150-
|> Repo.insert()
1164+
Multi.new()
1165+
|> Multi.insert(:insert_health, DeviceHealth.save(device_status))
1166+
|> Ecto.Multi.update_all(:update_device, &update_health_on_device/1, [])
1167+
|> Repo.transaction()
1168+
|> case do
1169+
{:ok, %{insert_health: health}} ->
1170+
{:ok, health}
1171+
1172+
{:error, _, changeset, _} ->
1173+
{:error, changeset}
1174+
end
1175+
end
1176+
1177+
defp update_health_on_device(%{insert_health: health}) do
1178+
Device
1179+
|> where(id: ^health.device_id)
1180+
|> update(set: [latest_health_id: ^health.id])
11511181
end
11521182

11531183
def truncate_device_health() do
1154-
days_to_retain =
1184+
interval =
11551185
Application.get_env(:nerves_hub, :device_health_days_to_retain)
11561186

1157-
days_ago = DateTime.shift(DateTime.utc_now(), day: -days_to_retain)
1187+
delete_limit = Application.get_env(:nerves_hub, :device_health_delete_limit)
1188+
time_ago = DateTime.shift(DateTime.utc_now(), day: -interval)
11581189

1159-
{count, _} =
1190+
query =
11601191
DeviceHealth
1161-
|> where([dh], dh.inserted_at < ^days_ago)
1162-
|> Repo.delete_all()
1192+
|> join(:inner, [dh], d in Device, on: dh.device_id == d.id)
1193+
|> where([dh, _d], dh.inserted_at < ^time_ago)
1194+
|> where([dh, d], dh.id != d.latest_health_id)
1195+
|> select([dh], dh.id)
1196+
|> limit(^delete_limit)
11631197

1164-
{:ok, count}
1198+
{delete_count, _} =
1199+
DeviceHealth
1200+
|> where([dh], dh.id in subquery(query))
1201+
|> Repo.delete_all(timeout: 30_000)
1202+
1203+
if delete_count == 0 do
1204+
:ok
1205+
else
1206+
# relax stress on Ecto pool and go again
1207+
Process.sleep(2000)
1208+
truncate_device_health()
1209+
end
11651210
end
11661211

11671212
def get_latest_health(device_id) do

lib/nerves_hub/devices/device.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule NervesHub.Devices.Device do
77
alias NervesHub.Deployments.Deployment
88
alias NervesHub.Devices.DeviceCertificate
99
alias NervesHub.Devices.DeviceConnection
10+
alias NervesHub.Devices.DeviceHealth
1011
alias NervesHub.Devices.DeviceMetric
1112
alias NervesHub.Extensions.DeviceExtensionsSetting
1213
alias NervesHub.Firmwares.FirmwareMetadata
@@ -37,6 +38,7 @@ defmodule NervesHub.Devices.Device do
3738
belongs_to(:product, Product)
3839
belongs_to(:deployment, Deployment)
3940
belongs_to(:latest_connection, DeviceConnection, type: :binary_id)
41+
belongs_to(:latest_health, DeviceHealth)
4042

4143
has_many(:device_certificates, DeviceCertificate, on_delete: :delete_all)
4244
has_many(:device_connections, DeviceConnection, on_delete: :delete_all)

lib/nerves_hub/devices/device_health.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@ defmodule NervesHub.Devices.DeviceHealth do
77

88
@type t :: %__MODULE__{}
99
@required_params [:device_id, :data]
10+
@optional_params [:status, :status_reasons]
1011

1112
schema "device_health" do
1213
belongs_to(:device, Device)
1314
field(:data, :map)
15+
16+
field(:status, Ecto.Enum,
17+
values: [:unknown, :healthy, :warning, :unhealthy],
18+
default: :unknown
19+
)
20+
21+
field(:status_reasons, :map)
22+
1423
timestamps(type: :utc_datetime_usec, updated_at: false)
1524
end
1625

1726
def save(params) do
1827
%__MODULE__{}
19-
|> cast(params, @required_params)
28+
|> cast(params, @required_params ++ @optional_params)
2029
|> validate_required(@required_params)
2130
end
2231
end

lib/nerves_hub/devices/filtering.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ defmodule NervesHub.Devices.Filtering do
3838
end
3939
end
4040

41+
def filter(query, _filters, :health_status, value) do
42+
where(query, [latest_health: lh], lh.status == ^value)
43+
end
44+
4145
def filter(query, _filters, :connection, value) do
4246
if value == "not_seen" do
4347
where(query, [d], d.status == :registered)

0 commit comments

Comments
 (0)