Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Log Driver Laravel Package

juststeveking/eloquent-log-driver

A Laravel logging driver that stores your application logs in the database through Eloquent, letting you query, filter, and manage log entries like any other model instead of writing them to flat files.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (10 minutes).

composer require juststeveking/eloquent-log-driver
php artisan vendor:publish --tag=eloquent-log-driver   # config + migration
php artisan migrate

Add a channel in config/logging.php:

'channels' => [
    'eloquent' => [
        'driver'  => 'eloquent',
        'level'   => 'info',
    ],
],

Then log through it explicitly — do not make it the default channel:

Log::channel('eloquent')->warning('Stripe webhook signature mismatch', [
    'event_id' => $event->id,
    'tenant'   => tenant()->id,
]);

Where to look first. The whole package is a handful of files under vendor/juststeveking/eloquent-log-driver/src — a ServiceProvider that calls Log::extend('eloquent', ...), a Monolog handler that persists records, the Eloquent model, and the migration stub. Read the handler first: it tells you exactly which columns are populated, whether writes are buffered, and who is responsible for the context JSON. Read the published migration second, because that is the schema you actually own. Do not read the README as ground truth for your stack — verify against the code.

First use case. A secondary, non-critical channel for events you want to SELECT rather than grep: failed webhook deliveries, payment-provider callbacks, auth anomalies, scheduled-command outcomes. This is the sweet spot — low volume, high value when queryable, and nowhere near the throughput where synchronous INSERTs hurt.

First day checklist:

  1. Confirm the handler instantiates on your Laravel/Monolog versions (see Gotchas — this is the most likely blocker).
  2. Add it to a stack alongside single so file logging stays primary.
  3. Emit one record per level and confirm rows land.
  4. Query them from tinker before building anything on top.

Implementation Patterns

Never let it be the default channel. Route it explicitly, or compose it into a stack where it sits second:

'stack' => [
    'driver'   => 'stack',
    'channels' => ['single', 'eloquent'],
    'ignore_exceptions' => false,
],

'webhooks' => [
    'driver'   => 'stack',
    'channels' => ['single', 'eloquent'],
],

Now Log::channel('webhooks')->info(...) gives you greppable files in dev/CI and queryable rows in staging/prod, and a database outage degrades rather than destroys logging.

Use a dedicated channel per domain, not per level. webhooks, payments, auth, scheduler. The channel name becomes your primary filter in the log viewer and your primary index target. Filtering by domain in SQL beats grepping a message column every time.

Attach queryable context rather than encoding it in the message. The context array is what makes this package more useful than a file:

Log::channel('eloquent')->error('Payment provider callback failed', [
    'provider'    => 'stripe',
    'event_id'    => $event->id,
    'tenant_id'   => tenant()->id,
    'user_id'     => optional(auth()->user())->id,
    'http_status' => $response->status(),
]);

If the package does not promote these into columns, add a migration and a custom model (see below) — a JSON context column is only queryable if your database supports JSON indexing, and even then it is slower than a real column.

Swap in your own model and schema. This is the highest-leverage customization and it is cheap. Publish the migration, then edit it before running: add tenant_id, user_id, request_id as indexed columns, pick TEXT for message and JSON for context, and add a composite index on (channel, created_at) plus (level, created_at). Then point the config at your model:

// config/eloquent-log-driver.php (check the published stub for the real key)
'model' => App\Models\LogEntry::class,

Your model can then carry scopes the package will never have:

class LogEntry extends Model
{
    protected $guarded = [];

    protected $casts = ['context' => 'array', 'created_at' => 'datetime'];

    public function scopeForChannel($query, string $channel)
    {
        return $query->where('channel', $channel);
    }

    public function scopeRecent($query, int $hours = 24)
    {
        return $query->where('created_at', '>=', now()->subHours($hours));
    }

    public function scopeFailures($query)
    {
        return $query->whereIn('level', ['error', 'critical', 'alert', 'emergency']);
    }
}
LogEntry::forChannel('webhooks')->failures()->recent(6)->latest()->paginate(50);

That single scope chain is your support console. It is also the entire justification for the package: an admin screen where support staff filter a tenant's failed webhook deliveries without SSH access.

Retention from day one. The package ships none. Add a pruning command and schedule it:

// app/Console/Commands/PruneLogs.php
LogEntry::where('created_at', '<', now()->subDays(30))->delete();

Chunk it (->chunkById(5000, fn ($rows) => ...)) so you do not hold a table lock on a large delete. If volume grows, delete by created_at range in a loop, or partition the table by month. Build this before production, not after the table hits a million rows.

Redact before persistence, not after. A database log table with a JSON context column will accumulate tokens, cookies, Authorization headers, and full request payloads the first time someone logs $request->all(). Add a Monolog processor that runs before the handler and scrubs known-sensitive keys:

class RedactSensitiveData
{
    private const KEYS = ['password', 'token', 'authorization', 'cookie', 'secret', 'card'];

