kabbouchi/laravel-ward
Ward is a lightweight Laravel package from Kabbouchi that puts a simple guard in front of your application, making it easy to restrict access, protect staging sites, and gate features with minimal configuration and sensible defaults.
Laravel Ward is a lightweight, self-hosted log dashboard: it reads the files in storage/logs, parses Monolog entries into timestamp / environment / level / message / context / stack trace, and serves them through a browsable web UI with filtering, search, and log clearing.
Minimal setup:
composer require kabbouchi/laravel-ward
php artisan vendor:publish --provider="Kabbouchi\Ward\WardServiceProvider"
The service provider is auto-discovered; the publish step gives you config/ward.php. Check the actual publish tag in the package README (--tag=ward-config is the typical convention) if the provider publish above lists multiple groups.
Then:
php artisan route:list | grep -i ward # confirm the registered route/prefix
and open the dashboard at the configured path (usually /ward). If the page is blank rather than a 404, the route is working and the frontend assets are the problem — see Gotchas.
Where to look first
config/ward.php — path/prefix, enabled flag, middleware stack, and any authorization callback. This is the only file you should need to touch for a normal install.storage/logs/ — the dashboard is a view over these files. Confirm laravel.log (or your daily files) exists and is readable by the PHP process.php artisan route:list — verifies registration and shows which middleware protects the dashboard.First use case
Reproduce a failure locally, open /ward, filter level to ERROR, and click the entry. This is the package's core value: you get the full stack trace, the request context, and the surrounding entries without tail -f in a second terminal or SSH access to a staging box. Compared with Telescope, Ward is retrospective — it works on logs that survived a crash, a queue worker, or a container restart, which is exactly when Telescope's pruned database rows are least useful.
Gate the dashboard by environment and middleware. The default middleware is usually just web, which is fine locally and wrong everywhere else. The cheapest safe pattern:
// config/ward.php
'enabled' => env('WARD_ENABLED', app()->environment('local')),
'middleware' => ['web', 'auth'],
For staging, add a gate in AuthServiceProvider and reference it from a small middleware, or use an IP allowlist at the proxy. Never rely on an unguessable path as the only control.
Route the dashboard next to your existing debugging tools. A practical split in day-to-day work: Telescope for "what did this request do," Ward for "what did this process write and when." Ward remains useful for queue workers, scheduled commands, and php artisan runs that never produce an HTTP request Telescope can record.
Make your logs searchable, because Ward's search is substring-based. The dashboard is only as good as your log lines. Add correlation context where it matters:
Log::withContext([
'request_id' => $request->header('X-Request-Id'),
'user_id' => auth()->id(),
'job' => static::class,
]);
Then searching the request ID or job class in Ward takes you straight to the failure. This is the single highest-leverage integration: it turns a log viewer into a tracing tool.
Daily logs work naturally. Ward scans the log directory, so LOG_CHANNEL=daily gives you a file list you can move between. If you use a single channel, keep an eye on file size (see Gotchas) and truncate deliberately rather than letting it grow to hundreds of megabytes.
Clearing logs as part of a workflow. The clear/delete action is the fastest way to reset noise between reproduction attempts: reproduce → read → clear → reproduce again. For automated tests, do not depend on the dashboard; truncate the file in test setup (file_put_contents(storage_path('logs/laravel.log'), '')) or use a dedicated test log channel so your real logs stay intact.
Custom channels. If you write to a non-default path, confirm Ward's configured log path (or directory scanner) points at it. Channels writing outside storage/logs will silently be invisible otherwise. Laravel's LOG_STACK in 9+ can fan out to multiple channels; Ward shows the resulting files individually, so make sure you are reading the channel that actually receives the entry.
Deployment checklist. Publish config and any public assets during deploy, run php artisan config:cache after publishing, and run php artisan route:cache only after confirming the package's routes are cacheable. In Docker, mount storage/logs as a volume — otherwise the container restart that motivated you to check the logs also deletes them.
Extension points. The practical customization surface is: the config path/prefix, the middleware stack, any authorization hook the config exposes, and published views for branding or removing destructive buttons. If you need a different parser (JSON logs, custom LineFormatter), that is where you will hit the package's limits — plan to fork rather than fight it.
enabled off in production unless it sits behind real authentication.php-fpm/www-data writes the log; your browser session reads it through PHP, so the PHP user needs read permission on the file and read/execute on the directory. Conversely, if the web user can write, the clear button works — verify that deliberately rather than discovering it by accident.LOG_DAILY_DAYS), truncate with : > storage/logs/laravel.log, and treat the dashboard as a tool for recent entries, not archival search.JsonFormatter or a LineFormatter with a different format string will produce entries the parser cannot split into level/message/context. If you need structured logs, run a separate channel for the dashboard.route:list shows the route but the browser renders nothing, the frontend assets were not published or the build manifest is missing. Publish assets as part of deploy; do not assume a dev server is running in production.config:cache and route:cache can make a newly published ward.php appear ignored. Clear caches (php artisan optimize:clear) before debugging the package itself.illuminate/* constraint in composer.json; on Laravel 11/12 the resolver may refuse to install. Options: --with-all-dependencies, a constraint override in your root composer.json, or a small fork. Given the package's scope — a route, a parser, and a UI over files — forking is realistic if it becomes load-bearing.config('app.timezone') against the timestamps in the raw file.How can I help you explore Laravel packages today?