Skip to content

Commit

Permalink
Add ability to mock metrics and fix their handling (#26)
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Shishkin <me@teran.dev>
  • Loading branch information
teran authored Jun 3, 2024
1 parent 690473c commit 26de5a8
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 37 deletions.
5 changes: 4 additions & 1 deletion cmd/anycastd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ func main() {
NextHop: cfg.Announcer.LocalAddress,
LocalASN: cfg.Announcer.LocalASN,
})
svc := service.New(svcCfg.Name, a, checks, time.Duration(svcCfg.CheckInterval))

metrics := service.NewMetrics()

svc := service.New(svcCfg.Name, a, checks, time.Duration(svcCfg.CheckInterval), metrics)

g.Go(func() error {
return svc.Run(ctx)
Expand Down
41 changes: 31 additions & 10 deletions service/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,36 @@ import (
"github.com/prometheus/client_golang/prometheus"
)

var up = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "anycastd",
Name: "up",
Help: "Service liveness status based on checks",
},
[]string{"service"},
)
type Metrics interface {
ServiceUp(service string)
ServiceDown(service string)
}

type metrics struct {
upGauge *prometheus.GaugeVec
}

func NewMetrics() Metrics {
upGauge := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "anycastd",
Name: "up",
Help: "Service liveness status based on checks",
},
[]string{"service"},
)

prometheus.MustRegister(upGauge)

return &metrics{
upGauge: upGauge,
}
}

func (m *metrics) ServiceUp(service string) {
m.upGauge.WithLabelValues(service).Set(1.0)
}

func init() {
prometheus.MustRegister(up)
func (m *metrics) ServiceDown(service string) {
m.upGauge.WithLabelValues(service).Set(0.0)
}
23 changes: 23 additions & 0 deletions service/metrics_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package service

import (
"github.com/stretchr/testify/mock"
)

var _ Metrics = (*MetricsMock)(nil)

type MetricsMock struct {
mock.Mock
}

func NewMetricsMock() *MetricsMock {
return &MetricsMock{}
}

func (m *MetricsMock) ServiceUp(service string) {
m.Called(service)
}

func (m *MetricsMock) ServiceDown(service string) {
m.Called(service)
}
8 changes: 6 additions & 2 deletions service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ type service struct {
announcer announcer.Announcer
checks []checkers.Checker
interval time.Duration
metrics Metrics
}

func New(
name string,
a announcer.Announcer,
checks []checkers.Checker,
interval time.Duration,
metrics Metrics,
) Service {
return &service{
name: name,
announcer: a,
checks: checks,
interval: interval,
metrics: metrics,
}
}

Expand All @@ -54,16 +57,17 @@ func (s *service) run(ctx context.Context) error {
if err := check.Check(ctx); err != nil {
log.Warnf("check failed: %s", err)

up.WithLabelValues(s.name).Set(0.0)
s.metrics.ServiceDown(s.name)

if err := s.announcer.Denounce(ctx); err != nil {
log.Warnf("denounce failed: %s", err)
return nil
}
return nil
}
}

up.WithLabelValues(s.name).Set(1.0)
s.metrics.ServiceUp(s.name)

if err := s.announcer.Announce(ctx); err != nil {
log.Warnf("announce failed: %s", err)
Expand Down
85 changes: 61 additions & 24 deletions service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

"github.com/teran/anycastd/announcer"
"github.com/teran/anycastd/checkers"
)
Expand All @@ -16,41 +17,77 @@ func init() {
log.SetLevel(log.TraceLevel)
}

func TestRunPass(t *testing.T) {
r := require.New(t)
func (s *serviceTestSuite) TestRunPass() {
s.announcerM.On("Announce").Return(nil).Once()

s.checkM.On("Check").Return(nil).Once()

announcerM := announcer.NewMock()
defer announcerM.AssertExpectations(t)
s.metricsM.On("ServiceUp", "test_service").Return().Once()

announcerM.On("Announce").Return(nil).Once()
svc := New("test_service", s.announcerM, []checkers.Checker{s.checkM}, 1*time.Second, s.metricsM).(*service)

err := svc.run(s.ctx)
s.Require().NoError(err)
}

checkM := checkers.NewMock()
defer checkM.AssertExpectations(t)
func (s *serviceTestSuite) TestRunFail() {
s.announcerM.On("Denounce").Return(nil).Once()

checkM.On("Check").Return(nil).Once()
s.checkM.On("Check").Return(errors.New("error")).Once()

svc := New("test_service", announcerM, []checkers.Checker{checkM}, 1*time.Second).(*service)
s.metricsM.On("ServiceDown", "test_service").Return().Once()

err := svc.run(context.TODO())
r.NoError(err)
svc := New("test_service", s.announcerM, []checkers.Checker{s.checkM}, 1*time.Second, s.metricsM).(*service)

err := svc.run(s.ctx)
s.Require().NoError(err)
}

func TestRunFail(t *testing.T) {
r := require.New(t)
func (s *serviceTestSuite) TestRunPassThenFailThenPass() {
aCall1 := s.announcerM.On("Announce").Return(nil).Once()
aCall2 := s.announcerM.On("Denounce").Return(nil).NotBefore(aCall1).Once()
s.announcerM.On("Announce").Return(nil).NotBefore(aCall2).Once()

cCall1 := s.checkM.On("Check").Return(nil).Once()
cCall2 := s.checkM.On("Check").Return(errors.New("error")).NotBefore(cCall1).Once()
s.checkM.On("Check").Return(nil).NotBefore(cCall2).Once()

announcerM := announcer.NewMock()
defer announcerM.AssertExpectations(t)
mCall1 := s.metricsM.On("ServiceUp", "test_service").Return().Once()
mCall2 := s.metricsM.On("ServiceDown", "test_service").Return().NotBefore(mCall1).Once()
s.metricsM.On("ServiceUp", "test_service").Return().NotBefore(mCall2).Once()

announcerM.On("Announce").Return(nil).Once()
announcerM.On("Denounce").Return(nil).Once()
svc := New("test_service", s.announcerM, []checkers.Checker{s.checkM}, 1*time.Second, s.metricsM).(*service)

checkM := checkers.NewMock()
defer checkM.AssertExpectations(t)
for i := 0; i < 3; i++ {
err := svc.run(s.ctx)
s.Require().NoError(err)
}
}

checkM.On("Check").Return(errors.New("error")).Once()
// Definitions ...
type serviceTestSuite struct {
suite.Suite

svc := New("test_service", announcerM, []checkers.Checker{checkM}, 1*time.Second).(*service)
ctx context.Context
announcerM *announcer.Mock
checkM *checkers.Mock
metricsM *MetricsMock
}

func (s *serviceTestSuite) SetupTest() {
s.ctx = context.Background()

s.announcerM = announcer.NewMock()
s.checkM = checkers.NewMock()
s.metricsM = NewMetricsMock()
}

func (s *serviceTestSuite) TearDownTest() {
s.announcerM.AssertExpectations(s.T())
s.checkM.AssertExpectations(s.T())
s.metricsM.AssertExpectations(s.T())
}

err := svc.run(context.TODO())
r.NoError(err)
func TestServiceTestSuite(t *testing.T) {
suite.Run(t, &serviceTestSuite{})
}

0 comments on commit 26de5a8

Please sign in to comment.