Skip to content

Commit

Permalink
[9.x] New db:show, db:table and db:monitor commands (laravel#43367
Browse files Browse the repository at this point in the history
)

* wip

* wip

* wip

* wip

* wip

* wip

* Create `AbstractDatabaseCommand`

* wip

* Change return type of connection count methods

* Add `db:monitor`

* Move dbal check

* Formatting

* Opt-in to expensive operations

* Apply fixes from StyleCI

* Ask for table

* Rename variable

* Change how getTableSize works

* Make `--max` optional in `db:monitor`

* wip

* Standardise headings

* make `db:monitor` use an argument for databases

* Use option again

* Move composer to abstract

* Add composer

* Apply fixes from StyleCI

* Update src/Illuminate/Database/Console/MonitorCommand.php

Co-authored-by: bastien-phi <bastien.philippe@soyhuce.fr>

* formatting

* Apply fixes from StyleCI

Co-authored-by: Joe Dixon <hello@joedixon.co.uk>
Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: Dries Vints <dries@vints.io>
Co-authored-by: bastien-phi <bastien.philippe@soyhuce.fr>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
6 people authored and Ken committed Aug 9, 2022
1 parent 4fcb8f8 commit 3465d1e
Show file tree
Hide file tree
Showing 7 changed files with 851 additions and 71 deletions.
196 changes: 196 additions & 0 deletions src/Illuminate/Database/Console/DatabaseInspectionCommand.php
@@ -0,0 +1,196 @@
<?php

namespace Illuminate\Database\Console;

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

abstract class DatabaseInspectionCommand extends Command
{
/**
* The Composer instance.
*
* @var \Illuminate\Support\Composer
*/
protected $composer;

/**
* Create a new command instance.
*
* @param \Illuminate\Support\Composer|null $composer
* @return void
*/
public function __construct(Composer $composer = null)
{
parent::__construct();

$this->composer = $composer ?? $this->laravel->make(Composer::class);
}

/**
* Get a human-readable platform name for the given platform.
*
* @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 int|null
*/
protected function getTableSize(ConnectionInterface $connection, string $table)
{
return match (true) {
$connection instanceof MySqlConnection => $this->getMySQLTableSize($connection, $table),
$connection instanceof PostgresConnection => $this->getPostgresTableSize($connection, $table),
$connection instanceof 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 getPostgresTableSize(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' => (int) $connection->selectOne($connection->raw('show status where variable_name = "threads_connected"'))->Value,
'PostgresConnection' => (int) $connection->selectOne('select count(*) as connections from pg_stat_activity')->connections,
'SqlServerConnection' => (int) $connection->selectOne('SELECT COUNT(*) connections FROM sys.dm_exec_sessions WHERE status = ?', ['running'])->connections,
default => null,
};
}

/**
* Get the connection configuration details for the given connection.
*
* @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|null
*/
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;
}
}
}
}
151 changes: 151 additions & 0 deletions src/Illuminate/Database/Console/MonitorCommand.php
@@ -0,0 +1,151 @@
<?php

namespace Illuminate\Database\Console;

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

#[AsCommand(name: 'db:monitor')]
class MonitorCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:monitor
{--databases= : The database connections to monitor}
{--max= : The maximum number of connections that can be open before an event is dispatched}';

/**
* 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 command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connection
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param \Illuminate\Support\Composer $composer
*/
public function __construct(ConnectionResolverInterface $connection, Dispatcher $events, Composer $composer)
{
parent::__construct($composer);

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

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

$this->displayConnections($databases);

if ($this->option('max')) {
$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'];
}

$maxConnections = $this->option('max');

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

/**
* Display the databases and their connection counts 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();
}

/**
* Dispatch the database 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']
)
);
});
}
}

0 comments on commit 3465d1e

Please sign in to comment.