Skip to content

Commit 5ed1a57

Browse files
joshknshoes
andauthored
Support Devices sending log messages via a new Logging extension (#2080)
Companion to nerves-hub/nerves_hub_link#303 New log lines are streamed to the UI, with only 25 max shown. This has been implemented using **[ClickHouse](https://clickhouse.com/)**. I've also used [hammer](https://hexdocs.pm/hammer) for rate limiting log messages (3 per second, with a burst capacity of 10). **Pros:** - built-in row truncation (defaults to 3 days) - isolate analytics from core platform functionality - the analytics system isn't required, and if turned off, the logging extension can't be attached **Cons:** - we don't get transaction isolation in tests - we don't get nice association helpers as these are separate ecto repos **Future possibilities:** - "Show more" - Filtering on level - Graph of number of log lines over X period - Search log messages (not sure how well this would work with ClickHouse) **Things to note:** - using `make iex-server` will try to start the analytics subsystem - to skip the analytics subsystem, use `ANALYTICS_ENABLED=false make iex-server` - the docker compose file has been updated to include ClickHouse, its recommended to use this for starting supplementary systems like Postgres and ClickHouse. --- **Simple UI** (to start with) https://github.com/user-attachments/assets/b4ccd5e8-2411-4423-809e-8b48d19ebbff --- **To finish:** - [x] `device_log_lines` truncation (customizable time period) - [x] tests --------- Co-authored-by: Nate Shoemaker <[email protected]>
1 parent 6db750b commit 5ed1a57

File tree

36 files changed

+875
-34
lines changed

36 files changed

+875
-34
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"castore",
2323
"checkmark",
2424
"circuitboard",
25+
"clickhouse",
2526
"clipcopy",
2627
"comeonin",
2728
"conzole",

.dialyzer_ignore.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
{"lib/x509/certificate.ex", "Unknown type: X509.ASN1.record/1."},
33
{"lib/x509/certificate/extension.ex", "Unknown type: X509.ASN1.record/1."},
44
{"lib/nerves_hub_web/channels/device_channel.ex", :unmatched_return, 1},
5-
{"lib/nerves_hub_web/channels/device_socket.ex", :unmatched_return, 1}
5+
{"lib/nerves_hub_web/channels/device_socket.ex", :unmatched_return, 1},
6+
{"lib/nerves_hub/analytics_repo.ex", :contract_supertype, 1}
67
]

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
FWUP_VERSION: "1.12.0"
2323
MIX_ENV: "test"
2424
DATABASE_URL: postgres://postgres:postgres@localhost:5432/nerves_hub_test
25+
CLICKHOUSE_URL: http://default:@localhost:8123/default
2526

2627
services:
2728
db:
@@ -34,6 +35,13 @@ jobs:
3435
--health-interval 10s
3536
--health-timeout 5s
3637
--health-retries 5
38+
clickhouse:
39+
image: clickhouse/clickhouse-server:25.4.2.31
40+
env:
41+
CLICKHOUSE_SKIP_USER_SETUP: 1
42+
ports:
43+
- "8123:8123"
44+
- "9000:9000"
3745

3846
steps:
3947
- name: Install system deps
@@ -47,7 +55,7 @@ jobs:
4755
- name: Set up Node.js
4856
uses: actions/setup-node@v4
4957
with:
50-
node-version: '20'
58+
node-version: "20"
5159

5260
- name: Install cspell
5361
run: npm install -g cspell

