Seeing What Happens

Telescope and Debugbar show every query, job and cache hit behind a request, and neither belongs in production.

Syntaxphp artisan telescope:install && php artisan migrate

Guessing at what a request does takes longer than looking at it. Two tools show it.

Telescope

Laravel Telescope is a first-party package that records requests, queries, jobs, mail, notifications, cache operations, exceptions and dumps into your own database and serves them at /telescope. Install it with composer require laravel/telescope, then php artisan telescope:install to publish config/telescope.php and its migrations, then php artisan migrate. The query watcher tags anything over 100ms as slow; the threshold is the slow option on Watchers\QueryWatcher.

Debugbar

Debugbar (fruitcake/laravel-debugbar, previously barryvdh/laravel-debugbar) injects a toolbar into HTML responses showing the query count, the queries, timings and the views rendered. It enables itself when APP_DEBUG is true and the environment is neither production nor testing. DEBUGBAR_ENABLED=false switches it off, but it cannot switch it on: in production or testing the bar stays off unless you also set the force_allow_enable option in config/debugbar.php.

The number to watch, and keeping both out of production

Query count per page. If it grows with the number of rows on the page, that is an N+1. Both tools expose request data, including sessions and input, so neither should be reachable in production. Install Telescope with --dev, register it for the local environment only, and keep it out of package discovery. Telescope also writes a row per entry, so schedule telescope:prune or telescope_entries will outgrow your own tables.

Example

Example · php
<?php

// app/Providers/AppServiceProvider.php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // Telescope is a --dev dependency, so both guards matter: the
        // environment check, and class_exists for the production build
        // where the package is absent.
        if ($this->app->environment('local') && class_exists(\Laravel\Telescope\TelescopeServiceProvider::class)) {
            $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);

            // Unqualified: App\Providers\TelescopeServiceProvider, the one
            // telescope:install published, which holds the filter and the
            // viewTelescope gate.
            $this->app->register(TelescopeServiceProvider::class);
        }
    }
}

When to use it

  • Finding which queries a slow admin page runs, and in what order.
  • Confirming a queued job was dispatched with the payload you expect.
  • Previewing an email in the browser instead of sending it.
  • Comparing the query count on a page before and after adding with().

More examples

Installing both for development only

After telescope:install, remove the TelescopeServiceProvider line it added to bootstrap/providers.php and register it from AppServiceProvider instead, then stop auto-discovery by adding "dont-discover": ["laravel/telescope"] under extra.laravel in composer.json. Without that last step package discovery tries to register the provider in every environment. Telescope's own kill switch is 'enabled' => env('TELESCOPE_ENABLED', true) in config/telescope.php, which stops collection without removing the package.

Example · shell
# --dev keeps Telescope out of the production build entirely.
composer require laravel/telescope --dev

# Publishes config/telescope.php and the telescope_entries migrations.
php artisan telescope:install
php artisan migrate

# The toolbar for HTML responses.
composer require fruitcake/laravel-debugbar --dev

Pruning what Telescope records

A busy local database can collect hundreds of thousands of telescope_entries rows in a week, and every one of them is a row your own queries scan past. daily() runs the command at midnight; dailyAt('03:00') is the same schedule with the hour chosen explicitly. On a staging install this matters more than locally, because the entries sit in the same database as the data you are testing against.

Example · php
<?php

// routes/console.php

declare(strict_types=1);

use Illuminate\Support\Facades\Schedule;

// Entries older than 48 hours are deleted. Without --hours the command
// keeps 24 hours.
Schedule::command('telescope:prune --hours=48')->daily();

The query count without either package

Useful where a toolbar cannot go: a JSON API, an Artisan command, a queued job, or a staging box where you will not install Telescope. DB::listen receives an Illuminate\Database\Events\QueryExecuted for every query, carrying sql, bindings, time and toRawSql(). Registering it in boot() costs one closure call per query, so keep it behind an environment check rather than shipping it everywhere.

Example · php
<?php

// app/Providers/AppServiceProvider.php - boot()

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

$queries = 0;
$milliseconds = 0.0;

DB::listen(function (QueryExecuted $query) use (&$queries, &$milliseconds): void {
    $queries++;
    $milliseconds += $query->time;   // milliseconds, as a float
});

$this->app->terminating(function () use (&$queries, &$milliseconds): void {
    Log::debug("queries={$queries} db_ms={$milliseconds}");
});

Discussion

  • Be the first to comment on this lesson.