March Docs

Diagnostics

Diagnostics: observe crashes the runtime owns.

A panic in an actor or a task is a runtime failure, not something application code can catch on its way out: the runtime owns the failure, the supervisor's restart and the process exit. observe_crashes registers a callback the runtime calls with a CrashReport for every panic nobody handles, at the point of failure, before the supervisor decides anything. with_context attaches request or job metadata that survives into that report. The module is vendor-neutral: building an event for an error service, queueing it and sending it belong in a package built on this one.

What is reported: a panic that no __try_call (Result.try_call, the property runner) or test handles. A panic inside try_finally (and so inside with_context or Logger.with_scope) is reported once, where it was raised, with that scope's context still attached. A panic inside an observer is never reported. Runtime errors that exit without a panic (an out-of-bounds array index, an allocation failure) are not reported.

Observers run inline, on the failing actor's or task's own thread, in registration order. An observer cannot change what happens next (the restart or the exit goes ahead exactly as without it) and a panic inside one is swallowed, but it must not block: no network I/O, no waiting on a reply. Hand the report to an actor or a queue and return.

Location. location is the panic, todo or unreachable call that raised the panic, compiled and interpreted. A panic raised inside a prelude helper (unwrap, ...), by an assert, a non-exhaustive match or a runtime error (division by zero) has none.

Backtraces. Interpreted, each frame is the line it was executing. Compiled, the frames are the native stack, named from a table of the program's own functions, on every platform: each frame is the source function with its file and DEFINITION line (lambdas as <host>.{fn}). Functions LLVM inlined into their caller do not appear, and an actor handler inlined into its actor's dispatch can leave no frame at all. CrashKind.TaskFailed is reserved: a compiled panic in a task exits the process and is reported as Panic.

Types

typeCrashKindCrashKind = Panic | ActorCrashed | TaskFailed#
typeDiagnosticValueDiagnosticValue =#

A JSON-safe context value.

typeSourceLocationSourceLocation = { file : String, line : Int }#
typeStackFrameStackFrame = { name : String, file : String, line : Int }#

One frame, innermost first: interpreted, the line it was executing; compiled, its function's definition line.

typeProcessInfoProcessInfo = { pid : Int, supervised : Bool }#

pid is the failing actor's pid, or -1 outside an actor; supervised is true when a supervisor will decide what happens next.

typeCrashReportCrashReport = {#
typeObserverHandleObserverHandle = ObserverHandle(Int)#
typeDiagFrameDiagFrame = DiagFrame(String, String, Int)#

What the runtime passes an observer's wrapper (march_runtime.c's diag_crash_cell / the interpreter's diag_dispatch): single-constructor types, tag 0, Ints stored raw, so the C runtime can build them. kind is 0 Panic, 1 ActorCrashed, 2 TaskFailed; the last two fields are the panic call's recorded site ("" and 0 when there is none). The names are unique on purpose: a colliding constructor name gets global tags.

typeDiagCrashDiagCrash = DiagCrash(Int, String, List(DiagFrame), Int, Int, List((String, DiagnosticValue)), String, Int)#

Functions

fncurrent_contextcurrent_context() : List((String, DiagnosticValue))#

The context a crash on this actor or task would report right now, innermost scope first. Empty outside every with_context.

Examples

march> List.length(Diagnostics.current_context())
0
march> Diagnostics.with_context([("job", Diagnostics.DInt(7))], fn -> List.length(Diagnostics.current_context()))
1
fnkind_to_stringkind_to_string(k : CrashKind) : String#

The name of a crash kind: "panic", "actor_crashed" or "task_failed".

Examples

march> Diagnostics.kind_to_string(Diagnostics.ActorCrashed)
"actor_crashed"
march> Diagnostics.kind_to_string(Diagnostics.Panic)
"panic"
fnobserve_crashesobserve_crashes(observer : CrashReport -> Unit) : ObserverHandle#

Call observer with a CrashReport for every unhandled panic from now on, until unobserve. Observers run in registration order, inline on the failing thread: hand the report off and return, never block.

let h = Diagnostics.observe_crashes(fn r -> send(reporter, Report(r)))
-- r.kind, r.message, r.location, r.backtrace, r.process, r.context

The handle is opaque; keep it to pass to unobserve.

fnunobserveunobserve(handle : ObserverHandle) : Unit#

Stop calling the observer handle registered. Removing one twice, or during a dispatch, is harmless; a dispatch already under way still calls it.

Examples

march> Diagnostics.unobserve(Diagnostics.observe_crashes(fn _ -> ()))
()
fnwith_contextwith_context(values : List((String, DiagnosticValue)), thunk : () -> a) : a#

Run thunk with values added to this actor's or task's crash context, and restore the previous context when it returns or panics. Inner scopes come first in a report's context, so they win for a repeated key.

Diagnostics.with_context([("request_id", Diagnostics.DStr(id))],
                         fn -> handle(req))

Examples

Returns the thunk's value; nested scopes list the inner keys first:

march> Diagnostics.with_context([("job", Diagnostics.DInt(7))], fn -> 40 + 2)
42
march> Diagnostics.with_context([("a", Diagnostics.DStr("x"))], fn -> Diagnostics.with_context([("b", Diagnostics.DBool(true))], fn -> List.map(Diagnostics.current_context(), fn p -> match p do (k, _) -> k end)))
["b", "a"]