How I built a request tracer for Laravel without a PHP extension
When a page in a Laravel app is slow, the usual tools tell you that it was slow. I wanted to know which function and which query made it slow, and which line of my code ran that query. So I built a tool for it: BlastRadius, my first open-source package.
!BlastRadius overview
What it does
You install it, open /blast-radius in your browser, and use your app normally. For each page you open, it shows:
- the API calls the page made
- a timeline of your own functions and database queries, with time and rows returned
- which query failed, and the line of your code that ran it
!Live requests
When something fails, the page turns red and shows the exception, the failing query and where it was called from:
!A failed request
It also has a Code flow tab that draws how your routes, jobs and events reach your functions, and a command that looks at a git change and lists the routes it could affect and the tests to run.
The interesting part: tracing without a PHP extension
Profilers like Xdebug or Blackfire hook into PHP itself, which means installing an extension. I wanted composer require --dev and nothing else, so BlastRadius does three things in plain PHP.
1. It rewrites your methods as they load. A small autoloader runs before Composer's. For classes in your app folder it loads a rewritten copy where every method reports to a tracer:
// before
public function pay() { /* your code */ }
// after (all on the same line, so line numbers don't move)
public function pay() {
$__br = Tracer::enter('Payments::pay');
try { /* your code */ }
catch (\Throwable $__be) { Tracer::fail($__br, $__be); throw $__be; }
finally { Tracer::leave($__br); }
}
The rewriting is done on the token level and keeps every line where it was, so stack traces and error messages still point at the right place. The copies are saved by a hash of the file's contents, so editing a file simply creates a new copy. If a rewrite ever produces invalid PHP, the loader falls back to your original file, so the worst case is "not timed", not "broken page".
2. It wraps the database connection. Laravel lets you register your own connection class per driver. BlastRadius registers a thin subclass that counts the rows each query returned or changed, and records failures, so you see "this query returned 27,313 rows in 293 ms" next to the function that called it.
3. It records each request in a middleware. When the request ends, the function timeline, the queries and any exception are saved in a small SQLite file (the last 300 requests). The page then groups them by the page that made them, using the Referer header.
The "what can this break" half
For a git diff, BlastRadius parses your code with php-parser, builds a call graph, and follows callers back to routes, jobs and commands. Every link has a confidence: CONFIRMED (the code says so), POSSIBLE (a guess from names) or UNRESOLVED (dynamic calls, reported instead of silently dropped). A path is only as certain as its weakest link.
What it can't do (yet)
- It's a development tool. It stays off in production unless you turn it on on purpose.
- It times your own code, not vendor packages.
- It records how many values a query bound, not the values themselves.
- Dynamic calls ($class::$method()) show up as unresolved, not guessed.
- It's an early release (0.1.x), tested on Laravel 8 to 12 and PHP 8.1 to 8.4. I've used it on a few apps, so expect rough edges.
Try it
composer require --dev idrees-saifuddin/blast-radius
Then open http://your-app.test/blast-radius.
Source and docs: Idrees-Saifuddin/blast-radius
If you try it, I'd really like to know what breaks and what's missing. Open an issue or leave a comment here.
Mentions
How I built a request tracer for Laravel without a PHP extension When a page in a Laravel app is slow, the usual tools tell you that it was slow. I wanted to know which function and which query made it slow, and which line of my code ran that query. So I built a tool for it: BlastRadius, my first open-source package. !BlastRadius overview What it does You install it, open /blast-radius in your browser, and use your app normally. For each page you open, it shows: - the API calls the page made - a timeline of your own functions and database queries, with time and rows returned - which query failed, and the line of your code that ran it !Live requests When something fails, the page turns red and shows the exception, the failing query and where it was called from: !A failed request It also has a Code flow tab that draws how your routes, jobs and events reach your functions, and a command that looks at a git change and lists the routes it could affect and the tests to run. The interesting part: tracing without a PHP extension Profilers like Xdebug or Blackfire hook into PHP itself, which means installing an extension. I wanted composer require --dev and nothing else, so BlastRadius does three things in plain PHP. 1. It rewrites your methods as they load. A small autoloader runs before Composer's. For classes in your app folder it loads a rewritten copy where every method reports to a tracer: // before public function pay() { /* your code */ } // after (all on the same line, so line numbers don't move) public function pay() { $__br = Tracer::enter('Payments::pay'); try { /* your code */ } catch (\Throwable $__be) { Tracer::fail($__br, $__be); throw $__be; } finally { Tracer::leave($__br); } } The rewriting is done on the token level and keeps every line where it was, so stack traces and error messages still point at the right place. The copies are saved by a hash of the file's contents, so editing a file simply creates a new copy. If a rewrite ever produces invalid PHP, the loader falls back to your original file, so the worst case is "not timed", not "broken page". 2. It wraps the database connection. Laravel lets you register your own connection class per driver. BlastRadius registers a thin subclass that counts the rows each query returned or changed, and records failures, so you see "this query returned 27,313 rows in 293 ms" next to the function that called it. 3. It records each request in a middleware. When the request ends, the function timeline, the queries and any exception are saved in a small SQLite file (the last 300 requests). The page then groups them by the page that made them, using the Referer header. The "what can this break" half For a git diff, BlastRadius parses your code with php-parser, builds a call graph, and follows callers back to routes, jobs and commands. Every link has a confidence: CONFIRMED (the code says so), POSSIBLE (a guess from names) or UNRESOLVED (dynamic calls, reported instead of silently dropped). A path is only as certain as its weakest link. What it can't do (yet) - It's a development tool. It stays off in production unless you turn it on on purpose. - It times your own code, not vendor packages. - It records how many values a query bound, not the values themselves. - Dynamic calls ($class::$method()) show up as unresolved, not guessed. - It's an early release (0.1.x), tested on Laravel 8 to 12 and PHP 8.1 to 8.4. I've used it on a few apps, so expect rough edges. Try it composer require --dev idrees-saifuddin/blast-radius Then open http://your-app.test/blast-radius. Source and docs: Idrees-Saifuddin/blast-radius If you try it, I'd really like to know what breaks and what's missing. Open an issue or leave a comment here.