# 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.
