Skip to content

Commit

Permalink
issue #6 - stats command
Browse files Browse the repository at this point in the history
  • Loading branch information
pounard committed Oct 20, 2023
1 parent 8d59fec commit c0e2ecb
Show file tree
Hide file tree
Showing 19 changed files with 1,118 additions and 13 deletions.
17 changes: 17 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ services:
- '@db_tools.restorer.factory.registry'
- '@db_tools.storage'
tags: ['console.command']
db_tools.command.stats:
class: MakinaCorpus\DbToolsBundle\Command\StatsCommand
arguments:
- '%doctrine.default_connection%'
- '@db_tools.stats_provider.factory.registry'
tags: ['console.command']

# Utilities
db_tools.storage:
Expand Down Expand Up @@ -62,6 +68,17 @@ services:
class: MakinaCorpus\DbToolsBundle\Restorer\MySQL\RestorerFactory
tags: ['db_tools.restorer.factory']

# Stats providers
db_tools.stats_provider.factory.registry:
class: MakinaCorpus\DbToolsBundle\Stats\StatsProviderFactoryRegistry
arguments: ['@doctrine']
db_tools.stats_provider.factory.pgsql:
class: MakinaCorpus\DbToolsBundle\Stats\PgSQL\PgSQLStatsProviderFactory
tags: ['db_tools.stats_provider.factory']
db_tools.stats_provider.factory.mysql:
class: MakinaCorpus\DbToolsBundle\Stats\MySQL\MySQLStatsProviderFactory
tags: ['db_tools.stats_provider.factory']

# Anonymization
db_tools.anonymization.anonymizer.registry:
class: MakinaCorpus\DbToolsBundle\Anonymizer\AnonymizerRegistry
Expand Down
2 changes: 1 addition & 1 deletion src/Backupper/BackupperFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public function create(string $binary, Connection $connection): BackupperInterfa
/**
* Check if given DBAL driver is supported by this backupper factory.
*/
public function isSupported($driver): bool;
public function isSupported(string $driver): bool;
}
4 changes: 2 additions & 2 deletions src/Backupper/MySQL/BackupperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public function create(string $binary, Connection $connection): BackupperInterfa
/**
* {@inheritdoc}
*/
public function isSupported($driver): bool
public function isSupported(string $driver): bool
{
return \in_array($driver, ['pdo_mysql']);
return \str_contains($driver, 'mysql') || \str_contains($driver, 'maria');
}
}
4 changes: 2 additions & 2 deletions src/Backupper/PgSQL/BackupperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public function create(string $binary, Connection $connection): BackupperInterfa
/**
* {@inheritdoc}
*/
public function isSupported($driver): bool
public function isSupported(string $driver): bool
{
return \in_array($driver, ['pdo_pgsql', 'pgsql']);
return \str_contains($driver, 'pgsql') || \str_contains($driver, 'postgres');
}
}
215 changes: 215 additions & 0 deletions src/Command/StatsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?php

declare(strict_types=1);

namespace MakinaCorpus\DbToolsBundle\Command;

use MakinaCorpus\DbToolsBundle\Stats\StatValue;
use MakinaCorpus\DbToolsBundle\Stats\StatValueList;
use MakinaCorpus\DbToolsBundle\Stats\StatsProviderFactoryRegistry;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableCellStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'db-tools:stats', description: 'Give some database statistics')]
class StatsCommand extends Command
{
public function __construct(
private string $defaultConnectionName,
private StatsProviderFactoryRegistry $statsProviderFactoryRegistry,
) {
parent::__construct();
}

/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription('Give some database statistics')
->addArgument(
'which',
InputArgument::OPTIONAL,
<<<TXT
Which statistics to display. Allowed values are:
- "global": display some global statistics,
- "table": display per-table statistics, such as size, row count, ...
- "index": display per-index statistics, such as size, number of reads, ...
TXT,
'global',
)
->addArgument(
'connection',
InputArgument::OPTIONAL,
'A doctrine connection name. If not given, use default connection'
)
->addOption(
'flat',
'f',
InputOption::VALUE_NONE,
'Display one line per value instead of using a table.',
)
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Show all values for all tags, ignores the --tag option.',
)
->addOption(
'tag',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
<<<TXT
Filter using given tags. Available tags are:
- "info": display global information,
- "read": read statistics,
- "write": write statistics,
- "maint": maintainance statistics, such as PostgreSQL VACUUM,
- "code": occasionaly display SQL code, such as CREATE statements.
TXT,
[StatValue::TAG_INFO],
)
;
}

private function displayFlat(iterable $collections, OutputInterface $output): bool
{
$some = false;

foreach ($collections as $collection) {
\assert($collection instanceof StatValueList);

$output->writeln($collection->name);
$output->writeln(\str_repeat('-', \strlen($collection->name)));

foreach ($collection as $value) {
\assert($value instanceof StatValue);

$prefix = '';
if ($value->tags) {
$prefix .= "[" . \implode(', ', $value->tags) . '] ';
}
if ($unitStr = $value->unitToString()) {
$prefix .= "(" . $unitStr . ") ";
}

$output->writeln(\sprintf("%s%s: %s", $prefix, $value->name, $value->toString()));
}

$output->writeln("");

if (!$some) {
$some = true;
}
}

return $some;
}

