The dashboard
The overview, paths, requests and alerts pages, the failures-right-now panel, finding one call, and where the dashboard lives and who can open it.
The dashboard is served by your application at /requizon, with its own views, stylesheet and script, so
it does not depend on your front-end build. It has three levels, each one a drill-down from a row of the one above,
and an alerts page beside them.
The pages#
| Page | Lists | Reads |
|---|---|---|
/requizon |
Every API, one row per host, with totals, success rate, average duration, failure count and failures right now | The hourly rollup, and the detail rows since its last run |
/requizon/{api}/paths |
The API's paths, failing-now first, then most failures, under the calls that failed every time. Opened from a host row, only that host's paths | The hourly rollup, and the detail rows since its last run |
/requizon/{api}/requests |
Individual calls in the selected window, newest first, each under the path it was called with, and a Details panel on every row that stored something behind it | The detail table, 50 rows a page |
/requizon/alerts |
Open alerts, the critical ones first, and the resolved ones behind a filter. See Alerts | The alerts table |
The overview and paths page read the rollup as far as
requizon:aggregate last got, and count
everything after that straight from the detail rows. With the scheduler running every five minutes that is a few
minutes of rows, so every page is current to the last call, the hour under way included. A rollup that has fallen
behind costs a slower page, not missing numbers.
The header carries an Alerts link with the number of alerts open, and open alerts are listed above the overview and each API's paths page.
Failures right now#
Every page opens with what is failing at this moment: the failures of the last 15 minutes, by type, with when the last one happened and a link to them in the requests list. The overview and paths tables carry the same count on each row and sort anything failing now to the top, so an API that broke five minutes ago is the first row, not lost in a 72-hour average.
REQUIZON_RECENT_MINUTES=5 # what "right now" means, 15 by default
The charts#
Every page draws the same two charts over the selected window:
- Response time: the average across everything on the page, plus a line for each of the three slowest members. APIs on the overview, paths on the paths page, and on the requests list whatever its filters leave.
- Responses: calls per bucket, stacked by status code or failure type, so the height of each column is the request volume. The seven most frequent outcomes get their own colour and the rest are folded into Other.
Each chart has a Data disclosure underneath with the numbers behind it as a table. Windows longer than four days are drawn in wider buckets (two hours for a week, four for a fortnight), so a fortnight stays readable.
Filtering the requests list#
The requests list filters by host, path, parameter, fingerprint, outcome, status code and a date range. Host matches
exactly. Path takes the path as it is counted, /orders/:id, for every call under it, or the path a call
was made to, as below.
The list covers the selected window, like the charts above it, so both always count the same period. It pages with Previous and Next rather than numbered pages. The detail table is the largest one Requizon keeps, and both a total to count up to and a jump into the middle of it would cost a scan of the window on every page load. Moving one page at a time costs the same however deep you go.
-
An outcome or status filter narrows the responses chart to the matching calls, so filtering to
503charts only 503s per hour. Response time is not recorded per outcome, so that chart is hidden while one is applied. - A date range replaces the window, widened to whole hours at both ends, because the charts are drawn from hourly buckets.
Finding one call#
A failure in your logs comes with a URL, and that is enough to find its calls. Paste it into the search box on the overview, with its scheme or without: its host picks the API, and the requests list opens on the calls made to exactly that path. With a query string, only the calls sent with that query are listed. The Path filter on the requests list takes the same URL, and fills the Host filter from it.
It finds a call even where the stored path was masked, such as /files/report.pdf stored as
/files/***, because every call also keeps a keyed hash of its path and query as they were sent. How
the path a call was made to is stored is on
Paths and table size. A search for one path lists
only its calls. The charts count whole paths, so they are not drawn for it, and a link widens the list to the path it
is counted under.
To find calls by what they sent, filter by a parameter as key=value. It matches a query or body
parameter exactly:
| Filter | Finds |
|---|---|
order_id=123 | A top-level parameter |
customer.id=7 | A nested one |
items.0.sku=ABC | The first item of a list, also written items[0][sku]=ABC |
paid=true | A JSON boolean, and null the same way |
Values compare as text, so 7 finds the query string's "7" and a JSON body's 7
alike. A redacted value is stored as its mask and cannot be found, nor can anything in a body kept only by shape.
No index covers this filter, so a rare value is looked for across the whole window: narrow the window or the dates
when it is slow.
The fingerprint filter lists every call made with the same method, host, path and parameters. You rarely type one: the Details panel links to it.
Opening a row#
A row in the requests list carries a Details button whenever anything was stored behind it. The details are fetched the first time you open them rather than sent with the page, so a page of failures with large bodies loads as fast as its table. The panel holds these blocks, in this order, and prints each one only when the row has it:
| Block | What it shows |
|---|---|
| URL | The method, host and path the call was made to, with the query string as stored, redacted |
| Failure | The message the call was classified with |
| All requests with these parameters | A link to every call with the same fingerprint: a failure and its retries side by side, so you can see whether a retry went through |
| Request headers | The headers we sent, as far as header capture kept them |
| Query | The query string, redacted, whatever the method was |
| Body | The parsed request body, redacted |
| Response headers | The headers that came back, as far as header capture kept them |
| Response body | The body of a call recorded as a failure, or of a success that success_bodies keeps |
Query and body come from separate columns and are filled for every method, so a POST that also carries a
query string shows both, and a name that appears in each is shown in each. Both blocks are
redacted by parameter name before anything is stored.
Headers are printed the way they went over the wire, one line per value, so a response that set four cookies shows
four Set-Cookie lines instead of one joined string. Which of them reach the panel is decided by
header capture, which out of the box keeps the response
headers of failed calls and no request headers at all.
Windows#
// config/requizon.php
'dashboard' => [
'windows' => [24, 72, 168, 336], // hours offered by the selector
'default_window' => 72,
'per_page' => 50, // rows per page of the requests list
'recent_minutes' => 15, // what counts as "right now"
'max_paths' => 500, // rows on an API's paths page
],
A default_window that is not in windows falls back to the first entry, so the selector never
claims one window while showing another. Offering a window longer than retention.detail_days works for
the charts, which read the rollup, but the requests list will not reach that far back.
max_paths caps the paths page at the paths with the most failures, then the most requests, so an API
whose paths the normaliser cannot bound still renders. It is a guard rather than a fix: an API that hits it needs
a rewrite or an allow-list, and
requizon:merge-paths to fold
what is already stored.
Access#
Every dashboard route runs through the middleware in requizon.middleware (['web'] by
default) and then Requizon's own check, which answers 403 to anyone it does not let in. The check your
published RequizonServiceProvider installs is:
- In the
localenvironment, everyone. - Anywhere else, whoever the
viewRequizongate allows, and nobody until you define it.
protected function gate(): void
{
Gate::define('viewRequizon', fn ($user) => in_array($user->email, [
'ops@example.com',
]));
}
The gate receives the authenticated user, so a guest is refused without the closure running. For a check that is not
about a user at all (an IP allow-list behind a VPN, say), replace the whole callback in the provider's
boot() method, after parent::boot():
use BoringO11y\Requizon\Requizon;
Requizon::auth(fn ($request) => $request->ip() === '10.0.0.5');
If the provider is not registered at all, no callback exists and the dashboard is reachable only in the
local environment.
The dashboard's stylesheet and script are served without the middleware or the gate, so its own error pages stay styled. They are two fixed files with no data in them.
Moving the dashboard#
REQUIZON_PATH=ops/outbound # served at /ops/outbound
REQUIZON_DOMAIN=ops.example.com # served only on that host
Add middleware to the list rather than replacing it, so the routes keep their session and cookies:
'middleware' => ['web', 'auth', 'verified'],
php artisan about prints the path the dashboard is currently served from.