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

Fixes reconnection bugs (#1256)

* Fixes reconnect by sending lastEventId see #1246

* Cleans up colors and spacing

* Fixes tests
This commit is contained in:
Amir Raminfar
2021-05-26 12:05:55 -07:00
committed by GitHub
parent 64f744a5d4
commit 920acf4256
5 changed files with 81 additions and 60 deletions

View File

@@ -27,7 +27,7 @@
</div> </div>
</template> </template>
<template v-slot="{ setLoading }"> <template v-slot="{ setLoading }">
<log-viewer-with-source :id="id" @loading-more="setLoading($event)" ref="source"></log-viewer-with-source> <log-viewer-with-source :id="id" @loading-more="setLoading($event)"></log-viewer-with-source>
</template> </template>
</scrollable-view> </scrollable-view>
</template> </template>

View File

@@ -66,22 +66,24 @@ describe("<LogEventSource />", () => {
test("should connect to EventSource", async () => { test("should connect to EventSource", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(1); expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
wrapper.destroy(); wrapper.destroy();
}); });
test("should close EventSource", async () => { test("should close EventSource", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
wrapper.destroy(); wrapper.destroy();
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(2); expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
}); });
test("should parse messages", async () => { test("should parse messages", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` }); sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
});
const [message, _] = wrapper.vm.messages; const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message; const { key, ...messageWithoutKey } = message;
@@ -90,15 +92,15 @@ describe("<LogEventSource />", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(` expect(messageWithoutKey).toMatchInlineSnapshot(`
Object { Object {
"date": 2019-06-12T10:55:42.459Z, "date": 2019-06-12T10:55:42.459Z,
"message": " \\"This is a message.\\"", "message": "\\"This is a message.\\"",
} }
`); `);
}); });
test("should parse messages with loki's timestamp format", async () => { test("should parse messages with loki's timestamp format", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` }); sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
const [message, _] = wrapper.vm.messages; const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message; const { key, ...messageWithoutKey } = message;
@@ -107,15 +109,17 @@ describe("<LogEventSource />", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(` expect(messageWithoutKey).toMatchInlineSnapshot(`
Object { Object {
"date": 2020-04-27T10:35:43.272Z, "date": 2020-04-27T10:35:43.272Z,
"message": " xxxxx", "message": "xxxxx",
} }
`); `);
}); });
test("should pass messages to slot", async () => { test("should pass messages to slot", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` }); sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
});
const [message, _] = wrapper.findComponent(LogViewer).vm.messages; const [message, _] = wrapper.findComponent(LogViewer).vm.messages;
const { key, ...messageWithoutKey } = message; const { key, ...messageWithoutKey } = message;
@@ -125,7 +129,7 @@ describe("<LogEventSource />", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(` expect(messageWithoutKey).toMatchInlineSnapshot(`
Object { Object {
"date": 2019-06-12T10:55:42.459Z, "date": 2019-06-12T10:55:42.459Z,
"message": " \\"This is a message.\\"", "message": "\\"This is a message.\\"",
} }
`); `);
}); });
@@ -147,91 +151,93 @@ describe("<LogEventSource />", () => {
test("should render messages", async () => { test("should render messages", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` }); sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
});
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"> "This is a message."</span></li> <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">"This is a message."</span></li>
</ul> </ul>
`); `);
}); });
test("should render messages with color", async () => { test("should render messages with color", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`, data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"> <span style="color:#000">black<span style="color:#AAA">white</span></span></span></li> <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
</ul> </ul>
`); `);
}); });
test("should render messages with html entities", async () => { test("should render messages with html entities", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`, data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"> &lt;test&gt;foo bar&lt;/test&gt;</span></li> <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul> </ul>
`); `);
}); });
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["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`, data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 11:55:42 PM</time></span> <span class="text"> &lt;test&gt;foo bar&lt;/test&gt;</span></li> <li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 11:55:42 PM</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul> </ul>
`); `);
}); });
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["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`, data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 23:55:42</time></span> <span class="text"> &lt;test&gt;foo bar&lt;/test&gt;</span></li> <li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 23:55:42</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul> </ul>
`); `);
}); });
test("should render messages with filter", async () => { test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" }); const wrapper = createLogEventSource({ searchFilter: "test" });
sources["/api/logs/stream?id=abc"].emitOpen(); sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-11T10:55:42.459034602Z Foo bar`, data: `2019-06-11T10:55:42.459034602Z Foo bar`,
}); });
sources["/api/logs/stream?id=abc"].emitMessage({ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`, data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li class=""><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"> This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li> <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li>
</ul> </ul>
`); `);
}); });

