[MM-54593] HA aware Support Packet (#27598)

This commit is contained in:
Ben Schumacher
2024-08-03 16:11:13 +02:00
committed by GitHub
parent 3f4b8e8137
commit 1158e6358c
24 changed files with 377 additions and 300 deletions

View File

@@ -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

View File

@@ -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');
};

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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"})

View File

@@ -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")

View File

@@ -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:

View File

@@ -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

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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"

View File

@@ -8,6 +8,10 @@ import (
"io"
)
const (
SupportPacketErrorFile = "warning.txt"
)
type SupportPacket struct {
/* Build information */

View File

@@ -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",