Audit · Reference
Laravel SDK
mybeast/audit-laravel writes your audit events to an outbox in your own transaction and ships them to Audit in order. For Laravel 12 and 13 on PHP 8.2 or later.
Install
composer require mybeast/audit-laravel
php artisan migrateLaravel finds the service provider by itself. migrate creates the outbox table. To change the config in a file rather than in .env, publish it to config/mybeast-audit.php:
php artisan vendor:publish --tag=mybeast-audit-configThe .env lines
Two lines send events, from the project in Audit (its setup guide, or Secrets when id made the secret):
MYBEAST_AUDIT_URL=https://services.my-beast.com/audit
MYBEAST_AUDIT_SECRET=sk_…Without them, or with a secret that isn't well-formed, events wait in the outbox. The rest are optional:
MYBEAST_AUDIT_ENABLED=true # false: check events, write nothing
MYBEAST_AUDIT_CONNECTION= # the connection of the actions you record
MYBEAST_AUDIT_OUTBOX_TABLE=audit_outbox # before the first migrate
MYBEAST_AUDIT_REQUEST_ID_HEADER=X-Request-IdConfiguration
| Key | Default | What it does |
|---|---|---|
enabledMYBEAST_AUDIT_ENABLED | true | false: Audit::record() still checks each event and throws on a wrong one, but writes nothing. For a checkout that should not write into the project's log. |
urlMYBEAST_AUDIT_URL | none | The service's address, https://services.my-beast.com/audit. |
secretMYBEAST_AUDIT_SECRET | none | The project's server secret, sk_…. Keep it on the server. |
request_id_headerMYBEAST_AUDIT_REQUEST_ID_HEADER | X-Request-Id | The request header an event's request id is read from when mybeast/logs-laravel gave none. Empty reads no header. |
outbox.connectionMYBEAST_AUDIT_CONNECTION | the default connection | The database connection of the actions you record. The outbox row joins their transaction only on the same connection. |
outbox.tableMYBEAST_AUDIT_OUTBOX_TABLE | audit_outbox | The outbox table's name. Change it before the first migrate. |
outbox.migrations | true | true: the SDK's migration runs with yours. false: publish it with vendor:publish --tag=mybeast-audit-migrations and run your copy. |
ship.schedule | true | true: the SDK adds audit:ship to your scheduler, every minute, on one server. |
ship.batch | 500 | Events in one request, 1 to 500. |
ship.max_batches | 20 | Requests in one run of audit:ship. What is left goes in the next run. |
ship.timeout | 10 | Seconds to wait for the service, 1 to 60. |
ship.settle_seconds | 2 | An event leaves once it is this many seconds old, so a transaction that committed late is not overtaken by one that committed first. |
A key that is missing or malformed reads as its default, never as an error when your app boots.
The outbox table
audit_outbox, or the name in outbox.table, on the connection in outbox.connection. A row goes once Audit has confirmed its event.
| Column | Holds |
|---|---|
id | The order events are sent in. |
event_id | The event's id, a UUID v7. Unique. |
payload | The event as JSON, exactly as it is sent. |
created_at | When it was recorded, UTC, to the millisecond. |
attempts, last_attempt_at, last_error | Tries that didn't get through, the last one and why. |
refused_at, refusal | Set when Audit refused the event, with its reason. The row then waits aside. |
To run the migration as one of your own, set outbox.migrations to false in the published config and publish the migration:
php artisan vendor:publish --tag=mybeast-audit-migrationsArtisan commands
| Command | What it does |
|---|---|
php artisan audit:ship | Sends the outbox to Audit, oldest first, and forgets each event Audit confirmed. The scheduler runs it every minute. |
php artisan audit:ship --status | Sends nothing. Says how many events wait, since when, the last error, and the events Audit refused with its reasons. |
php artisan audit:ship --retry-refused | Puts the refused events back in line, in their old place, then sends. Run it once you have fixed what Audit said. |
audit:ship ends with an error (exit code 1) when a run stops with events waiting, so your scheduler's monitoring sees it. It ends with 0 otherwise: also when another run holds the lock, and when the URL or the secret is missing, which it says. Send events to Audit says what each run does.
The Audit facade
MyBeast\Audit\Laravel\Facades\Audit:
record(…)checks the event, writes it to the outbox and returns its id. It throwsMyBeast\Audit\Laravel\Events\InvalidAuditEventfor an event Audit would refuse.fake()keeps events in memory from then on, and returns the fake with its assertions.ship()does whataudit:shipdoes, and returns a report:kept,duplicates,refused,stopped(why it stopped, or null),configured,busyandsent().status()returns what waits:waiting,refused,oldestandlast_error.
record()'s parameters, in order. The ones ending in a colon are best passed by name:
| Parameter | Type | What it is |
|---|---|---|
$action | string | What happened: customer.viewed. |
$actor | Actor | Actor::person($id), Actor::operator($id) or Actor::system($id). |
$target | ?Target | Target::of($type, $id) or Target::person($id). |
$context | array | Keyed, at most 4 levels and 8 KB as JSON. |
subject: | string|int|null | The person the data is about. |
account: | string|int|null | The account it happened in. |
reason: | ?string | The operator's reason. An email address in it becomes [e-mail removed]. |
at: | ?DateTimeInterface | When it happened, if not now. |
request: | ?string | The request's id, if not the one the SDK finds. |
Actors and targets are MyBeast\Audit\Laravel\Events\Actor and Target. Record events says what each part is for.
Testing with the fake
Audit::fake() keeps events in memory: nothing is written to the outbox and nothing is sent. A wrong event still throws, so a test catches it.
use MyBeast\Audit\Laravel\Facades\Audit;
public function test_support_sees_the_customer(): void
{
$audit = Audit::fake();
$this->actingAs($operator)->get("/customers/{$customer->id}")->assertOk();
$audit->assertRecorded('customer.viewed', fn (array $event) => $event['subject'] === $customer->user_id);
$audit->assertRecordedTimes('customer.viewed', 1);
$audit->assertNotRecorded('key.revealed');
}assertRecorded($action, $matching = null): at least one event of the action, matching the closure if you give one.assertRecordedTimes($action, $times): exactly that many.assertNotRecorded($action, $matching = null)andassertNothingRecorded().recorded($action = null, $matching = null)returns the events, each the array Audit would get: see The event.
Tests that don't fake it can set MYBEAST_AUDIT_ENABLED=false in phpunit.xml: events are checked and nothing is written.