diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7066557e..e62257e3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -4,64 +4,218 @@ on: - push - pull_request -env: - COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" - jobs: tests: name: "CI" - runs-on: ubuntu-latest + runs-on: "${{ matrix.operating-system }}" strategy: + fail-fast: false + matrix: php-version: - "8.1" + dependencies: [highest] + + operating-system: + - "ubuntu-latest" + include: - php-version: "8.1" dependencies: lowest + operating-system: ubuntu-latest steps: - name: "Checkout" uses: "actions/checkout@v2" + - name: Run CouchDB + timeout-minutes: 1 + continue-on-error: true + uses: "cobot/couchdb-action@master" + with: + couchdb version: '2.3.1' + + - name: Run MongoDB + uses: supercharge/mongodb-github-action@1.7.0 + with: + mongodb-version: 5.0 + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" extensions: mongodb, redis, amqp - - - name: Get composer cache directory - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- + tools: "composer:v2" + ini-values: "memory_limit=-1" - name: Add require for mongodb/mongodb to make tests runnable - run: 'composer require ${{ env.COMPOSER_FLAGS }} mongodb/mongodb --dev --no-update' + run: 'composer require mongodb/mongodb --dev --no-update' - - name: "Handle lowest dependencies update" - if: "contains(matrix.dependencies, 'lowest')" - run: "echo \"COMPOSER_FLAGS=$COMPOSER_FLAGS --prefer-lowest\" >> $GITHUB_ENV" - - - name: "Install latest dependencies" + - name: "Change dependencies" run: | - composer update ${{ env.COMPOSER_FLAGS }} + composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^7 + composer config --no-plugins allow-plugins.ocramius/package-versions true + + - name: "Update dependencies with composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" - name: "Run tests" - run: "composer exec phpunit -- --verbose" + run: "composer exec phpunit -- --exclude-group Elasticsearch,Elastica --verbose" - name: "Run tests with psr/log 3" if: "contains(matrix.dependencies, 'highest') && matrix.php-version >= '8.0'" run: | composer remove --no-update --dev graylog2/gelf-php ruflin/elastica elasticsearch/elasticsearch rollbar/rollbar composer require --no-update psr/log:^3 - composer update -W ${{ env.COMPOSER_FLAGS }} - composer exec phpunit -- --verbose + composer update -W + composer exec phpunit -- --exclude-group Elasticsearch,Elastica --verbose + + tests-es-7: + name: "CI with ES ${{ matrix.es-version }} on PHP ${{ matrix.php-version }}" + + needs: "tests" + + runs-on: "${{ matrix.operating-system }}" + + strategy: + fail-fast: false + + matrix: + operating-system: + - "ubuntu-latest" + + php-version: + - "8.1" + + dependencies: + - "highest" + - "lowest" + + es-version: + - "7.0.0" + - "7.17.0" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + # required for elasticsearch + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - name: Run Elasticsearch + timeout-minutes: 1 + uses: elastic/elastic-github-actions/elasticsearch@master + with: + stack-version: "${{ matrix.es-version }}" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mongodb, redis, amqp + tools: "composer:v2" + ini-values: "memory_limit=-1" + + - name: "Change dependencies" + run: "composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^${{ matrix.es-version }}" + + - name: "Update dependencies with composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run tests" + run: "composer exec phpunit -- --group Elasticsearch,Elastica --verbose" + + - name: "Run tests with psr/log 3" + if: "contains(matrix.dependencies, 'highest') && matrix.php-version >= '8.0'" + run: | + composer remove --no-update --dev graylog2/gelf-php ruflin/elastica elasticsearch/elasticsearch rollbar/rollbar + composer require --no-update --no-interaction --dev ruflin/elastica elasticsearch/elasticsearch:^7 + composer require --no-update psr/log:^3 + composer update -W + composer exec phpunit -- --group Elasticsearch,Elastica --verbose + + tests-es-8: + name: "CI with ES ${{ matrix.es-version }} on PHP ${{ matrix.php-version }}" + + needs: "tests" + + runs-on: "${{ matrix.operating-system }}" + + strategy: + fail-fast: false + + matrix: + operating-system: + - "ubuntu-latest" + + php-version: + - "8.1" + + dependencies: + - "highest" + - "lowest" + + es-version: + - "8.0.0" + - "8.2.0" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + # required for elasticsearch + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - name: Run Elasticsearch + timeout-minutes: 1 + uses: elastic/elastic-github-actions/elasticsearch@master + with: + stack-version: "${{ matrix.es-version }}" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mongodb, redis, amqp + tools: "composer:v2" + ini-values: "memory_limit=-1" + + - name: "Change dependencies" + run: | + composer remove --no-update --dev graylog2/gelf-php ruflin/elastica elasticsearch/elasticsearch rollbar/rollbar + composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^8 + + - name: "Update dependencies with composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run tests" + run: "composer exec phpunit -- --group Elasticsearch,Elastica --verbose" + + - name: "Run tests with psr/log 3" + if: "contains(matrix.dependencies, 'highest') && matrix.php-version >= '8.0'" + run: | + composer require --no-update psr/log:^3 + composer update -W + composer exec phpunit -- --group Elasticsearch,Elastica --verbose diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bac21998..5011a321 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,4 +28,4 @@ jobs: php-version: "${{ matrix.php-version }}" - name: "Lint PHP files" - run: "find src/ -type f -name '*.php' -not -name AsMonologProcessor.php -print0 | xargs -0 -L1 -P4 -- php -l -f" + run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dcc7a14..eb4963ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 2.5.0 (2022-04-08) + +* Added `callType` to IntrospectionProcessor (#1612) +* Fixed AsMonologProcessor syntax to be compatible with PHP 7.2 (#1651) + ### 2.4.0 (2022-03-14) * Added [`Monolog\LogRecord`](src/Monolog/LogRecord.php) interface that can be used to type-hint records like `array|\Monolog\LogRecord $record` to be forward compatible with the upcoming Monolog 3 changes diff --git a/composer.json b/composer.json index 3b1b8a10..0f19dd15 100644 --- a/composer.json +++ b/composer.json @@ -17,20 +17,25 @@ "psr/log": "^2.0 || ^3.0" }, "require-dev": { + "ext-json": "*", "aws/aws-sdk-php": "^3.0", "doctrine/couchdb": "~1.0@dev", - "elasticsearch/elasticsearch": "^7", + "elasticsearch/elasticsearch": "^7 || ^8", "graylog2/gelf-php": "^1.4.2", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.3", - "phpspec/prophecy": "^1.6.1", + "phpspec/prophecy": "^1.15", "phpstan/phpstan": "^1.4", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^9.5.16", "predis/predis": "^1.1", - "ruflin/elastica": ">=0.90@dev" + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" }, "suggest": { "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1dfa52e8..663b27a5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,7 +6,7 @@ parameters: path: src/Monolog/ErrorHandler.php - - message: "#^Cannot access offset 'table' on array\\\\|bool\\|float\\|int\\|object\\|string\\|null\\.$#" + message: "#^Cannot access offset 'table' on array\\\\|bool\\|float\\|int\\|object\\|string\\.$#" count: 1 path: src/Monolog/Formatter/WildfireFormatter.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a7442c90..fa2f6fd9 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -22,6 +22,9 @@ parameters: # can be removed when rollbar/rollbar can be added as dev require again (needs to allow monolog 3.x) - '#Rollbar\\RollbarLogger#' + # legacy elasticsearch namespace failures + - '# Elastic\\Elasticsearch\\#' + includes: - phpstan-baseline.neon - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/src/Monolog/Attribute/AsMonologProcessor.php b/src/Monolog/Attribute/AsMonologProcessor.php index 9781efe0..f8b25021 100644 --- a/src/Monolog/Attribute/AsMonologProcessor.php +++ b/src/Monolog/Attribute/AsMonologProcessor.php @@ -28,9 +28,9 @@ class AsMonologProcessor * @param string|null $method The method that processes the records (if the attribute is used at the class level). */ public function __construct( - public ?string $channel = null, - public ?string $handler = null, - public ?string $method = null, + public readonly ?string $channel = null, + public readonly ?string $handler = null, + public readonly ?string $method = null ) { } } diff --git a/src/Monolog/Handler/ElasticsearchHandler.php b/src/Monolog/Handler/ElasticsearchHandler.php index 55679afb..0a4fed35 100644 --- a/src/Monolog/Handler/ElasticsearchHandler.php +++ b/src/Monolog/Handler/ElasticsearchHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\LevelName; +use Elastic\Elasticsearch\Response\Elasticsearch; use Throwable; use RuntimeException; use Monolog\Level; @@ -21,6 +22,8 @@ use InvalidArgumentException; use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; use Elasticsearch\Client; use Monolog\LogRecord; +use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException; +use Elastic\Elasticsearch\Client as Client8; /** * Elasticsearch handler @@ -55,7 +58,7 @@ use Monolog\LogRecord; */ class ElasticsearchHandler extends AbstractProcessingHandler { - protected Client $client; + protected Client|Client8 $client; /** * @var mixed[] Handler config options @@ -64,12 +67,17 @@ class ElasticsearchHandler extends AbstractProcessingHandler protected array $options; /** - * @param Client $client Elasticsearch Client object - * @param mixed[] $options Handler configuration + * @var bool + */ + private $needsType; + + /** + * @param Client|Client8 $client Elasticsearch Client object + * @param mixed[] $options Handler configuration * * @phpstan-param InputOptions $options */ - public function __construct(Client $client, array $options = [], int|string|Level|LevelName $level = Level::Debug, bool $bubble = true) + public function __construct(Client|Client8 $client, array $options = [], int|string|Level|LevelName $level = Level::Debug, bool $bubble = true) { parent::__construct($level, $bubble); $this->client = $client; @@ -81,6 +89,14 @@ class ElasticsearchHandler extends AbstractProcessingHandler ], $options ); + + if ($client instanceof Client8 || $client::VERSION[0] === '7') { + $this->needsType = false; + // force the type to _doc for ES8/ES7 + $this->options['type'] = '_doc'; + } else { + $this->needsType = true; + } } /** @@ -147,9 +163,11 @@ class ElasticsearchHandler extends AbstractProcessingHandler foreach ($records as $record) { $params['body'][] = [ - 'index' => [ + 'index' => $this->needsType ? [ '_index' => $record['_index'], '_type' => $record['_type'], + ] : [ + '_index' => $record['_index'], ], ]; unset($record['_index'], $record['_type']); @@ -157,6 +175,7 @@ class ElasticsearchHandler extends AbstractProcessingHandler $params['body'][] = $record; } + /** @var Elasticsearch */ $responses = $this->client->bulk($params); if ($responses['errors'] === true) { @@ -174,9 +193,9 @@ class ElasticsearchHandler extends AbstractProcessingHandler * * Only the first error is converted into an exception. * - * @param mixed[] $responses returned by $this->client->bulk() + * @param mixed[]|Elasticsearch $responses returned by $this->client->bulk() */ - protected function createExceptionFromResponses(array $responses): ElasticsearchRuntimeException + protected function createExceptionFromResponses($responses): Throwable { foreach ($responses['items'] ?? [] as $item) { if (isset($item['index']['error'])) { @@ -184,6 +203,10 @@ class ElasticsearchHandler extends AbstractProcessingHandler } } + if (class_exists(ElasticInvalidArgumentException::class)) { + return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.'); + } + return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.'); } @@ -192,10 +215,14 @@ class ElasticsearchHandler extends AbstractProcessingHandler * * @param mixed[] $error */ - protected function createExceptionFromError(array $error): ElasticsearchRuntimeException + protected function createExceptionFromError(array $error): Throwable { $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null; + if (class_exists(ElasticInvalidArgumentException::class)) { + return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous); + } + return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous); } } diff --git a/src/Monolog/Handler/SymfonyMailerHandler.php b/src/Monolog/Handler/SymfonyMailerHandler.php new file mode 100644 index 00000000..4062aae2 --- /dev/null +++ b/src/Monolog/Handler/SymfonyMailerHandler.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Closure; +use Monolog\Level; +use Monolog\LevelName; +use Monolog\Logger; +use Monolog\LogRecord; +use Monolog\Utils; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Email; + +/** + * SymfonyMailerHandler uses Symfony's Mailer component to send the emails + * + * @author Jordi Boggiano + */ +class SymfonyMailerHandler extends MailHandler +{ + protected MailerInterface|TransportInterface $mailer; + /** @var Email|Closure(string, LogRecord[]): Email */ + private Email|Closure $emailTemplate; + + /** + * @phpstan-param Email|Closure(string, LogRecord[]): Email $email + * + * @param MailerInterface|TransportInterface $mailer The mailer to use + * @param Closure|Email $email An email template, the subject/body will be replaced + */ + public function __construct($mailer, Email|Closure $email, int|string|Level|LevelName $level = Level::Error, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->emailTemplate = $email; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string|null $format The format of the subject + */ + protected function getSubjectFormatter(?string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Email to be sent + * + * @param string $content formatted email body to be sent + * @param LogRecord[] $records Log records that formed the content + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->emailTemplate instanceof Email) { + $message = clone $this->emailTemplate; + } elseif (is_callable($this->emailTemplate)) { + $message = ($this->emailTemplate)($content, $records); + } + + if (!$message instanceof Email) { + $record = reset($records); + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record instanceof LogRecord ? Utils::getRecordMessageForException($record) : '')); + } + + if (\count($records) > 0) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->isHtmlBody($content)) { + if (null !== ($charset = $message->getHtmlCharset())) { + $message->html($content, $charset); + } else { + $message->html($content); + } + } else { + if (null !== ($charset = $message->getTextCharset())) { + $message->text($content, $charset); + } else { + $message->text($content); + } + } + + return $message->date(new \DateTimeImmutable()); + } +} diff --git a/src/Monolog/Test/TestCase.php b/src/Monolog/Test/TestCase.php index be402502..30ce4da5 100644 --- a/src/Monolog/Test/TestCase.php +++ b/src/Monolog/Test/TestCase.php @@ -26,6 +26,15 @@ use Psr\Log\LogLevel; */ class TestCase extends \PHPUnit\Framework\TestCase { + public function tearDown(): void + { + parent::tearDown(); + + if (isset($this->handler)) { + unset($this->handler); + } + } + /** * @param array $context * @param array $extra diff --git a/tests/Monolog/Formatter/ScalarFormatterTest.php b/tests/Monolog/Formatter/ScalarFormatterTest.php index e12bbc03..5f36fca7 100644 --- a/tests/Monolog/Formatter/ScalarFormatterTest.php +++ b/tests/Monolog/Formatter/ScalarFormatterTest.php @@ -23,6 +23,13 @@ class ScalarFormatterTest extends TestCase $this->formatter = new ScalarFormatter(); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->formatter); + } + public function buildTrace(\Exception $e) { $data = []; diff --git a/tests/Monolog/Handler/AmqpHandlerTest.php b/tests/Monolog/Handler/AmqpHandlerTest.php index 32172705..54cb90ef 100644 --- a/tests/Monolog/Handler/AmqpHandlerTest.php +++ b/tests/Monolog/Handler/AmqpHandlerTest.php @@ -78,7 +78,7 @@ class AmqpHandlerTest extends TestCase public function testHandlePhpAmqpLib() { - if (!class_exists('PhpAmqpLib\Connection\AMQPConnection')) { + if (!class_exists('PhpAmqpLib\Channel\AMQPChannel')) { $this->markTestSkipped("php-amqplib not installed"); } diff --git a/tests/Monolog/Handler/DynamoDbHandlerTest.php b/tests/Monolog/Handler/DynamoDbHandlerTest.php index d8ace447..33cf62c3 100644 --- a/tests/Monolog/Handler/DynamoDbHandlerTest.php +++ b/tests/Monolog/Handler/DynamoDbHandlerTest.php @@ -47,6 +47,13 @@ class DynamoDbHandlerTest extends TestCase $this->client = $clientMockBuilder->getMock(); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->client); + } + public function testGetFormatter() { $handler = new DynamoDbHandler($this->client, 'foo'); diff --git a/tests/Monolog/Handler/ElasticaHandlerTest.php b/tests/Monolog/Handler/ElasticaHandlerTest.php index a63b9360..6cc705d4 100644 --- a/tests/Monolog/Handler/ElasticaHandlerTest.php +++ b/tests/Monolog/Handler/ElasticaHandlerTest.php @@ -19,6 +19,9 @@ use Elastica\Client; use Elastica\Request; use Elastica\Response; +/** + * @group Elastica + */ class ElasticaHandlerTest extends TestCase { /** @@ -48,6 +51,13 @@ class ElasticaHandlerTest extends TestCase ->getMock(); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->client); + } + /** * @covers Monolog\Handler\ElasticaHandler::write * @covers Monolog\Handler\ElasticaHandler::handleBatch @@ -144,52 +154,6 @@ class ElasticaHandlerTest extends TestCase ]; } - /** - * Integration test using localhost Elastic Search server version <7 - * - * @covers Monolog\Handler\ElasticaHandler::__construct - * @covers Monolog\Handler\ElasticaHandler::handleBatch - * @covers Monolog\Handler\ElasticaHandler::bulkSend - * @covers Monolog\Handler\ElasticaHandler::getDefaultFormatter - */ - public function testHandleIntegration() - { - $msg = $this->getRecord(Level::Error, 'log', context: ['foo' => 7, 'bar', 'class' => new \stdClass], datetime: new \DateTimeImmutable("@0")); - - $expected = $msg->toArray(); - $expected['datetime'] = $msg['datetime']->format(\DateTime::ISO8601); - $expected['context'] = [ - 'class' => '[object] (stdClass: {})', - 'foo' => 7, - 0 => 'bar', - ]; - - $client = new Client(); - $handler = new ElasticaHandler($client, $this->options); - - try { - $handler->handleBatch([$msg]); - } catch (\RuntimeException $e) { - $this->markTestSkipped("Cannot connect to Elastic Search server on localhost"); - } - - // check document id from ES server response - $documentId = $this->getCreatedDocId($client->getLastResponse()); - $this->assertNotEmpty($documentId, 'No elastic document id received'); - - // retrieve document source from ES and validate - $document = $this->getDocSourceFromElastic( - $client, - $this->options['index'], - $this->options['type'], - $documentId - ); - $this->assertEquals($expected, $document); - - // remove test index from ES - $client->request("/{$this->options['index']}", Request::DELETE); - } - /** * Integration test using localhost Elastic Search server version 7+ * @@ -210,7 +174,9 @@ class ElasticaHandlerTest extends TestCase 0 => 'bar', ]; - $client = new Client(); + $clientOpts = ['url' => 'http://elastic:changeme@127.0.0.1:9200']; + $client = new Client($clientOpts); + $handler = new ElasticaHandler($client, $this->options); try { @@ -243,9 +209,14 @@ class ElasticaHandlerTest extends TestCase protected function getCreatedDocId(Response $response): ?string { $data = $response->getData(); - if (!empty($data['items'][0]['create']['_id'])) { - return $data['items'][0]['create']['_id']; + + if (!empty($data['items'][0]['index']['_id'])) { + return $data['items'][0]['index']['_id']; } + + var_dump('Unexpected response: ', $data); + + return null; } /** diff --git a/tests/Monolog/Handler/ElasticsearchHandlerTest.php b/tests/Monolog/Handler/ElasticsearchHandlerTest.php index 792c6dc9..0b4447d9 100644 --- a/tests/Monolog/Handler/ElasticsearchHandlerTest.php +++ b/tests/Monolog/Handler/ElasticsearchHandlerTest.php @@ -11,17 +11,22 @@ namespace Monolog\Handler; -use Elasticsearch\ClientBuilder; use Monolog\Formatter\ElasticsearchFormatter; use Monolog\Formatter\NormalizerFormatter; use Monolog\Test\TestCase; use Monolog\Level; use Elasticsearch\Client; +use Elastic\Elasticsearch\Client as Client8; +use Elasticsearch\ClientBuilder; +use Elastic\Elasticsearch\ClientBuilder as ClientBuilder8; +/** + * @group Elasticsearch + */ class ElasticsearchHandlerTest extends TestCase { /** - * @var Client mock + * @var Client|Client8 mock */ protected Client $client; @@ -35,55 +40,23 @@ class ElasticsearchHandlerTest extends TestCase public function setUp(): void { - // Elasticsearch lib required - if (!class_exists('Elasticsearch\Client')) { - $this->markTestSkipped('elasticsearch/elasticsearch not installed'); - } + $hosts = ['http://elastic:changeme@127.0.0.1:9200']; + $this->client = $this->getClientBuilder() + ->setHosts($hosts) + ->build(); - // base mock Elasticsearch Client object - $this->client = $this->getMockBuilder('Elasticsearch\Client') - ->onlyMethods(['bulk']) - ->disableOriginalConstructor() - ->getMock(); + try { + $this->client->info(); + } catch (\Throwable $e) { + $this->markTestSkipped('Could not connect to Elasticsearch on 127.0.0.1:9200'); + } } - /** - * @covers Monolog\Handler\ElasticsearchHandler::write - * @covers Monolog\Handler\ElasticsearchHandler::handleBatch - * @covers Monolog\Handler\ElasticsearchHandler::bulkSend - * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter - */ - public function testHandle() + public function tearDown(): void { - // log message - $msg = $this->getRecord(Level::Error, 'log', context: ['foo' => 7, 'bar', 'class' => new \stdClass], datetime: new \DateTimeImmutable("@0")); + parent::tearDown(); - // format expected result - $formatter = new ElasticsearchFormatter($this->options['index'], $this->options['type']); - $data = $formatter->format($msg); - unset($data['_index'], $data['_type']); - - $expected = [ - 'body' => [ - [ - 'index' => [ - '_index' => $this->options['index'], - '_type' => $this->options['type'], - ], - ], - $data, - ], - ]; - - // setup ES client mock - $this->client->expects($this->any()) - ->method('bulk') - ->with($expected); - - // perform tests - $handler = new ElasticsearchHandler($this->client, $this->options); - $handler->handle($msg); - $handler->handleBatch([$msg]); + unset($this->client); } /** @@ -100,7 +73,7 @@ class ElasticsearchHandlerTest extends TestCase } /** - * @covers Monolog\Handler\ElasticsearchHandler::setFormatter + * @covers Monolog\Handler\ElasticsearchHandler::setFormatter */ public function testSetFormatterInvalid() { @@ -124,6 +97,11 @@ class ElasticsearchHandlerTest extends TestCase 'type' => $this->options['type'], 'ignore_error' => false, ]; + + if ($this->client instanceof Client8 || $this->client::VERSION[0] === '7') { + $expected['type'] = '_doc'; + } + $handler = new ElasticsearchHandler($this->client, $this->options); $this->assertEquals($expected, $handler->getOptions()); } @@ -134,10 +112,10 @@ class ElasticsearchHandlerTest extends TestCase */ public function testConnectionErrors($ignore, $expectedError) { - $hosts = [['host' => '127.0.0.1', 'port' => 1]]; - $client = ClientBuilder::create() - ->setHosts($hosts) - ->build(); + $hosts = ['http://127.0.0.1:1']; + $client = $this->getClientBuilder() + ->setHosts($hosts) + ->build(); $handlerOpts = ['ignore_error' => $ignore]; $handler = new ElasticsearchHandler($client, $handlerOpts); @@ -167,7 +145,7 @@ class ElasticsearchHandlerTest extends TestCase * @covers Monolog\Handler\ElasticsearchHandler::bulkSend * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter */ - public function testHandleIntegration() + public function testHandleBatchIntegration() { $msg = $this->getRecord(Level::Error, 'log', context: ['foo' => 7, 'bar', 'class' => new \stdClass], datetime: new \DateTimeImmutable("@0")); @@ -179,21 +157,26 @@ class ElasticsearchHandlerTest extends TestCase 0 => 'bar', ]; - $hosts = [['host' => '127.0.0.1', 'port' => 9200]]; - $client = ClientBuilder::create() + $hosts = ['http://elastic:changeme@127.0.0.1:9200']; + $client = $this->getClientBuilder() ->setHosts($hosts) ->build(); $handler = new ElasticsearchHandler($client, $this->options); - - try { - $handler->handleBatch([$msg]); - } catch (\RuntimeException $e) { - $this->markTestSkipped('Cannot connect to Elasticsearch server on localhost'); - } + $handler->handleBatch([$msg]); // check document id from ES server response - $documentId = $this->getCreatedDocId($client->transport->getLastConnection()->getLastRequestInfo()); - $this->assertNotEmpty($documentId, 'No elastic document id received'); + if ($client instanceof Client8) { + $messageBody = $client->getTransport()->getLastResponse()->getBody(); + + $info = json_decode((string) $messageBody, true); + $this->assertNotNull($info, 'Decoding failed'); + + $documentId = $this->getCreatedDocIdV8($info); + $this->assertNotEmpty($documentId, 'No elastic document id received'); + } else { + $documentId = $this->getCreatedDocId($client->transport->getLastConnection()->getLastRequestInfo()); + $this->assertNotEmpty($documentId, 'No elastic document id received'); + } // retrieve document source from ES and validate $document = $this->getDocSourceFromElastic( @@ -221,21 +204,41 @@ class ElasticsearchHandlerTest extends TestCase if (!empty($data['items'][0]['index']['_id'])) { return $data['items'][0]['index']['_id']; } + + return null; + } + + /** + * Return last created document id from ES response + * + * @param array $data Elasticsearch last request info + * @return string|null + */ + protected function getCreatedDocIdV8(array $data) + { + if (!empty($data['items'][0]['index']['_id'])) { + return $data['items'][0]['index']['_id']; + } + + return null; } /** * Retrieve document by id from Elasticsearch * - * @param Client $client Elasticsearch client + * @return array */ - protected function getDocSourceFromElastic(Client $client, string $index, string $type, string $documentId): array + protected function getDocSourceFromElastic(Client|Client8 $client, string $index, string $type, string $documentId): array { $params = [ 'index' => $index, - 'type' => $type, 'id' => $documentId, ]; + if (!$client instanceof Client8 && $client::VERSION[0] !== '7') { + $params['type'] = $type; + } + $data = $client->get($params); if (!empty($data['_source'])) { @@ -244,4 +247,16 @@ class ElasticsearchHandlerTest extends TestCase return []; } + + /** + * @return ClientBuilder|ClientBuilder8 + */ + private function getClientBuilder() + { + if (class_exists(ClientBuilder8::class)) { + return ClientBuilder8::create(); + } + + return ClientBuilder::create(); + } } diff --git a/tests/Monolog/Handler/FlowdockHandlerTest.php b/tests/Monolog/Handler/FlowdockHandlerTest.php index 7c630034..0623ef1f 100644 --- a/tests/Monolog/Handler/FlowdockHandlerTest.php +++ b/tests/Monolog/Handler/FlowdockHandlerTest.php @@ -35,6 +35,13 @@ class FlowdockHandlerTest extends TestCase } } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testWriteHeader() { $this->createHandler(); diff --git a/tests/Monolog/Handler/HandlerWrapperTest.php b/tests/Monolog/Handler/HandlerWrapperTest.php index 51e80b31..7212081e 100644 --- a/tests/Monolog/Handler/HandlerWrapperTest.php +++ b/tests/Monolog/Handler/HandlerWrapperTest.php @@ -30,6 +30,13 @@ class HandlerWrapperTest extends TestCase $this->wrapper = new HandlerWrapper($this->handler); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->wrapper); + } + public function trueFalseDataProvider(): array { return [ diff --git a/tests/Monolog/Handler/InsightOpsHandlerTest.php b/tests/Monolog/Handler/InsightOpsHandlerTest.php index 213d89ed..b867a0b8 100644 --- a/tests/Monolog/Handler/InsightOpsHandlerTest.php +++ b/tests/Monolog/Handler/InsightOpsHandlerTest.php @@ -28,6 +28,13 @@ class InsightOpsHandlerTest extends TestCase private InsightOpsHandler&MockObject $handler; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->resource); + } + public function testWriteContent() { $this->createHandler(); diff --git a/tests/Monolog/Handler/LogEntriesHandlerTest.php b/tests/Monolog/Handler/LogEntriesHandlerTest.php index 29b8c88c..fa1d4484 100644 --- a/tests/Monolog/Handler/LogEntriesHandlerTest.php +++ b/tests/Monolog/Handler/LogEntriesHandlerTest.php @@ -27,6 +27,13 @@ class LogEntriesHandlerTest extends TestCase private LogEntriesHandler&MockObject $handler; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testWriteContent() { $this->createHandler(); diff --git a/tests/Monolog/Handler/LogmaticHandlerTest.php b/tests/Monolog/Handler/LogmaticHandlerTest.php index e8ecfcde..e35f86ac 100644 --- a/tests/Monolog/Handler/LogmaticHandlerTest.php +++ b/tests/Monolog/Handler/LogmaticHandlerTest.php @@ -27,6 +27,13 @@ class LogmaticHandlerTest extends TestCase private LogmaticHandler&MockObject $handler; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testWriteContent() { $this->createHandler(); diff --git a/tests/Monolog/Handler/PHPConsoleHandlerTest.php b/tests/Monolog/Handler/PHPConsoleHandlerTest.php index f99aba86..3dc3d18e 100644 --- a/tests/Monolog/Handler/PHPConsoleHandlerTest.php +++ b/tests/Monolog/Handler/PHPConsoleHandlerTest.php @@ -54,6 +54,13 @@ class PHPConsoleHandlerTest extends TestCase $this->connector->setErrorsDispatcher($this->errorDispatcher); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->connector, $this->debugDispatcher, $this->errorDispatcher); + } + protected function initDebugDispatcherMock(Connector $connector) { return $this->getMockBuilder('PhpConsole\Dispatcher\Debug') diff --git a/tests/Monolog/Handler/PushoverHandlerTest.php b/tests/Monolog/Handler/PushoverHandlerTest.php index 11f3cd24..4c3c9481 100644 --- a/tests/Monolog/Handler/PushoverHandlerTest.php +++ b/tests/Monolog/Handler/PushoverHandlerTest.php @@ -27,6 +27,13 @@ class PushoverHandlerTest extends TestCase private $res; private PushoverHandler&MockObject $handler; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testWriteHeader() { $this->createHandler(); diff --git a/tests/Monolog/Handler/RollbarHandlerTest.php b/tests/Monolog/Handler/RollbarHandlerTest.php index 1454896c..03eda4cf 100644 --- a/tests/Monolog/Handler/RollbarHandlerTest.php +++ b/tests/Monolog/Handler/RollbarHandlerTest.php @@ -38,6 +38,13 @@ class RollbarHandlerTest extends TestCase $this->setupRollbarLoggerMock(); } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->rollbarLogger, $this->reportedExceptionArguments); + } + /** * When reporting exceptions to Rollbar the * level has to be set in the payload data diff --git a/tests/Monolog/Handler/RotatingFileHandlerTest.php b/tests/Monolog/Handler/RotatingFileHandlerTest.php index 02286bbb..a5dd03aa 100644 --- a/tests/Monolog/Handler/RotatingFileHandlerTest.php +++ b/tests/Monolog/Handler/RotatingFileHandlerTest.php @@ -39,6 +39,18 @@ class RotatingFileHandlerTest extends TestCase }); } + public function tearDown(): void + { + parent::tearDown(); + + foreach (glob(__DIR__.'/Fixtures/*.rot') as $file) { + unlink($file); + } + restore_error_handler(); + + unset($this->lastError); + } + private function assertErrorWasTriggered($code, $message) { if (empty($this->lastError)) { @@ -239,12 +251,4 @@ class RotatingFileHandlerTest extends TestCase $handler->handle($this->getRecord()); $this->assertEquals('footest', file_get_contents($log)); } - - public function tearDown(): void - { - foreach (glob(__DIR__.'/Fixtures/*.rot') as $file) { - unlink($file); - } - restore_error_handler(); - } } diff --git a/tests/Monolog/Handler/SlackHandlerTest.php b/tests/Monolog/Handler/SlackHandlerTest.php index c2d93cb1..6d1bbb13 100644 --- a/tests/Monolog/Handler/SlackHandlerTest.php +++ b/tests/Monolog/Handler/SlackHandlerTest.php @@ -36,6 +36,13 @@ class SlackHandlerTest extends TestCase } } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testWriteHeader() { $this->createHandler(); diff --git a/tests/Monolog/Handler/SocketHandlerTest.php b/tests/Monolog/Handler/SocketHandlerTest.php index 02d469c9..bb7ab9ba 100644 --- a/tests/Monolog/Handler/SocketHandlerTest.php +++ b/tests/Monolog/Handler/SocketHandlerTest.php @@ -27,6 +27,13 @@ class SocketHandlerTest extends TestCase */ private $res; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->res); + } + public function testInvalidHostname() { $this->expectException(\UnexpectedValueException::class); diff --git a/tests/Monolog/Handler/StreamHandlerTest.php b/tests/Monolog/Handler/StreamHandlerTest.php index f763e302..2d93b30c 100644 --- a/tests/Monolog/Handler/StreamHandlerTest.php +++ b/tests/Monolog/Handler/StreamHandlerTest.php @@ -270,20 +270,20 @@ STRING; $this->markTestSkipped('We could not set a memory limit that would trigger the error.'); } - $stream = tmpfile(); + try { + $stream = tmpfile(); - if ($stream === false) { - $this->markTestSkipped('We could not create a temp file to be use as a stream.'); + if ($stream === false) { + $this->markTestSkipped('We could not create a temp file to be use as a stream.'); + } + + $handler = new StreamHandler($stream); + stream_get_contents($stream, 1024); + + $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize()); + } finally { + ini_set('memory_limit', $previousValue); } - - $exceptionRaised = false; - - $handler = new StreamHandler($stream); - stream_get_contents($stream, 1024); - - ini_set('memory_limit', $previousValue); - - $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize()); } public function testSimpleOOMPrevention(): void @@ -294,10 +294,13 @@ STRING; $this->markTestSkipped('We could not set a memory limit that would trigger the error.'); } - $stream = tmpfile(); - new StreamHandler($stream); - stream_get_contents($stream); - ini_set('memory_limit', $previousValue); - $this->assertTrue(true); + try { + $stream = tmpfile(); + new StreamHandler($stream); + stream_get_contents($stream); + $this->assertTrue(true); + } finally { + ini_set('memory_limit', $previousValue); + } } } diff --git a/tests/Monolog/Handler/SymfonyMailerHandlerTest.php b/tests/Monolog/Handler/SymfonyMailerHandlerTest.php new file mode 100644 index 00000000..ea4b9e26 --- /dev/null +++ b/tests/Monolog/Handler/SymfonyMailerHandlerTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Test\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +class SymfonyMailerHandlerTest extends TestCase +{ + /** @var MailerInterface&MockObject */ + private $mailer; + + public function setUp(): void + { + $this->mailer = $this + ->getMockBuilder(MailerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function tearDown(): void + { + parent::tearDown(); + + unset($this->mailer); + } + + public function testMessageCreationIsLazyWhenUsingCallback() + { + $this->mailer->expects($this->never()) + ->method('send'); + + $callback = function () { + throw new \RuntimeException('Email creation callback should not have been called in this test'); + }; + $handler = new SymfonyMailerHandler($this->mailer, $callback); + + $records = [ + $this->getRecord(Logger::DEBUG), + $this->getRecord(Logger::INFO), + ]; + $handler->handleBatch($records); + } + + public function testMessageCanBeCustomizedGivenLoggedData() + { + // Wire Mailer to expect a specific Email with a customized Subject + $expectedMessage = new Email(); + $this->mailer->expects($this->once()) + ->method('send') + ->with($this->callback(function ($value) use ($expectedMessage) { + return $value instanceof Email + && $value->getSubject() === 'Emergency' + && $value === $expectedMessage; + })); + + // Callback dynamically changes subject based on number of logged records + $callback = function ($content, array $records) use ($expectedMessage) { + $subject = count($records) > 0 ? 'Emergency' : 'Normal'; + return $expectedMessage->subject($subject); + }; + $handler = new SymfonyMailerHandler($this->mailer, $callback); + + // Logging 1 record makes this an Emergency + $records = [ + $this->getRecord(Logger::EMERGENCY), + ]; + $handler->handleBatch($records); + } + + public function testMessageSubjectFormatting() + { + // Wire Mailer to expect a specific Email with a customized Subject + $messageTemplate = new Email(); + $messageTemplate->subject('Alert: %level_name% %message%'); + $receivedMessage = null; + + $this->mailer->expects($this->once()) + ->method('send') + ->with($this->callback(function ($value) use (&$receivedMessage) { + $receivedMessage = $value; + + return true; + })); + + $handler = new SymfonyMailerHandler($this->mailer, $messageTemplate); + + $records = [ + $this->getRecord(Logger::EMERGENCY), + ]; + $handler->handleBatch($records); + + $this->assertEquals('Alert: EMERGENCY test', $receivedMessage->getSubject()); + } +} diff --git a/tests/Monolog/Handler/ZendMonitorHandlerTest.php b/tests/Monolog/Handler/ZendMonitorHandlerTest.php index 51362887..6cbd5e56 100644 --- a/tests/Monolog/Handler/ZendMonitorHandlerTest.php +++ b/tests/Monolog/Handler/ZendMonitorHandlerTest.php @@ -22,6 +22,13 @@ class ZendMonitorHandlerTest extends TestCase } } + public function tearDown(): void + { + parent::tearDown(); + + unset($this->zendMonitorHandler); + } + /** * @covers Monolog\Handler\ZendMonitorHandler::write */ diff --git a/tests/Monolog/PsrLogCompatTest.php b/tests/Monolog/PsrLogCompatTest.php index eb5dafbc..45f4f5e4 100644 --- a/tests/Monolog/PsrLogCompatTest.php +++ b/tests/Monolog/PsrLogCompatTest.php @@ -26,6 +26,13 @@ class PsrLogCompatTest extends TestCase { private TestHandler $handler; + public function tearDown(): void + { + parent::tearDown(); + + unset($this->handler); + } + public function getLogger(): LoggerInterface { $logger = new Logger('foo'); diff --git a/tests/Monolog/SignalHandlerTest.php b/tests/Monolog/SignalHandlerTest.php index 1ca47724..18ff3780 100644 --- a/tests/Monolog/SignalHandlerTest.php +++ b/tests/Monolog/SignalHandlerTest.php @@ -39,8 +39,10 @@ class SignalHandlerTest extends TestCase } } - protected function tearDown(): void + public function tearDown(): void { + parent::tearDown(); + if ($this->asyncSignalHandling !== null) { pcntl_async_signals($this->asyncSignalHandling); } @@ -53,6 +55,8 @@ class SignalHandlerTest extends TestCase pcntl_signal($signo, $handler); } } + + unset($this->signalHandlers, $this->blockedSignals, $this->asyncSignalHandling); } private function setSignalHandler($signo, $handler = SIG_DFL)