diff --git a/src/Command/AuditLogDeleteOldLogsCommand.php b/src/Command/AuditLogDeleteOldLogsCommand.php new file mode 100644 index 0000000..c5798ef --- /dev/null +++ b/src/Command/AuditLogDeleteOldLogsCommand.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +namespace DataDog\AuditBundle\Command; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'audit-logs:delete-old-logs', + description: 'Remove old records from the audit logs', +)] +class AuditLogDeleteOldLogsCommand extends Command +{ + public const DEFAULT_RETENTION_PERIOD = 'P3M'; + + public function __construct( + protected Connection $connection + ) { + parent::__construct(); + } + + #[\Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $date = (new \DateTime())->sub(new \DateInterval(self::DEFAULT_RETENTION_PERIOD)); + $formattedDate = $date->format('Y-m-d H:i:s'); + + $output->writeln(sprintf('<info>Delete all records before %s</info>', $formattedDate)); + + $result = $this->connection->executeQuery( + 'SELECT * FROM audit_logs WHERE logged_at < ? ORDER BY logged_at DESC LIMIT 1', + [$formattedDate] + ) + ->fetchAssociative(); + + if ($result === false) { + $output->writeln(sprintf('<info>No records to delete</info>')); + + return 0; + } + + $auditLogStartRecordId = $result['id']; + $auditAssociativeStartRecordId = max($result['source_id'], $result['target_id'], $result['blame_id']); + + $count = $this->deleteFromAuditLogs($auditLogStartRecordId); + $output->writeln(sprintf('<info> %s records from audit_logs deleted!</info>', $count)); + + $count = $this->deleteFromAuditAssociations($auditAssociativeStartRecordId); + $output->writeln(sprintf('<info> %s records from audit_associations deleted!</info>', $count)); + + return 0; + } + + private function deleteFromAuditLogs(int $startRecordId): int + { + $allRecords = 0; + $this->connection->executeQuery('SET FOREIGN_KEY_CHECKS=0'); + + $sql = 'DELETE LOW_PRIORITY FROM audit_logs WHERE id <= ? ORDER BY id LIMIT 10000'; + $stmt = $this->connection->prepare($sql); + $stmt->bindValue(1, $startRecordId, ParameterType::INTEGER); + do { + $startTime = microtime(true); + $deletedRows = $stmt->executeStatement(); + $allRecords += $deletedRows; + echo round((microtime(true) - $startTime), 3) . "s "; + sleep(1); + } while ($deletedRows > 0); + + $this->connection->executeQuery('SET FOREIGN_KEY_CHECKS=1'); + + return $allRecords; + } + + private function deleteFromAuditAssociations(int $startRecordId): int + { + $allRecords = 0; + $this->connection->executeQuery('SET FOREIGN_KEY_CHECKS=0'); + + $sql = 'DELETE LOW_PRIORITY FROM mscm.audit_associations WHERE id <= ? ORDER BY id LIMIT 10000'; + $stmt = $this->connection->prepare($sql); + $stmt->bindValue(1, $startRecordId, ParameterType::INTEGER); + do { + $startTime = microtime(true); + $deletedRows = $stmt->executeStatement(); + $allRecords += $deletedRows; + echo round((microtime(true) - $startTime), 3) . "s "; + sleep(1); + } while ($deletedRows > 0); + + $this->connection->executeQuery('SET FOREIGN_KEY_CHECKS=1'); + + return $allRecords; + } +} diff --git a/src/DependencyInjection/DataDogAuditExtension.php b/src/DependencyInjection/DataDogAuditExtension.php index 48cdd1e..7a47a68 100644 --- a/src/DependencyInjection/DataDogAuditExtension.php +++ b/src/DependencyInjection/DataDogAuditExtension.php @@ -2,6 +2,7 @@ namespace DataDog\AuditBundle\DependencyInjection; +use DataDog\AuditBundle\EventListener\AuditListener; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -17,7 +18,7 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $auditListener = $container->getDefinition('datadog.event_listener.audit'); + $auditListener = $container->getDefinition(AuditListener::class); if (isset($config['audited_entities']) && !empty($config['audited_entities'])) { $auditListener->addMethodCall('addAuditedEntities', array($config['audited_entities'])); diff --git a/src/EventListener/AuditListener.php b/src/EventListener/AuditListener.php index 571e08b..5b3627d 100644 --- a/src/EventListener/AuditListener.php +++ b/src/EventListener/AuditListener.php @@ -5,6 +5,7 @@ use DataDog\AuditBundle\DBAL\AuditLogger; use DataDog\AuditBundle\Entity\Association; use DataDog\AuditBundle\Entity\AuditLog; +use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\DBAL\Logging\LoggerChain; use Doctrine\DBAL\Logging\SQLLogger; use Doctrine\DBAL\Types\Type; @@ -13,11 +14,14 @@ use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Core\User\UserInterface; +#[AsDoctrineListener(Events::onFlush)] +#[AsAlias(id: 'datadog.event_listener.audit', public: false)] class AuditListener { /** diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index caea8b2..a9a6075 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -7,13 +7,26 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; return static function (ContainerConfigurator $container) { - // @formatter:off - $services = $container->services(); - $services - ->set('datadog.event_listener.audit', AuditListener::class)->private() - ->arg(0, new Reference(TokenStorageInterface::class)) - //->tag('doctrine.event_subscriber') - ->tag('doctrine.event_listener', ['event' => 'onFlush',]) + // default configuration for services in *this* file + $services = $container->services() + ->defaults() + ->autowire() // Automatically injects dependencies in your services. + ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. ; - // @formatter:on + + // makes classes in src/ available to be used as services + // this creates a service per class whose id is the fully-qualified class name + $services->load('DataDog\\AuditBundle\\', '../../../src/') + ->exclude('../../../src/{DependencyInjection,Entity,Resources,DataDogAuditBundle.php}'); + + +// // @formatter:off +// $services = $container->services(); +// $services +// ->set('datadog.event_listener.audit', AuditListener::class)->private() +// ->arg(0, new Reference(TokenStorageInterface::class)) +// //->tag('doctrine.event_subscriber') +// ->tag('doctrine.event_listener', ['event' => 'onFlush',]) +// ; +// // @formatter:on };