Просмотр исходного кода

ElasticSearch v8 support (#1662)

* ElasticSearch v8 support
* CI updates

Co-authored-by: Thomas Müller <mimmi20@live.de>
Jordi Boggiano 3 лет назад
Родитель
Сommit
bd24765917
27 измененных файлов с 506 добавлено и 193 удалено
  1. 196 25
      .github/workflows/continuous-integration.yml
  2. 10 4
      composer.json
  3. 3 0
      phpstan.neon.dist
  4. 39 8
      src/Monolog/Handler/ElasticsearchHandler.php
  5. 9 0
      src/Monolog/Test/TestCase.php
  6. 7 0
      tests/Monolog/Formatter/ScalarFormatterTest.php
  7. 1 1
      tests/Monolog/Handler/AmqpHandlerTest.php
  8. 7 0
      tests/Monolog/Handler/DynamoDbHandlerTest.php
  9. 20 57
      tests/Monolog/Handler/ElasticaHandlerTest.php
  10. 80 73
      tests/Monolog/Handler/ElasticsearchHandlerTest.php
  11. 7 0
      tests/Monolog/Handler/FlowdockHandlerTest.php
  12. 7 0
      tests/Monolog/Handler/HandlerWrapperTest.php
  13. 7 0
      tests/Monolog/Handler/InsightOpsHandlerTest.php
  14. 7 0
      tests/Monolog/Handler/LogEntriesHandlerTest.php
  15. 7 0
      tests/Monolog/Handler/LogmaticHandlerTest.php
  16. 7 0
      tests/Monolog/Handler/PHPConsoleHandlerTest.php
  17. 7 0
      tests/Monolog/Handler/PushoverHandlerTest.php
  18. 7 0
      tests/Monolog/Handler/RollbarHandlerTest.php
  19. 12 8
      tests/Monolog/Handler/RotatingFileHandlerTest.php
  20. 7 0
      tests/Monolog/Handler/SlackHandlerTest.php
  21. 7 0
      tests/Monolog/Handler/SocketHandlerTest.php
  22. 19 16
      tests/Monolog/Handler/StreamHandlerTest.php
  23. 7 0
      tests/Monolog/Handler/SwiftMailerHandlerTest.php
  24. 7 0
      tests/Monolog/Handler/SymfonyMailerHandlerTest.php
  25. 7 0
      tests/Monolog/Handler/ZendMonitorHandlerTest.php
  26. 7 0
      tests/Monolog/PsrLogCompatTest.php
  27. 5 1
      tests/Monolog/SignalHandlerTest.php

+ 196 - 25
.github/workflows/continuous-integration.yml

@@ -4,71 +4,242 @@ 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:
           - "7.2"
           - "7.3"
           - "7.4"
           - "8.0"
-          # disabled for now as phpspec/prophecy does not allow 8.1
-          # - "8.1"
+          - "8.1"
+
         dependencies: [highest]
+
+        operating-system:
+          - "ubuntu-latest"
+
         include:
           - php-version: "7.2"
             dependencies: lowest
-          - php-version: "8.0"
+            operating-system: ubuntu-latest
+          - 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
+          tools: "composer:v2"
+          ini-values: "memory_limit=-1"
 
-      - name: Get composer cache directory
-        id: composercache
-        run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+      - name: Add require for mongodb/mongodb to make tests runnable
+        run: 'composer require mongodb/mongodb --dev --no-update'
+
+      - name: "Change dependencies"
+        run: |
+          composer require --no-update --no-interaction --dev elasticsearch/elasticsearch:^7
+          composer config --no-plugins allow-plugins.ocramius/package-versions true
 
-      - name: Cache dependencies
-        uses: actions/cache@v2
+      - name: "Update dependencies with composer"
+        uses: "ramsey/composer-install@v1"
         with:
-          path: ${{ steps.composercache.outputs.dir }}
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
-          restore-keys: ${{ runner.os }}-composer-
+          dependency-versions: "${{ matrix.dependencies }}"
 
-      - name: Add require for mongodb/mongodb to make tests runnable
-        run: 'composer require ${{ env.COMPOSER_FLAGS }} mongodb/mongodb --dev --no-update'
+      - name: "Run tests"
+        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
+          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 }}"
 
-      - name: "Handle lowest dependencies update"
-        if: "contains(matrix.dependencies, 'lowest')"
-        run: "echo \"COMPOSER_FLAGS=$COMPOSER_FLAGS --prefer-lowest\" >> $GITHUB_ENV"
+    strategy:
+      fail-fast: false
+
+      matrix:
+        operating-system:
+          - "ubuntu-latest"
+
+        php-version:
+          - "7.2"
+          - "7.3"
+          - "7.4"
+          - "8.0"
+          - "8.1"
 
