This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
namespace MailPoet\Logging;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\LogEntity;
use MailPoetVendor\Monolog\Handler\AbstractProcessingHandler;
class LogHandler extends AbstractProcessingHandler {
/**
* Logs older than this many days will be deleted
*/
const DAYS_TO_KEEP_LOGS = 30;
/**
* How many records to delete on one run of purge routine
*/
const PURGE_LIMIT = 1000;
/**
* Percentage value, what is the probability of running purge routine
* @var int
*/
const LOG_PURGE_PROBABILITY = 5;
/** @var callable|null */
private $randFunction;
/** @var LogRepository */
private $logRepository;
public function __construct(
LogRepository $logRepository,
$level = \MailPoetVendor\Monolog\Logger::DEBUG,
$bubble = \true,
$randFunction = null
) {
parent::__construct($level, $bubble);
$this->randFunction = $randFunction;
$this->logRepository = $logRepository;
}
protected function write(array $record): void {
$message = is_string($record['formatted']) ? $record['formatted'] : null;
$entity = new LogEntity();
$entity->setName($record['channel']);
$entity->setLevel((int)$record['level']);
$entity->setMessage($message);
$entity->setCreatedAt($record['datetime']);
$entity->setRawMessage($record['message']);
$entity->setContext($record['context']);
$this->logRepository->saveLog($entity);
if ($this->getRandom() <= self::LOG_PURGE_PROBABILITY) {
$this->purgeOldLogs();
}
}
private function getRandom() {
if ($this->randFunction) {
return call_user_func($this->randFunction, 0, 100);
}
return rand(0, 100);
}
private function purgeOldLogs() {
$this->logRepository->purgeOldLogs(self::DAYS_TO_KEEP_LOGS, self::PURGE_LIMIT);
}
}
@@ -0,0 +1,139 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Logging;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\LogEntity;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\InvalidStateException;
use MailPoet\Util\Helpers;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
/**
* @extends Repository<LogEntity>
*/
class LogRepository extends Repository {
public function saveLog(LogEntity $log): void {
// Save log entity using DBAL to avoid calling "flush()" on the entity manager.
// Calling "flush()" can have unintended side effects, such as saving unwanted
// changes or trying to save entities that were detached from the entity manager.
$this->entityManager->getConnection()->insert(
$this->entityManager->getClassMetadata(LogEntity::class)->getTableName(),
[
'name' => $log->getName(),
'level' => $log->getLevel(),
'message' => $log->getMessage(),
'raw_message' => $log->getRawMessage(),
'context' => json_encode($log->getContext()),
'created_at' => (
$log->getCreatedAt() ?? Carbon::now()->millisecond(0)
)->format('Y-m-d H:i:s'),
],
);
// sync the changes with the entity manager
if ($this->entityManager->isOpen()) {
$lastInsertId = (int)$this->entityManager->getConnection()->lastInsertId();
$log->setId($lastInsertId);
$this->entityManager->getUnitOfWork()->registerManaged($log, ['id' => $log->getId()], []);
$this->entityManager->refresh($log);
}
}
/**
* @param \DateTimeInterface|null $dateFrom
* @param \DateTimeInterface|null $dateTo
* @param string|null $search
* @param string $offset
* @param string $limit
* @return LogEntity[]
*/
public function getLogs(
\DateTimeInterface $dateFrom = null,
\DateTimeInterface $dateTo = null,
string $search = null,
string $offset = null,
string $limit = null
): array {
$query = $this->doctrineRepository->createQueryBuilder('l')
->select('l');
if ($dateFrom instanceof \DateTimeInterface) {
$query
->andWhere('l.createdAt >= :dateFrom')
->setParameter('dateFrom', $dateFrom->format('Y-m-d 00:00:00'));
}
if ($dateTo instanceof \DateTimeInterface) {
$query
->andWhere('l.createdAt <= :dateTo')
->setParameter('dateTo', $dateTo->format('Y-m-d 23:59:59'));
}
if ($search) {
$search = Helpers::escapeSearch($search);
$query
->andWhere('l.name LIKE :search or l.message LIKE :search')
->setParameter('search', "%$search%");
}
$query->orderBy('l.createdAt', 'desc');
if ($offset !== null) {
$query->setFirstResult((int)$offset);
}
if ($limit === null) {
$query->setMaxResults(500);
} else {
$query->setMaxResults((int)$limit);
}
return $query->getQuery()->getResult();
}
public function purgeOldLogs(int $daysToKeepLogs, int $limit = 1000) {
$logsTable = $this->entityManager->getClassMetadata(LogEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement(
"
DELETE FROM $logsTable
WHERE `created_at` < :date
ORDER BY `id` ASC LIMIT :limit
",
[
'date' => Carbon::now()->subDays($daysToKeepLogs)->toDateTimeString(),
'limit' => $limit,
],
[
'date' => ParameterType::STRING,
'limit' => ParameterType::INTEGER,
]
);
}
public function getRawMessagesForNewsletter(NewsletterEntity $newsletter, string $topic): array {
return $this->entityManager->createQueryBuilder()
->select('DISTINCT logs.rawMessage message')
->from(LogEntity::class, 'logs')
->where('logs.name = :topic')
->andWhere('logs.context LIKE :context')
->orderBy('logs.createdAt')
->setParameter('context', json_encode(['newsletter_id' => $newsletter->getId()]))
->setParameter('topic', $topic)
->getQuery()
->getSingleColumnResult();
}
public function persist($entity): void {
throw new InvalidStateException('Use saveLog() instead to avoid unintended side effects');
}
public function flush(): void {
throw new InvalidStateException('Use saveLog() instead to avoid unintended side effects');
}
protected function getEntityClassName() {
return LogEntity::class;
}
}
@@ -0,0 +1,113 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Logging;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Settings\SettingsController;
use MailPoetVendor\Monolog\Processor\IntrospectionProcessor;
use MailPoetVendor\Monolog\Processor\MemoryUsageProcessor;
use MailPoetVendor\Monolog\Processor\WebProcessor;
/**
* Usage:
* $logger = Logger::getLogger('logger name');
* $logger->debug('This is a debug message');
* $logger->info('This is an info');
* $logger->warning('This is a warning');
* $logger->error('This is an error message');
*
* By default only errors are saved but can be changed in settings to save everything or nothing
*
* Name is anything which will be found in the log table.
* We can use it for separating different messages like: 'cron', 'rendering', 'export', ...
*
* If WP_DEBUG is true additional information will be added to every log message.
*/
class LoggerFactory {
const TOPIC_NEWSLETTERS = 'newsletters';
const TOPIC_POST_NOTIFICATIONS = 'post-notifications';
const TOPIC_MSS = 'mss';
const TOPIC_BRIDGE = 'bridge-api';
const TOPIC_SENDING = 'sending';
const TOPIC_CRON = 'cron';
const TOPIC_API = 'api';
const TOPIC_TRACKING = 'tracking';
const TOPIC_COUPONS = 'coupons';
const TOPIC_PROVISIONING = 'provisioning';
const TOPIC_SEGMENTS = 'segments';
/** @var LoggerFactory */
private static $instance;
/** @var \MailPoetVendor\Monolog\Logger[] */
private $loggerInstances = [];
/** @var SettingsController */
private $settings;
/** @var LogRepository */
private $logRepository;
public function __construct(
LogRepository $logRepository,
SettingsController $settings
) {
$this->settings = $settings;
$this->logRepository = $logRepository;
}
/**
* @param string $name
* @param bool $attachOptionalProcessors
*
* @return \MailPoetVendor\Monolog\Logger
*/
public function getLogger($name = 'MailPoet', $attachOptionalProcessors = WP_DEBUG) {
if (!isset($this->loggerInstances[$name])) {
$this->loggerInstances[$name] = new \MailPoetVendor\Monolog\Logger($name);
if ($attachOptionalProcessors) {
// Adds the line/file/class/method from which the log call originated
$this->loggerInstances[$name]->pushProcessor(new IntrospectionProcessor());
// Adds the current request URI, request method and client IP to a log record
$this->loggerInstances[$name]->pushProcessor(new WebProcessor());
// Adds the current memory usage to a log record
$this->loggerInstances[$name]->pushProcessor(new MemoryUsageProcessor());
}
// Adds the plugin's versions to the log, we always want to see this
$this->loggerInstances[$name]->pushProcessor(new PluginVersionProcessor());
$this->loggerInstances[$name]->pushHandler(new LogHandler(
$this->logRepository,
$this->getDefaultLogLevel()
));
}
return $this->loggerInstances[$name];
}
public static function getInstance() {
if (!self::$instance instanceof LoggerFactory) {
self::$instance = new LoggerFactory(
ContainerWrapper::getInstance()->get(LogRepository::class),
SettingsController::getInstance()
);
}
return self::$instance;
}
private function getDefaultLogLevel() {
$logLevel = $this->settings->get('logging', 'errors');
switch ($logLevel) {
case 'everything':
return \MailPoetVendor\Monolog\Logger::DEBUG;
case 'nothing':
return \MailPoetVendor\Monolog\Logger::EMERGENCY;
default:
return \MailPoetVendor\Monolog\Logger::ERROR;
}
}
}
@@ -0,0 +1,17 @@
<?php declare (strict_types = 1);
namespace MailPoet\Logging;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
use MailPoetVendor\Monolog\Processor\ProcessorInterface;
class PluginVersionProcessor implements ProcessorInterface {
public function __invoke(array $record): array {
$record['extra']['free_plugin_version'] = Env::$version;
$record['extra']['premium_plugin_version'] = defined('MAILPOET_PREMIUM_VERSION') ? MAILPOET_PREMIUM_VERSION : 'premium not installed';
return $record;
}
}
@@ -0,0 +1 @@
<?php