View File

@@ -22,10 +22,11 @@ export default {
return { return {
messages: [], messages: [],
buffer: [], buffer: [],
es: null,
lastEventId: null,
}; };
}, },
created() { created() {
this.es = null;
this.loadLogs(); this.loadLogs();
this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 }); this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 });
}, },
@@ -33,28 +34,37 @@ export default {
this.es.close(); this.es.close();
}, },
methods: { methods: {
onContainerStateChange(newValue, oldValue) {
if (newValue == "running" && newValue != oldValue) {
this.connect();
}
},
loadLogs() { loadLogs() {
this.reset(); this.reset();
this.connect(); this.connect();
}, },
connect() { onContainerStopped() {
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
this.es.addEventListener("container-stopped", (e) => {
this.es.close(); this.es.close();
this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date() }); this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date(), key: new Date() });
this.flushBuffer(); this.flushBuffer();
this.flushBuffer.flush(); this.flushBuffer.flush();
}); },
this.es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e))); onMessage(e) {
this.es.onmessage = (e) => { this.lastEventId = e.lastEventId;
this.buffer.push(this.parseMessage(e.data)); this.buffer.push(this.parseMessage(e.data));
this.flushBuffer(); this.flushBuffer();
}; },
onContainerStateChange(newValue, oldValue) {
if (newValue == "running" && newValue != oldValue) {
this.buffer.push({
event: "container-started",
message: "Container started",
date: new Date(),
key: new Date(),
});
this.connect();
}
},
connect() {
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}&lastEventId=${this.lastEventId ?? ""}`);
this.es.addEventListener("container-stopped", (e) => this.onContainerStopped());
this.es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
this.es.onmessage = (e) => this.onMessage(e);
}, },
flushNow() { flushNow() {
this.messages.push(...this.buffer); this.messages.push(...this.buffer);
@@ -96,7 +106,7 @@ export default {
} }
const key = data.substring(0, i); const key = data.substring(0, i);
const date = new Date(key); const date = new Date(key);
const message = data.substring(i); const message = data.substring(i + 1);
return { key, date, message }; return { key, date, message };
}, },
}, },

View File

@@ -1,6 +1,6 @@
<template> <template>
<ul class="events" :class="settings.size"> <ul class="events" :class="settings.size">
<li v-for="item in filtered" :key="item.key" :class="{ event: !!item.event }"> <li v-for="item in filtered" :key="item.key" :data-event="item.event">
<span class="date" v-if="settings.showTimestamp"><relative-time :date="item.date"></relative-time></span> <span class="date" v-if="settings.showTimestamp"><relative-time :date="item.date"></relative-time></span>
<span class="text" v-html="colorize(item.message)"></span> <span class="text" v-html="colorize(item.message)"></span>
</li> </li>
@@ -73,6 +73,12 @@ export default {
scroll-snap-align: end; scroll-snap-align: end;
scroll-margin-block-end: 5rem; scroll-margin-block-end: 5rem;
} }
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
} }
&.small { &.small {
@@ -104,10 +110,6 @@ export default {
white-space: pre-wrap; white-space: pre-wrap;
} }
li.event {
color: #f14668;
}
::v-deep mark { ::v-deep mark {
border-radius: 2px; border-radius: 2px;
background-color: var(--secondary-color); background-color: var(--secondary-color);

View File

@@ -1,4 +1,3 @@
package web package web
import ( import (
@@ -88,7 +87,12 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("X-Accel-Buffering", "no")
reader, err := h.client.ContainerLogs(r.Context(), container.ID, h.config.TailSize, r.Header.Get("Last-Event-ID")) lastEventId := r.Header.Get("Last-Event-ID")
if len(r.URL.Query().Get("lastEventId")) > 0 {
lastEventId = r.URL.Query().Get("lastEventId")
}
reader, err := h.client.ContainerLogs(r.Context(), container.ID, h.config.TailSize, lastEventId)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n") fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
@@ -100,7 +104,6 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
} }
defer reader.Close() defer reader.Close()
buffered := bufio.NewReader(reader) buffered := bufio.NewReader(reader)
var readerError error var readerError error
var message string var message string