Bier.ServerTiming (bier v0.1.0)

Copy Markdown View Source

Per-request accumulator for the real per-phase durations reported in the Server-Timing response header (Bier.Plugs.Observability).

PostgREST measures each request phase — JWT verification, query-string parse, SQL planning, the database transaction, and response rendering — and reports them as named Server-Timing metrics. Bier mirrors that by timing each phase where the work actually happens (measure/2 wraps the real call site) and accumulating the durations here, keyed to the current request.

Why the process dictionary

A request is handled start to finish — router pipeline, controller, query executor, and the Plug.Conn.register_before_send/2 callback that writes the header — in a single Bandit process, and the phases are spread across modules at different call depths. Threading a Plug.Conn accumulator through every one would be invasive and, worse, lossy: error paths discard the inner conn (e.g. Bier.Plugs.ActionController hands the original conn to the fallback controller), so a JWT or parse timing measured on a request that then fails would never reach the header. Process-scoped state survives those hand-offs and is naturally request-scoped, since each request runs in its own process.

reset/1 is called once at the top of the pipeline so a connection reused across keep-alive requests never carries stale phases. Timing is collected only when server-timing-enabled is set for the instance; otherwise measure/2 runs its function with no instrumentation and no process-dictionary writes.

Summary

Functions

The raw accumulator state, for handing off to another process.

Time fun, accumulate its elapsed wall-clock under phase, and return its result unchanged.

Initialise (or clear) the accumulator for the current request.

Adopt an accumulator state export/0-ed from another process. A nil state (the source was outside an initialised request) is a no-op, keeping measure/2 uninstrumented.

The phases accumulated so far for the current request, as a map of phase => milliseconds (float). Empty when timing is disabled or nothing was recorded — Bier.Plugs.Observability reports a phase absent from this map as 0.0 (truthful: no time was spent there) rather than a fabricated value.

Functions

export()

@spec export() :: term()

The raw accumulator state, for handing off to another process.

Bier.Cancellation runs the request's database work in a task; the task adopts the request's state via restore/1, and the phases it measures are handed back the same way. Opaque — only meaningful to restore/1.

measure(phase, fun)

@spec measure(atom(), (-> result)) :: result when result: var

Time fun, accumulate its elapsed wall-clock under phase, and return its result unchanged.

A no-op wrapper (just fun.(), no timing) when server-timing is disabled for this request, or when called outside an initialised request. Repeated measures of the same phase accumulate, so a phase that issues several round-trips (e.g. a read plus an exact-count query) reports their sum.

reset(bool)

@spec reset(boolean()) :: :ok

Initialise (or clear) the accumulator for the current request.

enabled? mirrors the instance's server-timing-enabled: when false the accumulator is put into a disabled state so measure/2 skips instrumentation entirely. Called once per request by Bier.Plugs.Observability.

restore(state)

@spec restore(term()) :: :ok

Adopt an accumulator state export/0-ed from another process. A nil state (the source was outside an initialised request) is a no-op, keeping measure/2 uninstrumented.

snapshot()

@spec snapshot() :: %{optional(atom()) => float()}

The phases accumulated so far for the current request, as a map of phase => milliseconds (float). Empty when timing is disabled or nothing was recorded — Bier.Plugs.Observability reports a phase absent from this map as 0.0 (truthful: no time was spent there) rather than a fabricated value.