hesamrad/laravel-flashlight
Laravel Flashlight is a lightweight package for adding customizable flash notifications to Laravel apps. It offers a clean API for success, error, warning, and info messages, helping you give users clear, consistent feedback with minimal setup.
Install and publish.
composer require hesamrad/laravel-flashlight
php artisan vendor:publish --provider="HesamRad\Flashlight\FlashlightServiceProvider"
php artisan migrate
The publish step should give you a config/flashlight.php and, if the package ships a database driver, a migration for the request-log table. Run migrate only if you intend to persist logs — check the config first to see whether a file/database driver is selectable.
Where to look first. Open the published config before touching any code. Most request-logging packages expose the same three knobs, and they determine everything else you'll do:
Cross-reference the config keys against the middleware's handle() method in vendor/hesamrad/laravel-flashlight/src/ — with a package this small, the source is the documentation, and reading the middleware takes two minutes.
Register the middleware. Either globally (Laravel 11+ bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\HesamRad\Flashlight\Http\Middleware\LogRequest::class);
})
…or, better for a first outing, on a single route group so blast radius is small:
Route::middleware(['flashlight'])->prefix('api/v1')->group(function () {
// ...
});
First use case. Point it at one noisy or high-value surface — your public API, a webhook receiver, or an integration endpoint you're debugging with a partner — for a day. Then query the log table for 4xx/5xx rows and see whether you get enough context to reproduce a failure without SSH-ing into production. That single test tells you whether the package's default payload (method, path, status, IP, user agent, timing, headers/body — whatever it captures) is sufficient or whether you'll need to enrich it.
Scope by route group, not globally. Logging every request including health checks, asset 404s, and Horizon/queue traffic is the fastest way to make the table useless. Default to route-group attachment or a config exclusion list, and widen later if needed:
Route::middleware('flashlight')->group(base_path('routes/api.php'));
Capture the response, not just the request. The interesting part of a log entry is usually the status code and duration. If the package's middleware does its work before $next($request), you'll only have the request — in that case extend the middleware (or add a thin wrapper) that times around the handler:
$start = microtime(true);
$response = $next($request);
$duration = (microtime(true) - $start) * 1000;
// persist $response->getStatusCode(), $duration, ...
Subclassing the shipped middleware and swapping the binding in a service provider is the cleanest extension point and keeps you upgrade-safe-ish.
Order the middleware deliberately. Put the logger after StartSession and your auth middleware if you want user_id populated, and before anything that can throw early (throttling, signature verification) only if you care about rejected requests. For most teams the useful position is: session → auth → flashlight → controller. Rejections by earlier middleware simply won't be logged, which is usually fine and much cheaper.
Enrich with app context. Associate entries with your tenant/user so the log is queryable in a multi-tenant admin:
// in a subclassed middleware, before persisting
$payload['user_id'] = $request->user()?->id;
$payload['tenant_id'] = tenant('id');
$payload['request_id'] = $request->header('X-Request-Id');
A request_id you also emit in your exception handler is the single highest-value addition — it lets you jump from a Sentry/Bugsnag event to the exact request row.
Build the read side immediately. A log nobody queries is a disk leak. In practice you want:
status >= 400 and a date range,->failed(), ->forRoute('...'), ->today()),created_at, status, and whatever column holds the path — add these in a follow-up migration if the package's default migration only indexes the primary key.Retention from day one. Either schedule pruning or truncate:
// routes/console.php or a scheduled command
Schedule::call(fn () => FlashlightLog::where('created_at', '<', now()->subDays(14))->delete())->daily();
Prefer chunked deletes (->delete() in chunkById batches) once the table is large, and consider OPTIMIZE TABLE / VACUUM depending on the driver.
Testing. If the middleware is global, your test suite will write a row per request and slow down. Disable it in phpunit.xml via the config env toggle, or don't register it globally at all. To assert on logging behavior itself:
$this->postJson('/api/webhooks/stripe', $payload);
$this->assertDatabaseHas('flashlight_logs', ['status' => 422]);
Operational loop. The realistic day-to-day workflow is: customer reports an error → search logs by path + time window + status → read the stored body/headers → reproduce. Make sure the stored body is JSON-decoded on read (cast the column to array/json in your own model or accessor) so you're not eyeballing escaped strings.
Redact aggressively before you ship. Request bodies routinely contain passwords, tokens, and PII, and headers contain Authorization and cookies. Check whether the package has a redaction/except-keys config; if not, add one in your subclass. This is the number one reason teams rip out request loggers. Also decide explicitly whether you're logging bodies at all — headers + status + timing often give 80% of the value at 5% of the risk.
Never serialize uploaded files. $request->all() on a multipart request can produce enormous or unserializable payloads. Strip UploadedFile instances and cap body size (e.g., truncate to 8–16 KB) before persisting.
Synchronous writes cost latency on every request. A DB insert per request is fine for low-traffic apps and noticeable under load. Options, in order of preference: sample (log 1 in N successes, 100% of errors), queue the write, or switch to the file/log-channel driver if the package supports one. Measure before optimizing, but know the knob exists.
config:cache will bite you. After changing config/flashlight.php, run php artisan config:clear locally and redeploy with a fresh config:cache — a stale cached config silently keeps the old exclusions.
Middleware registration differs by Laravel version. Laravel 11+ uses bootstrap/app.php; older apps use app/Http/Kernel.php ($middleware for global, $middlewareAliases for named). If the package's README predates Laravel 11, translate the snippet yourself.
Octane and long-running workers. Confirm the middleware holds no per-request state on a singleton. If the service provider binds the logger as a singleton, request data can leak between requests under Octane. Test with octane:start before deploying behind it.
Growth is the silent failure mode. Unindexed tables plus months of traffic produce slow admin queries and slow pruning. Set retention and indexes when you adopt the package, not after.
Migration ordering on existing databases. If you publish migrations into an app that already has data, and the package's migration shares a table name with a previous attempt, you'll get duplicate-table errors. Publish, read the migration filename/table name, and rename if there's a collision.
Small-package risk management. With ~14 stars and no dependents, treat this as vendored code rather than infrastructure. Concretely: pin an exact version in composer.json, read the middleware and migration before adopting, and keep your extension in a subclass so you can fork (MIT license permits it) if upstream goes quiet. The surface area is small enough that forking is genuinely cheap — the main cost of this package going unmaintained is a Laravel-major upgrade breaking the service provider or middleware signature, which is a 10-line fix.
Prefer Laravel's built-ins where they suffice. For plain debugging, Log::withContext() plus a single log line in a middleware covers a lot of ground and adds no dependency. Adopt Flashlight when you specifically want queryable, structured, retained request records — an audit/debugging dataset rather than log lines.
How can I help you explore Laravel packages today?