diff --git a/server/channels/app/health_check_snapshot_test.go b/server/channels/app/health_check_snapshot_test.go index 5f8a8f364cb..f58b59d5aeb 100644 --- a/server/channels/app/health_check_snapshot_test.go +++ b/server/channels/app/health_check_snapshot_test.go @@ -78,7 +78,8 @@ func TestBuildHealthSnapshotOnlyLeaderHasDiagnostics(t *testing.T) { if node.IsLeader { assert.Equal(t, "id-2", node.ClusterInfo.Id) require.NotNil(t, node.Diagnostics) - assert.Equal(t, 3, node.Diagnostics.Diagnostics.Cluster.NumberOfNodes) + require.NotNil(t, node.Diagnostics.Diagnostics.Cluster.NumberOfNodes) + assert.Equal(t, 3, *node.Diagnostics.Diagnostics.Cluster.NumberOfNodes) } else { assert.Nil(t, node.Diagnostics, "follower %q", node.ClusterInfo.Id) } diff --git a/server/channels/app/platform/support_packet.go b/server/channels/app/platform/support_packet.go index ad77745e75a..80b5cd82a3d 100644 --- a/server/channels/app/platform/support_packet.go +++ b/server/channels/app/platform/support_packet.go @@ -190,8 +190,9 @@ func (ps *PlatformService) GetSupportPacketDiagnostics(rctx request.CTX) (*model totalMemoryBytes, err := getTotalMemory() if err != nil { fail(model.SectionServerHost, errors.Wrap(err, "error while getting total memory")) + } else { + d.Server.TotalMemoryMB = new(totalMemoryBytes / 1024 / 1024) } - d.Server.TotalMemoryMB = totalMemoryBytes / 1024 / 1024 containerLimits, err := getContainerLimits() if err != nil { rctx.Logger().Debug("Failed to get container limits for Support Packet", mlog.Err(err)) @@ -199,9 +200,11 @@ func (ps *PlatformService) GetSupportPacketDiagnostics(rctx request.CTX) (*model d.Server.ContainerCPULimit = containerLimits.CPULimit d.Server.ContainerMemoryLimitMB = containerLimits.MemoryLimitMB } - d.Server.Hostname, err = os.Hostname() + serverHostname, err := os.Hostname() if err != nil { fail(model.SectionServerHost, errors.Wrap(err, "error while getting hostname")) + } else { + d.Server.Hostname = new(serverHostname) } d.Server.ProcessID = os.Getpid() d.Server.StartedAt = ps.startTime.UTC() @@ -219,29 +222,36 @@ func (ps *PlatformService) GetSupportPacketDiagnostics(rctx request.CTX) (*model installationType = unknownDataPoint } d.Server.InstallationType = installationType - d.Server.OpenFileDescriptors, err = getOpenFileDescriptors() + openFileDescriptors, err := getOpenFileDescriptors() if err != nil { fail(model.SectionServerFDs, errors.Wrap(err, "error while getting open file descriptor count")) + } else { + d.Server.OpenFileDescriptors = new(openFileDescriptors) } - d.Server.MaxFileDescriptors, err = getMaxFileDescriptors() + maxFileDescriptors, err := getMaxFileDescriptors() if err != nil { fail(model.SectionServerFDs, errors.Wrap(err, "error while getting max file descriptor limit")) + } else { + d.Server.MaxFileDescriptors = new(maxFileDescriptors) } /* Config */ d.Config.Source = ps.DescribeConfig() /* DB */ - d.Database.Type, d.Database.SchemaVersion, err = ps.DatabaseTypeAndSchemaVersion() + databaseType, schemaVersion, err := ps.DatabaseTypeAndSchemaVersion() if err != nil { fail(model.SectionDatabaseIdentity, errors.Wrap(err, "error while getting DB type and schema version")) + } else { + d.Database.Type = new(databaseType) + d.Database.SchemaVersion = new(schemaVersion) } databaseVersion, err := ps.Store.GetDbVersion(false) if err != nil { fail(model.SectionDatabaseIdentity, errors.Wrap(err, "error while getting DB version")) } else { - d.Database.Version = databaseVersion + d.Database.Version = new(databaseVersion) } d.Database.MasterConnections = ps.Store.TotalMasterDbConnections() d.Database.ReplicaConnections = ps.Store.TotalReadDbConnections() @@ -269,9 +279,9 @@ func (ps *PlatformService) GetSupportPacketDiagnostics(rctx request.CTX) (*model if diskErr != nil { fail(model.SectionFilestoreDisk, errors.Wrap(diskErr, "error while getting disk space info")) } else { - d.FileStore.FilesystemType = di.FilesystemType - d.FileStore.TotalMB = di.TotalMB - d.FileStore.AvailableMB = di.AvailableMB + d.FileStore.FilesystemType = new(di.FilesystemType) + d.FileStore.TotalMB = new(di.TotalMB) + d.FileStore.AvailableMB = new(di.AvailableMB) } } @@ -286,8 +296,10 @@ func (ps *PlatformService) GetSupportPacketDiagnostics(rctx request.CTX) (*model if e != nil { fail(model.SectionCluster, 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 + d.Cluster.NumberOfNodes = new(max(len(clusterInfo), 1)) // clusterInfo is empty if the node is the only one in the cluster } + } else { + d.Cluster.NumberOfNodes = new(1) } /* LDAP */ @@ -416,18 +428,18 @@ func (ps *PlatformService) applyStoreDiagnostics(rctx request.CTX, diagnostics * return nil } - diagnostics.Database.MasterConnectionsInUse = storeDiagnostics.MasterConnectionsInUse - diagnostics.Database.MasterConnectionsIdle = storeDiagnostics.MasterConnectionsIdle - diagnostics.Database.MasterPoolWaitCount = storeDiagnostics.MasterPoolWaitCount - diagnostics.Database.MasterPoolWaitDurationMs = storeDiagnostics.MasterPoolWaitDurationMs - diagnostics.Database.MasterConnectionsClosedMaxIdle = storeDiagnostics.MasterConnectionsClosedMaxIdle - diagnostics.Database.MasterConnectionsClosedMaxLifetime = storeDiagnostics.MasterConnectionsClosedMaxLifetime - diagnostics.Database.ReplicaConnectionsInUse = storeDiagnostics.ReplicaConnectionsInUse - diagnostics.Database.ReplicaConnectionsIdle = storeDiagnostics.ReplicaConnectionsIdle - diagnostics.Database.ReplicaPoolWaitCount = storeDiagnostics.ReplicaPoolWaitCount - diagnostics.Database.ReplicaPoolWaitDurationMs = storeDiagnostics.ReplicaPoolWaitDurationMs - diagnostics.Database.ReplicaConnectionsClosedMaxIdle = storeDiagnostics.ReplicaConnectionsClosedMaxIdle - diagnostics.Database.ReplicaConnectionsClosedMaxLifetime = storeDiagnostics.ReplicaConnectionsClosedMaxLifetime + diagnostics.Database.MasterConnectionsInUse = new(storeDiagnostics.MasterConnectionsInUse) + diagnostics.Database.MasterConnectionsIdle = new(storeDiagnostics.MasterConnectionsIdle) + diagnostics.Database.MasterPoolWaitCount = new(storeDiagnostics.MasterPoolWaitCount) + diagnostics.Database.MasterPoolWaitDurationMs = new(storeDiagnostics.MasterPoolWaitDurationMs) + diagnostics.Database.MasterConnectionsClosedMaxIdle = new(storeDiagnostics.MasterConnectionsClosedMaxIdle) + diagnostics.Database.MasterConnectionsClosedMaxLifetime = new(storeDiagnostics.MasterConnectionsClosedMaxLifetime) + diagnostics.Database.ReplicaConnectionsInUse = new(storeDiagnostics.ReplicaConnectionsInUse) + diagnostics.Database.ReplicaConnectionsIdle = new(storeDiagnostics.ReplicaConnectionsIdle) + diagnostics.Database.ReplicaPoolWaitCount = new(storeDiagnostics.ReplicaPoolWaitCount) + diagnostics.Database.ReplicaPoolWaitDurationMs = new(storeDiagnostics.ReplicaPoolWaitDurationMs) + diagnostics.Database.ReplicaConnectionsClosedMaxIdle = new(storeDiagnostics.ReplicaConnectionsClosedMaxIdle) + diagnostics.Database.ReplicaConnectionsClosedMaxLifetime = new(storeDiagnostics.ReplicaConnectionsClosedMaxLifetime) diagnostics.Database.CacheHitRatio = storeDiagnostics.CacheHitRatio diagnostics.Database.Deadlocks = storeDiagnostics.Deadlocks diagnostics.Database.TempFiles = storeDiagnostics.TempFiles diff --git a/server/channels/app/platform/support_packet_test.go b/server/channels/app/platform/support_packet_test.go index 390b653f933..c7014947eae 100644 --- a/server/channels/app/platform/support_packet_test.go +++ b/server/channels/app/platform/support_packet_test.go @@ -285,15 +285,23 @@ func TestGetSupportPacketDiagnostics(t *testing.T) { /* Server information */ assert.NotEmpty(t, d.Server.OS) assert.NotEmpty(t, d.Server.Architecture) - assert.NotEmpty(t, d.Server.Hostname) + require.NotNil(t, d.Server.Hostname) + assert.NotEmpty(t, *d.Server.Hostname) assert.Equal(t, model.CurrentVersion, d.Server.Version) // BuildHash is not present in tests assert.NotEmpty(t, d.Server.GoVersion) assert.Equal(t, "docker", d.Server.InstallationType) assert.Positive(t, d.Server.CPUCores) - assert.Positive(t, d.Server.TotalMemoryMB) - assert.True(t, d.Server.OpenFileDescriptors == -1 || d.Server.OpenFileDescriptors > 0, "OpenFileDescriptors should be -1 (unsupported) or positive, got %d", d.Server.OpenFileDescriptors) - assert.True(t, d.Server.MaxFileDescriptors == -1 || d.Server.MaxFileDescriptors > 0, "MaxFileDescriptors should be -1 (unsupported) or positive, got %d", d.Server.MaxFileDescriptors) + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + require.NotNil(t, d.Server.TotalMemoryMB) + assert.Positive(t, *d.Server.TotalMemoryMB) + } else { + assert.Nil(t, d.Server.TotalMemoryMB) + } + require.NotNil(t, d.Server.OpenFileDescriptors) + require.NotNil(t, d.Server.MaxFileDescriptors) + assert.True(t, *d.Server.OpenFileDescriptors == -1 || *d.Server.OpenFileDescriptors > 0, "OpenFileDescriptors should be -1 (unsupported) or positive, got %d", *d.Server.OpenFileDescriptors) + assert.True(t, *d.Server.MaxFileDescriptors == -1 || *d.Server.MaxFileDescriptors > 0, "MaxFileDescriptors should be -1 (unsupported) or positive, got %d", *d.Server.MaxFileDescriptors) assert.Positive(t, d.Server.ProcessID) assert.False(t, d.Server.StartedAt.IsZero()) if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { @@ -307,37 +315,50 @@ func TestGetSupportPacketDiagnostics(t *testing.T) { 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) + require.NotNil(t, d.Database.Type) + assert.NotEmpty(t, *d.Database.Type) + require.NotNil(t, d.Database.Version) + assert.NotEmpty(t, *d.Database.Version) + require.NotNil(t, d.Database.SchemaVersion) + assert.NotEmpty(t, *d.Database.SchemaVersion) assert.NotZero(t, d.Database.MasterConnections) assert.Zero(t, d.Database.ReplicaConnections) assert.Zero(t, d.Database.SearchConnections) - assert.GreaterOrEqual(t, d.Database.MasterConnectionsInUse, 0) - assert.GreaterOrEqual(t, d.Database.MasterConnectionsIdle, 0) - assert.GreaterOrEqual(t, d.Database.MasterPoolWaitCount, int64(0)) - assert.GreaterOrEqual(t, d.Database.MasterPoolWaitDurationMs, int64(0)) - assert.GreaterOrEqual(t, d.Database.MasterConnectionsClosedMaxIdle, int64(0)) - assert.GreaterOrEqual(t, d.Database.MasterConnectionsClosedMaxLifetime, int64(0)) - assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsInUse, 0) - assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsIdle, 0) - assert.GreaterOrEqual(t, d.Database.ReplicaPoolWaitCount, int64(0)) - assert.GreaterOrEqual(t, d.Database.ReplicaPoolWaitDurationMs, int64(0)) - assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsClosedMaxIdle, int64(0)) - assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsClosedMaxLifetime, int64(0)) + require.NotNil(t, d.Database.MasterConnectionsInUse) + assert.GreaterOrEqual(t, *d.Database.MasterConnectionsInUse, 0) + for _, v := range []*int{d.Database.MasterConnectionsIdle, d.Database.ReplicaConnectionsInUse, d.Database.ReplicaConnectionsIdle} { + require.NotNil(t, v) + assert.GreaterOrEqual(t, *v, 0) + } + for _, v := range []*int64{ + d.Database.MasterPoolWaitCount, + d.Database.MasterPoolWaitDurationMs, + d.Database.MasterConnectionsClosedMaxIdle, + d.Database.MasterConnectionsClosedMaxLifetime, + d.Database.ReplicaPoolWaitCount, + d.Database.ReplicaPoolWaitDurationMs, + d.Database.ReplicaConnectionsClosedMaxIdle, + d.Database.ReplicaConnectionsClosedMaxLifetime, + } { + require.NotNil(t, v) + assert.GreaterOrEqual(t, *v, int64(0)) + } /* File store */ assert.Equal(t, "OK", d.FileStore.Status) assert.Empty(t, d.FileStore.Error) assert.Equal(t, "local", d.FileStore.Driver) if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { - assert.NotEmpty(t, d.FileStore.FilesystemType, "FilesystemType should not be empty on supported platforms") - assert.Positive(t, d.FileStore.TotalMB, "TotalMB should be positive on supported platforms") - assert.Positive(t, d.FileStore.AvailableMB, "AvailableMB should be positive on supported platforms") + require.NotNil(t, d.FileStore.FilesystemType) + require.NotNil(t, d.FileStore.TotalMB) + require.NotNil(t, d.FileStore.AvailableMB) + assert.NotEmpty(t, *d.FileStore.FilesystemType, "FilesystemType should not be empty on supported platforms") + assert.Positive(t, *d.FileStore.TotalMB, "TotalMB should be positive on supported platforms") + assert.Positive(t, *d.FileStore.AvailableMB, "AvailableMB should be positive on supported platforms") } else { - assert.Empty(t, d.FileStore.FilesystemType) - assert.Zero(t, d.FileStore.TotalMB) - assert.Zero(t, d.FileStore.AvailableMB) + assert.Nil(t, d.FileStore.FilesystemType) + assert.Nil(t, d.FileStore.TotalMB) + assert.Nil(t, d.FileStore.AvailableMB) } /* Websockets */ @@ -345,7 +366,8 @@ func TestGetSupportPacketDiagnostics(t *testing.T) { /* Cluster */ assert.Empty(t, d.Cluster.ID) - assert.Zero(t, d.Cluster.NumberOfNodes) + require.NotNil(t, d.Cluster.NumberOfNodes) + assert.Equal(t, 1, *d.Cluster.NumberOfNodes) assert.False(t, d.Cluster.IsLeader) /* LDAP */ @@ -439,9 +461,9 @@ func TestGetSupportPacketDiagnostics(t *testing.T) { assert.Equal(t, "OK", packet.FileStore.Status) assert.Equal(t, "amazons3", packet.FileStore.Driver) - assert.Empty(t, packet.FileStore.FilesystemType) - assert.Zero(t, packet.FileStore.TotalMB) - assert.Zero(t, packet.FileStore.AvailableMB) + assert.Nil(t, packet.FileStore.FilesystemType) + assert.Nil(t, packet.FileStore.TotalMB) + assert.Nil(t, packet.FileStore.AvailableMB) }) t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) { @@ -984,18 +1006,19 @@ func TestGetSupportPacketDiagnostics(t *testing.T) { }) packet := getDiagnostics(t) - assert.Equal(t, 3, packet.Database.MasterConnectionsInUse) - assert.Equal(t, 7, packet.Database.MasterConnectionsIdle) - assert.Equal(t, int64(11), packet.Database.MasterPoolWaitCount) - assert.Equal(t, int64(2025), packet.Database.MasterPoolWaitDurationMs) - assert.Equal(t, int64(13), packet.Database.MasterConnectionsClosedMaxIdle) - assert.Equal(t, int64(17), packet.Database.MasterConnectionsClosedMaxLifetime) - assert.Equal(t, 5, packet.Database.ReplicaConnectionsInUse) - assert.Equal(t, 9, packet.Database.ReplicaConnectionsIdle) - assert.Equal(t, int64(19), packet.Database.ReplicaPoolWaitCount) - assert.Equal(t, int64(4090), packet.Database.ReplicaPoolWaitDurationMs) - assert.Equal(t, int64(23), packet.Database.ReplicaConnectionsClosedMaxIdle) - assert.Equal(t, int64(29), packet.Database.ReplicaConnectionsClosedMaxLifetime) + require.NotNil(t, packet.Database.MasterConnectionsInUse) + assert.Equal(t, 3, *packet.Database.MasterConnectionsInUse) + assert.Equal(t, new(7), packet.Database.MasterConnectionsIdle) + assert.Equal(t, new(int64(11)), packet.Database.MasterPoolWaitCount) + assert.Equal(t, new(int64(2025)), packet.Database.MasterPoolWaitDurationMs) + assert.Equal(t, new(int64(13)), packet.Database.MasterConnectionsClosedMaxIdle) + assert.Equal(t, new(int64(17)), packet.Database.MasterConnectionsClosedMaxLifetime) + assert.Equal(t, new(5), packet.Database.ReplicaConnectionsInUse) + assert.Equal(t, new(9), packet.Database.ReplicaConnectionsIdle) + assert.Equal(t, new(int64(19)), packet.Database.ReplicaPoolWaitCount) + assert.Equal(t, new(int64(4090)), packet.Database.ReplicaPoolWaitDurationMs) + assert.Equal(t, new(int64(23)), packet.Database.ReplicaConnectionsClosedMaxIdle) + assert.Equal(t, new(int64(29)), packet.Database.ReplicaConnectionsClosedMaxLifetime) }) t.Run("OpenID disabled", func(t *testing.T) { @@ -1176,7 +1199,7 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { }) } - requireSectionErrors := func(t *testing.T, failed map[model.NodeSection][]string) { + requireSectionErrors := func(t *testing.T, failed map[model.NodeSection][]string) *model.SupportPacketDiagnostics { t.Helper() nodeDiagnostics, err := th.Service.GetSupportPacketDiagnostics(th.Context) @@ -1204,6 +1227,7 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { assert.ErrorContains(t, err, msg) } } + return nodeDiagnostics.Diagnostics } t.Run("all sections present and clean", func(t *testing.T) { @@ -1213,17 +1237,20 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { t.Run("DB schema version fails", func(t *testing.T) { setStore(t, &failingDiagnosticsStore{schemaVersionErr: errors.New("schema down")}) - requireSectionErrors(t, map[model.NodeSection][]string{ + d := requireSectionErrors(t, map[model.NodeSection][]string{ model.SectionDatabaseIdentity: {"error while getting DB type and schema version"}, }) + assert.Nil(t, d.Database.Type) + assert.Nil(t, d.Database.SchemaVersion) }) t.Run("DB version fails", func(t *testing.T) { setStore(t, &failingDiagnosticsStore{dbVersionErr: errors.New("version down")}) - requireSectionErrors(t, map[model.NodeSection][]string{ + d := requireSectionErrors(t, map[model.NodeSection][]string{ model.SectionDatabaseIdentity: {"error while getting DB version"}, }) + assert.Nil(t, d.Database.Version) }) t.Run("both DB identity sites fail", func(t *testing.T) { @@ -1240,9 +1267,21 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { t.Run("store diagnostics fail", func(t *testing.T) { setStore(t, &failingDiagnosticsStore{diagnosticsErr: errors.New("stats down")}) - requireSectionErrors(t, map[model.NodeSection][]string{ + d := requireSectionErrors(t, map[model.NodeSection][]string{ model.SectionDatabaseStats: {"error while collecting support packet database diagnostics"}, }) + assert.Nil(t, d.Database.MasterConnectionsInUse) + assert.Nil(t, d.Database.MasterConnectionsIdle) + assert.Nil(t, d.Database.MasterPoolWaitCount) + assert.Nil(t, d.Database.MasterPoolWaitDurationMs) + assert.Nil(t, d.Database.MasterConnectionsClosedMaxIdle) + assert.Nil(t, d.Database.MasterConnectionsClosedMaxLifetime) + assert.Nil(t, d.Database.ReplicaConnectionsInUse) + assert.Nil(t, d.Database.ReplicaConnectionsIdle) + assert.Nil(t, d.Database.ReplicaPoolWaitCount) + assert.Nil(t, d.Database.ReplicaPoolWaitDurationMs) + assert.Nil(t, d.Database.ReplicaConnectionsClosedMaxIdle) + assert.Nil(t, d.Database.ReplicaConnectionsClosedMaxLifetime) }) t.Run("disk space fails", func(t *testing.T) { @@ -1265,9 +1304,12 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { cfg.FileSettings.Directory = model.NewPointer(filepath.Join(t.TempDir(), "missing")) }) - requireSectionErrors(t, map[model.NodeSection][]string{ + d := requireSectionErrors(t, map[model.NodeSection][]string{ model.SectionFilestoreDisk: {"error while getting disk space info"}, }) + assert.Nil(t, d.FileStore.FilesystemType) + assert.Nil(t, d.FileStore.TotalMB) + assert.Nil(t, d.FileStore.AvailableMB) }) t.Run("cluster infos fail", func(t *testing.T) { @@ -1283,9 +1325,10 @@ func TestGetSupportPacketDiagnosticsSectionErrors(t *testing.T) { }) th.Service.clusterIFace = cluster - requireSectionErrors(t, map[model.NodeSection][]string{ + d := requireSectionErrors(t, map[model.NodeSection][]string{ model.SectionCluster: {"error while getting cluster infos"}, }) + assert.Nil(t, d.Cluster.NumberOfNodes) }) t.Run("LDAP vendor info fails", func(t *testing.T) { @@ -1523,39 +1566,39 @@ func TestSupportPacketMarshalGolden(t *testing.T) { diagnostics.License.IsNonProduction = true diagnostics.Server.OS = "linux" diagnostics.Server.Architecture = "amd64" - diagnostics.Server.Hostname = "mm-host" + diagnostics.Server.Hostname = new("mm-host") diagnostics.Server.InstallationType = "docker" diagnostics.Server.CPUCores = 8 - diagnostics.Server.TotalMemoryMB = 32768 + diagnostics.Server.TotalMemoryMB = new(uint64(32768)) diagnostics.Server.ContainerCPULimit = 6.5 diagnostics.Server.ContainerMemoryLimitMB = 16384 diagnostics.Server.ProcessID = 90210 diagnostics.Server.StartedAt = time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) diagnostics.Server.HostStartedAt = time.Date(2025, 12, 30, 0, 0, 0, 0, time.UTC) - diagnostics.Server.OpenFileDescriptors = 512 - diagnostics.Server.MaxFileDescriptors = 8192 + diagnostics.Server.OpenFileDescriptors = new(int64(512)) + diagnostics.Server.MaxFileDescriptors = new(int64(8192)) diagnostics.Server.Version = "11.0.0" diagnostics.Server.BuildHash = "abc123" diagnostics.Server.GoVersion = "go1.26" diagnostics.Config.Source = "memory://" - diagnostics.Database.Type = "postgres" - diagnostics.Database.Version = "16.4" - diagnostics.Database.SchemaVersion = "123" + diagnostics.Database.Type = new("postgres") + diagnostics.Database.Version = new("16.4") + diagnostics.Database.SchemaVersion = new("123") diagnostics.Database.MasterConnections = 40 diagnostics.Database.ReplicaConnections = 20 diagnostics.Database.SearchConnections = 10 - diagnostics.Database.MasterConnectionsInUse = 5 - diagnostics.Database.MasterConnectionsIdle = 35 - diagnostics.Database.MasterPoolWaitCount = 100 - diagnostics.Database.MasterPoolWaitDurationMs = 220 - diagnostics.Database.MasterConnectionsClosedMaxIdle = 2 - diagnostics.Database.MasterConnectionsClosedMaxLifetime = 1 - diagnostics.Database.ReplicaConnectionsInUse = 3 - diagnostics.Database.ReplicaConnectionsIdle = 17 - diagnostics.Database.ReplicaPoolWaitCount = 12 - diagnostics.Database.ReplicaPoolWaitDurationMs = 66 - diagnostics.Database.ReplicaConnectionsClosedMaxIdle = 4 - diagnostics.Database.ReplicaConnectionsClosedMaxLifetime = 3 + diagnostics.Database.MasterConnectionsInUse = new(5) + diagnostics.Database.MasterConnectionsIdle = new(35) + diagnostics.Database.MasterPoolWaitCount = new(int64(100)) + diagnostics.Database.MasterPoolWaitDurationMs = new(int64(220)) + diagnostics.Database.MasterConnectionsClosedMaxIdle = new(int64(2)) + diagnostics.Database.MasterConnectionsClosedMaxLifetime = new(int64(1)) + diagnostics.Database.ReplicaConnectionsInUse = new(3) + diagnostics.Database.ReplicaConnectionsIdle = new(17) + diagnostics.Database.ReplicaPoolWaitCount = new(int64(12)) + diagnostics.Database.ReplicaPoolWaitDurationMs = new(int64(66)) + diagnostics.Database.ReplicaConnectionsClosedMaxIdle = new(int64(4)) + diagnostics.Database.ReplicaConnectionsClosedMaxLifetime = new(int64(3)) diagnostics.Database.CacheHitRatio = &cacheHitRatio diagnostics.Database.Deadlocks = &deadlocks diagnostics.Database.TempFiles = &tempFiles @@ -1568,12 +1611,12 @@ func TestSupportPacketMarshalGolden(t *testing.T) { diagnostics.Database.PostsLastAutovacuum = &postsLastAutovacuum diagnostics.FileStore.Status = model.StatusOk diagnostics.FileStore.Driver = model.ImageDriverLocal - diagnostics.FileStore.FilesystemType = "ext4" - diagnostics.FileStore.TotalMB = 204800 - diagnostics.FileStore.AvailableMB = 102400 + diagnostics.FileStore.FilesystemType = new("ext4") + diagnostics.FileStore.TotalMB = new(uint64(204800)) + diagnostics.FileStore.AvailableMB = new(uint64(102400)) diagnostics.Websocket.Connections = 77 diagnostics.Cluster.ID = "cluster-id" - diagnostics.Cluster.NumberOfNodes = 3 + diagnostics.Cluster.NumberOfNodes = new(3) diagnostics.Notifications.Email.Status = model.StatusOk diagnostics.Notifications.Push.Status = model.StatusFail diagnostics.Notifications.Push.Error = "proxy timeout" diff --git a/server/platform/shared/healthcheck/node.go b/server/platform/shared/healthcheck/node.go index b278501a76c..aee1869588e 100644 --- a/server/platform/shared/healthcheck/node.go +++ b/server/platform/shared/healthcheck/node.go @@ -35,8 +35,8 @@ func (n *NodeSnapshot) SchemaVersion() (string, bool) { if n != nil && n.ClusterInfo != nil && n.ClusterInfo.SchemaVersion != "" { return n.ClusterInfo.SchemaVersion, true } - if diag, ok := n.Diag(); ok && diag.Database.SchemaVersion != "" && n.sectionOK(model.SectionDatabaseIdentity) { - return diag.Database.SchemaVersion, true + if diag, ok := n.Diag(); ok && model.SafeDereference(diag.Database.SchemaVersion) != "" && n.sectionOK(model.SectionDatabaseIdentity) { + return *diag.Database.SchemaVersion, true } return "", false diff --git a/server/platform/shared/healthcheck/node_test.go b/server/platform/shared/healthcheck/node_test.go index 8228890ebda..314d537ddc8 100644 --- a/server/platform/shared/healthcheck/node_test.go +++ b/server/platform/shared/healthcheck/node_test.go @@ -51,7 +51,7 @@ func TestSchemaVersion(t *testing.T) { Diagnostics: &model.SupportPacketDiagnostics{}, }, } - node.Diagnostics.Diagnostics.Database.SchemaVersion = "diag" + node.Diagnostics.Diagnostics.Database.SchemaVersion = new("diag") version, ok := node.SchemaVersion() require.True(t, ok) @@ -65,7 +65,7 @@ func TestSchemaVersion(t *testing.T) { Errors: model.SectionErrors{model.SectionDatabaseIdentity: nil}, }, } - node.Diagnostics.Diagnostics.Database.SchemaVersion = "diag" + node.Diagnostics.Diagnostics.Database.SchemaVersion = new("diag") version, ok := node.SchemaVersion() require.True(t, ok) @@ -79,7 +79,32 @@ func TestSchemaVersion(t *testing.T) { Errors: model.SectionErrors{model.SectionDatabaseIdentity: errors.New("collect failed")}, }, } - node.Diagnostics.Diagnostics.Database.SchemaVersion = "diag" + node.Diagnostics.Diagnostics.Database.SchemaVersion = new("diag") + + _, ok := node.SchemaVersion() + require.False(t, ok) + }) + + t.Run("nil schema version in diagnostics", func(t *testing.T) { + node := &NodeSnapshot{ + Diagnostics: &model.NodeDiagnostics{ + Diagnostics: &model.SupportPacketDiagnostics{}, + Errors: model.SectionErrors{model.SectionDatabaseIdentity: nil}, + }, + } + + _, ok := node.SchemaVersion() + require.False(t, ok) + }) + + t.Run("empty schema version from an older packet", func(t *testing.T) { + node := &NodeSnapshot{ + Diagnostics: &model.NodeDiagnostics{ + Diagnostics: &model.SupportPacketDiagnostics{}, + Errors: model.SectionErrors{model.SectionDatabaseIdentity: nil}, + }, + } + node.Diagnostics.Diagnostics.Database.SchemaVersion = new("") _, ok := node.SchemaVersion() require.False(t, ok) diff --git a/server/platform/shared/healthcheck/packet/reader.go b/server/platform/shared/healthcheck/packet/reader.go index d718e05945b..3888055de3d 100644 --- a/server/platform/shared/healthcheck/packet/reader.go +++ b/server/platform/shared/healthcheck/packet/reader.go @@ -179,7 +179,7 @@ func readNodes(z *budgetedZip, root []string, byNode map[string][]string) ([]*he if len(byNode) == 0 && slices.Contains(root, model.SupportPacketDiagnosticsFileName) { node := read("", model.SupportPacketDiagnosticsFileName) if diag, ok := node.Diag(); ok { - node.Hostname = diag.Server.Hostname + node.Hostname = model.SafeDereference(diag.Server.Hostname) } nodes = append(nodes, node) } diff --git a/server/public/model/health_sections_test.go b/server/public/model/health_sections_test.go index 05029da40fc..07a4f26dbc1 100644 --- a/server/public/model/health_sections_test.go +++ b/server/public/model/health_sections_test.go @@ -35,8 +35,8 @@ func TestSectionErrorsThreeStates(t *testing.T) { func TestSectionErrorsFailClosedWithinSection(t *testing.T) { diagnostics := &SupportPacketDiagnostics{} - diagnostics.Server.OpenFileDescriptors = 5000 - diagnostics.Server.MaxFileDescriptors = 0 + diagnostics.Server.OpenFileDescriptors = new(int64(5000)) + diagnostics.Server.MaxFileDescriptors = new(int64(0)) node := NodeDiagnostics{ Diagnostics: diagnostics, @@ -48,8 +48,8 @@ func TestSectionErrorsFailClosedWithinSection(t *testing.T) { _, ok := node.Errors[SectionServerFDs] require.True(t, ok) require.EqualError(t, node.Errors[SectionServerFDs], "max file descriptors read failed") - assert.EqualValues(t, 5000, node.Diagnostics.Server.OpenFileDescriptors) - assert.EqualValues(t, 0, node.Diagnostics.Server.MaxFileDescriptors) + assert.EqualValues(t, 5000, *node.Diagnostics.Server.OpenFileDescriptors) + assert.EqualValues(t, 0, *node.Diagnostics.Server.MaxFileDescriptors) } func TestNodeAndWorkspaceSectionsDoNotOverlap(t *testing.T) { diff --git a/server/public/model/support_packet.go b/server/public/model/support_packet.go index a9680ed83aa..f6d6c93c8a5 100644 --- a/server/public/model/support_packet.go +++ b/server/public/model/support_packet.go @@ -16,6 +16,7 @@ const ( SupportPacketPluginsFileName = "plugins.json" ) +// SupportPacketDiagnostics reports one node's diagnostics. A nil pointer field was not collected, which is not the same as zero. type SupportPacketDiagnostics struct { Version int `yaml:"version"` @@ -31,14 +32,14 @@ type SupportPacketDiagnostics struct { Server struct { // Machine - OS string `yaml:"os"` - Architecture string `yaml:"architecture"` - Hostname string `yaml:"hostname"` - InstallationType string `yaml:"installation_type"` + OS string `yaml:"os"` + Architecture string `yaml:"architecture"` + Hostname *string `yaml:"hostname,omitempty"` + InstallationType string `yaml:"installation_type"` // Capacity CPUCores int `yaml:"cpu_cores"` - TotalMemoryMB uint64 `yaml:"total_memory_mb"` + TotalMemoryMB *uint64 `yaml:"total_memory_mb,omitempty"` ContainerCPULimit float64 `yaml:"container_cpu_limit,omitempty"` ContainerMemoryLimitMB uint64 `yaml:"container_memory_limit_mb,omitempty"` @@ -46,8 +47,8 @@ type SupportPacketDiagnostics struct { ProcessID int `yaml:"process_id"` StartedAt time.Time `yaml:"started_at"` HostStartedAt time.Time `yaml:"host_started_at,omitempty"` - OpenFileDescriptors int64 `yaml:"open_file_descriptors"` - MaxFileDescriptors int64 `yaml:"max_file_descriptors"` + OpenFileDescriptors *int64 `yaml:"open_file_descriptors,omitempty"` + MaxFileDescriptors *int64 `yaml:"max_file_descriptors,omitempty"` // Software Version string `yaml:"version"` @@ -60,24 +61,24 @@ type SupportPacketDiagnostics struct { } `yaml:"config"` Database struct { - Type string `yaml:"type"` - Version string `yaml:"version"` - SchemaVersion string `yaml:"schema_version"` + Type *string `yaml:"type,omitempty"` + Version *string `yaml:"version,omitempty"` + SchemaVersion *string `yaml:"schema_version,omitempty"` MasterConnections int `yaml:"master_connections"` ReplicaConnections int `yaml:"replica_connections"` SearchConnections int `yaml:"search_connections"` - MasterConnectionsInUse int `yaml:"master_connections_in_use"` - MasterConnectionsIdle int `yaml:"master_connections_idle"` - MasterPoolWaitCount int64 `yaml:"master_pool_wait_count"` - MasterPoolWaitDurationMs int64 `yaml:"master_pool_wait_duration_ms"` - MasterConnectionsClosedMaxIdle int64 `yaml:"master_connections_closed_max_idle"` - MasterConnectionsClosedMaxLifetime int64 `yaml:"master_connections_closed_max_lifetime"` - ReplicaConnectionsInUse int `yaml:"replica_connections_in_use"` - ReplicaConnectionsIdle int `yaml:"replica_connections_idle"` - ReplicaPoolWaitCount int64 `yaml:"replica_pool_wait_count"` - ReplicaPoolWaitDurationMs int64 `yaml:"replica_pool_wait_duration_ms"` - ReplicaConnectionsClosedMaxIdle int64 `yaml:"replica_connections_closed_max_idle"` - ReplicaConnectionsClosedMaxLifetime int64 `yaml:"replica_connections_closed_max_lifetime"` + MasterConnectionsInUse *int `yaml:"master_connections_in_use,omitempty"` + MasterConnectionsIdle *int `yaml:"master_connections_idle,omitempty"` + MasterPoolWaitCount *int64 `yaml:"master_pool_wait_count,omitempty"` + MasterPoolWaitDurationMs *int64 `yaml:"master_pool_wait_duration_ms,omitempty"` + MasterConnectionsClosedMaxIdle *int64 `yaml:"master_connections_closed_max_idle,omitempty"` + MasterConnectionsClosedMaxLifetime *int64 `yaml:"master_connections_closed_max_lifetime,omitempty"` + ReplicaConnectionsInUse *int `yaml:"replica_connections_in_use,omitempty"` + ReplicaConnectionsIdle *int `yaml:"replica_connections_idle,omitempty"` + ReplicaPoolWaitCount *int64 `yaml:"replica_pool_wait_count,omitempty"` + ReplicaPoolWaitDurationMs *int64 `yaml:"replica_pool_wait_duration_ms,omitempty"` + ReplicaConnectionsClosedMaxIdle *int64 `yaml:"replica_connections_closed_max_idle,omitempty"` + ReplicaConnectionsClosedMaxLifetime *int64 `yaml:"replica_connections_closed_max_lifetime,omitempty"` CacheHitRatio *float64 `yaml:"cache_hit_ratio,omitempty"` Deadlocks *int64 `yaml:"deadlocks,omitempty"` TempFiles *int64 `yaml:"temp_files,omitempty"` @@ -91,12 +92,12 @@ type SupportPacketDiagnostics struct { } `yaml:"database"` FileStore struct { - Status string `yaml:"file_status"` - Error string `yaml:"error,omitempty"` - Driver string `yaml:"file_driver"` - FilesystemType string `yaml:"filesystem_type,omitempty"` - TotalMB uint64 `yaml:"total_mb,omitempty"` - AvailableMB uint64 `yaml:"available_mb,omitempty"` + Status string `yaml:"file_status"` + Error string `yaml:"error,omitempty"` + Driver string `yaml:"file_driver"` + FilesystemType *string `yaml:"filesystem_type,omitempty"` + TotalMB *uint64 `yaml:"total_mb,omitempty"` + AvailableMB *uint64 `yaml:"available_mb,omitempty"` } `yaml:"file_store"` Websocket struct { @@ -105,7 +106,7 @@ type SupportPacketDiagnostics struct { Cluster struct { ID string `yaml:"id"` - NumberOfNodes int `yaml:"number_of_nodes"` + NumberOfNodes *int `yaml:"number_of_nodes,omitempty"` IsLeader bool `yaml:"is_leader,omitempty"` } `yaml:"cluster"` diff --git a/server/public/model/support_packet_test.go b/server/public/model/support_packet_test.go index 83e0fe4a724..07547d8c5c5 100644 --- a/server/public/model/support_packet_test.go +++ b/server/public/model/support_packet_test.go @@ -70,3 +70,31 @@ outgoing_webhooks: 14 assert.Equal(t, expected, string(data)) } + +func TestSupportPacketDiagnosticsYAMLOmitsNilAndKeepsZero(t *testing.T) { + t.Parallel() + + var diagnostics SupportPacketDiagnostics + diagnostics.Server.OpenFileDescriptors = new(int64(0)) + diagnostics.Database.MasterConnectionsInUse = new(0) + diagnostics.FileStore.AvailableMB = new(uint64(0)) + + data, err := yaml.Marshal(&diagnostics) + require.NoError(t, err) + + body := string(data) + assert.Contains(t, body, "open_file_descriptors: 0\n") + assert.Contains(t, body, "master_connections_in_use: 0\n") + assert.Contains(t, body, "available_mb: 0\n") + assert.NotContains(t, body, "max_file_descriptors") + assert.NotContains(t, body, "schema_version") + + var roundTrip SupportPacketDiagnostics + err = yaml.Unmarshal(data, &roundTrip) + require.NoError(t, err) + assert.Nil(t, roundTrip.Server.MaxFileDescriptors) + assert.Nil(t, roundTrip.Database.SchemaVersion) + assert.Equal(t, new(int64(0)), roundTrip.Server.OpenFileDescriptors) + assert.Equal(t, new(0), roundTrip.Database.MasterConnectionsInUse) + assert.Equal(t, new(uint64(0)), roundTrip.FileStore.AvailableMB) +}