Skip to content

Commit d0a6cb6

Browse files
authored
Add level and channel filter to search query expression (#29)
1 parent ba447c6 commit d0a6cb6

28 files changed

+354
-127
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ read logs from any directory.
2121

2222
- 📂 **View all the Monolog logs** in your `%kernel.logs_dir%` directory,
2323
- 🔍 **Search** the logs,
24-
- 🎚 **Filter** by log level (error, info, debug, etc.), or by channel.
24+
- 🎚 **Filter** by log level (error, info, debug, etc.), by channel, date range or log content,
2525
- 🌑 **Dark mode**,
2626
- 💾 **Download** or **delete** log files from the UI,
2727
- ☎️ **API access** for folders, files & log entries,

docs/advanced-search-queries.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ The search query allows for more fine-grained control over the search results. T
66
|--------------------------------------|-------|---------------------------------------------------------------------------------|
77
| `before:<date>`,`before:"<date>"` | `b` | Show all logs messages that occur before the specified date. |
88
| `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. |
912
| `exclude:<word>`,`exclude:"<words>"` | `-` | Exclude the specific sentence from the results. Can be specified multiple times |
1013

1114
## Example
1215

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

1619
```text
17-
before:2020-01-31 after:2020-01-01 exclude:Controller "Failed to read"
20+
before:2020-01-31 after:2020-01-01 severity:warning|error channel:app exclude:Controller "Failed to read"
1821
```
1922

2023
### In shorthand
2124

2225
```text
23-
b:2020-01-31 a:2020-01-01 -:Controller "Failed to read"
26+
b:2020-01-31 a:2020-01-01 s:warning|error c:app -:Controller "Failed to read"
2427
```
2528

2629
### Multiple exclusions

frontend/src/components/LogFile.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import ButtonGroup from '@/components/ButtonGroup.vue';
33
import type LogFile from '@/models/LogFile';
44
import bus from '@/services/EventBus';
5+
import {useSearchStore} from '@/stores/search';
56
import axios from 'axios';
67
import {ref, watch} from 'vue';
78
import {useRoute, useRouter} from 'vue-router';
@@ -14,6 +15,7 @@ const toggleRef = ref();
1415
const selectedFile = ref<string | null>(null);
1516
const route = useRoute();
1617
const router = useRouter();
18+
const searchStore = useSearchStore();
1719
const baseUri = axios.defaults.baseURL;
1820
const deleteFile = (identifier: string) => {
1921
axios.delete('/api/file/' + encodeURI(identifier))
@@ -32,7 +34,7 @@ watch(() => route.query.file, () => selectedFile.value = String(route.query.file
3234
<!-- LogFile -->
3335
<button-group ref="toggleRef" alignment="right" :split="file.can_download || file.can_delete" class="mb-1" :hide-on-selected="true">
3436
<template v-slot:btn_left>
35-
<router-link :to="'/log?file=' + encodeURI(file.identifier)"
37+
<router-link :to="'/log?' + searchStore.toQueryString({file: file.identifier})"
3638
class="btn btn-file text-start btn-outline-primary w-100"
3739
v-bind:class="{'btn-outline-primary-active': selectedFile === file.identifier }"
3840
:title="file.name">

frontend/src/models/LogRecords.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ import type Paginator from '@/models/Paginator';
33
import type Performance from '@/models/Performance';
44

55
export default interface LogRecords {
6-
levels: {
7-
choices: {[key: string]: string};
8-
selected: string[];
9-
};
10-
channels: {
11-
choices: {[key: string]: string};
12-
selected: string[];
13-
};
146
logs: LogRecord[];
157
paginator: Paginator | null;
168
performance?: Performance;

frontend/src/stores/log_records.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,20 @@ import {ref} from 'vue'
55

66
export const useLogRecordStore = defineStore('log_records', () => {
77
const defaultData: LogRecords = {
8-
levels: {choices: {}, selected: []},
9-
channels: {choices: {}, selected: []},
108
logs: [],
119
paginator: null
1210
};
1311

1412
const loading = ref(false);
1513
const records = ref<LogRecords>(defaultData);
1614

17-
async function fetch(file: string, levels: string[], channels: string[], direction: string, perPage: string, query: string, offset: number) {
15+
async function fetch(file: string, direction: string, perPage: string, query: string, offset: number) {
1816
const params: { [key: string]: string } = {file, direction, per_page: perPage};
1917

2018
if (query !== '') {
2119
params.query = query;
2220
}
23-
const levelChoices = Object.keys(records.value.levels.choices);
24-
if (levels.length > 0 && levels.length !== levelChoices.length) {
25-
params.levels = levels.join(',');
26-
}
27-
const channelChoices = Object.keys(records.value.channels.choices);
28-
if (channels.length > 0 && channels.length !== channelChoices.length) {
29-
params.channels = channels.join(',');
30-
}
21+
3122
if (offset > 0) {
3223
params.offset = offset.toString();
3324
}

frontend/src/stores/search.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {defineStore} from 'pinia'
2+
import {ref} from 'vue'
3+
4+
export const useSearchStore = defineStore('search', () => {
5+
const query = ref('');
6+
const perPage = ref('50');
7+
const sort = ref('desc');
8+
9+
function toQueryString(params: { [key: string]: string } = {}): string {
10+
if (query.value !== '') {
11+
params.query = query.value;
12+
}
13+
14+
if (perPage.value !== '50') {
15+
params.perPage = perPage.value;
16+
}
17+
18+
if (sort.value !== 'desc') {
19+
params.sort = sort.value;
20+
}
21+
22+
return new URLSearchParams(params).toString();
23+
}
24+
25+
return {query, perPage, sort, toQueryString}
26+
});

frontend/src/views/LogView.vue

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
11
<script setup lang="ts">
2-
import DropdownChecklist from '@/components/DropdownChecklist.vue';
32
import LogRecord from '@/components/LogRecord.vue';
43
import PerformanceDetails from '@/components/PerformanceDetails.vue';
5-
import type Checklist from '@/models/Checklist';
6-
import Arrays from '@/services/Arrays';
74
import {filter} from '@/services/Objects';
85
import {nullify} from '@/services/Optional';
96
import {useLogRecordStore} from '@/stores/log_records';
7+
import {useSearchStore} from '@/stores/search';
108
import {onMounted, ref} from 'vue';
119
import {useRoute, useRouter} from 'vue-router';
1210
1311
const router = useRouter();
1412
const route = useRoute();
1513
const logRecordStore = useLogRecordStore();
14+
const searchStore = useSearchStore();
1615
16+
const searchRef = ref<HTMLInputElement>();
1717
const file = ref('');
18-
const query = ref('');
19-
const levels = ref<Checklist>({choices: {}, selected: []});
20-
const channels = ref<Checklist>({choices: {}, selected: []});
21-
const perPage = ref('50');
22-
const sort = ref('desc');
2318
const offset = ref(0);
2419
const badRequest = ref(false);
2520
2621
const navigate = () => {
27-
const fileOffset = offset.value > 0 && logRecordStore.records.paginator?.direction !== sort.value ? 0 : offset.value;
22+
const fileOffset = offset.value > 0 && logRecordStore.records.paginator?.direction !== searchStore.sort ? 0 : offset.value;
2823
router.push({
2924
query: filter({
3025
file: file.value,
31-
query: nullify(query.value, ''),
32-
perPage: nullify(perPage.value, '50'),
33-
sort: nullify(sort.value, 'desc'),
34-
levels: nullify(levels.value.selected.join(','), Object.keys(logRecordStore.records.levels.choices).join(',')),
35-
channels: nullify(channels.value.selected.join(','), Object.keys(logRecordStore.records.channels.choices).join(',')),
26+
query: nullify(searchStore.query, ''),
27+
perPage: nullify(searchStore.perPage, '50'),
28+
sort: nullify(searchStore.sort, 'desc'),
3629
offset: nullify(fileOffset, 0)
3730
})
3831
});
@@ -41,53 +34,47 @@ const navigate = () => {
4134
const load = () => {
4235
badRequest.value = false;
4336
logRecordStore
44-
.fetch(file.value, levels.value.selected, channels.value.selected, sort.value, perPage.value, query.value, offset.value)
45-
.then(() => {
46-
levels.value = logRecordStore.records.levels;
47-
channels.value = logRecordStore.records.channels;
48-
})
37+
.fetch(file.value, searchStore.sort, searchStore.perPage, searchStore.query, offset.value)
4938
.catch((error: Error) => {
5039
if (error.message === 'bad-request') {
5140
badRequest.value = true;
5241
return;
5342
}
54-
5543
router.push({name: error.message});
44+
})
45+
.finally(() => {
46+
searchRef.value?.focus();
5647
});
5748
}
5849
5950
onMounted(() => {
60-
file.value = String(route.query.file);
61-
query.value = String((route.query.query ?? ''));
62-
perPage.value = String((route.query.perPage ?? '50'));
63-
sort.value = String((route.query.sort ?? 'desc'));
64-
levels.value.selected = Arrays.split(String(route.query.levels ?? ''), ',');
65-
channels.value.selected = Arrays.split(String(route.query.channels ?? ''), ',');
66-
offset.value = parseInt(String(route.query.offset ?? '0'));
51+
file.value = String(route.query.file);
52+
searchStore.query = String((route.query.query ?? ''));
53+
searchStore.perPage = String((route.query.perPage ?? '50'));
54+
searchStore.sort = String((route.query.sort ?? 'desc'));
55+
offset.value = parseInt(String(route.query.offset ?? '0'));
6756
load();
6857
});
6958
</script>
7059

7160
<template>
72-
<div class="slv-content h-100 overflow-hidden slv-loadable" v-bind:class="{ 'slv-loading': logRecordStore.loading }">
61+
<div class="slv-content h-100 overflow-hidden">
7362
<div class="d-flex align-items-stretch pt-1">
74-
<dropdown-checklist label="Levels" v-model="levels" class="pe-1"></dropdown-checklist>
75-
<dropdown-checklist label="Channels" v-model="channels" class="pe-1"></dropdown-checklist>
76-
7763
<div class="flex-grow-1 input-group">
7864
<input type="text"
7965
class="form-control"
8066
:class="{'is-invalid': badRequest}"
81-
placeholder="Search log entries"
82-
aria-label="Search log entries"
67+
ref="searchRef"
68+
placeholder="Search log entries, Use severity:, channel:, before:, after:, or exclude: to fine-tune the search."
69+
aria-label="Search log entries, Use severity:, channel:, before:, after:, or exclude: to fine-tune the search."
8370
aria-describedby="button-search"
8471
@change="navigate"
85-
v-model="query">
72+
v-model="searchStore.query">
8673

8774
<select class="slv-menu-sort-direction form-control"
8875
aria-label="Sort direction"
8976
title="Sort direction"
90-
v-model="sort"
77+
v-model="searchStore.sort"
9178
@change="navigate">
9279
<option value="desc">Newest First</option>
9380
<option value="asc">Oldest First</option>
@@ -96,7 +83,7 @@ onMounted(() => {
9683
<select class="slv-menu-page-size form-control"
9784
aria-label="Entries per page"
9885
title="Entries per page"
99-
v-model="perPage"
86+
v-model="searchStore.perPage"
10087
@change="navigate">
10188
<option value="50">50</option>
10289
<option value="100">100</option>
@@ -112,17 +99,15 @@ onMounted(() => {
11299
<button class="btn btn-dark ms-1 me-1" type="button" aria-label="Refresh" title="Refresh" @click="load">
113100
<i class="bi bi-arrow-clockwise"></i>
114101
</button>
115-
116-
<div></div>
117102
</div>
118103

119-
<main class="overflow-auto d-none d-md-block">
104+
<main class="overflow-auto d-none d-md-block slv-loadable" v-bind:class="{ 'slv-loading': logRecordStore.loading }">
120105
<div class="slv-entries list-group pt-1 pe-1 pb-3">
121106
<log-record :logRecord="record" v-for="(record, index) in logRecordStore.records.logs ?? []" v-bind:key="index"></log-record>
122107
</div>
123108
</main>
124109

125-
<footer class="pt-1 pb-1 d-flex">
110+
<footer class="pt-1 pb-1 d-flex" v-show="!logRecordStore.loading">
126111
<button class="btn btn-sm btn-outline-secondary"
127112
@click="offset = 0; navigate()"
128113
v-bind:disabled="logRecordStore.records.paginator?.first !== false">
@@ -131,7 +116,7 @@ onMounted(() => {
131116
<button class="ms-2 btn btn-sm btn-outline-secondary"
132117
@click="offset = logRecordStore.records.paginator?.offset ?? 0; navigate()"
133118
v-bind:disabled="logRecordStore.records.paginator?.more !== true">
134-
Next {{ perPage }}
119+
Next {{ searchStore.perPage }}
135120
</button>
136121

137122
<div class="flex-grow-1"></div>

phpmd.baseline.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0"?>
2+
<phpmd-baseline>
3+
<violation rule="PHPMD\Rule\CyclomaticComplexity" file="src/Service/Parser/TermParser.php" method="parse"/>
4+
<violation rule="PHPMD\Rule\Design\NpathComplexity" file="src/Service/Parser/TermParser.php" method="parse"/>
5+
</phpmd-baseline>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Entity\Expression;
5+
6+
class ChannelTerm implements TermInterface
7+
{
8+
/**
9+
* @codeCoverageIgnore Simple DTO
10+
*
11+
* @param string[] $channels
12+
*/
13+
public function __construct(public readonly array $channels)
14+
{
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FD\LogViewer\Entity\Expression;
5+
6+
class SeverityTerm implements TermInterface
7+
{
8+
/**
9+
* @codeCoverageIgnore Simple DTO
10+
*
11+
* @param string[] $severities
12+
*/
13+
public function __construct(public readonly array $severities)
14+
{
15+
}
16+
}

0 commit comments

Comments
 (0)