graham-campbell/logviewer
A Laravel package that provides a browser-based log viewer for your application's log files, letting you browse, filter, and manage logs without opening a terminal. Simple to install and configure, it offers a quick way to inspect errors and debug issues in local or staging environments.
1. Install
composer require graham-campbell/logviewer
This package targets Laravel 5.x. On 5.5+ auto-discovery registers the provider; on older 5.x (or if you've disabled discovery) register manually in config/app.php:
'providers' => [
// ...
GrahamCampbell\LogViewer\LogViewerServiceProvider::class,
],
Optional facade, only needed if you want to reference it programmatically:
'aliases' => [
'LogViewer' => GrahamCampbell\LogViewer\Facades\LogViewer::class,
],
2. Publish the config so you can see and change the route and middleware:
php artisan vendor:publish --provider="GrahamCampbell\LogViewer\LogViewerServiceProvider"
3. Where to look first
config/logviewer.php — this is effectively the entire surface area of the package. Confirm the route/URI, the middleware stack, and any view/path overrides here before touching anything else. Getting a 404 or a 500? Nine times out of ten it's this file.vendor/graham-campbell/logviewer/src/ — the controller and route registration, for tracing why a route 404s or why a request is 500ing.vendor/graham-campbell/logviewer/resources/views/ — publish these if you want to customise the listing (redaction, branding, escaping fixes).4. First use case
Hit the registered route — typically /logviewer on a default install; verify against your published config — and you get a paginated, colour-coded listing of storage/logs. The immediate payoff: a teammate reports a 500 in staging, you open the route, filter to ERROR, and read the stack trace without SSH, tail -f, or downloading a file. Treat it as a read-only log tail in the browser, nothing more.
5. Before you call it "done," gate it. The zero-config default is not safe. Add auth (plus a role check) and an environment restriction before the first deploy. See the middleware pattern below — this is the single most important step in the Getting Started flow.
Gate the route with real authorisation middleware. The package exposes raw storage/logs over HTTP; the default middleware is not a security control. Publish the config and set an explicit stack:
// config/logviewer.php
return [
'middleware' => [
'web',
'auth',
'can:view-logs', // Gate/policy — restrict to named engineers
'throttle:60,1',
],
// 'prefix' / 'uri' depending on your version of the config stub
];
Define the ability once so the rule lives in code, not in a config string:
// AuthServiceProvider::boot()
Gate::define('view-logs', fn ($user) => $user->isEngineer());
Restrict by environment inside the provider. Wrap LogViewerServiceProvider registration so it only binds outside production, or guard the route itself:
if (app()->environment('local', 'staging')) {
$this->app->register(\GrahamCampbell\LogViewer\LogViewerServiceProvider::class);
}
This is the pattern most teams end up on: staging-only viewer, break-glass production access only via a named individual + an explicit temporary config change.
Keep the route off the public surface entirely. Put it behind your VPN/private subnet, or an IP allowlist at the web-server layer, in addition to the app-layer auth. Two independent gates, because logs will contain credentials and PII if you run a real app.
Pick the driver to match your logging setup. The viewer handles both single and daily. If you're on daily (the common staging choice), you'll get a per-file list to browse; if you're on single, you get one paginated stream. Confirm which one you're on before debugging "where did my log go" — the viewer shows what's on disk, so a misconfigured LOG_CHANNEL means an empty viewer even though the app is logging fine (to somewhere else).
Publish and patch the view if you need redaction. The package does no redaction. If your logs carry tokens, emails, or PII, publish the views and add a masker before rendering lines:
php artisan vendor:publish --provider="GrahamCampbell\LogViewer\LogViewerServiceProvider" --tag=views
Then apply a regex mask over each line in the published blade template (bearer tokens, password=..., email addresses). Keep the masker in a small helper class so it's testable — it's the only place sensitive data can leak out of this package.
Use it alongside, not instead of, the dev tooling. For everyday local work, php artisan pail or Laravel Telescope gives you tailing, query inspection, and request context. Reach for LogViewer specifically when you need browser access from a machine that has no shell — a support laptop, a QA tester's browser, a designer triaging a staging bug. That's the niche it fills; don't ask it to fill more.
Clear/delete is a footgun, not a feature. The UI can delete log files. If that button is reachable by anyone who isn't you, either remove it in the published view or make sure the middleware stack is tight enough that it's effectively admin-only. Losing the log that explains the incident because someone clicked "clear" is a real failure mode.
Pin the version and record it as unmaintained. This package has zero dependents and no active maintainer. Pin an exact version in composer.json (not ^), and add a note in your dependency inventory so the next Laravel/PHP upgrade doesn't silently break it after a composer update. If you keep it, you are the maintainer of record — budget a fork-and-patch reserve rather than an upgrade cadence.
XSS is a live risk, not a theoretical one. Log lines contain user input by definition. Any escaping gap in the view becomes stored XSS inside an authenticated admin surface — which is worse than public XSS, because the session it steals has privileges. If you publish views, use {{ }}/e() consistently on log content and never {!! !!}. If you don't publish views, still assume the upstream view was written against the assumptions of 2015 and don't expose this beyond a trusted group.
"No logs" usually means one of three things. (1) You're on a containerised/autoscaled deployment and this request landed on a pod with an empty or different storage/logs. (2) The LOG_CHANNEL is stderr or a centralised channel, not single/daily. (3) A permission/ownership drift on storage/logs means the PHP process can't read it — you'll get a 500 from the admin panel itself, not an empty list. Check the actual filesystem before debugging the package.
Log rotation mid-read gives you truncated or inconsistent output. On daily, a file can rotate while you're paging through it. If a view looks like it's missing the tail, refresh rather than assuming the log is wrong. For anything 100MB+, expect slow responses and memory pressure despite the pagination — this is a per-host convenience, not a log-search tool.
Multi-node deployments make this actively misleading. Each request hits one node's disk. You will see "no errors" while errors pile up on another instance. If you're on more than one node, the correct answer isn't to patch this — it's to ship logs off-host and drop the in-app viewer.
Route caching can hide the route. If php artisan route:list doesn't show the logviewer route after install, run php artisan route:clear and php artisan config:clear, then re-check config/app.php for the provider registration. Cached routes and cached config are the two most common reasons the route 404s.
Config key names drift between versions. The published config/logviewer.php is the authority for your installed version — don't trust blog posts or the README for the middleware/prefix key names. If middleware silently has no effect, confirm you're editing the key the provider actually reads, not a leftover from a different version's stub.
Don't build on it. The extension points are: config (route + middleware), the published view, and the facade. There is no driver abstraction, no event hook, no query interface. If you need search, retention, download-as-file, redaction, or an audit trail of who viewed what, you're writing it yourself inside the Blade template — at which point you've done most of the work of building the capability natively. That's the honest decision point: 2–4 hours to stand this up behind proper middleware, versus a short sprint to do the same thing in your own admin panel with RBAC and audit baked in. For a legacy Laravel 5 monolith that just needs a stopgap, the package is fine. For anything else, it's a spec, not a dependency.
Keep a runbook entry. The meaningful ramp-up isn't for engineers (install, register, browse — five minutes); it's the operational guidance around which environments are permitted, what the middleware guarantees, and why it must never be exposed publicly. Put that on one page next to the config, because the package's zero-configuration default is explicitly not a safe default.
How can I help you explore Laravel packages today?