mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-08 12:08:41 -05:00
[MM-67976] Add server uptime to support packet (#35838)
* MM-67976: Add server uptime to support packet Add process start time (started_at) and approximate host start time (host_started_at, Linux only, derived from /proc/uptime) to the support packet diagnostics. This helps diagnose restart loops and distinguish process restarts from full machine/container reboots. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Add HostStartedAt assertions to support packet happy-path test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert unrelated changes * Add unit tests for getHostUptimeSeconds and fix cross-platform portability - Extract parseUptimeFile helper in uptime_linux.go to enable unit testing with synthetic file paths without touching the real /proc/uptime - Add uptime_linux_test.go covering all error paths (file read error, empty file, non-numeric value) and happy-path parsing - Add uptime_other_test.go asserting the non-Linux stub returns ErrHostUptimeUnsupportedPlatform and zero seconds - Guard HostStartedAt assertions in support_packet_test.go with runtime.GOOS so the test correctly asserts zero-time on non-Linux platforms instead of failing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: use t.TempDir() for guaranteed-missing path in uptime test Replace the hard-coded /nonexistent/proc/uptime path with a path constructed from t.TempDir() to guarantee the file is absent without relying on filesystem layout assumptions. Also remove the misleading comment about swapping implementations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Reorder Server struct fields and assignments to group process identity fields together Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add macOS support for host uptime in support packet 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:
co-authored by
Claude Sonnet 4.6
parent
848ceb3c73
commit
58052db8e3
@@ -120,6 +120,8 @@ type PlatformService struct {
|
||||
|
||||
pdpService einterfaces.PolicyDecisionPointInterface
|
||||
|
||||
startTime time.Time
|
||||
|
||||
// installTypeOverride overrides MM_INSTALL_TYPE in support packet diagnostics.
|
||||
installTypeOverride string
|
||||
|
||||
@@ -150,6 +152,7 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
|
||||
Store: sc.Store,
|
||||
clusterIFace: sc.Cluster,
|
||||
hashSeed: maphash.MakeSeed(),
|
||||
startTime: time.Now(),
|
||||
goroutineExitSignal: make(chan struct{}, 1),
|
||||
goroutineBuffered: make(chan struct{}, runtime.NumCPU()),
|
||||
WebSocketRouter: &WebSocketRouter{
|
||||
|
||||
@@ -107,6 +107,11 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
|
||||
if err != nil {
|
||||
rErr = multierror.Append(errors.Wrap(err, "error while getting hostname"))
|
||||
}
|
||||
d.Server.ProcessID = os.Getpid()
|
||||
d.Server.StartedAt = ps.startTime.UTC()
|
||||
if hostUptimeSeconds, hostUptimeErr := getHostUptimeSeconds(); hostUptimeErr == nil {
|
||||
d.Server.HostStartedAt = time.Now().Add(-time.Duration(hostUptimeSeconds) * time.Second).UTC()
|
||||
}
|
||||
d.Server.Version = model.CurrentVersion
|
||||
d.Server.BuildHash = model.BuildHash
|
||||
d.Server.GoVersion = runtime.Version()
|
||||
@@ -126,7 +131,6 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
|
||||
if err != nil {
|
||||
rErr = multierror.Append(rErr, errors.Wrap(err, "error while getting max file descriptor limit"))
|
||||
}
|
||||
d.Server.ProcessID = os.Getpid()
|
||||
|
||||
/* Config */
|
||||
d.Config.Source = ps.DescribeConfig()
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -213,6 +214,13 @@ func TestGetSupportPacketDiagnostics(t *testing.T) {
|
||||
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" {
|
||||
assert.False(t, d.Server.HostStartedAt.IsZero())
|
||||
assert.True(t, !d.Server.HostStartedAt.After(d.Server.StartedAt))
|
||||
} else {
|
||||
assert.True(t, d.Server.HostStartedAt.IsZero(), "HostStartedAt should be zero on unsupported platforms")
|
||||
}
|
||||
|
||||
/* Config */
|
||||
assert.Equal(t, "memory://", d.Config.Source)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// getHostUptimeSeconds uses kern.boottime sysctl to return the number of seconds
|
||||
// the host OS has been running.
|
||||
func getHostUptimeSeconds() (int64, error) {
|
||||
tv, err := unix.SysctlTimeval("kern.boottime")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get kern.boottime: %w", err)
|
||||
}
|
||||
bootTime := time.Unix(tv.Sec, int64(tv.Usec)*int64(time.Microsecond))
|
||||
return int64(time.Since(bootTime).Seconds()), nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetHostUptimeSeconds(t *testing.T) {
|
||||
t.Run("returns a positive value from kern.boottime", func(t *testing.T) {
|
||||
seconds, err := getHostUptimeSeconds()
|
||||
require.NoError(t, err)
|
||||
assert.Positive(t, seconds)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getHostUptimeSeconds reads /proc/uptime and returns the number of seconds
|
||||
// the host OS has been running.
|
||||
func getHostUptimeSeconds() (int64, error) {
|
||||
return parseUptimeFile("/proc/uptime")
|
||||
}
|
||||
|
||||
// parseUptimeFile reads an uptime file in the /proc/uptime format and returns
|
||||
// the number of seconds as an int64. It is a separate function to allow unit
|
||||
// tests to supply a synthetic file path.
|
||||
func parseUptimeFile(path string) (int64, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read /proc/uptime: %w", err)
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) == 0 {
|
||||
return 0, fmt.Errorf("unexpected /proc/uptime format")
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse /proc/uptime value: %w", err)
|
||||
}
|
||||
|
||||
return int64(f), nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetHostUptimeSeconds(t *testing.T) {
|
||||
t.Run("returns a positive value from /proc/uptime", func(t *testing.T) {
|
||||
seconds, err := getHostUptimeSeconds()
|
||||
require.NoError(t, err)
|
||||
assert.Positive(t, seconds)
|
||||
})
|
||||
|
||||
t.Run("error on unreadable file", func(t *testing.T) {
|
||||
missingPath := t.TempDir() + "/uptime"
|
||||
_, err := parseUptimeFile(missingPath)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to read")
|
||||
})
|
||||
|
||||
t.Run("error on empty file", func(t *testing.T) {
|
||||
f, err := os.CreateTemp(t.TempDir(), "uptime")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
_, err = parseUptimeFile(f.Name())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unexpected /proc/uptime format")
|
||||
})
|
||||
|
||||
t.Run("error on non-numeric first field", func(t *testing.T) {
|
||||
f, err := os.CreateTemp(t.TempDir(), "uptime")
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteString("notanumber 0.00\n")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
_, err = parseUptimeFile(f.Name())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to parse /proc/uptime value")
|
||||
})
|
||||
|
||||
t.Run("parses valid uptime correctly", func(t *testing.T) {
|
||||
f, err := os.CreateTemp(t.TempDir(), "uptime")
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteString("12345.67 890.12\n")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
seconds, err := parseUptimeFile(f.Name())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(12345), seconds)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrHostUptimeUnsupportedPlatform is returned when host uptime detection is not supported on the current platform.
|
||||
var ErrHostUptimeUnsupportedPlatform = errors.New("host uptime detection not supported on this platform")
|
||||
|
||||
// getHostUptimeSeconds returns an error on non-Linux platforms.
|
||||
func getHostUptimeSeconds() (int64, error) {
|
||||
return 0, ErrHostUptimeUnsupportedPlatform
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetHostUptimeSeconds(t *testing.T) {
|
||||
seconds, err := getHostUptimeSeconds()
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrHostUptimeUnsupportedPlatform)
|
||||
assert.Equal(t, int64(0), seconds)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package model
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,18 +26,20 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
StartedAt time.Time `yaml:"started_at"`
|
||||
HostStartedAt time.Time `yaml:"host_started_at,omitempty"`
|
||||
Version string `yaml:"version"`
|
||||
BuildHash string `yaml:"build_hash"`
|
||||
GoVersion string `yaml:"go_version"`
|
||||
InstallationType string `yaml:"installation_type"`
|
||||
} `yaml:"server"`
|
||||
|
||||
Config struct {
|
||||
|
||||
Reference in New Issue
Block a user