diff --git a/api/v4/source/system.yaml b/api/v4/source/system.yaml index 335aa08c36..976220f8eb 100644 --- a/api/v4/source/system.yaml +++ b/api/v4/source/system.yaml @@ -1138,7 +1138,7 @@ - name: plugin_packets in: query description: | - Specifies plugin identifiers whose content should be included in the support packet. + Specifies plugin identifiers whose content should be included in the Support Packet. __Minimum server version__: 9.8.0 required: false diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/support_packet_generation_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/support_packet_generation_spec.js index f1615f99ac..1f1c67eea7 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/support_packet_generation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/support_packet_generation_spec.js @@ -49,6 +49,6 @@ const goToSupportPacketGenerationModal = () => { cy.findByRole('button', {name: 'Menu Icon'}).should('exist').click(); cy.findByRole('button', {name: 'Commercial Support dialog'}).click(); - // * Ensure the download support packet button exist and that text regarding setting the proper settings exist + // * Ensure the download Support Packet button exist and that text regarding setting the proper settings exist cy.get('a.DownloadSupportPacket').should('exist'); }; diff --git a/server/channels/api4/system.go b/server/channels/api4/system.go index 9ae2a42de6..2e4a004802 100644 --- a/server/channels/api4/system.go +++ b/server/channels/api4/system.go @@ -20,6 +20,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/audit" + "github.com/mattermost/mattermost/server/v8/config" "github.com/mattermost/mattermost/server/v8/platform/services/cache" "github.com/mattermost/mattermost/server/v8/platform/services/upgrader" "github.com/mattermost/mattermost/server/v8/platform/shared/web" @@ -84,7 +85,7 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { return } - // Support packet generation is limited to system admins (MM-42271). + // Support Packet generation is limited to system admins (MM-42271). if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { c.SetPermissionError(model.PermissionManageSystem) return @@ -427,14 +428,14 @@ func downloadLogs(c *Context, w http.ResponseWriter, r *http.Request) { return } - fileData, err := c.App.GetMattermostLog(c.AppContext) + fileData, err := c.App.Srv().Platform().GetLogFile(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", + web.WriteFileResponse(config.LogFilename, "text/plain", int64(len(fileData.Body)), time.Now(), diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index 4c5c280727..1e99ea51bf 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -211,7 +211,7 @@ func TestGenerateSupportPacket(t *testing.T) { th.LoginSystemManager() defer th.TearDown() - t.Run("system admin and local client can generate support packet", func(t *testing.T) { + t.Run("system admin and local client can generate Support Packet", func(t *testing.T) { l := model.NewTestLicense() th.App.Srv().SetLicense(l) diff --git a/server/channels/app/analytics.go b/server/channels/app/analytics.go index 701b14def8..b87fe951b5 100644 --- a/server/channels/app/analytics.go +++ b/server/channels/app/analytics.go @@ -33,7 +33,7 @@ func (a *App) getAnalytics(rctx request.CTX, name string, teamID string, forSupp } skipIntensiveQueries := false - // When generating a Support packet, always run intensive queries. + // When generating a Support Packet, always run intensive queries. if !forSupportPacket { if systemUserCount > int64(*a.Config().AnalyticsSettings.MaxUsersForStatistics) { rctx.Logger().Debug("More than limit users are on the system, intensive queries skipped", mlog.Int("limit", *a.Config().AnalyticsSettings.MaxUsersForStatistics)) diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 4646f10e1a..6f10ab37c1 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -731,7 +731,6 @@ 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) diff --git a/server/channels/app/busy_test.go b/server/channels/app/busy_test.go index e3ca20300d..960ff443a6 100644 --- a/server/channels/app/busy_test.go +++ b/server/channels/app/busy_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -146,6 +147,9 @@ func (c *ClusterMock) GetLogs(page, perPage int) ([]string, *model.AppError) { func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) { return nil, nil } +func (c *ClusterMock) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) { + return nil, nil +} func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil } func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError { return nil diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index 61317b7e7c..a6057ee3f7 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -7660,28 +7660,6 @@ 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") diff --git a/server/channels/app/platform/busy_test.go b/server/channels/app/platform/busy_test.go index 3e54bfe92b..2e296040a9 100644 --- a/server/channels/app/platform/busy_test.go +++ b/server/channels/app/platform/busy_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -144,6 +145,9 @@ func (c *ClusterMock) GetLogs(page, perPage int) ([]string, *model.AppError) func (c *ClusterMock) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) { return nil, nil } +func (c *ClusterMock) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) { + return nil, nil +} func (c *ClusterMock) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { return nil, nil } func (c *ClusterMock) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError { return nil diff --git a/server/channels/app/platform/log.go b/server/channels/app/platform/log.go index f9930c9a16..88813b180e 100644 --- a/server/channels/app/platform/log.go +++ b/server/channels/app/platform/log.go @@ -6,14 +6,16 @@ package platform import ( "context" "encoding/json" - "errors" "io" "net/http" "os" "time" + "github.com/pkg/errors" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/config" ) @@ -206,6 +208,40 @@ func (ps *PlatformService) GetLogsSkipSend(page, perPage int, logFilter *model.L return lines, nil } +func (ps *PlatformService) GetLogFile(_ request.CTX) (*model.FileData, error) { + if !*ps.Config().LogSettings.EnableFile { + return nil, errors.New("Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false") + } + + mattermostLog := config.GetLogFileLocation(*ps.Config().LogSettings.FileLocation) + mattermostLogFileData, err := os.ReadFile(mattermostLog) + if err != nil { + return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog) + } + + return &model.FileData{ + Filename: config.LogFilename, + Body: mattermostLogFileData, + }, nil +} + +func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileData, error) { + if !*ps.Config().NotificationLogSettings.EnableFile { + return nil, errors.New("Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false") + } + + notificationsLog := config.GetNotificationsLogFileLocation(*ps.Config().LogSettings.FileLocation) + notificationsLogFileData, err := os.ReadFile(notificationsLog) + if err != nil { + return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog) + } + + return &model.FileData{ + Filename: config.LogNotificationFilename, + Body: notificationsLogFileData, + }, nil +} + func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool { logLevels := logFilter.LogLevels if len(logLevels) == 0 { diff --git a/server/channels/app/platform/log_test.go b/server/channels/app/platform/log_test.go new file mode 100644 index 0000000000..a5a8c5c1b3 --- /dev/null +++ b/server/channels/app/platform/log_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package platform + +import ( + "os" + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetMattermostLog(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + // disable mattermost log file setting in config so we should get an warning + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.LogSettings.EnableFile = false + }) + + fileData, err := th.Service.GetLogFile(th.Context) + assert.Nil(t, fileData) + assert.ErrorContains(t, err, "Unable to retrieve mattermost logs because LogSettings.EnableFile is set to false") + + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(dir) + assert.NoError(t, err) + }) + + // Enable log file but point to an empty directory to get an error trying to read the file + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.LogSettings.EnableFile = true + *cfg.LogSettings.FileLocation = dir + }) + + logLocation := config.GetLogFileLocation(dir) + + // There is no mattermost.log file yet, so this fails + fileData, err = th.Service.GetLogFile(th.Context) + assert.Nil(t, fileData) + assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation) + + // Happy path where we get a log file and no warning + d1 := []byte("hello\ngo\n") + err = os.WriteFile(logLocation, d1, 0777) + require.NoError(t, err) + + fileData, err = th.Service.GetLogFile(th.Context) + require.NoError(t, err) + require.NotNil(t, fileData) + assert.Equal(t, "mattermost.log", fileData.Filename) + assert.Positive(t, len(fileData.Body)) +} + +func TestGetNotificationLogFile(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + // Disable notifications file setting in config so we should get an warning + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.NotificationLogSettings.EnableFile = false + }) + + fileData, err := th.Service.GetNotificationLogFile(th.Context) + assert.Nil(t, fileData) + assert.ErrorContains(t, err, "Unable to retrieve notifications logs because NotificationLogSettings.EnableFile is set to false") + + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + t.Cleanup(func() { + err = os.RemoveAll(dir) + assert.NoError(t, err) + }) + + // Enable notifications file but point to an empty directory to get an error trying to read the file + th.Service.UpdateConfig(func(cfg *model.Config) { + *cfg.NotificationLogSettings.EnableFile = true + *cfg.LogSettings.FileLocation = dir + }) + + logLocation := config.GetNotificationsLogFileLocation(dir) + + // There is no notifications.log file yet, so this fails + fileData, err = th.Service.GetNotificationLogFile(th.Context) + assert.Nil(t, fileData) + assert.ErrorContains(t, err, "failed read notifcation log file at path "+logLocation) + + // Happy path where we have file and no error + d1 := []byte("hello\ngo\n") + err = os.WriteFile(logLocation, d1, 0777) + require.NoError(t, err) + + fileData, err = th.Service.GetNotificationLogFile(th.Context) + assert.NoError(t, err) + require.NotNil(t, fileData) + assert.Equal(t, "notifications.log", fileData.Filename) + assert.Positive(t, len(fileData.Body)) +} diff --git a/server/channels/app/platform/metrics.go b/server/channels/app/platform/metrics.go index 980b9aa942..90dc37089b 100644 --- a/server/channels/app/platform/metrics.go +++ b/server/channels/app/platform/metrics.go @@ -4,6 +4,7 @@ package platform import ( + "bytes" "context" "fmt" "net" @@ -11,6 +12,7 @@ import ( "net/http/pprof" "path" "runtime" + rpprof "runtime/pprof" "strings" "sync" "text/template" @@ -23,11 +25,15 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/einterfaces" ) -const TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second +const ( + TimeToWaitForConnectionsToCloseOnServerShutdown = time.Second + cpuProfileDuration = 5 * time.Second +) type platformMetrics struct { server *http.Server @@ -247,3 +253,52 @@ func (ps *PlatformService) Metrics() einterfaces.MetricsInterface { return ps.metricsIFace } + +func (ps *PlatformService) CreateCPUProfile(_ request.CTX) (*model.FileData, error) { + var b bytes.Buffer + + err := rpprof.StartCPUProfile(&b) + if err != nil { + return nil, errors.Wrap(err, "failed to start CPU profile") + } + + time.Sleep(cpuProfileDuration) + + rpprof.StopCPUProfile() + + fileData := &model.FileData{ + Filename: "cpu.prof", + Body: b.Bytes(), + } + return fileData, nil +} + +func (ps *PlatformService) CreateHeapProfile(_ request.CTX) (*model.FileData, error) { + var b bytes.Buffer + + err := rpprof.Lookup("heap").WriteTo(&b, 0) + if err != nil { + return nil, errors.Wrap(err, "failed to lookup heap profile") + } + + fileData := &model.FileData{ + Filename: "heap.prof", + Body: b.Bytes(), + } + return fileData, nil +} + +func (ps *PlatformService) CreateGoroutineProfile(_ request.CTX) (*model.FileData, error) { + var b bytes.Buffer + + err := rpprof.Lookup("goroutine").WriteTo(&b, 2) + if err != nil { + return nil, errors.Wrap(err, "failed to lookup goroutine profile") + } + + fileData := &model.FileData{ + Filename: "goroutines", + Body: b.Bytes(), + } + return fileData, nil +} diff --git a/server/channels/app/support_packet.go b/server/channels/app/support_packet.go index e06f4ce285..5ddba8d5b2 100644 --- a/server/channels/app/support_packet.go +++ b/server/channels/app/support_packet.go @@ -4,13 +4,9 @@ package app import ( - "bytes" "encoding/json" - "os" "runtime" - "runtime/pprof" - "strings" - "time" + "sync" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -19,61 +15,86 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/config" -) - -const ( - cpuProfileDuration = 5 * time.Second ) func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData { - // If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any - var warnings []string - - // Creating an array of files that we are going to be adding to our zip file - fileDatas := []model.FileData{} - // A array of the functions that we can iterate through since they all have the same return value functions := map[string]func(c request.CTX) (*model.FileData, error){ - "support package": a.generateSupportPacketYaml, - "plugins": a.createPluginsFile, - "config": a.createSanitizedConfigFile, - "cpu profile": a.createCPUProfile, - "heap profile": a.createHeapProfile, - "goroutines": a.createGoroutineProfile, - "metadata": a.createSupportPacketMetadata, + "support packet": a.generateSupportPacketYaml, + "plugins": a.createPluginsFile, + "config": a.createSanitizedConfigFile, + "cpu profile": a.Srv().Platform().CreateCPUProfile, + "heap profile": a.Srv().Platform().CreateHeapProfile, + "goroutines": a.Srv().Platform().CreateGoroutineProfile, + "metadata": a.createSupportPacketMetadata, } if options.IncludeLogs { - functions["mattermost log"] = a.GetMattermostLog - functions["notification log"] = a.getNotificationsLog + functions["mattermost log"] = a.Srv().Platform().GetLogFile + functions["notification log"] = a.Srv().Platform().GetNotificationLogFile } - for name, fn := range functions { - fileData, err := fn(c) - if err != nil { - c.Logger().Error("Failed to generate file for support package", mlog.Err(err), mlog.String("file", name)) - warnings = append(warnings, err.Error()) - } + // If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any + var warnings *multierror.Error + // Creating an array of files that we are going to be adding to our zip file + var fileDatas []model.FileData + var wg sync.WaitGroup + var mut sync.Mutex // Protects warnings and fileDatas - if fileData != nil { - fileDatas = append(fileDatas, *fileData) + wg.Add(1) + go func() { + defer wg.Done() + + for name, fn := range functions { + fileData, err := fn(c) + mut.Lock() + if err != nil { + c.Logger().Error("Failed to generate file for Support Packet", mlog.String("file", name), mlog.Err(err)) + warnings = multierror.Append(warnings, err) + } + + if fileData != nil { + fileDatas = append(fileDatas, *fileData) + } + mut.Unlock() } + }() + + // Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time + if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable { + wg.Add(1) + go func() { + defer wg.Done() + + files, err := cluster.GenerateSupportPacket(c, options) + mut.Lock() + if err != nil { + c.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err)) + warnings = multierror.Append(warnings, err) + } + + for _, node := range files { + fileDatas = append(fileDatas, node...) + } + mut.Unlock() + }() } + wg.Wait() + if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { pluginContext := pluginContext(c) for _, id := range options.PluginPackets { hooks, err := pluginsEnvironment.HooksForPlugin(id) if err != nil { c.Logger().Error("Failed to call hooks for plugin", mlog.Err(err), mlog.String("plugin", id)) - warnings = append(warnings, err.Error()) + warnings = multierror.Append(warnings, err) continue } pluginData, err := hooks.GenerateSupportData(pluginContext) if err != nil { - c.Logger().Warn("Failed to generate plugin file for support package", mlog.Err(err), mlog.String("plugin", id)) - warnings = append(warnings, err.Error()) + c.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.Err(err), mlog.String("plugin", id)) + warnings = multierror.Append(warnings, err) continue } for _, data := range pluginData { @@ -83,11 +104,10 @@ func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketO } // Adding a warning.txt file to the fileDatas if any warning - if len(warnings) > 0 { - finalWarning := strings.Join(warnings, "\n") + if warnings != nil { fileDatas = append(fileDatas, model.FileData{ - Filename: "warning.txt", - Body: []byte(finalWarning), + Filename: model.SupportPacketErrorFile, + Body: []byte(warnings.Error()), }) } @@ -108,8 +128,8 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) /* Cluster */ var clusterID string - if a.Cluster() != nil { - clusterID = a.Cluster().GetClusterId() + if cluster := a.Cluster(); cluster != nil { + clusterID = cluster.GetClusterId() } /* File store */ @@ -124,8 +144,7 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) /* LDAP */ var vendorName, vendorVersion string - ldap := a.Ldap() - if ldap != nil { + if ldap := a.Ldap(); ldap != nil { vendorName, vendorVersion, err = ldap.GetVendorNameAndVendorVersion(c) if err != nil { rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info")) @@ -143,9 +162,9 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) var elasticServerVersion string var elasticServerPlugins []string - if a.Srv().Platform().SearchEngine.ElasticsearchEngine != nil { - elasticServerVersion = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetFullVersion() - elasticServerPlugins = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetPlugins() + if se := a.Srv().Platform().SearchEngine.ElasticsearchEngine; se != nil { + elasticServerVersion = se.GetFullVersion() + elasticServerPlugins = se.GetPlugins() } /* License */ @@ -182,19 +201,20 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) analytics, appErr := a.GetAnalyticsForSupportPacket(c) if appErr != nil { rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics")) - } - if len(analytics) < 11 { - rErr = multierror.Append(errors.New("not enought analytics information found")) } else { - totalChannels = int(analytics[0].Value) + int(analytics[1].Value) - totalPosts = int(analytics[2].Value) - totalTeams = int(analytics[4].Value) - websocketConnections = int(analytics[5].Value) - masterDbConnections = int(analytics[6].Value) - replicaDbConnections = int(analytics[7].Value) - dailyActiveUsers = int(analytics[8].Value) - monthlyActiveUsers = int(analytics[9].Value) - inactiveUserCount = int(analytics[10].Value) + if len(analytics) < 11 { + rErr = multierror.Append(errors.New("not enough analytics information found")) + } else { + totalChannels = int(analytics[0].Value) + int(analytics[1].Value) + totalPosts = int(analytics[2].Value) + totalTeams = int(analytics[4].Value) + websocketConnections = int(analytics[5].Value) + masterDbConnections = int(analytics[6].Value) + replicaDbConnections = int(analytics[7].Value) + dailyActiveUsers = int(analytics[8].Value) + monthlyActiveUsers = int(analytics[9].Value) + inactiveUserCount = int(analytics[10].Value) + } } /* Jobs */ @@ -228,7 +248,7 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs")) } - // Creating the struct for support packet yaml file + // Creating the struct for Support Packet yaml file supportPacket := model.SupportPacket{ /* Build information */ ServerOS: runtime.GOOS, @@ -283,10 +303,10 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) MigrationJobs: migrationJobs, } - // Marshal to a Yaml File + // Marshal to a YAML File supportPacketYaml, err := yaml.Marshal(&supportPacket) if err != nil { - rErr = multierror.Append(errors.Wrap(err, "failed to marshal support package into yaml")) + rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml")) } fileData := &model.FileData{ @@ -296,61 +316,6 @@ func (a *App) generateSupportPacketYaml(c request.CTX) (*model.FileData, error) return fileData, rErr.ErrorOrNil() } -func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) { - // Getting the plugins installed on the server, prettify it, and then add them to the file data array - pluginsResponse, appErr := a.GetPlugins() - if appErr != nil { - return nil, errors.Wrap(appErr, "failed to get plugin list for support package") - } - - pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") - if err != nil { - return nil, errors.Wrap(err, "failed to marshal plugin list into json") - } - - fileData := &model.FileData{ - Filename: "plugins.json", - Body: pluginsPrettyJSON, - } - return fileData, nil -} - -func (a *App) getNotificationsLog(_ request.CTX) (*model.FileData, error) { - if !*a.Config().NotificationLogSettings.EnableFile { - return nil, errors.New("Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") - } - - notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation) - notificationsLogFileData, err := os.ReadFile(notificationsLog) - if err != nil { - return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog) - } - - fileData := &model.FileData{ - Filename: "notifications.log", - Body: notificationsLogFileData, - } - return fileData, nil -} - -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") - } - - mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation) - mattermostLogFileData, err := os.ReadFile(mattermostLog) - if err != nil { - return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog) - } - - fileData := &model.FileData{ - Filename: "mattermost.log", - Body: mattermostLogFileData, - } - return fileData, nil -} - func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) { // Getting sanitized config, prettifying it, and then adding it to our file data array sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ") @@ -365,51 +330,21 @@ func (a *App) createSanitizedConfigFile(_ request.CTX) (*model.FileData, error) return fileData, nil } -func (a *App) createCPUProfile(_ request.CTX) (*model.FileData, error) { - var b bytes.Buffer - - err := pprof.StartCPUProfile(&b) - if err != nil { - return nil, errors.Wrap(err, "failed to start CPU profile") +func (a *App) createPluginsFile(_ request.CTX) (*model.FileData, error) { + // Getting the plugins installed on the server, prettify it, and then add them to the file data array + pluginsResponse, appErr := a.GetPlugins() + if appErr != nil { + return nil, errors.Wrap(appErr, "failed to get plugin list for Support Packet") } - time.Sleep(cpuProfileDuration) - - pprof.StopCPUProfile() - - fileData := &model.FileData{ - Filename: "cpu.prof", - Body: b.Bytes(), - } - return fileData, nil -} - -func (a *App) createHeapProfile(request.CTX) (*model.FileData, error) { - var b bytes.Buffer - - err := pprof.Lookup("heap").WriteTo(&b, 0) + pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") if err != nil { - return nil, errors.Wrap(err, "failed to lookup heap profile") + return nil, errors.Wrap(err, "failed to marshal plugin list into json") } fileData := &model.FileData{ - Filename: "heap.prof", - Body: b.Bytes(), - } - return fileData, nil -} - -func (a *App) createGoroutineProfile(_ request.CTX) (*model.FileData, error) { - var b bytes.Buffer - - err := pprof.Lookup("goroutine").WriteTo(&b, 2) - if err != nil { - return nil, errors.Wrap(err, "failed to lookup goroutine profile") - } - - fileData := &model.FileData{ - Filename: "goroutines", - Body: b.Bytes(), + Filename: "plugins.json", + Body: pluginsPrettyJSON, } return fileData, nil } @@ -417,12 +352,12 @@ func (a *App) createGoroutineProfile(_ request.CTX) (*model.FileData, error) { func (a *App) createSupportPacketMetadata(_ request.CTX) (*model.FileData, error) { metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil) if err != nil { - return nil, errors.Wrap(err, "failed to generate packet metadata") + return nil, errors.Wrap(err, "failed to generate Packet metadata") } b, err := yaml.Marshal(metadata) if err != nil { - return nil, errors.Wrap(err, "failed to marshal packet metadata into yaml") + return nil, errors.Wrap(err, "failed to marshal Packet metadata into yaml") } fileData := &model.FileData{ diff --git a/server/channels/app/support_packet_test.go b/server/channels/app/support_packet_test.go index 1b5ccfcce5..e1f0989877 100644 --- a/server/channels/app/support_packet_test.go +++ b/server/channels/app/support_packet_test.go @@ -40,7 +40,7 @@ func TestCreatePluginsFile(t *testing.T) { // Plugins off in settings so no fileData and we get a warning instead fileData, err = th.App.createPluginsFile(th.Context) assert.Nil(t, fileData) - assert.ErrorContains(t, err, "failed to get plugin list for support package") + assert.ErrorContains(t, err, "failed to get plugin list for Support Packet") } func TestGenerateSupportPacketYaml(t *testing.T) { @@ -68,7 +68,7 @@ func TestGenerateSupportPacketYaml(t *testing.T) { } t.Run("Happy path", func(t *testing.T) { - // Happy path where we have a support packet yaml file without any warnings + // Happy path where we have a Support Packet yaml file without any warnings packet := generateSupportPacket(t) /* Build information */ @@ -207,7 +207,7 @@ func TestGenerateSupportPacket(t *testing.T) { } genMockLogFiles() - t.Run("generate support packet with logs", func(t *testing.T) { + t.Run("generate Support Packet with logs", func(t *testing.T) { fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{ IncludeLogs: true, }) @@ -232,7 +232,7 @@ func TestGenerateSupportPacket(t *testing.T) { assert.ElementsMatch(t, testFiles, rFileNames) }) - t.Run("generate support packet without logs", func(t *testing.T) { + t.Run("generate Support Packet without logs", func(t *testing.T) { fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{ IncludeLogs: false, }) @@ -326,96 +326,6 @@ func TestGenerateSupportPacket(t *testing.T) { }) } -func TestGetNotificationsLog(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - // Disable notifications file setting in config so we should get an warning - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.NotificationLogSettings.EnableFile = false - }) - - fileData, err := th.App.getNotificationsLog(th.Context) - assert.Nil(t, fileData) - assert.ErrorContains(t, err, "Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") - - dir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(dir) - assert.NoError(t, err) - }) - - // Enable notifications file but point to an empty directory to get an error trying to read the file - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.NotificationLogSettings.EnableFile = true - *cfg.LogSettings.FileLocation = dir - }) - - logLocation := config.GetNotificationsLogFileLocation(dir) - - // There is no notifications.log file yet, so this fails - fileData, err = th.App.getNotificationsLog(th.Context) - assert.Nil(t, fileData) - assert.ErrorContains(t, err, "failed read notifcation log file at path "+logLocation) - - // Happy path where we have file and no error - d1 := []byte("hello\ngo\n") - err = os.WriteFile(logLocation, d1, 0777) - require.NoError(t, err) - - fileData, err = th.App.getNotificationsLog(th.Context) - assert.NoError(t, err) - require.NotNil(t, fileData) - assert.Equal(t, "notifications.log", fileData.Filename) - assert.Positive(t, len(fileData.Body)) -} - -func TestGetMattermostLog(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - // disable mattermost log file setting in config so we should get an warning - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.LogSettings.EnableFile = false - }) - - 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") - - dir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err = os.RemoveAll(dir) - assert.NoError(t, err) - }) - - // Enable log file but point to an empty directory to get an error trying to read the file - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.LogSettings.EnableFile = true - *cfg.LogSettings.FileLocation = dir - }) - - logLocation := config.GetLogFileLocation(dir) - - // There is no mattermost.log file yet, so this fails - fileData, err = th.App.GetMattermostLog(th.Context) - assert.Nil(t, fileData) - assert.ErrorContains(t, err, "failed read mattermost log file at path "+logLocation) - - // Happy path where we get a log file and no warning - d1 := []byte("hello\ngo\n") - err = os.WriteFile(logLocation, d1, 0777) - require.NoError(t, err) - - fileData, err = th.App.GetMattermostLog(th.Context) - require.NoError(t, err) - require.NotNil(t, fileData) - assert.Equal(t, "mattermost.log", fileData.Filename) - assert.Positive(t, len(fileData.Body)) -} - func TestCreateSanitizedConfigFile(t *testing.T) { th := Setup(t) defer th.TearDown() diff --git a/server/channels/testlib/cluster.go b/server/channels/testlib/cluster.go index f9ac36ab41..9f27df90ce 100644 --- a/server/channels/testlib/cluster.go +++ b/server/channels/testlib/cluster.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -63,6 +64,10 @@ func (c *FakeClusterInterface) QueryLogs(page, perPage int) (map[string][]string return make(map[string][]string), nil } +func (c *FakeClusterInterface) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) { + return nil, nil +} + func (c *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError { return nil } diff --git a/server/cmd/mmctl/commands/system_e2e_test.go b/server/cmd/mmctl/commands/system_e2e_test.go index 64d298b364..6c7d785d30 100644 --- a/server/cmd/mmctl/commands/system_e2e_test.go +++ b/server/cmd/mmctl/commands/system_e2e_test.go @@ -112,7 +112,7 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() { printer.SetFormat(printer.FormatPlain) s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) }) - s.Run("Download support packet with default filename", func() { + s.Run("Download Support Packet with default filename", func() { printer.Clean() s.T().Cleanup(cleanupSupportPacket(s.T())) @@ -141,7 +141,7 @@ func (s *MmctlE2ETestSuite) TestSupportPacketCmdF() { s.True(found) }) - s.Run("Download support packet with custom filename", func() { + s.Run("Download Support Packet with custom filename", func() { printer.Clean() err := SystemSupportPacketCmd.ParseFlags([]string{"-o", "foo.zip"}) diff --git a/server/cmd/mmctl/commands/system_test.go b/server/cmd/mmctl/commands/system_test.go index aabbafeebb..c42d275caf 100644 --- a/server/cmd/mmctl/commands/system_test.go +++ b/server/cmd/mmctl/commands/system_test.go @@ -238,7 +238,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() { printer.SetFormat(printer.FormatPlain) s.T().Cleanup(func() { printer.SetFormat(printer.FormatJSON) }) - s.Run("Download support packet with default filename", func() { + s.Run("Download Support Packet with default filename", func() { printer.Clean() s.T().Cleanup(cleanupSupportPacket(s.T())) @@ -274,7 +274,7 @@ func (s *MmctlUnitTestSuite) TestSupportPacketCmdF() { s.True(found) }) - s.Run("Download support packet with custom filename", func() { + s.Run("Download Support Packet with custom filename", func() { printer.Clean() data := []byte("some bytes") diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml index 1869e00d71..e233016f1f 100644 --- a/server/docker-compose.yaml +++ b/server/docker-compose.yaml @@ -133,6 +133,8 @@ services: - "RUN_SERVER_IN_BACKGROUND=false" - "MM_CLUSTERSETTINGS_ENABLE=true" - "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster" + - "MM_LOGSETTINGS_FILELOCATION=./logs/leader" + - "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/leader" networks: - mm-test depends_on: @@ -169,6 +171,8 @@ services: - "RUN_SERVER_IN_BACKGROUND=false" - "MM_CLUSTERSETTINGS_ENABLE=true" - "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster" + - "MM_LOGSETTINGS_FILELOCATION=./logs/follower" + - "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/follower" networks: - mm-test depends_on: @@ -205,6 +209,8 @@ services: - "RUN_SERVER_IN_BACKGROUND=false" - "MM_CLUSTERSETTINGS_ENABLE=true" - "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster" + - "MM_LOGSETTINGS_FILELOCATION=./logs/follower2" + - "MM_NOTIFICATIONLOGSETTINGSS_FILELOCATION=./logs/follower2" networks: - mm-test depends_on: diff --git a/server/einterfaces/cluster.go b/server/einterfaces/cluster.go index 05c2b6cc32..c92248b536 100644 --- a/server/einterfaces/cluster.go +++ b/server/einterfaces/cluster.go @@ -5,6 +5,7 @@ package einterfaces import ( "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" ) type ClusterMessageHandler func(msg *model.ClusterMessage) @@ -27,6 +28,7 @@ type ClusterInterface interface { GetClusterStats() ([]*model.ClusterStats, *model.AppError) GetLogs(page, perPage int) ([]string, *model.AppError) QueryLogs(page, perPage int) (map[string][]string, *model.AppError) + GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) GetPluginStatuses() (model.PluginStatuses, *model.AppError) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError // WebConnCountForUser returns the number of active webconn connections diff --git a/server/einterfaces/mocks/ClusterInterface.go b/server/einterfaces/mocks/ClusterInterface.go index c049500429..4300fc76b0 100644 --- a/server/einterfaces/mocks/ClusterInterface.go +++ b/server/einterfaces/mocks/ClusterInterface.go @@ -9,6 +9,8 @@ import ( mock "github.com/stretchr/testify/mock" model "github.com/mattermost/mattermost/server/public/model" + + request "github.com/mattermost/mattermost/server/public/shared/request" ) // ClusterInterface is an autogenerated mock type for the ClusterInterface type @@ -36,6 +38,36 @@ func (_m *ClusterInterface) ConfigChanged(previousConfig *model.Config, newConfi return r0 } +// GenerateSupportPacket provides a mock function with given fields: rctx, options +func (_m *ClusterInterface) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) (map[string][]model.FileData, error) { + ret := _m.Called(rctx, options) + + if len(ret) == 0 { + panic("no return value specified for GenerateSupportPacket") + } + + var r0 map[string][]model.FileData + var r1 error + if rf, ok := ret.Get(0).(func(request.CTX, *model.SupportPacketOptions) (map[string][]model.FileData, error)); ok { + return rf(rctx, options) + } + if rf, ok := ret.Get(0).(func(request.CTX, *model.SupportPacketOptions) map[string][]model.FileData); ok { + r0 = rf(rctx, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]model.FileData) + } + } + + if rf, ok := ret.Get(1).(func(request.CTX, *model.SupportPacketOptions) error); ok { + r1 = rf(rctx, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetClusterId provides a mock function with given fields: func (_m *ClusterInterface) GetClusterId() string { ret := _m.Called() diff --git a/server/public/model/client4.go b/server/public/model/client4.go index a9265da14c..47cfc9eeb3 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -4636,7 +4636,7 @@ func (c *Client4) GetFileInfosForPostIncludeDeleted(ctx context.Context, postId // General/System Section -// GenerateSupportPacket downloads the generated support packet +// GenerateSupportPacket generates and downloads a Support Packet. func (c *Client4) GenerateSupportPacket(ctx context.Context) ([]byte, *Response, error) { r, err := c.DoAPIGet(ctx, c.systemRoute()+"/support_packet", "") if err != nil { diff --git a/server/public/model/cluster_message.go b/server/public/model/cluster_message.go index 649dc8987a..cb1c9f3230 100644 --- a/server/public/model/cluster_message.go +++ b/server/public/model/cluster_message.go @@ -45,16 +45,18 @@ const ( // m.ClusterEventMap in metrics/metrics.go file. // Gossip communication - ClusterGossipEventRequestGetLogs = "gossip_request_get_logs" - ClusterGossipEventResponseGetLogs = "gossip_response_get_logs" - ClusterGossipEventRequestGetClusterStats = "gossip_request_cluster_stats" - ClusterGossipEventResponseGetClusterStats = "gossip_response_cluster_stats" - ClusterGossipEventRequestGetPluginStatuses = "gossip_request_plugin_statuses" - ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses" - ClusterGossipEventRequestSaveConfig = "gossip_request_save_config" - ClusterGossipEventResponseSaveConfig = "gossip_response_save_config" - ClusterGossipEventRequestWebConnCount = "gossip_request_webconn_count" - ClusterGossipEventResponseWebConnCount = "gossip_response_webconn_count" + ClusterGossipEventRequestGetLogs = "gossip_request_get_logs" + ClusterGossipEventResponseGetLogs = "gossip_response_get_logs" + ClusterGossipEventRequestGenerateSupportPacket = "gossip_request_generate_support_packet" + ClusterGossipEventResponseGenerateSupportPacket = "gossip_response_generate_support_packet" + ClusterGossipEventRequestGetClusterStats = "gossip_request_cluster_stats" + ClusterGossipEventResponseGetClusterStats = "gossip_response_cluster_stats" + ClusterGossipEventRequestGetPluginStatuses = "gossip_request_plugin_statuses" + ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses" + ClusterGossipEventRequestSaveConfig = "gossip_request_save_config" + ClusterGossipEventResponseSaveConfig = "gossip_response_save_config" + ClusterGossipEventRequestWebConnCount = "gossip_request_webconn_count" + ClusterGossipEventResponseWebConnCount = "gossip_response_webconn_count" // SendTypes for ClusterMessage. ClusterSendBestEffort = "best_effort" diff --git a/server/public/model/support_packet.go b/server/public/model/support_packet.go index 8ec8e523d9..4acd844ede 100644 --- a/server/public/model/support_packet.go +++ b/server/public/model/support_packet.go @@ -8,6 +8,10 @@ import ( "io" ) +const ( + SupportPacketErrorFile = "warning.txt" +) + type SupportPacket struct { /* Build information */ diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index e357f14f43..1d44973b96 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -3310,7 +3310,7 @@ "combined_system_message.removed_from_team.one_you": "You were **removed from the team**.", "combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.", "combined_system_message.you": "You", - "commercial_support.description": "If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the support packet below that includes more details about your Mattermost environment.", + "commercial_support.description": "If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the Support Packet below that includes more details about your Mattermost environment.", "commercial_support.download_contents": "**Select your Support Packet contents to download**", "commercial_support.download_support_packet": "Download Support Packet", "commercial_support.title": "Commercial Support",