Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reading HTTP access logs #242

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
58f7763
WIP: http access logs
arukompas Jul 10, 2023
90daf6d
Fix styling
arukompas Jul 10, 2023
f165329
WIP: http access & error logs, with more tests
arukompas Jul 11, 2023
a4c64b9
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 11, 2023
9c9dce8
Fix styling
arukompas Jul 11, 2023
37c82f6
more improvements, bug fixes
arukompas Jul 11, 2023
c0e98c3
more improvements, support Nginx error logs
arukompas Jul 12, 2023
6ad4ef2
Fix styling
arukompas Jul 12, 2023
f7c25af
WIP: displaying access logs in the UI
arukompas Jul 13, 2023
5a0a05b
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 13, 2023
83ab161
Fix styling
arukompas Jul 13, 2023
89b4ed9
support for large access log files, other improvements
arukompas Jul 13, 2023
6540c72
small improvements
arukompas Jul 14, 2023
02ffb71
Fix styling
arukompas Jul 14, 2023
1d0d640
refactor LogReader to be more flexible towards more log types
arukompas Jul 14, 2023
c4f9909
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 14, 2023
c99deec
Fix styling
arukompas Jul 14, 2023
1569d39
remove unused HttpLogReader and fix unit tests for the LogReader
arukompas Jul 14, 2023
460c7b6
speed improvements
arukompas Jul 15, 2023
df1af64
Fix styling
arukompas Jul 15, 2023
7273c23
lots of improvements, refactor for http logs
arukompas Jul 16, 2023
095f7ce
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 16, 2023
1a40f4d
Fix styling
arukompas Jul 16, 2023
caed8ac
custom columns for the access/http-error logs
arukompas Jul 16, 2023
d7e07d8
bug fixes + sort severities
arukompas Jul 16, 2023
7a382c5
bug fixes, improvements
arukompas Jul 17, 2023
278ffc4
Fix styling
arukompas Jul 17, 2023
cf6834d
refactor, bug fixes, improvements
arukompas Jul 19, 2023
a498815
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 19, 2023
a790d66
Fix styling
arukompas Jul 19, 2023
ce2f677
refactor, improvements to the log api
arukompas Jul 20, 2023
beaa885
Fix styling
arukompas Jul 20, 2023
215b5a4
caching improvements
arukompas Jul 21, 2023
cc17aac
Merge branch 'main' into access-logs
arukompas Jul 22, 2023
6152121
Fix styling
arukompas Jul 22, 2023
ab3a70e
add Horizon log support
arukompas Jul 22, 2023
d5bcd28
change successful log icon
arukompas Jul 23, 2023
e01ffab
add Postgres and PHP FPM log support
arukompas Jul 23, 2023
06ff6f9
support for Supervisor and Redis logs
arukompas Jul 23, 2023
e23ba21
Fix styling
arukompas Jul 23, 2023
e08ebbd
file type selector + bug fixes
arukompas Jul 23, 2023
d00f833
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 23, 2023
390b6a8
Fix styling
arukompas Jul 23, 2023
e8458b2
fix log levels, add gzip compression on caches
arukompas Jul 28, 2023
83c653a
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 28, 2023
df0fb1e
Fix styling
arukompas Jul 28, 2023
c927a6c
fix tests
arukompas Jul 28, 2023
154b914
Fix styling
arukompas Jul 28, 2023
be17f84
fix broken tests on Windows
arukompas Jul 28, 2023
e12d113
Merge branch 'access-logs' of github.com:opcodesio/log-viewer into ac…
arukompas Jul 28, 2023
0841880
fix tests for Windows
arukompas Jul 28, 2023
66ced08
debug test on windows
arukompas Jul 28, 2023
1abf37c
more debugging of windows test
arukompas Jul 28, 2023
aad97db
potential fix
arukompas Jul 28, 2023
b0258be
remove dumps
arukompas Jul 28, 2023
4af528c
potential fix
arukompas Jul 28, 2023
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ phpstan.neon
testbench.yaml
vendor
node_modules
tests/Feature/performance_test.log
44 changes: 13 additions & 31 deletions config/log-viewer.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

use Opcodes\LogViewer\Level;

