[MM-54288] Support Packet V2 (#29403)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
091d1bba8b
Коммит
8d4bf4bae0
@@ -21,6 +21,7 @@ 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/public/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
@@ -40,6 +41,23 @@ func (ps *PlatformService) Config() *model.Config {
|
||||
return ps.configStore.Get()
|
||||
}
|
||||
|
||||
// getSanitizedConfig gets the configuration without any secrets.
|
||||
func (ps *PlatformService) getSanitizedConfig(rctx request.CTX) *model.Config {
|
||||
cfg := ps.Config().Clone()
|
||||
|
||||
manifests, err := ps.getPluginManifests()
|
||||
if err != nil {
|
||||
// getPluginManifests might error, e.g. when plugins are disabled.
|
||||
// Sanitize all plugin settings in this case.
|
||||
rctx.Logger().Warn("Failed to get plugin manifests for config sanitization. Will sanitize all plugin settings.", mlog.Err(err))
|
||||
cfg.Sanitize(nil)
|
||||
} else {
|
||||
cfg.Sanitize(manifests)
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Registers a function with a given listener to be called when the config is reloaded and may have changed. The function
|
||||
// will be called with two arguments: the old config and the new config. AddConfigListener returns a unique ID
|
||||
// for the listener that can later be used to remove it.
|
||||
|
||||
@@ -20,6 +20,12 @@ func RegisterElasticsearchInterface(f func(*PlatformService) searchengine.Search
|
||||
elasticsearchInterface = f
|
||||
}
|
||||
|
||||
var ldapDiagnosticInterface func(*PlatformService) einterfaces.LdapDiagnosticInterface
|
||||
|
||||
func RegisterLdapDiagnosticInterface(f func(*PlatformService) einterfaces.LdapDiagnosticInterface) {
|
||||
ldapDiagnosticInterface = f
|
||||
}
|
||||
|
||||
var licenseInterface func(*PlatformService) einterfaces.LicenseInterface
|
||||
|
||||
func RegisterLicenseInterface(f func(*PlatformService) einterfaces.LicenseInterface) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
@@ -143,7 +144,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
*memoryConfig.MetricsSettings.Enable = true
|
||||
*memoryConfig.ServiceSettings.ListenAddress = "localhost:0"
|
||||
*memoryConfig.MetricsSettings.ListenAddress = "localhost:0"
|
||||
configStore.Set(memoryConfig)
|
||||
_, _, err = configStore.Set(memoryConfig)
|
||||
require.NoError(tb, err)
|
||||
|
||||
options = append(options, ConfigStore(configStore))
|
||||
|
||||
@@ -152,7 +154,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
Store: dbStore,
|
||||
}, options...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
require.NoError(tb, err)
|
||||
}
|
||||
|
||||
th := &TestHelper{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -16,6 +17,7 @@ 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/public/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
@@ -246,6 +248,44 @@ func (ps *PlatformService) GetNotificationLogFile(_ request.CTX) (*model.FileDat
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) GetAdvancedLogs(_ request.CTX) ([]*model.FileData, error) {
|
||||
advancedLoggingJSON := ps.Config().LogSettings.AdvancedLoggingJSON
|
||||
if utils.IsEmptyJSON(advancedLoggingJSON) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cfg := make(mlog.LoggerConfiguration)
|
||||
err := json.Unmarshal(advancedLoggingJSON, &cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid advanced logging configuration")
|
||||
}
|
||||
|
||||
var ret []*model.FileData
|
||||
for _, t := range cfg {
|
||||
if t.Type != "file" {
|
||||
continue
|
||||
}
|
||||
var fileOption struct {
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
if err := json.Unmarshal(t.Options, &fileOption); err != nil {
|
||||
return nil, errors.Wrap(err, "error decoding file target options")
|
||||
}
|
||||
data, err := os.ReadFile(fileOption.Filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read notifcation log file at path %s", fileOption.Filename)
|
||||
}
|
||||
|
||||
fileName := path.Base(fileOption.Filename)
|
||||
ret = append(ret, &model.FileData{
|
||||
Filename: fileName,
|
||||
Body: data,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func isLogFilteredByLevel(logFilter *model.LogFilter, entry *model.LogEntry) bool {
|
||||
logLevels := logFilter.LogLevels
|
||||
if len(logLevels) == 0 {
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -104,3 +109,89 @@ func TestGetNotificationLogFile(t *testing.T) {
|
||||
assert.Equal(t, "notifications.log", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
|
||||
func TestGetAdvancedLogs(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("log messanges from std and LDAP level get returned", func(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "logs")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(dir)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
optLDAP := map[string]string{
|
||||
"filename": path.Join(dir, "ldap.log"),
|
||||
}
|
||||
dataLDAP, err := json.Marshal(optLDAP)
|
||||
require.NoError(t, err)
|
||||
|
||||
optStd := map[string]string{
|
||||
"filename": path.Join(dir, "std.log"),
|
||||
}
|
||||
dataStd, err := json.Marshal(optStd)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := mlog.LoggerConfiguration{
|
||||
"ldap-file": mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Levels: []mlog.Level{
|
||||
mlog.LvlLDAPError,
|
||||
mlog.LvlLDAPWarn,
|
||||
mlog.LvlLDAPInfo,
|
||||
mlog.LvlLDAPDebug,
|
||||
},
|
||||
Options: dataLDAP,
|
||||
},
|
||||
"std": mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Levels: []mlog.Level{
|
||||
mlog.LvlError,
|
||||
},
|
||||
Options: dataStd,
|
||||
},
|
||||
}
|
||||
cfgData, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Service.UpdateConfig(func(c *model.Config) {
|
||||
c.LogSettings.AdvancedLoggingJSON = cfgData
|
||||
})
|
||||
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
|
||||
th.Service.Logger().Error("Some Error")
|
||||
err = th.Service.Logger().Flush()
|
||||
require.NoError(t, err)
|
||||
|
||||
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fileDatas, 2)
|
||||
|
||||
// Check the order of the log files
|
||||
var ldapIndex = 0
|
||||
var stdIndex = 1
|
||||
if fileDatas[1].Filename == "ldap.log" {
|
||||
ldapIndex = 1
|
||||
stdIndex = 0
|
||||
}
|
||||
|
||||
assert.Equal(t, "ldap.log", fileDatas[ldapIndex].Filename)
|
||||
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[ldapIndex].Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
|
||||
|
||||
assert.Equal(t, "std.log", fileDatas[stdIndex].Filename)
|
||||
testlib.AssertLog(t, bytes.NewBuffer(fileDatas[stdIndex].Body), mlog.LvlError.Name, "Some Error")
|
||||
})
|
||||
// Disable AdvancedLoggingJSON
|
||||
th.Service.UpdateConfig(func(c *model.Config) {
|
||||
c.LogSettings.AdvancedLoggingJSON = nil
|
||||
})
|
||||
t.Run("No logs returned when AdvancedLoggingJSON is empty", func(t *testing.T) {
|
||||
// Confirm no logs get returned
|
||||
fileDatas, err := th.Service.GetAdvancedLogs(th.Context)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fileDatas, 0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"net/http/pprof"
|
||||
"path"
|
||||
"runtime"
|
||||
rpprof "runtime/pprof"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
@@ -25,7 +23,6 @@ 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"
|
||||
)
|
||||
@@ -259,52 +256,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ package platform
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/maphash"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -88,6 +90,8 @@ type PlatformService struct {
|
||||
searchConfigListenerId string
|
||||
searchLicenseListenerId string
|
||||
|
||||
ldapDiagnostic einterfaces.LdapDiagnosticInterface
|
||||
|
||||
Jobs *jobs.JobServer
|
||||
|
||||
hubs []*Hub
|
||||
@@ -459,6 +463,10 @@ func (ps *PlatformService) initEnterprise() {
|
||||
ps.SearchEngine.RegisterElasticsearchEngine(elasticsearchInterface(ps))
|
||||
}
|
||||
|
||||
if ldapDiagnosticInterface != nil {
|
||||
ps.ldapDiagnostic = ldapDiagnosticInterface(ps)
|
||||
}
|
||||
|
||||
if licenseInterface != nil {
|
||||
ps.licenseManager = licenseInterface(ps)
|
||||
}
|
||||
@@ -547,6 +555,29 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) getPluginManifests() ([]*model.Manifest, error) {
|
||||
if ps.pluginEnv == nil {
|
||||
return nil, errors.New("plugin environment not initialized")
|
||||
}
|
||||
|
||||
pluginsEnvironment := ps.pluginEnv.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("getPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get list of available plugins: %w", err)
|
||||
}
|
||||
|
||||
manifests := make([]*model.Manifest, len(plugins))
|
||||
for i := range plugins {
|
||||
manifests[i] = plugins[i].Manifest
|
||||
}
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) FileBackend() filestore.FileBackend {
|
||||
return ps.filestore
|
||||
}
|
||||
@@ -554,3 +585,17 @@ func (ps *PlatformService) FileBackend() filestore.FileBackend {
|
||||
func (ps *PlatformService) ExportFileBackend() filestore.FileBackend {
|
||||
return ps.exportFilestore
|
||||
}
|
||||
|
||||
func (ps *PlatformService) LdapDiagnostic() einterfaces.LdapDiagnosticInterface {
|
||||
return ps.ldapDiagnostic
|
||||
}
|
||||
|
||||
// DatabaseTypeAndSchemaVersion returns the Database type (postgres or mysql) and current version of the schema
|
||||
func (ps *PlatformService) DatabaseTypeAndSchemaVersion() (string, string, error) {
|
||||
schemaVersion, err := ps.Store.GetDBSchemaVersion()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return model.SafeDereference(ps.Config().SqlSettings.DriverName), strconv.Itoa(schemaVersion), nil
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -231,3 +233,20 @@ func TestSetTelemetryId(t *testing.T) {
|
||||
require.Equal(t, clientConfig["DiagnosticId"], id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
databaseType, schemaVersion, err := th.Service.DatabaseTypeAndSchemaVersion()
|
||||
require.NoError(t, err)
|
||||
if *th.Service.Config().SqlSettings.DriverName == model.DatabaseDriverPostgres {
|
||||
assert.Equal(t, "postgres", databaseType)
|
||||
} else {
|
||||
assert.Equal(t, "mysql", databaseType)
|
||||
}
|
||||
|
||||
// It's hard to check wheather the schema version is correct or not.
|
||||
// So, we just check if it's greater than 1.
|
||||
assert.GreaterOrEqual(t, schemaVersion, strconv.Itoa(1))
|
||||
}
|
||||
|
||||
259
server/channels/app/platform/support_packet.go
Обычный файл
259
server/channels/app/platform/support_packet.go
Обычный файл
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"runtime"
|
||||
rpprof "runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
const (
|
||||
envVarInstallType = "MM_INSTALL_TYPE"
|
||||
unknownDataPoint = "unknown"
|
||||
)
|
||||
|
||||
func (ps *PlatformService) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) ([]model.FileData, error) {
|
||||
functions := map[string]func(request.CTX) (*model.FileData, error){
|
||||
"diagnostics": ps.getSupportPacketDiagnostics,
|
||||
"config": ps.getSanitizedConfigFile,
|
||||
"cpu profile": ps.getCPUProfile,
|
||||
"heap profile": ps.getHeapProfile,
|
||||
"goroutines": ps.getGoroutineProfile,
|
||||
}
|
||||
|
||||
if options != nil && options.IncludeLogs {
|
||||
functions["mattermost log"] = ps.GetLogFile
|
||||
functions["notification log"] = ps.GetNotificationLogFile
|
||||
}
|
||||
|
||||
var (
|
||||
fileDatas []model.FileData
|
||||
rErr *multierror.Error
|
||||
)
|
||||
|
||||
for name, fn := range functions {
|
||||
fileData, err := fn(rctx)
|
||||
if err != nil {
|
||||
rctx.Logger().Error("Failed to generate file for Support Packet",
|
||||
mlog.String("file", name),
|
||||
mlog.Err(err),
|
||||
)
|
||||
rErr = multierror.Append(rErr, err)
|
||||
}
|
||||
|
||||
if fileData != nil {
|
||||
fileDatas = append(fileDatas, *fileData)
|
||||
}
|
||||
}
|
||||
|
||||
if options != nil && options.IncludeLogs {
|
||||
advancedLogs, err := ps.GetAdvancedLogs(rctx)
|
||||
if err != nil {
|
||||
rctx.Logger().Error("Failed to read advanced log files for Support Packet", mlog.Err(err))
|
||||
rErr = multierror.Append(rErr, err)
|
||||
}
|
||||
|
||||
for _, log := range advancedLogs {
|
||||
fileDatas = append(fileDatas, *log)
|
||||
}
|
||||
}
|
||||
|
||||
return fileDatas, rErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model.FileData, error) {
|
||||
var (
|
||||
rErr *multierror.Error
|
||||
err error
|
||||
d model.SupportPacketDiagnostics
|
||||
)
|
||||
|
||||
d.Version = model.CurrentSupportPacketVersion
|
||||
|
||||
/* License */
|
||||
if license := ps.License(); license != nil {
|
||||
d.License.Company = license.Customer.Company
|
||||
d.License.Users = model.SafeDereference(license.Features.Users)
|
||||
d.License.SkuShortName = license.SkuShortName
|
||||
d.License.IsTrial = license.IsTrial
|
||||
d.License.IsGovSKU = license.IsGovSku
|
||||
}
|
||||
|
||||
/* Server */
|
||||
d.Server.OS = runtime.GOOS
|
||||
d.Server.Architecture = runtime.GOARCH
|
||||
d.Server.Hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting hostname"))
|
||||
}
|
||||
d.Server.Version = model.CurrentVersion
|
||||
d.Server.BuildHash = model.BuildHash
|
||||
installationType := os.Getenv(envVarInstallType)
|
||||
if installationType == "" {
|
||||
installationType = unknownDataPoint
|
||||
}
|
||||
d.Server.InstallationType = installationType
|
||||
|
||||
/* Config */
|
||||
d.Config.Source = ps.DescribeConfig()
|
||||
|
||||
/* DB */
|
||||
d.Database.Type, d.Database.SchemaVersion, err = ps.DatabaseTypeAndSchemaVersion()
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting DB type and schema version"))
|
||||
}
|
||||
|
||||
databaseVersion, err := ps.Store.GetDbVersion(false)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
|
||||
} else {
|
||||
d.Database.Version = databaseVersion
|
||||
}
|
||||
d.Database.MasterConnectios = ps.Store.TotalMasterDbConnections()
|
||||
d.Database.ReplicaConnectios = ps.Store.TotalReadDbConnections()
|
||||
d.Database.SearchConnections = ps.Store.TotalSearchDbConnections()
|
||||
|
||||
/* File store */
|
||||
d.FileStore.Status = model.StatusOk
|
||||
err = ps.FileBackend().TestConnection()
|
||||
if err != nil {
|
||||
d.FileStore.Status = model.StatusFail
|
||||
d.FileStore.Error = err.Error()
|
||||
}
|
||||
d.FileStore.Driver = ps.FileBackend().DriverName()
|
||||
|
||||
/* Websockets */
|
||||
d.Websocket.Connections = ps.TotalWebsocketConnections()
|
||||
|
||||
/* Cluster */
|
||||
if cluster := ps.Cluster(); cluster != nil {
|
||||
d.Cluster.ID = cluster.GetClusterId()
|
||||
clusterInfo := cluster.GetClusterInfos()
|
||||
d.Cluster.NumberOfNodes = len(clusterInfo)
|
||||
}
|
||||
|
||||
/* LDAP */
|
||||
if ldap := ps.LdapDiagnostic(); ldap != nil && (*ps.Config().LdapSettings.Enable || *ps.Config().LdapSettings.EnableSync) {
|
||||
d.LDAP.Status = model.StatusOk
|
||||
appErr := ldap.RunTest(rctx)
|
||||
if appErr != nil {
|
||||
d.LDAP.Status = model.StatusFail
|
||||
d.LDAP.Error = appErr.Error()
|
||||
}
|
||||
|
||||
severName, serverVersion := unknownDataPoint, unknownDataPoint
|
||||
// Only if the LDAP test was successful, try to get the LDAP server info
|
||||
if d.LDAP.Status == model.StatusOk {
|
||||
severName, serverVersion, err = ldap.GetVendorNameAndVendorVersion(rctx)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
|
||||
}
|
||||
|
||||
if severName == "" {
|
||||
severName = unknownDataPoint
|
||||
}
|
||||
if serverVersion == "" {
|
||||
serverVersion = unknownDataPoint
|
||||
}
|
||||
}
|
||||
d.LDAP.ServerName = severName
|
||||
d.LDAP.ServerVersion = serverVersion
|
||||
}
|
||||
|
||||
/* Elastic Search */
|
||||
if se := ps.SearchEngine.ElasticsearchEngine; se != nil {
|
||||
d.ElasticSearch.ServerVersion = se.GetFullVersion()
|
||||
d.ElasticSearch.ServerPlugins = se.GetPlugins()
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(&d)
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
|
||||
}
|
||||
|
||||
fileData := &model.FileData{
|
||||
Filename: "diagnostics.yaml",
|
||||
Body: b,
|
||||
}
|
||||
return fileData, rErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) {
|
||||
config := ps.getSanitizedConfig(rctx)
|
||||
spConfig := model.SupportPacketConfig{
|
||||
Config: config,
|
||||
FeatureFlags: *config.FeatureFlags,
|
||||
}
|
||||
sanitizedConfigPrettyJSON, err := json.MarshalIndent(spConfig, "", " ")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sanitized config into json")
|
||||
}
|
||||
|
||||
fileData := &model.FileData{
|
||||
Filename: "sanitized_config.json",
|
||||
Body: sanitizedConfigPrettyJSON,
|
||||
}
|
||||
return fileData, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) getCPUProfile(_ 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) getHeapProfile(_ 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) getGoroutineProfile(_ 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
|
||||
}
|
||||
398
server/channels/app/platform/support_packet_test.go
Обычный файл
398
server/channels/app/platform/support_packet_test.go
Обычный файл
@@ -0,0 +1,398 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
emocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
fmocks "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
|
||||
)
|
||||
|
||||
func TestGenerateSupportPacket(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(dir)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.LogSettings.FileLocation = dir
|
||||
*cfg.NotificationLogSettings.FileLocation = dir
|
||||
})
|
||||
|
||||
logLocation := config.GetLogFileLocation(dir)
|
||||
notificationsLogLocation := config.GetNotificationsLogFileLocation(dir)
|
||||
|
||||
genMockLogFiles := func() {
|
||||
d1 := []byte("hello\ngo\n")
|
||||
genErr := os.WriteFile(logLocation, d1, 0600)
|
||||
require.NoError(t, genErr)
|
||||
genErr = os.WriteFile(notificationsLogLocation, d1, 0600)
|
||||
require.NoError(t, genErr)
|
||||
}
|
||||
genMockLogFiles()
|
||||
|
||||
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
|
||||
var rFileNames []string
|
||||
for _, fileData := range fileDatas {
|
||||
require.NotNil(t, fileData)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
|
||||
rFileNames = append(rFileNames, fileData.Filename)
|
||||
}
|
||||
return rFileNames
|
||||
}
|
||||
|
||||
expectedFileNames := []string{
|
||||
"diagnostics.yaml",
|
||||
"sanitized_config.json",
|
||||
"cpu.prof",
|
||||
"heap.prof",
|
||||
"goroutines",
|
||||
}
|
||||
|
||||
expectedFileNamesWithLogs := append(expectedFileNames, []string{
|
||||
"mattermost.log",
|
||||
"notifications.log",
|
||||
}...)
|
||||
|
||||
var fileDatas []model.FileData
|
||||
|
||||
t.Run("generate Support Packet with logs", func(t *testing.T) {
|
||||
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("generate Support Packet without logs", func(t *testing.T) {
|
||||
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("remove the log files and ensure that an error is returned", func(t *testing.T) {
|
||||
err = os.Remove(logLocation)
|
||||
require.NoError(t, err)
|
||||
err = os.Remove(notificationsLogLocation)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(genMockLogFiles)
|
||||
|
||||
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed read mattermost log file")
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
||||
})
|
||||
|
||||
t.Run("with advanced logs", func(t *testing.T) {
|
||||
optLDAP := map[string]string{
|
||||
"filename": path.Join(dir, "ldap.log"),
|
||||
}
|
||||
dataLDAP, err := json.Marshal(optLDAP)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := mlog.LoggerConfiguration{
|
||||
"ldap-file": mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Levels: []mlog.Level{
|
||||
mlog.LvlLDAPError,
|
||||
mlog.LvlLDAPWarn,
|
||||
mlog.LvlLDAPInfo,
|
||||
mlog.LvlLDAPDebug,
|
||||
},
|
||||
Options: dataLDAP,
|
||||
},
|
||||
}
|
||||
cfgData, err := json.Marshal(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Service.UpdateConfig(func(c *model.Config) {
|
||||
c.LogSettings.AdvancedLoggingJSON = cfgData
|
||||
})
|
||||
|
||||
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
|
||||
err = th.Service.Logger().Flush()
|
||||
require.NoError(t, err)
|
||||
|
||||
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
||||
IncludeLogs: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
rFileNames := getFileNames(t, fileDatas)
|
||||
|
||||
assert.ElementsMatch(t, append(expectedFileNamesWithLogs, "ldap.log"), rFileNames)
|
||||
|
||||
found := false
|
||||
for _, fileData := range fileDatas {
|
||||
if fileData.Filename == "ldap.log" {
|
||||
testlib.AssertLog(t, bytes.NewBuffer(fileData.Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSupportPacketDiagnostics(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Setenv(envVarInstallType, "docker")
|
||||
|
||||
licenseUsers := 100
|
||||
license := model.NewTestLicense("ldap")
|
||||
license.SkuShortName = model.LicenseShortSkuEnterprise
|
||||
license.Features.Users = model.NewPointer(licenseUsers)
|
||||
ok := th.Service.SetLicense(license)
|
||||
require.True(t, ok)
|
||||
|
||||
getDiagnostics := func(t *testing.T) *model.SupportPacketDiagnostics {
|
||||
t.Helper()
|
||||
|
||||
fileData, err := th.Service.getSupportPacketDiagnostics(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "diagnostics.yaml", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var d model.SupportPacketDiagnostics
|
||||
require.NoError(t, yaml.Unmarshal(fileData.Body, &d))
|
||||
return &d
|
||||
}
|
||||
|
||||
t.Run("Happy path", func(t *testing.T) {
|
||||
d := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, 1, d.Version)
|
||||
|
||||
/* License */
|
||||
assert.Equal(t, "My awesome Company", d.License.Company)
|
||||
assert.Equal(t, licenseUsers, d.License.Users)
|
||||
assert.Equal(t, model.LicenseShortSkuEnterprise, d.License.SkuShortName)
|
||||
assert.Equal(t, false, d.License.IsTrial)
|
||||
assert.Equal(t, false, d.License.IsGovSKU)
|
||||
|
||||
/* Server information */
|
||||
assert.NotEmpty(t, d.Server.OS)
|
||||
assert.NotEmpty(t, d.Server.Architecture)
|
||||
assert.NotEmpty(t, d.Server.Hostname)
|
||||
assert.Equal(t, model.CurrentVersion, d.Server.Version)
|
||||
// BuildHash is not present in tests
|
||||
assert.Equal(t, "docker", d.Server.InstallationType)
|
||||
|
||||
/* Config */
|
||||
assert.Equal(t, "memory://", d.Config.Source)
|
||||
|
||||
/* DB */
|
||||
assert.NotEmpty(t, d.Database.Type)
|
||||
assert.NotEmpty(t, d.Database.Version)
|
||||
assert.NotEmpty(t, d.Database.SchemaVersion)
|
||||
assert.NotZero(t, d.Database.MasterConnectios)
|
||||
assert.Zero(t, d.Database.ReplicaConnectios)
|
||||
assert.Zero(t, d.Database.SearchConnections)
|
||||
|
||||
/* File store */
|
||||
assert.Equal(t, "OK", d.FileStore.Status)
|
||||
assert.Empty(t, d.FileStore.Error)
|
||||
assert.Equal(t, "local", d.FileStore.Driver)
|
||||
|
||||
/* Websockets */
|
||||
assert.Zero(t, d.Websocket.Connections)
|
||||
|
||||
/* Cluster */
|
||||
assert.Empty(t, d.Cluster.ID)
|
||||
assert.Zero(t, d.Cluster.NumberOfNodes)
|
||||
|
||||
/* LDAP */
|
||||
assert.Empty(t, d.LDAP.Status)
|
||||
assert.Empty(t, d.LDAP.Error)
|
||||
assert.Empty(t, d.LDAP.ServerName)
|
||||
assert.Empty(t, d.LDAP.ServerVersion)
|
||||
|
||||
/* Elastic Search */
|
||||
assert.Empty(t, d.ElasticSearch.ServerVersion)
|
||||
assert.Empty(t, d.ElasticSearch.ServerPlugins)
|
||||
})
|
||||
|
||||
t.Run("filestore fails", func(t *testing.T) {
|
||||
fb := &fmocks.FileBackend{}
|
||||
err := SetFileStore(fb)(th.Service)
|
||||
require.NoError(t, err)
|
||||
fb.On("DriverName").Return("mock")
|
||||
fb.On("TestConnection").Return(errors.New("all broken"))
|
||||
|
||||
packet := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, "FAIL", packet.FileStore.Status)
|
||||
assert.Equal(t, "all broken", packet.FileStore.Error)
|
||||
assert.Equal(t, "mock", packet.FileStore.Driver)
|
||||
})
|
||||
|
||||
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapDiagnosticInterface{}
|
||||
th.Service.ldapDiagnostic = ldapMock
|
||||
|
||||
packet := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, "", packet.LDAP.ServerName)
|
||||
assert.Equal(t, "", packet.LDAP.ServerVersion)
|
||||
})
|
||||
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.LdapSettings.EnableSync = model.NewPointer(true)
|
||||
})
|
||||
|
||||
t.Run("no LDAP vendor info found", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapDiagnosticInterface{}
|
||||
ldapMock.On(
|
||||
"GetVendorNameAndVendorVersion",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return("", "", nil)
|
||||
ldapMock.On(
|
||||
"RunTest",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return(nil)
|
||||
th.Service.ldapDiagnostic = ldapMock
|
||||
|
||||
packet := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, "OK", packet.LDAP.Status)
|
||||
assert.Empty(t, packet.LDAP.Error)
|
||||
assert.Equal(t, "unknown", packet.LDAP.ServerName)
|
||||
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
|
||||
})
|
||||
|
||||
t.Run("found LDAP vendor info", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapDiagnosticInterface{}
|
||||
ldapMock.On(
|
||||
"GetVendorNameAndVendorVersion",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return("some vendor", "v1.0.0", nil)
|
||||
ldapMock.On(
|
||||
"RunTest",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return(nil)
|
||||
th.Service.ldapDiagnostic = ldapMock
|
||||
|
||||
packet := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, "OK", packet.LDAP.Status)
|
||||
assert.Empty(t, packet.LDAP.Error)
|
||||
assert.Equal(t, "some vendor", packet.LDAP.ServerName)
|
||||
assert.Equal(t, "v1.0.0", packet.LDAP.ServerVersion)
|
||||
})
|
||||
|
||||
t.Run("LDAP test fails", func(t *testing.T) {
|
||||
ldapMock := &emocks.LdapDiagnosticInterface{}
|
||||
ldapMock.On(
|
||||
"GetVendorNameAndVendorVersion",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return("some vendor", "v1.0.0", nil)
|
||||
ldapMock.On(
|
||||
"RunTest",
|
||||
mock.AnythingOfType("*request.Context"),
|
||||
).Return(model.NewAppError("", "some error", nil, "", 0))
|
||||
th.Service.ldapDiagnostic = ldapMock
|
||||
|
||||
packet := getDiagnostics(t)
|
||||
|
||||
assert.Equal(t, "FAIL", packet.LDAP.Status)
|
||||
assert.Equal(t, "some error", packet.LDAP.Error)
|
||||
assert.Equal(t, "unknown", packet.LDAP.ServerName)
|
||||
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSanitizedConfigFile(t *testing.T) {
|
||||
t.Setenv("MM_FEATUREFLAGS_TestFeature", "true")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewPointer("example.com")
|
||||
})
|
||||
|
||||
// Happy path where we have a sanitized config file with no err
|
||||
fileData, err := th.Service.getSanitizedConfigFile(th.Context)
|
||||
require.NotNil(t, fileData)
|
||||
assert.Equal(t, "sanitized_config.json", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var config model.Config
|
||||
err = json.Unmarshal(fileData.Body, &config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure sensitive fields are redacted
|
||||
assert.Equal(t, model.FakeSetting, *config.SqlSettings.DataSource)
|
||||
|
||||
// Ensure non-sensitive fields are present
|
||||
assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections)
|
||||
|
||||
// Ensure feature flags are present
|
||||
assert.Equal(t, "true", config.FeatureFlags.TestFeature)
|
||||
}
|
||||
|
||||
func TestGetCPUProfile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
fileData, err := th.Service.getCPUProfile(th.Context)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "cpu.prof", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
|
||||
func TestGetHeapProfile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
fileData, err := th.Service.getHeapProfile(th.Context)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "heap.prof", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
|
||||
func TestGetGoroutineProfile(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
fileData, err := th.Service.getGoroutineProfile(th.Context)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "goroutines", fileData.Filename)
|
||||
assert.Positive(t, len(fileData.Body))
|
||||
}
|
||||
Ссылка в новой задаче
Block a user