Packages

Crash reporting for embedded Linux. Captures supervised crashes, redacts them on the device, and hands them to the Bellwether Agent.

Current section

Files

Jump to
bellwether README.md
Raw

README.md

# Bellwether — BEAM Adapter
Crash reporting for embedded Linux, from the runtime's side.
This captures supervised crashes, redacts them **on the device**, and hands
them to the Bellwether Agent — a small native daemon that owns the spool and
the uplink. The Adapter never talks to the network: it runs inside a process
that may be in the middle of dying, so its job is to shape, fingerprint and
hand over, then get out of the way.
def deps do
[{:bellwether, "~> 0.1"}]
end
## Wiring
One child. It starts the socket client and the Reporter and attaches the
`:logger` handler, so there is one place to configure rather than four to keep
in step.
children = [
{Bellwether.Supervisor,
socket: "/tmp/bellwether.sock",
own_apps: [:my_app],
backtrace_depth: 32}
]
No call sites are needed. The BEAM puts `:crash_reason` into Logger metadata
for every crash a supervisor reports, so the handler catches the domain
without your code mentioning it.
Options may equally live in application config, where a release can change
them without a code change:
config :bellwether,
socket: "/tmp/bellwether.sock",
own_apps: [:my_app],
backtrace_depth: 32,
keep_messages_for: [RuntimeError],
block: ["otp:MyApp.KnownNoisy*"]
## The two settings that decide how good a report is
**`own_apps`** separates your code from the framework in a Fingerprint. Get it
wrong and every crash groups on whichever OTP function happened to sit above
yours.
**`backtrace_depth`** raises the VM's limit, which is **eight frames**. Eight
is often short of the call that explains a fault, and it costs grouping too: a
crash inside a dependency can carry none of your frames within eight, so
unrelated faults collapse onto the framework frames they share. It is a global
VM flag, which is why this sets it only when asked.
## Redaction
Every payload that leaves is a **Shape**: code-derived parts intact,
data-derived parts replaced by their type.
%Lock{id: 4, site: "acme-1", state: :unlocked}
#=> "%Lock{id: int, site: binary(6), state: :unlocked}"
Atoms, struct names and `module.function/arity` are written in source and
survive. Binaries, integers, floats, pids and refs are data and become their
type. On a fleet where process state holds site credentials, this is what
makes a crash reporter deployable at all.
The exception to know about: a message-carrying exception reports almost
nothing by default, because a message is where people interpolate an address
or a serial.
%RuntimeError{message: binary(37)} # by default
%KeyError{key: :missing, term: %{site: binary(6)}} # structured, and readable
If you know your own messages are safe, name them with `keep_messages_for`.
Only your application can judge that, which is why the default is the one that
cannot leak.
## Reporting something by hand
Supervised crashes arrive on their own. For anything else:
# A fault you caught rather than let crash.
Bellwether.capture_exception(e, __STACKTRACE__)
# Something the runtime has no opinion about.
Bellwether.capture_event(:coproc, :alarm, %{radio: :zwave, fault: :no_response},
fingerprint: "radio:zwave:no_response")
Both Shape what you pass. Give anything recurring a `:fingerprint` — without
one the server has nothing to group on, which is right for a one-off and wrong
for everything else.
## The Agent
The Adapter is half of it. The Agent is a static binary that explains each
boot, harvests kernel panics and `erl_crash.dump`, holds the spool across
reboots, and uploads. On Nerves:
mix bellwether.agent --endpoint https://... --key bwk_...
which installs it into `rootfs_overlay/` with a matching config, so the socket
path is stated once rather than in two files that must agree.
**`aarch64` is bundled with this package**, which covers most Nerves boards.
A Hex package cannot carry every target and two megabytes of the wrong
architecture helps nobody, so other targets are pointed at with `--from` or
built locally — `mix help bellwether.agent` lists where it looks.
Nothing is downloaded. A binary that arrived from the network with no
provenance is not something to put in firmware.
## What is in here
| Module | |
| --- | --- |
| `Bellwether` | `capture_exception/3`, `capture_event/4` |
| `Bellwether.Supervisor` | everything above, as one child |
| `Bellwether.Shape` | redaction — code survives, data becomes its type |
| `Bellwether.Fingerprint` | grouping, without line numbers or byte sizes |
| `Bellwether.Crash` | a crash reason and stacktrace to an Event payload |
| `Bellwether.Dedupe` | one exemplar per window, plus a count |
| `Bellwether.Filter` | faults an operator asked never to be told about |
Apache-2.0.
## Publishing
`priv/bin/` is committed, so refresh it before a release or the package ships
whichever Agent was built last:
mix bellwether.bundle
mix hex.publish
`mix test` fails if the binaries are missing or are not static aarch64. It
cannot tell a one-commit-stale binary from a current one, which is why the
first command is not optional.