Skip to content

Commit 3cb0a8a

Browse files
authored
Add monolog logstash log support (#243)
1 parent 5901359 commit 3cb0a8a

File tree

8 files changed

+136
-1
lines changed

8 files changed

+136
-1
lines changed

dev/config/config.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ monolog:
4545
formatter: 'monolog.formatter.json'
4646
level: debug
4747
channels: [ "!event", "!deprecation" ]
48+
logstash:
49+
type: stream
50+
path: "%kernel.logs_dir%/logstash/%kernel.environment%.log"
51+
formatter: 'monolog.formatter.logstash'
52+
level: debug
53+
channels: [ "!event", "!deprecation" ]
4854
error:
4955
type: stream
5056
path: "%kernel.logs_dir%/error.log"
@@ -69,6 +75,13 @@ fd_log_viewer:
6975
in: "%kernel.logs_dir%/json"
7076
downloadable: true
7177
deletable: true
78+
monolog-logstash:
79+
type: monolog.logstash
80+
name: Logstash monolog
81+
finder:
82+
in: "%kernel.logs_dir%/logstash"
83+
downloadable: true
84+
deletable: true
7285
error-log:
7386
type: php-error-log
7487
name: PHP error_log

docs/configuration-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ This entry allows you to add more log file directories to the Log Viewer. Each e
4848

4949
### log_files.type
5050

51-
**type**: `string` (`enum: monolog(.json)|http-access|apache-error|nginx-error|php-error-log`)
51+
**type**: `string` (`enum: monolog(.json|.logstash)|http-access|apache-error|nginx-error|php-error-log`)
5252

5353
This is the type of log file that will be read.
5454

5555
- `monolog` is the default type and will read the default monolog log files.
5656
- `monolog.json` will read the monolog log files that use `formatter: 'monolog.formatter.json'`.
57+
- `monolog.logstash` will read the monolog log files that use `formatter: 'monolog.formatter.logstash'`.
5758
- `http-access` will read the access log files of Apache and Nginx.
5859
- `apache-error` will read the error log files of Apache.
5960
- `nginx-error` will read the error log files of Nginx.

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ parameters:
120120
count: 1
121121
path: src/Service/File/Monolog/MonologJsonParser.php
122122

123+
-
124+
message: '#^Method FD\\LogViewer\\Service\\File\\Monolog\\MonologLogstashParser\:\:parse\(\) should return array\{date\: string, severity\: string, channel\: string, message\: string, context\: array\<int\|string, mixed\>\|string, extra\: array\<int\|string, mixed\>\|string\}\|null but returns array\{date\: mixed, severity\: mixed, channel\: mixed, message\: mixed, context\: mixed, extra\: mixed\}\.$#'
125+
identifier: return.type
126+
count: 1
127+
path: src/Service/File/Monolog/MonologLogstashParser.php
128+
123129
-
124130
message: '#^Binary operation "\." between ''/bundles…'' and mixed results in an error\.$#'
125131
identifier: binaryOp.invalid

src/Resources/config/services.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@
124124
$services->set('fd.symfony.log.viewer.monolog_json_file_parser', MonologFileParser::class)
125125
->tag('fd.symfony.log.viewer.log_file_parser', ['name' => 'monolog.json'])
126126
->arg('$formatType', MonologFileParser::TYPE_JSON);
127+
$services->set('fd.symfony.log.viewer.monolog_logstash_file_parser', MonologFileParser::class)
128+
->tag('fd.symfony.log.viewer.log_file_parser', ['name' => 'monolog.logstash'])
129+
->arg('$formatType', MonologFileParser::TYPE_LOGSTASH);
127130
$services->set(HttpAccessFileParser::class)->tag('fd.symfony.log.viewer.log_file_parser', ['name' => 'http-access']);
128131
$services->set(NginxErrorFileParser::class)->tag('fd.symfony.log.viewer.log_file_parser', ['name' => 'nginx-error']);
129132
$services->set(ApacheErrorFileParser::class)->tag('fd.symfony.log.viewer.log_file_parser', ['name' => 'apache-error']);

src/Service/File/Monolog/MonologFileParser.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class MonologFileParser implements LogFileParserInterface
1616
{
1717
public const TYPE_LINE = 'line';
1818
public const TYPE_JSON = 'json';
19+
public const TYPE_LOGSTASH = 'logstash';
1920

2021
/**
2122
* @param self::TYPE_* $formatType
@@ -27,6 +28,7 @@ public function __construct(private readonly string $formatType, private readonl
2728
public function getLogIndex(LogFilesConfig $config, LogFile $file, LogQueryDto $logQuery): LogRecordCollection
2829
{
2930
return match ($this->formatType) {
31+
self::TYPE_LOGSTASH => $this->logParser->parse(new SplFileInfo($file->path), new MonologLogstashParser(), $config, $logQuery),
3032
self::TYPE_JSON => $this->logParser->parse(new SplFileInfo($file->path), new MonologJsonParser(), $config, $logQuery),
3133
self::TYPE_LINE => $this->logParser->parse(
3234
new SplFileInfo($file->path),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Service\File\Monolog;
5+
6+
use FD\LogViewer\Service\File\LogLineParserInterface;
7+
use JsonException;
8+
9+
class MonologLogstashParser implements LogLineParserInterface
10+
{
11+
public function matches(string $line): int
12+
{
13+
return self::MATCH_START;
14+
}
15+
16+
/**
17+
* @inheritDoc
18+
*/
19+
public function parse(string $message): ?array
20+
{
21+
try {
22+
$json = json_decode($message, true, flags: JSON_THROW_ON_ERROR);
23+
} catch (JsonException) {
24+
return null;
25+
}
26+
27+
if (is_array($json) === false) {
28+
return null;
29+
}
30+
31+
return [
32+
'date' => $json['@timestamp'],
33+
'severity' => $json['level'],
34+
'channel' => $json['channel'],
35+
'message' => $json['message'],
36+
'context' => $json['context'] ?? [],
37+
'extra' => $json['extra'] ?? [],
38+
];
39+
}
40+
}

tests/Unit/Service/File/Monolog/MonologFileParserTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use FD\LogViewer\Service\File\Monolog\MonologFileParser;
1212
use FD\LogViewer\Service\File\Monolog\MonologJsonParser;
1313
use FD\LogViewer\Service\File\Monolog\MonologLineParser;
14+
use FD\LogViewer\Service\File\Monolog\MonologLogstashParser;
1415
use FD\LogViewer\Tests\Utility\TestEntityTrait;
1516
use InvalidArgumentException;
1617
use PHPUnit\Framework\Attributes\CoversClass;
@@ -64,6 +65,22 @@ public function testGetLogIndexForJsonParser(): void
6465
static::assertSame($recordCollection, $parser->getLogIndex($config, $file, $logQuery));
6566
}
6667

