From 686295090be1f8ac828befa5a2f5b8dae4cc267e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Sat, 16 Jul 2022 11:13:31 +0200 Subject: [PATCH] Manager: implement endpoint for getting the full task log Previously only the log tail was available, which is fine for many cases, but for serious debugging the entire log is needed. Manifest task: T99730 --- internal/manager/api_impl/interfaces.go | 1 + internal/manager/api_impl/jobs.go | 39 +++++++++++++++++++ .../api_impl/mocks/api_impl_mock.gen.go | 15 +++++++ internal/manager/task_logs/task_logs.go | 14 +++++++ internal/manager/task_logs/task_logs_test.go | 10 ++++- 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/internal/manager/api_impl/interfaces.go b/internal/manager/api_impl/interfaces.go index 7dcc9632..16e12a4a 100644 --- a/internal/manager/api_impl/interfaces.go +++ b/internal/manager/api_impl/interfaces.go @@ -123,6 +123,7 @@ type LogStorage interface { WriteTimestamped(logger zerolog.Logger, jobID, taskID string, logText string) error RotateFile(logger zerolog.Logger, jobID, taskID string) Tail(jobID, taskID string) (string, error) + TaskLog(jobID, taskID string) (string, error) } // LastRendered processes the "last rendered" images. diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go index 2f382867..af258bf8 100644 --- a/internal/manager/api_impl/jobs.go +++ b/internal/manager/api_impl/jobs.go @@ -215,6 +215,45 @@ func (f *Flamenco) SetTaskStatus(e echo.Context, taskID string) error { return e.NoContent(http.StatusNoContent) } +func (f *Flamenco) FetchTaskLog(e echo.Context, taskID string) error { + logger := requestLogger(e) + ctx := e.Request().Context() + + logger = logger.With().Str("task", taskID).Logger() + if !uuid.IsValid(taskID) { + logger.Warn().Msg("fetchTaskLog: bad task ID ") + return sendAPIError(e, http.StatusBadRequest, "bad task ID") + } + + dbTask, err := f.persist.FetchTask(ctx, taskID) + if err != nil { + if errors.Is(err, persistence.ErrTaskNotFound) { + return sendAPIError(e, http.StatusNotFound, "no such task") + } + 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() + + fullLog, err := f.logStorage.TaskLog(dbTask.Job.UUID, taskID) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + logger.Debug().Msg("task log unavailable, task has no log on disk") + return e.NoContent(http.StatusNoContent) + } + logger.Error().Err(err).Msg("unable to fetch task log") + return sendAPIError(e, http.StatusInternalServerError, "error fetching task log: %v", err) + } + + if fullLog == "" { + logger.Debug().Msg("task log unavailable, on-disk task log is empty") + return e.NoContent(http.StatusNoContent) + } + + logger.Debug().Msg("fetched task log") + return e.String(http.StatusOK, fullLog) +} + func (f *Flamenco) FetchTaskLogTail(e echo.Context, taskID string) error { logger := requestLogger(e) ctx := e.Request().Context() diff --git a/internal/manager/api_impl/mocks/api_impl_mock.gen.go b/internal/manager/api_impl/mocks/api_impl_mock.gen.go index 17c1e8e5..3e817913 100644 --- a/internal/manager/api_impl/mocks/api_impl_mock.gen.go +++ b/internal/manager/api_impl/mocks/api_impl_mock.gen.go @@ -620,6 +620,21 @@ func (mr *MockLogStorageMockRecorder) Tail(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tail", reflect.TypeOf((*MockLogStorage)(nil).Tail), arg0, arg1) } +// TaskLog mocks base method. +func (m *MockLogStorage) TaskLog(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TaskLog", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TaskLog indicates an expected call of TaskLog. +func (mr *MockLogStorageMockRecorder) TaskLog(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskLog", reflect.TypeOf((*MockLogStorage)(nil).TaskLog), arg0, arg1) +} + // Write mocks base method. func (m *MockLogStorage) Write(arg0 zerolog.Logger, arg1, arg2, arg3 string) error { m.ctrl.T.Helper() diff --git a/internal/manager/task_logs/task_logs.go b/internal/manager/task_logs/task_logs.go index 561be1ef..52817a51 100644 --- a/internal/manager/task_logs/task_logs.go +++ b/internal/manager/task_logs/task_logs.go @@ -159,6 +159,20 @@ func (s *Storage) filepath(jobID, taskID string) string { return path.Join(dirpath, filename) } +// TaskLog reads the entire log file. +func (s *Storage) TaskLog(jobID, taskID string) (string, error) { + filepath := s.filepath(jobID, taskID) + + s.taskLock(taskID) + defer s.taskUnlock(taskID) + + buffer, err := os.ReadFile(filepath) + if err != nil { + return "", fmt.Errorf("reading log file of job %q task %q: %w", jobID, taskID, err) + } + return string(buffer), nil +} + // Tail reads the final few lines of a task log. func (s *Storage) Tail(jobID, taskID string) (string, error) { filepath := s.filepath(jobID, taskID) diff --git a/internal/manager/task_logs/task_logs_test.go b/internal/manager/task_logs/task_logs_test.go index 3cc0a203..9296ba74 100644 --- a/internal/manager/task_logs/task_logs_test.go +++ b/internal/manager/task_logs/task_logs_test.go @@ -74,7 +74,7 @@ func TestLogRotation(t *testing.T) { assert.True(t, errors.Is(err, fs.ErrNotExist)) } -func TestLogTail(t *testing.T) { +func TestLogTailAndFullLog(t *testing.T) { s, finish, mocks := taskLogsTestFixtures(t) defer finish() @@ -110,6 +110,14 @@ func TestLogTail(t *testing.T) { err = s.Write(zerolog.Nop(), jobID, taskID, bigString) assert.NoError(t, err) + // Check the full log, it should be the entire bigString plus what was written before that. + contents, err = s.TaskLog(jobID, taskID) + if assert.NoError(t, err) { + expect := "Just a single line\nAnd another line!\n" + bigString + assert.Equal(t, expect, contents) + } + + // Check the tail, it should only be the few last lines of bigString. contents, err = s.Tail(jobID, taskID) assert.NoError(t, err) assert.Equal(t,