[MM-67978] Add open file descriptor count to support packet diagnostics (#35834)

* MM-67978: Add open file descriptor count to support packet diagnostics

Add OpenFileDescriptors and MaxFileDescriptors fields to
SupportPacketDiagnostics.Server. On Linux the open count is read from
/proc/self/fd; on macOS from /dev/fd. Both platforms use
syscall.Getrlimit(RLIMIT_NOFILE) for the soft limit. Unsupported
platforms (e.g. Windows) return -1 for both fields.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* MM-67978: Move OpenFileDescriptors/MaxFileDescriptors after TotalMemoryMB

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: guard int64 overflow in getMaxFileDescriptors and update tests for unsupported platforms

Add overflow check before casting rlimit.Cur (uint64) to int64 in
getMaxFileDescriptors on Linux. Update fd_test.go and
support_packet_test.go assertions to accept -1 as a valid sentinel for
unsupported platforms instead of requiring a strictly positive value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: remove unrelated test refactoring from support_packet_test.go

Restore the three unrelated changes that crept into this branch:
- SetLogRootPathOverride instead of t.Setenv("MM_LOG_PATH", ...)
- installTypeOverride field instead of t.Setenv(envVarInstallType, ...)
- Restore explanatory comment in TestGetSanitizedConfigFile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Schumacher
2026-04-21 09:54:32 +00:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent bfaff4da7e
commit 848ceb3c73
7 changed files with 130 additions and 10 deletions
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build darwin
package platform
import (
"os"
"syscall"
)
// getOpenFileDescriptors returns the number of open file descriptors for the current process.
func getOpenFileDescriptors() (int64, error) {
entries, err := os.ReadDir("/dev/fd")
if err != nil {
return -1, err
}
// Subtract 1 because the ReadDir call itself opens a file descriptor that appears in the listing.
return max(int64(len(entries))-1, 0), nil
}
// getMaxFileDescriptors returns the soft file descriptor limit for the current process.
func getMaxFileDescriptors() (int64, error) {
var rlimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit); err != nil {
return -1, err
}
return int64(rlimit.Cur), nil
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build linux
package platform
import (
"fmt"
"math"
"os"
"syscall"
)
// getOpenFileDescriptors returns the number of open file descriptors for the current process.
func getOpenFileDescriptors() (int64, error) {
entries, err := os.ReadDir("/proc/self/fd")
if err != nil {
return -1, err
}
// Subtract 1 because the ReadDir call itself opens a file descriptor that appears in the listing.
return max(int64(len(entries))-1, 0), nil
}
// getMaxFileDescriptors returns the soft file descriptor limit for the current process.
func getMaxFileDescriptors() (int64, error) {
var rlimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit); err != nil {
return -1, err
}
if rlimit.Cur > math.MaxInt64 {
return -1, fmt.Errorf("rlimit.Cur %d overflows int64", rlimit.Cur)
}
return int64(rlimit.Cur), nil
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !linux && !darwin
package platform
// getOpenFileDescriptors returns -1 on unsupported platforms.
func getOpenFileDescriptors() (int64, error) {
return -1, nil
}
// getMaxFileDescriptors returns -1 on unsupported platforms.
func getMaxFileDescriptors() (int64, error) {
return -1, nil
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetOpenFileDescriptors(t *testing.T) {
count, err := getOpenFileDescriptors()
require.NoError(t, err)
if count == -1 {
return
}
assert.Positive(t, count)
}
func TestGetMaxFileDescriptors(t *testing.T) {
maxFDs, err := getMaxFileDescriptors()
require.NoError(t, err)
// -1 means unsupported platform; otherwise should be positive
assert.True(t, maxFDs == -1 || maxFDs > 0, "maxFDs should be -1 (unsupported) or positive, got %d", maxFDs)
}
@@ -118,6 +118,14 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
installationType = unknownDataPoint
}
d.Server.InstallationType = installationType
d.Server.OpenFileDescriptors, err = getOpenFileDescriptors()
if err != nil {
rErr = multierror.Append(rErr, errors.Wrap(err, "error while getting open file descriptor count"))
}
d.Server.MaxFileDescriptors, err = getMaxFileDescriptors()
if err != nil {
rErr = multierror.Append(rErr, errors.Wrap(err, "error while getting max file descriptor limit"))
}
d.Server.ProcessID = os.Getpid()
/* Config */
@@ -210,6 +210,8 @@ func TestGetSupportPacketDiagnostics(t *testing.T) {
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)
assert.Positive(t, d.Server.ProcessID)
/* Config */
+12 -10
View File
@@ -25,16 +25,18 @@ type SupportPacketDiagnostics struct {
} `yaml:"license"`
Server struct {
OS string `yaml:"os"`
Architecture string `yaml:"architecture"`
CPUCores int `yaml:"cpu_cores"`
TotalMemoryMB uint64 `yaml:"total_memory_mb"`
Hostname string `yaml:"hostname"`
ProcessID int `yaml:"process_id"`
Version string `yaml:"version"`
BuildHash string `yaml:"build_hash"`
GoVersion string `yaml:"go_version"`
InstallationType string `yaml:"installation_type"`
OS string `yaml:"os"`
Architecture string `yaml:"architecture"`
CPUCores int `yaml:"cpu_cores"`
TotalMemoryMB uint64 `yaml:"total_memory_mb"`
OpenFileDescriptors int64 `yaml:"open_file_descriptors"`
MaxFileDescriptors int64 `yaml:"max_file_descriptors"`
Hostname string `yaml:"hostname"`
ProcessID int `yaml:"process_id"`
Version string `yaml:"version"`
BuildHash string `yaml:"build_hash"`
GoVersion string `yaml:"go_version"`
InstallationType string `yaml:"installation_type"`
} `yaml:"server"`
Config struct {