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

Yammi Audit Log Laravel Laravel Package

romalytar/yammi-audit-log-laravel

Yammi Audit Log brings change history and execution tracing to Laravel apps: every change records who made it, who started it, and a correlation id linking the whole cascade. Global capture from the first migration, safe defaults, optional dashboard.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and first run

composer require romalytar/yammi-audit-log-laravel
php artisan migrate
php artisan audit-log:ui enable   # optional dashboard at /audit-log

There is no trait to add, no interface to implement, no model registration. Capture is global from the first migration — a single Eloquent listener picks up created / updated / deleted / restored on every model.

Where to look first

  1. php artisan vendor:publish --tag=audit-log-config — publish config/audit-log.php. You will live in this file; skim it end to end before writing any code, because most of the package's behaviour (capture mode, retention, async writes, integrity) is toggled here rather than in your domain code.
  2. docs/provenance.md — actor vs. origin vs. correlation id. This is the concept the whole package is built around; if you skim it, the dashboard columns will be meaningless.
  3. docs/configuration.md — the settings resolution order (DB row → config value → package default) matters when something behaves differently in staging vs. production.

Your first real use case

Pick the model that causes you the most "why did this change?" support tickets — usually Order, Invoice, Subscription, or a User. Do nothing to it, deploy the package to staging, exercise it once through your real UI (not tinker), then open /audit-log. That single round-trip will tell you more than the README, because you will see your own queue topology reflected in the chain tree, with your real job class names as actors.

Then deliberately test the case traditional audit logs fail: trigger a change via a queued job dispatched from an HTTP request, and confirm the dashboard shows actor = YourJob, origin = the logged-in user, and one correlation id spanning both. If that works, the package is worth keeping; if your app never has that shape, spatie/laravel-activitylog already covers you and you should stop here.

Sanity-check before rollout

AuditLog::fake();
$order->update(['status' => 'paid']);
AuditLog::assertRecorded(Order::class, $order->id, 'updated',
    fn ($r) => $r->diff()->field('status')?->new === 'paid');

This lets you write your first test before you have migrated production data, and gives you a feel for the diff() API you will use in debugging sessions.

Implementation Patterns

Async writes for hot models

The default synchronous write is fine for low-churn models. For anything written in bulk (imports, sync jobs, high-volume API endpoints), turn on AUDIT_LOG_WRITE_ASYNC=true. Two consequences you must plan for:

  • The audit row lands after the write, so a test asserting immediately after ->update() may need Queue::fake() discipline or a real queue in CI.
  • Your audit writes now consume queue workers. If you run a single low-priority queue, audit writes will starve behind business jobs (or vice versa). Give them a dedicated queue name so ops can scale them independently.

Dedicated connection for the audit table

In multi-tenant or heavily-sharded setups, move audit_log to its own connection via the config. This keeps audit writes off the same pool as your tenant traffic and lets you point audit storage at cheaper hardware. The gotcha: the connection must be available from every process that writes — including queue workers and scheduler runs — so verify that connection is in your supervisor environment.

Recording Query Builder and raw SQL changes yourself

Eloquent events only. If you use DB::table('orders')->update(...), bulk upserts, or ->increment(), you get nothing. Wrap those paths explicitly:

DB::transaction(function () use ($ids) {
    DB::table('orders')->whereIn('id', $ids)->update(['status' => 'archived']);
    foreach (Order::whereIn('id', $ids)->get() as $order) {
        AuditLog::record($order, 'archived', ['status' => 'archived']);
    }
});

Adopt one pattern early: a small AuditRecorder service in your app with a recordBulk(string $model, array $ids, string $event, array $changes) helper, so this is not repeated ad hoc across the codebase.

Correlation ids across your own dispatch boundaries

The package propagates correlation through Laravel's queue. It cannot do that for anything you hand off outside Laravel — a webhook to a partner, a message to an external broker, a cron-invoked shell script. If those loops come back into your app, capture the correlation id yourself at dispatch time and re-attach it on re-entry, otherwise the chain tree shows two disconnected cascades where you know they are actually one.

Testing discipline

Use AuditLog::fake() in feature tests for anything that touches audited models — it keeps your assertions fast and stops audit rows polluting fixtures. But keep at least one non-fake integration test per critical workflow that actually writes the audit row, because fake() bypasses the redaction pipeline, the diff builder and the actor resolution — the exact things you want to check actually work in production. Common bug class: a test asserts diff()->field('status') and passes under fake() while production redacts status because a name collides with the default secrets list.

