Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] New db:show, db:table and db:monitor commands #43367

Merged
merged 31 commits into from Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c93e1e9
wip
jbrooksuk Jul 21, 2022
a2bf0ff
wip
joedixon Jul 21, 2022
1cc2c91
wip
jbrooksuk Jul 21, 2022
af8cfda
wip
jbrooksuk Jul 21, 2022
a460f0c
wip
jbrooksuk Jul 21, 2022
1074fa0
wip
jbrooksuk Jul 21, 2022
61dc8ef
Create `AbstractDatabaseCommand`
jbrooksuk Jul 21, 2022
ebc70b3
wip
jbrooksuk Jul 21, 2022
f931bcd
Change return type of connection count methods
jbrooksuk Jul 22, 2022
b7454a9
Add `db:monitor`
jbrooksuk Jul 22, 2022
42acac3
Move dbal check
joedixon Jul 22, 2022
1ba1355
Formatting
joedixon Jul 22, 2022
da5ed93
Opt-in to expensive operations
joedixon Jul 22, 2022
601bd44
Merge branch '9.x' into feat/db-commands
jbrooksuk Jul 22, 2022
77b415e
Apply fixes from StyleCI
StyleCIBot Jul 22, 2022
501c629
Ask for table
joedixon Jul 22, 2022
a10a800
Rename variable
jbrooksuk Jul 22, 2022
43b51c9
Merge remote-tracking branch 'origin/feat/db-commands' into feat/db-c…
jbrooksuk Jul 22, 2022
68d088d
Change how getTableSize works
jbrooksuk Jul 22, 2022
1e139bd
Make `--max` optional in `db:monitor`
jbrooksuk Jul 22, 2022
7ec2a72
wip
jbrooksuk Jul 25, 2022
b203136
Merge branch '9.x' into feat/db-commands
jbrooksuk Jul 25, 2022
132f87b
Standardise headings
joedixon Jul 25, 2022
7d296b2
make `db:monitor` use an argument for databases
jbrooksuk Jul 26, 2022
6ed7002
Use option again
jbrooksuk Jul 26, 2022
d7ffbe6
Move composer to abstract
joedixon Jul 26, 2022
9ff225c
Add composer
joedixon Jul 26, 2022
f664414
Apply fixes from StyleCI
StyleCIBot Jul 26, 2022
80996f6
Update src/Illuminate/Database/Console/MonitorCommand.php
driesvints Jul 27, 2022
8e7644d
formatting
taylorotwell Aug 2, 2022
fa8902f
Apply fixes from StyleCI
StyleCIBot Aug 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
205 changes: 205 additions & 0 deletions src/Illuminate/Database/Console/AbstractDatabaseCommand.php
@@ -0,0 +1,205 @@
<?php

namespace Illuminate\Database\Console;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Arr;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;