    public function __invoke(array $record): array
    {
        $record['context'] = $this->scrub($record['context']);
        return $record;
    }

    private function scrub(array $data): array
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = $this->scrub($value);
            } elseif (in_array(strtolower((string) $key), self::KEYS, true)) {
                $data[$key] = '[redacted]';
            }
        }
        return $data;
    }
}

Register it in the channel's handler configuration (or push it onto the logger obtained via Log::channel('eloquent')). Whichever route, assert it with a test.

Buffer or queue writes if latency matters. The handler writes synchronously per record — one round trip per log line, on the request or worker path. For anything beyond low volume, wrap or replace the write path with a queued job or a batched flush on terminate. A simple approach that keeps the package intact: point the channel at a custom handler you register yourself that pushes to a queue, and let the job insert. If that sounds like more work than the package is worth, that is a reasonable conclusion — see Gotchas.

Guard the recursion. A database-backed logger is logging to the same failure domain it is trying to report on. Wrap the handler so a failed INSERT falls back to a file channel instead of throwing:

try {
    $handler->handle($record);
} catch (\Throwable $e) {
    error_log('[eloquent-log-driver] ' . $e->getMessage());
}

Uncaught, a dead database turns every log call into an exception, which Laravel logs, which calls the handler again — you lose diagnostics exactly during the incident you need them.

Testing. Assert against the database, not the facade, because the point of this channel is persistence:

public function test_failed_webhook_is_persisted()
{
    LogEntry::truncate();

    $this->postJson('/webhooks/stripe', [...])->assertOk();

    $this->assertDatabaseHas('logs', [
        'channel' => 'webhooks',
        'level'   => 'error',
    ]);
}

Keep a trivial smoke test that emits one record per level through every configured channel. That test is what catches a framework upgrade silently breaking the handler — the failure mode otherwise is "we stopped receiving logs three weeks ago and nobody noticed."

Gotchas and Tips

The Monolog 2 → 3 signature break is the most likely integration blocker. This package last shipped in November 2020, targeting Laravel 8 / Monolog 2, where handlers implement write(array $record). Monolog 3 (Laravel 9+) requires write(LogRecord $record): void. On a current stack, expect a hard failure at channel instantiation or first write. Verify in a one-day spike before planning anything around it. The fix is mechanical:

// before
protected function write(array $record): void

// after
protected function write(LogRecord $record): void
{
    // ...
}

...but it requires forking, and once you are forking you should ask whether reimplementing ~150 lines yourself is cheaper than maintaining a fork. It usually is.

Vendor or fork it from day one. Seven stars, zero dependents, single maintainer, no release since 2020. There is no evidence anyone is running this on Laravel 10/11/12, no downstream forks carrying compatibility patches, and no support channel. Do not list it as a direct Composer dependency you expect to upgrade; copy the handler into app/Logging under your own namespace, own the migration, and treat upstream as a reference implementation. The architectural idea — a custom channel registered via Log::extend — is sound and worth keeping. The dependency is not.

Transactions roll back your logs. If the handler shares the default connection and you log inside a DB::transaction, the log rows vanish on rollback. That is arguably correct for audit-style events and wrong for diagnostics ("we rolled back because of X" is precisely what you want to keep). Use a separate connection for the log channel if this matters.

Contention and connection pressure. The channel writes to your primary database — same CPU, same disk, same connection pool as business data. Under load you are adding write amplification to the contended resource. At minimum, use a dedicated connection, and consider a read replica's write endpoint if you have one.

Unbounded growth is the default. Row-per-record with no retention is a table that grows forever. Index channel, level, and created_at — never message or context (full-text on a JSON blob will not do what you want). Decide the retention window before production, not after the first disk alert.

PII leaks into the table silently. Nothing here redacts. Anything you pass as context — request payloads, headers, user objects — is persisted and queryable by anyone with database access. Add the processor, and add an access-control story for the admin viewer if you build one: a log table is a data-exfiltration surface as much as a debugging aid.

Silent failure is the worst failure mode. If the handler breaks, logging stops and nothing alerts, because the thing that would alert is the logger. Cover this with the CI smoke test above and, optionally, an alert on "no rows written to logs in the last N minutes" if the channel is load-bearing for anything.

Config quirks. Publish the config before editing it — reading an unpublished package default is a common wasted hour. Check the exact config key names in the published stub against the package source; a typo in 'driver' => 'eloquent' fails at first write, not at boot. And run php artisan config:clear after edits if you use config:cache in local development.

Extension points worth knowing. Three are useful and all three are small: the configurable model class, the migration (edit before running, do not migrate then alter), and the Laravel Log::extend seam itself — you can register a variant driver that wraps the handler with redaction and a fallback without touching vendor code. Everything else the package does not offer (retention, alerting, search, RBAC, dashboards) you are building yourself, which is the honest reason to treat this as a two-day blueprint rather than an adoption.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo