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:
mekhat.enabledistrue.app.debugistrue.Application.environmentis notproduction.
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:
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
maat mekhat:installPublishes 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 |
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
maat debugbarmaat debugbar --url=http://localhost:9000maat debugbar --filter=/apimaat debugbar --filter='/users/\d+' # slash-delimited: a regexmaat debugbar --slow=200maat debugbar --verbosePolls 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.
maat debugbar:clearEmpties 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.valueis stringified at the call site.Bar.measure<T>(label, Future<T> Function() body)— times an async operation and appends it to the Timeline panel, withfailed: trueset and the exception rethrown ifbodythrows.Bar.measureSync<T>(label, T Function() body)— the synchronous version ofmeasure.
Security
-
Every route except the two static asset routes (
/_debugbar/assets/{hash}/bar.cssand.js) is behind a loopback guard: refused with403unless the client’s address is127.0.0.1,::1,localhost, or listed inallowed_ips. Anulladdress (nothing shelf could determine) is refused, not treated as trusted. -
The loopback guard only covers the
/_debugbarendpoints. The bar injected into your own HTML responses, and theX-Debugbar-Idheader on every response, go to any client that loads the page — gated solely by the three enablement conditions, not byallowed_ips. On a staging box withAPP_ENV=staging,app.debug, andmekhat.enabledall set, every visitor gets the full payload rendered into the page they load. -
Request.ipfalls back to127.0.0.1when shelf’s connection info is absent.Application.serveruns onshelf_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/_debugbarwould be readable by anyone. This is amaat-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.defaultKeysmaskspassword,token,secret,api_keyand the like, but notkey— andconfig/app.dartstores 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:- A key the redactor masks renders
********whatever its value’s type is — sopasswordstill 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. bool,numandnullcan never be a credential and are always shown as-is —debugstaystrue,portstays anint.- A
Stringis 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>. - A
Maprecurses. AListrecurses 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. - Everything else — a domain object, a
Duration, anything with atoJson()— is described as<TypeName>by itsruntimeType, never by callingtoString()ortoJson(). An arbitrary object’stoStringcan embed anything, andtoJson()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.
urlandhostare deliberately not on the allow-list, which is whyapp.urlshows 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. Losingapp.urlfrom the panel is the deliberate trade. Do not add either name without a design pass. - A key the redactor masks renders
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.jsandbar.cssare generated once, ahead of time, intoMekhatAssets, and served as-is; there is no bundler, no watch mode, and nothing to compile in an application that installs Mekhat.