js/mysqlnd-analytics
Collect and inspect MySQL Native Driver (mysqlnd) statistics from your PHP or Laravel app. Gain visibility into query performance, connection usage, and database health with simple analytics and reporting.
Know what you are installing. Despite the js/ vendor prefix, this is a standalone PHP library (Johannes Schlüter's), not a Laravel package and not JavaScript. There is no service provider, facade, or config file. It takes the array returned by mysqli_get_client_stats() and runs rule-based heuristics over it, emitting tuning advice. Nothing on the request path belongs to it.
Step 1 — prove the data source exists. Before touching the library, confirm you can get stats at all. In php artisan tinker or a scratch script:
var_dump(extension_loaded('mysqli')); // true required
var_dump(function_exists('mysqli_get_client_stats')); // true required
$stats = mysqli_get_client_stats();
print_r(array_slice($stats, 0, 15, true));
If ext-mysqli is absent — common when images only install pdo_mysql — stop here. You need docker-php-ext-install mysqli, and ideally only in a diagnostic image.
Step 2 — read the rule set before you run it. The half-day that pays for itself is reading src/ and skimming the checks: connection reuse, buffered vs. unbuffered result sets, prepared-statement reuse, compression, SSL overhead, query cache behaviour. That tells you which of your access patterns even have a matching heuristic.
Step 3 — run the analyser against captured stats. Do this outside Laravel first, in a scratch script with a saved $stats array, so you can iterate without booting the framework:
require __DIR__ . '/vendor/autoload.php';
$stats = json_decode(file_get_contents(__DIR__ . '/stats.json'), true);
$analyser = new \JS\MysqlndAnalytics\Analyser(); // check src/ for the exact entry point
foreach ($analyser->analyse($stats) as $finding) {
echo $finding, PHP_EOL;
}
First real use case: a load test on staging. Reset nothing (you can't), note the snapshot, drive traffic, snapshot again, subtract, analyse the delta, and look for connection churn or unbuffered-result-set flags. That is the whole of the package's value in one loop. If it doesn't flag a deliberately planted problem, stop — you have your answer.
The delta pattern is mandatory, not optional. mysqlnd counters are monotonically increasing lifetime totals for the process. Analysing raw totals tells you about history since worker start, not about the traffic you care about. Store a baseline and subtract:
final class MysqlndStatsCollector
{
public function snapshot(): array
{
return function_exists('mysqli_get_client_stats')
? mysqli_get_client_stats()
: [];
}
public function delta(array $before, array $after): array
{
$delta = [];
foreach ($after as $key => $value) {
if (!is_numeric($value)) { continue; }
$delta[$key] = $value - ($before[$key] ?? 0);
}
return $delta;
}
}
Insulate the third-party types behind a DTO. This is the single most important decision given the maintenance profile. The analyser's return types should never appear in application code:
final class MysqlndReport
{
public function __construct(
public readonly array $findings,
public readonly array $rawCounters,
public readonly int $servedRequests,
) {}
}
final class MysqlndAnalyser
{
public function analyse(array $delta, int $servedRequests): MysqlndReport
{
$findings = $this->library->analyse($delta); // only place the lib is touched
return new MysqlndReport($findings, $delta, $servedRequests);
}
}
Swapping to a homegrown rule set later then becomes a one-class change instead of a grep.
Expose it through an artisan command, never a route. A CLI-only path is the right shape for a per-worker diagnostic and keeps the output off the network:
php artisan mysqlnd:report --baseline --json
Guard it with $this->confirm('This reports process-local counters; continue?') in production, or gate the command registration on !app()->isProduction().
Gate the guidance by server flavour. Query-cache advice was removed in MySQL 8.0 but remains meaningful on MariaDB. Wrap the analyser call in a version check and annotate or drop irrelevant findings; otherwise the tool's most prominent advice is actively wrong on a modern target.
Sampling pattern when you want more than a one-off. Hook terminate() or a queued middleware to capture a snapshot every N requests, write the delta to a dedicated log channel or Redis, and analyse asynchronously. Do not analyse on every request — the cost is small but the output is noise below a few hundred queries.
Under Octane it gets more useful and less attributable. Long-lived workers accumulate heterogeneous history, so the analyser sees a much richer sample. Offset that by snapshotting on an interval (per N requests, not per worker-lifetime) so the delta corresponds to a describable window. Under classic PHP-FPM, treat each worker as an isolated, short sample.
Pair it with ground truth, don't substitute for it. The intended workflow is: Pulse/Telescope or slow query log identifies which query is slow; a deliberate load test with this library plus SHOW GLOBAL STATUS and performance_schema tells you whether the shape of your database access (churn, round-trips, payload) is the contributing cause. Run them in the same session; the combination is what produces a decision.
mysqli_get_client_stats() aggregates all mysqlnd connections in the process — including those opened by PDO. This is the bridge that lets a pdo_mysql Laravel app harvest stats, but it also means you cannot separate PDO traffic from mysqli traffic in the numbers. Fine for a whole-app health check, useless for attributing to a subsystem.ext-mysqli is frequently missing in production images where only pdo_mysql is installed. Feature-detect with function_exists() in every code path, and fail into a clear "diagnostics unavailable" message rather than a fatal.local-only.How can I help you explore Laravel packages today?