Merge branch 'new-dashboard'
This commit is contained in:
commit
4fbd8507a0
25 changed files with 1836 additions and 414 deletions
|
@ -9,7 +9,7 @@ WORKDIR /app
|
|||
RUN mix deps.get
|
||||
RUN mix deps.compile
|
||||
|
||||
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node:latest as assets
|
||||
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine-node:latest-build as assets
|
||||
COPY --from=deps /app /app
|
||||
COPY assets /app/assets
|
||||
WORKDIR /app
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
/* This file is for your main application css. */
|
||||
|
||||
@import "./phoenix.css";
|
||||
@import "../../deps/phoenix_live_view/assets/css/live_view.css";
|
7
augie/assets/css/app.scss
Normal file
7
augie/assets/css/app.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
/* This file is for your main application css. */
|
||||
|
||||
// import "./phoenix.css";
|
||||
// import "../../deps/phoenix_live_view/assets/css/live_view.css";
|
||||
|
||||
@import "~foundation-sites/scss/foundation";
|
||||
@include foundation-everything();
|
|
@ -1,7 +1,7 @@
|
|||
// We need to import the CSS so that webpack will load it.
|
||||
// The MiniCssExtractPlugin is used to separate it out into
|
||||
// its own CSS file.
|
||||
import css from "../css/app.css"
|
||||
import css from "../css/app.scss"
|
||||
|
||||
// webpack automatically bundles all modules in your
|
||||
// entry points. Those entry points can be configured
|
||||
|
@ -21,3 +21,10 @@ import LiveSocket from "phoenix_live_view"
|
|||
|
||||
let liveSocket = new LiveSocket("/live", Socket)
|
||||
liveSocket.connect()
|
||||
|
||||
import jQuery from "jquery";
|
||||
window.$ = jQuery;
|
||||
import "what-input"
|
||||
import "foundation-sites/js/foundation"
|
||||
|
||||
$(document).foundation();
|
||||
|
|
|
@ -6,20 +6,25 @@
|
|||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"foundation-sites": "^6.5.3",
|
||||
"jquery": "^3.4.1",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view"
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"what-input": "^5.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"copy-webpack-plugin": "^4.5.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"copy-webpack-plugin": "^5.0.5",
|
||||
"css-loader": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"webpack": "4.4.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"terser-webpack-plugin": "^2.2.1",
|
||||
"webpack": "4.41.2",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@ module.exports = (env, options) => ({
|
|||
path: path.resolve(__dirname, '../priv/static/js')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
|
@ -29,8 +28,8 @@ module.exports = (env, options) => ({
|
|||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader']
|
||||
test: /\.s?css$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,19 +19,20 @@ defmodule Augie.Application do
|
|||
AugieWeb.Endpoint,
|
||||
# Starts a worker by calling: Augie.Worker.start_link(arg)
|
||||
# {Augie.Worker, arg},
|
||||
Augie.UpdateLockManager,
|
||||
Augie.DistributedSupervisor,
|
||||
Augie.Collector.RaspberryPi
|
||||
Augie.UpdateLockManager,
|
||||
Augie.Collector.RaspberryPi,
|
||||
Augie.Collector.ErlangSystem,
|
||||
Augie.Collector.ErlangMemory
|
||||
]
|
||||
|
||||
children =
|
||||
if enable_mdns?(),
|
||||
do:
|
||||
children ++
|
||||
[
|
||||
Augie.MdnsInterfaceWatcher,
|
||||
{Cluster.Supervisor, [topologies, [name: Augie.ClusterSupervisor]]}
|
||||
],
|
||||
do: [
|
||||
Augie.MdnsInterfaceWatcher,
|
||||
{Cluster.Supervisor, [topologies, [name: Augie.ClusterSupervisor]]}
|
||||
| children
|
||||
],
|
||||
else: children
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
|
|
@ -24,6 +24,17 @@ defmodule Augie.Collector do
|
|||
# config :augie, Augie.Collector, sample_gc_freq: 1000
|
||||
@default_sample_gc_freq 1_000
|
||||
|
||||
@doc """
|
||||
Check if the collector is up and running - sometimes it takes a while to start
|
||||
due to Swarm syncs.
|
||||
"""
|
||||
@spec alive?() :: bool
|
||||
def alive? do
|
||||
__MODULE__
|
||||
|> Swarm.whereis_name()
|
||||
|> is_pid()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Store a record (or records) for a given device at the current time. Devices and metrics are
|
||||
created dynamically.
|
||||
|
@ -107,28 +118,52 @@ defmodule Augie.Collector do
|
|||
containing a five-element tuple of `:metric`, the device identifier, the
|
||||
sample name, the sample value and the sample time.
|
||||
"""
|
||||
@spec subscribe(device_id) :: :ok | {:error, any}
|
||||
def subscribe(device_id), do: Swarm.join({__MODULE__, :subscribers, device_id}, self())
|
||||
@spec subscribe_to_device(device_id) :: :ok | {:error, any}
|
||||
def subscribe_to_device(device_id) do
|
||||
with :ok <- :pg2.create({__MODULE__, :subscribers, device_id}),
|
||||
:ok <- :pg2.join({__MODULE__, :subscribers, device_id}, self()),
|
||||
do: :ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Subscribe to a specifi metric from a specific device.
|
||||
"""
|
||||
@spec subscribe(device_id, metric_name) :: :ok | {:error, any}
|
||||
def subscribe(device_id, metric_name),
|
||||
do: Swarm.join({__MODULE__, :subscribers, device_id, metric_name}, self())
|
||||
@spec subscribe_to_metric(device_id, metric_name) :: :ok | {:error, any}
|
||||
def subscribe_to_metric(device_id, metric_name) do
|
||||
with :ok <- :pg2.create({__MODULE__, :subscribers, device_id, metric_name}),
|
||||
:ok <- :pg2.join({__MODULE__, :subscribers, device_id, metric_name}, self()),
|
||||
do: :ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Subscribe to updates about new devices.
|
||||
"""
|
||||
@spec subscribe_to_new_devices() :: :ok | {:error, any}
|
||||
def subscribe_to_new_devices do
|
||||
with :ok <- :pg2.create({__MODULE__, :new_device_subscribers}),
|
||||
:ok <- :pg2.join({__MODULE__, :new_device_subscribers}, self()),
|
||||
do: :ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unsubscribe the caller from the device.
|
||||
"""
|
||||
@spec unsubscribe(device_id) :: :ok | {:error, any}
|
||||
def unsubscribe(device_id), do: Swarm.leave({__MODULE__, :subscribers, device_id}, self())
|
||||
@spec unsubscribe_from_device(device_id) :: :ok | {:error, any}
|
||||
def unsubscribe_from_device(device_id),
|
||||
do: :pg2.leave({__MODULE__, :subscribers, device_id}, self())
|
||||
|
||||
@doc """
|
||||
Unsubscribe the caller from a specific metric.
|
||||
"""
|
||||
@spec unsubscribe(device_id, metric_name) :: :ok | {:error, any}
|
||||
def unsubscribe(device_id, metric_name),
|
||||
do: Swarm.leave({__MODULE__, :subscribers, device_id, metric_name}, self())
|
||||
@spec unsubscribe_from_metric(device_id, metric_name) :: :ok | {:error, any}
|
||||
def unsubscribe_from_metric(device_id, metric_name),
|
||||
do: :pg2.leave({__MODULE__, :subscribers, device_id, metric_name}, self())
|
||||
|
||||
@doc """
|
||||
Unsubscribe the caller from new device events.
|
||||
"""
|
||||
@spec unsubscribe_from_new_devices :: :ok | {:error, any}
|
||||
def unsubscribe_from_new_devices, do: :pg2.leave({__MODULE__, :new_device_subscribers}, self())
|
||||
|
||||
@doc false
|
||||
def child_spec(_) do
|
||||
|
@ -158,7 +193,7 @@ defmodule Augie.Collector do
|
|||
|
||||
{:ok, timer} = :timer.send_interval(sample_gc_freq, :gc)
|
||||
|
||||
{:ok, %{table: table, sample_ttl: sample_ttl, timer: timer}}
|
||||
{:ok, %{table: table, sample_ttl: sample_ttl, timer: timer, devices: []}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -168,42 +203,46 @@ defmodule Augie.Collector do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:record, device_id, values, sampled_at}, %{table: table} = state) do
|
||||
def handle_cast(
|
||||
{:record, device_id, values, sampled_at},
|
||||
%{table: table, devices: devices} = state
|
||||
) do
|
||||
sampled_at_us = sampled_at |> DateTime.to_unix(:microsecond)
|
||||
|
||||
rows =
|
||||
values
|
||||
|> Enum.map(fn {name, value} ->
|
||||
# I know, I know. Side-effects in a map.
|
||||
Swarm.publish(
|
||||
{__MODULE__, :subscribers, device_id},
|
||||
{:metric, device_id, name, value, sampled_at}
|
||||
)
|
||||
|
||||
Swarm.publish(
|
||||
{__MODULE__, :subscribers, name, device_id},
|
||||
{:metric, device_id, name, value, sampled_at}
|
||||
)
|
||||
|
||||
broadcast_metric(device_id, name, value, sampled_at)
|
||||
{device_id, :metric, name, value, sampled_at_us}
|
||||
end)
|
||||
|
||||
:ets.insert(table, rows)
|
||||
|
||||
{:noreply, state}
|
||||
if Enum.member?(devices, device_id) do
|
||||
{:noreply, state}
|
||||
else
|
||||
broadcast_new_device(device_id)
|
||||
{:noreply, %{state | devices: [device_id | devices]}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:metadata, device_id, metadata}, %{table: table} = state) do
|
||||
def handle_cast({:metadata, device_id, metadata}, %{table: table, devices: devices} = state) do
|
||||
rows =
|
||||
metadata
|
||||
|> Enum.map(fn {name, value} ->
|
||||
Swarm.publish({__MODULE__, :subscribers, device_id}, {:metadata, device_id, name, value})
|
||||
broadcast_metadata(device_id, name, value)
|
||||
{device_id, :metadata, name, value}
|
||||
end)
|
||||
|
||||
:ets.insert(table, rows)
|
||||
|
||||
{:noreply, state}
|
||||
if Enum.member?(devices, device_id) do
|
||||
{:noreply, state}
|
||||
else
|
||||
broadcast_new_device(device_id)
|
||||
{:noreply, %{state | devices: [device_id | devices]}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -292,6 +331,7 @@ defmodule Augie.Collector do
|
|||
{last, min, max, avg} =
|
||||
samples
|
||||
|> Map.keys()
|
||||
|> Enum.sort()
|
||||
|> Enum.reduce({[], [], [], []}, fn name, {last, min, max, avg} ->
|
||||
samples_for_this_metric =
|
||||
samples
|
||||
|
@ -319,4 +359,52 @@ defmodule Augie.Collector do
|
|||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp broadcast_metric(device_id, metric, sample, sampled_at) do
|
||||
device_members =
|
||||
case :pg2.get_members({__MODULE__, :subscribers, device_id}) do
|
||||
members when is_list(members) -> members
|
||||
{:error, {:no_such_group, _}} -> []
|
||||
end
|
||||
|
||||
metric_members =
|
||||
case :pg2.get_members({__MODULE__, :subscribers, device_id, metric}) do
|
||||
members when is_list(members) -> members
|
||||
{:error, {:no_such_group, _}} -> []
|
||||
end
|
||||
|
||||
device_members
|
||||
|> Enum.concat(metric_members)
|
||||
|> Enum.sort()
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(&send(&1, {:metric, device_id, metric, sample, sampled_at}))
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp broadcast_metadata(device_id, name, value) do
|
||||
case :pg2.get_members({__MODULE__, :subscribers, device_id}) do
|
||||
members when is_list(members) ->
|
||||
members
|
||||
|> Enum.each(&send(&1, {:metadata, device_id, name, value}))
|
||||
|
||||
:ok
|
||||
|
||||
{:error, {:no_such_group, _}} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp broadcast_new_device(device_id) do
|
||||
case :pg2.get_members({__MODULE__, :new_device_subscribers}) do
|
||||
members when is_list(members) ->
|
||||
members
|
||||
|> Enum.each(&send(&1, {:new_device, device_id}))
|
||||
|
||||
:ok
|
||||
|
||||
{:error, {:no_such_group, _}} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,6 +72,6 @@ defmodule Augie.Collector.Cluster do
|
|||
end
|
||||
|
||||
defp update_metadata(nodes) do
|
||||
Collector.metadata(__MODULE__, nodes: nodes, collector: __MODULE__)
|
||||
Collector.metadata(__MODULE__, nodes: nodes, collector: __MODULE__, name: "Cluster Health")
|
||||
end
|
||||
end
|
||||
|
|
52
augie/lib/augie/collector/erlang_memory.ex
Normal file
52
augie/lib/augie/collector/erlang_memory.ex
Normal file
|
@ -0,0 +1,52 @@
|
|||
defmodule Augie.Collector.ErlangMemory do
|
||||
alias Augie.Collector
|
||||
use GenServer
|
||||
|
||||
@moduledoc """
|
||||
This module retrieves information about the Erlang system it is running on.
|
||||
"""
|
||||
|
||||
@update_interval 5_000
|
||||
|
||||
@doc false
|
||||
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
{:ok, %{timer: timer, count: 0}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, %{timer: timer}) do
|
||||
:timer.cancel(timer)
|
||||
:ok
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:tick, %{count: 3} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(:tick, %{count: count} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
{:noreply, %{state | count: count + 1}}
|
||||
end
|
||||
|
||||
defp collect_stats() do
|
||||
ExErlstats.memory()
|
||||
|> Enum.into([])
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
end
|
||||
|
||||
defp metadata() do
|
||||
[
|
||||
name: "Erlang Memory",
|
||||
collector: __MODULE__,
|
||||
node: node()
|
||||
]
|
||||
end
|
||||
end
|
57
augie/lib/augie/collector/erlang_system.ex
Normal file
57
augie/lib/augie/collector/erlang_system.ex
Normal file
|
@ -0,0 +1,57 @@
|
|||
defmodule Augie.Collector.ErlangSystem do
|
||||
alias Augie.Collector
|
||||
use GenServer
|
||||
|
||||
@moduledoc """
|
||||
This module retrieves information about the Erlang system it is running on.
|
||||
"""
|
||||
|
||||
@update_interval 5_000
|
||||
|
||||
@doc false
|
||||
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
{:ok, %{timer: timer, count: 0}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, %{timer: timer}) do
|
||||
:timer.cancel(timer)
|
||||
:ok
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:tick, %{count: 3} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(:tick, %{count: count} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
{:noreply, %{state | count: count + 1}}
|
||||
end
|
||||
|
||||
defp collect_stats() do
|
||||
sys_info =
|
||||
ExErlstats.system_info()
|
||||
|> Map.take(~w[port_count process_count schedulers_online]a)
|
||||
|
||||
ExErlstats.stats()
|
||||
|> Map.take(~w[run_queue, total_active_tasks total_run_queue_lengths]a)
|
||||
|> Map.merge(sys_info)
|
||||
|> Enum.into([])
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
end
|
||||
|
||||
defp metadata() do
|
||||
ExErlstats.system_info()
|
||||
|> Map.take(~w[otp_release port_limit process_limit schedulers version]a)
|
||||
|> Map.merge(%{name: "Erlang System", collector: __MODULE__, node: node()})
|
||||
|> Enum.into([])
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ defmodule Augie.Collector.RaspberryPi do
|
|||
the collector. Mostly using the `vcgencmd` utility.
|
||||
"""
|
||||
|
||||
@update_frequency 5_000
|
||||
@update_interval 5_000
|
||||
@vcgencmd_stats [
|
||||
temperature: "measure_temp",
|
||||
arm_clock: "measure_clock arm",
|
||||
|
@ -17,25 +17,14 @@ defmodule Augie.Collector.RaspberryPi do
|
|||
sdram_p_voltage: "measure_volts sdram_p"
|
||||
]
|
||||
|
||||
@doc false
|
||||
def child_spec(_) do
|
||||
%{
|
||||
id: {__MODULE__, hostname()},
|
||||
start: {GenServer, :start_link, [__MODULE__, []]},
|
||||
restart: :permanent,
|
||||
type: :worker
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
hostname = hostname()
|
||||
Collector.metadata({__MODULE__, hostname}, metadata())
|
||||
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
|
||||
{:ok, %{hostname: hostname, timer: timer}}
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
{:ok, %{timer: timer, count: 0}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -45,11 +34,17 @@ defmodule Augie.Collector.RaspberryPi do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:tick, %{hostname: hostname} = state) do
|
||||
Collector.record({__MODULE__, hostname}, collect_stats())
|
||||
def handle_info(:tick, %{count: 3} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
Collector.metadata({__MODULE__, node()}, metadata())
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(:tick, %{count: count} = state) do
|
||||
Collector.record({__MODULE__, node()}, collect_stats())
|
||||
{:noreply, %{state | count: count + 1}}
|
||||
end
|
||||
|
||||
defp collect_stats do
|
||||
@vcgencmd_stats
|
||||
|> Enum.reduce([], fn {name, command}, stats ->
|
||||
|
@ -92,7 +87,8 @@ defmodule Augie.Collector.RaspberryPi do
|
|||
model: read_file("/sys/firmware/devicetree/base/model", "Unknown model"),
|
||||
serial_number: read_file("/sys/firmware/devicetree/base/serial-number"),
|
||||
memory: total_memory(),
|
||||
collector: __MODULE__
|
||||
collector: __MODULE__,
|
||||
name: "Raspberry Pi Status"
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule Augie.MdnsClusterStrategy do
|
|||
|
||||
@service_domain :"_erlang._tcp.local"
|
||||
@host_domain "_ppp.local"
|
||||
@update_frequency 10_000
|
||||
@update_interval 10_000
|
||||
|
||||
@doc false
|
||||
def start_link(args), do: GenServer.start_link(__MODULE__, args)
|
||||
|
@ -24,7 +24,7 @@ defmodule Augie.MdnsClusterStrategy do
|
|||
:ok = Mdns.EventManager.register()
|
||||
:timer.sleep(100)
|
||||
advertise_services()
|
||||
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
send(self(), :tick)
|
||||
{:ok, %{seen: [], state: state, timer: timer}}
|
||||
end
|
||||
|
|
|
@ -17,14 +17,14 @@ defmodule Augie.MdnsInterfaceWatcher do
|
|||
"""
|
||||
|
||||
@default_interface "eth0"
|
||||
@update_frequency 10_000
|
||||
@update_interval 10_000
|
||||
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, [])
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
interface = System.get_env("MDNS_INTERFACE", @default_interface)
|
||||
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
send(self(), :tick)
|
||||
{:ok, %{interface: interface, timer: timer, ip_address: nil, server_running: false}}
|
||||
end
|
||||
|
|
|
@ -23,22 +23,16 @@ defmodule Augie.UpdateLockManager do
|
|||
@lockfile_location "/tmp/balena/updates.lock"
|
||||
|
||||
# How frequently to message the other peer(s) and decide whom should be the lock holder.
|
||||
@update_frequency 10_000
|
||||
@update_interval 10_000
|
||||
|
||||
@doc false
|
||||
def start_link(args) do
|
||||
with {:ok, pid} <- GenServer.start_link(__MODULE__, [args]),
|
||||
:yes <- Swarm.register_name({__MODULE__, node()}, pid),
|
||||
:ok <- Swarm.join(__MODULE__, pid) do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
def start_link(args), do: GenServer.start_link(__MODULE__, [args])
|
||||
|
||||
@impl true
|
||||
def init(_args) do
|
||||
if File.exists?(@lockfile_location), do: File.rmdir(@lockfile_location)
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, timer} = :timer.send_interval(@update_frequency, :tick)
|
||||
{:ok, timer} = :timer.send_interval(@update_interval, :tick)
|
||||
now = DateTime.utc_now()
|
||||
{:ok, %State{timer: timer, started_at: now}}
|
||||
end
|
||||
|
@ -66,7 +60,7 @@ defmodule Augie.UpdateLockManager do
|
|||
|> Enum.reject(fn {_, %{last_seen: d}} ->
|
||||
now = DateTime.utc_now() |> DateTime.to_unix()
|
||||
last_seen_at = d |> DateTime.to_unix()
|
||||
now - last_seen_at > @update_frequency * 3
|
||||
now - last_seen_at > @update_interval * 3
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
|
|
56
augie/lib/augie_web/live/dashboard_live.ex
Normal file
56
augie/lib/augie_web/live/dashboard_live.ex
Normal file
|
@ -0,0 +1,56 @@
|
|||
defmodule AugieWeb.DashboardLive do
|
||||
use Phoenix.LiveView
|
||||
alias Augie.Collector
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<%= if @collector_alive do %>
|
||||
<div class="grid-x grid-padding-x grid-padding-y small-up-1 medium-up-2 large-up-3">
|
||||
<%= for device <- @devices do %>
|
||||
<div class="cell">
|
||||
<%= live_render(@socket, AugieWeb.DeviceMetricLive, id: Base.encode64(:erlang.term_to_binary(device)), session: %{id: device}) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="callout alert">
|
||||
<h2>Collector not running</h2>
|
||||
|
||||
<p>
|
||||
The statistics collector is not running, so there are not stats to show you. This probably means that the machine is still coming up.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_, socket) do
|
||||
if Collector.alive?() do
|
||||
if connected?(socket), do: Collector.subscribe_to_new_devices()
|
||||
{:ok, devices} = Collector.devices()
|
||||
|
||||
{:ok, assign(socket, devices: device_sorter(devices), collector_alive: true)}
|
||||
else
|
||||
if connected?(socket), do: Process.send_after(self(), :collector_check, 250)
|
||||
{:ok, assign(socket, devices: [], collector_alive: false)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:collector_check, socket) do
|
||||
{:ok, socket} = mount(nil, socket)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info({:new_device, device_id}, socket) do
|
||||
devices = [device_id | socket.assigns.devices]
|
||||
{:noreply, assign(socket, devices: device_sorter(devices))}
|
||||
end
|
||||
|
||||
defp device_sorter(devices) do
|
||||
devices
|
||||
|> Enum.sort_by(fn
|
||||
{module, id} when is_atom(module) -> {module, id}
|
||||
module when is_atom(module) -> {module, 0}
|
||||
end)
|
||||
end
|
||||
end
|
79
augie/lib/augie_web/live/device_metric_live.ex
Normal file
79
augie/lib/augie_web/live/device_metric_live.ex
Normal file
|
@ -0,0 +1,79 @@
|
|||
defmodule AugieWeb.DeviceMetricLive do
|
||||
use Phoenix.LiveView
|
||||
alias Augie.Collector
|
||||
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="card">
|
||||
<div class="card-divider">
|
||||
<h4><%= Keyword.get(@metadata, :name, "Unknown Device") %></h4>
|
||||
</div>
|
||||
<%= if Enum.any?(@metadata) do %>
|
||||
<table class="table card-section unstriped stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2">Metadata</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {name, value} <- Keyword.drop(@metadata, ~w[name collector]a) do %>
|
||||
<tr>
|
||||
<td><%= to_string name %></td>
|
||||
<td>
|
||||
<%= if is_list(value) do %>
|
||||
<%= Enum.join(value, ", ") %>
|
||||
<% else %>
|
||||
<%= to_string value %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
<%= if Enum.any?(@last) do %>
|
||||
<table class="table card-section unstriped stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2">Metrics</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {name, {sample, _}} <- @last do %>
|
||||
<tr>
|
||||
<td><%= to_string name %></td>
|
||||
<td><%= sample %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(%{id: device_id}, socket) do
|
||||
if connected?(socket), do: Collector.subscribe_to_device(device_id)
|
||||
|
||||
{:ok, status} = Collector.status(device_id)
|
||||
{:ok, assign(socket, Enum.into(status, %{}))}
|
||||
end
|
||||
|
||||
def handle_info({:metric, _key, metric_name, value, sample_time}, socket) do
|
||||
last =
|
||||
socket.assigns.last
|
||||
|> Keyword.put(metric_name, {value, sample_time})
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|
||||
{:noreply, assign(socket, %{last: last})}
|
||||
end
|
||||
|
||||
def handle_info({:metadata, _key, name, value}, socket) do
|
||||
metadata =
|
||||
socket.assigns.metadata
|
||||
|> Keyword.put(name, value)
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|
||||
{:noreply, assign(socket, %{metadata: metadata})}
|
||||
end
|
||||
end
|
|
@ -18,6 +18,7 @@ defmodule AugieWeb.Router do
|
|||
pipe_through(:browser)
|
||||
|
||||
get("/", PageController, :index)
|
||||
live("/dashboard", DashboardLive)
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
|
|
@ -1,30 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Augie · Phoenix Framework</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Augie the hexapod robot</title>
|
||||
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<section class="container">
|
||||
<nav role="navigation">
|
||||
<ul>
|
||||
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a href="http://phoenixframework.org/" class="phx-logo">
|
||||
<img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
|
||||
</a>
|
||||
</section>
|
||||
</header>
|
||||
<main role="main" class="container">
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<ul class="dropdown menu" data-dropdown-menu>
|
||||
<li class="menu-text">Augie <span class="label"><%= app_version() %></span></li>
|
||||
<li>
|
||||
<%= link "Dashboard", to: Routes.live_path(@conn, AugieWeb.DashboardLive) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="grid-x grid-margin-x">
|
||||
<div class="cell">
|
||||
<%= if get_flash(@conn, :info) do %>
|
||||
<div class="callout warning">
|
||||
<%= get_flash(@conn, :info) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<div class="callout alert">
|
||||
<%= get_flash(@conn, :error) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<section class="phx-hero">
|
||||
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
|
||||
<p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<article class="column">
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://hexdocs.pm/phoenix/overview.html">Guides & Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md">v1.4 Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="column">
|
||||
<h2>Help</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
|
@ -1,3 +1,7 @@
|
|||
defmodule AugieWeb.LayoutView do
|
||||
use AugieWeb, :view
|
||||
|
||||
def app_version() do
|
||||
Application.spec(:augie, :vsn)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,21 +32,22 @@ defmodule Augie.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.4.11"},
|
||||
{:phoenix_pubsub, "~> 1.1"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:balena_device, "~> 0.1"},
|
||||
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
|
||||
{:ex_erlstats, "~> 0.1"},
|
||||
{:floki, ">= 0.0.0", only: :test},
|
||||
{:gettext, "~> 0.11"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:balena_device, "~> 0.1"},
|
||||
{:mdns, "~> 1.0"},
|
||||
{:swarm, "~> 3.4"},
|
||||
{:swarm_dynamic_supervisor, "~> 0.1.0"},
|
||||
{:libcluster, "~> 3.1"},
|
||||
{:mdns, "~> 1.0"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.4.0"},
|
||||
{:floki, ">= 0.0.0", only: :test},
|
||||
{:credo, "~> 1.1", only: [:dev, :test], runtime: false}
|
||||
{:phoenix_pubsub, "~> 1.1"},
|
||||
{:phoenix, "~> 1.4.11"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:swarm_dynamic_supervisor, "~> 0.1.0"},
|
||||
{:swarm, "~> 3.4"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"dbus": {:hex, :dbus, "0.7.0", "71b7660523be6e222a8a4f9ddcbf54ad95638479bd17654975bb751aadbe81de", [:make, :rebar3], [], "hexpm"},
|
||||
"dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"ex_erlstats": {:hex, :ex_erlstats, "0.1.6", "f06af6136e67c2ba65848d411f63de8d672f972907e2e357c14ab3d067a6269c", [:mix], [], "hexpm"},
|
||||
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
|
||||
"floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
|
||||
|
|
Binary file not shown.
Reference in a new issue