mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 06:28:42 +01:00
feat!: moves search to backend for better experience. It does remove show search results in context. (#3245)
This commit is contained in:
2
assets/auto-imports.d.ts
vendored
2
assets/auto-imports.d.ts
vendored
@@ -614,7 +614,6 @@ declare module 'vue' {
|
|||||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||||
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
|
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
|
||||||
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
|
|
||||||
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
||||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||||
@@ -971,7 +970,6 @@ declare module '@vue/runtime-core' {
|
|||||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||||
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
|
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
|
||||||
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
|
|
||||||
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
|
||||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||||
|
|||||||
5
assets/components.d.ts
vendored
5
assets/components.d.ts
vendored
@@ -14,7 +14,6 @@ declare module 'vue' {
|
|||||||
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
||||||
'Carbon:play': typeof import('~icons/carbon/play')['default']
|
'Carbon:play': typeof import('~icons/carbon/play')['default']
|
||||||
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
|
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
|
||||||
'Carbon:searchLocate': typeof import('~icons/carbon/search-locate')['default']
|
|
||||||
'Carbon:star': typeof import('~icons/carbon/star')['default']
|
'Carbon:star': typeof import('~icons/carbon/star')['default']
|
||||||
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
|
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
|
||||||
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
|
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
|
||||||
@@ -37,7 +36,6 @@ declare module 'vue' {
|
|||||||
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
|
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
|
||||||
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
|
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
|
||||||
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
|
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
|
||||||
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
|
|
||||||
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
||||||
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
|
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
|
||||||
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
|
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
|
||||||
@@ -55,8 +53,6 @@ declare module 'vue' {
|
|||||||
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
|
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
|
||||||
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
|
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
|
||||||
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
||||||
'MaterialSymbols:collapseAllRounded': typeof import('~icons/material-symbols/collapse-all-rounded')['default']
|
|
||||||
'MaterialSymbols:expandAllRounded': typeof import('~icons/material-symbols/expand-all-rounded')['default']
|
|
||||||
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
|
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
|
||||||
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
||||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||||
@@ -70,7 +66,6 @@ declare module 'vue' {
|
|||||||
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
|
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
|
||||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||||
'Mdi:mdi:close': typeof import('~icons/mdi/mdi')['default']
|
|
||||||
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
|
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
|
||||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||||
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<li v-for="(value, name) in validValues" :key="name">
|
<li v-for="(value, name) in validValues" :key="name">
|
||||||
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null"><null></span>
|
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null"><null></span>
|
||||||
<template v-else-if="Array.isArray(value)">
|
<template v-else-if="Array.isArray(value)">
|
||||||
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
|
<span class="font-bold" v-html="JSON.stringify(value)"> </span>
|
||||||
</template>
|
</template>
|
||||||
<span class="font-bold" v-html="markSearch(value)" v-else></span>
|
<span class="font-bold" v-html="value" v-else></span>
|
||||||
</li>
|
</li>
|
||||||
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
|
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -34,8 +34,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { type ComplexLogEntry } from "@/models/LogEntry";
|
import { type ComplexLogEntry } from "@/models/LogEntry";
|
||||||
|
|
||||||
const { markSearch } = useSearchFilter();
|
|
||||||
|
|
||||||
const { logEntry, showContainerName = false } = defineProps<{
|
const { logEntry, showContainerName = false } = defineProps<{
|
||||||
logEntry: ComplexLogEntry;
|
logEntry: ComplexLogEntry;
|
||||||
showContainerName?: boolean;
|
showContainerName?: boolean;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("<ContainerEventSource />", () => {
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
settings.value.hourStyle = hourStyle;
|
settings.value.hourStyle = hourStyle;
|
||||||
search.searchFilter.value = searchFilter;
|
search.searchQueryFilter.value = searchFilter;
|
||||||
if (searchFilter) {
|
if (searchFilter) {
|
||||||
search.showSearch.value = true;
|
search.showSearch.value = true;
|
||||||
}
|
}
|
||||||
@@ -146,24 +146,11 @@ describe("<ContainerEventSource />", () => {
|
|||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render messages with html entities", async () => {
|
|
||||||
const wrapper = createLogEventSource();
|
|
||||||
sources[sourceUrl].emitOpen();
|
|
||||||
sources[sourceUrl].emitMessage({
|
|
||||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.runAllTimers();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render dates with 12 hour style", async () => {
|
test("should render dates with 12 hour style", async () => {
|
||||||
const wrapper = createLogEventSource({ hourStyle: "12" });
|
const wrapper = createLogEventSource({ hourStyle: "12" });
|
||||||
sources[sourceUrl].emitOpen();
|
sources[sourceUrl].emitOpen();
|
||||||
sources[sourceUrl].emitMessage({
|
sources[sourceUrl].emitMessage({
|
||||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
@@ -175,25 +162,9 @@ describe("<ContainerEventSource />", () => {
|
|||||||
test("should render dates with 24 hour style", async () => {
|
test("should render dates with 24 hour style", async () => {
|
||||||
const wrapper = createLogEventSource({ hourStyle: "24" });
|
const wrapper = createLogEventSource({ hourStyle: "24" });
|
||||||
sources[sourceUrl].emitOpen();
|
sources[sourceUrl].emitOpen();
|
||||||
sources[sourceUrl].emitMessage({
|
|
||||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.runAllTimers();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render messages with filter", async () => {
|
|
||||||
const wrapper = createLogEventSource({ searchFilter: "test" });
|
|
||||||
sources[sourceUrl].emitOpen();
|
|
||||||
sources[sourceUrl].emitMessage({
|
sources[sourceUrl].emitMessage({
|
||||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||||
});
|
});
|
||||||
sources[sourceUrl].emitMessage({
|
|
||||||
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 50" />
|
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
|
||||||
<slot :messages="messages"></slot>
|
<slot :messages="messages"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:data-key="item.id"
|
:data-key="item.id"
|
||||||
:data-time="item.date.getTime()"
|
:data-time="item.date.getTime()"
|
||||||
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
|
|
||||||
class="group/entry"
|
class="group/entry"
|
||||||
>
|
>
|
||||||
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
|
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
|
||||||
@@ -25,7 +24,6 @@ const { loading, progress, currentDate } = useScrollContext();
|
|||||||
|
|
||||||
const { messages } = defineProps<{
|
const { messages } = defineProps<{
|
||||||
messages: LogEntry<string | JSONObject>[];
|
messages: LogEntry<string | JSONObject>[];
|
||||||
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
|
|
||||||
showContainerName: boolean;
|
showContainerName: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -12,26 +12,13 @@
|
|||||||
<carbon:copy-file />
|
<carbon:copy-file />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
|
|
||||||
:title="t('log_actions.jump_to_context')"
|
|
||||||
v-if="isSearching()"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
|
|
||||||
@click.prevent="handleJumpLineSelected($event, logEntry)"
|
|
||||||
:href="`#${logEntry.id}`"
|
|
||||||
>
|
|
||||||
<carbon:search-locate />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { LogEntry, JSONObject } from "@/models/LogEntry";
|
import { LogEntry, JSONObject } from "@/models/LogEntry";
|
||||||
|
|
||||||
const { message, logEntry } = defineProps<{
|
const { message } = defineProps<{
|
||||||
message: () => string;
|
message: () => string;
|
||||||
logEntry: LogEntry<string | JSONObject>;
|
logEntry: LogEntry<string | JSONObject>;
|
||||||
}>();
|
}>();
|
||||||
@@ -40,9 +27,6 @@ const { showToast } = useToast();
|
|||||||
const { copy, isSupported, copied } = useClipboard();
|
const { copy, isSupported, copied } = useClipboard();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { isSearching } = useSearchFilter();
|
|
||||||
const { handleJumpLineSelected } = useLogSearchContext();
|
|
||||||
|
|
||||||
async function copyLogMessageToClipBoard() {
|
async function copyLogMessageToClipBoard() {
|
||||||
await copy(message());
|
await copy(message());
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
<SideDrawer ref="drawer">
|
<SideDrawer ref="drawer">
|
||||||
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
|
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
|
||||||
</SideDrawer>
|
</SideDrawer>
|
||||||
<LogList :messages="filtered" :last-selected-item="lastSelectedItem" :show-container-name="showContainerName" />
|
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRouteHash } from "@vueuse/router";
|
|
||||||
import SideDrawer from "@/components/common/SideDrawer.vue";
|
import SideDrawer from "@/components/common/SideDrawer.vue";
|
||||||
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
|
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||||
|
|
||||||
@@ -19,27 +18,35 @@ const props = defineProps<{
|
|||||||
const { messages, visibleKeys } = toRefs(props);
|
const { messages, visibleKeys } = toRefs(props);
|
||||||
|
|
||||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||||
const { filteredMessages } = useSearchFilter();
|
const { debouncedSearchFilter } = useSearchFilter();
|
||||||
|
const { streamConfig } = useLoggingContext();
|
||||||
|
|
||||||
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
|
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
|
||||||
|
|
||||||
const { entry } = provideLogDetails(drawer);
|
const { entry } = provideLogDetails(drawer);
|
||||||
|
|
||||||
const visible = filteredPayload(messages);
|
const visibleMessages = filteredPayload(messages);
|
||||||
const filtered = filteredMessages(visible);
|
|
||||||
|
|
||||||
const { lastSelectedItem } = useLogSearchContext() as {
|
const router = useRouter();
|
||||||
lastSelectedItem: Ref<LogEntry<string | JSONObject> | undefined>;
|
|
||||||
};
|
watchEffect(() => {
|
||||||
const routeHash = useRouteHash();
|
const query = {} as Record<string, string>;
|
||||||
watch(
|
if (debouncedSearchFilter.value !== "") {
|
||||||
routeHash,
|
query.search = debouncedSearchFilter.value;
|
||||||
(hash) => {
|
}
|
||||||
if (hash) {
|
|
||||||
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
|
if (!streamConfig.value.stderr) {
|
||||||
}
|
query.stderr = streamConfig.value.stderr.toString();
|
||||||
},
|
}
|
||||||
{ immediate: true, flush: "post" },
|
|
||||||
);
|
if (!streamConfig.value.stdout) {
|
||||||
|
query.stdout = streamConfig.value.stdout.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
query,
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="postcss"></style>
|
<style scoped lang="postcss"></style>
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ const { showContainerName = false } = defineProps<{
|
|||||||
showContainerName?: boolean;
|
showContainerName?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { markSearch } = useSearchFilter();
|
const colorize = (value: string) => ansiConvertor.toHtml(value);
|
||||||
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
|
|
||||||
const urlPattern = /(https?:\/\/[^\s]+)/g;
|
const urlPattern = /(https?:\/\/[^\s]+)/g;
|
||||||
const linkify = (text: string) =>
|
const linkify = (text: string) =>
|
||||||
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
||||||
</div>
|
</div>
|
||||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
||||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><test>foo bar</test></div>
|
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -30,10 +29,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
|
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
|
||||||
</div>
|
</div>
|
||||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
||||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><test>foo bar</test></div>
|
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -53,49 +51,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
|
|||||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
|
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<ContainerEventSource /> > render html correctly > should render messages with filter 1`] = `
|
|
||||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
|
||||||
<li data-v-cf9ff940="" data-key="2" data-time="1560336942459" class="group/entry">
|
|
||||||
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
|
|
||||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
|
||||||
</div>
|
|
||||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
|
||||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><mark>test</mark> bar</div>
|
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
|
||||||
<!--v-if-->
|
|
||||||
<div class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer" title="log_actions.jump_to_context"><a class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700" href="#2"><svg viewBox="0 0 32 32" width="1.2em" height="1.2em">
|
|
||||||
<path fill="currentColor" d="m30 28.586l-4.688-4.688a8.028 8.028 0 1 0-1.415 1.414L28.586 30zM19 25a6 6 0 1 1 6-6a6.007 6.007 0 0 1-6 6M2 12h8v2H2zM2 2h16v2H2zm0 5h16v2H2z"></path>
|
|
||||||
</svg></a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<ContainerEventSource /> > render html correctly > should render messages with html entities 1`] = `
|
|
||||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
|
||||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
|
||||||
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
|
|
||||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
|
||||||
</div>
|
|
||||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
|
||||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><test>foo bar</test></div>
|
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -7,14 +7,17 @@
|
|||||||
ref="container"
|
ref="container"
|
||||||
:style="style"
|
:style="style"
|
||||||
>
|
>
|
||||||
<div class="input input-primary flex h-auto items-center !shadow-lg">
|
<div
|
||||||
|
class="input input-primary flex h-auto items-center !shadow-lg"
|
||||||
|
:class="!isValidQuery ? 'input-warning' : ''"
|
||||||
|
>
|
||||||
<mdi:magnify />
|
<mdi:magnify />
|
||||||
<input
|
<input
|
||||||
class="input input-ghost w-72 flex-1"
|
class="input input-ghost w-72 flex-1"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Find / RegEx"
|
placeholder="Find / RegEx"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="searchFilter"
|
v-model="searchQueryFilter"
|
||||||
@keyup.esc="resetSearch()"
|
@keyup.esc="resetSearch()"
|
||||||
/>
|
/>
|
||||||
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
|
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
|
||||||
@@ -26,7 +29,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const input = ref<HTMLInputElement>();
|
const input = ref<HTMLInputElement>();
|
||||||
const container = ref<HTMLDivElement>();
|
const container = ref<HTMLDivElement>();
|
||||||
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
|
const { searchQueryFilter, showSearch, resetSearch, isValidQuery } = useSearchFilter();
|
||||||
|
|
||||||
const { style } = useDraggable(container);
|
const { style } = useDraggable(container);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ShallowRef, type Ref } from "vue";
|
import { ShallowRef, type Ref } from "vue";
|
||||||
import { encodeXML } from "entities";
|
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
import {
|
import {
|
||||||
type LogEvent,
|
type LogEvent,
|
||||||
@@ -12,78 +12,39 @@ import {
|
|||||||
import { Service, Stack } from "@/models/Stack";
|
import { Service, Stack } from "@/models/Stack";
|
||||||
import { Container, GroupedContainers } from "@/models/Container";
|
import { Container, GroupedContainers } from "@/models/Container";
|
||||||
|
|
||||||
|
const { isSearching, debouncedSearchFilter } = useSearchFilter();
|
||||||
|
|
||||||
function parseMessage(data: string): LogEntry<string | JSONObject> {
|
function parseMessage(data: string): LogEntry<string | JSONObject> {
|
||||||
const e = JSON.parse(data, (key, value) => {
|
const e = JSON.parse(data) as LogEvent;
|
||||||
if (typeof value === "string") {
|
|
||||||
return encodeXML(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}) as LogEvent;
|
|
||||||
return asLogEntry(e);
|
return asLogEntry(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useContainerStream(container: Ref<Container>): LogStreamSource {
|
export function useContainerStream(container: Ref<Container>): LogStreamSource {
|
||||||
const { streamConfig } = useLoggingContext();
|
const url = computed(() =>
|
||||||
|
withBase(`/api/hosts/${container.value.host}/containers/${container.value.id}/logs/stream`),
|
||||||
|
);
|
||||||
|
|
||||||
const url = computed(() => {
|
const loadMoreUrl = computed(() =>
|
||||||
const params = Object.entries(toValue(streamConfig))
|
withBase(`/api/hosts/${container.value.host}/containers/${container.value.id}/logs`),
|
||||||
.filter(([, value]) => value)
|
);
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
|
|
||||||
return withBase(
|
|
||||||
`/api/hosts/${container.value.host}/containers/${container.value.id}/logs/stream?${new URLSearchParams(params).toString()}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadMoreUrl = computed(() => {
|
|
||||||
const params = Object.entries(toValue(streamConfig))
|
|
||||||
.filter(([, value]) => value)
|
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
|
|
||||||
return withBase(
|
|
||||||
`/api/hosts/${container.value.host}/containers/${container.value.id}/logs?${new URLSearchParams(params).toString()}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return useLogStream(url, loadMoreUrl);
|
return useLogStream(url, loadMoreUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
|
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
|
||||||
const { streamConfig } = useLoggingContext();
|
const url = computed(() => withBase(`/api/stacks/${stack.value.name}/logs/stream`));
|
||||||
|
|
||||||
const url = computed(() => {
|
|
||||||
const params = Object.entries(toValue(streamConfig))
|
|
||||||
.filter(([, value]) => value)
|
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
|
|
||||||
return withBase(`/api/stacks/${stack.value.name}/logs/stream?${new URLSearchParams(params).toString()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return useLogStream(url);
|
return useLogStream(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupedStream(group: Ref<GroupedContainers>): LogStreamSource {
|
export function useGroupedStream(group: Ref<GroupedContainers>): LogStreamSource {
|
||||||
const { streamConfig } = useLoggingContext();
|
const url = computed(() => withBase(`/api/groups/${group.value.name}/logs/stream`));
|
||||||
|
|
||||||
const url = computed(() => {
|
|
||||||
const params = Object.entries(toValue(streamConfig))
|
|
||||||
.filter(([, value]) => value)
|
|
||||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
|
|
||||||
return withBase(`/api/groups/${group.value.name}/logs/stream?${new URLSearchParams(params).toString()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return useLogStream(url);
|
return useLogStream(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMergedStream(containers: Ref<Container[]>): LogStreamSource {
|
export function useMergedStream(containers: Ref<Container[]>): LogStreamSource {
|
||||||
const { streamConfig } = useLoggingContext();
|
|
||||||
|
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
const params = [
|
const ids = containers.value.map((c) => ["id", c.id]).join(",");
|
||||||
...Object.entries(toValue(streamConfig)).map(([key, value]) => [key, value ? "1" : "0"]),
|
return withBase(`/api/hosts/${containers.value[0].host}/logs/mergedStream/${ids}`);
|
||||||
...containers.value.map((c) => ["id", c.id]),
|
|
||||||
];
|
|
||||||
|
|
||||||
return withBase(
|
|
||||||
`/api/hosts/${containers.value[0].host}/logs/mergedStream?${new URLSearchParams(params).toString()}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return useLogStream(url);
|
return useLogStream(url);
|
||||||
@@ -135,11 +96,16 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
|
|||||||
buffer.value = [];
|
buffer.value = [];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let empty = false;
|
||||||
if (messages.value.length == 0) {
|
if (messages.value.length == 0) {
|
||||||
// sort the buffer the very first time because of multiple logs in parallel
|
// sort the buffer the very first time because of multiple logs in parallel
|
||||||
buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime());
|
buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||||
|
empty = true;
|
||||||
}
|
}
|
||||||
messages.value = [...messages.value, ...buffer.value];
|
messages.value = [...messages.value, ...buffer.value];
|
||||||
|
if (isSearching && messages.value.length < 90 && empty) {
|
||||||
|
loadOlderLogs();
|
||||||
|
}
|
||||||
buffer.value = [];
|
buffer.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,15 +125,26 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
|
|||||||
buffer.value = [];
|
buffer.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect({ clear } = { clear: true }) {
|
const { streamConfig } = useLoggingContext();
|
||||||
close();
|
|
||||||
|
|
||||||
if (clear) {
|
const params = computed(() => {
|
||||||
clearMessages();
|
const params = Object.entries(toValue(streamConfig))
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {} as Record<string, string>);
|
||||||
|
|
||||||
|
if (isSearching.value) {
|
||||||
|
params["filter"] = debouncedSearchFilter.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
es = new EventSource(url.value);
|
return params;
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlWithParams = computed(() => withBase(`${url.value}?${new URLSearchParams(params.value).toString()}`));
|
||||||
|
|
||||||
|
function connect({ clear } = { clear: true }) {
|
||||||
|
close();
|
||||||
|
if (clear) clearMessages();
|
||||||
|
es = new EventSource(urlWithParams.value);
|
||||||
es.addEventListener("container-event", (e) => {
|
es.addEventListener("container-event", (e) => {
|
||||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
|
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
|
||||||
const containerEvent = new ContainerEventLogEntry(
|
const containerEvent = new ContainerEventLogEntry(
|
||||||
@@ -190,7 +167,7 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
|
|||||||
es.onerror = () => clearMessages();
|
es.onerror = () => clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(url, () => connect(), { immediate: true });
|
watch(urlWithParams, () => connect(), { immediate: true });
|
||||||
|
|
||||||
let fetchingInProgress = false;
|
let fetchingInProgress = false;
|
||||||
|
|
||||||
@@ -208,11 +185,9 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
|
|||||||
fetchingInProgress = true;
|
fetchingInProgress = true;
|
||||||
try {
|
try {
|
||||||
const stopWatcher = watchOnce(url, () => abortController.abort("stream changed"));
|
const stopWatcher = watchOnce(url, () => abortController.abort("stream changed"));
|
||||||
|
const moreParams = { ...params.value, from: from.toISOString(), to: to.toISOString(), fill: "1" };
|
||||||
const logs = await (
|
const logs = await (
|
||||||
await fetch(
|
await fetch(`${loadMoreUrl.value}?${new URLSearchParams(moreParams).toString()}`, { signal })
|
||||||
`${loadMoreUrl.value}&${new URLSearchParams({ from: from.toISOString(), to: to.toISOString(), fill: "1" }).toString()}`,
|
|
||||||
{ signal },
|
|
||||||
)
|
|
||||||
).text();
|
).text();
|
||||||
stopWatcher();
|
stopWatcher();
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ type LogContext = {
|
|||||||
|
|
||||||
// export for testing
|
// export for testing
|
||||||
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
|
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const stdout = searchParams.has("stdout") ? searchParams.get("stdout") === "true" : true;
|
||||||
|
const stderr = searchParams.has("stderr") ? searchParams.get("stderr") === "true" : true;
|
||||||
|
|
||||||
export const provideLoggingContext = (containers: Ref<Container[]>) => {
|
export const provideLoggingContext = (containers: Ref<Container[]>) => {
|
||||||
provide(
|
provide(
|
||||||
loggingContextKey,
|
loggingContextKey,
|
||||||
reactive({
|
reactive({
|
||||||
streamConfig: { stdout: true, stderr: true },
|
streamConfig: { stdout, stderr },
|
||||||
containers,
|
containers,
|
||||||
loadingMore: false,
|
loadingMore: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { JSONObject, LogEntry } from "@/models/LogEntry";
|
|
||||||
|
|
||||||
const lastSelectedItem = ref<LogEntry<string | JSONObject> | undefined>(undefined);
|
|
||||||
export const useLogSearchContext = () => {
|
|
||||||
const { resetSearch } = useSearchFilter();
|
|
||||||
|
|
||||||
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
|
|
||||||
lastSelectedItem.value = item;
|
|
||||||
resetSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lastSelectedItem,
|
|
||||||
handleJumpLineSelected,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,86 +1,34 @@
|
|||||||
import { type Ref } from "vue";
|
const searchQueryFilter = ref<string>("");
|
||||||
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
|
const debouncedSearchFilter = refDebounced(searchQueryFilter);
|
||||||
import { encodeXML } from "entities";
|
|
||||||
|
|
||||||
const searchFilter = ref<string>("");
|
|
||||||
const debouncedSearchFilter = useDebounce(searchFilter);
|
|
||||||
const showSearch = ref(false);
|
const showSearch = ref(false);
|
||||||
|
|
||||||
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
for (const key in record) {
|
if (searchParams.get("search") !== null && searchParams.get("search") !== "") {
|
||||||
const value = record[key];
|
searchQueryFilter.value = searchParams.get("search") || "";
|
||||||
if (typeof value === "string" && regex.test(value)) {
|
showSearch.value = true;
|
||||||
return true;
|
}
|
||||||
}
|
function resetSearch() {
|
||||||
if (isObject(value) && regex.test(JSON.stringify(value))) {
|
searchQueryFilter.value = "";
|
||||||
return true;
|
showSearch.value = false;
|
||||||
}
|
|
||||||
if (Array.isArray(value) && matchRecord(value, regex)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSearching = computed(() => showSearch.value && debouncedSearchFilter.value !== "");
|
||||||
|
|
||||||
|
const isValidQuery = computed(() => {
|
||||||
|
try {
|
||||||
|
new RegExp(searchQueryFilter.value);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function useSearchFilter() {
|
export function useSearchFilter() {
|
||||||
const regex = $computed(() => {
|
|
||||||
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
|
|
||||||
return new RegExp(encodeXML(debouncedSearchFilter.value), isSmartCase ? "i" : "");
|
|
||||||
});
|
|
||||||
|
|
||||||
function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
|
|
||||||
return computed(() => {
|
|
||||||
if (debouncedSearchFilter.value && showSearch.value) {
|
|
||||||
try {
|
|
||||||
return messages.value.filter((d) => {
|
|
||||||
if (d instanceof SimpleLogEntry) {
|
|
||||||
return regex.test(d.message);
|
|
||||||
} else if (d instanceof ComplexLogEntry) {
|
|
||||||
return matchRecord(d.message, regex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof SyntaxError) {
|
|
||||||
console.info(`Ignoring SyntaxError from search.`, e);
|
|
||||||
return messages.value;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function markSearch(log: { toString(): string }): string;
|
|
||||||
function markSearch(log: string[]): string[];
|
|
||||||
function markSearch(log: { toString(): string } | string[]) {
|
|
||||||
if (!debouncedSearchFilter.value) {
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
if (Array.isArray(log)) {
|
|
||||||
return log.map((d) => markSearch(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalRegex = new RegExp(regex.source, regex.flags + "g");
|
|
||||||
return log.toString().replaceAll(globalRegex, (match) => `<mark>${match}</mark>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetSearch() {
|
|
||||||
searchFilter.value = "";
|
|
||||||
showSearch.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSearching() {
|
|
||||||
return showSearch.value && searchFilter.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filteredMessages,
|
searchQueryFilter,
|
||||||
searchFilter,
|
isValidQuery,
|
||||||
|
debouncedSearchFilter,
|
||||||
showSearch,
|
showSearch,
|
||||||
markSearch,
|
|
||||||
resetSearch,
|
resetSearch,
|
||||||
isSearching,
|
isSearching,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
|
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
|
export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
|
||||||
|
const { isSearching } = useSearchFilter();
|
||||||
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
|
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
return messages.value.map((d) => {
|
return messages.value
|
||||||
if (d instanceof ComplexLogEntry) {
|
.map((d) => {
|
||||||
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
|
if (d instanceof ComplexLogEntry) {
|
||||||
} else {
|
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
|
||||||
return d;
|
} else {
|
||||||
}
|
return d;
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.filter((d) => {
|
||||||
|
if (isSearching.value && d instanceof ComplexLogEntry) {
|
||||||
|
return Object.values(d.message).some((v) => JSON.stringify(v).includes("<mark>"));
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class SimpleLogEntry extends LogEntry<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ComplexLogEntry extends LogEntry<JSONObject> {
|
export class ComplexLogEntry extends LogEntry<JSONObject> {
|
||||||
private readonly filteredMessage: ComputedRef<JSONObject>;
|
private readonly filteredMessage: ComputedRef<Record<string, any>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: JSONObject,
|
message: JSONObject,
|
||||||
@@ -89,7 +89,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
|
|||||||
return ComplexLogItem;
|
return ComplexLogItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get message(): JSONObject {
|
public get message(): Record<string, any> {
|
||||||
return unref(this.filteredMessage);
|
return unref(this.filteredMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -29,8 +29,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
|
|
||||||
github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||||
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
@@ -223,12 +221,10 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
|
||||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
|
||||||
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
|
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
|
||||||
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package analytics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
|
||||||
|
|||||||
51
internal/docker/escape.go
Normal file
51
internal/docker/escape.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func escape(logEvent *LogEvent) {
|
||||||
|
switch value := logEvent.Message.(type) {
|
||||||
|
case string:
|
||||||
|
logEvent.Message = html.EscapeString(value)
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
escapeAnyMap(value)
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
escapeStringMap(value)
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
panic("not implemented")
|
||||||
|
|
||||||
|
case map[string]string:
|
||||||
|
panic("not implemented")
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
|
||||||
|
|
||||||
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
switch value := pair.Value.(type) {
|
||||||
|
case string:
|
||||||
|
orderedMap.Set(pair.Key, html.EscapeString(value))
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
escapeAnyMap(value)
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
escapeStringMap(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeStringMap(orderedMap *orderedmap.OrderedMap[string, string]) {
|
||||||
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
orderedMap.Set(pair.Key, html.EscapeString(pair.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ func (g *EventGenerator) consumeReader() {
|
|||||||
logEvent := createEvent(message, streamType)
|
logEvent := createEvent(message, streamType)
|
||||||
logEvent.ContainerID = g.containerID
|
logEvent.ContainerID = g.containerID
|
||||||
logEvent.Level = guessLogLevel(logEvent)
|
logEvent.Level = guessLogLevel(logEvent)
|
||||||
|
escape(logEvent)
|
||||||
g.buffer <- logEvent
|
g.buffer <- logEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,6 @@ func createEvent(message string, streamType StdType) *LogEvent {
|
|||||||
logEvent.Message = data
|
logEvent.Message = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if data, err := ParseLogFmt(message); err == nil {
|
} else if data, err := ParseLogFmt(message); err == nil {
|
||||||
logEvent.Message = data
|
logEvent.Message = data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,10 @@ func guessLogLevel(logEvent *LogEvent) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
if level, ok := value["level"].(string); ok {
|
panic("not implemented")
|
||||||
return strings.ToLower(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
case map[string]string:
|
case map[string]string:
|
||||||
if level, ok := value["level"]; ok {
|
panic("not implemented")
|
||||||
return strings.ToLower(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/goccy/go-json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
@@ -29,10 +29,6 @@ func TestGuessLogLevel(t *testing.T) {
|
|||||||
{"[foo] [ ERROR] Something went wrong", "error"},
|
{"[foo] [ ERROR] Something went wrong", "error"},
|
||||||
{"123 ERROR Something went wrong", "error"},
|
{"123 ERROR Something went wrong", "error"},
|
||||||
{"123 Something went wrong", ""},
|
{"123 Something went wrong", ""},
|
||||||
{map[string]interface{}{"level": "info"}, "info"},
|
|
||||||
{map[string]interface{}{"level": "INFO"}, "info"},
|
|
||||||
{map[string]string{"level": "info"}, "info"},
|
|
||||||
{map[string]string{"level": "INFO"}, "info"},
|
|
||||||
{orderedmap.New[string, string](
|
{orderedmap.New[string, string](
|
||||||
orderedmap.WithInitialData(
|
orderedmap.WithInitialData(
|
||||||
orderedmap.Pair[string, string]{Key: "key", Value: "value"},
|
orderedmap.Pair[string, string]{Key: "key", Value: "value"},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/goccy/go-json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package profile
|
package profile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package releases
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"github.com/goccy/go-json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
140
internal/support/search/search.go
Normal file
140
internal/support/search/search.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseRegex(search string) (*regexp.Regexp, error) {
|
||||||
|
flags := ""
|
||||||
|
|
||||||
|
if search == strings.ToLower(search) {
|
||||||
|
flags = "(?i)"
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(flags + search)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Str("search", search).Msg("failed to compile regex")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return re, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Search(re *regexp.Regexp, logEvent *docker.LogEvent) bool {
|
||||||
|
switch value := logEvent.Message.(type) {
|
||||||
|
case string:
|
||||||
|
if re.MatchString(value) {
|
||||||
|
logEvent.Message = re.ReplaceAllString(value, "<mark>$0</mark>")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
return searchMapAny(re, value)
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
return searchMapString(re, value)
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
panic("not implemented")
|
||||||
|
case map[string]string:
|
||||||
|
panic("not implemented")
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchMapAny(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, any]) bool {
|
||||||
|
found := false
|
||||||
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
switch value := pair.Value.(type) {
|
||||||
|
case string:
|
||||||
|
if re.MatchString(value) {
|
||||||
|
found = true
|
||||||
|
orderedMap.Set(pair.Key, re.ReplaceAllString(value, "<mark>$0</mark>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
for i, v := range value {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case string:
|
||||||
|
if re.MatchString(v) {
|
||||||
|
found = true
|
||||||
|
value[i] = re.ReplaceAllString(v, "<mark>$0</mark>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
if searchMapAny(re, value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
if searchMapString(re, value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
if searchMap(re, value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type inside searchMapAny")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchMap(re *regexp.Regexp, data map[string]interface{}) bool {
|
||||||
|
found := false
|
||||||
|
for key, value := range data {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if re.MatchString(value) {
|
||||||
|
data[key] = re.ReplaceAllString(value, "<mark>$0</mark>")
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
for i, v := range value {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case string:
|
||||||
|
if re.MatchString(v) {
|
||||||
|
found = true
|
||||||
|
value[i] = re.ReplaceAllString(v, "<mark>$0</mark>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
if searchMap(re, value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type inside searchMap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchMapString(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, string]) bool {
|
||||||
|
found := false
|
||||||
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
if re.MatchString(pair.Value) {
|
||||||
|
orderedMap.Set(pair.Key, re.ReplaceAllString(pair.Value, "<mark>$0</mark>"))
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ func Test_createRoutes_index(t *testing.T) {
|
|||||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func Test_createRoutes_redirect(t *testing.T) {
|
func Test_createRoutes_redirect(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
|
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
@@ -16,6 +17,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
|
"github.com/amir20/dozzle/internal/support/search"
|
||||||
"github.com/amir20/dozzle/internal/utils"
|
"github.com/amir20/dozzle/internal/utils"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -106,6 +108,15 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
|||||||
buffer := utils.NewRingBuffer[*docker.LogEvent](500)
|
buffer := utils.NewRingBuffer[*docker.LogEvent](500)
|
||||||
delta := to.Sub(from)
|
delta := to.Sub(from)
|
||||||
|
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
if r.URL.Query().Has("filter") {
|
||||||
|
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if buffer.Len() > 0 {
|
if buffer.Len() > 0 {
|
||||||
break
|
break
|
||||||
@@ -118,7 +129,13 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for event := range events {
|
for event := range events {
|
||||||
buffer.Push(event)
|
if regex != nil {
|
||||||
|
if search.Search(regex, event) {
|
||||||
|
buffer.Push(event)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.Push(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.URL.Query().Has("fill") { // only auto fill if fill query parameter is set
|
if !r.URL.Query().Has("fill") { // only auto fill if fill query parameter is set
|
||||||
@@ -249,17 +266,31 @@ func streamLogsForContainers(w http.ResponseWriter, r *http.Request, multiHostCl
|
|||||||
newContainers := make(chan docker.Container)
|
newContainers := make(chan docker.Container)
|
||||||
multiHostClient.SubscribeContainersStarted(r.Context(), newContainers, filter)
|
multiHostClient.SubscribeContainersStarted(r.Context(), newContainers, filter)
|
||||||
|
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
if r.URL.Query().Has("filter") {
|
||||||
|
var err error
|
||||||
|
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-logs:
|
case logEvent := <-logs:
|
||||||
if buf, err := json.Marshal(event); err != nil {
|
if regex != nil {
|
||||||
|
if !search.Search(regex, logEvent) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if buf, err := json.Marshal(logEvent); err != nil {
|
||||||
log.Error().Err(err).Msg("error encoding log event")
|
log.Error().Err(err).Msg("error encoding log event")
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(w, "data: %s\n", buf)
|
fmt.Fprintf(w, "data: %s\n", buf)
|
||||||
}
|
}
|
||||||
if event.Timestamp > 0 {
|
if logEvent.Timestamp > 0 {
|
||||||
fmt.Fprintf(w, "id: %d\n", event.Timestamp)
|
fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "\n")
|
fmt.Fprintf(w, "\n")
|
||||||
f.Flush()
|
f.Flush()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/goccy/go-json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user