Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1f76c7f
Add search expression parser
frankdekker Jan 22, 2024
29bc716
Word parser
frankdekker Jan 22, 2024
989e61b
Quoted string parser
frankdekker Jan 22, 2024
5e168ce
Add coverage
frankdekker Jan 23, 2024
dd0f904
Add coverage
frankdekker Jan 23, 2024
3dbbff2
Add coverage
frankdekker Jan 23, 2024
18809f3
Add coverage
frankdekker Jan 23, 2024
0e50a3f
Add coverage
frankdekker Jan 23, 2024
5521710
Add coverage
frankdekker Jan 23, 2024
d6e30f6
Add coverage
frankdekker Jan 23, 2024
3203422
Add coverage
frankdekker Jan 23, 2024
e59811c
Add coverage
frankdekker Jan 23, 2024
c42c081
Add coverage
frankdekker Jan 23, 2024
a00f1ae
Add coverage
frankdekker Jan 23, 2024
79e5fe0
Add expression to search
frankdekker Jan 23, 2024
1f0e410
Show error page on api failure
frankdekker Jan 23, 2024
69c7eca
Apply filter
frankdekker Jan 23, 2024
c1d4f6a
Merge branch 'master' into Add-search-expressions
frankdekker Jan 26, 2024
552a93c
Merge branch 'master' into Add-search-expressions
frankdekker Jan 26, 2024
60c9cf0
Merge branch 'master' into Add-search-expressions
frankdekker Jan 27, 2024
41c65e3
Update tests
frankdekker Jan 27, 2024
10097e7
Update documentation
frankdekker Jan 27, 2024
06364b3
Update documentation
frankdekker Jan 27, 2024
b0e00da
Add shorthands
frankdekker Jan 27, 2024
dfbc821
Fix tests
frankdekker Jan 27, 2024
3c0fd84
Fix tests
frankdekker Jan 27, 2024
1b28f6c
Fix complexity
frankdekker Jan 27, 2024
e9c6825
Add coverage
frankdekker Jan 27, 2024
f2e3500
Add coverage
frankdekker Jan 27, 2024
bb602b1
Add coverage
frankdekker Jan 27, 2024
82e462a
Update assets
frankdekker Jan 27, 2024
83edf82
Update assets
frankdekker Jan 27, 2024
36d099e
Add coverage
frankdekker Jan 27, 2024
a181fe5
Add support for invalid search queries
frankdekker Jan 27, 2024
7f71e1e
Add coverage
frankdekker Jan 27, 2024
c86b06c
Add bad request validation
frankdekker Jan 27, 2024
1861eee
Add bad request validation
frankdekker Jan 27, 2024
b251c0c
Build assets
frankdekker Jan 27, 2024
41277af
Build assets
frankdekker Jan 27, 2024
42d34ec
Build assets
frankdekker Jan 27, 2024
5d35ea8
Fix tests
frankdekker Jan 27, 2024
04279e7
Add coverage
frankdekker Jan 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ By default, the application is available at: `{APP_URL}/log-viewer`.
- [Disabling the default monolog configuration](docs/disabling-default-monolog-configuration.md)
- [Adding additional log files](docs/adding-additional-log-files.md)
- [Configuring the back home url](docs/configuring-the-back-home-route.md)
- [Advanced search queries](docs/search-queries.md)
- [Full configuration reference](docs/configuration-reference.md)
39 changes: 39 additions & 0 deletions docs/advanced-search-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Searching logs

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

| Operator | Short | Description |
|--------------------------------------|-------|---------------------------------------------------------------------------------|
| `before:<date>`,`before:"<date>"` | `b` | Show all logs messages that occur before the specified date. |
| `after:<date>`,`after:"<date>"` | `a` | Show all logs messages that occur after the specified date. |
| `exclude:<word>`,`exclude:"<words>"` | `-` | Exclude the specific sentence from the results. Can be specified multiple times |

## Example

Search all log entries between `2020-01-01` and `2020-01-31`, excluding all entries that contain the word `"Controller"` and must
include `"Exception"`.

```text
before:2020-01-31 after:2020-01-01 exclude:Controller "Failed to read"
```

### In shorthand

```text
b:2020-01-31 a:2020-01-01 -:Controller "Failed to read"
```

### Multiple exclusions

```text
before:2020-01-31 after:2020-01-01 exclude:Controller exclude:404
```

### Handling whitespace

If you want to search for a sentence that contains whitespace, you can use double quotes to indicate that the words should be treated as a single
word.

