Sailfish

Installing Sailfish

Grant the database user what it needs, require the package, migrate and narrow the gate. The first snapshot is taken within five minutes.

Sailfish is a Laravel package with its own routes, views and assets. Installing it takes a Composer registry, a migration and one service provider, and the scheduler you already run does the rest. It reads performance_schema over one of your application's database connections and stores its history in tables next to your own.

Requirements#

  • PHP 8.2 or newer
  • Laravel 12 or 13
  • MySQL 8.0
  • The Laravel scheduler running on at least one server (php artisan schedule:run every minute)
  • A database user that can SELECT from performance_schema

Database grants#

Sailfish uses one connection, both to read performance_schema and to store its sailfish_* tables. Its user needs SELECT on performance_schema. An application user granted only its own schema does not have that, and sailfish:collect names the grant it is missing rather than recording nothing.

GRANT SELECT ON performance_schema.* TO 'app'@'%';

-- Optional: index sizes
GRANT SELECT ON mysql.innodb_index_stats TO 'app'@'%';

-- Optional: InnoDB's history list length
GRANT PROCESS ON *.* TO 'app'@'%';

The two optional grants each add one figure. Without mysql.innodb_index_stats the dashboard shows no index sizes, and without PROCESS the history list chart and its event stay empty. A user with ALL on *.* already has all three.

If your application's user should not have these, point SAILFISH_DB_CONNECTION at a second connection to the same server whose user does. See configuration.

1. Authenticate Composer#

Sailfish is distributed through a private Composer registry. Your license key is issued through Anystack at checkout. Run this once on every machine that installs the package, including CI and build servers:

composer config --global --auth http-basic.sailfish.composer.sh \
  you@example.com \
  YOUR-LICENSE-KEY

The username is the email address the license belongs to and the key is the password. On servers, supply the same pair through COMPOSER_AUTH instead of committing an auth.json:

export COMPOSER_AUTH='{"http-basic":{"sailfish.composer.sh":{"username":"you@example.com","password":"YOUR-LICENSE-KEY"}}}'

2. Require the package and migrate#

Add the registry to your application's composer.json:

{
    "repositories": [
        { "type": "composer", "url": "https://sailfish.composer.sh" }
    ]
}

Then require Sailfish and run the migrations:

composer require boring-o11y/my-sailfish
php artisan migrate

Package discovery registers Sailfish's own service provider, which adds the commands, the schedule and the dashboard routes. The migrations create the sailfish_* tables described in what gets recorded.

3. Let the scheduler run#

Sailfish puts its commands on Laravel's scheduler for you:

CommandScheduledWhat it does
sailfish:collect Every five minutes, withoutOverlapping and onOneServer Reads performance_schema, stores what moved since the last run, then runs sailfish:detect
sailfish:daily-digest Daily at 09:00 Sends the events that go out in the daily digest
sailfish:rollup Daily at 03:00 Merges old snapshots into hourly and daily ones (see retention)

The first collection only records a baseline, because counters running since the server booted describe no particular interval. Samples start with the second one, five minutes later. Set SAILFISH_SCHEDULE_ENABLED=false to register the commands yourself.

4. Decide who sees the dashboard#

The dashboard is served at /sailfish. In the local environment anyone can open it. Everywhere else nobody gets in until you say who can. Publish the provider:

php artisan vendor:publish --tag=sailfish-provider

Register it in bootstrap/providers.php, then narrow its gates:

// app/Providers/SailfishServiceProvider.php
protected function gate(): void
{
    Gate::define('viewSailfish', fn ($user) => $user->isAdmin());
    Gate::define('viewSailfishQueryText', fn ($user) => $user->isDba());
}

viewSailfish opens the dashboard. viewSailfishQueryText decides who sees running statements on the Activity page as they were sent, with their values. Everyone else sees them with each literal replaced by ?, because those values can be email addresses and tokens.

5. Check it#

php artisan about gets a Sailfish section with the connection, the schemas watched, the last snapshot and the retention. To take a snapshot straight away rather than wait for the scheduler:

php artisan sailfish:collect
Next

Sailfish sends nothing until you give it somewhere to send. Set up notifications to hear about the events it finds.

Common questions

Why does sailfish:collect say it is missing a grant?

The connection's user cannot SELECT from performance_schema. An application user granted only its own schema cannot, and Sailfish names the grant it needs rather than recording nothing. Grant SELECT on performance_schema.* to that user, or point SAILFISH_DB_CONNECTION at a connection whose user has it.

Why is the Sailfish dashboard empty after installing?

The first collection only records a baseline, because counters that have been running since the server booted do not describe any interval. Samples start with the second collection, five minutes later. If nothing appears after that, check that php artisan schedule:run is running every minute.

Why does the Sailfish dashboard return 403 in production?

Outside the local environment the dashboard asks the viewSailfish gate, and nobody gets in until you define it. Publish the provider with php artisan vendor:publish --tag=sailfish-provider, register it, and narrow the gate to the users who should see it.

Install Sailfish today.

Checkout ends with your license key, and the installation guide takes it from there.