Skip to content

Add v2 major release audit infrastructure - #9

Draft
iamfarhad wants to merge 44 commits into
feature/audit-advanced-apisfrom
feature/v2-major-release-infrastructure
Draft

Add v2 major release audit infrastructure#9
iamfarhad wants to merge 44 commits into
feature/audit-advanced-apisfrom
feature/v2-major-release-infrastructure

Conversation

@iamfarhad

@iamfarhad iamfarhad commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

This PR builds the v2 major-release infrastructure on top of feature/audit-advanced-apis / PR #8.

Included:

  • Production migration tooling:
    • audit:make-migration
    • audit:migrate
    • AuditMigrationGenerator
  • True driver-level batch insert support when AUDIT_BATCH_ENABLED=true, queueing is disabled, and hash-chain mode is off
  • Multi-tenancy support:
    • TenantResolverInterface
    • default TenantResolver
    • optional tenant_type / tenant_id payload columns
    • forTenant() audit scope
  • Append-only Eloquent protection for audit rows via AUDIT_APPEND_ONLY=true
  • Relationship auditing helper:
    • RelationshipAuditor::attached()
    • RelationshipAuditor::detached()
    • RelationshipAuditor::synced()
  • Operational commands:
    • audit:config-check
    • audit:doctor
    • audit:stats
    • audit:timeline
    • audit:diff
    • audit:verify
    • audit:partition
    • audit:upgrade
  • Config validation service
  • Authorization helper service
  • Computed change-set support via optional changes column
  • Snapshot service
  • API resources:
    • AuditLogResource
    • AuditTimelineResource
    • AuditDiffResource
  • Audit lifecycle events:
    • AuditCreating
    • AuditCreated
    • AuditVerificationFailed
  • Rich top-level README coverage for all v2 features
  • Expanded v2 documentation in docs/v2-major-release.md

Excluded by request:

  • Filament plugin
  • Nova / Backpack integrations
  • New external storage drivers such as ClickHouse, OpenSearch, S3, and webhook drivers

Documentation updates

  • Reworked README.md into a full feature guide covering v2 installation, config, production table management, search/analytics/timeline/diff, restore safety, integrity/security, redaction, tenancy, batch inserts, relationship auditing, snapshots, API resources, commands, retention, events, upgrades, comparison, troubleshooting, and docs links.
  • Expanded docs/v2-major-release.md into a deeper production guide with migration examples, PostgreSQL/MySQL upgrade examples, batch limitations, tenancy resolver examples, hash-chain behavior, relationship auditing, restore safety, API resources, operational commands, partition guidance, events, presets, production checklist, troubleshooting, and upgrade order.

Hardening / review fixes

  • audit:migrate now explicitly creates storage instead of relying on ensureStorageExists(), so it works even when runtime auto-migration is disabled.
  • Native batch inserts are disabled automatically when hash-chain mode is enabled, preserving sequential hash-chain integrity.
  • Queued audit writes now dispatch AuditCreated from the queued job after storage succeeds instead of dispatching too early.
  • audit:doctor and audit:upgrade use the configured audit connection rather than the app default database connection.
  • Restore/rollback now uses AuditAuthorization, supports fillable filtering, and can save without creating a new audit row when AUDIT_RESTORE_AUDIT=false.
  • Hash-chain verification dispatches AuditVerificationFailed when verification fails.

Notes

This is intentionally a stacked PR on top of PR #8 so the advanced audit APIs can stay reviewable and mergeable independently.

The v2 features are mostly opt-in through config flags. Existing tables are protected by payload filtering, so optional v2 columns are only written when present.

The partition command is guidance-only and does not run risky database partition DDL automatically.

Relationship auditing is explicit via RelationshipAuditor; it does not monkey-patch Laravel relation methods.

Verification

This PR is still draft because GitHub Actions do not run for this stacked PR target: the repository workflows are configured for pull requests into main / master, while this PR targets feature/audit-advanced-apis.

Recommended after PR #8 is merged or this PR is retargeted to main:

composer install
composer test
composer pint:test

Then mark ready only after the full matrix is green.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the v2 infrastructure for the Laravel Audit Log package, incorporating significant features such as native batch insert support, multi-tenancy context, append-only security mode, and relationship auditing. It also adds a comprehensive set of Artisan commands for configuration validation, migrations, and health checks. Feedback identifies critical issues regarding database connection consistency in management commands, potential sensitive data exposure in the snapshot service, and performance bottlenecks related to schema filtering during batch operations and row counting for snapshots.

}

$table = $tables->resolve($entityClass);
$exists = Schema::connection(config('database.default'))->hasTable($table);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The health check uses the default database connection to verify the existence of audit tables. However, the audit logger can be configured to use a different connection via AUDIT_MYSQL_CONNECTION or AUDIT_PGSQL_CONNECTION. This check should use the connection defined in the audit logger configuration to be accurate.

Comment on lines +31 to +42
if (! Schema::hasTable($tableName)) {
$this->warn("Missing audit table [{$tableName}].");
continue;
}

$missing = array_values(array_filter([
'audit_hash',
'previous_hash',
'tenant_type',
'tenant_id',
'changes',
], fn (string $column): bool => ! Schema::hasColumn($tableName, $column)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the doctor command, this upgrade check defaults to the primary database connection. It should respect the connection configured for each audit table to correctly identify missing columns in multi-database setups.

entityId: $model->getKey(),
action: 'snapshot',
oldValues: null,
newValues: $model->getAttributes(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Using $model->getAttributes() directly will include all model fields in the snapshot, including sensitive data (e.g., passwords, tokens) that should be excluded or redacted. This bypasses the standard audit log field filtering logic. Please ensure that the snapshot only includes attributes that are configured to be audited.

$this->store($log);
$this->validateEntityType($log->getEntityType());
$this->ensureStorageExists($log->getEntityType());
$grouped[$this->getTableName($log->getEntityType())][] = $this->payloadForInsert($log);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The payloadForInsert method (which calls filterPayloadForTable) is executed for every row in a batch. filterPayloadForTable performs multiple Schema::hasColumn checks per row. While Laravel caches these results, iterating over the column list for every field in every row of a large batch (e.g., 500 rows) adds significant overhead. It is recommended to fetch the column list once per table before starting the batch loop.

$this->store($log);
$this->validateEntityType($log->getEntityType());
$this->ensureStorageExists($log->getEntityType());
$grouped[$this->getTableName($log->getEntityType())][] = $this->payloadForInsert($log);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The payloadForInsert method is executed for every row in a batch, triggering repeated schema filtering. To improve performance during batch inserts, consider fetching the column listing once per table and reusing it for all rows in the batch instead of relying on individual hasColumn checks inside the loop.

Comment on lines +21 to +24
$count = \iamfarhad\LaravelAuditLog\Models\EloquentAuditLog::forEntity($model::class)
->newQuery()
->where('entity_id', (string) $model->getKey())
->count();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing a count() on the audit table for every model update can become a major performance bottleneck as the audit log grows to millions of rows. Consider using a more efficient trigger for periodic snapshots, such as a dedicated counter or checking the ID of the last snapshot row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant