diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go index 6271c6ef..bfbc06c3 100644 --- a/internal/manager/api_impl/jobs.go +++ b/internal/manager/api_impl/jobs.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "os" "github.com/labstack/echo/v4" @@ -197,13 +198,23 @@ func (f *Flamenco) FetchTaskLogTail(e echo.Context, taskID string) error { logger.Error().Err(err).Msg("error fetching task") return sendAPIError(e, http.StatusInternalServerError, "error fetching task: %v", err) } + logger = logger.With().Str("job", dbTask.Job.UUID).Logger() tail, err := f.logStorage.Tail(dbTask.Job.UUID, taskID) if err != nil { + if errors.Is(err, os.ErrNotExist) { + logger.Debug().Msg("task tail unavailable, task has no log on disk") + return e.NoContent(http.StatusNoContent) + } logger.Error().Err(err).Msg("unable to fetch task log tail") return sendAPIError(e, http.StatusInternalServerError, "error fetching task log tail: %v", err) } + if tail == "" { + logger.Debug().Msg("task tail unavailable, on-disk task log is empty") + return e.NoContent(http.StatusNoContent) + } + logger.Debug().Msg("fetched task tail") return e.String(http.StatusOK, tail) } diff --git a/internal/manager/api_impl/jobs_test.go b/internal/manager/api_impl/jobs_test.go index 3a1cc909..0507f7b5 100644 --- a/internal/manager/api_impl/jobs_test.go +++ b/internal/manager/api_impl/jobs_test.go @@ -4,7 +4,9 @@ package api_impl import ( "errors" + "fmt" "net/http" + "os" "testing" "time" @@ -187,3 +189,46 @@ func TestSetJobStatus_happy(t *testing.T) { assertResponseEmpty(t, echoCtx) } + +func TestFetchTaskLogTail(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mf := newMockedFlamenco(mockCtrl) + + jobID := "18a9b096-d77e-438c-9be2-74397038298b" + taskID := "2e020eee-20f8-4e95-8dcf-65f7dfc3ebab" + dbJob := persistence.Job{ + UUID: jobID, + Name: "test job", + Status: api.JobStatusActive, + Settings: persistence.StringInterfaceMap{}, + Metadata: persistence.StringStringMap{}, + } + dbTask := persistence.Task{ + UUID: taskID, + Job: &dbJob, + Name: "test task", + } + + // The task can be found, but has no on-disk task log. + // This should not cause any error, but instead be returned as "no content". + mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&dbTask, nil) + mf.logStorage.EXPECT().Tail(jobID, taskID). + Return("", fmt.Errorf("wrapped error: %w", os.ErrNotExist)) + + echoCtx := mf.prepareMockedRequest(nil) + err := mf.flamenco.FetchTaskLogTail(echoCtx, taskID) + assert.NoError(t, err) + assertResponseEmpty(t, echoCtx) + + // Check that a 204 No Content is also returned when the task log file on disk exists, but is empty. + mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&dbTask, nil) + mf.logStorage.EXPECT().Tail(jobID, taskID). + Return("", fmt.Errorf("wrapped error: %w", os.ErrNotExist)) + + echoCtx = mf.prepareMockedRequest(nil) + err = mf.flamenco.FetchTaskLogTail(echoCtx, taskID) + assert.NoError(t, err) + assertResponseEmpty(t, echoCtx) +} diff --git a/internal/manager/task_logs/task_logs_test.go b/internal/manager/task_logs/task_logs_test.go index 12d181d4..e95eb06f 100644 --- a/internal/manager/task_logs/task_logs_test.go +++ b/internal/manager/task_logs/task_logs_test.go @@ -84,9 +84,13 @@ func TestLogTail(t *testing.T) { jobID := "25c5a51c-e0dd-44f7-9f87-74f3d1fbbd8c" taskID := "20ff9d06-53ec-4019-9e2e-1774f05f170a" - err := s.Write(zerolog.Nop(), jobID, taskID, "Just a single line") - assert.NoError(t, err) contents, err := s.Tail(jobID, taskID) + assert.ErrorIs(t, err, os.ErrNotExist) + assert.Equal(t, "", contents) + + err = s.Write(zerolog.Nop(), jobID, taskID, "Just a single line") + assert.NoError(t, err) + contents, err = s.Tail(jobID, taskID) assert.NoError(t, err) assert.Equal(t, "Just a single line\n", string(contents)) diff --git a/web/app/src/stores/tasklog.js b/web/app/src/stores/tasklog.js index ce14ac39..c13fd80a 100644 --- a/web/app/src/stores/tasklog.js +++ b/web/app/src/stores/tasklog.js @@ -50,6 +50,8 @@ export const useTaskLog = defineStore('taskLog', { * @param {string} logChunk */ addChunk(logChunk) { + if (!logChunk) return; + const lines = logChunk.trimEnd().split('\n'); if (lines.length == 0) return;