Skip to content

Commit 2919173

Browse files
authored
Add support for searching context and extra (#43)
1 parent f6b2ec7 commit 2919173

File tree

16 files changed

+407
-37
lines changed

16 files changed

+407
-37
lines changed

docs/advanced-search-queries.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
The search query allows for more fine-grained control over the search results. The following operators are supported:
44

5-
| Operator | Short | Description |
6-
|--------------------------------------|-------|---------------------------------------------------------------------------------|
7-
| `before:<date>`,`before:"<date>"` | `b` | Show all logs messages that occur before the specified date. |
8-
| `after:<date>`,`after:"<date>"` | `a` | Show all logs messages that occur after the specified date. |
9-
| `severity:<pipe-separated-string>` | `s` | Show all logs messages that match the given severity/severities. |
10-
| `channel:<pipe-separated-string>` | `c` | Show all logs messages that match the given channel(s). |
11-
| `after:<date>`,`after:"<date>"` | `a` | Show all logs messages that occur after the specified date. |
12-
| `exclude:<word>`,`exclude:"<words>"` | `-` | Exclude the specific sentence from the results. Can be specified multiple times |
5+
| Operator | Short | Description |
6+
|---------------------------------------------|-------|---------------------------------------------------------------------------------|
7+
| `before:<date>`,`before:"<date>"` | `b` | Show all logs messages that occur before the specified date. |
8+
| `after:<date>`,`after:"<date>"` | `a` | Show all logs messages that occur after the specified date. |
9+
| `severity:<pipe-separated-string>` | `s` | Show all logs messages that match the given severity/severities. |
10+
| `channel:<pipe-separated-string>` | `c` | Show all logs messages that match the given channel(s). |
11+
| `after:<date>`,`after:"<date>"` | `a` | Show all logs messages that occur after the specified date. |
12+
| `exclude:<word>`,`exclude:"<words>"` | `-` | Exclude the specific sentence from the results. Can be specified multiple times |
13+
| `context:<string>`, `context:<key>=<value>` | | Show all logs messages that match the given context. |
14+
| `extra:<string>`, `extra:<key>=<value>` | | Show all logs messages that match the given extra. |
1315

1416
## Example
1517

16-
Search all log entries between `2020-01-01` and `2020-01-31`, for severity `warning` or `error`, in channel `app`
18+
Search all log entries between `2020-01-01` and `2020-01-31`, for severity `warning` or `error`, in channel `app`
1719
excluding all entries that contain the word `"Controller"` and must include `"Exception"`.
1820

1921
```text
@@ -40,3 +42,18 @@ word.
4042
```text
4143
before:"2020-01-31 23:59:59" after:"2020-01-01 00:00:00" exclude:"new IndexController" "Failed to read"
4244
```
45+
46+
### Searching context or extra
47+
48+
Matches if at least one of the array values contains the given string:
49+
```text
50+
context:foo
51+
extra:bar
52+
```
53+
54+
### Searching specific array key-values in context or extra:
55+
```text
56+
context:userId=123
57+
context:request.uri=/api/v1/users
58+
extra:trace_id=12345ab
59+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Entity\Expression;
5+
6+
class KeyValueTerm implements TermInterface
7+
{
8+
public const TYPE_CONTEXT = 'context';
9+
public const TYPE_EXTRA = 'extra';
10+
11+
/**
12+
* @codeCoverageIgnore Simple DTO
13+
* @param self::TYPE_* $type
14+
* @param string[]|null $keys
15+
*/
16+
public function __construct(public readonly string $type, public readonly ?array $keys, public readonly string $value)
17+
{
18+
}
19+
}

src/Reader/String/StringReader.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function __construct(private readonly string $string)
1515
$this->length = strlen($this->string);
1616
}
1717