-      - name: "Install latest dependencies"
+        dependencies:
+          - "highest"
+          - "lowest"
+
+        es-version:
+          - "7.0.0"
+          - "7.17.0"
+
+        exclude:
+          # php 7.3 is required
+          - php-version: "7.2"
+            es-version: "7.17.0"
+          # tests failing due an error in deprecated guzzlehttp/ringphp
+          - php-version: "7.3"
+            es-version: "7.0.0"
+          - php-version: "7.4"
+            es-version: "7.0.0"
+
+    steps:
+      - name: "Checkout"
+        uses: "actions/checkout@v2"
+
+      # required for elasticsearch
+      - name: Configure sysctl limits
         run: |
-          composer update ${{ env.COMPOSER_FLAGS }}
+          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 -- --verbose"
+        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:
+          # ES 8 requires PHP 7.4+
+          - "7.4"
+          - "8.0"
+          - "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 ${{ env.COMPOSER_FLAGS }}
-          composer exec phpunit -- --verbose
+          composer update -W
+          composer exec phpunit -- --group Elasticsearch,Elastica --verbose

+ 10 - 4
composer.json

@@ -17,19 +17,22 @@
         "psr/log": "^1.0.1 || ^2.0 || ^3.0"
     },
     "require-dev": {
+        "ext-json": "*",
         "aws/aws-sdk-php": "^2.4.9 || ^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.15",
         "phpstan/phpstan": "^0.12.91",
-        "phpunit/phpunit": "^8.5",
+        "phpunit/phpunit": "^8.5.14",
         "predis/predis": "^1.1",
         "rollbar/rollbar": "^1.3 || ^2 || ^3",
-        "ruflin/elastica": ">=0.90@dev",
+        "ruflin/elastica": "^7",
         "swiftmailer/swiftmailer": "^5.3|^6.0",
         "symfony/mailer": "^5.4 || ^6",
         "symfony/mime": "^5.4 || ^6"
@@ -72,6 +75,9 @@
     "config": {
         "lock": false,
         "sort-packages": true,
-        "platform-check": false
+        "platform-check": false,
+        "allow-plugins": {
+            "composer/package-versions-deprecated": true
+        }
     }
 }

+ 3 - 0
phpstan.neon.dist

@@ -35,3 +35,6 @@ parameters:
         - '#::popProcessor\(\) should return callable#'
         - '#Parameter \#1 \$ of callable \(callable\(Monolog\\Handler\\Record\): Monolog\\Handler\\Record\)#'
         - '#is incompatible with native type array.#'
+
+        # legacy elasticsearch namespace failures
+        - '# Elastic\\Elasticsearch\\#'

+ 39 - 8
src/Monolog/Handler/ElasticsearchHandler.php

@@ -11,6 +11,7 @@
 
 namespace Monolog\Handler;
 
+use Elastic\Elasticsearch\Response\Elasticsearch;
 use Throwable;
 use RuntimeException;
 use Monolog\Logger;
@@ -19,6 +20,8 @@ use Monolog\Formatter\ElasticsearchFormatter;
 use InvalidArgumentException;
 use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException;
 use Elasticsearch\Client;
+use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException;
+use Elastic\Elasticsearch\Client as Client8;
 
 /**
  * Elasticsearch handler
@@ -44,7 +47,7 @@ use Elasticsearch\Client;
 class ElasticsearchHandler extends AbstractProcessingHandler
 {
     /**
-     * @var Client
+     * @var Client|Client8
      */
     protected $client;
 
@@ -54,11 +57,20 @@ class ElasticsearchHandler extends AbstractProcessingHandler
     protected $options = [];
 
     /**
-     * @param Client  $client  Elasticsearch Client object
-     * @param mixed[] $options Handler configuration
+     * @var bool
      */
-    public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
+    private $needsType;
+
+    /**
+     * @param Client|Client8 $client  Elasticsearch Client object
+     * @param mixed[]        $options Handler configuration
+     */
+    public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
     {
+        if (!$client instanceof Client && !$client instanceof Client8) {
+            throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required');
+        }
+
         parent::__construct($level, $bubble);
         $this->client = $client;
         $this->options = array_merge(
@@ -69,6 +81,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;
+        }
     }
 
     /**
@@ -133,9 +153,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']);
@@ -143,6 +165,7 @@ class ElasticsearchHandler extends AbstractProcessingHandler
                 $params['body'][] = $record;
             }
 
+            /** @var Elasticsearch */
             $responses = $this->client->bulk($params);
 
             if ($responses['errors'] === true) {
@@ -160,9 +183,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'])) {
@@ -170,6 +193,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.');
     }
 
