Skip to content

Mekhat Request Bar

A request touches routes, queries, views, and models, and by the time the response reaches the browser all of that is gone — you would have to open another tool, filter by time, and hope you can match the row back to the page you are looking at. Mekhat renders it into the page itself: the bar sits at the bottom of the HTML response that caused it, already scoped to that one request, with no login and no second tab.

Mekhat vs. Udjat’s panel

They are not competitors; they read the same collector. Udjat watches every request, exception, log line, query, and more, and writes it into udjat_entries, where a Vizier panel lists it — durable, searchable, cross-request history. Mekhat renders one request’s slice of that same collection directly into the page (or a standalone console page, or a terminal tail) with no storage of its own: an in-memory ring buffer that a process restart empties.

Run both together. Mekhat’s provider turns Udjat’s collection on for you — see Enabling Mekhat — and every response Mekhat renders carries the same X-Debugbar-Id value as Udjat’s own batch_id, so an id copied off a curl response is exactly what to search the Udjat panel for when you want the durable copy of the same request.

Enabling Mekhat

Three conditions, all required:

  1. mekhat.enabled is true.
  2. app.debug is true.
  3. Application.environment is not production.

The third cannot be overridden by any config key, from Mekhat or anywhere else. Application.environment defaults to production when APP_ENV is unset, so an unconfigured deployment gets the same refusal a deliberately production one does. When mekhat.enabled is true but one of the other two conditions fails, MekhatServiceProvider logs a warning naming which condition failed — the alternative, a bar that is simply absent with no explanation, is the kind of thing people debug for an hour before finding this page.

Mekhat also needs udjat.enabled: true. Udjat’s request watcher is what opens the batch Mekhat renders; without it no batch ever opens and the bar renders, permanently empty, with no error anywhere. If mekhat.enabled is on and udjat.enabled is off, the provider warns about this by name too. The recommended pairing keeps Udjat’s collection on with no rows written to disk:

config/udjat.dart
final udjat = {
'enabled': envBool('UDJAT_ENABLED'),
'store': false, // Mekhat still gets the batch; nothing is written to disk.
};

Mekhat’s own provider sets UdjatRecorder.enabled = true at boot whenever Mekhat itself is permitted, whatever udjat.enabled says — but that alone does not make a batch open. UdjatServiceProvider only prepends UdjatRequestWatcher (the thing that actually opens a batch) when its own udjat.enabled config is true; setting recorder.enabled after the fact cannot retroactively register a watcher that was never added. udjat.enabled still has to be true in your own config for a batch to ever open.

Installing

Terminal window
maat mekhat:install

Publishes config/mekhat.dart. An existing file is left alone unless you pass --force.

Configuration

Key Default What it does
enabled false — MekhatConfig’s own fallback, used whenever config/mekhat.dart hasn’t been published or the key isn’t literally true. The published stub instead sets it to envBool('MEKHAT_ENABLED'), which is itself false until that variable is set. Off unless you say otherwise. Publishing the file does not turn the bar on by itself — see the callout below.
buffer_size 50 How many request payloads the in-memory ring buffer keeps, for the console page and maat debugbar. Never written to disk.
allowed_ips [] Addresses other than loopback that may read /_debugbar. Empty means loopback only.
redact_headers [] Header names to mask. An empty list (the default) keeps Udjat’s own default header list; a non-empty list replaces it entirely, rather than adding to it.
redact_keys [] Body/payload keys to mask. Same rule: empty keeps Udjat’s defaults, non-empty replaces them.
queues [] Extra queue names the Queue panel reports alongside the connection’s own default queue. Khepri cannot enumerate queues on its own, so a named queue you dispatch to has to be listed here to show up.

Publishing the config never turns the bar on

The published enabled value is envBool('MEKHAT_ENABLED'), whose own default is false. Someone runs mekhat:install, deploys, and never opens the file again — the file itself must not be the thing that switches a request bar on in that story.

What each panel shows

The bar’s payload has four panels built directly from the request or the application, and one panel per type of entry Udjat’s batch recorded for it:

