Skip to content

Commit 0a45301

Browse files
committed
[Monolog] Added ElasticsearchLogstashHandler
1 parent 87a6f04 commit 0a45301

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

src/Symfony/Bridge/Monolog/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* The `RouteProcessor` class has been made final
8+
* Added `ElasticsearchLogstashHandler`
89

910
4.3.0
1011
-----
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Symfony\Bridge\Monolog\Handler;
13+
14+
use Monolog\Formatter\FormatterInterface;
15+
use Monolog\Formatter\LogstashFormatter;
16+
use Monolog\Handler\AbstractHandler;
17+
use Monolog\Logger;
18+
use Symfony\Component\HttpClient\HttpClient;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* Push logs directly to Elasticsearch and format them according to Logstash specification.
23+
*
24+
* This handler dials directly with the HTTP interface of elasticsearch. This
25+
* means it will slow down your application if elasticsearch takes times to
26+
* answer. Even if all HTTP calls are done asynchronously.
27+
*
28+
* In a development environment, it's fine to keep the default configuration:
29+
* For each log, an HTTP request will be made to push the log to Elasticsearch.
30+
*
31+
* But in a production environment, it's highly recommended to wrap this handler
32+
* in an handler with buffering capability (like the FingersCrossedHandler, or
33+
* BufferHandler) in order to call Elasticsearch only once. For even better
34+
* performance and fault tolerance, a proper ELK stack is recommended.
35+
*
36+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
37+
*/
38+
class ElasticsearchLogstashHandler extends AbstractHandler
39+
{
40+
private $endpoint;
41+
private $index;
42+
private $client;
43+
private $responses;
44+
45+
public function __construct(string $endpoint = 'http://127.0.0.1:9200', string $index = 'monolog', HttpClientInterface $client = null, int $level = Logger::DEBUG, bool $bubble = true)
46+
{
47+
if (!interface_exists(HttpClientInterface::class)) {
48+
throw new \LogicException(sprintf('%s handler needs symfony/http-client, please run `composer require symfony/http-client`.', __CLASS__));
49+
}
50+
51+
parent::__construct($level, $bubble);
52+
$this->endpoint = $endpoint;
53+
$this->index = $index;
54+
$this->client = $client ?: HttpClient::create(['timeout' => 1]);
55+
$this->responses = new \SplObjectStorage();
56+
}
57+
58+
public function handle(array $record): bool
59+
{
60+
if (!$this->isHandling($record)) {
61+
return false;
62+
}
63+
64+
$this->sendToElasticsearch([$record]);
65+
66+
return false === $this->bubble;
67+
}
68+
69+
public function handleBatch(array $records): void
70+
{
71+
$records = array_filter($records, function (array $record) {
72+
return $this->isHandling($record);
73+
});
74+
75+
if (!$records) {
76+
return;
77+
}
78+
79+
$this->sendToElasticsearch($records);
80+
}
81+
82+
protected function getDefaultFormatter(): FormatterInterface
83+
{
84+
return new LogstashFormatter('application', null, null, 'ctxt_', LogstashFormatter::V1);
85+
}
86+
87+
private function sendToElasticsearch(array $records)
88+
{
89+
$formatter = $this->getFormatter();
90+
91+
$body = '';
92+
foreach ($records as $record) {
93+
if ($this->processors) {
94+
foreach ($this->processors as $processor) {
95+
$record = \call_user_func($processor, $record);
96+
}
97+
}
98+
99+
$body .= json_encode([
100+
'index' => [
101+
'_index' => $this->index,
102+
'_type' => '_doc',
103+
],
104+
]);
105+
$body .= "\n";
106+
$body .= $formatter->format($record);
107+
$body .= "\n";
108+
}
109+
110+
$response = $this->client->request('POST', $this->endpoint.'/_bulk', [
111+
'body' => $body,
112+
'headers' => [
113+
'Content-Type' => 'application/json',
114+
],
115+
]);
116+
117+
$this->responses->attach($response);
118+
119+
foreach ($this->client->stream($this->responses, 0) as $response => $chunk) {
120+
if (!$chunk->isTimeout() && $chunk->isFirst()) {
121+
$this->responses->detach($response);
122+
}
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)