Tier 0: crash and supervisor reports, enriched with what OTP already captured.
This is the case for being inside the VM. A crash event carrying the dying process's state, its last message, its stacktrace, its ancestors, and the request context it was holding is not assemblable from outside the BEAM — no scrape interval and no log line reconstructs it.
watch :process_crash do
source crash_report: :any
enrich [:stacktrace, :process_state, :last_message, :request_context]
severity :error
fire immediately, cooldown: :timer.minutes(1)
end
watch :child_terminated do
source supervisor_report: :child_terminated
enrich [:supervisor, :child_id, :reason, :pid]
fire immediately
endWhat you can actually get
The process is already dead
By the time a report arrives there is nothing left to introspect —
Process.info/2 on the pid returns nil. Enrichment reads what OTP
captured at crash time, so what is available depends on what crashed.
| Crashed thing | What the report carries |
|---|---|
A GenServer, gen_statem, or gen_event | Reason, stacktrace, state, last message, plus everything below |
Any proc_lib process (Task, Agent, supervised children) | Reason, stacktrace, initial call, ancestors, message queue, dictionary |
A bare spawn | Nothing — bare processes produce no report at all |
:request_context
The one field Kepler cannot supply for you. A log event carries the metadata
of the process that logged it, so :request_context is whatever your
application put in Logger.metadata/1 on the process that died, with
:logger's own bookkeeping keys removed.
That means user and request attribution works only if you propagate context
into spawned processes. Kepler can read Logger.metadata; it cannot make
your application set it. If your Tasks do not inherit request context
today, this field will be empty and no amount of library will fix it —
that is an application discipline problem, and worth finding out about before
you build on it.
SASL reports are off by default
Elixir disables SASL reports, which filters proc_lib crash reports and all
supervisor reports out before any handler sees them. What still works:
| Default config | With handle_sasl_reports: true | |
|---|---|---|
crash_report: :any | OTP behaviours only — but these are the rich ones | Every proc_lib process, including plain Tasks |
supervisor_report: | Never fires | Every child termination |
Kepler warns at boot when you declare a watch this affects. To see everything:
config :logger, handle_sasl_reports: trueDeduplication
One GenServer crash under a supervisor produces three reports: a
gen_server terminate report, a proc_lib crash report, and a supervisor
child_terminated report. A crash_report: :any watch would otherwise fire
twice for one crash.
Kepler fires on the first report for a given pid and suppresses the rest
for a short window. The first is also the richest — the behaviour's own
report is the one carrying state and last message. supervisor_report:
matches exactly one report kind and is never deduplicated, so a watch on
supervisor churn still sees every restart.
Cost
A :logger filter that pattern-matches the report label, and nothing else
until something crashes. If nothing in your system is dying, this costs a
failed match per log event.
Summary
Functions
Installs the :logger handler for the given routes.
Whether the VM is delivering SASL reports to handlers.
Removes the handler. Safe to call when it was never installed.
Types
@type route() :: %{name: atom(), enrich: [atom()], ring: Kepler.RingBuffer.t() | nil}
One watch's interest in a report kind.
Functions
@spec install(%{required(atom()) => [route()]}) :: {:ok, :ets.table()} | :ignore
Installs the :logger handler for the given routes.
routes maps each report label to the watches interested in it. Returns
:ignore when no watch asked for reports, so applications that do not use
this source never add a handler.
@spec sasl_reports?() :: boolean()
Whether the VM is delivering SASL reports to handlers.
Elixir disables them by default, which filters out proc_lib crash reports
and every supervisor report before any handler sees them.
@spec uninstall() :: :ok
Removes the handler. Safe to call when it was never installed.