sinemacula/laravel-log-database
A Laravel package that stores application log entries in a database table instead of flat files, making logs easier to query, filter, and retain. It plugs into Laravel's logging channels so existing Log calls work unchanged while records are persisted and browsable.
sinemacula/laravel-log-database adds a Monolog handler that persists Laravel log records to a database table instead of (or alongside) files, so you can query, filter, and join application logs with the rest of your data.
composer require sinemacula/laravel-log-database
php artisan vendor:publish --tag=log-database-config
php artisan vendor:publish --tag=log-database-migrations
php artisan migrate
config/logging.php that uses the package's driver, e.g.:
'channels' => [
'database' => [
'driver' => 'database',
'connection' => env('LOG_DB_CONNECTION'),
'table' => 'logs',
'level' => env('LOG_LEVEL', 'debug'),
],
],
LOG_CHANNEL=database (or add it to a stack) and write a log:
Log::info('Invoice generated', ['invoice_id' => $invoice->id]);
Where to look first: the published config file (connection, table name, minimum level, and any except/context options) and the generated migration (column types and indexes). Verify the driver name and config keys against the published file — the keys above are the conventional shape, not a guarantee.
First use case: replacing daily file logs on a multi-container or serverless deployment where local files are ephemeral or scattered, giving you a single queryable log table:
DB::table('logs')->where('level_name', 'error')->latest()->limit(50)->get();
Upgrading to v1.1.0: this release only widens the illuminate/* constraints to include Laravel 13. There are no API, config, or schema changes. If you were blocked on Laravel 13 with v1.0.0, composer update sinemacula/laravel-log-database is sufficient; no migration or republish is required.
Stack it, don't replace everything. The most common production setup is a stack that keeps a fallback file channel alive so logging survives a database outage:
'stack' => [
'driver' => 'stack',
'channels' => ['database', 'single'],
],
Ordering matters: the first channel that throws can prevent later ones from running.
Split by severity. Define two channels over the same driver — a low-threshold database channel for debug+, and an errors channel at error+ routed to a separate table or connection for alerting and retention policies:
Log::channel('errors')->error('Payment gateway timeout', ['gateway' => 'stripe']);
Per-tenant / per-service connections. Because the channel accepts a connection, you can route logs to a dedicated logging database (or a separate RDS/read-replica-friendly instance) without touching the app's default connection. Set LOG_DB_CONNECTION and leave application writes on the primary.
Query and display. Wrap the log table in your own Eloquent model or repository for admin panels and dashboards — a LogEntry model with casts for context (JSON → array) and created_at gives you Scout/filtering for free:
LogEntry::query()
->where('level_name', '>=', 'warning')
->where('context->invoice_id', $id)
->latest('created_at')
->paginate(50);
Retention as a scheduled command. Always pair the channel with pruning. A nightly model:prune on your log model, or a raw delete by date, keeps the table from becoming the largest object in the database:
LogEntry::where('created_at', '<', now()->subDays(30))->delete();
Correlating with requests and jobs. Push a request/job identifier into Monolog's shared context in a middleware or JobProcessing listener so every row is traceable; the package records Monolog's context, so this arrives without per-call boilerplate.
Testing. In phpunit.xml set LOG_CHANNEL=null (or array) so the suite doesn't hammer the log table; add a dedicated test that swaps the channel and asserts a row is written when you want coverage of your logging contract.
connection that isn't the one most likely to fail.DB::transaction() on the same connection, the log row rolls back with the business data — you lose exactly the records you need when debugging a failed transaction. Use a separate connection, or log after the transaction commits.level_name, created_at, and (if the driver supports it) the JSON context column, and schedule deletion from day one. Missing the created_at index is the usual cause of a slow admin log viewer.message and serialized context may be truncated or rejected depending on column type; long stack traces and large context arrays can silently lose the tail. Trim context at the call site (Log::info($msg, ['id' => $id])) rather than dumping whole models.daily files to a table frequently multiplies write load on the primary DB and can dwarf your table statistics. Benchmark at production LOG_LEVEL and consider debug writes only in non-production.LOG_LEVEL=warning on queue processes) rather than globally.LOG_CHANNEL/stack), level threshold allows the record, the migration ran on the configured connection, and the driver name in config/logging.php matches what the package registers. php artisan about and config('logging.channels.database') confirm what's resolved.except/context options); for anything beyond that — custom columns, enrichment — subclass or decorate the handler rather than editing vendor code, and note that config keys are not covered by semver the way the public API is. Re-diff the published config after minor upgrades.laravel/framework and monolog/monolog constraints resolve together — Monolog major bumps have historically been the real compatibility hurdle for logging packages, not the framework version.How can I help you explore Laravel packages today?