Packages
bellwether
0.1.0
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
Current section
Files
bellwether
README.md
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.