abstract class AbstractDatabaseCommand extends Command
{
/**
* Get a human-readable platform name.
*
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
* @param string $database
* @return string
*/
protected function getPlatformName(AbstractPlatform $platform, $database)
{
return match (class_basename($platform)) {
'MySQLPlatform' => 'MySQL <= 5',
'MySQL57Platform' => 'MySQL 5.7',
'MySQL80Platform' => 'MySQL 8',
'PostgreSQL100Platform', 'PostgreSQLPlatform' => 'Postgres',
'SqlitePlatform' => 'SQLite',
'SQLServerPlatform' => 'SQL Server',
'SQLServer2012Platform' => 'SQL Server 2012',
default => $database,
};
}

/**
* Get the size of a table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return null
jbrooksuk marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function getTableSize(ConnectionInterface $connection, string $table)
{
return match (class_basename($connection)) {
jbrooksuk marked this conversation as resolved.
Show resolved Hide resolved
'MySqlConnection' => $this->getMySQLTableSize($connection, $table),
'PostgresConnection' => $this->getPgsqlTableSize($connection, $table),
'SqliteConnection' => $this->getSqliteTableSize($connection, $table),
default => null,
};
}

/**
* Get the size of a MySQL table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getMySQLTableSize(ConnectionInterface $connection, string $table)
{
return $connection->selectOne('SELECT (data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?', [
$connection->getDatabaseName(),
$table,
])->size;
}

/**
* Get the size of a Postgres table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getPgsqlTableSize(ConnectionInterface $connection, string $table)
{
return $connection->selectOne('SELECT pg_total_relation_size(?) AS size;', [
$table,
])->size;
}

/**
* Get the size of a SQLite table in bytes.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return mixed
*/
protected function getSqliteTableSize(ConnectionInterface $connection, string $table)
{
return $connection->selectOne('SELECT SUM(pgsize) FROM dbstat WHERE name=?', [
$table,
])->size;
}

/**
* Get the number of open connections for a database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return null
*/
protected function getConnectionCount(ConnectionInterface $connection)
{
return match (class_basename($connection)) {
'MySqlConnection' => $this->getMySQLConnectionCount($connection),
'PostgresConnection' => $this->getPgsqlConnectionCount($connection),
'SqlServerConnection' => $this->getSqlServerConnectionCount($connection),
default => null,
};
}

/**
* Get the number of open connections for a Postgres database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int
*/
protected function getPgsqlConnectionCount(ConnectionInterface $connection)
{
return (int) $connection->selectOne('select count(*) as connections from pg_stat_activity')->connections;
}

/**
* Get the number of open connections for a MySQL database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int
*/
protected function getMySQLConnectionCount(ConnectionInterface $connection)
{
return (int) $connection->selectOne($connection->raw('show status where variable_name = "threads_connected"'))->Value;
}

/**
* Get the number of open connections for an SQL Server database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int
*/
protected function getSqlServerConnectionCount(ConnectionInterface $connection)
{
return (int) $connection->selectOne('SELECT COUNT(*) connections FROM sys.dm_exec_sessions WHERE status = ?', ['running'])->connections;
}

/**
* Get the connection details from the configuration.
*
* @param string $database
* @return array
*/
protected function getConfigFromDatabase($database)
{
$database ??= config('database.default');

return Arr::except(config('database.connections.'.$database), ['password']);
}

/**
* Ensure the dependencies for the database commands are available.
*
* @return int
jbrooksuk marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function ensureDependenciesExist()
{
if (! interface_exists('Doctrine\DBAL\Driver')) {
if (! $this->components->confirm('Displaying model information requires the Doctrine DBAL (doctrine/dbal) package. Would you like to install it?')) {
return 1;
}

return $this->installDependencies();
}
}

/**
* Install the command's dependencies.
*
* @return void
*
* @throws \Symfony\Component\Process\Exception\ProcessSignaledException
*/
protected function installDependencies()
{
$command = collect($this->composer->findComposer())
->push('require doctrine/dbal')
->implode(' ');

$process = Process::fromShellCommandline($command, null, null, null, null);

if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->components->warn($e->getMessage());
}
}

try {
$process->run(fn ($type, $line) => $this->output->write($line));
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
}
}
145 changes: 145 additions & 0 deletions src/Illuminate/Database/Console/MonitorCommand.php
@@ -0,0 +1,145 @@
<?php

namespace Illuminate\Database\Console;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\DatabaseBusy;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'db:monitor')]
class MonitorCommand extends AbstractDatabaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:monitor
{--databases= : The names of the databases to monitor}
jbrooksuk marked this conversation as resolved.
Show resolved Hide resolved
{--max=5 : The maximum number of connections that can be open before an event is dispatched}';
jbrooksuk marked this conversation as resolved.
Show resolved Hide resolved

/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'db:monitor';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Monitor the number of connections on the specified database';

/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $connection;

/**
* The events dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;

/**
* Create a new db monitor command.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connection
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function __construct(ConnectionResolverInterface $connection, Dispatcher $events)
{
parent::__construct();

$this->connection = $connection;
$this->events = $events;
}

/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$databases = $this->parseDatabases($this->option('databases'));

$this->displayConnections($databases);

$this->dispatchEvents($databases);
}

/**
* Parse the database into an array of the connections.
*
* @param string $databases
* @return \Illuminate\Support\Collection
*/
protected function parseDatabases($databases)
{
return collect(explode(',', $databases))->map(function ($database) {
if (! $database) {
$database = $this->laravel['config']['database.default'];
}

return [
'database' => $database,
'connections' => $connections = $this->getConnectionCount($this->connection->connection($database)),
'status' => $connections >= $this->option('max') ? '<fg=yellow;options=bold>ALERT</>' : '<fg=green;options=bold>OK</>',
];
});
}

/**
* Display the databases and their connections in the console.
*
* @param \Illuminate\Support\Collection $databases
* @return void
*/
protected function displayConnections($databases)
{
$this->newLine();

$this->components->twoColumnDetail('<fg=gray>Database name</>', '<fg=gray>Connections</>');

$databases->each(function ($database) {
$status = '['.$database['connections'].'] '.$database['status'];

$this->components->twoColumnDetail($database['database'], $status);
});

$this->newLine();
}

/**
* Fire the monitoring events.
*
* @param \Illuminate\Support\Collection $databases
* @return void
*/
protected function dispatchEvents($databases)
{
$databases->each(function ($database) {
if ($database['status'] === '<fg=green;options=bold>OK</>') {
return;
}

$this->events->dispatch(
new DatabaseBusy(
$database['database'],
$database['connections']
)
);
});
}
}