Skip to content

Commit

Permalink
Implement system info API endpoint (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
streamer45 authored May 7, 2024
1 parent eb3c3f0 commit 2f3bdeb
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 11 deletions.
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

0 comments on commit 2f3bdeb

Please sign in to comment.