From c938b2ea1b7cab3cba71b757590200242b3effd8 Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Tue, 17 Dec 2019 14:58:29 -0800 Subject: [PATCH] Fetch more (#209) * Adds code to fetch more * Adds working in progress * Adds debugging test * Cleans up and creates a new component * Adds debug logs * Adds debounce for messages * Fixes scrolling * Fixes go code to handle length * Fixes tests * Adds loader * Fixes tests --- assets/components/InfiniteLoader.vue | 38 +++++++++++++++ assets/components/LogEventSource.spec.js | 41 ++++++++++++---- assets/components/LogEventSource.vue | 47 ++++++++++++++++--- assets/components/LogViewer.vue | 1 + .../components/ScrollableLogsWithSource.vue | 19 ++++++++ assets/components/ScrollableView.vue | 6 +-- .../__snapshots__/LogEventSource.spec.js.snap | 9 ---- assets/pages/Container.vue | 11 ++--- assets/styles.scss | 4 -- docker/client.go | 44 +++++++++++++++++ main.go | 23 +++++++-- package-lock.json | 23 +++++---- package.json | 7 +-- 13 files changed, 216 insertions(+), 57 deletions(-) create mode 100644 assets/components/InfiniteLoader.vue create mode 100644 assets/components/ScrollableLogsWithSource.vue delete mode 100644 assets/components/__snapshots__/LogEventSource.spec.js.snap diff --git a/assets/components/InfiniteLoader.vue b/assets/components/InfiniteLoader.vue new file mode 100644 index 00000000..1d6c222a --- /dev/null +++ b/assets/components/InfiniteLoader.vue @@ -0,0 +1,38 @@ + + + + diff --git a/assets/components/LogEventSource.spec.js b/assets/components/LogEventSource.spec.js index 98f802d3..7b689d47 100644 --- a/assets/components/LogEventSource.spec.js +++ b/assets/components/LogEventSource.spec.js @@ -3,15 +3,26 @@ import { sources } from "eventsourcemock"; import { shallowMount, mount, createLocalVue } from "@vue/test-utils"; import Vuex from "vuex"; import MockDate from "mockdate"; +import debounce from "lodash.debounce"; import LogEventSource from "./LogEventSource.vue"; import LogViewer from "./LogViewer.vue"; +jest.mock("lodash.debounce", () => jest.fn(fn => fn)); + describe("", () => { beforeEach(() => { global.BASE_PATH = ""; global.EventSource = EventSource; MockDate.set("6/12/2019", 0); window.scrollTo = jest.fn(); + + const observe = jest.fn(); + const unobserve = jest.fn(); + global.IntersectionObserver = jest.fn(() => ({ + observe, + unobserve + })); + debounce.mockClear(); }); afterEach(() => MockDate.reset()); @@ -48,7 +59,17 @@ describe("", () => { test("renders correctly", async () => { const wrapper = createLogEventSource(); - expect(wrapper.element).toMatchSnapshot(); + expect(wrapper.element).toMatchInlineSnapshot(` +
+
+ +
    +
+ `); }); test("should connect to EventSource", async () => { @@ -68,15 +89,15 @@ describe("", () => { 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."` }); + const [message, _] = wrapper.vm.messages; const { key, ...messageWithoutKey } = message; - expect(key).toBeGreaterThanOrEqual(0); - + expect(key).toBe("2019-06-12T10:55:42.459034602Z"); expect(messageWithoutKey).toMatchInlineSnapshot(` Object { "date": 2019-06-12T10:55:42.459Z, - "message": " \\"This is a message.\\"", + "message": "\\"This is a message.\\"", } `); }); @@ -89,12 +110,12 @@ describe("", () => { const { key, ...messageWithoutKey } = message; - expect(key).toBeGreaterThanOrEqual(0); + expect(key).toBe("2019-06-12T10:55:42.459034602Z"); expect(messageWithoutKey).toMatchInlineSnapshot(` Object { "date": 2019-06-12T10:55:42.459Z, - "message": " \\"This is a message.\\"", + "message": "\\"This is a message.\\"", } `); }); @@ -106,7 +127,7 @@ describe("", () => { expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
    -
  • today at 10:55 AM "This is a message."
  • +
  • today at 10:55 AM "This is a message."
`); }); @@ -120,7 +141,7 @@ describe("", () => { expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
    -
  • today at 10:55 AM blackwhite
  • +
  • today at 10:55 AM blackwhite
`); }); @@ -134,7 +155,7 @@ describe("", () => { expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
    -
  • today at 10:55 AM <test>foo bar</test>
  • +
  • today at 10:55 AM <test>foo bar</test>
`); }); @@ -151,7 +172,7 @@ describe("", () => { expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
    -
  • today at 10:55 AM This is a test <hi></hi>
  • +
  • today at 10:55 AM This is a test <hi></hi>
`); }); diff --git a/assets/components/LogEventSource.vue b/assets/components/LogEventSource.vue index a59c9cdd..f87f3eb3 100644 --- a/assets/components/LogEventSource.vue +++ b/assets/components/LogEventSource.vue @@ -1,15 +1,18 @@ diff --git a/assets/components/ScrollableView.vue b/assets/components/ScrollableView.vue index a69a509f..ca1793b8 100644 --- a/assets/components/ScrollableView.vue +++ b/assets/components/ScrollableView.vue @@ -3,7 +3,7 @@
-
+
@@ -40,6 +40,7 @@ export default { } }).observe(content, { childList: true, subtree: true }); }, + methods: { scrollToBottom(behavior = "instant") { const { content } = this.$refs; @@ -54,8 +55,7 @@ export default { const { content } = this.$refs; this.paused = content.scrollTop + content.clientHeight + 1 < content.scrollHeight; } - }, - watch: {} + } }; diff --git a/assets/styles.scss b/assets/styles.scss index bc5c16cb..c62d2834 100644 --- a/assets/styles.scss +++ b/assets/styles.scss @@ -18,7 +18,3 @@ body { h1.title { font-family: "Gafata", sans-serif; } - -.column { - min-width: 0; -} diff --git a/docker/client.go b/docker/client.go index 9a8bbfea..90b69a74 100644 --- a/docker/client.go +++ b/docker/client.go @@ -10,6 +10,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" @@ -36,6 +37,7 @@ type Client interface { FindContainer(string) (Container, error) ContainerLogs(context.Context, string, int) (<-chan string, <-chan error) Events(context.Context) (<-chan events.Message, <-chan error) + ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) ([]string, error) } // NewClient creates a new instance of Client @@ -190,3 +192,45 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in func (d *dockerClient) Events(ctx context.Context) (<-chan events.Message, <-chan error) { return d.cli.Events(ctx, types.EventsOptions{}) } + +func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) ([]string, error) { + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Since: strconv.FormatInt(from.Unix(), 10), + Until: strconv.FormatInt(to.Unix(), 10), + } + reader, _ := d.cli.ContainerLogs(ctx, id, options) + + defer reader.Close() + + var messages []string + hdr := make([]byte, 8) + var buffer bytes.Buffer + + for { + _, err := reader.Read(hdr) + if err != nil { + if err == io.EOF { + break + } else { + return nil, err + } + } + count := binary.BigEndian.Uint32(hdr[4:]) + _, err = io.CopyN(&buffer, reader, int64(count)) + + if err != nil { + if err == io.EOF { + break + } else { + return nil, err + } + } + messages = append(messages, strings.TrimSpace(buffer.String())) + buffer.Reset() + } + + return messages, nil +} diff --git a/main.go b/main.go index 648189f7..ed29a1f8 100644 --- a/main.go +++ b/main.go @@ -34,9 +34,9 @@ var ( ) type handler struct { - client docker.Client + client docker.Client showAll bool - box packr.Box + box packr.Box } func init() { @@ -91,6 +91,7 @@ func createRoutes(base string, h *handler) *mux.Router { s := r.PathPrefix(base).Subrouter() s.HandleFunc("/api/containers.json", h.listContainers) s.HandleFunc("/api/logs/stream", h.streamLogs) + s.HandleFunc("/api/logs", h.fetchLogsBetweenDates) s.HandleFunc("/api/events/stream", h.streamEvents) s.HandleFunc("/version", h.version) s.PathPrefix("/").Handler(http.StripPrefix(base, http.HandlerFunc(h.index))) @@ -108,9 +109,9 @@ func main() { box := packr.NewBox("./static") r := createRoutes(base, &handler{ - client: dockerClient, + client: dockerClient, showAll: showAll, - box: box, + box: box, }) srv := &http.Server{Addr: addr, Handler: r} @@ -170,6 +171,20 @@ func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) { } } +func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=UTF-8") + + from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from")) + to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to")) + id := r.URL.Query().Get("id") + + messages, _ := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to) + + for _, m := range messages { + fmt.Fprintln(w, m) + } +} + func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") if id == "" { diff --git a/package-lock.json b/package-lock.json index 09f1bd37..9bdcec9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7647,6 +7647,11 @@ "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10530,9 +10535,9 @@ } }, "splitpanes": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.1.1.tgz", - "integrity": "sha512-K4jlE6gLKElsjtoUqzX6x0Uq4k7rnSBAMmxOJNWYTTIAuO7seC0+x3ADGpUgqtazMYdq0nAfWB9+NI/t/tW0mw==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.1.2.tgz", + "integrity": "sha512-IiVeC1wk7yVq4QZ3VTj6FbOTUJQmy/gFxpR1pDk67P+Pj8V0daUsPeoBNcKmcvJp2v3272GHSeLTNEyDCKRXSQ==" }, "sprintf-js": { "version": "1.0.3", @@ -11549,9 +11554,9 @@ "dev": true }, "vue": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", - "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", + "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" }, "vue-hot-reload-api": { "version": "2.3.4", @@ -11591,9 +11596,9 @@ "integrity": "sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ==" }, "vue-template-compiler": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz", - "integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz", + "integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==", "dev": true, "requires": { "de-indent": "^1.0.2", diff --git a/package.json b/package.json index 16f48edb..20a6cafb 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,10 @@ "ansi-to-html": "^0.6.13", "bulma": "^0.8.0", "date-fns": "^2.8.1", - "splitpanes": "^2.1.1", + "lodash.debounce": "^4.0.8", + "splitpanes": "^2.1.2", "store": "^2.0.12", - "vue": "^2.6.10", + "vue": "^2.6.11", "vue-meta": "^2.3.1", "vue-router": "^3.1.3", "vuex": "^3.1.2" @@ -54,7 +55,7 @@ "sass": "^1.23.7", "vue-hot-reload-api": "^2.3.4", "vue-jest": "^3.0.5", - "vue-template-compiler": "^2.6.10" + "vue-template-compiler": "^2.6.11" }, "husky": { "hooks": {