Panel Source Shows
Request The response in hand, not an udjat_entries row Method, path, status, duration, the query string, redacted headers, the client IP, and the error when the handler threw
Route request.route The matched URI pattern, route name, methods, and middleware list — or matched: false with the raw path when nothing matched
Auth request.attributes[authUserAttribute] Whether a user is authenticated, its runtime type, and its identifier — never the model itself
Config The application’s resolved config, collected once at boot Every config key, flattened to dotted paths — see the reveal rule below for what’s shown at each one
Query Udjat’s query entries Each query’s SQL and timing
Log Udjat’s log entries Each log line recorded during the request
Event Udjat’s event entries Each dispatched event, flattened to text
View Udjat’s view entries Each template rendered, its name, timing, and redacted data
Model Udjat’s model entries Rows hydrated per model class — see the caveat below
Exception Udjat’s exception entries Each reported exception’s type and message
Mail Udjat’s mail entries Each mail actually sent
Notification Udjat’s notification entries Each notification sent, failed, or skipped
Message Bar.info calls Whatever your own code passed, with its optional label
Timeline Bar.measure/Bar.measureSync calls Named spans with their duration and whether the body threw
Queue GET /_debugbar/queue, fetched on demand Each queue’s job count — reserved jobs included, see below — and the total failed-job count

The Queue panel is fetched, not collected. Every other panel above is assembled per request, inside the same ~2 microseconds the rest of the bar costs. Queue depth is not something a request did — it is ambient state that changes on its own — so nothing queue-related rides in the per-request payload at all. bar.js only calls /_debugbar/queue when you click the tab, shows a loading state while it does, and has a Refresh button for checking again without leaving the tab. The tab itself only appears when the application has a queue connection bound; an application with no queue gets no tab, not an empty one.

The endpoint reads Khepri’s public interface only — QueueConnection.size() per queue and FailedJobStore.all() — which is also its honest limit: Khepri has no public way to split a queue’s pending jobs from its reserved ones, so the count shown is every job on that queue, reserved included. Reserved, delayed, oldest-wait and throughput require querying Khepri’s jobs table directly, which is exactly what Akhet’s dashboard is built to do — see it there instead of here. List extra queue names in mekhat.queues to have them show up alongside the connection’s own default queue.

The Model panel’s count means “rows that came from a SELECT.” The watcher is attached to ModelDefinition.onHydrate, which fires when a row is turned into a model instance — not instantiate, which also runs for a model your own code just built in memory. A create or an update is not counted; only a load is. Read the number as “rows read,” not “rows touched.”

maat debugbar

Terminal window
maat debugbar
maat debugbar --url=http://localhost:9000
maat debugbar --filter=/api
maat debugbar --filter='/users/\d+' # slash-delimited: a regex
maat debugbar --slow=200
maat debugbar --verbose

Polls a running server’s /_debugbar/requests?after= endpoint twice a second and prints one line per request as it lands — this is a separate process from the server, reading over HTTP, not the server’s own memory.

maat debugbar and /_debugbar both assume a single-isolate server. With APP_WORKERS > 1 each isolate keeps its own ring buffer, so a round-robin poll thrashes between them, missing entries other isolates recorded and resending ones this one already showed, indefinitely.

Flag Effect
--url= Base URL to poll. Defaults to app.url, then http://localhost:8080.
--filter= Only show requests whose path contains this substring, or, when wrapped in slashes (/pattern/), matches this regular expression.
--slow= Only show requests at or above this many milliseconds, and auto-expand their detail block. Must be a non-negative integer.
--verbose Expand every request’s detail block (queries, the exception, matched views), not only slow or errored ones.

A request that threw, or one over --slow, is expanded automatically even without --verbose.

Terminal window
maat debugbar:clear

Empties the buffer of the process running the command — a running server’s buffer keeps its own copy in its own isolate, so this only affects a server in the same process (a test, a script), not a separately-running one. To clear a live server’s buffer, restart it or wait for the ring buffer to evict old entries on its own.

The Bar API

Call this from your own application code; it costs one null check when the bar is off or not installed, so it is safe to leave in committed code.

