diff --git a/composer.json b/composer.json index 0577008..5f951f1 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ "minimum-stability": "dev", "require": { "oat-sa/oatbox-extension-installer": "~1.1||dev-master", - "oat-sa/generis" : ">=15.22", - "oat-sa/tao-core" : ">=50.24.6", + "oat-sa/generis": ">=15.32.0", + "oat-sa/tao-core": ">=53.11.4", "oat-sa/extension-tao-funcacl" : ">=7.0.0", "oat-sa/extension-tao-delivery-rdf" : ">=14.0.0", "oat-sa/extension-tao-item" : ">=11.0.0", diff --git a/migrations/Version202310191436392752_taoEventLog.php b/migrations/Version202310191436392752_taoEventLog.php new file mode 100644 index 0000000..53a18a7 --- /dev/null +++ b/migrations/Version202310191436392752_taoEventLog.php @@ -0,0 +1,47 @@ +getExtensionsManager()->isEnabled('taoDacSimple')) { + $this->getEventManager()->attach( + 'oat\taoDacSimple\model\event\DacChangedEvent', + [LoggerService::class, 'log'] + ); + } + } + + public function down(Schema $schema): void + { + $this->getEventManager()->detach( + 'oat\taoDacSimple\model\event\DacChangedEvent', + [LoggerService::class, 'log'] + ); + } + + private function getExtensionsManager(): common_ext_ExtensionsManager + { + return $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID); + } + + private function getEventManager(): EventManager + { + return $this->getServiceLocator()->get(EventManager::SERVICE_ID); + } +} diff --git a/model/StorageInterface.php b/model/StorageInterface.php index 2e83e8f..39d2fea 100644 --- a/model/StorageInterface.php +++ b/model/StorageInterface.php @@ -34,6 +34,8 @@ interface StorageInterface */ public function log(LogEntity $logEntity); + public function logMultiple(LogEntity ...$logEntities): bool; + /** * Search records in log which are meet the search criteria * diff --git a/model/eventLog/LoggerService.php b/model/eventLog/LoggerService.php index 03ebda9..b5e9be0 100644 --- a/model/eventLog/LoggerService.php +++ b/model/eventLog/LoggerService.php @@ -25,13 +25,15 @@ use common_exception_Error; use common_session_Session; use common_session_SessionManager; -use common_user_User; use Context; use DateTimeImmutable; +use DateTimeZone; use JsonSerializable; use oat\dtms\DateInterval; +use oat\oatbox\event\BulkEvent; use oat\oatbox\event\Event; use oat\oatbox\service\ServiceManager; +use oat\oatbox\user\User; use oat\taoEventLog\model\storage\RdsStorage as DeprecatedRdsStorage; use oat\dtms\DateTime; use oat\taoEventLog\model\AbstractLog; @@ -75,24 +77,26 @@ public function setAction($action = '') */ public function log(Event $event) { - /** @var common_session_Session $session */ - $session = common_session_SessionManager::getSession(); - - /** @var common_user_User $currentUser */ - $currentUser = $session->getUser(); - - $data = is_subclass_of($event, JsonSerializable::class) ? $event : []; - - $logEntity = new EventLogEntity( - $event, - $this->getAction(), - $currentUser, - (new DateTime('now', new \DateTimeZone('UTC'))), - $data - ); + $currentUser = $this->getUser(); try { - $this->getStorage()->log($logEntity); + if ($event instanceof BulkEvent) { + $this->getStorage()->logMultiple( + ...array_map( + fn (array $eventData): EventLogEntity => $this->createEventLogEntity( + $event, + $currentUser, + $eventData + ), + $event->getValues() + ) + ); + + return; + } + + $data = is_subclass_of($event, JsonSerializable::class) ? $event : []; + $this->getStorage()->log($this->createEventLogEntity($event, $currentUser, $data)); } catch (\Exception $e) { \common_Logger::e('Error logging to DB ' . $e->getMessage()); } @@ -137,4 +141,23 @@ protected function getStorage() $storage = $this->getServiceManager()->get(self::SERVICE_ID)->getOption(self::OPTION_STORAGE); return $this->getServiceManager()->get($storage); } + + private function getUser(): User + { + /** @var common_session_Session $session */ + $session = common_session_SessionManager::getSession(); + + return $session->getUser(); + } + + private function createEventLogEntity(Event $event, User $user, $data): EventLogEntity + { + return new EventLogEntity( + $event, + $this->getAction(), + $user, + (new DateTime('now', new DateTimeZone('UTC'))), + $data + ); + } } diff --git a/model/eventLog/RdsStorage.php b/model/eventLog/RdsStorage.php index f68a13d..f119083 100644 --- a/model/eventLog/RdsStorage.php +++ b/model/eventLog/RdsStorage.php @@ -25,6 +25,7 @@ use oat\taoEventLog\model\LogEntity; use Doctrine\DBAL\Schema\SchemaException; use oat\taoEventLog\model\storage\AbstractRdsStorage; +use Throwable; /** * Class RdsStorage @@ -36,6 +37,8 @@ class RdsStorage extends AbstractRdsStorage public const SERVICE_ID = 'taoEventLog/eventLogStorage'; + public const OPTION_INSERT_CHUNK_SIZE = 'insertChunkSize'; + public const EVENT_LOG_ID = self::ID; public const EVENT_LOG_EVENT_NAME = 'event_name'; public const EVENT_LOG_ACTION = 'action'; @@ -44,6 +47,8 @@ class RdsStorage extends AbstractRdsStorage public const EVENT_LOG_OCCURRED = 'occurred'; public const EVENT_LOG_PROPERTIES = 'properties'; + private const DEFAULT_INSERT_CHUNK_SIZE = 100; + /** * @return string */ @@ -73,6 +78,49 @@ public function log(LogEntity $logEntity) return $result === 1; } + public function logMultiple(LogEntity ...$logEntities): bool + { + $inserts = array_map( + static fn (LogEntity $logEntity): array => [ + self::EVENT_LOG_EVENT_NAME => $logEntity->getEvent()->getName(), + self::EVENT_LOG_ACTION => $logEntity->getAction(), + self::EVENT_LOG_USER_ID => $logEntity->getUser()->getIdentifier(), + self::EVENT_LOG_USER_ROLES => implode(',', $logEntity->getUser()->getRoles()), + self::EVENT_LOG_OCCURRED => $logEntity->getTime()->format(self::DATE_TIME_FORMAT), + self::EVENT_LOG_PROPERTIES => json_encode($logEntity->getData()), + ], + $logEntities + ); + + try { + $persistence = $this->getPersistence(); + + $persistence->transactional(function () use ($inserts, $persistence) { + $insertCount = count($inserts); + $insertChunkSize = $this->getInsertChunkSize(); + + foreach (array_chunk($inserts, $insertChunkSize) as $index => $chunk) { + $this->logDebug( + sprintf( + 'Processing chunk %d/%d with %d log entries', + $index + 1, + ceil($insertCount / $insertChunkSize), + count($chunk) + ) + ); + + $persistence->insertMultiple($this->getTableName(), $chunk); + } + }); + + return true; + } catch (Throwable $exception) { + $this->logError('Error when inserting log entries: ' . $exception->getMessage()); + + return false; + } + } + /** * @param array $params * @deprecated use $this->search() instead @@ -161,4 +209,9 @@ public static function install($persistence) $persistence->exec($query); } } + + private function getInsertChunkSize(): int + { + return $this->getOption(self::OPTION_INSERT_CHUNK_SIZE, self::DEFAULT_INSERT_CHUNK_SIZE); + } } diff --git a/model/storage/RdsStorage.php b/model/storage/RdsStorage.php index efd5b58..4851881 100644 --- a/model/storage/RdsStorage.php +++ b/model/storage/RdsStorage.php @@ -44,6 +44,11 @@ public function log(LogEntity $logEntity) { } + public function logMultiple(LogEntity ...$logEntities): bool + { + return true; + } + public static function tableColumns() { } diff --git a/scripts/install/RegisterLoggerService.php b/scripts/install/RegisterLoggerService.php index e576bfa..f341ca3 100644 --- a/scripts/install/RegisterLoggerService.php +++ b/scripts/install/RegisterLoggerService.php @@ -147,6 +147,10 @@ public function __invoke($params) 'oat\\taoDacSimple\\model\\event\\DacRemovedEvent', [LoggerService::class, 'logEvent'] ); + $this->registerEvent( + 'oat\taoDacSimple\model\event\DacChangedEvent', + [LoggerService::class, 'log'] + ); } if ($extensionManager->isEnabled('taoTestTaker')) { diff --git a/test/model/storage/RdsStorageTest.php b/test/model/storage/RdsStorageTest.php index 2379e7b..2993992 100644 --- a/test/model/storage/RdsStorageTest.php +++ b/test/model/storage/RdsStorageTest.php @@ -15,13 +15,12 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2017 (original work) Open Assessment Technologies SA; - * - * + * Copyright (c) 2017-2023 (original work) Open Assessment Technologies SA; */ namespace oat\taoEventLog\test\model\requestLog\rds; +use oat\oatbox\log\LoggerService; use oat\tao\test\TaoPhpUnitTestRunner; use oat\taoEventLog\model\eventLog\RdsStorage; use oat\oatbox\service\ServiceManager; @@ -32,13 +31,11 @@ use oat\taoEventLog\scripts\install\RegisterRdsStorage; /** - * Class RdsStorageTest - * @package oat\taoEventLog\test\model\requestLog\rds * @author Aleh Hutnikau, */ class RdsStorageTest extends TaoPhpUnitTestRunner { - public function testCount() + public function testCount(): void { $storage = $this->getService(); $this->assertEquals(60, $storage->count()); @@ -49,7 +46,7 @@ public function testCount() $this->assertEquals(11, count($result)); } - public function testSearch() + public function testSearch(): void { $storage = $this->getService(); $this->assertEquals(60, count($storage->search())); @@ -96,10 +93,52 @@ public function testSearch() $this->assertEquals('test_event_10', $result[0][RdsStorage::EVENT_LOG_EVENT_NAME]); } - /** - * @return RdsStorage - */ - protected function getService() + public function testLogMultiple(): void + { + $storage = $this->getService(); + + $event = $this->createMock(Event::class); + $event->expects($this->any()) + ->method('getName') + ->willReturn('testEvent'); + + $user = $this->createMock(User::class); + $user->expects($this->any()) + ->method('getRoles') + ->willReturn([]); + + $entities = [ + new LogEntity( + $event, + 'action1', + $user, + new DateTime() + ), + new LogEntity( + $event, + 'action2', + $user, + new DateTime() + ) + ]; + + $this->assertTrue($storage->logMultiple(...$entities)); + + $result = $storage->search( + [ + [ + RdsStorage::EVENT_LOG_EVENT_NAME, '=', 'testEvent'] + ], + [ + 'sort' => RdsStorage::EVENT_LOG_OCCURRED, + 'order' => 'ASC' + ] + ); + + $this->assertEquals(2, count($result)); + } + + private function getService(): RdsStorage { $persistenceManager = $this->getSqlMock('test_eventlog'); (new RegisterRdsStorage())->createTable($persistenceManager->getPersistenceById('test_eventlog')); @@ -109,12 +148,13 @@ protected function getService() $config = new \common_persistence_KeyValuePersistence([], new \common_persistence_InMemoryKvDriver()); $config->set(\common_persistence_Manager::SERVICE_ID, $persistenceManager); $serviceManager = new ServiceManager($config); + $serviceManager->register(LoggerService::SERVICE_ID, $this->createMock(LoggerService::class)); $storage->setServiceManager($serviceManager); $this->loadFixtures($storage); return $storage; } - protected function loadFixtures(RdsStorage $storage) + private function loadFixtures(RdsStorage $storage): void { for ($i = 0; $i < 60; $i++) { $eventProphecy = $this->prophesize(Event::class); @@ -122,7 +162,7 @@ protected function loadFixtures(RdsStorage $storage) $userProphecy = $this->prophesize(User::class); $userProphecy->getIdentifier()->willReturn('test_user_' . $i); - $userProphecy->getRoles()->willReturn(['role_' . (($i % 5) + 1) , 'role_2' . (($i % 5) + 2)]); + $userProphecy->getRoles()->willReturn(['role_' . (($i % 5) + 1), 'role_2' . (($i % 5) + 2)]); $logEntity = new LogEntity( $eventProphecy->reveal(),