givanov95/laravel-activity-log
Laravel package for recording activity logs and audit trails. Track user actions and model changes to see who did what and when, with a simple, configurable logging layer for your application.
Architecture fit. This is an in-application audit/activity log that writes into the same relational database as the host app, on the caller's connection and inside the caller's transaction. That is the right default for a Laravel monolith or modular monolith: entries commit or roll back with the business change, no message broker, no outbox, no second datastore to operate. The ActivityLog::record() builder is expressive enough to represent the audit cases most TPMs actually get asked for by compliance and support: who did it (Actor::user/model/guest/system, with role and a stored label), what it was done to (subject with stored label), what else was touched (related models), what changed (Changes), and free-form properties. The stored-label design is the important detail — entries stay readable after the subject is renamed or deleted, which is exactly where naive subject_id-only audit tables fail in production.
Integration feasibility. Low-to-moderate. There is no migration/ETL: it is a new table plus a handful of call sites. The Changes::fromModel() helper removes the tedious part of writing entries by hand (attribute diffing), so instrumenting a first workflow is hours, not weeks. The friction is not technical but organisational: someone must decide which actions are auditable and enforce it in code review, otherwise coverage drifts and the log becomes untrustworthy precisely where it matters (permissions, billing, exports, deletions).
Technical risk. Three items deserve attention before sign-off:
Changes::fromModel() throws LogicException on Laravel 11 because it needs Model::getPrevious() from Laravel 12; the documented fallback is snapshot()->diff(). This is a runtime failure, not a compile-time one, so it will surface as a 500 in whichever flow first exercises it. If the fleet is mixed 11/12, this must be a written rule with a lint/test guard, not tribal knowledge.scrub(), the hidden/encrypted-attribute masking, and activity-log:prune are genuinely useful, but retention_days is off by default and masking only covers attributes you remembered to configure. An unmasked or unpruned log is a GDPR liability sitting in your primary database. This is the package's largest organisational risk and it has a default that fails open.Key questions.
properties allowed to hold PII at all, or is it contractually free of it?scrub() an erasure mechanism we are willing to defend as satisfying a data-subject request?Stack fit. Assumes Laravel with Eloquent and a relational database; the append-only ActivityLogEntry model and the scope set (inScope, forSubject, involving, byActor, byRole, action, between) map cleanly onto the questions support and compliance ask ("what happened to this account", "what did this admin do last week"). Batch UUID per request or job gives free correlation across entries produced by one operation — valuable for reconstructing a multi-step workflow in an incident review. No queue, no cache dependency, no external service: it fits an existing Laravel deployment without new infrastructure, which is the main reason to prefer it over an event-stream or warehouse-based audit approach at this stage.
Migration path. Greenfield table — generate and run the migration, no backfill. Historical auditability is not provided; the log starts at deployment. If prior history exists in application tables (updated_at, status columns, existing log tables), plan a one-off export as a separate exercise rather than trying to synthesise entries. Do not pretend the new log covers the pre-deployment period in any compliance statement.
Compatibility. Target Laravel 12 as the primary path so Changes::fromModel() is available. On Laravel 11, standardise on snapshot()->diff() and add a static check or test that fails if fromModel() appears in code — the exception is only raised at runtime in the affected flow, which is the worst time to find it. Verify the encrypted-cast masking behaviour against the actual casts in use before relying on it. Confirm the database supports the indexing the scopes need at expected volume.
Sequencing.
retention_days) and the prune schedule before the first entry is written — the default is off, and turning it on later means retrofitting a policy onto accumulated data.properties PII rule up front, not after the first incident.->by() is used wherever the signed-in user is not the true actor (jobs, console commands, impersonation, webhooks).Maintenance. Small and self-contained: one table, one model, one command, one config file. The maintenance burden is in the policy surface, not the code — retention, masking, and actor attribution need an owner who reviews them when the schema or the org chart changes. Treat the config as a governed artefact, not an app default.
Support. High leverage. Stored actor and subject labels mean support can answer "who changed this and when" without joins to mutable tables or reconstructing state. The scopes cover the majority of ad-hoc queries, which reduces the temptation to grant broad database access to support staff. Batch UUID is the difference between a support ticket resolved in minutes and one escalated to engineering.
Scaling. Audit tables grow monotonically and faster than the entities they describe; append-only is good for integrity and unforgiving for volume. Mitigations exist in the package (retention, prune) but are opt-in. Expect write amplification on hot paths — every instrumented write adds at least one row and one index entry within the caller's transaction. Watch for: table and index bloat, properties/changes JSON growth, and long-running reporting queries competing with OLTP traffic. Plan retention and, if volume warrants, an archival destination before the table reaches a size where pruning becomes a maintenance window.
Failure modes.
LogicException on Laravel 11 in the first flow to call Changes::fromModel().retention_days ships off and nobody owns it.scrub() over- or under-reach — verify in staging that anonymisation hits exactly the subject's fields and the actor IP, and that it satisfies the erasure obligation you claim it does.Ramp-up. A developer can write a first entry in an hour; writing a correct one — right actor, right scope, right masking, right changes semantics — takes a review cycle. Budget half a day of enablement covering the actor resolver, the Laravel 11 fallback, masking configuration, and the rollback semantics, plus a written standard for what must be logged. The failure mode to design against is a team that adopts the API quickly, skips the policy decisions, and discovers both the retention gap and the PII gap at the first audit.
How can I help you explore Laravel packages today?