Skip to content

Commit

Permalink
Feature/Implement exporter
Browse files Browse the repository at this point in the history
* Added exporter structure
* Added serverPrometheus interface, serverPrometheusWrapper
* Added mocks
  • Loading branch information
bestwebua committed Sep 22, 2024
1 parent 06333bb commit 2160341
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 10 deletions.
6 changes: 6 additions & 0 deletions consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ package heartbeat
import "log"

const (
// Exporter
exporterStartMessage = "Prometheus exporter started on "
exporterErrorMsg = "Failed to start prometheus exporter on port "
exporterShutdownMessage = "Prometheus exporter is in the shutdown mode and won't accept new connections"
exporterStopMessage = "Prometheus exporter stopped gracefully"

// Logger
infoLogLevel = "INFO"
warningLogLevel = "WARNING"
Expand Down
113 changes: 113 additions & 0 deletions exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package heartbeat

import (
"context"
"errors"
"net"
"net/http"
"strconv"
"sync"
"time"

"github.com/prometheus/client_golang/prometheus/promhttp"
)

// WaitGroup interface
type waitGroup interface {
Add(int)
Done()
Wait()
}

// serverPrometheusWrapper structure. Used for testing purposes
type serverPrometheusWrapper struct {
*http.Server
}

// serverPrometheusWrapper methods
func (wrapper *serverPrometheusWrapper) Port() string {
return wrapper.Addr
}

// serverPrometheus interface
type serverPrometheus interface {
ListenAndServe() error
Shutdown(context.Context) error
Port() string
}

// Exporter structure
type exporter struct {
server serverPrometheus
shutdownTimeout time.Duration
err error
route string
ctx context.Context
wg waitGroup
logger logger
}

// Exporter builder. Returns pointer to new exporter structure
func newExporter(port, shutdownTimeout int, route string, logger logger) *exporter {
handler := http.NewServeMux()
handler.Handle(route, promhttp.Handler())

return &exporter{
server: &serverPrometheusWrapper{
Server: &http.Server{
Addr: ":" + strconv.Itoa(port),
Handler: handler,
},
},
logger: logger,
shutdownTimeout: time.Duration(shutdownTimeout),
route: route,
}
}

// Exporter methods

// Starts exporter, runs listen channel from the parent (heartbeat server)
func (exporter *exporter) start(parentContext context.Context, wg *sync.WaitGroup) error {
exporter.ctx, exporter.wg = parentContext, wg
exporter.listenShutdownSignal()
exporter.logger.info(exporterStartMessage + exporter.server.Port() + exporter.route)

return exporter.server.ListenAndServe()
}

// Stops exporter with timeout
func (exporter *exporter) stop() error {
ctx, cancel := context.WithTimeout(exporter.ctx, exporter.shutdownTimeout*time.Second)
defer cancel()

return exporter.server.Shutdown(ctx)
}

// Exporter shutdown signal listener. Stops exporter by shutdown signal
func (exporter *exporter) listenShutdownSignal() {
go func() {
defer exporter.wg.Done()
exporterLogger := exporter.logger

<-exporter.ctx.Done()
exporterLogger.warning(exporterShutdownMessage)
if err := exporter.stop(); err != nil {
exporter.err = err
}

exporterLogger.info(exporterStopMessage)
}()
}

// Exporter port-for-bind checker
func (exporter *exporter) isPortAvailable() (err error) {
port := exporter.server.Port()
listener, err := net.Listen("tcp", port)
if err != nil {
return errors.New(exporterErrorMsg + port)
}

listener.Close()
return
}
107 changes: 107 additions & 0 deletions exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package heartbeat

import (
"context"
"net"
"strconv"
"sync"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestNewExporter(t *testing.T) {
t.Run("creates new exporter", func(t *testing.T) {
port, shutdownTimeout, route, logger := 8080, 42, "/some_route", newLogger(false, false)
exporter := newExporter(port, shutdownTimeout, route, logger)

assert.Equal(t, logger, exporter.logger)
assert.Equal(t, time.Duration(shutdownTimeout), exporter.shutdownTimeout)
assert.Equal(t, ":"+strconv.Itoa(port), exporter.server.Port())
assert.Equal(t, route, exporter.route)
})
}

func TestExporterStart(t *testing.T) {
t.Run("starts exporter server", func(t *testing.T) {
prometheusServer, logger := new(serverPrometheusMock), new(loggerMock)
port, route, parentContext, wg := ":8080", "/some_route", context.Background(), new(sync.WaitGroup)
exporter := &exporter{
server: prometheusServer,
logger: logger,
route: route,
}
prometheusServer.On("Port").Once().Return(port)
logger.On("info", []string{exporterStartMessage + port + route}).Once().Return(nil)
prometheusServer.On("ListenAndServe").Once().Return(nil)

assert.NoError(t, exporter.start(parentContext, wg))
assert.Equal(t, parentContext, exporter.ctx)
assert.Equal(t, wg, exporter.wg)
})
}

func TestExporterStop(t *testing.T) {
t.Run("stops exporter server", func(t *testing.T) {
prometheusServer := new(serverPrometheusMock)
exporter := &exporter{
server: prometheusServer,
ctx: context.Background(),
shutdownTimeout: time.Duration(2),
}
prometheusServer.On("Shutdown", mock.AnythingOfType("*context.timerCtx")).Once().Return(nil)

assert.NoError(t, exporter.stop())
})
}

func TestListenShutdownSignal(t *testing.T) {
t.Run("listens shutdown signal", func(t *testing.T) {
prometheusServer, logger, wg := new(serverPrometheusMock), new(loggerMock), &sync.WaitGroup{}
parentContext, cancel := context.WithCancel(context.Background())
exporter := &exporter{
server: prometheusServer,
logger: logger,
ctx: parentContext,
wg: wg,
shutdownTimeout: time.Duration(2),
}

wg.Add(1) // Add 1 to the wait group
logger.On("warning", []string{exporterShutdownMessage}).Once().Return(nil)
logger.On("info", []string{exporterStopMessage}).Once().Return(nil)
prometheusServer.On("Shutdown", mock.AnythingOfType("*context.timerCtx")).Once().Return(nil)
exporter.listenShutdownSignal()
cancel() // Simulate shutdown signal
wg.Wait() // Wait for goroutine to finish

assert.Nil(t, exporter.err)
})
}

func TestIsPortAvailable(t *testing.T) {
prometheusServer, port := new(serverPrometheusMock), ":8282"
t.Run("checks if port is available", func(t *testing.T) {
exporter := &exporter{
server: prometheusServer,
logger: new(loggerMock),
}
prometheusServer.On("Port").Once().Return(port)

assert.NoError(t, exporter.isPortAvailable())
})

t.Run("checks if port is unavailable", func(t *testing.T) {
exporter := &exporter{
server: prometheusServer,
logger: new(loggerMock),
}
prometheusServer.On("Port").Once().Return(port)
listener, _ := net.Listen("tcp", port)
defer listener.Close()

assert.Error(t, exporter.isPortAvailable(), exporterErrorMsg+port)
})
}
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ module heartbeat
go 1.23.0

require (
github.com/prometheus/client_golang v1.20.3
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.59.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.25.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
36 changes: 35 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 changes: 7 additions & 8 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import (
"os"
)

// TODO: uncomment when this logger will embedded as interface
// // Logger interface
// type logger interface {
// infoActivity(...string)
// info(...string)
// warning(...string)
// error(...string)
// }
// Logger interface
type logger interface {
infoActivity(...string)
info(...string)
warning(...string)
error(...string)
}

// Custom logger that supports 3 different log levels (info, warning, error)
type eventLogger struct {
Expand Down
4 changes: 3 additions & 1 deletion test_helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package heartbeat

import "regexp"
import (
"regexp"
)

// Regex builder
func newRegex(regexPattern string) (*regexp.Regexp, error) {
Expand Down
Loading

0 comments on commit 2160341

Please sign in to comment.