mojtabaahn/laravel-web-logs
Browse and read your Laravel application logs directly in the browser. This lightweight package adds a simple web UI for viewing, searching, and filtering log files—no SSH or server access needed—with configurable routes, middleware, and access control.
1. Confirm it will even install. The package is archived (last release Feb 2022), so its composer.json almost certainly pins illuminate/* to Laravel 8/9. Check before anything else:
composer show mojtabaahn/laravel-web-logs --all | grep -i illuminate
# or read vendor/mojtabaahn/laravel-web-logs/composer.json
On Laravel 10+ you'll get a dependency conflict — either fork it and widen the constraints or stop here and use opcodesio/log-viewer instead (same feature set, maintained).
2. Install and publish.
composer require mojtabaahn/laravel-web-logs
php artisan vendor:publish --tag=web-logs-config # check `php artisan vendor:publish` output for the exact tag
3. Read the published config before browsing. config/web-logs.php is the whole package: log directory path, file pattern (usually *.log), route prefix, and — most importantly — middleware. Key names differ slightly between versions, so trust the published file, not the README.
4. Lock it down immediately. Add auth (and a can: gate) to the middleware array before you ever expose the route. Log files contain stack traces, SQL, request payloads, tokens, and emails.
5. First use case. Locally: reproduce a 500, open http://localhost:8000/web-logs (or your prefix), pick laravel.log, filter level ERROR, click the timestamp to jump to context. That's the whole value proposition — no SSH, no tail -f, no leaving the browser.
Where to look first in the source (vendor/mojtabaahn/laravel-web-logs/):
src/Http/routes.php (or routes/) — the actual endpoints and param names.file param is sanitized.config/web-logs.php — the only supported extension point.Gate-based access (do this, not just auth).
// AppServiceProvider::boot() (Laravel 11) or AuthServiceProvider (≤10)
Gate::define('viewWebLogs', fn (User $user) => $user->is_admin());
// config/web-logs.php
'middleware' => ['web', 'auth', 'can:viewWebLogs'],
Do not omit web — auth and can need the session middleware to resolve the user, and ordering matters.
Environment-aware config so production is never exposed accidentally.
'middleware' => array_filter([
'web',
app()->isLocal() ? null : 'auth',
app()->isLocal() ? null : 'can:viewWebLogs',
]),
Better: also add a hard kill switch.
// a one-line middleware in front of everything
abort_unless(config('app.debug') || app()->isLocal(), 404);
Return 404, not 403 — no reason to advertise the endpoint.
Register only where you want it. Because the service provider is auto-discovered, you can't easily skip it per environment. The clean options: a config-driven enabled flag if the version supports it, a global middleware returning 404, or move the package to require-dev and only install locally.
Request-ID correlation is the highest-value habit. If every log line carries a request ID, the viewer becomes a real triage tool rather than a pretty cat:
// middleware
Log::withContext(['rid' => $rid = (string) Str::uuid()]);
$response->headers->set('X-Request-Id', $rid);
Then in the viewer, paste the rid from the user's error report and get the full trace for that request — including jobs and nested logs, if you propagate the context.
Separate channels, not one giant file. Use daily plus dedicated channels (payments, jobs, webhooks) and set LOG_DAILY_DAYS retention. The viewer's file list becomes your channel switcher, and a runaway channel doesn't drown everything else. Verify your channels use the default Monolog line formatter — see Gotchas.
Triage workflow that actually holds up:
ERROR (or search the ID) → read the stack trace.php artisan tinker / a queue retry.Keep the viewer for local and staging; production belongs to Sentry/Telescope/CloudWatch. This package reads a local disk — it's useless on Vapor, Cloud Run, or any read-only/ephemeral filesystem.
Pairing it with the CLI. The viewer is a convenience, not a replacement: tail -f storage/logs/laravel.log | grep -i error and less +F are still faster for streaming. Reach for the web UI when you don't have shell access — Windows dev, shared hosting, a locked-down PaaS.
Admin panel integration. Point prefix at something inside your existing admin namespace (e.g. admin/logs) so it inherits your layout/nav and your existing auth stack, rather than standing alone at /web-logs with a separate middleware stack to keep in sync.
Maintenance risk is the headline. Archived since Feb 2022, 154 stars, no security response. Audit the controller before exposing it anywhere non-local, and budget a migration to opcodesio/log-viewer at some point — the feature set is a superset and it handles big files and JSON logs properly.
The default middleware is the trap. Fresh installs commonly ship with ['web'] only, i.e. a public route that dumps your entire log directory — including APP_KEY in a boot-time stack trace, DB credentials in a connection error, and full request bodies. Set the gate before the first php artisan serve.
config:cache and route:cache will make you think the config is broken. Editing config/web-logs.php on a cached production box does nothing until php artisan config:cache re-runs. Same for changing prefix or middleware — the cached route table wins. If the route 404s after a deploy, run php artisan route:list --path=web-logs first.
Custom Monolog formatters break everything. The parser assumes the stock format:
[2024-05-01 10:22:31] production.ERROR: Something failed {"exception":"..."}
Switch to JsonFormatter, a custom LineFormatter with a different pattern, or a non-default timestamp format and you'll get unparsed lines: no level filter, no timestamp grouping, everything lumped as one entry. Check config/logging.php before blaming the package.
Multi-line stack traces and "one entry" grouping. The viewer groups entries by the [timestamp] env.LEVEL line. Filters and pagination operate on those groups. A trace without its header line (e.g. from a mid-write rotation) shows up as an orphan blob with no level — expected, not a bug.
Whole-file reads are the scaling wall. Versions of this package load the log into memory to render it. A 500 MB laravel.log will either time out or OOM your PHP process — and the process doing the OOMing is your web container. Keep files small: daily channel, LOG_DAILY_DAYS=7, and manual truncation (: > storage/logs/laravel.log) on staging. If you must keep a big file, check the source for a tail/limit parameter before you open it in a browser during an incident.
Permission ownership in Docker is the classic silent failure. php artisan run as root in a container creates root:root laravel.log; PHP-FPM runs as www-data and can't read it — the viewer shows an empty list or an empty file, and nothing in laravel.log explains why. Either run artisan as the app user, or chmod -R g+r / fix the group. Same story for Kubernetes read-only mounts.
Path handling. The file selector takes a filename from the query string. Confirm the version you install uses basename() and restricts to the configured path — and be aware that a symlink inside storage/logs pointing outside it may be followed. Don't symlink /var/log into the logs directory and call it done.
Timezone drift. Entries are stamped in APP_TIMEZONE at write time. Change the app timezone and old logs now "lie" relative to new ones — correlation by timestamp goes wrong during the transition. Prefer request IDs; they don't drift.
Front-end assets. If the UI pulls CSS/JS from a CDN, an air-gapped or firewalled environment renders a bare HTML table. Not broken — just unstyled. Check the Blade layout if it looks wrong.
After a Laravel upgrade that "breaks the page": php artisan view:clear && php artisan route:clear before debugging the package itself. Cached compiled views from an older Blade version are a common red herring.
Log level confusion. APP_DEBUG=false plus LOG_LEVEL=error means info/debug lines never get written — the viewer isn't filtering them out, they don't exist. If "nothing shows up," verify the file on disk with wc -l and tail -1 before touching config.
Don't leave it reading a path that grows unbounded. Set a channel per concern, cap retention, and treat the viewer as read-only tooling for a bounded directory. The moment storage/logs becomes multi-gigabyte shared storage, this package costs you more incidents than it solves.
How can I help you explore Laravel packages today?