Monitoring and Debugging
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05, with
laravel/telescope 5.25.0, laravel/pulse 1.8.1 and laravel/pail. The tools divide by environment. Telescope
records everything in development, Pulse aggregates the essentials in production, Nightwatch or an error tracker
watches production from outside, Pail tails the logs, and the exception page and Xdebug explain a single failure.
Logging itself, with channels, context and the exception handler, is on
Error Handling and Logging.
Telescope in development
composer require laravel/telescope --dev, php artisan telescope:install and php artisan migrate add a dashboard
at /telescope. Its watchers record requests, queries, models, cache operations, Redis commands, views, jobs, mail,
notifications, events, logs, exceptions, gates, outgoing HTTP calls and scheduled tasks into telescope_entries. Eight
requests to Bookshelf produced:
query 91 | model 10 | cache 8 | request 8 | redis 5 | view 4 | command 1
Source: Laravel — Available Watchers
Entries from one request share a batch_id, so the request list can show what each request cost:
<?php
$requests = DB::table('telescope_entries as r')
->where('r.type', 'request')
->select('r.content')
->selectSub(fn ($q) => $q->from('telescope_entries as q')->whereColumn('q.batch_id', 'r.batch_id')->where('q.type', 'query')->selectRaw('count(*)'), 'queries')
->orderBy('r.sequence')
->get();
Source: Laravel — Telescope
/books 200 126 ms 6 queries
/books/1 200 30 ms 6 queries
/api/v1/books 200 49 ms 11 queries
/api/v2/books?include=author%2Creviews 200 41 ms 28 queries
/stats/popular 200 849 ms 4 queries
/does-not-exist 404 22 ms 0 queries
/shelf/notes 200 16 ms 3 queries
/welcome 200 20 ms 3 queries
Source: Laravel — Query Watcher
The 28 queries of the JSON:API route are the per-row relationship loading found on
Building APIs and API Resources. In the dashboard, the
request’s Queries tab lists them, repeated with different ids. /stats/popular is slow because its value is
recomputed after the cache entry expires (Cache and Rate
Limiting). The counts include the session, cache and rate-limiter queries of the database drivers, about three
per request here. The query watcher tags queries slower than 100 ms as slow by default. None was.
php artisan telescope:prune --hours=48 deletes old entries (491 entries pruned. after the experiments) and belongs
in the schedule. Telescope::filter() and the watchers array in config/telescope.php limit what is recorded.
The dev-dependency trap
telescope:install registered TelescopeServiceProvider in bootstrap/providers.php, while the package was installed
with --dev. The production image from Docker and Sail runs
composer install --no-dev. With the generated providers file copied into it, every Artisan command failed:
In TelescopeServiceProvider.php line 11:
Class "Laravel\Telescope\TelescopeApplicationServiceProvider" not found
The documented local-only setup removes the line from bootstrap/providers.php, registers the providers only in the
local environment, and stops auto-discovery:
<?php
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
if ($this->app->environment('local') && class_exists(\Laravel\Telescope\TelescopeServiceProvider::class)) {
$this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); // dev dependency: never in production
$this->app->register(TelescopeServiceProvider::class);
}
}
}
// composer.json: "extra": { "laravel": { "dont-discover": ["laravel/telescope"] } }
After the change, /telescope still answered 200 locally, and the test below confirms it is absent in the testing
environment. Any package in require-dev that registers a provider by hand has the same trap.
Pulse in production
Telescope stores every event, which is too much for production. Pulse (composer require laravel/pulse, then
publishing its config and migrations) aggregates instead. Recorders capture slow requests, slow queries, slow jobs,
slow outgoing requests, exceptions, cache hits and misses, queue throughput and the users with the most requests,
and roll them into time buckets in pulse_entries and pulse_aggregates. After the same traffic:
pulse_entries: cache_hit=3 cache_miss=2 exception=3
pulse_values: system vm (written by php artisan pulse:check --once)
Source: Laravel — Pulse Recorders
The 849 ms request did not appear as slow, because the slow request, query, job and outgoing request thresholds
default to 1000 ms (PULSE_SLOW_REQUESTS_THRESHOLD and the others). They should be set to what the application calls
slow. php artisan pulse:check, run as a daemon on each server, feeds the Servers card with CPU, memory and disk.
The exception entries were the deliberate errors from the next section.
Pulse writes in the same request by default (the storage ingest). Under load, PULSE_INGEST_DRIVER=redis with a
php artisan pulse:work process moves the writes off the request path, and sample_rate on busy recorders keeps a
fraction of the events. A dedicated PULSE_DB_CONNECTION keeps its tables away from the application database.
Dashboard access
Both dashboards are open in local and closed elsewhere until a gate allows someone:
local /telescope 200 local /pulse 200
production /telescope 403 production /pulse 403
<?php
// tests/Feature/MonitoringTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
uses(RefreshDatabase::class);
test('telescope is not even registered outside local', function () {
expect(app()->environment())->toBe('testing');
$this->get('/telescope')->assertNotFound();
});
test('the pulse dashboard is closed to everyone by default', function () {
$this->get('/pulse')->assertForbidden();
$this->actingAs(User::factory()->create())->get('/pulse')->assertForbidden();
});
test('operators can open pulse once the gate allows them', function () {
Gate::define('viewPulse', fn (User $user) => $user->email === 'ops@bookshelf.test');
$this->actingAs(User::factory()->create(['email' => 'ops@bookshelf.test']))->get('/pulse')->assertOk();
});
All three pass. The viewPulse gate belongs in AppServiceProvider::boot(). The viewTelescope gate in the
generated provider starts with an empty e-mail list, and viewHorizon works the same way
(Queues and Jobs).
Outside the application: Nightwatch and error trackers
Laravel Nightwatch is a hosted monitoring service: an agent sends requests, queries, jobs, exceptions and logs to nightwatch.laravel.com, where they can be searched with traces across requests and jobs. Sentry and Flare collect exceptions with their context in the same way. The exception handler reports to them (Error Handling and Logging). An outside service keeps working when the application, its database or its server is down, which a dashboard inside the application cannot do. Nightwatch was not run for this page, because it needs an account and outbound access.
Pail
php artisan pail tails the application log in the terminal, with filters (--filter, --level, --user) and
without having to know which file or channel is in use:
$ php artisan pail --timeout=10
INFO Tailing application logs. Press Ctrl+C to exit
┌ 23:10:09 INFO ───────────────────────────────────────────────────────────────┐
│ loan created │
└ GET: /log/demo • Auth ID: guest • isbn: 978-0-446-67697-7 • trace_id: 01a10e54-ed34-7355-be47-1d03970fd3d1 ┘
┌ 23:10:09 WARNING ────────────────────────────────────────────────────────────┐
│ shelf nearly full │
└ GET: /log/demo • Auth ID: guest • shelf: A3 • trace_id: 01a10e54-ed34-7355-be47-1d03970fd3d1 ┘
Each entry shows the route, the user and the Context values, here the trace_id. Pail needs the pcntl
extension, and it is started with composer run dev alongside the server, the queue and Vite.
The exception page, dump and dd
With APP_DEBUG=true, an exception renders Laravel’s exception page. With false, it renders the generic error page.
The same route on the two servers:
local 500 916 440 bytes stack trace with code, request headers and body, routing, the queries run, "Copy as Markdown"
local JSON 500 {"message":"catalogue index is corrupt","exception":"RuntimeException","file":…,"line":…,"trace":[…57 frames]}
production 500 <title>Server Error</title>, no message
Source: Laravel — Error Handling
The "Copy as Markdown" button copies the exception, the trace and the request into the clipboard for an issue or an AI
assistant. The local page includes request headers and the queries, which is why APP_DEBUG must never be on in
production.
dump($value) prints a value with Symfony’s VarDumper and continues; the response contained three sf-dump blocks.
dd($value) dumps and stops. ->dd() and ->dump() exist on query builders, collections and responses, and
$query->dumpRawSql() prints the SQL with bindings. A dump() left in the code reaches users if APP_DEBUG is on, so
a static-analysis rule against them in CI is worth having.
Xdebug
Step debugging needs Xdebug in the PHP that serves the request and an IDE listening on port 9003. Herd includes
Xdebug and switches it on per site. Sail starts with SAIL_XDEBUG_MODE=develop,debug
(Docker and Sail). On a host, xdebug.mode=debug and
xdebug.start_with_request=trigger in php.ini make it start only when the browser extension or
XDEBUG_TRIGGER asks. The extension slows every request even when idle, so it stays out of production. Installing
and configuring Xdebug, including its profiler, is on
Debugging and Profiling.
Where older books and tutorials are outdated
-
barryvdh/laravel-debugbaras the only insight tool (common in Laravel 5 tutorials) still works for HTML pages. Telescope also covers API requests, jobs, mail and the queue. -
Filp/Whoops and then Ignition were the error pages of earlier versions. Current applications use Laravel’s own exception page, and Ignition is no longer in the skeleton.
-
tail -f storage/logs/laravel.logis replaced byphp artisan pail, which works with any channel and filters by user. -
Telescope in production was common, and it fills the database. Pulse and Nightwatch are designed for production.