Pulse
daravel_pulse serves a dashboard, from your application, of what it is
doing: how many requests it answers and how fast, which routes are slow,
which queries are slow, which exceptions it throws, how long the queues are
and which jobs fail. It follows Laravel Pulse, without a database of its
own or an external service: every worker isolate records in memory, and the
page merges them.
dartisan pub:add pulse
pub:add adds the dependency, the .withPulse() call below (without
middleware: add it before the page leaves local) and the PULSE_* settings
to .env.example. With dart pub add daravel_pulse you register it
yourself:
// bootstrap/app.dart
import 'package:daravel_pulse/daravel_pulse.dart';
Application createApp() => Application.configure(basePath: basePath)
.withPulse(middleware: ['auth', 'can:viewPulse'])
// ...
.create();
The dashboard is at /pulse (PULSE_PATH, or withPulse(path: '/admin/pulse')). It refreshes every five seconds and shows the last hour,
6 hours or 24 hours.
Who can see it#
Outside APP_ENV=local and testing the dashboard answers 403 until
withPulse is given middleware, such as auth and the gate Laravel Pulse
uses:
// bootstrap/policies.dart
gate.define('viewPulse', (user, args) => user is User && user.isAdmin);
PULSE_ENABLED=false records nothing and answers 404.
What it shows#
- Requests. The count, server errors (5xx), slow requests and the average time, with a chart of the period. The dashboard's own polling is not counted.
- Slow requests. Grouped by route pattern (
/users/{id}, never the raw path), with their count and the slowest. - Slow queries. Grouped by SQL and connection. The bindings are never
kept, so the values in a
wheredo not reach the page. - N+1 and query storms. A request that ran one statement
PULSE_N_PLUS_ONE_THRESHOLDtimes, orPULSE_QUERY_STORM_THRESHOLDqueries in all, with the statement and the route. Fast queries add up, so no slow query threshold would catch these. - Connection waits. Queries that waited
PULSE_POOL_WAIT_THRESHOLDfor a free connection, by connection. These mean the pool is too small, not that the database is slow. A transaction counts once, when it takes its connection, and SQLite has no pool. - Shadow parity drift. Routes that answered differently from Laravel, with
withUpstream(shadowSampleRate:): the paths and kinds of the differences, never values. - Exceptions. Server errors grouped by class and by the first frame in your code, with the latest message and stack trace. 4xx responses (validation, not found, forbidden) are not exceptions here.
- Queues and jobs. The pending jobs of each queue in
PULSE_QUEUES, thefailed_jobsrows of the period (from every worker process, as they come from the table), and the slow or failed jobs this server's isolates ran. - Workers. Each isolate, the requests it answered, those in flight, and the process's memory.
Settings#
| Key | Default | Meaning |
|---|---|---|
PULSE_ENABLED |
true |
false: nothing recorded, 404 |
PULSE_PATH |
/pulse |
The dashboard's path |
PULSE_SLOW_REQUESTS_THRESHOLD |
1000 |
Milliseconds from which a request is listed |
PULSE_SLOW_QUERIES_THRESHOLD |
1000 |
Milliseconds from which a query is listed |
PULSE_N_PLUS_ONE_THRESHOLD |
5 |
Repeats of one statement in a request that are listed |
PULSE_QUERY_STORM_THRESHOLD |
25 |
Queries in a request that are listed, whatever they are |
PULSE_POOL_WAIT_THRESHOLD |
20 |
Milliseconds of waiting for a connection that are listed |
PULSE_PARITY_DRIFT_THRESHOLD |
5 |
Drifts of one route in the period from which the Shadow parity drift card marks it; Daravel's own |
PULSE_SLOW_JOBS_THRESHOLD |
1000 |
Milliseconds from which a job is listed |
PULSE_QUEUES |
default |
Queues of the default connection whose size is shown |
PULSE_PUBLISH_SECONDS |
5 |
How often a worker sends its report, under a supervisor |
The threshold keys are Laravel Pulse's, so one .env serves both.
How it works#
Each isolate keeps a PulseRecorder: request counts by minute, and the
slow requests, slow queries, exceptions and jobs in ten-minute buckets,
grouped (at most 50 groups of a kind per bucket), for 24 hours. Memory stays
bounded however busy the server is.
With APP_ISOLATES above 1, an isolate that recorded something sends its
report to the coordinator a few seconds later (PULSE_PUBLISH_SECONDS), and
the isolate answering the dashboard merges every worker's latest report
with its own. A worker that crashes takes its report with it. Data lives in
the process: a restart starts from zero, and two servers behind a load
balancer each show their own. For history across restarts and servers,
export /metrics to Prometheus (Operations).
Laravel Pulse stores its data in pulse_* tables; Daravel's is not
compatible with them, so a Laravel application sharing the database keeps
its own dashboard.