diff --git a/server/channels/app/support_packet.go b/server/channels/app/support_packet.go index c1b83cb0da..09445be532 100644 --- a/server/channels/app/support_packet.go +++ b/server/channels/app/support_packet.go @@ -5,15 +5,16 @@ package app import ( "encoding/json" - "fmt" "os" "runtime" "strings" + "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "gopkg.in/yaml.v2" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/config" ) @@ -25,21 +26,21 @@ func (a *App) GenerateSupportPacket() []model.FileData { fileDatas := []model.FileData{} // A array of the functions that we can iterate through since they all have the same return value - functions := []func() (*model.FileData, string){ - a.generateSupportPacketYaml, - a.createPluginsFile, - a.createSanitizedConfigFile, - a.getMattermostLog, - a.getNotificationsLog, + functions := map[string]func() (*model.FileData, error){ + "support package": a.generateSupportPacketYaml, + "plugins": a.createPluginsFile, + "config": a.createSanitizedConfigFile, + "mattermost log": a.getMattermostLog, + "notification log": a.getNotificationsLog, } - for _, fn := range functions { - fileData, warning := fn() - - if fileData != nil { + for name, fn := range functions { + fileData, err := fn() + if err != nil { + mlog.Error("Failed to generate file for support package", mlog.Err(err), mlog.String("file", name)) + warnings = append(warnings, err.Error()) + } else if fileData != nil { fileDatas = append(fileDatas, *fileData) - } else { - warnings = append(warnings, warning) } } @@ -55,7 +56,9 @@ func (a *App) GenerateSupportPacket() []model.FileData { return fileDatas } -func (a *App) generateSupportPacketYaml() (*model.FileData, string) { +func (a *App) generateSupportPacketYaml() (*model.FileData, error) { + var rErr error + // Here we are getting information regarding Elastic Search var elasticServerVersion string var elasticServerPlugins []string @@ -73,26 +76,41 @@ func (a *App) generateSupportPacketYaml() (*model.FileData, string) { // Here we are getting information regarding the database (mysql/postgres + current schema version) databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion() - databaseVersion, _ := a.Srv().Store().GetDbVersion(false) uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{}) if err != nil { - return nil, errors.Wrap(err, "error while getting user count").Error() + rErr = multierror.Append(errors.Wrap(err, "error while getting user count")) } - analytics, err := a.GetAnalytics("standard", "") - if analytics == nil { - return nil, errors.Wrap(err, "error while getting analytics").Error() + dataRetentionJobs, err := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeDataRetention, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs")) + } + messageExportJobs, err := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeMessageExport, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs")) + } + elasticPostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeElasticsearchPostIndexing, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs")) + } + elasticPostAggregationJobs, _ := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeElasticsearchPostAggregation, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs")) + } + blevePostIndexingJobs, _ := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeBlevePostIndexing, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs")) + } + ldapSyncJobs, err := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeLdapSync, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs")) + } + migrationJobs, err := a.Srv().Store().Job().GetAllByTypePage(model.JobTypeMigrations, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs")) } - - elasticPostIndexing, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_indexing", 0, 2) - elasticPostAggregation, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_aggregation", 0, 2) - ldapSyncJobs, _ := a.Srv().Store().Job().GetAllByTypePage("ldap_sync", 0, 2) - messageExport, _ := a.Srv().Store().Job().GetAllByTypePage("message_export", 0, 2) - dataRetentionJobs, _ := a.Srv().Store().Job().GetAllByTypePage("data_retention", 0, 2) - complianceJobs, _ := a.Srv().Store().Job().GetAllByTypePage("compliance", 0, 2) - migrationJobs, _ := a.Srv().Store().Job().GetAllByTypePage("migrations", 0, 2) licenseTo := "" supportedUsers := 0 @@ -103,140 +121,128 @@ func (a *App) generateSupportPacketYaml() (*model.FileData, string) { // Creating the struct for support packet yaml file supportPacket := model.SupportPacket{ - LicenseTo: licenseTo, - ServerOS: runtime.GOOS, - ServerArchitecture: runtime.GOARCH, - ServerVersion: model.CurrentVersion, - BuildHash: model.BuildHash, - DatabaseType: databaseType, - DatabaseVersion: databaseVersion, - DatabaseSchemaVersion: databaseSchemaVersion, - LdapVendorName: vendorName, - LdapVendorVersion: vendorVersion, - ElasticServerVersion: elasticServerVersion, - ElasticServerPlugins: elasticServerPlugins, - ActiveUsers: int(uniqueUserCount), - LicenseSupportedUsers: supportedUsers, - 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), - ElasticPostIndexingJobs: elasticPostIndexing, - ElasticPostAggregationJobs: elasticPostAggregation, - LdapSyncJobs: ldapSyncJobs, - MessageExportJobs: messageExport, + LicenseTo: licenseTo, + ServerOS: runtime.GOOS, + ServerArchitecture: runtime.GOARCH, + ServerVersion: model.CurrentVersion, + BuildHash: model.BuildHash, + DatabaseType: databaseType, + DatabaseVersion: databaseVersion, + DatabaseSchemaVersion: databaseSchemaVersion, + LdapVendorName: vendorName, + LdapVendorVersion: vendorVersion, + ElasticServerVersion: elasticServerVersion, + ElasticServerPlugins: elasticServerPlugins, + ActiveUsers: int(uniqueUserCount), + LicenseSupportedUsers: supportedUsers, + + // Jobs DataRetentionJobs: dataRetentionJobs, - ComplianceJobs: complianceJobs, + MessageExportJobs: messageExportJobs, + ElasticPostIndexingJobs: elasticPostIndexingJobs, + ElasticPostAggregationJobs: elasticPostAggregationJobs, + BlevePostIndexingJobs: blevePostIndexingJobs, + LdapSyncJobs: ldapSyncJobs, MigrationJobs: migrationJobs, } + analytics, appErr := a.GetAnalytics("standard", "") + 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 { + supportPacket.TotalChannels = int(analytics[0].Value) + int(analytics[1].Value) + supportPacket.TotalPosts = int(analytics[2].Value) + supportPacket.TotalTeams = int(analytics[4].Value) + supportPacket.WebsocketConnections = int(analytics[5].Value) + supportPacket.MasterDbConnections = int(analytics[6].Value) + supportPacket.ReplicaDbConnections = int(analytics[7].Value) + supportPacket.DailyActiveUsers = int(analytics[8].Value) + supportPacket.MonthlyActiveUsers = int(analytics[9].Value) + supportPacket.InactiveUserCount = int(analytics[10].Value) + } + // Marshal to a Yaml File supportPacketYaml, err := yaml.Marshal(&supportPacket) - if err == nil { - fileData := model.FileData{ - Filename: "support_packet.yaml", - Body: supportPacketYaml, - } - return &fileData, "" + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "failed to marshal support package into yaml")) } - warning := fmt.Sprintf("yaml.Marshal(&supportPacket) Error: %s", err.Error()) - return nil, warning + fileData := &model.FileData{ + Filename: "support_packet.yaml", + Body: supportPacketYaml, + } + return fileData, rErr } -func (a *App) createPluginsFile() (*model.FileData, string) { - var warning string - +func (a *App) createPluginsFile() (*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 { - pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") - if err == nil { - fileData := model.FileData{ - Filename: "plugins.json", - Body: pluginsPrettyJSON, - } - - return &fileData, "" - } - - warning = fmt.Sprintf("json.MarshalIndent(pluginsResponse) Error: %s", err.Error()) - } else { - warning = fmt.Sprintf("c.App.GetPlugins() Error: %s", appErr.Error()) + if appErr != nil { + return nil, errors.Wrap(appErr, "failed to get plugin list for support package") } - return nil, warning -} - -func (a *App) getNotificationsLog() (*model.FileData, string) { - var warning string - - // Getting notifications.log - if *a.Config().NotificationLogSettings.EnableFile { - // notifications.log - notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation) - - notificationsLogFileData, notificationsLogFileDataErr := os.ReadFile(notificationsLog) - - if notificationsLogFileDataErr == nil { - fileData := model.FileData{ - Filename: "notifications.log", - Body: notificationsLogFileData, - } - return &fileData, "" - } - - warning = fmt.Sprintf("os.ReadFile(notificationsLog) Error: %s", notificationsLogFileDataErr.Error()) - - } else { - warning = "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json" + pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal plugin list into json") } - return nil, warning + fileData := &model.FileData{ + Filename: "plugins.json", + Body: pluginsPrettyJSON, + } + return fileData, nil + } -func (a *App) getMattermostLog() (*model.FileData, string) { - var warning string - - // Getting mattermost.log - if *a.Config().LogSettings.EnableFile { - // mattermost.log - mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation) - - mattermostLogFileData, mattermostLogFileDataErr := os.ReadFile(mattermostLog) - - if mattermostLogFileDataErr == nil { - fileData := model.FileData{ - Filename: "mattermost.log", - Body: mattermostLogFileData, - } - return &fileData, "" - } - warning = fmt.Sprintf("os.ReadFile(mattermostLog) Error: %s", mattermostLogFileDataErr.Error()) - - } else { - warning = "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json" +func (a *App) getNotificationsLog() (*model.FileData, error) { + if !*a.Config().NotificationLogSettings.EnableFile { + return nil, errors.New("Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") } - return nil, warning + 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) createSanitizedConfigFile() (*model.FileData, string) { +func (a *App) getMattermostLog() (*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() (*model.FileData, error) { // Getting sanitized config, prettifying it, and then adding it to our file data array sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ") - if err == nil { - fileData := model.FileData{ - Filename: "sanitized_config.json", - Body: sanitizedConfigPrettyJSON, - } - return &fileData, "" + if err != nil { + return nil, errors.Wrap(err, "failed to sanitized config into json") } - warning := fmt.Sprintf("json.MarshalIndent(c.App.GetSanitizedConfig()) Error: %s", err.Error()) - return nil, warning + fileData := &model.FileData{ + Filename: "sanitized_config.json", + Body: sanitizedConfigPrettyJSON, + } + return fileData, nil } diff --git a/server/channels/app/support_packet_test.go b/server/channels/app/support_packet_test.go index 09e93e1ef8..f842e8681f 100644 --- a/server/channels/app/support_packet_test.go +++ b/server/channels/app/support_packet_test.go @@ -18,12 +18,12 @@ func TestCreatePluginsFile(t *testing.T) { th := Setup(t) defer th.TearDown() - // Happy path where we have a plugins file with no warning - fileData, warning := th.App.createPluginsFile() + // Happy path where we have a plugins file with no err + fileData, err := th.App.createPluginsFile() require.NotNil(t, fileData) assert.Equal(t, "plugins.json", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) // Turn off plugins so we can get an error th.App.UpdateConfig(func(cfg *model.Config) { @@ -31,9 +31,9 @@ func TestCreatePluginsFile(t *testing.T) { }) // Plugins off in settings so no fileData and we get a warning instead - fileData, warning = th.App.createPluginsFile() + fileData, err = th.App.createPluginsFile() assert.Nil(t, fileData) - assert.Contains(t, warning, "c.App.GetPlugins() Error:") + assert.ErrorContains(t, err, "failed to get plugin list for support package") } func TestGenerateSupportPacketYaml(t *testing.T) { @@ -46,16 +46,17 @@ func TestGenerateSupportPacketYaml(t *testing.T) { th.App.Srv().SetLicense(license) // Happy path where we have a support packet yaml file without any warnings - fileData, warning := th.App.generateSupportPacketYaml() + fileData, err := th.App.generateSupportPacketYaml() require.NotNil(t, fileData) assert.Equal(t, "support_packet.yaml", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) var packet model.SupportPacket require.NoError(t, yaml.Unmarshal(fileData.Body, &packet)) assert.Equal(t, 3, packet.ActiveUsers) // from InitBasic. assert.Equal(t, licenseUsers, packet.LicenseSupportedUsers) } + func TestGenerateSupportPacket(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -67,12 +68,15 @@ func TestGenerateSupportPacket(t *testing.T) { require.NoError(t, err) fileDatas := th.App.GenerateSupportPacket() + var rFileNames []string testFiles := []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "mattermost.log", "notifications.log"} - for i, fileData := range fileDatas { + for _, fileData := range fileDatas { require.NotNil(t, fileData) - assert.Equal(t, testFiles[i], fileData.Filename) assert.Positive(t, len(fileData.Body)) + + rFileNames = append(rFileNames, fileData.Filename) } + assert.ElementsMatch(t, testFiles, rFileNames) // Remove these two files and ensure that warning.txt file is generated err = os.Remove("notifications.log") @@ -81,11 +85,14 @@ func TestGenerateSupportPacket(t *testing.T) { require.NoError(t, err) fileDatas = th.App.GenerateSupportPacket() testFiles = []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "warning.txt"} - for i, fileData := range fileDatas { + rFileNames = nil + for _, fileData := range fileDatas { require.NotNil(t, fileData) - assert.Equal(t, testFiles[i], fileData.Filename) assert.Positive(t, len(fileData.Body)) + + rFileNames = append(rFileNames, fileData.Filename) } + assert.ElementsMatch(t, testFiles, rFileNames) } func TestGetNotificationsLog(t *testing.T) { @@ -97,9 +104,9 @@ func TestGetNotificationsLog(t *testing.T) { *cfg.NotificationLogSettings.EnableFile = false }) - fileData, warning := th.App.getNotificationsLog() + fileData, err := th.App.getNotificationsLog() assert.Nil(t, fileData) - assert.Equal(t, warning, "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json") + assert.ErrorContains(t, err, "Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") // Enable notifications file but delete any notifications file to get an error trying to read the file th.App.UpdateConfig(func(cfg *model.Config) { @@ -109,21 +116,21 @@ func TestGetNotificationsLog(t *testing.T) { // If any previous notifications.log file, lets delete it os.Remove("notifications.log") - fileData, warning = th.App.getNotificationsLog() + fileData, err = th.App.getNotificationsLog() assert.Nil(t, fileData) - assert.Contains(t, warning, "os.ReadFile(notificationsLog) Error:") + assert.ErrorContains(t, err, "failed read notifcation log file at path") - // Happy path where we have file and no warning + // Happy path where we have file and no error d1 := []byte("hello\ngo\n") - err := os.WriteFile("notifications.log", d1, 0777) + err = os.WriteFile("notifications.log", d1, 0777) defer os.Remove("notifications.log") require.NoError(t, err) - fileData, warning = th.App.getNotificationsLog() + fileData, err = th.App.getNotificationsLog() require.NotNil(t, fileData) assert.Equal(t, "notifications.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } func TestGetMattermostLog(t *testing.T) { @@ -135,9 +142,9 @@ func TestGetMattermostLog(t *testing.T) { *cfg.LogSettings.EnableFile = false }) - fileData, warning := th.App.getMattermostLog() + fileData, err := th.App.getMattermostLog() assert.Nil(t, fileData) - assert.Equal(t, "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json", warning) + assert.ErrorContains(t, err, "Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false") // We enable the setting but delete any mattermost log file th.App.UpdateConfig(func(cfg *model.Config) { @@ -147,31 +154,31 @@ func TestGetMattermostLog(t *testing.T) { // If any previous mattermost.log file, lets delete it os.Remove("mattermost.log") - fileData, warning = th.App.getMattermostLog() + fileData, err = th.App.getMattermostLog() assert.Nil(t, fileData) - assert.Contains(t, warning, "os.ReadFile(mattermostLog) Error:") + assert.ErrorContains(t, err, "failed read mattermost log file at path mattermost.log") // Happy path where we get a log file and no warning d1 := []byte("hello\ngo\n") - err := os.WriteFile("mattermost.log", d1, 0777) + err = os.WriteFile("mattermost.log", d1, 0777) defer os.Remove("mattermost.log") require.NoError(t, err) - fileData, warning = th.App.getMattermostLog() + fileData, err = th.App.getMattermostLog() require.NotNil(t, fileData) assert.Equal(t, "mattermost.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } func TestCreateSanitizedConfigFile(t *testing.T) { th := Setup(t) defer th.TearDown() - // Happy path where we have a sanitized config file with no warning - fileData, warning := th.App.createSanitizedConfigFile() + // Happy path where we have a sanitized config file with no err + fileData, err := th.App.createSanitizedConfigFile() require.NotNil(t, fileData) assert.Equal(t, "sanitized_config.json", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } diff --git a/server/public/model/system.go b/server/public/model/system.go index 98f68d04e1..8d573428fc 100644 --- a/server/public/model/system.go +++ b/server/public/model/system.go @@ -78,36 +78,39 @@ type ServerBusyState struct { } type SupportPacket struct { - LicenseTo string `yaml:"license_to"` - ServerOS string `yaml:"server_os"` - ServerArchitecture string `yaml:"server_architecture"` - ServerVersion string `yaml:"server_version"` - BuildHash string `yaml:"build_hash,omitempty"` - DatabaseType string `yaml:"database_type"` - DatabaseVersion string `yaml:"database_version"` - DatabaseSchemaVersion string `yaml:"database_schema_version"` - LdapVendorName string `yaml:"ldap_vendor_name,omitempty"` - LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"` - ElasticServerVersion string `yaml:"elastic_server_version,omitempty"` - ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"` - ActiveUsers int `yaml:"active_users"` - LicenseSupportedUsers int `yaml:"license_supported_users,omitempty"` - TotalChannels int `yaml:"total_channels"` - TotalPosts int `yaml:"total_posts"` - TotalTeams int `yaml:"total_teams"` - DailyActiveUsers int `yaml:"daily_active_users"` - MonthlyActiveUsers int `yaml:"monthly_active_users"` - WebsocketConnections int `yaml:"websocket_connections"` - MasterDbConnections int `yaml:"master_db_connections"` - ReplicaDbConnections int `yaml:"read_db_connections"` - InactiveUserCount int `yaml:"inactive_user_count"` - ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"` - ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"` - LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"` - MessageExportJobs []*Job `yaml:"message_export_jobs"` - DataRetentionJobs []*Job `yaml:"data_retention_jobs"` - ComplianceJobs []*Job `yaml:"compliance_jobs"` - MigrationJobs []*Job `yaml:"migration_jobs"` + LicenseTo string `yaml:"license_to"` + ServerOS string `yaml:"server_os"` + ServerArchitecture string `yaml:"server_architecture"` + ServerVersion string `yaml:"server_version"` + BuildHash string `yaml:"build_hash,omitempty"` + DatabaseType string `yaml:"database_type"` + DatabaseVersion string `yaml:"database_version"` + DatabaseSchemaVersion string `yaml:"database_schema_version"` + LdapVendorName string `yaml:"ldap_vendor_name,omitempty"` + LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"` + ElasticServerVersion string `yaml:"elastic_server_version,omitempty"` + ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"` + ActiveUsers int `yaml:"active_users"` + LicenseSupportedUsers int `yaml:"license_supported_users,omitempty"` + TotalChannels int `yaml:"total_channels"` + TotalPosts int `yaml:"total_posts"` + TotalTeams int `yaml:"total_teams"` + DailyActiveUsers int `yaml:"daily_active_users"` + MonthlyActiveUsers int `yaml:"monthly_active_users"` + WebsocketConnections int `yaml:"websocket_connections"` + MasterDbConnections int `yaml:"master_db_connections"` + ReplicaDbConnections int `yaml:"read_db_connections"` + InactiveUserCount int `yaml:"inactive_user_count"` + + // Jobs + DataRetentionJobs []*Job `yaml:"data_retention_jobs"` + MessageExportJobs []*Job `yaml:"message_export_jobs"` + ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"` + ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"` + BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"` + LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"` + MigrationJobs []*Job `yaml:"migration_jobs"` + ComplianceJobs []*Job `yaml:"compliance_jobs"` } type FileData struct {