private function displayTable(iterable $collections, OutputInterface $output): bool
{
$rows = [];
$headers = ["table"];
$first = true;

$style = clone Table::getStyleDefinition('symfony-style-guide');
$style->setCellHeaderFormat('<info>%s</info>');
$style->setPadType(\STR_PAD_LEFT);

$alignLeftStyle = new TableCellStyle(['align' => 'left']);

foreach ($collections as $collection) {
\assert($collection instanceof StatValueList);

$row = [
// Align left table name.
new TableCell($collection->name, ['style' => $alignLeftStyle]),
];

foreach ($collection as $value) {
\assert($value instanceof StatValue);

if ($first) {
$header = $value->name;
if ($unitStr = $value->unitToString()) {
$header .= "\n(" . $unitStr . ")";
}
if ($value->tags) {
$header .= "\n" . "[" . \implode(', ', $value->tags) . ']';
}

$headers[] = $header;
}

if ($value->alignLeft()) {
$row[] = new TableCell($value->toString(), ['style' => $alignLeftStyle]);
} else {
$row[] = $value->toString();
}
}

$rows[] = $row;
$first = false;
}

if (!$rows) {
return false;
}

// Can't change pad style while using SymfonyStyle.
(new Table($output))
->setStyle($style)
->setHeaders($headers)
->setRows($rows)
->render()
;

$output->writeln("");

return true;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$connection = $input->getArgument('connection') ?? $this->defaultConnectionName;

$tags = null;
if (!$input->getOption('all') && ($rawTags = $input->getOption('tag'))) {
$tags = $rawTags;
}

$which = $input->getArgument('which');

$collections = match ($which) {
'table' => $this->statsProviderFactoryRegistry->get($connection)->getTableStats($tags),
'index' => $this->statsProviderFactoryRegistry->get($connection)->getIndexStats($tags),
'global' => $this->statsProviderFactoryRegistry->get($connection)->getGlobalStats($tags),
default => throw new InvalidArgumentException(\sprintf("'which' allowed values are: '%s'", \implode("', '", ['global', 'table', 'index']))),
};

$hasValues = false;

if ($input->getOption('flat')) {
$hasValues = $this->displayFlat($collections, $output);
} else {
$hasValues = $this->displayTable($collections, $output);
}

if (!$hasValues) {
$io->warning(\sprintf("Statistics for '%s' are not supported for the current '%s' connexion database driver.", $which, $connection));
}

return Command::SUCCESS;
}
}
15 changes: 12 additions & 3 deletions src/DependencyInjection/Compiler/DbToolsPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class DbToolsPass implements CompilerPassInterface
public function process(ContainerBuilder $container): void
{
if ($container->has('db_tools.backupper.factory.registry')) {
$definition = $container->findDefinition('db_tools.backupper.factory.registry');
$definition = $container->getDefinition('db_tools.backupper.factory.registry');

$taggedServices = $container->findTaggedServiceIds('db_tools.backupper.factory');
foreach ($taggedServices as $id => $tags) {
Expand All @@ -22,16 +22,25 @@ public function process(ContainerBuilder $container): void
}

if ($container->has('db_tools.restorer.factory.registry')) {
$definition = $container->findDefinition('db_tools.restorer.factory.registry');
$definition = $container->getDefinition('db_tools.restorer.factory.registry');

$taggedServices = $container->findTaggedServiceIds('db_tools.restorer.factory');
foreach ($taggedServices as $id => $tags) {
$definition->addMethodCall('addRestorerFactory', [new Reference($id)]);
}
}

if ($container->has('db_tools.stats_provider.factory.registry')) {
$definition = $container->getDefinition('db_tools.stats_provider.factory.registry');

$taggedServices = $container->findTaggedServiceIds('db_tools.stats_provider.factory');
foreach ($taggedServices as $id => $tags) {
$definition->addMethodCall('register', [new Reference($id)]);
}
}

if ($container->has('db_tools.anonymization.anonymizator.registry')) {
$definition = $container->findDefinition('db_tools.anonymization.anonymizator.registry');
$definition = $container->getDefinition('db_tools.anonymization.anonymizator.registry');

$taggedServices = $container->findTaggedServiceIds('db_tools.anonymization.anonymizator');
foreach ($taggedServices as $id => $tags) {
Expand Down
4 changes: 2 additions & 2 deletions src/Restorer/MySQL/RestorerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public function create(string $binary, Connection $connection): RestorerInterfac
/**
* {@inheritdoc}
*/
public function isSupported($driver): bool
public function isSupported(string $driver): bool
{
return \in_array($driver, ['pdo_mysql']);
return \str_contains($driver, 'mysql') || \str_contains($driver, 'maria');
}
}
4 changes: 2 additions & 2 deletions src/Restorer/PgSQL/RestorerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public function create(string $binary, Connection $connection): RestorerInterfac
/**
* {@inheritdoc}
*/
public function isSupported($driver): bool
public function isSupported(string $driver): bool
{
return \in_array($driver, ['pdo_pgsql', 'pgsql']);
return \str_contains($driver, 'pgsql') || \str_contains($driver, 'postgres');
}
}
2 changes: 1 addition & 1 deletion src/Restorer/RestorerFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public function create(string $binary, Connection $connection): RestorerInterfac
/**
* Check if given DBAL driver is supported by this restorer factory.
*/
public function isSupported($driver): bool;
public function isSupported(string $driver): bool;
}
Loading

0 comments on commit c0e2ecb

Please sign in to comment.