Actor attribution for token and client clients

If you expose API endpoints authenticated via Sanctum or Passport, the actor string includes the client ("Jane Doe via mobile-app"). This is a large win for incident investigation on mobile backends, but test it explicitly per-guard — misconfigured guards yield "system" as the actor and silently lose the attribution.

Read path

For programmatic access, use the JSON API rather than querying the table directly: it validates and bounds inputs (one-year range cap, 10k export limit). Direct SQL against audit_log is a last resort — the changed-field index table means field('status') searches are fast via the package's query DSL but painful if you hand-roll JSON extraction in raw SQL.

Gotchas and Tips

No trait means no opt-out per model

Global capture is the headline feature, but it also means every model gets audited — including join models, cache-like tables, and anything churning heavily. A remember_token refresh, a last_seen_at heartbeat column, or a views_count increment will each generate an audit row. Before you complain about volume in production, go straight to the capture policy (ignore fields, conditional capture, sampling for high-churn models). It is documented but not part of the default install. Set this up on day one, not on day thirty.

Retention is on by default — 180 days — and audit data is PII

This is correct behaviour but a footgun for compliance teams who assume "audit log" means "kept forever". Decide explicitly: extend retention, archive elsewhere, or place a legal hold on relevant subjects. Legal holds are exempt from pruning, so they only help if you applied them before the retention window passed. Put the decision in writing.

Fail-open cuts both ways

The write path is fail-open: if the audit insert fails, your business operation succeeds and the error is logged. That is the right trade-off for production, but it means silent gaps in the trail. The capture-health banner and nav badge (v2.2) are your only signal that this is happening — check them after any infrastructure change (new DB connection, queue migration, read-replica promotion). For paths where you require an audit row (financial events, permission changes), wrap both the business write and AuditLog::record() in your own DB::transaction and fail closed.

Integrity is not enabled by default

If you have regulatory requirements for tamper-evident logs, AUDIT_LOG_INTEGRITY=true costs a chain-head select per write. Do not enable it in production on a busy system without measuring first — the README flags it, but the failure mode is "audit writes slow down and start failing open", not "the app breaks visibly".

fake() masks production behaviour

Repeating from above because it is the most common source of "works in tests, broken in prod": AuditLog::fake() bypasses redaction, diff-building, label snapshots and actor resolution. Anything you assert about what the record contains rather than that a record exists should have at least one integration test against the real pipeline.

UI off by default, and the API fails closed

Both are correct defaults but both will bite you during setup. The UI needs php artisan audit-log:ui enable, and the API refuses to register without an auth guard in its middleware — so a blank /audit-log or a 500 from the API usually means "a deliberate safety feature, not a bug". Check the config before you start debugging the code.

Settings resolution order

DB row → config value → package default. Changing config/audit-log.php and redeploying does nothing if someone edited the value in the Settings UI — the DB row wins. This is a genuine operational hazard on teams where more than one person has UI access. Either restrict Settings UI access to the platform team, or document loudly that config changes may not apply.

Eloquent $model->getChanges() is the underlying diff

The audit diff is built from Eloquent's change tracking, so fields you do not touch do not appear. Two consequences: a ->save() with no dirty attributes produces no record (correct), and a model with a boot() hook that mutates attributes produces diff entries you may not have expected. When a diff looks wrong, check your model events/observers before blaming the package.

Extend it, do not fork it

Three extension points worth knowing before you write custom code:

  1. AuditLog::record() for manual events — use it for Query Builder paths and domain events that are not model changes (User logged in, Payment refunded).
  2. Capture policy for field-level or conditional control — cheaper and more maintainable than if statements in your models.
  3. The event_version schema contract — if you ever change what a given event means, this is how you keep old and new records interpretable side by side rather than silently reinterpreting history.

When the package is the wrong tool

If your changes do not flow through queues and there is no workflow you need to reconstruct, the provenance chain is dead weight, and spatie/laravel-activitylog is a lighter, more widely-adopted fit. Equally, this is not event sourcing — the time machine is read-only forensics for investigation, not a system you rebuild state from. Do not build replay logic on top of it; the docs call this out as a non-goal and the invariant that makes it safe to install (additive, read-only, fails open) depends on respecting that boundary.

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