18-
public function get(): string
18+
public function char(): string
1919
{
2020
return $this->string[$this->position];
2121
}
@@ -48,7 +48,7 @@ public function read(string $string): bool
4848
*/
4949
public function skip(array $chars): self
5050
{
51-
while ($this->eol() === false && in_array($this->get(), $chars, true)) {
51+
while ($this->eol() === false && in_array($this->char(), $chars, true)) {
5252
$this->next();
5353
}
5454

@@ -72,6 +72,7 @@ public function next(int $skip = 1): self
7272

7373
/**
7474
* True if the end of line is reached
75+
* @phpstan-impure
7576
*/
7677
public function eol(): bool
7778
{

src/Resources/config/services.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
use FD\LogViewer\Service\Matcher\ChannelTermMatcher;
3232
use FD\LogViewer\Service\Matcher\DateAfterTermMatcher;
3333
use FD\LogViewer\Service\Matcher\DateBeforeTermMatcher;
34+
use FD\LogViewer\Service\Matcher\KeyValueMatcher;
3435
use FD\LogViewer\Service\Matcher\LogRecordMatcher;
3536
use FD\LogViewer\Service\Matcher\SeverityTermMatcher;
3637
use FD\LogViewer\Service\Matcher\WordTermMatcher;
3738
use FD\LogViewer\Service\Parser\DateParser;
3839
use FD\LogViewer\Service\Parser\ExpressionParser;
40+
use FD\LogViewer\Service\Parser\KeyValueParser;
3941
use FD\LogViewer\Service\Parser\QuotedStringParser;
4042
use FD\LogViewer\Service\Parser\StringParser;
4143
use FD\LogViewer\Service\Parser\TermParser;
@@ -46,6 +48,7 @@
4648
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
4749

4850
use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service;
51+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
4952
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
5053

5154
return static function (ContainerConfigurator $container): void {
@@ -72,17 +75,20 @@
7275
$services->set(JsonManifestAssetLoader::class)
7376
->arg('$manifestPath', '%kernel.project_dir%/public/bundles/fdlogviewer/.vite/manifest.json');
7477

78+
$services->set(StringParser::class)
79+
->arg('$quotedStringParser', inline_service(QuotedStringParser::class))
80+
->arg('$wordParser', inline_service(WordParser::class));
7581
$services->set(ExpressionParser::class)
7682
->arg(
7783
'$termParser',
7884
inline_service(TermParser::class)
79-
->arg(
80-
'$stringParser',
81-
inline_service(StringParser::class)
82-
->arg('$quotedStringParser', inline_service(QuotedStringParser::class))
83-
->arg('$wordParser', inline_service(WordParser::class))
85+
->args(
86+
[
87+
service(StringParser::class),
88+
inline_service(DateParser::class),
89+
inline_service(KeyValueParser::class)->args([service(StringParser::class)])
90+
]
8491
)
85-
->arg('$dateParser', inline_service(DateParser::class))
8692
);
8793

8894
$services->set(FinderFactory::class);
@@ -110,6 +116,7 @@
110116
$services->set(DateAfterTermMatcher::class)->tag('fd.symfony.log.viewer.term_matcher');
111117
$services->set(SeverityTermMatcher::class)->tag('fd.symfony.log.viewer.term_matcher');
112118
$services->set(ChannelTermMatcher::class)->tag('fd.symfony.log.viewer.term_matcher');
119+
$services->set(KeyValueMatcher::class)->tag('fd.symfony.log.viewer.term_matcher');
113120
$services->set(WordTermMatcher::class)->tag('fd.symfony.log.viewer.term_matcher');
114121
$services->set(LogRecordMatcher::class)->arg('$termMatchers', tagged_iterator('fd.symfony.log.viewer.term_matcher'));
115122
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Service\Matcher;
5+
6+
use FD\LogViewer\Entity\Expression\KeyValueTerm;
7+
use FD\LogViewer\Entity\Expression\TermInterface;
8+
use FD\LogViewer\Entity\Index\LogRecord;
9+
10+
/**
11+
* @implements TermMatcherInterface<KeyValueTerm>
12+
*/
13+
class KeyValueMatcher implements TermMatcherInterface
14+
{
15+
public function supports(TermInterface $term): bool
16+
{
17+
return $term instanceof KeyValueTerm;
18+
}
19+
20+
public function matches(TermInterface $term, LogRecord $record): bool
21+
{
22+
$data = $term->type === KeyValueTerm::TYPE_CONTEXT ? $record->context : $record->extra;
23+
24+
return $term->keys === null ? $this->matchValue($term->value, $data) : $this->matchKeysValue($term->keys, $term->value, $data);
25+
}
26+
27+
/**
28+
* @param string|array<int|string, mixed> $data
29+
*/
30+
private function matchValue(string $term, string|array $data): bool
31+
{
32+
if (is_string($data)) {
33+
return stripos($data, $term) !== false;
34+
}
35+
36+
$match = false;
37+
array_walk_recursive(
38+
$data,
39+
function ($value) use (&$match, $term) {
40+
$match = $match || stripos((string)$value, $term) !== false;
41+
}
42+
);
43+
44+
return $match;
45+
}
46+
47+
/**
48+
* @param string[] $keys
49+
* @param string|array<int|string, mixed> $data
50+
*/
51+
private function matchKeysValue(array $keys, string $term, string|array $data): bool
52+
{
53+
if (is_string($data)) {
54+
return stripos($data, $term) !== false;
55+
}
56+
57+
$value = $data;
58+
foreach ($keys as $key) {
59+
if (is_array($value) === false || isset($value[$key]) === false) {
60+
return false;
61+
}
62+
$value = $value[$key];
63+
}
64+
65+
return is_scalar($value) && stripos((string)$value, $term) !== false;
66+
}
67+
}

src/Service/Parser/ExpressionParser.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
/**
1010
* BNF:
1111
* <expression> ::= <term> | <term> <expression>
12-
* <term> ::= <date-term> | <exclude-term> | <severity-term> | <channel-term> <string>
12+
* <term> ::= <date-term> | <exclude-term> | <severity-term> | <channel-term> <string> | <context-term> | <extra-term>
1313
* <date-term> ::= before:<string> | af
1414
* <severity-term> ::= severity:<string>
1515
* <channel-term> ::= channel:<string>
1616
* <exclude-term> ::= exclude:<string>
17+
* <context-term> ::= context:<string> | context:<key>=<string>
18+
* <extra-term> ::= extra:<string> | extra:<key>=<string>
1719
* <string> ::= "<characters without unescaped double quote>" | '<characters without unescaped quote>' | <characters-without-space>
1820
*/
1921
class ExpressionParser
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Service\Parser;
5+
6+
use FD\LogViewer\Entity\Expression\KeyValueTerm;
7+
use FD\LogViewer\Reader\String\StringReader;
8+
9+
class KeyValueParser
10+
{
11+
public function __construct(private readonly StringParser $stringParser)
12+
{
13+
}
14+
15+
/**
16+
* @phpstan-param KeyValueTerm::TYPE_* $type
17+
*/
18+
public function parse(string $type, StringReader $string): KeyValueTerm
19+
{
20+
$value = $this->stringParser->parse($string, ['.', '=']);
21+
if ($string->eol()) {
22+
return new KeyValueTerm($type, null, $value);
23+
}
24+
25+
$keys = [$value];
26+
if ($string->char() === '.') {
27+
while ($string->eol() === false && $string->char() === '.') {
28+
$string->next();
29+
$keys[] = $this->stringParser->parse($string, ['.', '=']);
30+
}
31+
32+
if ($string->char() !== '=') {
33+
return new KeyValueTerm($type, null, implode('.', $keys));
34+
}
35+
}
36+
37+
if ($string->char() === '=') {
38+
// skip =
39+
$string->next();
40+
41+
return new KeyValueTerm($type, $keys, $this->stringParser->parse($string));
42+
}
43+
44+
return new KeyValueTerm($type, null, $value);
45+
}
46+
}

src/Service/Parser/QuotedStringParser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function parse(StringReader $string, string $quote, string $escapeChar):
1515
$result = '';
1616
$escaped = false;
1717
for (; $string->eol() === false; $string->next()) {
18-
$char = $string->get();
18+
$char = $string->char();
1919

2020
// skip the escape character
2121
if ($char === $escapeChar && $escaped === false) {

src/Service/Parser/StringParser.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ public function __construct(private readonly QuotedStringParser $quotedStringPar
1111
{
1212
}
1313

14-
public function parse(StringReader $string): string
14+
/**
15+
* @param string[] $stopAt
16+
*/
17+
public function parse(StringReader $string, array $stopAt = []): string
1518
{
16-
if (in_array($string->get(), ['"', "'"], true)) {
17-
return $this->quotedStringParser->parse($string, $string->get(), '\\');
19+
if ($string->eol() === false && in_array($string->char(), ['"', "'"], true)) {
20+
return $this->quotedStringParser->parse($string, $string->char(), '\\');
1821
}
1922

20-
return $this->wordParser->parse($string);
23+
return $this->wordParser->parse($string, $stopAt);
2124
}
2225
}

src/Service/Parser/TermParser.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use FD\LogViewer\Entity\Expression\ChannelTerm;
77
use FD\LogViewer\Entity\Expression\DateAfterTerm;
88
use FD\LogViewer\Entity\Expression\DateBeforeTerm;
9+
use FD\LogViewer\Entity\Expression\KeyValueTerm;
910
use FD\LogViewer\Entity\Expression\SeverityTerm;
1011
use FD\LogViewer\Entity\Expression\TermInterface;
1112
use FD\LogViewer\Entity\Expression\WordTerm;
@@ -19,8 +20,11 @@
1920
*/
2021
class TermParser
2122
{
22-
public function __construct(private readonly StringParser $stringParser, private readonly DateParser $dateParser)
23-
{
23+
public function __construct(
24+
private readonly StringParser $stringParser,
25+
private readonly DateParser $dateParser,
26+
private readonly KeyValueParser $keyValueParser
27+
) {
2428
}
2529

2630
/**
@@ -50,6 +54,14 @@ public function parse(StringReader $string): TermInterface
5054
return new WordTerm($this->stringParser->parse($string), WordTerm::TYPE_EXCLUDE);
5155
}
5256

57+
if ($string->read('context:')) {
58+
return $this->keyValueParser->parse(KeyValueTerm::TYPE_CONTEXT, $string);
59+
}
60+
61+
if ($string->read('extra:')) {
62+
return $this->keyValueParser->parse(KeyValueTerm::TYPE_EXTRA, $string);
63+
}
64+
5365
return new WordTerm($this->stringParser->parse($string), WordTerm::TYPE_INCLUDE);
5466
}
5567
}

0 commit comments

Comments
 (0)