1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

fix: fixes search and urls in detail panel (#3907)

This commit is contained in:
Amir Raminfar
2025-05-21 19:59:00 -07:00
committed by GitHub
parent 804199aa9a
commit b70c2da290
9 changed files with 40 additions and 24 deletions

View File

@@ -138,7 +138,7 @@ describe("<ContainerEventSource />", () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen(); sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message."}`,
}); });
vi.runAllTimers(); vi.runAllTimers();
@@ -154,7 +154,7 @@ describe("<ContainerEventSource />", () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen(); sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message."}`,
}); });
vi.runAllTimers(); vi.runAllTimers();
@@ -167,7 +167,7 @@ describe("<ContainerEventSource />", () => {
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":"foo bar", "id":1}`, data: `{"ts":1560336942459, "m":"foo bar", "id":1, "rm": "foo bar"}`,
}); });
vi.runAllTimers(); vi.runAllTimers();

View File

@@ -29,7 +29,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
Raw JSON Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)"> <UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
<button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }"> <button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" /> <mdi:check class="swap-on" />
<mdi:content-copy class="swap-off" /> <mdi:content-copy class="swap-off" />
@@ -37,7 +37,7 @@
</UseClipboard> </UseClipboard>
</div> </div>
<div class="bg-base-200 max-h-48 overflow-scroll rounded-sm border border-white/20 p-2"> <div class="bg-base-200 max-h-48 overflow-scroll rounded-sm border border-white/20 p-2">
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre> <pre v-html="syntaxHighlight(entry.rawMessage)"></pre>
</div> </div>
</section> </section>
<table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry"> <table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry">
@@ -59,7 +59,7 @@
{{ key.join(".") }} {{ key.join(".") }}
</td> </td>
<td class="truncate max-md:hidden"> <td class="truncate max-md:hidden">
<code v-html="JSON.stringify(value)"></code> <code>{{ JSON.stringify(value) }}</code>
</td> </td>
<td> <td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" /> <input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
@@ -96,14 +96,15 @@ function toggleField(key: string[]) {
const fields = computed({ const fields = computed({
get() { get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = []; const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const allFields = flattenJSONToMap(entry.unfilteredMessage); const rawFields = JSON.parse(entry.rawMessage);
const allFields = flattenJSONToMap(rawFields);
if (visibleKeys.value.size === 0) { if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) { for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true }); fieldsWithValue.push({ key, value, enabled: true });
} }
} else { } else {
for (const [key, enabled] of visibleKeys.value) { for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(entry.unfilteredMessage, key); const value = getDeep(rawFields, key);
fieldsWithValue.push({ key, value, enabled }); fieldsWithValue.push({ key, value, enabled });
} }
@@ -141,8 +142,8 @@ const toggleAllFields = computed({
}, },
}); });
function syntaxHighlight(json: any) { function syntaxHighlight(json: string) {
json = JSON.stringify(json, null, 2); json = JSON.stringify(JSON.parse(json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")), null, 2);
return json.replace( return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g, /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
function (match: string) { function (match: string) {

View File

@@ -6,14 +6,13 @@
></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"
:message="() => decodeXML(stripAnsi(logEntry.message))" :message="() => stripAnsi(logEntry.rawMessage)"
:log-entry="logEntry" :log-entry="logEntry"
/> />
</LogItem> </LogItem>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry"; import { SimpleLogEntry } from "@/models/LogEntry";
import { decodeXML } from "entities";
import AnsiConvertor from "ansi-to-html"; import AnsiConvertor from "ansi-to-html";
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";

View File

@@ -74,6 +74,7 @@ SimpleLogEntry {
"id": 1, "id": 1,
"level": undefined, "level": undefined,
"position": undefined, "position": undefined,
"rawMessage": "This is a message.",
"std": "stderr", "std": "stderr",
} }
`; `;

View File

@@ -28,6 +28,7 @@ export interface LogEvent {
readonly p: Position; readonly p: Position;
readonly s: "stdout" | "stderr" | "unknown"; readonly s: "stdout" | "stderr" | "unknown";
readonly c: string; readonly c: string;
readonly rm: string;
} }
export abstract class LogEntry<T extends string | JSONObject> { export abstract class LogEntry<T extends string | JSONObject> {
@@ -38,6 +39,7 @@ export abstract class LogEntry<T extends string | JSONObject> {
public readonly id: number, public readonly id: number,
public readonly date: Date, public readonly date: Date,
public readonly std: Std, public readonly std: Std,
public readonly rawMessage: string,
public readonly level?: Level, public readonly level?: Level,
) { ) {
this._message = message; this._message = message;
@@ -59,8 +61,9 @@ export class SimpleLogEntry extends LogEntry<string> {
public readonly level: Level, public readonly level: Level,
public readonly position: Position, public readonly position: Position,
public readonly std: Std, public readonly std: Std,
public readonly rawMessage: string,
) { ) {
super(message, containerID, id, date, std, level); super(message, containerID, id, date, std, rawMessage, level);
} }
getComponent(): Component { getComponent(): Component {
return SimpleLogItem; return SimpleLogItem;
@@ -77,9 +80,10 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
date: Date, date: Date,
public readonly level: Level, public readonly level: Level,
public readonly std: Std, public readonly std: Std,
public readonly rawMessage: string,
visibleKeys?: Ref<Map<string[], boolean>>, visibleKeys?: Ref<Map<string[], boolean>>,
) { ) {
super(message, containerID, id, date, std, level); super(message, containerID, id, date, std, rawMessage, level);
if (visibleKeys) { if (visibleKeys) {
this.filteredMessage = computed(() => { this.filteredMessage = computed(() => {
if (visibleKeys.value.size === 0) { if (visibleKeys.value.size === 0) {
@@ -123,6 +127,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
event.date, event.date,
event.level, event.level,
event.std, event.std,
event.rawMessage,
visibleKeys, visibleKeys,
); );
} }
@@ -187,6 +192,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
new Date(event.ts), new Date(event.ts),
event.l, event.l,
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"), event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
event.rm,
); );
} else { } else {
return new SimpleLogEntry( return new SimpleLogEntry(
@@ -197,6 +203,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
event.l, event.l,
event.p, event.p,
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"), event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
event.rm,
); );
} }
} }

View File

@@ -181,8 +181,8 @@ const hoursAgo = (hours: number) => {
const fakeMessages = computedWithControl( const fakeMessages = computedWithControl(
() => i18n.global.locale.value, () => i18n.global.locale.value,
() => [ () => [
new SimpleLogEntry(t("settings.log.preview"), "123", 1, hoursAgo(16), "info", undefined, "stdout"), new SimpleLogEntry(t("settings.log.preview"), "123", 1, hoursAgo(16), "info", undefined, "stdout", ""),
new SimpleLogEntry(t("settings.log.warning"), "123", 2, hoursAgo(12), "warn", undefined, "stdout"), new SimpleLogEntry(t("settings.log.warning"), "123", 2, hoursAgo(12), "warn", undefined, "stdout", ""),
new SimpleLogEntry( new SimpleLogEntry(
t("settings.log.multi-line-error.start-line"), t("settings.log.multi-line-error.start-line"),
"123", "123",
@@ -191,6 +191,7 @@ const fakeMessages = computedWithControl(
"error", "error",
"start", "start",
"stderr", "stderr",
"",
), ),
new SimpleLogEntry( new SimpleLogEntry(
t("settings.log.multi-line-error.middle-line"), t("settings.log.multi-line-error.middle-line"),
@@ -200,8 +201,9 @@ const fakeMessages = computedWithControl(
"error", "error",
"middle", "middle",
"stderr", "stderr",
"",
), ),
new SimpleLogEntry(t("settings.log.multi-line-error.end-line"), "123", 5, new Date(), "error", "end", "stderr"), new SimpleLogEntry(t("settings.log.multi-line-error.end-line"), "123", 5, new Date(), "error", "end", "stderr", ""),
new ComplexLogEntry( new ComplexLogEntry(
{ {
message: t("settings.log.complex"), message: t("settings.log.complex"),
@@ -215,8 +217,9 @@ const fakeMessages = computedWithControl(
new Date(), new Date(),
"info", "info",
"stdout", "stdout",
"",
), ),
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr"), new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr", ""),
], ],
); );
</script> </script>

View File

@@ -124,6 +124,7 @@ func createEvent(message string, streamType StdType) *LogEvent {
logEvent.Timestamp = timestamp.UnixMilli() logEvent.Timestamp = timestamp.UnixMilli()
message = strings.TrimSuffix(message[index+1:], "\n") message = strings.TrimSuffix(message[index+1:], "\n")
logEvent.Message = message logEvent.Message = message
logEvent.RawMessage = message
if message == "" { if message == "" {
logEvent.Message = "" // empty message so do nothing logEvent.Message = "" // empty message so do nothing
} else if json.Valid([]byte(message)) { } else if json.Valid([]byte(message)) {

View File

@@ -167,6 +167,7 @@ func ParseContainerAction(input string) (ContainerAction, error) {
type LogEvent struct { type LogEvent struct {
Message any `json:"m,omitempty"` Message any `json:"m,omitempty"`
RawMessage string `json:"rm,omitempty"`
Timestamp int64 `json:"ts"` Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"` Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"` Level string `json:"l,omitempty"`

View File

@@ -81,15 +81,18 @@ Content-Type: text/html
<pre>dev</pre> <pre>dev</pre>
/* snapshot: Test_handler_between_dates */ /* snapshot: Test_handler_between_dates */
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"} {"m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"} {"m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
/* snapshot: Test_handler_between_dates_with_everything_complex */ /* snapshot: Test_handler_between_dates_with_everything_complex */
{"m":{"msg":"a complex log message"},"ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"} {"m":{"msg":"a complex log message"},"rm":"{\"msg\":\"a complex log message\"}","ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"}
/* snapshot: Test_handler_between_dates_with_fill */ /* snapshot: Test_handler_between_dates_with_fill */
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"} {"m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"} {"m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
/* snapshot: Test_handler_download_logs */ /* snapshot: Test_handler_download_logs */
INFO Testing logs... INFO Testing logs...
@@ -179,7 +182,7 @@ data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"
/* snapshot: Test_handler_streamLogs_happy_with_id */ /* snapshot: Test_handler_streamLogs_happy_with_id */
:ping :ping
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout","c":"123456"} data: {"m":"INFO Testing logs...","rm":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout","c":"123456"}
id: 1589396137772 id: 1589396137772