@@ -178,10 +205,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);
     }
 }

+ 9 - 0
src/Monolog/Test/TestCase.php

@@ -25,6 +25,15 @@ use Monolog\Formatter\FormatterInterface;
  */
 class TestCase extends \PHPUnit\Framework\TestCase
 {
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        if (isset($this->handler)) {
+            unset($this->handler);
+        }
+    }
+
     /**
      * @param mixed[] $context
      *

+ 7 - 0
tests/Monolog/Formatter/ScalarFormatterTest.php

@@ -22,6 +22,13 @@ class ScalarFormatterTest extends \PHPUnit\Framework\TestCase
         $this->formatter = new ScalarFormatter();
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->formatter);
+    }
+
     public function buildTrace(\Exception $e)
     {
         $data = [];

+ 1 - 1
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");
         }
 

+ 7 - 0
tests/Monolog/Handler/DynamoDbHandlerTest.php

@@ -45,6 +45,13 @@ class DynamoDbHandlerTest extends TestCase
         $this->client = $clientMockBuilder->getMock();
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->client);
+    }
+
     public function testConstruct()
     {
         $this->assertInstanceOf('Monolog\Handler\DynamoDbHandler', new DynamoDbHandler($this->client, 'foo'));

+ 20 - 57
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
@@ -155,60 +165,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 = [
-            'level' => Logger::ERROR,
-            'level_name' => 'ERROR',
-            'channel' => 'meh',
-            'context' => ['foo' => 7, 'bar', 'class' => new \stdClass],
-            'datetime' => new \DateTimeImmutable("@0"),
-            'extra' => [],
-            'message' => 'log',
-        ];
-
-        $expected = $msg;
-        $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+
      *
@@ -237,7 +193,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 {
@@ -271,9 +229,14 @@ class ElasticaHandlerTest extends TestCase
     protected function getCreatedDocId(Response $response)
     {
         $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;
     }
 
     /**

+ 80 - 73
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\Logger;
 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;
 
@@ -35,63 +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 = [
-            'level' => Logger::ERROR,
-            'level_name' => 'ERROR',
-            'channel' => 'meh',
-            'context' => ['foo' => 7, 'bar', 'class' => new \stdClass],
-            'datetime' => new \DateTimeImmutable("@0"),
-            'extra' => [],
-            'message' => 'log',
-        ];
-
-        // 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);
+        parent::tearDown();
 
-        // perform tests
-        $handler = new ElasticsearchHandler($this->client, $this->options);
-        $handler->handle($msg);
-        $handler->handleBatch([$msg]);
+        unset($this->client);
     }
 
     /**
@@ -108,7 +73,7 @@ class ElasticsearchHandlerTest extends TestCase
     }
 
     /**
-     * @covers                   Monolog\Handler\ElasticsearchHandler::setFormatter
+     * @covers Monolog\Handler\ElasticsearchHandler::setFormatter
      */
     public function testSetFormatterInvalid()
     {
@@ -132,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());
     }
@@ -142,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);
@@ -178,7 +148,7 @@ class ElasticsearchHandlerTest extends TestCase
      * @covers Monolog\Handler\ElasticsearchHandler::bulkSend
      * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter
      */
-    public function testHandleIntegration()
+    public function testHandleBatchIntegration()
     {
         $msg = [
             'level' => Logger::ERROR,
@@ -198,21 +168,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(
@@ -241,25 +216,45 @@ 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
+     * @param  Client|Client8 $client     Elasticsearch client
      * @param  string $index
      * @param  string $type
      * @param  string $documentId
      * @return array
      */
-    protected function getDocSourceFromElastic(Client $client, $index, $type, $documentId)
+    protected function getDocSourceFromElastic($client, $index, $type, $documentId)
     {
         $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'])) {
@@ -268,4 +263,16 @@ class ElasticsearchHandlerTest extends TestCase
 
         return [];
     }
+
+    /**
+     * @return ClientBuilder|ClientBuilder8
+     */
+    private function getClientBuilder()
+    {
+        if (class_exists(ClientBuilder8::class)) {
+            return ClientBuilder8::create();
+        }
+
+        return ClientBuilder::create();
+    }
 }

+ 7 - 0
tests/Monolog/Handler/FlowdockHandlerTest.php

@@ -38,6 +38,13 @@ class FlowdockHandlerTest extends TestCase
         }
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testWriteHeader()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/HandlerWrapperTest.php

@@ -32,6 +32,13 @@ class HandlerWrapperTest extends TestCase
         $this->wrapper = new HandlerWrapper($this->handler);
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->wrapper);
+    }
+
     /**
      * @return array
      */

