diff --git a/Makefile b/Makefile index 618c25e3..51d1ec45 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ fake_assets: .PHONY: test test: fake_assets generate - go test -cover -race -count 1 -timeout 20s ./... + go test -cover -race -count 1 -timeout 40s ./... .PHONY: build build: dist generate diff --git a/assets/composable/eventStreams.ts b/assets/composable/eventStreams.ts index 3e971a49..7ee98770 100644 --- a/assets/composable/eventStreams.ts +++ b/assets/composable/eventStreams.ts @@ -210,7 +210,7 @@ function useLogStream(url: Ref, loadMoreUrl?: Ref) { const stopWatcher = watchOnce(url, () => abortController.abort("stream changed")); const logs = await ( await fetch( - `${loadMoreUrl.value}&${new URLSearchParams({ from: from.toISOString(), to: to.toISOString() }).toString()}`, + `${loadMoreUrl.value}&${new URLSearchParams({ from: from.toISOString(), to: to.toISOString(), fill: "1" }).toString()}`, { signal }, ) ).text(); diff --git a/internal/utils/ring_buffer.go b/internal/utils/ring_buffer.go index f94bfb2c..8d6e4079 100644 --- a/internal/utils/ring_buffer.go +++ b/internal/utils/ring_buffer.go @@ -45,6 +45,12 @@ func (r *RingBuffer[T]) Push(data T) { } } +func (r *RingBuffer[T]) Len() int { + r.mutex.RLock() + defer r.mutex.RUnlock() + return len(r.data) +} + func (r *RingBuffer[T]) Clear() { r.mutex.Lock() defer r.mutex.Unlock() diff --git a/internal/web/__snapshots__/web.snapshot b/internal/web/__snapshots__/web.snapshot index 1b745eb9..36c05c37 100644 --- a/internal/web/__snapshots__/web.snapshot +++ b/internal/web/__snapshots__/web.snapshot @@ -89,6 +89,15 @@ Content-Type: application/x-jsonl; charset=UTF-8 {"m":"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"} +/* snapshot: Test_handler_between_dates_with_fill */ +HTTP/1.1 200 OK +Connection: close +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: application/x-jsonl; charset=UTF-8 + +{"m":"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"} + /* snapshot: Test_handler_download_logs */ INFO Testing logs... diff --git a/internal/web/logs.go b/internal/web/logs.go index a7934dce..7f89921a 100644 --- a/internal/web/logs.go +++ b/internal/web/logs.go @@ -103,17 +103,29 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) return } - events, err := containerService.LogsBetweenDates(r.Context(), from, to, stdTypes) - if err != nil { - log.Error().Err(err).Msg("error fetching logs") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - buffer := utils.NewRingBuffer[*docker.LogEvent](500) + delta := to.Sub(from) - for event := range events { - buffer.Push(event) + for { + if buffer.Len() > 0 { + break + } + events, err := containerService.LogsBetweenDates(r.Context(), from, to, stdTypes) + if err != nil { + log.Error().Err(err).Msg("error fetching logs") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for event := range events { + buffer.Push(event) + } + + if !r.URL.Query().Has("fill") { // only auto fill if fill query parameter is set + break + } + + from = from.Add(-delta) } encoder := json.NewEncoder(w) diff --git a/internal/web/logs_test.go b/internal/web/logs_test.go index b73443dd..4da99d4c 100644 --- a/internal/web/logs_test.go +++ b/internal/web/logs_test.go @@ -243,7 +243,7 @@ func Test_handler_between_dates(t *testing.T) { require.NoError(t, err, "NewRequest should not return an error.") from, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00Z") - to, _ := time.Parse(time.RFC3339, "2018-01-01T010:00:00Z") + to, _ := time.Parse(time.RFC3339, "2018-01-01T10:00:00Z") q := req.URL.Query() q.Add("from", from.Format(time.RFC3339)) @@ -276,6 +276,51 @@ func Test_handler_between_dates(t *testing.T) { mockedClient.AssertExpectations(t) } +func Test_handler_between_dates_with_fill(t *testing.T) { + id := "123456" + req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs", nil) + require.NoError(t, err, "NewRequest should not return an error.") + + from, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00Z") + to, _ := time.Parse(time.RFC3339, "2018-01-01T10:00:00Z") + + q := req.URL.Query() + q.Add("from", from.Format(time.RFC3339)) + q.Add("to", to.Format(time.RFC3339)) + q.Add("stdout", "true") + q.Add("stderr", "true") + q.Add("fill", "true") + + req.URL.RawQuery = q.Encode() + + mockedClient := new(MockedClient) + + first := makeMessage("2020-05-13T18:55:37.772853839Z INFO Testing stdout logs...\n", docker.STDOUT) + second := makeMessage("2020-05-13T18:56:37.772853839Z INFO Testing stderr logs...\n", docker.STDERR) + data := append(first, second...) + + mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, from, to, docker.STDALL). + Return(io.NopCloser(bytes.NewReader([]byte{})), nil). + Once() + mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, time.Date(2017, time.December, 31, 14, 0, 0, 0, time.UTC), to, docker.STDALL). + Return(io.NopCloser(bytes.NewReader(data)), nil). + Once() + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) + mockedClient.On("Host").Return(docker.Host{ + ID: "localhost", + }) + mockedClient.On("ListContainers").Return([]docker.Container{ + {ID: id, Name: "test", Host: "localhost", State: "running"}, + }, nil) + mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil) + + handler := createDefaultHandler(mockedClient) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + abide.AssertHTTPResponse(t, t.Name(), rr.Result()) + mockedClient.AssertExpectations(t) +} + func makeMessage(message string, stream docker.StdType) []byte { data := make([]byte, 8) binary.BigEndian.PutUint32(data[4:], uint32(len(message)))