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
This commit is contained in:
Sybren A. Stüvel 2022-07-16 11:13:31 +02:00
parent fee0717179
commit 686295090b
5 changed files with 78 additions and 1 deletions

View File

@ -123,6 +123,7 @@ type LogStorage interface {
WriteTimestamped(logger zerolog.Logger, jobID, taskID string, logText string) error WriteTimestamped(logger zerolog.Logger, jobID, taskID string, logText string) error
RotateFile(logger zerolog.Logger, jobID, taskID string) RotateFile(logger zerolog.Logger, jobID, taskID string)
Tail(jobID, taskID string) (string, error) Tail(jobID, taskID string) (string, error)
TaskLog(jobID, taskID string) (string, error)
} }
// LastRendered processes the "last rendered" images. // LastRendered processes the "last rendered" images.

View File

@ -215,6 +215,45 @@ func (f *Flamenco) SetTaskStatus(e echo.Context, taskID string) error {
return e.NoContent(http.StatusNoContent) 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 { func (f *Flamenco) FetchTaskLogTail(e echo.Context, taskID string) error {
logger := requestLogger(e) logger := requestLogger(e)
ctx := e.Request().Context() ctx := e.Request().Context()

View File

@ -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) 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. // Write mocks base method.
func (m *MockLogStorage) Write(arg0 zerolog.Logger, arg1, arg2, arg3 string) error { func (m *MockLogStorage) Write(arg0 zerolog.Logger, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -159,6 +159,20 @@ func (s *Storage) filepath(jobID, taskID string) string {
return path.Join(dirpath, filename) 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. // Tail reads the final few lines of a task log.
func (s *Storage) Tail(jobID, taskID string) (string, error) { func (s *Storage) Tail(jobID, taskID string) (string, error) {
filepath := s.filepath(jobID, taskID) filepath := s.filepath(jobID, taskID)

View File

@ -74,7 +74,7 @@ func TestLogRotation(t *testing.T) {
assert.True(t, errors.Is(err, fs.ErrNotExist)) assert.True(t, errors.Is(err, fs.ErrNotExist))
} }
func TestLogTail(t *testing.T) { func TestLogTailAndFullLog(t *testing.T) {
s, finish, mocks := taskLogsTestFixtures(t) s, finish, mocks := taskLogsTestFixtures(t)
defer finish() defer finish()
@ -110,6 +110,14 @@ func TestLogTail(t *testing.T) {
err = s.Write(zerolog.Nop(), jobID, taskID, bigString) err = s.Write(zerolog.Nop(), jobID, taskID, bigString)
assert.NoError(t, err) 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) contents, err = s.Tail(jobID, taskID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,