ahmedmerza/logscope
LogScope is a Laravel package for viewing, searching, and managing application logs from a clean dashboard. Filter entries, inspect context, and troubleshoot faster without digging through raw log files.
composer require ahmedmerza/logscope
php artisan logscope:install # or vendor:publish --provider="AhmedMerza\LogScope\LogScopeServiceProvider"
php artisan migrate
Because the package is database-backed, installation is not a pure "drop in a route and go" affair — there is a schema and (almost certainly) a log-writing path that has to be wired into Monolog. Read these three things in this order before touching anything else:
config/logscope.php — the retention window, the table name, the route prefix/middleware, and whether the package replaces your log channel or mirrors it. This single file answers 90% of the questions you'll have later.level, channel, and created_at aren't indexed, you will feel it within a week on any real volume.src/LogScopeServiceProvider.php — it tells you whether the package registers a Monolog handler, a custom log driver (logscope), or both, and whether the UI route is public by default.First use case: point a dedicated channel at it and let your exception handler write there, so you get a searchable feed of production.ERROR without SSH-ing to run tail -f storage/logs/laravel.log. Typical first win:
// config/logging.php
'channels' => [
'logscope' => [
'driver' => 'logscope',
'level' => 'warning',
],
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'logscope'],
],
],
Then visit the configured route (check the config — don't assume /logscope) and confirm your existing Log::error() calls appear. Do this in local first with a deliberate Log::error('smoke test', ['user_id' => 1]).
Keep the file channel as the source of truth during rollout. Mirror into the database rather than replacing single. Database logging can fail (connection down, table locked, migrations not run), and if it's the only sink you lose the very logs that would tell you why.
Level-partition the write path. Writing every debug line to MySQL is the classic way this pattern dies. Set the handler's threshold to warning or error in production and leave debug on the file channel. If the package exposes per-channel configuration, use it — one channel per severity band, each with its own retention.
Trim on a schedule, not on read. Add the package's prune command (check php artisan list | grep logscope) to the scheduler:
$schedule->command('logscope:prune')->dailyAt('03:15');
If no command exists, ship your own with LogScopeModel::where('created_at', '<', now()->subDays(30))->delete() in chunks — never a single unbounded delete().
Lock the UI down before it ever reaches production. Wrap the route in the config's middleware array with auth plus a gate:
Gate::define('viewLogScope', fn (User $u) => $u->hasRole('admin'));
If the package supports a custom authorization callback, use it rather than relying on middleware alone — middleware gets copy-pasted away during refactors; a gate in the service provider survives.
Use contextual logging deliberately. Log a consistent context array (request_id, user_id, tenant_id, job_id) so the viewer's filters become genuinely useful:
Log::error('Payment capture failed', [
'request_id' => request()->header('X-Request-Id'),
'tenant_id' => tenant()->id,
'order_id' => $order->id,
'gateway' => $gateway->name(),
]);
This is the difference between a pretty log viewer and an actually operable one. Without tenant_id you cannot give a customer-support engineer a scoped view.
Scope the query for multi-tenant setups. If your viewer is tenant-facing, add a global scope or a query macro that constrains on the context column, and store tenant_id as a first-class indexed column rather than buried in JSON — filtering on JSON extracts is slow and awkward.
Pair it with a digest. The viewer is pull-based; nobody stares at it. Schedule a command that queries recent error/critical rows grouped by message hash and posts a summary to Slack, so the viewer becomes the drill-down rather than the alert.
Test against it without hitting the database. Use Log::spy() / Log::shouldReceive() in unit tests and reserve real writes for one integration test that asserts a Log::error() produces a row. Database log handlers have a habit of swallowing exceptions inside catch blocks; a single smoke test catches that class of bug early.
Recursive logging is the headline risk. If the database write fails, the handler logs the failure, which tries to write to the database. Verify the handler wraps its own writes in try/catch and silently falls back to error_log() — and never let the package's own channel be part of a stack that includes itself.
Transactions can erase your evidence. Rows written inside a transaction that later rolls back disappear. If you log inside a DB transaction and then throw, expect the log to vanish. Either log after commit (DB::afterCommit) or accept the loss knowingly.
Payload size. Full stack traces plus serialized context can hit row limits or bloat the table fast. Check whether the package truncates; if not, truncate message and context before they reach the handler, and cap trace depth.
Serialization surprises. Arrays with closures, Eloquent models with large relations, and UploadedFile instances in context can throw during encoding. Prefer scalar context. If a log call silently disappears, JSON encoding is the first suspect.
Schema and index drift. If you publish and later modify the migration, subsequent migrate runs on other environments won't pick up changes. Add indexes via a separate migration you control, and verify the ones you need exist:
SHOW INDEX FROM logscope_entries;
Migrations must run before the handler is active. On a fresh deploy, any log write before php artisan migrate completes will error. Gate the channel behind an env flag, or ensure the deploy ordering is migrate-then-restart-workers.
Queue workers and Octane need a restart. Long-lived processes hold the old Monolog stack. php artisan queue:restart and an Octane reload are mandatory after changing logging config — this is the #1 reason "it works locally but not in prod" for log handlers.
Vendor assets and Tailwind. "Beautiful" usually means published CSS/JS. If the UI renders unstyled, you're missing vendor:publish --tag=logscope-assets. Check the public/ path matches your asset URL, especially behind a subdirectory or CDN.
Be conservative about adoption. Three stars, zero dependents, and a future-dated release mean you are the de facto QA. Pin the exact version in composer.json, read the handler source before production, and keep the file channel as a fallback so removing the package is always a one-line config change. Treat the DB table as derived data you can drop at any time — never store anything there you couldn't reconstruct.
Debugging checklist when logs don't appear: (1) is the channel in the active stack, (2) does the level threshold allow it, (3) did the migration run on this environment, (4) are workers running stale code, (5) did a try/catch swallow the write error — check storage/logs/laravel.log precisely because LogScope isn't writing there.
How can I help you explore Laravel packages today?