. */ namespace Xibo\Helper; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; use Xibo\Storage\PdoStorageService; /** * Class DatabaseLogHandler * @package Xibo\Helper */ class DatabaseLogHandler extends AbstractProcessingHandler { /** @var \PDO */ private static $pdo; /** @var \PDOStatement|null */ private static $statement; /** @var int Log Level */ protected $level = Logger::ERROR; /** @var int Track the number of failures since a success */ private $failureCount = 0; /** * @param int $level The minimum logging level at which this handler will be triggered */ public function __construct($level = Logger::ERROR) { parent::__construct($level); } /** * Gets minimum logging level at which this handler will be triggered. * * @return int */ public function getLevel(): int { return $this->level; } /** * @param int|string $level * @return $this|\Monolog\Handler\AbstractHandler */ public function setLevel($level): \Monolog\Handler\AbstractHandler { $this->level = Logger::toMonologLevel($level); return $this; } /** * @inheritDoc * @throws \Exception */ protected function write(array $record): void { if (self::$statement == null) { self::$pdo = PdoStorageService::newConnection('log'); $SQL = ' INSERT INTO `log` ( `runNo`, `logdate`, `channel`, `type`, `page`, `function`, `message`, `userid`, `displayid`, `sessionHistoryId`, `requestId` ) VALUES ( :runNo, :logdate, :channel, :type, :page, :function, :message, :userid, :displayid, :sessionHistoryId, :requestId ) '; self::$statement = self::$pdo->prepare($SQL); } $params = [ 'runNo' => $record['extra']['uid'] ?? '', 'logdate' => $record['datetime']->format('Y-m-d H:i:s'), 'type' => $record['level_name'], 'channel' => $record['channel'], 'page' => $record['extra']['route'] ?? '', 'function' => $record['extra']['method'] ?? '', 'message' => $record['message'], 'userid' => $record['extra']['userId'] ?? 0, 'displayid' => $record['extra']['displayId'] ?? 0, 'sessionHistoryId' => $record['extra']['sessionHistoryId'] ?? 0, 'requestId' => $record['extra']['requestId'] ?? 0, ]; try { // Insert self::$statement->execute($params); // Reset failure count $this->failureCount = 0; // Successful write PdoStorageService::incrementStat('log', 'insert'); } catch (\Exception $e) { // Increment failure count $this->failureCount++; // Try to create a new statement if ($this->failureCount <= 1) { // Clear the stored statement, and try again // this will rebuild the connection self::$statement = null; // Try again. $this->write($record); } // If the failureCount is > 1, then we ignore the error. } } /** * Deleting logs must happen on the same DB connection as the log handler writes logs * otherwise we can end up with a deadlock where the log handler has written things, locked the table * and, we're then trying to get the same lock. * @param string $cutOff */ public static function tidyLogs(string $cutOff): void { try { if (self::$pdo === null) { self::$pdo = PdoStorageService::newConnection('log'); } $statement = self::$pdo->prepare('DELETE FROM `log` WHERE logdate < :maxage LIMIT 10000'); do { // Execute statement $statement->execute(['maxage' => $cutOff]); // initialize number of rows deleted $rowsDeleted = $statement->rowCount(); PdoStorageService::incrementStat('log', 'delete'); // pause for a second sleep(2); } while ($rowsDeleted > 0); } catch (\PDOException) { } } }