diff --git a/assets/components/LogContainer.vue b/assets/components/LogContainer.vue
index 064aca91..1368fb84 100644
--- a/assets/components/LogContainer.vue
+++ b/assets/components/LogContainer.vue
@@ -27,7 +27,7 @@
-
+
diff --git a/assets/components/LogEventSource.spec.js b/assets/components/LogEventSource.spec.js
index 3409c995..899f7c59 100644
--- a/assets/components/LogEventSource.spec.js
+++ b/assets/components/LogEventSource.spec.js
@@ -66,22 +66,24 @@ describe("", () => {
test("should connect to EventSource", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].emitOpen();
- expect(sources["/api/logs/stream?id=abc"].readyState).toBe(1);
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
wrapper.destroy();
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
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 () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].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="].emitOpen();
+ 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 { key, ...messageWithoutKey } = message;
@@ -90,15 +92,15 @@ describe("", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"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 () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].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="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message;
@@ -107,15 +109,17 @@ describe("", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2020-04-27T10:35:43.272Z,
- "message": " xxxxx",
+ "message": "xxxxx",
}
`);
});
test("should pass messages to slot", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].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="].emitOpen();
+ 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 { key, ...messageWithoutKey } = message;
@@ -125,7 +129,7 @@ describe("", () => {
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2019-06-12T10:55:42.459Z,
- "message": " \\"This is a message.\\"",
+ "message": "\\"This is a message.\\"",
}
`);
});
@@ -147,91 +151,93 @@ describe("", () => {
test("should render messages", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].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="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
+ data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
+ });
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - "This is a message."
+ - "This is a message."
`);
});
test("should render messages with color", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].emitOpen();
- sources["/api/logs/stream?id=abc"].emitMessage({
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - blackwhite
+ - blackwhite
`);
});
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
- sources["/api/logs/stream?id=abc"].emitOpen();
- sources["/api/logs/stream?id=abc"].emitMessage({
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z foo bar`,
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - <test>foo bar</test>
+ - <test>foo bar</test>
`);
});
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
- sources["/api/logs/stream?id=abc"].emitOpen();
- sources["/api/logs/stream?id=abc"].emitMessage({
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z foo bar`,
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - <test>foo bar</test>
+ - <test>foo bar</test>
`);
});
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
- sources["/api/logs/stream?id=abc"].emitOpen();
- sources["/api/logs/stream?id=abc"].emitMessage({
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z foo bar`,
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - <test>foo bar</test>
+ - <test>foo bar</test>
`);
});
test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
- sources["/api/logs/stream?id=abc"].emitOpen();
- sources["/api/logs/stream?id=abc"].emitMessage({
+ sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
+ sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
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 `,
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
- - This is a test <hi></hi>
+ - This is a test <hi></hi>
`);
});
diff --git a/assets/components/LogEventSource.vue b/assets/components/LogEventSource.vue
index a5bd8687..9e2c0f51 100644
--- a/assets/components/LogEventSource.vue
+++ b/assets/components/LogEventSource.vue
@@ -22,10 +22,11 @@ export default {
return {
messages: [],
buffer: [],
+ es: null,
+ lastEventId: null,
};
},
created() {
- this.es = null;
this.loadLogs();
this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 });
},
@@ -33,28 +34,37 @@ export default {
this.es.close();
},
methods: {
- onContainerStateChange(newValue, oldValue) {
- if (newValue == "running" && newValue != oldValue) {
- this.connect();
- }
- },
loadLogs() {
this.reset();
this.connect();
},
+ onContainerStopped() {
+ this.es.close();
+ this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date(), key: new Date() });
+ this.flushBuffer();
+ this.flushBuffer.flush();
+ },
+ onMessage(e) {
+ this.lastEventId = e.lastEventId;
+ this.buffer.push(this.parseMessage(e.data));
+ 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}`);
- this.es.addEventListener("container-stopped", (e) => {
- this.es.close();
- this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date() });
- this.flushBuffer();
- this.flushBuffer.flush();
- });
+ 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.buffer.push(this.parseMessage(e.data));
- this.flushBuffer();
- };
+ this.es.onmessage = (e) => this.onMessage(e);
},
flushNow() {
this.messages.push(...this.buffer);
@@ -96,7 +106,7 @@ export default {
}
const key = data.substring(0, i);
const date = new Date(key);
- const message = data.substring(i);
+ const message = data.substring(i + 1);
return { key, date, message };
},
},
diff --git a/assets/components/LogViewer.vue b/assets/components/LogViewer.vue
index fc668110..d2ce75b3 100644
--- a/assets/components/LogViewer.vue
+++ b/assets/components/LogViewer.vue
@@ -1,6 +1,6 @@
- -
+
-
@@ -73,6 +73,12 @@ export default {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
+ &[data-event="container-stopped"] {
+ color: #f14668;
+ }
+ &[data-event="container-started"] {
+ color: hsl(141, 53%, 53%);
+ }
}
&.small {
@@ -104,10 +110,6 @@ export default {
white-space: pre-wrap;
}
-li.event {
- color: #f14668;
-}
-
::v-deep mark {
border-radius: 2px;
background-color: var(--secondary-color);
diff --git a/web/logs.go b/web/logs.go
index d0491e42..15105e94 100644
--- a/web/logs.go
+++ b/web/logs.go
@@ -1,4 +1,3 @@
-
package web
import (
@@ -88,7 +87,12 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "keep-alive")
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 == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
@@ -100,11 +104,10 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
}
defer reader.Close()
-
buffered := bufio.NewReader(reader)
var readerError error
var message string
- for {
+ for {
message, readerError = buffered.ReadString('\n')
fmt.Fprintf(w, "data: %s\n", message)
if index := strings.IndexAny(message, " "); index != -1 {