mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
fix: fixes url pattern matcher with quotes (#3902)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<LogItem :logEntry @click="showDrawer(LogDetails, { entry: logEntry })" class="clickable">
|
<LogItem :logEntry @click="showDrawer(LogDetails, { entry: logEntry })" class="clickable">
|
||||||
<ul class="space-x-4">
|
<ul class="space-x-4" @click="preventDefaultOnLinks">
|
||||||
<li v-for="(value, name) in validValues" :key="name" class="inline-flex">
|
<li v-for="(value, name) in validValues" :key="name" class="inline-flex">
|
||||||
<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)">
|
||||||
@@ -27,6 +27,11 @@ const validValues = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showDrawer = useDrawer();
|
const showDrawer = useDrawer();
|
||||||
|
function preventDefaultOnLinks(event: MouseEvent) {
|
||||||
|
if (event.target instanceof HTMLAnchorElement && event.target.rel?.includes("external")) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ describe("<ContainerEventSource />", () => {
|
|||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render dates with 12 hour style", async () => {
|
test("should render dates with 12 hour style", async () => {
|
||||||
@@ -173,7 +173,7 @@ describe("<ContainerEventSource />", () => {
|
|||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render dates with 24 hour style", async () => {
|
test("should render dates with 24 hour style", async () => {
|
||||||
@@ -186,7 +186,7 @@ describe("<ContainerEventSource />", () => {
|
|||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="events group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
|
<ul class="group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }" data-logs>
|
||||||
<li
|
<li
|
||||||
v-for="item in messages"
|
v-for="item in messages"
|
||||||
ref="list"
|
ref="list"
|
||||||
@@ -55,7 +55,7 @@ useIntersectionObserver(
|
|||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "@/main.css";
|
@reference "@/main.css";
|
||||||
.events {
|
ul {
|
||||||
font-family:
|
font-family:
|
||||||
ui-monospace,
|
ui-monospace,
|
||||||
SFMono-Regular,
|
SFMono-Regular,
|
||||||
@@ -99,6 +99,10 @@ useIntersectionObserver(
|
|||||||
@apply bg-secondary inline-block rounded-xs;
|
@apply bg-secondary inline-block rounded-xs;
|
||||||
animation: pops 200ms ease-out;
|
animation: pops 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(a[rel~="external"]) {
|
||||||
|
@apply text-primary underline-offset-4 hover:underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pops {
|
@keyframes pops {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<LogItem :logEntry>
|
<LogItem :logEntry>
|
||||||
<div
|
<div
|
||||||
class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap"
|
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap"
|
||||||
v-html="linkify(colorize(logEntry.message))"
|
v-html="colorize(logEntry.message)"
|
||||||
></div>
|
></div>
|
||||||
<LogMessageActions
|
<LogMessageActions
|
||||||
class="absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100"
|
class="absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100"
|
||||||
@@ -28,15 +28,4 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const colorize = (value: string) => ansiConvertor.toHtml(value);
|
const colorize = (value: string) => ansiConvertor.toHtml(value);
|
||||||
const urlPattern =
|
|
||||||
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_+.~#?&/=]*/g;
|
|
||||||
const linkify = (text: string) =>
|
|
||||||
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "@/main.css";
|
|
||||||
.log-wrapper :deep(a) {
|
|
||||||
@apply text-primary underline-offset-4 hover:underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
|
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
<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 group-[.compact]:items-stretch">
|
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
@@ -13,8 +13,8 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||||
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
<div class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,9 +23,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
||||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
|
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
<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 group-[.compact]:items-stretch">
|
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
@@ -35,8 +35,8 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||||
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
<div class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,9 +45,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
|
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
|
||||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
|
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
<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 group-[.compact]:items-stretch">
|
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
@@ -57,8 +57,8 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||||
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
|
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
|
||||||
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
<div class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
package search
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/container"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MarkerStart = "\uE000"
|
|
||||||
MarkerEnd = "\uE001"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 *container.LogEvent) bool {
|
|
||||||
switch value := logEvent.Message.(type) {
|
|
||||||
case string:
|
|
||||||
if re.MatchString(value) {
|
|
||||||
logEvent.Message = re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
|
|
||||||
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 replaced, matched := searchString(re, value); matched {
|
|
||||||
found = true
|
|
||||||
orderedMap.Set(pair.Key, replaced)
|
|
||||||
}
|
|
||||||
|
|
||||||
case []any:
|
|
||||||
if searchArray(re, value) {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
case int, float64, bool:
|
|
||||||
formatted := fmt.Sprintf("%v", value)
|
|
||||||
if re.MatchString(formatted) {
|
|
||||||
orderedMap.Set(pair.Key, re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd))
|
|
||||||
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 replaced, matched := searchString(re, value); matched {
|
|
||||||
found = true
|
|
||||||
data[key] = replaced
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
if searchArray(re, value) {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case map[string]interface{}:
|
|
||||||
if searchMap(re, value) {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case int, float64, bool:
|
|
||||||
formatted := fmt.Sprintf("%v", value)
|
|
||||||
if re.MatchString(formatted) {
|
|
||||||
data[key] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
|
|
||||||
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 replaced, matched := searchString(re, pair.Value); matched {
|
|
||||||
found = true
|
|
||||||
orderedMap.Set(pair.Key, replaced)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchArray(re *regexp.Regexp, data []any) bool {
|
|
||||||
found := false
|
|
||||||
for i, value := range data {
|
|
||||||
switch value := value.(type) {
|
|
||||||
case string:
|
|
||||||
if replaced, matched := searchString(re, value); matched {
|
|
||||||
found = true
|
|
||||||
data[i] = replaced
|
|
||||||
}
|
|
||||||
case int, float64, bool:
|
|
||||||
formatted := fmt.Sprintf("%v", value)
|
|
||||||
if re.MatchString(formatted) {
|
|
||||||
data[i] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
if searchArray(re, value) {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
case map[string]interface{}:
|
|
||||||
if searchMap(re, value) {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchString(re *regexp.Regexp, value string) (string, bool) {
|
|
||||||
if re.MatchString(value) {
|
|
||||||
replaced := re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
|
|
||||||
return replaced, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, false
|
|
||||||
}
|
|
||||||
@@ -2,20 +2,25 @@ package support_web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"html"
|
"html"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/amir20/dozzle/internal/container"
|
"github.com/amir20/dozzle/internal/container"
|
||||||
"github.com/amir20/dozzle/internal/support/search"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// URL marker regex compiled once for performance
|
||||||
|
var urlMarkerRegex = regexp.MustCompile(URLMarkerStart + "(.*?)" + URLMarkerEnd)
|
||||||
|
|
||||||
func EscapeHTMLValues(logEvent *container.LogEvent) {
|
func EscapeHTMLValues(logEvent *container.LogEvent) {
|
||||||
|
// Mark URLs before HTML escaping
|
||||||
|
MarkURLs(logEvent)
|
||||||
|
|
||||||
switch value := logEvent.Message.(type) {
|
switch value := logEvent.Message.(type) {
|
||||||
case string:
|
case string:
|
||||||
value = html.EscapeString(value)
|
logEvent.Message = escapeAndProcessMarkers(value)
|
||||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
|
||||||
logEvent.Message = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
|
||||||
|
|
||||||
case *orderedmap.OrderedMap[string, any]:
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
escapeAnyMap(value)
|
escapeAnyMap(value)
|
||||||
@@ -34,28 +39,53 @@ func EscapeHTMLValues(logEvent *container.LogEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func escapeAndProcessMarkers(value string) string {
|
||||||
|
value = html.EscapeString(value)
|
||||||
|
value = strings.ReplaceAll(value, MarkerStart, "<mark>")
|
||||||
|
value = strings.ReplaceAll(value, MarkerEnd, "</mark>")
|
||||||
|
// Process URL markers
|
||||||
|
value = urlMarkerRegex.ReplaceAllString(value, "<a href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer external\">$1</a>")
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
|
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
|
||||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
switch value := pair.Value.(type) {
|
switch value := pair.Value.(type) {
|
||||||
case string:
|
case string:
|
||||||
value = html.EscapeString(value)
|
orderedMap.Set(pair.Key, escapeAndProcessMarkers(value))
|
||||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
|
||||||
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
|
||||||
orderedMap.Set(pair.Key, value)
|
|
||||||
case *orderedmap.OrderedMap[string, any]:
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
escapeAnyMap(value)
|
escapeAnyMap(value)
|
||||||
case *orderedmap.OrderedMap[string, string]:
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
escapeStringMap(value)
|
escapeStringMap(value)
|
||||||
|
case map[string]interface{}:
|
||||||
|
escapeMapStringInterface(value)
|
||||||
|
case map[string]string:
|
||||||
|
escapeStringMapString(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeStringMap(orderedMap *orderedmap.OrderedMap[string, string]) {
|
func escapeStringMap(orderedMap *orderedmap.OrderedMap[string, string]) {
|
||||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
value := html.EscapeString(pair.Value)
|
orderedMap.Set(pair.Key, escapeAndProcessMarkers(pair.Value))
|
||||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
}
|
||||||
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
}
|
||||||
orderedMap.Set(pair.Key, value)
|
|
||||||
|
func escapeMapStringInterface(value map[string]interface{}) {
|
||||||
|
for key, val := range value {
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
value[key] = escapeAndProcessMarkers(val)
|
||||||
|
case map[string]interface{}:
|
||||||
|
escapeMapStringInterface(val)
|
||||||
|
case map[string]string:
|
||||||
|
escapeStringMapString(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeStringMapString(value map[string]string) {
|
||||||
|
for key, val := range value {
|
||||||
|
value[key] = escapeAndProcessMarkers(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
198
internal/support/web/regex.go
Normal file
198
internal/support/web/regex.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package support_web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/container"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PatternMatcher defines the interface for a regex pattern matcher
|
||||||
|
type PatternMatcher struct {
|
||||||
|
Regex *regexp.Regexp
|
||||||
|
MarkerStart string
|
||||||
|
MarkerEnd string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPatternMatcher creates a new pattern matcher with the specified regex and markers
|
||||||
|
func NewPatternMatcher(re *regexp.Regexp, markerStart, markerEnd string) *PatternMatcher {
|
||||||
|
return &PatternMatcher{
|
||||||
|
Regex: re,
|
||||||
|
MarkerStart: markerStart,
|
||||||
|
MarkerEnd: markerEnd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRegex compiles a regex pattern with optional case-insensitive flag
|
||||||
|
func CreateRegex(pattern string, caseInsensitive bool) (*regexp.Regexp, error) {
|
||||||
|
flags := ""
|
||||||
|
if caseInsensitive || pattern == strings.ToLower(pattern) {
|
||||||
|
flags = "(?i)"
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(flags + pattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Str("pattern", pattern).Msg("failed to compile regex")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return re, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkInLogEvent applies the pattern matcher to the log event's message
|
||||||
|
func (pm *PatternMatcher) MarkInLogEvent(logEvent *container.LogEvent) bool {
|
||||||
|
switch value := logEvent.Message.(type) {
|
||||||
|
case string:
|
||||||
|
if pm.Regex.MatchString(value) {
|
||||||
|
logEvent.Message = pm.Regex.ReplaceAllString(value, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
return pm.markMapAny(value)
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
return pm.markMapString(value)
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
return pm.markMap(value)
|
||||||
|
|
||||||
|
case map[string]string:
|
||||||
|
panic("not implemented")
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PatternMatcher) markMapAny(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 replaced, matched := pm.markString(value); matched {
|
||||||
|
found = true
|
||||||
|
orderedMap.Set(pair.Key, replaced)
|
||||||
|
}
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
if pm.markArray(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, any]:
|
||||||
|
if pm.markMapAny(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case *orderedmap.OrderedMap[string, string]:
|
||||||
|
if pm.markMapString(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
if pm.markMap(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case int, float64, bool:
|
||||||
|
formatted := fmt.Sprintf("%v", value)
|
||||||
|
if pm.Regex.MatchString(formatted) {
|
||||||
|
orderedMap.Set(pair.Key, pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd))
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type inside markMapAny")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PatternMatcher) markMap(data map[string]interface{}) bool {
|
||||||
|
found := false
|
||||||
|
for key, value := range data {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if replaced, matched := pm.markString(value); matched {
|
||||||
|
found = true
|
||||||
|
data[key] = replaced
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
if pm.markArray(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case map[string]interface{}:
|
||||||
|
if pm.markMap(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case int, float64, bool:
|
||||||
|
formatted := fmt.Sprintf("%v", value)
|
||||||
|
if pm.Regex.MatchString(formatted) {
|
||||||
|
data[key] = pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Debug().Type("type", value).Msg("unknown logEvent type inside markMap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PatternMatcher) markMapString(orderedMap *orderedmap.OrderedMap[string, string]) bool {
|
||||||
|
found := false
|
||||||
|
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
if replaced, matched := pm.markString(pair.Value); matched {
|
||||||
|
found = true
|
||||||
|
orderedMap.Set(pair.Key, replaced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PatternMatcher) markArray(data []any) bool {
|
||||||
|
found := false
|
||||||
|
for i, value := range data {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if replaced, matched := pm.markString(value); matched {
|
||||||
|
found = true
|
||||||
|
data[i] = replaced
|
||||||
|
}
|
||||||
|
case int, float64, bool:
|
||||||
|
formatted := fmt.Sprintf("%v", value)
|
||||||
|
if pm.Regex.MatchString(formatted) {
|
||||||
|
data[i] = pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
if pm.markArray(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
case map[string]interface{}:
|
||||||
|
if pm.markMap(value) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PatternMatcher) markString(value string) (string, bool) {
|
||||||
|
if pm.Regex.MatchString(value) {
|
||||||
|
replaced := pm.Regex.ReplaceAllString(value, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||||
|
return replaced, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, false
|
||||||
|
}
|
||||||
22
internal/support/web/search.go
Normal file
22
internal/support/web/search.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package support_web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MarkerStart = "\uE000"
|
||||||
|
MarkerEnd = "\uE001"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseRegex(search string) (*regexp.Regexp, error) {
|
||||||
|
return CreateRegex(search, search == strings.ToLower(search))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
|
||||||
|
matcher := NewPatternMatcher(re, MarkerStart, MarkerEnd)
|
||||||
|
return matcher.MarkInLogEvent(logEvent)
|
||||||
|
}
|
||||||
21
internal/support/web/url.go
Normal file
21
internal/support/web/url.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package support_web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/internal/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
URLMarkerStart = "\uE002"
|
||||||
|
URLMarkerEnd = "\uE003"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Standard URL regex pattern to match http/https URLs
|
||||||
|
var urlRegex = regexp.MustCompile(`(https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))`)
|
||||||
|
|
||||||
|
// MarkURLs marks URLs in the logEvent message with special markers
|
||||||
|
func MarkURLs(logEvent *container.LogEvent) bool {
|
||||||
|
matcher := NewPatternMatcher(urlRegex, URLMarkerStart, URLMarkerEnd)
|
||||||
|
return matcher.MarkInLogEvent(logEvent)
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/amir20/dozzle/internal/auth"
|
"github.com/amir20/dozzle/internal/auth"
|
||||||
"github.com/amir20/dozzle/internal/container"
|
"github.com/amir20/dozzle/internal/container"
|
||||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||||
"github.com/amir20/dozzle/internal/support/search"
|
|
||||||
support_web "github.com/amir20/dozzle/internal/support/web"
|
support_web "github.com/amir20/dozzle/internal/support/web"
|
||||||
"github.com/amir20/dozzle/internal/utils"
|
"github.com/amir20/dozzle/internal/utils"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -68,7 +67,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
var regex *regexp.Regexp
|
var regex *regexp.Regexp
|
||||||
if r.URL.Query().Has("filter") {
|
if r.URL.Query().Has("filter") {
|
||||||
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
|
regex, err = support_web.ParseRegex(r.URL.Query().Get("filter"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -144,7 +143,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if regex != nil {
|
if regex != nil {
|
||||||
if !search.Search(regex, event) {
|
if !support_web.Search(regex, event) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +157,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
support_web.EscapeHTMLValues(event) // only escape when not exporting
|
support_web.EscapeHTMLValues(event)
|
||||||
buffer.Push(event)
|
buffer.Push(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +279,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
if r.URL.Query().Has("filter") {
|
if r.URL.Query().Has("filter") {
|
||||||
var err error
|
var err error
|
||||||
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
|
regex, err = support_web.ParseRegex(r.URL.Query().Get("filter"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -317,7 +316,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
|||||||
if _, ok := levels[log.Level]; !ok {
|
if _, ok := levels[log.Level]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if search.Search(regex, log) {
|
if support_web.Search(regex, log) {
|
||||||
events = append(events, log)
|
events = append(events, log)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,7 +383,7 @@ loop:
|
|||||||
select {
|
select {
|
||||||
case logEvent := <-liveLogs:
|
case logEvent := <-liveLogs:
|
||||||
if regex != nil {
|
if regex != nil {
|
||||||
if !search.Search(regex, logEvent) {
|
if !support_web.Search(regex, logEvent) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user