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

Monitoring metrics and additional logging #76

Merged
merged 8 commits into from
Aug 11, 2023
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
12 changes: 9 additions & 3 deletions api/gin/webServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import (
"strings"
"sync"

logger "github.com/multiversx/mx-chain-logger-go"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/multiversx/mx-chain-core-go/core/check"
logger "github.com/multiversx/mx-chain-logger-go"
apiErrors "github.com/multiversx/mx-chain-notifier-go/api/errors"
"github.com/multiversx/mx-chain-notifier-go/api/groups"
"github.com/multiversx/mx-chain-notifier-go/api/shared"
"github.com/multiversx/mx-chain-notifier-go/common"
"github.com/multiversx/mx-chain-notifier-go/config"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)

var log = logger.GetOrCreate("api/gin")
Expand Down Expand Up @@ -119,6 +119,12 @@ func (w *webServer) createGroups() error {
}
groupsMap["events"] = eventsGroup

statusGroup, err := groups.NewStatusGroup(w.facade)
if err != nil {
return err
}
groupsMap["status"] = statusGroup

if w.apiType == common.WSAPIType {
hubHandler, err := groups.NewHubGroup(w.facade)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion api/groups/baseGroup.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package groups

import (
"github.com/gin-gonic/gin"
logger "github.com/multiversx/mx-chain-logger-go"
"github.com/multiversx/mx-chain-notifier-go/api/shared"
"github.com/gin-gonic/gin"
)

var log = logger.GetOrCreate("api/groups")
Expand Down
77 changes: 77 additions & 0 deletions api/groups/statusGroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package groups

import (
"fmt"
"net/http"
"sync"

"github.com/gin-gonic/gin"
"github.com/multiversx/mx-chain-core-go/core/check"
"github.com/multiversx/mx-chain-notifier-go/api/errors"
"github.com/multiversx/mx-chain-notifier-go/api/shared"
)

const (
metricsPath = "/metrics"
prometheusMetricsPath = "/prometheus-metrics"
)

type statusGroup struct {
*baseGroup
facade shared.FacadeHandler
mutFacade sync.RWMutex
additionalMiddlewares []gin.HandlerFunc
}

// NewStatusGroup returns a new instance of status group
func NewStatusGroup(facade shared.FacadeHandler) (*statusGroup, error) {
if check.IfNil(facade) {
return nil, fmt.Errorf("%w for status group", errors.ErrNilFacadeHandler)
}

sg := &statusGroup{
facade: facade,
baseGroup: &baseGroup{},
additionalMiddlewares: make([]gin.HandlerFunc, 0),
}

endpoints := []*shared.EndpointHandlerData{
{
Path: metricsPath,
Handler: sg.getMetrics,
Method: http.MethodGet,
},
{
Path: prometheusMetricsPath,
Handler: sg.getPrometheusMetrics,
Method: http.MethodGet,
},
}
sg.endpoints = endpoints

return sg, nil
}

// getMetrics will expose the notifier's metrics statistics in json format
func (sg *statusGroup) getMetrics(c *gin.Context) {
metricsResults := sg.facade.GetMetrics()

shared.JSONResponse(c, http.StatusOK, gin.H{"metrics": metricsResults}, "")
}

// getPrometheusMetrics will expose notifier's metrics in prometheus format
func (sg *statusGroup) getPrometheusMetrics(c *gin.Context) {
metricsResults := sg.facade.GetMetricsForPrometheus()

c.String(http.StatusOK, metricsResults)
}

// GetAdditionalMiddlewares returns additional middlewares for this group
func (sg *statusGroup) GetAdditionalMiddlewares() []gin.HandlerFunc {
return sg.additionalMiddlewares
}

// IsInterfaceNil returns true if there is no value under the interface
func (sg *statusGroup) IsInterfaceNil() bool {
return sg == nil
}
115 changes: 115 additions & 0 deletions api/groups/statusGroup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package groups_test

import (
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/multiversx/mx-chain-core-go/core/check"
apiErrors "github.com/multiversx/mx-chain-notifier-go/api/errors"
"github.com/multiversx/mx-chain-notifier-go/api/groups"
"github.com/multiversx/mx-chain-notifier-go/data"
"github.com/multiversx/mx-chain-notifier-go/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const statusPath = "/status"

type statusMetricsResponse struct {
Data struct {
Metrics map[string]*data.EndpointMetricsResponse `json:"metrics"`
}
Error string `json:"error"`
}

func TestNewStatusGroup(t *testing.T) {
t.Parallel()

t.Run("nil facade should error", func(t *testing.T) {
t.Parallel()

sg, err := groups.NewStatusGroup(nil)

require.True(t, errors.Is(err, apiErrors.ErrNilFacadeHandler))
require.True(t, check.IfNil(sg))
})

t.Run("should work", func(t *testing.T) {
t.Parallel()

sg, err := groups.NewStatusGroup(&mocks.FacadeStub{})

assert.NotNil(t, sg)
assert.Nil(t, err)
})
}

func TestGetMetrics_ShouldWork(t *testing.T) {
t.Parallel()

expectedMetrics := map[string]*data.EndpointMetricsResponse{
"/guardian/config": {
NumRequests: 5,
TotalResponseTime: 100,
},
}
facade := &mocks.FacadeStub{
GetMetricsCalled: func() map[string]*data.EndpointMetricsResponse {
return expectedMetrics
},
}

statusGroup, err := groups.NewStatusGroup(facade)
require.Nil(t, err)

ws := startWebServer(statusGroup, statusPath)

req, _ := http.NewRequest("GET", "/status/metrics", nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

var apiResp statusMetricsResponse
loadResponse(resp.Body, &apiResp)
require.Equal(t, http.StatusOK, resp.Code)

require.Equal(t, expectedMetrics, apiResp.Data.Metrics)
}

func TestGetPrometheusMetrics_ShouldWork(t *testing.T) {
t.Parallel()

expectedMetrics := `num_requests{endpoint="/guardian/config"} 37`
facade := &mocks.FacadeStub{
GetMetricsForPrometheusCalled: func() string {
return expectedMetrics
},
}

statusGroup, err := groups.NewStatusGroup(facade)
require.NoError(t, err)

ws := startWebServer(statusGroup, statusPath)

req, _ := http.NewRequest("GET", "/status/prometheus-metrics", nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

bodyBytes, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)

require.Equal(t, http.StatusOK, resp.Code)
require.Equal(t, expectedMetrics, string(bodyBytes))
}

func TestStatusGroup_IsInterfaceNil(t *testing.T) {
t.Parallel()

sg, _ := groups.NewStatusGroup(nil)
assert.True(t, sg.IsInterfaceNil())

sg, _ = groups.NewStatusGroup(&mocks.FacadeStub{})
assert.False(t, sg.IsInterfaceNil())
}
4 changes: 3 additions & 1 deletion api/shared/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package shared
import (
"net/http"

"github.com/multiversx/mx-chain-notifier-go/data"
"github.com/gin-gonic/gin"
"github.com/multiversx/mx-chain-notifier-go/data"
)

// HTTPServerCloser defines the basic actions of starting and closing that a web server should be able to do
Expand All @@ -29,6 +29,8 @@ type FacadeHandler interface {
HandleFinalizedEvents(finalizedBlock data.FinalizedBlock)
GetConnectorUserAndPass() (string, string)
ServeHTTP(w http.ResponseWriter, r *http.Request)
GetMetrics() map[string]*data.EndpointMetricsResponse
GetMetricsForPrometheus() string
IsInterfaceNil() bool
}

Expand Down
3 changes: 3 additions & 0 deletions common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ var ErrInvalidRedisConnType = errors.New("invalid redis connection type")

// ErrReceivedEmptyEvents signals that empty events have been received
var ErrReceivedEmptyEvents = errors.New("received empty events")

// ErrNilStatusMetricsHandler signals that a nil status metrics handler has been provided
var ErrNilStatusMetricsHandler = errors.New("nil status metrics handler")
15 changes: 15 additions & 0 deletions common/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package common

import (
"time"

"github.com/multiversx/mx-chain-notifier-go/data"
)

// StatusMetricsHandler defines the behavior of a component that handles status metrics
type StatusMetricsHandler interface {
AddRequest(path string, duration time.Duration)
GetAll() map[string]*data.EndpointMetricsResponse
GetMetricsForPrometheus() string
IsInterfaceNil() bool
}
11 changes: 11 additions & 0 deletions data/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package data

import (
"time"
)

// EndpointMetricsResponse defines the response for status metrics endpoint
type EndpointMetricsResponse struct {
NumRequests uint64 `json:"num_requests"`
TotalResponseTime time.Duration `json:"total_response_time"`
}
25 changes: 21 additions & 4 deletions facade/notifierFacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/multiversx/mx-chain-core-go/core/check"
logger "github.com/multiversx/mx-chain-logger-go"
"github.com/multiversx/mx-chain-notifier-go/common"
"github.com/multiversx/mx-chain-notifier-go/config"
"github.com/multiversx/mx-chain-notifier-go/data"
"github.com/multiversx/mx-chain-notifier-go/dispatcher"
Expand All @@ -14,17 +15,19 @@ var log = logger.GetOrCreate("facade")

// ArgsNotifierFacade defines the arguments necessary for notifierFacade creation
type ArgsNotifierFacade struct {
APIConfig config.ConnectorApiConfig
EventsHandler EventsHandler
WSHandler dispatcher.WSHandler
EventsInterceptor EventsInterceptor
APIConfig config.ConnectorApiConfig
EventsHandler EventsHandler
WSHandler dispatcher.WSHandler
EventsInterceptor EventsInterceptor
StatusMetricsHandler common.StatusMetricsHandler
}

type notifierFacade struct {
config config.ConnectorApiConfig
eventsHandler EventsHandler
wsHandler dispatcher.WSHandler
eventsInterceptor EventsInterceptor
statusMetrics common.StatusMetricsHandler
}

// NewNotifierFacade creates a new notifier facade instance
Expand All @@ -39,6 +42,7 @@ func NewNotifierFacade(args ArgsNotifierFacade) (*notifierFacade, error) {
config: args.APIConfig,
wsHandler: args.WSHandler,
eventsInterceptor: args.EventsInterceptor,
statusMetrics: args.StatusMetricsHandler,
}, nil
}

Expand All @@ -52,6 +56,9 @@ func checkArgs(args ArgsNotifierFacade) error {
if check.IfNil(args.EventsInterceptor) {
return ErrNilEventsInterceptor
}
if check.IfNil(args.StatusMetricsHandler) {
return common.ErrNilStatusMetricsHandler
}

return nil
}
Expand Down Expand Up @@ -149,6 +156,16 @@ func (nf *notifierFacade) GetConnectorUserAndPass() (string, string) {
return nf.config.Username, nf.config.Password
}

// GetMetrics will return metrics in json format
func (nf *notifierFacade) GetMetrics() map[string]*data.EndpointMetricsResponse {
return nf.statusMetrics.GetAll()
}

// GetMetricsForPrometheus will return metrics in prometheus format
func (nf *notifierFacade) GetMetricsForPrometheus() string {
return nf.statusMetrics.GetMetricsForPrometheus()
}

// IsInterfaceNil returns true if there is no value under the interface
func (nf *notifierFacade) IsInterfaceNil() bool {
return nf == nil
Expand Down
21 changes: 17 additions & 4 deletions facade/notifierFacade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/multiversx/mx-chain-core-go/data/block"
"github.com/multiversx/mx-chain-core-go/data/smartContractResult"
"github.com/multiversx/mx-chain-core-go/data/transaction"
"github.com/multiversx/mx-chain-notifier-go/common"
"github.com/multiversx/mx-chain-notifier-go/config"
"github.com/multiversx/mx-chain-notifier-go/data"
"github.com/multiversx/mx-chain-notifier-go/facade"
Expand All @@ -20,10 +21,11 @@ import (

func createMockFacadeArgs() facade.ArgsNotifierFacade {
return facade.ArgsNotifierFacade{
EventsHandler: &mocks.EventsHandlerStub{},
APIConfig: config.ConnectorApiConfig{},
WSHandler: &mocks.WSHandlerStub{},
EventsInterceptor: &mocks.EventsInterceptorStub{},
EventsHandler: &mocks.EventsHandlerStub{},
APIConfig: config.ConnectorApiConfig{},
WSHandler: &mocks.WSHandlerStub{},
EventsInterceptor: &mocks.EventsInterceptorStub{},
StatusMetricsHandler: &mocks.StatusMetricsStub{},
}
}

Expand Down Expand Up @@ -63,6 +65,17 @@ func TestNewNotifierFacade(t *testing.T) {
require.Equal(t, facade.ErrNilEventsInterceptor, err)
})

t.Run("nil status metrics handler", func(t *testing.T) {
t.Parallel()

args := createMockFacadeArgs()
args.StatusMetricsHandler = nil

f, err := facade.NewNotifierFacade(args)
require.True(t, check.IfNil(f))
require.Equal(t, common.ErrNilStatusMetricsHandler, err)
})

t.Run("should work", func(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading