Why Not Just Use Telescope in Production?
Laravel Telescope is excellent during development, but its write-on-every-request model — storing queries, jobs, logs, and HTTP payloads to a dedicated database table — creates measurable overhead and a growing storage footprint. Most teams disable it in production entirely, leaving a blind spot for intermittent issues that only appear under real traffic.
The goal here is a targeted inspector: capture only what you opt into, store it ephemerally in Redis via tagged cache, and surface it through a locked-down Filament panel. No extra database tables, no always-on overhead.
The Core Idea: Tagged Cache as a Ring Buffer
Redis supports tagging via Laravel's Cache::tags(). We can write request snapshots under a shared tag and expire them automatically, giving us a rolling window of recent traffic without permanent storage.
// app/Http/Middleware/InspectRequest.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class InspectRequest
{
public function handle(Request $request, Closure $next): mixed
{
$response = $next($request);
if (! config('inspector.enabled')) {
return $response;
}
$key = 'req:' . Str::uuid();
Cache::tags(['inspector'])->put($key, [
'method' => $request->method(),
'path' => $request->path(),
'status' => $response->getStatusCode(),
'duration_ms' => (int) ((microtime(true) - LARAVEL_START) * 1000),
'memory_kb' => (int) (memory_get_peak_usage(true) / 1024),
'ip' => $request->ip(),
'at' => now()->toIso8601String(),
], now()->addMinutes(30));
return $response;
}
}
Register it selectively — not globally — using a route group or a specific middleware alias so you control which routes are traced.
Storing Query Counts Without a Full Query Log
Rather than logging every SQL statement (Telescope's approach), count them cheaply using the DB listen callback scoped to the request lifecycle:
// Inside InspectRequest::handle, before $next($request)
$queryCount = 0;
\DB::listen(static function () use (&$queryCount) {
$queryCount++;
});
$response = $next($request);
// Then include $queryCount in the Cache::tags()->put() payload above.
This adds negligible overhead compared to capturing full bindings and execution times for every query.
Surfacing Data in a Filament Panel
Create a read-only Filament resource backed not by Eloquent but by a custom getTableQuery override that reads from the tagged cache.
// app/Filament/Resources/RequestSnapshotResource.php
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('method')->badge(),
TextColumn::make('path'),
TextColumn::make('status')->badge()
->color(fn ($state) => $state >= 500 ? 'danger' : ($state >= 400 ? 'warning' : 'success')),
TextColumn::make('duration_ms')->suffix(' ms')->sortable(),
TextColumn::make('query_count')->label('Queries'),
TextColumn::make('at')->since(),
])
->paginated([25, 50]);
}
public static function getEloquentQuery(): Builder
{
// Filament expects a Builder; wrap cache reads in a collection-backed fake.
// Use a custom ListRecords page that overrides getTableRecords() instead.
throw new \LogicException('Use custom list page.');
}
The cleaner approach is to override getTableRecords() in a custom ListRequestSnapshots page:
protected function getTableRecords(): Collection
{
$keys = Redis::connection()->keys('*inspector*req:*');
return collect($keys)
->map(fn ($key) => Cache::tags(['inspector'])->get(
Str::after($key, config('cache.prefix') . 'inspector|')
))
->filter()
->sortByDesc('at')
->values();
}
Lock the panel behind a gate or a dedicated guard so it is never publicly accessible.
Enabling Only When Needed
Add a runtime toggle via an artisan command that flips a cache flag:
php artisan inspector:on # sets inspector.enabled in cache for 1 hour
php artisan inspector:off
The middleware reads Cache::get('inspector.enabled', false) so there is zero overhead when the inspector is off.
Takeaways
- Tagged cache as ephemeral storage gives you a self-expiring ring buffer with no schema migrations.
- Opt-in middleware keeps production overhead at zero when the inspector is disabled.
- Query counting, not logging, captures the signal you actually need (N+1 detection) without the storage cost.
- A Filament panel gives you a polished UI without building a custom frontend.
- Runtime toggle via artisan lets you enable tracing surgically during an incident and disable it immediately after.