How to Monitor VintageNet with Vigilmon
VintageNet is the networking configuration and management library for Nerves-based Elixir embedded Linux devices. It handles WiFi, Ethernet, LTE, and VPN connections through a unified API, manages interface state machines, and provides automatic failover when a primary network interface goes down. It replaces traditional Linux network management tools (NetworkManager, wpa_supplicant scripts) with a supervised Elixir process tree that integrates cleanly with the BEAM.
Embedded devices live in environments that software services don't: RF interference degrades WiFi signal without dropping the connection entirely, LTE links have intermittent coverage, Ethernet cables get unplugged and replugged, and failover between interfaces introduces brief connectivity gaps. Without monitoring, these network events are invisible until a remote device stops responding to commands, fails to deliver sensor data, or misses its OTA update window.
Vigilmon gives your Nerves device external network observability: detect interface outages, track failover events, and alert when a remote device goes silent before your users or operators notice.
Why Monitor VintageNet on Embedded Devices?
Network failures on embedded devices have higher blast radius than on servers:
- Silent connectivity loss — WiFi can stay "associated" at the driver level while routing is broken; the device appears up but can't reach any external service
- Failover storms — a misconfigured priority order triggers constant failover between interfaces, consuming power and causing periodic connectivity gaps
- LTE signal degradation — RSSI drops over days as signal conditions change; the connection stays up but latency becomes too high for real-time applications
- DHCP lease failure — an interface connects but fails to get an IP address; VintageNet reports the interface as
:configuredbut routing doesn't work - DNS resolution failures — interface is up and IP is assigned but the DNS resolver isn't reachable; the device can ping by IP but not by hostname
- Device unreachability — network issues prevent OTA updates, telemetry delivery, and remote management commands from reaching the device
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| Interface state by name | Whether each interface is :configured, :disconnected, or in an error state |
| Active interface (current route) | Which interface is currently handling traffic |
| Failover event count | How often the active interface switches — high counts indicate instability |
| WiFi RSSI | Signal strength in dBm — declining RSSI predicts impending disconnection |
| WiFi SSID and BSSID | Whether the device roamed to a different access point |
| LTE signal quality (RSRP/RSRQ) | LTE signal strength for cellular-connected devices |
| IP address assignment time | How long DHCP takes after an interface connects |
| Heartbeat interval | Whether the device successfully phoned home within the expected window |
Step 1: Subscribe to VintageNet Property Changes
VintageNet exposes all network state through a property-based API (VintageNet.PropertyTable). Subscribe to interface state changes to instrument events:
# lib/my_app/network_monitor.ex
defmodule MyApp.NetworkMonitor do
use GenServer
require Logger
@interfaces ["eth0", "wlan0", "ppp0"]
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
# Subscribe to state changes for all managed interfaces
Enum.each(@interfaces, fn iface ->
VintageNet.subscribe(["interface", iface, "connection"])
VintageNet.subscribe(["interface", iface, "addresses"])
VintageNet.subscribe(["interface", iface, "wifi", "rssi"])
end)
VintageNet.subscribe(["interface", "eth0", "wifi", "access_point_info"])
initial_state =
Map.new(@interfaces, fn iface ->
connection = VintageNet.get(["interface", iface, "connection"])
{iface, %{connection: connection, address: nil, last_transition: System.system_time(:second)}}
end)
{:ok, initial_state}
end
def handle_info(
{VintageNet, ["interface", iface, "connection"], old_value, new_value, _meta},
state
) do
Logger.info("Network interface state changed",
interface: iface,
from: inspect(old_value),
to: inspect(new_value)
)
:telemetry.execute(
[:my_app, :network, :interface_state_change],
%{count: 1},
%{interface: iface, from: old_value, to: new_value}
)
now = System.system_time(:second)
updated =
Map.update(state, iface, %{connection: new_value, address: nil, last_transition: now}, fn s ->
%{s | connection: new_value, last_transition: now}
end)
{:noreply, updated}
end
def handle_info(
{VintageNet, ["interface", iface, "wifi", "rssi"], _old, rssi, _meta},
state
) do
:telemetry.execute(
[:my_app, :network, :wifi_rssi],
%{rssi: rssi || 0},
%{interface: iface}
)
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", iface, "addresses"], _old, addresses, _meta},
state
) do
ip =
case addresses do
[%{address: addr} | _] -> :inet.ntoa(addr) |> to_string()
_ -> nil
end
Logger.info("Interface IP address changed", interface: iface, ip: ip)
updated = Map.update(state, iface, %{connection: nil, address: ip, last_transition: nil}, fn s ->
%{s | address: ip}
end)
{:noreply, updated}
end
def handle_info(_msg, state), do: {:noreply, state}
def current_status do
GenServer.call(__MODULE__, :status)
end
def handle_call(:status, _from, state), do: {:reply, state, state}
end
Step 2: Detect Failover Events
Track when the active (default-route) interface switches:
# lib/my_app/failover_tracker.ex
defmodule MyApp.FailoverTracker do
use GenServer
require Logger
def start_link(_), do: GenServer.start_link(__MODULE__, %{active: nil, failover_count: 0}, name: __MODULE__)
def init(state) do
VintageNet.subscribe(["connection"])
current = VintageNet.get(["connection"])
{:ok, %{state | active: current}}
end
def handle_info({VintageNet, ["connection"], old_iface, new_iface, _meta}, state) do
new_count = state.failover_count + 1
Logger.info("Network failover event",
from_interface: inspect(old_iface),
to_interface: inspect(new_iface),
total_failovers: new_count
)
:telemetry.execute(
[:my_app, :network, :failover],
%{count: 1, total: new_count},
%{from: inspect(old_iface), to: inspect(new_iface)}
)
{:noreply, %{state | active: new_iface, failover_count: new_count}}
end
def handle_info(_msg, state), do: {:noreply, state}
def failover_count, do: GenServer.call(__MODULE__, :failover_count)
def handle_call(:failover_count, _from, state), do: {:reply, state.failover_count, state}
end
Step 3: Define Telemetry Metrics
# lib/my_app/telemetry.ex
def metrics do
[
# Interface state transitions
counter("my_app.network.interface_state_change.count",
tags: [:interface, :from, :to]
),
# WiFi signal strength
last_value("my_app.network.wifi_rssi.rssi",
tags: [:interface],
unit: :dbm
),
# Failover events
counter("my_app.network.failover.count",
tags: [:from, :to]
),
last_value("my_app.network.failover.total",
tags: [:from, :to]
)
]
end
Step 4: Health Endpoint
Expose network state through a health check endpoint that Vigilmon can poll:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
network_status = MyApp.NetworkMonitor.current_status()
eth0 = network_status["eth0"]
wlan0 = network_status["wlan0"]
active_connection = VintageNet.get(["connection"])
wifi_rssi = VintageNet.get(["interface", "wlan0", "wifi", "rssi"])
checks = %{
network: %{
active_interface: inspect(active_connection),
eth0: format_interface(eth0),
wlan0: format_interface(wlan0),
wifi_rssi_dbm: wifi_rssi
}
}
network_ok =
[eth0, wlan0]
|> Enum.filter(& &1)
|> Enum.any?(fn s -> s.connection == :internet end)
status = if network_ok, do: 200, else: 503
conn
|> put_status(status)
|> json(%{status: if(status == 200, do: "ok", else: "degraded"), checks: checks})
end
defp format_interface(nil), do: %{status: "unknown"}
defp format_interface(%{connection: conn, address: addr}) do
%{connection: inspect(conn), address: addr}
end
end
Step 5: Heartbeat Monitor for Device Liveness
The most critical monitoring for embedded devices: a heartbeat that proves the device is alive and network-connected. If the device goes silent, Vigilmon alerts before operators notice.
# lib/my_app/vigilmon_heartbeat.ex
defmodule MyApp.VigilmonHeartbeat do
use GenServer
require Logger
@heartbeat_url System.get_env("VIGILMON_HEARTBEAT_URL")
@interval :timer.minutes(5)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:ping, state) do
case ping_vigilmon() do
{:ok, _} ->
Logger.debug("Vigilmon heartbeat sent")
{:error, reason} ->
Logger.warning("Vigilmon heartbeat failed", reason: inspect(reason))
end
schedule()
{:noreply, state}
end
defp ping_vigilmon do
if @heartbeat_url do
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 10_000}], [])
else
{:error, :no_heartbeat_url}
end
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
Add to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.NetworkMonitor,
MyApp.FailoverTracker,
MyApp.VigilmonHeartbeat,
MyAppWeb.Endpoint
]
Set the environment variable in your Nerves firmware configuration:
# config/target.exs
config :my_app,
vigilmon_heartbeat_url: System.get_env("VIGILMON_HEARTBEAT_URL")
Step 6: Set Up Monitors in Vigilmon
Heartbeat monitor (primary):
- Sign in at vigilmon.online
- Click New Monitor → Heartbeat
- Set expected interval to 8 minutes (5-minute ping interval + 3-minute grace)
- Copy the ping URL and set it as
VIGILMON_HEARTBEAT_URLin your firmware environment - Enable PagerDuty or SMS alerting — a missed heartbeat from an embedded device is an emergency
HTTP monitor (if your device has an accessible endpoint):
- Click New Monitor → HTTP
- URL:
http://device-hostname.local/healthor external IP - Interval: 60 seconds
- Alert on any non-200 response
Step 7: Alerting
Configure notifications in Vigilmon → Notifications → New Channel:
Slack alert for device offline:
🔴 DOWN: device-serial-0042 heartbeat
Last seen: 8 minutes ago
Action: Check device power, network connection, and LTE signal
PagerDuty for production devices:
Page on-call immediately when the heartbeat monitor misses two consecutive expected pings. For industrial, medical, or commercial embedded deployments, a silent device can mean data loss, physical system failure, or safety risk.
WiFi signal degradation alert:
# In your anomaly checker, emit an event when RSSI drops below threshold
defmodule MyApp.WifiQualityChecker do
use GenServer
@weak_signal_dbm -75
@critical_signal_dbm -85
@check_interval :timer.minutes(1)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:check, state) do
rssi = VintageNet.get(["interface", "wlan0", "wifi", "rssi"])
cond do
is_nil(rssi) ->
:ok
rssi < @critical_signal_dbm ->
:telemetry.execute(
[:my_app, :network, :wifi_quality_alert],
%{rssi: rssi},
%{level: :critical}
)
rssi < @weak_signal_dbm ->
:telemetry.execute(
[:my_app, :network, :wifi_quality_alert],
%{rssi: rssi},
%{level: :warning}
)
true ->
:ok
end
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :check, @check_interval)
end
What You've Built
| What | How |
|------|-----|
| Interface state monitoring | VintageNet property subscriptions on connection and address |
| WiFi RSSI tracking | Property subscription on wifi.rssi with telemetry |
| Failover event detection | Subscription on global connection property |
| Health endpoint | JSON response reporting per-interface state |
| Device liveness heartbeat | GenServer pinging Vigilmon every 5 minutes |
| Slack + PagerDuty alerts | Vigilmon heartbeat and HTTP monitors with notification channels |
| WiFi quality alerting | Threshold-based RSSI checker emitting telemetry events |
VintageNet manages your device's connectivity. Vigilmon ensures that connectivity is actually working — alerting you when a device goes silent, a failover storm starts, or WiFi signal degrades below safe operating levels, before the device misses a critical update or stops delivering data.
Next Steps
- Add LTE signal metrics (RSRP/RSRQ) via
VintageNet.get(["interface", "ppp0", "mobile", "signal"])for cellular-connected devices - Track IP address lease times and alert when DHCP renewal starts taking longer than expected
- Use Vigilmon's response time history to detect when round-trip latency to your device increases, predicting impending network degradation
- For fleets, create one Vigilmon heartbeat monitor per device and group them in a status page for your operations team
Get started free at vigilmon.online.