Bar.info(order.total, label: 'checkout total');
final placeId = await Bar.measure('geocode', () => geocoder.lookup(address));
final parsed = Bar.measureSync('parse csv', () => parseCsv(raw));
  • Bar.info(value, {label}) — appends a message to the Message panel. value is stringified at the call site.
  • Bar.measure<T>(label, Future<T> Function() body) — times an async operation and appends it to the Timeline panel, with failed: true set and the exception rethrown if body throws.
  • Bar.measureSync<T>(label, T Function() body) — the synchronous version of measure.

Security

  • Every route except the two static asset routes (/_debugbar/assets/{hash}/bar.css and .js) is behind a loopback guard: refused with 403 unless the client’s address is 127.0.0.1, ::1, localhost, or listed in allowed_ips. A null address (nothing shelf could determine) is refused, not treated as trusted.

  • The loopback guard only covers the /_debugbar endpoints. The bar injected into your own HTML responses, and the X-Debugbar-Id header on every response, go to any client that loads the page — gated solely by the three enablement conditions, not by allowed_ips. On a staging box with APP_ENV=staging, app.debug, and mekhat.enabled all set, every visitor gets the full payload rendered into the page they load.

  • Request.ip falls back to 127.0.0.1 when shelf’s connection info is absent. Application.serve runs on shelf_io.serve, which always populates it, so this only matters if your application is served through a different adapter. On such an adapter every client would look like loopback to the guard above, and /_debugbar would be readable by anyone. This is a maat-level gap outside Mekhat’s own code; Mekhat does not attempt to work around it.

  • Production is refused unconditionally — see Enabling Mekhat — regardless of mekhat.enabled.

  • Redaction is Udjat’s: query bindings are positional and cannot be masked by key; see Udjat’s redaction section for the full list of what cannot be redacted and why.

  • The Config panel does not use a deny-list of secret key names. Redactor.defaultKeys masks password, token, secret, api_key and the like, but not key — and config/app.dart stores the application’s signing key under exactly that name. A deny-list also has the wrong owner: a package added later that introduces a new secret-bearing config key would leak it by default, from a mask list that lives in a different package entirely. Instead the reveal rule is structural, applied in this order:

    1. A key the redactor masks renders ******** whatever its value’s type is — so password still reads the way it does in every other panel, rather than as a length. The mask list is not the defence here, but it is not discarded either.
    2. bool, num and null can never be a credential and are always shown as-is — debug stays true, port stays an int.
    3. A String is shown only for a small allow-list of mode-ish keys (driver, default, env, name, locale, timezone, level), and otherwise described as <String, N chars>.
    4. A Map recurses. A List recurses into its items — but a list item has no key of its own, so a string inside a list is always described and never revealed, whatever the list’s own key is called.
    5. Everything else — a domain object, a Duration, anything with a toJson() — is described as <TypeName> by its runtimeType, never by calling toString() or toJson(). An arbitrary object’s toString can embed anything, and toJson() expansion past redaction is a real bypass this codebase has already been bitten by.

    A package that adds a new secret-bearing config key therefore cannot leak it by naming it something new; it would have to both land on one of the seven allow-listed names and hold a credential, which is exactly backwards from how secrets get named.

    url and host are deliberately not on the allow-list, which is why app.url shows as a length rather than a value. A database or queue URL carries credentials in the same shape (postgres://user:pass@host) and lives under the same key name across every connection block; there is no way to reveal the useful one without also revealing the dangerous one. Losing app.url from the panel is the deliberate trade. Do not add either name without a design pass.

What it does not do

  • No cache collector — Mekhat has no watcher for a cache get/put/miss.
  • No gate or policy collector — authorization checks are not recorded.
  • No persistence across restarts — the buffer is in-memory only; use Udjat (with store: true) for anything that needs to survive a restart.
  • No open-in-editor links from a stack frame to your source file.
  • No JS build pipeline — bar.js and bar.css are generated once, ahead of time, into MekhatAssets, and served as-is; there is no bundler, no watch mode, and nothing to compile in an application that installs Mekhat.