return [

/*
Expand Down Expand Up @@ -134,6 +132,19 @@
'include_files' => [
'*.log',
'**/*.log',

// You can include paths to other log types as well, such as apache, nginx, and more.
'/var/log/httpd/*',
'/var/log/nginx/*',

// MacOS Apple Silicon logs
'/opt/homebrew/var/log/nginx/*',
'/opt/homebrew/var/log/httpd/*',
'/opt/homebrew/var/log/php-fpm.log',
'/opt/homebrew/var/log/postgres*log',
'/opt/homebrew/var/log/redis*log',
'/opt/homebrew/var/log/supervisor*log',

// '/absolute/paths/supported',
],

Expand Down Expand Up @@ -164,35 +175,6 @@
'/vendor/barryvdh/laravel-debugbar/',
],

/*
|--------------------------------------------------------------------------
| Log matching patterns
|--------------------------------------------------------------------------
| Regexes for matching log files
|
*/

'patterns' => [
'laravel' => [
'log_matching_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\].*/',

/**
* This pattern, used for processing Laravel logs, returns these results:
* $matches[0] - the full log line being tested.
* $matches[1] - full timestamp between the square brackets (includes microseconds and timezone offset)
* $matches[2] - timestamp microseconds, if available
* $matches[3] - timestamp timezone offset, if available
* $matches[4] - contents between timestamp and the severity level
* $matches[5] - environment (local, production, etc)
* $matches[6] - log severity (info, debug, error, etc)
* $matches[7] - the log text, the rest of the text.
*/
'log_parsing_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\](.*?(\w+)\.|.*?)('
.implode('|', array_filter(Level::caseValues()))
.')?: (.*?)( in [\/].*?:[0-9]+)?$/is',
],
],

