diff --git a/app/common/Implement/Log/Handler/MySQL.php b/app/common/Implement/Log/Handler/MySQL.php index 1a580f7..64184d3 100644 --- a/app/common/Implement/Log/Handler/MySQL.php +++ b/app/common/Implement/Log/Handler/MySQL.php @@ -11,8 +11,20 @@ use PDOStatement; class MySQL extends AbstractProcessingHandler { private bool $initialized = false; - private PDOStatement $statement; - private PDOStatement $statementDeprecated; + private array $tables = [ + 'default' => 'monolog', + 'deprecated' => 'monolog_deprecated' + ]; + private array $statements = [ + 'default' => null, + 'deprecated' => null + ]; + private array $baseQueries = [ + 'check' => "SHOW TABLES LIKE '%s'", + 'create' => "CREATE TABLE IF NOT EXISTS %s (channel VARCHAR(255), level VARCHAR(100), message LONGTEXT, time DATETIME, context LONGTEXT, extra LONGTEXT)", + 'insert' => "INSERT INTO %s (channel, level, message, time, context, extra) VALUES (:channel, :level, :message, :time, :context, :extra)", + 'delete' => "DELETE FROM %s WHERE time < DATE_SUB(CURDATE(), INTERVAL %d DAY)" + ]; public function __construct(protected Connection $connection, protected int $retainDays = 90, int|string|Level $level = Level::Debug, bool $bubble = true) @@ -22,18 +34,14 @@ class MySQL extends AbstractProcessingHandler public function write(LogRecord $record): void { if (!$this->initialized) { - if (!$this->checkTableExists()) { - $this->createTable(); - } - if (!$this->checkTableDeprecatedExists()) { - $this->createTableDeprecated(); + if (!$this->checkTablesExist()) { + $this->createTables(); } $this->cleanup(); - $this->cleanupDeprecated(); $this->initialized(); } if (str_contains(strtolower($record->message), 'deprecated:')) { - $this->statementDeprecated->execute([ + $this->statements['deprecated']->execute([ 'channel' => $record->channel, 'level' => $record->level->getName(), 'message' => $record->formatted, @@ -43,7 +51,7 @@ class MySQL extends AbstractProcessingHandler ]); return; } - $this->statement->execute([ + $this->statements['default']->execute([ 'channel' => $record->channel, 'level' => $record->level->getName(), 'message' => $record->formatted, @@ -55,21 +63,19 @@ class MySQL extends AbstractProcessingHandler private function initialized(): void { - $query = <<statement = $this->connection->getPDO()->prepare($query); - $query = <<statementDeprecated = $this->connection->getPDO()->prepare($query); + foreach ($this->tables as $type => $table) { + $query = sprintf($this->baseQueries['insert'], $table); + $this->statements[$type] = $this->connection->getPDO()->prepare($query); + } $this->initialized = true; } - private function checkTableExists(): bool + private function checkTablesExist(): bool { - $query = "SHOW TABLES LIKE 'monolog'"; + return array_all($this->tables, fn($table) => $this->checkTableExists($table)); + } + private function checkTableExists(string $table): bool + { + $query = sprintf($this->baseQueries['check'], $table); try { $result = $this->connection->query($query); } catch (PDOException) { @@ -77,51 +83,34 @@ QUERY; } return $result->rowCount() > 0; } - private function checkTableDeprecatedExists(): bool + private function createTables(): void { - $query = "SHOW TABLES LIKE 'monolog_deprecated'"; - try { - $result = $this->connection->query($query); - } catch (PDOException) { - return false; + foreach ($this->tables as $table) { + if (!$this->checkTableExists($table)) { + $this->createTable($table); + } } - return $result->rowCount() > 0; } - private function createTable(): void + private function createTable(string $table): void { - $query = <<connection->getPDO()->exec($query); - } - private function createTableDeprecated(): void - { - $query = <<baseQueries['create'], $table); + try { + $result = $this->connection->getPDO()->exec($query); + if ($result === false) { + throw new PDOException('Failed to create table: ' . $table); + } + } catch (PDOException) {} } private function cleanup(): void { - $query = "DELETE FROM monolog WHERE time < DATE_SUB(CURDATE(), INTERVAL {$this->retainDays} DAY)"; - $this->connection->query($query); - } - private function cleanupDeprecated(): void - { - $query = "DELETE FROM monolog_deprecated WHERE time < DATE_SUB(CURDATE(), INTERVAL {$this->retainDays} DAY)"; + foreach ($this->tables as $table) { + $query = sprintf($this->baseQueries['delete'], $table, $this->retainDays); + try { + $result = $this->connection->getPDO()->query($query); + if ($result === false) { + throw new PDOException('Failed to delete from table: ' . $table); + } + } catch (PDOException) {} + } } } diff --git a/app/tests/unit/common/Implement/Log/Handler/MySQLTest.php b/app/tests/unit/common/Implement/Log/Handler/MySQLTest.php new file mode 100644 index 0000000..6b958aa --- /dev/null +++ b/app/tests/unit/common/Implement/Log/Handler/MySQLTest.php @@ -0,0 +1,59 @@ +getMockBuilder(PDO::class) + ->disableOriginalConstructor() + ->getMock(); + $pdo->method('prepare')->willReturn($this->getMockBuilder(PDOStatement::class)->getMock()); + $this->connection = $this->getMockBuilder(Define\Connection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->method('getPDO')->willReturn($pdo); + } + + public function testWrite(): void + { + $faker = Factory::create(); + $context = []; + $extra = []; + for ($i = 0; $i < 5; $i++) { + $context[$faker->word] = $faker->word; + $extra[$faker->word] = $faker->word; + } + $recordArray = [ + 'message' => 'Test message', + 'context' => $context, + 'level' => 100, + 'level_name' => 'DEBUG', + 'channel' => $faker->word, + 'datetime' => DateTimeImmutable::createFromMutable($faker->dateTime()), + 'extra' => $extra, + ]; + $record = new Monolog\LogRecord( + $recordArray['datetime'], + $recordArray['channel'], + Monolog\Level::fromName($recordArray['level_name']), + $recordArray['message'], + $recordArray['context'], + $recordArray['extra']); + + $handler = new MySQL($this->connection); + $handler->write($record); + + $this->assertTrue(true); + } +}