68+
public function testGetLogIndexForLogstashParser(): void
69+
{
70+
$config = $this->createLogFileConfig();
71+
$logQuery = new LogQueryDto(['identifier'], new DateTimeZone('Europe/Amsterdam'));
72+
$file = $this->createLogFile();
73+
$recordCollection = new LogRecordCollection(new ArrayIterator([]), null);
74+
75+
$this->logParser->expects(self::once())
76+
->method('parse')
77+
->with(new SplFileInfo('path'), new MonologLogstashParser(), $config, $logQuery)
78+
->willReturn($recordCollection);
79+
80+
$parser = new MonologFileParser(MonologFileParser::TYPE_LOGSTASH, $this->logParser);
81+
static::assertSame($recordCollection, $parser->getLogIndex($config, $file, $logQuery));
82+
}
83+
6784
public function testGetLogIndexInvalidType(): void
6885
{
6986
$config = $this->createLogFileConfig();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Tests\Unit\Service\File\Monolog;
5+
6+
use FD\LogViewer\Service\File\LogLineParserInterface;
7+
use FD\LogViewer\Service\File\Monolog\MonologLogstashParser;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\TestCase;
10+
11+
#[CoversClass(MonologLogstashParser::class)]
12+
class MonologLogstashParserTest extends TestCase
13+
{
14+
private MonologLogstashParser $parser;
15+
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
$this->parser = new MonologLogstashParser();
20+
}
21+
22+
public function testMatches(): void
23+
{
24+
static::assertSame(LogLineParserInterface::MATCH_START, $this->parser->matches('line'));
25+
}
26+
27+
public function testParseInvalidJson(): void
28+
{
29+
static::assertNull($this->parser->parse('invalid json'));
30+
}
31+
32+
public function testParseNonArrayJson(): void
33+
{
34+
static::assertNull($this->parser->parse('"string"'));
35+
}
36+
37+
public function testParse(): void
38+
{
39+
$json = '{"@timestamp":"2021-01-01 00:00:00.000000+00:00","@version":1,' .
40+
'"host":"my-host","message":"message","type":"app","channel":"app","level":"INFO",' .
41+
'"monolog_level":200,"context":["context"],"extra":["extra"]}';
42+
43+
$expected = [
44+
'date' => '2021-01-01 00:00:00.000000+00:00',
45+
'severity' => 'INFO',
46+
'channel' => 'app',
47+
'message' => 'message',
48+
'context' => ['context'],
49+
'extra' => ['extra'],
50+
];
51+
static::assertSame($expected, $this->parser->parse($json));
52+
}
53+
}

0 commit comments

Comments
 (0)