mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* [MM-66840] Add CPU cores and total memory to support packet Add system resource information to support packet diagnostics to help with troubleshooting and capacity analysis. * Clarify that memory is in MB * Add comment clarifying CPU/memory are host values Clarifies that the CPU cores and total memory values in the support packet represent the host machine's resources, not any container limits that may be configured in Docker or Kubernetes environments. --------- Co-authored-by: Hanzo Dev <[email protected]>
324 lines
9.5 KiB
Go
324 lines
9.5 KiB
Go
// 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"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
"github.com/hashicorp/go-multierror"
|
|
"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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
// Note: These values represent the host machine's resources, not any
|
|
// container limits (e.g., Docker or Kubernetes) that may be in effect.
|
|
d.Server.CPUCores = runtime.NumCPU()
|
|
totalMemoryBytes, err := getTotalMemory()
|
|
if err != nil {
|
|
rErr = multierror.Append(rErr, errors.Wrap(err, "error while getting total memory"))
|
|
}
|
|
d.Server.TotalMemoryMB = totalMemoryBytes / 1024 / 1024
|
|
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, e := cluster.GetClusterInfos()
|
|
if e != nil {
|
|
rErr = multierror.Append(rErr, errors.Wrap(e, "error while getting cluster infos"))
|
|
} else {
|
|
d.Cluster.NumberOfNodes = max(len(clusterInfo), 1) // clusterInfo is empty if the node is the only one in the cluster
|
|
}
|
|
}
|
|
|
|
/* 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
|
|
}
|
|
|
|
/* SAML */
|
|
if idpDescriptorURL := model.SafeDereference(ps.Config().SamlSettings.IdpDescriptorURL); idpDescriptorURL != "" {
|
|
d.SAML.ProviderType = detectSAMLProviderType(idpDescriptorURL)
|
|
}
|
|
|
|
/* Elastic Search */
|
|
if se := ps.SearchEngine.ElasticsearchEngine; se != nil {
|
|
d.ElasticSearch.Backend = *ps.Config().ElasticsearchSettings.Backend
|
|
d.ElasticSearch.ServerVersion = se.GetFullVersion()
|
|
d.ElasticSearch.ServerPlugins = se.GetPlugins()
|
|
if *ps.Config().ElasticsearchSettings.EnableIndexing {
|
|
appErr := se.TestConfig(rctx, ps.Config())
|
|
if appErr != nil {
|
|
d.ElasticSearch.Error = appErr.Error()
|
|
}
|
|
}
|
|
}
|
|
|
|
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, &model.SanitizeOptions{PartiallyRedactDataSources: true})
|
|
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
|
|
}
|
|
|
|
// detectSAMLProviderType attempts to identify the SAML provider type based on the IdpDescriptorURL.
|
|
// It returns "unknown" if the provider cannot be identified.
|
|
func detectSAMLProviderType(idpDescriptorURL string) string {
|
|
if idpDescriptorURL == "" {
|
|
return unknownDataPoint
|
|
}
|
|
|
|
// Normalize URL to lowercase for case-insensitive matching
|
|
normalizedURL := strings.ToLower(idpDescriptorURL)
|
|
|
|
// Check for common SAML provider patterns in the EntityID/IdpDescriptorURL
|
|
// Order matters: more specific patterns should come before generic ones
|
|
switch {
|
|
case strings.Contains(normalizedURL, "login.microsoftonline.com") || strings.Contains(normalizedURL, "sts.windows.net"):
|
|
return "Azure AD"
|
|
case strings.Contains(normalizedURL, ".okta.com") || strings.Contains(normalizedURL, ".oktapreview.com"):
|
|
return "Okta"
|
|
case strings.Contains(normalizedURL, ".auth0.com"):
|
|
return "Auth0"
|
|
case strings.Contains(normalizedURL, ".onelogin.com"):
|
|
return "OneLogin"
|
|
case strings.Contains(normalizedURL, "accounts.google.com"):
|
|
return "Google Workspace"
|
|
case strings.Contains(normalizedURL, "sso.jumpcloud.com"):
|
|
return "JumpCloud"
|
|
case strings.Contains(normalizedURL, "duo.com/saml2"):
|
|
return "Duo"
|
|
case strings.Contains(normalizedURL, ".centrify.com"):
|
|
return "Centrify"
|
|
case strings.Contains(normalizedURL, "/realms/"):
|
|
return "Keycloak"
|
|
case strings.Contains(normalizedURL, "/adfs/") || strings.Contains(normalizedURL, "/FederationMetadata/"):
|
|
return "ADFS"
|
|
case strings.Contains(normalizedURL, "shibboleth.net") || strings.Contains(normalizedURL, "/idp/shibboleth"):
|
|
return "Shibboleth"
|
|
default:
|
|
return unknownDataPoint
|
|
}
|
|
}
|