Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php declare(strict_types=1);
2
 
3
/*
4
 * This file is part of the Monolog package.
5
 *
6
 * (c) Jordi Boggiano <j.boggiano@seld.be>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Monolog\Handler;
13
 
14
use Elastic\Elasticsearch\Response\Elasticsearch;
15
use Throwable;
16
use RuntimeException;
17
use Monolog\Logger;
18
use Monolog\Formatter\FormatterInterface;
19
use Monolog\Formatter\ElasticsearchFormatter;
20
use InvalidArgumentException;
21
use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException;
22
use Elasticsearch\Client;
23
use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException;
24
use Elastic\Elasticsearch\Client as Client8;
25
 
26
/**
27
 * Elasticsearch handler
28
 *
29
 * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html
30
 *
31
 * Simple usage example:
32
 *
33
 *    $client = \Elasticsearch\ClientBuilder::create()
34
 *        ->setHosts($hosts)
35
 *        ->build();
36
 *
37
 *    $options = array(
38
 *        'index' => 'elastic_index_name',
39
 *        'type'  => 'elastic_doc_type',
40
 *    );
41
 *    $handler = new ElasticsearchHandler($client, $options);
42
 *    $log = new Logger('application');
43
 *    $log->pushHandler($handler);
44
 *
45
 * @author Avtandil Kikabidze <akalongman@gmail.com>
46
 */
47
class ElasticsearchHandler extends AbstractProcessingHandler
48
{
49
    /**
50
     * @var Client|Client8
51
     */
52
    protected $client;
53
 
54
    /**
55
     * @var mixed[] Handler config options
56
     */
57
    protected $options = [];
58
 
59
    /**
60
     * @var bool
61
     */
62
    private $needsType;
63
 
64
    /**
65
     * @param Client|Client8 $client  Elasticsearch Client object
66
     * @param mixed[]        $options Handler configuration
67
     */
68
    public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
69
    {
70
        if (!$client instanceof Client && !$client instanceof Client8) {
71
            throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required');
72
        }
73
 
74
        parent::__construct($level, $bubble);
75
        $this->client = $client;
76
        $this->options = array_merge(
77
            [
78
                'index'        => 'monolog', // Elastic index name
79
                'type'         => '_doc',    // Elastic document type
80
                'ignore_error' => false,     // Suppress Elasticsearch exceptions
81
            ],
82
            $options
83
        );
84
 
85
        if ($client instanceof Client8 || $client::VERSION[0] === '7') {
86
            $this->needsType = false;
87
            // force the type to _doc for ES8/ES7
88
            $this->options['type'] = '_doc';
89
        } else {
90
            $this->needsType = true;
91
        }
92
    }
93
 
94
    /**
95
     * {@inheritDoc}
96
     */
97
    protected function write(array $record): void
98
    {
99
        $this->bulkSend([$record['formatted']]);
100
    }
101
 
102
    /**
103
     * {@inheritDoc}
104
     */
105
    public function setFormatter(FormatterInterface $formatter): HandlerInterface
106
    {
107
        if ($formatter instanceof ElasticsearchFormatter) {
108
            return parent::setFormatter($formatter);
109
        }
110
 
111
        throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter');
112
    }
113
 
114
    /**
115
     * Getter options
116
     *
117
     * @return mixed[]
118
     */
119
    public function getOptions(): array
120
    {
121
        return $this->options;
122
    }
123
 
124
    /**
125
     * {@inheritDoc}
126
     */
127
    protected function getDefaultFormatter(): FormatterInterface
128
    {
129
        return new ElasticsearchFormatter($this->options['index'], $this->options['type']);
130
    }
131
 
132
    /**
133
     * {@inheritDoc}
134
     */
135
    public function handleBatch(array $records): void
136
    {
137
        $documents = $this->getFormatter()->formatBatch($records);
138
        $this->bulkSend($documents);
139
    }
140
 
141
    /**
142
     * Use Elasticsearch bulk API to send list of documents
143
     *
144
     * @param  array[]           $records Records + _index/_type keys
145
     * @throws \RuntimeException
146
     */
147
    protected function bulkSend(array $records): void
148
    {
149
        try {
150
            $params = [
151
                'body' => [],
152
            ];
153
 
154
            foreach ($records as $record) {
155
                $params['body'][] = [
156
                    'index' => $this->needsType ? [
157
                        '_index' => $record['_index'],
158
                        '_type'  => $record['_type'],
159
                    ] : [
160
                        '_index' => $record['_index'],
161
                    ],
162
                ];
163
                unset($record['_index'], $record['_type']);
164
 
165
                $params['body'][] = $record;
166
            }
167
 
168
            /** @var Elasticsearch */
169
            $responses = $this->client->bulk($params);
170
 
171
            if ($responses['errors'] === true) {
172
                throw $this->createExceptionFromResponses($responses);
173
            }
174
        } catch (Throwable $e) {
175
            if (! $this->options['ignore_error']) {
176
                throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e);
177
            }
178
        }
179
    }
180
 
181
    /**
182
     * Creates elasticsearch exception from responses array
183
     *
184
     * Only the first error is converted into an exception.
185
     *
186
     * @param mixed[]|Elasticsearch $responses returned by $this->client->bulk()
187
     */
188
    protected function createExceptionFromResponses($responses): Throwable
189
    {
190
        foreach ($responses['items'] ?? [] as $item) {
191
            if (isset($item['index']['error'])) {
192
                return $this->createExceptionFromError($item['index']['error']);
193
            }
194
        }
195
 
196
        if (class_exists(ElasticInvalidArgumentException::class)) {
197
            return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.');
198
        }
199
 
200
        return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.');
201
    }
202
 
203
    /**
204
     * Creates elasticsearch exception from error array
205
     *
206
     * @param mixed[] $error
207
     */
208
    protected function createExceptionFromError(array $error): Throwable
209
    {
210
        $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null;
211
 
212
        if (class_exists(ElasticInvalidArgumentException::class)) {
213
            return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous);
214
        }
215
 
216
        return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous);
217
    }
218
}