/*
|--------------------------------------------------------------------------
| Cache driver
Expand Down
2 changes: 1 addition & 1 deletion public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/app.js": "/app.js?id=2ca3fa12f273bd645611f1acf3d81355",
"/app.css": "/app.css?id=93151d8b186ef7758df8582425ff8082",
"/app.js": "/app.js?id=4e92d6b3e88d6701d24f412e6aec4727",
"/app.css": "/app.css?id=2644d93568c08a41a0612c7c2c38618e",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"
Expand Down
2 changes: 1 addition & 1 deletion resources/css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ html.dark {

.log-list {
table > thead th {
@apply sticky top-0 z-10 bg-gray-100 dark:bg-gray-900 py-2 px-2 text-left text-sm font-semibold text-gray-500 dark:text-gray-400;
@apply sticky top-0 z-10 bg-gray-100 dark:bg-gray-900 py-2 px-1 lg:px-2 text-left text-xs lg:text-sm font-semibold text-gray-500 dark:text-gray-400;
}

.log-group {
Expand Down
159 changes: 159 additions & 0 deletions resources/js/components/BaseLogTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<template>
<table class="table-fixed min-w-full max-w-full border-separate" style="border-spacing: 0">
<thead class="bg-gray-50">
<tr>
<th class="hidden lg:table-cell"><span class="sr-only">Expand/Collapse</span></th>
<th v-for="(column) in logViewerStore.columns" scope="col">
<div>{{ column.label }}</div>
</th>
<th scope="col" class="hidden lg:table-cell"><span class="sr-only">Log index</span></th>
</tr>
</thead>

<template v-if="logViewerStore.logs && logViewerStore.logs.length > 0">
<tbody v-for="(log, index) in logViewerStore.logs" :key="index"
:class="[index === 0 ? 'first' : '', 'log-group']"
:id="`tbody-${index}`" :data-index="index"
>
<tr @click="logViewerStore.toggle(index)"
:class="['log-item group', log.level_class, logViewerStore.isOpen(index) ? 'active' : '', logViewerStore.shouldBeSticky(index) ? 'sticky z-2' : '']"
:style="{ top: logViewerStore.stackTops[index] || 0 }"
>
<td class="log-level hidden lg:table-cell">
<div class="flex items-center lg:pl-2">
<button :aria-expanded="logViewerStore.isOpen(index)"
@keydown="handleLogToggleKeyboardNavigation"
class="log-level-icon opacity-75 w-5 h-5 hidden lg:block group focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-brand-500 rounded-md"
>
<span class="sr-only" v-if="!logViewerStore.isOpen(index)">Expand log entry</span>
<span class="sr-only" v-if="logViewerStore.isOpen(index)">Collapse log entry</span>
<span class="w-full h-full group-hover:hidden group-focus:hidden">
<ExclamationCircleIcon v-if="log.level_class === 'danger'" />
<ExclamationTriangleIcon v-else-if="log.level_class === 'warning'" />
<CheckCircleIcon v-else-if="log.level_class === 'success'" />
<InformationCircleIcon v-else />
</span>
<span class="w-full h-full hidden group-hover:inline-block group-focus:inline-block">
<ChevronRightIcon :class="[logViewerStore.isOpen(index) ? 'rotate-90' : '', 'transition duration-100']" />
</span>
</button>
</div>
</td>

<template v-for="(column, colIndex) in logViewerStore.columns">
<!-- Severity -->
<td :key="`${log.index}-column-${colIndex}`" v-if="column.data_path === 'level'" class="log-level truncate">
<span>{{ log.level_name }}</span>
</td>
<!-- /Severity -->

<!-- Datetime -->
<td :key="`${log.index}-column-${colIndex}`" v-else-if="column.data_path === 'datetime'" class="whitespace-nowrap text-gray-900 dark:text-gray-200">
<span class="hidden lg:inline" v-html="highlightSearchResult(log.datetime, searchStore.query)"></span>
<span class="lg:hidden">{{ log.time }}</span>
</td>
<!-- /Datetime -->

<!-- Message -->
<td :key="`${log.index}-column-${colIndex}`" v-else-if="column.data_path === 'message'" class="max-w-[1px] w-full truncate text-gray-500 dark:text-gray-300 dark:opacity-90">
<span v-html="highlightSearchResult(`${log.message}`, searchStore.query)"></span>
</td>
<!-- /Message -->

<td :key="`${log.index}-column-${colIndex}`" v-else class="text-gray-500 dark:text-gray-300 dark:opacity-90" :class="column.class || ''">
<span v-html="highlightSearchResult(getDataAtPath(log, column.data_path), searchStore.query)"></span>
</td>
</template>

<td class="whitespace-nowrap text-gray-500 dark:text-gray-300 dark:opacity-90 text-xs hidden lg:table-cell">
<LogCopyButton :log="log" class="pr-2 large-screen" />
</td>
</tr>
<tr v-show="logViewerStore.isOpen(index)">
<td colspan="6">
<div class="lg:hidden flex justify-between px-2 pt-2 pb-1 text-xs">
<div class="flex-1"><span class="font-semibold">Datetime:</span> {{ log.datetime }}</div>
<div>
<LogCopyButton :log="log" />
</div>
</div>
<pre class="log-stack" v-html="highlightSearchResult(log.full_text, searchStore.query)"></pre>
<template v-if="hasContext(log)">
<p class="mx-2 lg:mx-8 pt-2 border-t font-semibold text-gray-700 dark:text-gray-400 text-xs lg:text-sm">Context:</p>
<pre class="log-stack" v-html="highlightSearchResult(JSON.stringify(log.context, null, 2), searchStore.query)"></pre>
</template>

<div v-if="log.extra && log.extra.log_text_incomplete" class="py-4 px-8 text-gray-500 italic">
The contents of this log have been cut short to the first {{ LogViewer.max_log_size_formatted }}.
The full size of this log entry is <strong>{{ log.extra.log_size_formatted }}</strong>
</div>
</td>
</tr>
</tbody>
</template>

<tbody v-else class="log-group">
<tr>
<td colspan="6">
<div class="bg-white text-gray-600 dark:bg-gray-800 dark:text-gray-200 p-12">
<div class="text-center font-semibold">No results</div>
<div class="text-center mt-6">
<button v-if="searchStore.query?.length > 0"
class="px-3 py-2 border dark:border-gray-700 text-gray-800 dark:text-gray-200 hover:border-brand-600 dark:hover:border-brand-700 rounded-md"
@click="clearQuery">Clear search query
</button>
<button v-if="searchStore.query?.length > 0 && fileStore.selectedFile"
class="px-3 ml-3 py-2 border dark:border-gray-700 text-gray-800 dark:text-gray-200 hover:border-brand-600 dark:hover:border-brand-700 rounded-md"
@click.prevent="clearSelectedFile">Search all files
</button>
<button
v-if="severityStore.levelsFound.length > 0 && severityStore.levelsSelected.length === 0"
class="px-3 ml-3 py-2 border dark:border-gray-700 text-gray-800 dark:text-gray-200 hover:border-brand-600 dark:hover:border-brand-700 rounded-md"
@click="severityStore.selectAllLevels">Select all severities
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</template>

<script setup>
import {
ChevronRightIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
CheckCircleIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/solid';
import { highlightSearchResult } from '../helpers.js';
import { useLogViewerStore } from '../stores/logViewer.js';
import { useSearchStore } from '../stores/search.js';
import { useFileStore } from '../stores/files.js';
import LogCopyButton from './LogCopyButton.vue';
import { handleLogToggleKeyboardNavigation } from '../keyboardNavigation';
import { useSeverityStore } from '../stores/severity.js';

const fileStore = useFileStore();
const logViewerStore = useLogViewerStore();
const searchStore = useSearchStore();
const severityStore = useSeverityStore();
const emit = defineEmits(['clearSelectedFile', 'clearQuery']);

const clearSelectedFile = () => {
emit('clearSelectedFile');
}

const clearQuery = () => {
emit('clearQuery');
}

const getDataAtPath = (obj, path) => {
return String(path.split('.').reduce((acc, part) => acc && acc[part], obj));
}

const hasContext = (log) => {
return log.context && Object.keys(log.context).length > 0;
}
</script>
9 changes: 7 additions & 2 deletions resources/js/components/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
<host-selector class="mb-8 mt-6" />
</template>

<div class="flex justify-between items-baseline mt-6" v-if="fileStore.folders?.length > 0">
<template v-if="fileStore.fileTypesAvailable && fileStore.fileTypesAvailable.length > 1">
<file-type-selector class="mb-8 mt-6" />
</template>

<div class="flex justify-between items-baseline mt-6" v-if="fileStore.filteredFolders?.length > 0">
<div class="ml-1 block text-sm text-gray-500 dark:text-gray-400 truncate">Log files on {{ fileStore.selectedHost?.name }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<label for="file-sort-direction" class="sr-only">Sort direction</label>
Expand Down Expand Up @@ -72,7 +76,7 @@

<div id="file-list-container" class="relative h-full overflow-hidden">
<div class="file-list" @scroll="(event) => fileStore.onScroll(event)">
<div v-for="folder in fileStore.folders"
<div v-for="folder in fileStore.filteredFolders"
:key="folder.identifier"
:id="`folder-${folder.identifier}`"
class="relative folder-container"
Expand Down Expand Up @@ -215,6 +219,7 @@ import SpinnerIcon from './SpinnerIcon.vue';
import SiteSettingsDropdown from './SiteSettingsDropdown.vue';
import HostSelector from './HostSelector.vue';
import { handleKeyboardFileNavigation, handleKeyboardFileSettingsNavigation } from '../keyboardNavigation';
import FileTypeSelector from './FileTypeSelector.vue';

const router = useRouter();
const route = useRoute();
Expand Down
38 changes: 38 additions & 0 deletions resources/js/components/FileTypeSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<Listbox as="div" v-model="fileStore.selectedFileTypes" multiple>
<ListboxLabel class="ml-1 block text-sm text-gray-500 dark:text-gray-400">Selected file types</ListboxLabel>

<div class="relative mt-1">
<ListboxButton id="hosts-toggle-button" class="cursor-pointer relative text-gray-800 dark:text-gray-200 w-full cursor-default rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 py-2 pl-4 pr-10 text-left hover:border-brand-600 hover:dark:border-brand-800 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500 text-sm">
<span class="block truncate">{{ fileStore.selectedFileTypesString }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>

<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
<ListboxOptions class="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md shadow-md bg-white dark:bg-gray-800 py-1 border border-gray-200 dark:border-gray-700 ring-1 ring-brand ring-opacity-5 focus:outline-none text-sm">
<ListboxOption as="template" v-for="fileType in fileStore.fileTypesAvailable" :key="fileType.identifier" :value="fileType.identifier" v-slot="{ active, selected }">
<li :class="[active ? 'text-white bg-brand-600' : 'text-gray-900 dark:text-gray-300', 'relative cursor-default select-none py-2 pl-3 pr-9']">
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">{{ fileType.name }}</span>

<span v-if="selected" :class="[active ? 'text-white' : 'text-brand-600', 'absolute inset-y-0 right-0 flex items-center pr-4']">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</template>

<script setup>
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/vue'
import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/20/solid'
import { useRouter } from 'vue-router';
import { useFileStore } from '../stores/files.js';

const router = useRouter();
const fileStore = useFileStore();
</script>
2 changes: 1 addition & 1 deletion resources/js/components/LevelButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const logViewerStore = useLogViewerStore();
const severityStore = useSeverityStore();

watch(
() => severityStore.selectedLevels,
() => severityStore.excludedLevels,
() => logViewerStore.loadLogs()
);
</script>
Loading
Loading