MM-57013 Added download button for downloading logs from server logs page in system console (#26389)

* added download system logs

* download all logs

* download all logs check-lint fix

* check lint fix

* download logs api

* download logs api working

* download logs working with error log

* linting issues and code cleanup

* CI check fix

* documented the api and logs from file with error handling

* test and final changes done

* final changes done

* Fix order of server-side translations

* Fix incorrect indentation of logs.yaml

---------

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Этот коммит содержится в:
Arya Khochare
2024-06-24 23:35:23 +05:30
коммит произвёл GitHub
родитель 71c25fb316
Коммит a8b18ac807
15 изменённых файлов: 187 добавлений и 26 удалений

Просмотреть файл

@@ -50,6 +50,7 @@ func (api *API) InitSystem() {
api.BaseRoutes.APIRoot.Handle("/caches/invalidate", api.APISessionRequired(invalidateCaches)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APISessionRequired(getLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs/download", api.APISessionRequired(downloadLogs)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/logs/query", api.APISessionRequired(queryLogs)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/logs", api.APIHandler(postLog)).Methods("POST")
@@ -414,6 +415,36 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.ArrayToJSON(lines)))
}
func downloadLogs(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("downloadLogs", audit.Fail)
defer c.LogAuditRec(auditRec)
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("downloadLogs", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionGetLogs) {
c.SetPermissionError(model.PermissionGetLogs)
return
}
fileData, err := c.App.GetMattermostLog(c.AppContext)
if err != nil {
c.Err = model.NewAppError("downloadLogs", "api.system.logs.download_bytes_buffer.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
reader := bytes.NewReader(fileData.Body)
web.WriteFileResponse("mattermost.log",
"text/plain",
int64(len(fileData.Body)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
reader,
true,
w,
r)
}
func postLog(c *Context, w http.ResponseWriter, r *http.Request) {
forceToDebug := false

Просмотреть файл

@@ -402,6 +402,46 @@ func TestGetLogs(t *testing.T) {
CheckUnauthorizedStatus(t, resp)
}
func TestDownloadLogs(t *testing.T) {
th := Setup(t)
defer th.TearDown()
for i := 0; i < 20; i++ {
th.TestLogger.Info(strconv.Itoa(i))
}
err := th.TestLogger.Flush()
require.NoError(t, err, "failed to flush log")
t.Run("Download Logs as system admin", func(t *testing.T) {
resData, resp, err2 := th.SystemAdminClient.DownloadLogs(context.Background())
require.NoError(t, err2)
require.Equal(t, "text/plain", resp.Header.Get("Content-Type"))
require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;filename=\"mattermost.log\"")
bodyString := string(resData)
for i := 0; i < 20; i++ {
assert.Contains(t, bodyString, fmt.Sprintf(`"msg":"%d"`, i))
}
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
_, resp, err2 := th.Client.DownloadLogs(context.Background())
require.Error(t, err2)
CheckForbiddenStatus(t, resp)
})
_, resp, err := th.Client.DownloadLogs(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.Client.Logout(context.Background())
_, resp, err = th.Client.DownloadLogs(context.Background())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestPostLog(t *testing.T) {
th := Setup(t)
defer th.TearDown()

Просмотреть файл

@@ -728,6 +728,7 @@ type AppIface interface {
GetLatestVersion(rctx request.CTX, latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError)
GetLogs(rctx request.CTX, page, perPage int) ([]string, *model.AppError)
GetLogsSkipSend(rctx request.CTX, page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError)
GetMattermostLog(ctx request.CTX) (*model.FileData, error)
GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError)
GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string
GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError)

Просмотреть файл

@@ -7616,6 +7616,28 @@ func (a *OpenTracingAppLayer) GetMarketplacePlugins(rctx request.CTX, filter *mo
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMattermostLog(ctx request.CTX) (*model.FileData, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMattermostLog")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetMattermostLog(ctx)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetMemberCountsByGroup")

Просмотреть файл

@@ -44,7 +44,7 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO
}
if options.IncludeLogs {
functions["mattermost log"] = a.getMattermostLog
functions["mattermost log"] = a.GetMattermostLog
functions["notification log"] = a.getNotificationsLog
}
@@ -321,7 +321,7 @@ func (a *App) getNotificationsLog(_ request.CTX) (*model.FileData, error) {
return fileData, nil
}
func (a *App) getMattermostLog(_ request.CTX) (*model.FileData, error) {
func (a *App) GetMattermostLog(ctx request.CTX) (*model.FileData, error) {
if !*a.Config().LogSettings.EnableFile {
return nil, errors.New("Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false")
}

Просмотреть файл

@@ -347,7 +347,7 @@ func TestGetMattermostLog(t *testing.T) {
*cfg.LogSettings.EnableFile = false
})
fileData, err := th.App.getMattermostLog(th.Context)
fileData, err := th.App.GetMattermostLog(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false")
@@ -367,7 +367,7 @@ func TestGetMattermostLog(t *testing.T) {
logLocation := config.GetLogFileLocation(dir)
// There is no mattermost.log file yet, so this fails
fileData, err = th.App.getMattermostLog(th.Context)
fileData, err = th.App.GetMattermostLog(th.Context)
assert.Nil(t, fileData)
assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation)
@@ -376,7 +376,7 @@ func TestGetMattermostLog(t *testing.T) {
err = os.WriteFile(logLocation, d1, 0777)
require.NoError(t, err)
fileData, err = th.App.getMattermostLog(th.Context)
fileData, err = th.App.GetMattermostLog(th.Context)
require.NoError(t, err)
require.NotNil(t, fileData)
assert.Equal(t, "mattermost.log", fileData.Filename)

Просмотреть файл

@@ -2938,6 +2938,10 @@
"id": "api.system.id_loaded.not_available.app_error",
"translation": "ID Loaded Push Notifications are not configured or supported on this server."
},
{
"id": "api.system.logs.download_bytes_buffer.app_error",
"translation": "Failed to write logs to buffer"
},
{
"id": "api.system.logs.invalidFilter",
"translation": "Invalid log filter"

Просмотреть файл

@@ -5862,6 +5862,20 @@ func (c *Client4) GetLogs(ctx context.Context, page, perPage int) ([]string, *Re
return c.ArrayFromJSON(r.Body), BuildResponse(r), nil
}
// Download logs as mattermost.log file
func (c *Client4) DownloadLogs(ctx context.Context) ([]byte, *Response, error) {
r, err := c.DoAPIGet(ctx, "/logs/download", "")
if err != nil {
return nil, BuildResponse(r), err
}
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, BuildResponse(r), NewAppError("DownloadLogs", "model.client.read_file.app_error", nil, "", r.StatusCode).Wrap(err)
}
return data, BuildResponse(r), nil
}
// PostLog is a convenience Web Service call so clients can log messages into
// the server-side logs. For example we typically log javascript error messages
// into the server-side. It returns the log message if the logging was successful.