```text
before:"2020-01-31 23:59:59" after:"2020-01-01 00:00:00" exclude:"new IndexController" "Failed to read"
```
6 changes: 6 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ErrorView from '@/views/ErrorView.vue';
import FileNotFoundView from '@/views/FileNotFoundView.vue';
import HomeView from '@/views/HomeView.vue'
import LogView from '@/views/LogView.vue';
Expand All @@ -21,6 +22,11 @@ function router(baseUri: string) {
path: '/404',
name: 'file-not-found',
component: FileNotFoundView
},
{
path: '/5XX',
name: 'error',
component: ErrorView
}
]
});
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/stores/log_records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ export const useLogRecordStore = defineStore('log_records', () => {
const response = await axios.get<LogRecords>('/api/logs', {params: params});
records.value = response.data;
} catch (e) {
if (e instanceof AxiosError && e.response?.status === 400) {
throw new Error('bad-request');
}
if (e instanceof AxiosError && e.response?.status === 404) {
throw e;
throw new Error('file-not-found');
}
if (e instanceof AxiosError && [500, 501, 502, 503, 504].includes(Number(e.response?.status))) {
throw new Error('error');
}
console.error(e);
records.value = defaultData;
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/views/ErrorView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div class="failure">
<div class="alert alert-danger label">
An error occurred while reading the log file.
</div>
</div>
</template>

<style scoped>
.failure {
position: relative;

.label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>
<script setup lang="ts"></script>
26 changes: 18 additions & 8 deletions frontend/src/views/LogView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ const router = useRouter();
const route = useRoute();
const logRecordStore = useLogRecordStore();

const file = ref('');
const query = ref('');
const levels = ref<Checklist>({choices: {}, selected: []});
const channels = ref<Checklist>({choices: {}, selected: []});
const perPage = ref('50');
const sort = ref('desc');
const offset = ref(0);
const file = ref('');
const query = ref('');
const levels = ref<Checklist>({choices: {}, selected: []});
const channels = ref<Checklist>({choices: {}, selected: []});
const perPage = ref('50');
const sort = ref('desc');
const offset = ref(0);
const badRequest = ref(false);

const navigate = () => {
const fileOffset = offset.value > 0 && logRecordStore.records.paginator?.direction !== sort.value ? 0 : offset.value;
Expand All @@ -38,13 +39,21 @@ const navigate = () => {
}

const load = () => {
badRequest.value = false;
logRecordStore
.fetch(file.value, levels.value.selected, channels.value.selected, sort.value, perPage.value, query.value, offset.value)
.then(() => {
levels.value = logRecordStore.records.levels;
channels.value = logRecordStore.records.channels;
})
.catch(() => router.push({name: 'file-not-found'}));
.catch((error: Error) => {
if (error.message === 'bad-request') {
badRequest.value = true;
return;
}

router.push({name: error.message});
});
}

onMounted(() => {
Expand All @@ -68,6 +77,7 @@ onMounted(() => {
<div class="flex-grow-1 input-group">
<input type="text"
class="form-control"
:class="{'is-invalid': badRequest}"
placeholder="Search log entries"
aria-label="Search log entries"
aria-describedby="button-search"
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions src/Controller/LogRecordsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

namespace FD\LogViewer\Controller;

use Exception;
use FD\LogViewer\Service\File\LogFileService;
use FD\LogViewer\Service\File\LogQueryDtoFactory;
use FD\LogViewer\Service\File\LogRecordsOutputProvider;
use FD\LogViewer\Service\Parser\InvalidDateTimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class LogRecordsController extends AbstractController
Expand All @@ -20,10 +23,18 @@ public function __construct(
) {
}

/**
* @throws Exception
*/
public function __invoke(Request $request): JsonResponse
{
$logQuery = $this->queryDtoFactory->create($request);
$file = $this->fileService->findFileByIdentifier($logQuery->fileIdentifier);
try {
$logQuery = $this->queryDtoFactory->create($request);
} catch (InvalidDateTimeException $exception) {
throw new BadRequestHttpException('Invalid date.', $exception);
}

$file = $this->fileService->findFileByIdentifier($logQuery->fileIdentifier);
if ($file === null) {
throw new NotFoundHttpException(sprintf('Log file with id `%s` not found.', $logQuery->fileIdentifier));
}
Expand Down
16 changes: 16 additions & 0 deletions src/Entity/Expression/DateAfterTerm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\Entity\Expression;

use DateTimeImmutable;

class DateAfterTerm implements TermInterface
{
/**
* @codeCoverageIgnore Simple DTO
*/
public function __construct(public readonly DateTimeImmutable $date)
{
}
}
16 changes: 16 additions & 0 deletions src/Entity/Expression/DateBeforeTerm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\Entity\Expression;

use DateTimeImmutable;

class DateBeforeTerm implements TermInterface
{
/**
* @codeCoverageIgnore Simple DTO
*/
public function __construct(public readonly DateTimeImmutable $date)
{
}
}
15 changes: 15 additions & 0 deletions src/Entity/Expression/Expression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\Entity\Expression;

class Expression
{
/**
* @codeCoverageIgnore Simple DTO
* @param TermInterface[] $terms
*/
public function __construct(public readonly array $terms)
{
}
}
8 changes: 8 additions & 0 deletions src/Entity/Expression/TermInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\Entity\Expression;

interface TermInterface
{
}
18 changes: 18 additions & 0 deletions src/Entity/Expression/WordTerm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\Entity\Expression;

class WordTerm implements TermInterface
{
public const TYPE_INCLUDE = 'include';
public const TYPE_EXCLUDE = 'exclude';

/**
* @codeCoverageIgnore Simple DTO
* @phpstan-param self::TYPE_* $type
*/
public function __construct(public readonly string $string, public readonly string $type)
{
}
}
3 changes: 2 additions & 1 deletion src/Entity/Request/LogQueryDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace FD\LogViewer\Entity\Request;

use FD\LogViewer\Entity\Expression\Expression;
use FD\LogViewer\Entity\Output\DirectionEnum;

class LogQueryDto
Expand All @@ -16,7 +17,7 @@ class LogQueryDto
public function __construct(
public readonly string $fileIdentifier,
public readonly ?int $offset = null,
public readonly string $query = '',
public readonly ?Expression $query = null,
public readonly DirectionEnum $direction = DirectionEnum::Desc,
public readonly ?array $levels = null,
public readonly ?array $channels = null,
Expand Down
2 changes: 1 addition & 1 deletion src/Iterator/LogLineParserIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
namespace FD\LogViewer\Iterator;

use FD\LogViewer\Entity\Output\DirectionEnum;
use FD\LogViewer\Reader\Stream\AbstractStreamReader;
use FD\LogViewer\Service\File\LogLineParserInterface;
use FD\LogViewer\StreamReader\AbstractStreamReader;
use IteratorAggregate;
use Traversable;

Expand Down
17 changes: 14 additions & 3 deletions src/Iterator/LogRecordFilterIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

namespace FD\LogViewer\Iterator;

use FD\LogViewer\Entity\Expression\Expression;
use FD\LogViewer\Entity\Index\LogRecord;
use FD\LogViewer\Service\Matcher\LogRecordMatcher;
use IteratorAggregate;
use Traversable;

Expand All @@ -22,9 +24,14 @@ class LogRecordFilterIterator implements IteratorAggregate
* @param string[]|null $levels
* @param string[]|null $channels
*/
public function __construct(private readonly iterable $iterator, ?array $levels, ?array $channels)
{
$this->levels = $levels === null ? null : array_flip($levels);
public function __construct(
private readonly LogRecordMatcher $matcher,
private readonly iterable $iterator,
private readonly ?Expression $query,
?array $levels,
?array $channels
) {
$this->levels = $levels === null ? null : array_flip($levels);
$this->channels = $channels === null ? null : array_flip($channels);
}

Expand All @@ -39,6 +46,10 @@ public function getIterator(): Traversable
continue;
}

if ($this->query !== null && $this->matcher->matches($record, $this->query) === false) {
continue;
}

yield $key => $record;
}
}
Expand Down
6 changes: 1 addition & 5 deletions src/Iterator/LogRecordIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@ class LogRecordIterator implements IteratorAggregate
public function __construct(
private readonly Traversable $iterator,
private readonly LogLineParserInterface $lineParser,
private readonly string $query
) {
}

public function getIterator(): Traversable
{
foreach ($this->iterator as $message) {
if ($this->query !== '' && stripos($message, $this->query) === false) {
continue;
}

$lineData = $this->lineParser->parse($message);
if ($lineData === null) {
yield new LogRecord(0, 'error', 'parse', $message, [], []);
continue;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\StreamReader;
namespace FD\LogViewer\Reader\Stream;

use Generator;
use IteratorAggregate;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\StreamReader;
namespace FD\LogViewer\Reader\Stream;

use Generator;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\StreamReader;
namespace FD\LogViewer\Reader\Stream;

use Generator;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);

namespace FD\LogViewer\StreamReader;
namespace FD\LogViewer\Reader\Stream;

use FD\LogViewer\Entity\Output\DirectionEnum;
use RuntimeException;
Expand Down
Loading