Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-58085] Implement system info API endpoint #136

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.37
github.com/prometheus/client_golang v1.15.0
github.com/prometheus/procfs v0.9.0
github.com/stretchr/testify v1.9.0
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.22.0
Expand Down Expand Up @@ -60,7 +61,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
Expand Down
39 changes: 39 additions & 0 deletions service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ func (c *Client) Send(msg ClientMessage) error {
return c.wsClient.Send(ws.BinaryMessage, data)
}

func (c *Client) Connected() bool {
c.mut.RLock()
defer c.mut.RUnlock()

if c.closed || c.wsClient == nil {
return false
}

return c.wsClient.GetConnState() == ws.WSConnOpen
}

func (c *Client) ReceiveCh() <-chan ClientMessage {
return c.receiveCh
}
Expand Down Expand Up @@ -369,3 +380,31 @@ func (c *Client) GetVersionInfo() (VersionInfo, error) {

return info, nil
}

func (c *Client) GetSystemInfo() (SystemInfo, error) {
if c.httpClient == nil {
return SystemInfo{}, fmt.Errorf("http client is not initialized")
}

req, err := http.NewRequest("GET", c.cfg.httpURL+"/system", nil)
if err != nil {
return SystemInfo{}, fmt.Errorf("failed to build request: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return SystemInfo{}, fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()

var info SystemInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return SystemInfo{}, fmt.Errorf("decoding http response failed: %w", err)
}

if resp.StatusCode != http.StatusOK {
return SystemInfo{}, fmt.Errorf("request failed with status %s", resp.Status)
}

return info, nil
}
20 changes: 20 additions & 0 deletions service/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,3 +756,23 @@ func TestReconnectClientHerd(t *testing.T) {
// We wait for all clients to reconnect successfully.
reconnectWg.Wait()
}

func TestClientGetSystemInfo(t *testing.T) {
th := SetupTestHelper(t, nil)
defer th.Teardown()

c, err := NewClient(ClientConfig{
URL: th.apiURL,
AuthKey: th.srvc.cfg.API.Security.AdminSecretKey,
})
require.NoError(t, err)
require.NotNil(t, c)
defer c.Close()

t.Run("success", func(t *testing.T) {
info, err := c.GetSystemInfo()
require.NoError(t, err)
require.NotEmpty(t, info)
require.NotEmpty(t, info)
})
}
12 changes: 11 additions & 1 deletion service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
"github.com/mattermost/rtcd/service/store"
"github.com/mattermost/rtcd/service/ws"

godeltaprof "github.com/grafana/pyroscope-go/godeltaprof/http/pprof"
"github.com/mattermost/mattermost/server/public/shared/mlog"

godeltaprof "github.com/grafana/pyroscope-go/godeltaprof/http/pprof"
"github.com/prometheus/procfs"
)

type Service struct {
Expand All @@ -31,6 +33,7 @@ type Service struct {
store store.Store
auth *auth.Service
metrics *perf.Metrics
proc procfs.FS
log *mlog.Logger
sessionCache *auth.SessionCache
// connMap maps user sessions to the websocket connection they originated
Expand Down Expand Up @@ -60,6 +63,12 @@ func New(cfg Config) (*Service, error) {

s.log.Info("rtcd: starting up", getVersionInfo().logFields()...)

proc, err := procfs.NewDefaultFS()
if err != nil {
s.log.Error("failed to create proc file-system", mlog.Err(err))
}
s.proc = proc

s.store, err = store.New(cfg.Store.DataSource)
if err != nil {
return nil, fmt.Errorf("failed to create store: %w", err)
Expand Down Expand Up @@ -102,6 +111,7 @@ func New(cfg Config) (*Service, error) {
s.apiServer.RegisterHandleFunc("/register", s.registerClient)
s.apiServer.RegisterHandleFunc("/unregister", s.unregisterClient)
s.apiServer.RegisterHandler("/ws", s.wsServer)
s.apiServer.RegisterHandleFunc("/system", s.getSystemInfo)

if val := os.Getenv("PERF_PROFILES"); val == "true" {
runtime.SetMutexProfileFraction(5)
Expand Down
37 changes: 37 additions & 0 deletions service/system.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package service

import (
"encoding/json"
"net/http"

"github.com/mattermost/mattermost/server/public/shared/mlog"

"github.com/prometheus/procfs"
)

type SystemInfo struct {
Load procfs.LoadAvg `json:"load"`
}

func (s *Service) getSystemInfo(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.NotFound(w, req)
return
}

var info SystemInfo
avg, err := s.proc.LoadAvg()
if err == nil {
info.Load = *avg
} else {
s.log.Error("failed to get load average", mlog.Err(err))
}

w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&info); err != nil {
s.log.Error("failed to encode data", mlog.Err(err))
}
}
34 changes: 34 additions & 0 deletions service/system_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package service

import (
"encoding/json"
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func TestGetSystem(t *testing.T) {
th := SetupTestHelper(t, nil)
defer th.Teardown()

t.Run("invalid method", func(t *testing.T) {
resp, err := http.Post(th.apiURL+"/system", "", nil)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})

t.Run("valid response", func(t *testing.T) {
resp, err := http.Get(th.apiURL + "/system")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
var info SystemInfo
err = json.NewDecoder(resp.Body).Decode(&info)
require.NoError(t, err)
require.NotEmpty(t, info.Load)
})
}
18 changes: 9 additions & 9 deletions service/ws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import (
)

const (
wsConnClosed int32 = iota
wsConnOpen
wsConnClosing
WSConnClosed int32 = iota
WSConnOpen
WSConnClosing
)

type Client struct {
Expand Down Expand Up @@ -86,7 +86,7 @@ func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) {
c.wg.Add(2)
go c.connReader()
go c.connWriter()
c.setConnState(wsConnOpen)
c.setConnState(WSConnOpen)

return c, nil
}
Expand All @@ -98,7 +98,7 @@ func (c *Client) connReader() {
close(c.conn.closeCh)
c.wg.Wait()
close(c.errorCh)
c.setConnState(wsConnClosed)
c.setConnState(WSConnClosed)
}()

c.conn.ws.SetReadLimit(connMaxReadBytes)
Expand Down Expand Up @@ -164,7 +164,7 @@ func (c *Client) connWriter() {
}

func (c *Client) sendError(err error) {
if c.getConnState() != wsConnOpen {
if c.GetConnState() != WSConnOpen {
return
}
select {
Expand All @@ -176,7 +176,7 @@ func (c *Client) sendError(err error) {

// SendMsg sends a WebSocket message with the specified type and data.
func (c *Client) Send(mt MessageType, data []byte) error {
if c.getConnState() != wsConnOpen {
if c.GetConnState() != WSConnOpen {
return fmt.Errorf("failed to send message: connection is closed")
}

Expand Down Expand Up @@ -206,7 +206,7 @@ func (c *Client) ErrorCh() <-chan error {

// Close closes the underlying WebSocket connection.
func (c *Client) Close() error {
c.setConnState(wsConnClosing)
c.setConnState(WSConnClosing)
if err := c.flush(); err != nil {
return err
}
Expand All @@ -219,7 +219,7 @@ func (c *Client) setConnState(st int32) {
atomic.StoreInt32(&c.connState, st)
}

func (c *Client) getConnState() int32 {
func (c *Client) GetConnState() int32 {
return atomic.LoadInt32(&c.connState)
}

Expand Down
Loading