+ 7 - 0
tests/Monolog/Handler/InsightOpsHandlerTest.php

@@ -30,6 +30,13 @@ class InsightOpsHandlerTest extends TestCase
      */
     private $handler;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->resource);
+    }
+
     public function testWriteContent()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/LogEntriesHandlerTest.php

@@ -29,6 +29,13 @@ class LogEntriesHandlerTest extends TestCase
      */
     private $handler;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testWriteContent()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/LogmaticHandlerTest.php

@@ -29,6 +29,13 @@ class LogmaticHandlerTest extends TestCase
      */
     private $handler;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testWriteContent()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/PHPConsoleHandlerTest.php

@@ -56,6 +56,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')

+ 7 - 0
tests/Monolog/Handler/PushoverHandlerTest.php

@@ -25,6 +25,13 @@ class PushoverHandlerTest extends TestCase
     private $res;
     private $handler;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testWriteHeader()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/RollbarHandlerTest.php

@@ -44,6 +44,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

+ 12 - 8
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();
-    }
 }

+ 7 - 0
tests/Monolog/Handler/SlackHandlerTest.php

@@ -39,6 +39,13 @@ class SlackHandlerTest extends TestCase
         }
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testWriteHeader()
     {
         $this->createHandler();

+ 7 - 0
tests/Monolog/Handler/SocketHandlerTest.php

@@ -30,6 +30,13 @@ class SocketHandlerTest extends TestCase
      */
     private $res;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->res);
+    }
+
     public function testInvalidHostname()
     {
         $this->expectException(\UnexpectedValueException::class);

+ 19 - 16
tests/Monolog/Handler/StreamHandlerTest.php

@@ -271,20 +271,20 @@ STRING;
             $this->markTestSkipped('We could not set a memory limit that would trigger the error.');
         }
 
-        $stream = tmpfile();
-
-        if ($stream === false) {
-            $this->markTestSkipped('We could not create a temp file to be use as a stream.');
-        }
-
-        $exceptionRaised = false;
+        try {
+            $stream = tmpfile();
 
-        $handler = new StreamHandler($stream);
-        stream_get_contents($stream, 1024);
+            if ($stream === false) {
+                $this->markTestSkipped('We could not create a temp file to be use as a stream.');
+            }
 
-        ini_set('memory_limit', $previousValue);
+            $handler = new StreamHandler($stream);
+            stream_get_contents($stream, 1024);
 
-        $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
+            $this->assertEquals($expectedChunkSize, $handler->getStreamChunkSize());
+        } finally {
+            ini_set('memory_limit', $previousValue);
+        }
     }
 
     /**
@@ -298,10 +298,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);
+        }
     }
 }

+ 7 - 0
tests/Monolog/Handler/SwiftMailerHandlerTest.php

@@ -28,6 +28,13 @@ class SwiftMailerHandlerTest extends TestCase
             ->getMock();
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->mailer);
+    }
+
     public function testMessageCreationIsLazyWhenUsingCallback()
     {
         $this->mailer->expects($this->never())

+ 7 - 0
tests/Monolog/Handler/SymfonyMailerHandlerTest.php

@@ -30,6 +30,13 @@ class SymfonyMailerHandlerTest extends TestCase
             ->getMock();
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->mailer);
+    }
+
     public function testMessageCreationIsLazyWhenUsingCallback()
     {
         $this->mailer->expects($this->never())

+ 7 - 0
tests/Monolog/Handler/ZendMonitorHandlerTest.php

@@ -25,6 +25,13 @@ class ZendMonitorHandlerTest extends TestCase
         }
     }
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->zendMonitorHandler);
+    }
+
     /**
      * @covers  Monolog\Handler\ZendMonitorHandler::write
      */

+ 7 - 0
tests/Monolog/PsrLogCompatTest.php

@@ -25,6 +25,13 @@ class PsrLogCompatTest extends TestCase
 {
     private $handler;
 
+    public function tearDown(): void
+    {
+        parent::tearDown();
+
+        unset($this->handler);
+    }
+
     public function getLogger(): LoggerInterface
     {
         $logger = new Logger('foo');

+ 5 - 1
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)