assets/ui-rework/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import DeviceLocationMapWithGeocoder from "./hooks/deviceLocationMapWithGeocoder
1414
import Flash from "./hooks/flash.js"
1515
import HighlightCode from "./hooks/highlightCode.js"
1616
import LocalTime from "./hooks/localTime.js"
17+
import LogLineLocalTime from "./hooks/logLineLocalTime.js"
1718
import SharedSecretClipboardClick from "./hooks/sharedSecretClipboardClick.js"
1819
import SimpleDate from "./hooks/simpleDate.js"
1920
import SupportScriptOutput from "./hooks/supportScriptOutput.js"
@@ -38,6 +39,7 @@ let liveSocket = new LiveSocket("/live", Socket, {
3839
Flash,
3940
HighlightCode,
4041
LocalTime,
42+
LogLineLocalTime,
4143
SharedSecretClipboardClick,
4244
SimpleDate,
4345
SupportScriptOutput,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export default {
2+
mounted() {
3+
this.updated()
4+
},
5+
updated() {
6+
let dt = new Date(this.el.textContent.trim())
7+
8+
function p(s) {
9+
return s < 10 ? "0" + s : s
10+
}
11+
12+
function p3(s) {
13+
let result
14+
if (s < 10) {
15+
result = "00" + s
16+
} else if (s < 100) {
17+
result = "0" + s
18+
} else {
19+
result = s
20+
}
21+
return result
22+
}
23+
24+
const tzAbbr = () => {
25+
var dateObject = new Date(),
26+
dateString = dateObject + "",
27+
tzAbbr =
28+
// Works for the majority of modern browsers
29+
dateString.match(/\(([^\)]+)\)$/) ||
30+
// IE outputs date strings in a different format:
31+
dateString.match(/([A-Z]+) [\d]{4}$/)
32+
33+
if (tzAbbr) {
34+
// Old Firefox uses the long timezone name (e.g., "Central
35+
// Daylight Time" instead of "CDT")
36+
tzAbbr = tzAbbr[1].match(/[A-Z]/g).join("")
37+
}
38+
39+
// Return a GMT offset for browsers that don't include the
40+
// user's zone abbreviation (e.g. "GMT-0500".)
41+
// First seen on: http://stackoverflow.com/a/12496442
42+
if (!tzAbbr && /(GMT\W*\d{4})/.test(dateString)) {
43+
return /(GMT\W*\d{4})/.exec(dateString)[1]
44+
}
45+
46+
return tzAbbr
47+
}
48+
49+
const day = dt.getDate()
50+
const month = dt.getMonth() + 1
51+
const year = dt.getFullYear()
52+
const hour = dt.getHours()
53+
const minute = dt.getMinutes()
54+
const seconds = dt.getSeconds()
55+
const milliseconds = dt.getMilliseconds()
56+
const timezone = tzAbbr()
57+
58+
const time = `${p(hour)}:${p(minute)}:${p(seconds)}.${p3(milliseconds)}`
59+
60+
this.el.textContent = `${year}-${p(month)}-${p(day)} ${time} ${timezone}`
61+
}
62+
}

config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ config :mime, :types, %{
1818
config :nerves_hub,
1919
env: Mix.env(),
2020
namespace: NervesHub,
21-
ecto_repos: [NervesHub.Repo]
21+
ecto_repos: [NervesHub.AnalyticsRepo, NervesHub.Repo]
2222

2323
##
2424
# NervesHub Device

config/dev.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ config :nerves_hub, NervesHub.ObanRepo,
8888
pool_size: 10,
8989
ssl: false
9090

91+
if System.get_env("ANALYTICS_ENABLED", "true") == "true" do
92+
config :nerves_hub, NervesHub.AnalyticsRepo,
93+
url: System.get_env("CLICKHOUSE_URL", "http://default:@localhost:8123/default")
94+
95+
config :nerves_hub, analytics_enabled: true
96+
else
97+
config :nerves_hub, analytics_enabled: false
98+
end
99+
91100
##
92101
# Firmware upload
93102
#

config/runtime.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ config :nerves_hub,
5050
System.get_env("FEATURES_HEALTH_INTERVAL_MINUTES", "60") |> String.to_integer(),
5151
ui_polling_seconds:
5252
System.get_env("FEATURES_HEALTH_UI_POLLING_SECONDS", "60") |> String.to_integer()
53+
],
54+
logging: [
55+
days_to_keep: String.to_integer(System.get_env("EXTENSIONS_LOGGING_DAYS_TO_KEEP", "3"))
5356
]
5457
],
5558
new_ui: System.get_env("NEW_UI_ENABLED", "true") == "true"
@@ -264,6 +267,16 @@ if config_env() == :prod do
264267
database_auto_migrator: System.get_env("DATABASE_AUTO_MIGRATOR", "true") == "true"
265268
end
266269

270+
if config_env() == :prod do
271+
if clickhouse_url = System.get_env("CLICKHOUSE_URL") do
272+
config :nerves_hub, NervesHub.AnalyticsRepo, url: clickhouse_url
273+
274+
config :nerves_hub, analytics_enabled: true
275+
else
276+
config :nerves_hub, analytics_enabled: false
277+
end
278+
end
279+
267280
# Libcluster is using Postgres for Node discovery
268281
# The library only accepts keyword configs, so the DATABASE_URL has to be
269282
# parsed and put together with the ssl pieces from above.

config/test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ config :nerves_hub, NervesHub.ObanRepo,
7171
pool: Ecto.Adapters.SQL.Sandbox,
7272
pool_size: 10
7373

74+
config :nerves_hub, NervesHub.AnalyticsRepo,
75+
url: System.get_env("CLICKHOUSE_URL", "http://default:@localhost:8123/default")
76+
77+
config :nerves_hub, analytics_enabled: true
78+
7479
config :nerves_hub, Oban, testing: :manual
7580

7681
##

docker-compose.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: "3.9"
2-
31
services:
42
postgres:
53
image: postgres:16
@@ -8,3 +6,15 @@ services:
86
POSTGRES_PASSWORD: postgres
97
ports:
108
- 5432:5432
9+
clickhouse:
10+
image: clickhouse/clickhouse-server:25.4.2.31
11+
container_name: clickhouse-server
12+
environment:
13+
CLICKHOUSE_SKIP_USER_SETUP: 1
14+
ports:
15+
- "8123:8123"
16+
- "9000:9000"
17+
ulimits:
18+
nofile:
19+
soft: 262144
20+
hard: 262144

0 commit comments

Comments
 (0)