Skip to content

Commit

Permalink
Replace config object with option pattern (#23)
Browse files Browse the repository at this point in the history
* Replace config objects with option pattern
  • Loading branch information
instabledesign authored and rmasclef committed Oct 31, 2019
1 parent 471fcb1 commit 37ad04d
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 75 deletions.
30 changes: 28 additions & 2 deletions correlation_id/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,37 @@ type Config struct {
IdGenerator func(*http.Request) string
}

func NewConfig() *Config {
return &Config{
func (c *Config) apply(options ...Option) *Config {
for _, option := range options {
option(c)
}
return c
}

// GetConfig returns a new correlation configuration with all options applied
func GetConfig(options ...Option) *Config {
config := &Config{
HeaderName: HeaderName,
IdGenerator: func(_ *http.Request) string {
return DefaultIdGenerator.Generate(10)
},
}
return config.apply(options...)
}

// Option defines a correlationId middleware/tripperware configuration option
type Option func(*Config)

// WithHeaderName will configure HeaderName correlation option
func WithHeaderName(headerName string) Option {
return func(config *Config) {
config.HeaderName = headerName
}
}

// WithIdGenerator will configure IdGenerator correlation option
func WithIdGenerator(idGenerator func(*http.Request) string) Option {
return func(config *Config) {
config.IdGenerator = idGenerator
}
}
14 changes: 13 additions & 1 deletion correlation_id/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package correlation_id_test

import (
"math/rand"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -14,6 +15,17 @@ func TestNewConfig(t *testing.T) {
correlation_id.DefaultIdGenerator = correlation_id.NewRandomIdGenerator(defaultRand)

for _, expectedId := range []string{"p1LGIehp1s", "uqtCDMLxiD"} {
assert.Equal(t, expectedId, correlation_id.NewConfig().IdGenerator(nil))
assert.Equal(t, expectedId, correlation_id.GetConfig().IdGenerator(nil))
}
}

func TestConfig_Options(t *testing.T) {
config := correlation_id.GetConfig(
correlation_id.WithHeaderName("my-personal-header-name"),
correlation_id.WithIdGenerator(func(request *http.Request) string {
return "toto"
}),
)
assert.Equal(t, "my-personal-header-name", config.HeaderName)
assert.Equal(t, "toto", config.IdGenerator(nil))
}
52 changes: 46 additions & 6 deletions metrics/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ package metrics
import "net/http"

type Config struct {
Recorder Recorder
Recorder Recorder
// func that allows you to provide a strategy to identify/group metrics
// you can group metrics by request host/url/... or app name ...
// by default, we group metrics by request url
IdentifierProvider func(req *http.Request) string
IdentifierProvider func(req *http.Request) string
// if set to true, each response status will be represented by a metrics
// if set to false, response status codes will be grouped by first digit (204/201/200/... -> 2xx; 404/403/... -> 4xx)
SplitStatus bool
SplitStatus bool
// if set to true, recorder will add a responseSize metric
ObserveResponseSize bool
ObserveResponseSize bool
// if set to true, recorder will add a metric representing the number of inflight requests
MeasureInflightRequests bool
}

func NewConfig(recorder Recorder) *Config {
return &Config{
func (c *Config) apply(options ...Option) *Config {
for _, option := range options {
option(c)
}
return c
}

// NewConfig returns a new metrics configuration with all options applied
func NewConfig(recorder Recorder, options ...Option) *Config {
config := &Config{
Recorder: recorder,
SplitStatus: false,
ObserveResponseSize: true,
Expand All @@ -27,4 +35,36 @@ func NewConfig(recorder Recorder) *Config {
return req.URL.String()
},
}
return config.apply(options...)
}

// Option defines a metrics middleware/tripperware configuration option
type Option func(*Config)

// WithSplitStatus will configure SplitStatus metrics option
func WithSplitStatus(splitStatus bool) Option {
return func(config *Config) {
config.SplitStatus = splitStatus
}
}

// WithObserveResponseSize will configure ObserveResponseSize metrics option
func WithObserveResponseSize(observeResponseSize bool) Option {
return func(config *Config) {
config.ObserveResponseSize = observeResponseSize
}
}

// WithMeasureInflightRequests will configure MeasureInflightRequests metrics option
func WithMeasureInflightRequests(measureInflightRequests bool) Option {
return func(config *Config) {
config.MeasureInflightRequests = measureInflightRequests
}
}

// WithIdentifierProvider will configure IdentifierProvider metrics option
func WithIdentifierProvider(identifierProvider func(req *http.Request) string) Option {
return func(config *Config) {
config.IdentifierProvider = identifierProvider
}
}
27 changes: 27 additions & 0 deletions metrics/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package metrics_test

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gol4ng/httpware/metrics"
)

func TestConfig_Options(t *testing.T) {
var recorder metrics.Recorder
config := metrics.NewConfig(
recorder,
metrics.WithSplitStatus(true),
metrics.WithObserveResponseSize(false),
metrics.WithMeasureInflightRequests(false),
metrics.WithIdentifierProvider(func(req *http.Request) string {
return "my-personal-identifier"
}),
)
assert.Equal(t, true, config.SplitStatus)
assert.Equal(t, false, config.ObserveResponseSize)
assert.Equal(t, false, config.MeasureInflightRequests)
assert.Equal(t, "my-personal-identifier", config.IdentifierProvider(nil))
}
3 changes: 2 additions & 1 deletion middleware/correlation_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

// CorrelationId middleware get request id header if provided or generate a request id
// It will add the request ID to request context and add it to response header to
func CorrelationId(config *correlation_id.Config) httpware.Middleware {
func CorrelationId(options ...correlation_id.Option) httpware.Middleware {
config := correlation_id.GetConfig(options...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
id := req.Header.Get(config.HeaderName)
Expand Down
20 changes: 8 additions & 12 deletions middleware/correlation_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestCorrelationId(t *testing.T) {
handlerReq = r
})

middleware.CorrelationId(correlation_id.NewConfig())(handler).ServeHTTP(responseWriter, req)
middleware.CorrelationId()(handler).ServeHTTP(responseWriter, req)
respHeaderValue := responseWriter.Header().Get(correlation_id.HeaderName)
reqContextValue := handlerReq.Context().Value(correlation_id.HeaderName).(string)
assert.Equal(t, "p1LGIehp1s", req.Header.Get(correlation_id.HeaderName))
Expand All @@ -45,19 +45,15 @@ func TestCorrelationId(t *testing.T) {

func ExampleCorrelationId() {
port := ":5001"

config := correlation_id.NewConfig()
// you can override default header name
config.HeaderName = "my-personal-header-name"
// you can override default id generator
config.IdGenerator = func(request *http.Request) string {
return "my-fixed-request-id"
}

// we recommend to use MiddlewareStack to simplify managing all wanted middlewares
// caution middleware order matters
stack := httpware.MiddlewareStack(
middleware.CorrelationId(config),
middleware.CorrelationId(
correlation_id.WithHeaderName("my-personal-header-name"),
correlation_id.WithIdGenerator(func(request *http.Request) string {
return "my-fixed-request-id"
}),
),
)

srv := http.NewServeMux()
Expand All @@ -68,7 +64,7 @@ func ExampleCorrelationId() {
}()

resp, err := http.Get("http://localhost" + port)
fmt.Printf("%s: %v %v\n", config.HeaderName, resp.Header.Get(config.HeaderName), err)
fmt.Printf("%s: %v %v\n", "my-personal-header-name", resp.Header.Get("my-personal-header-name"), err)

//Output: my-personal-header-name: my-fixed-request-id <nil>
}
3 changes: 2 additions & 1 deletion middleware/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
"github.com/gol4ng/httpware/metrics"
)

func Metrics(config *metrics.Config) httpware.Middleware {
func Metrics(recorder metrics.Recorder, options ... metrics.Option) httpware.Middleware {
config := metrics.NewConfig(recorder, options...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
writerInterceptor := NewResponseWriterInterceptor(writer)
Expand Down
12 changes: 4 additions & 8 deletions middleware/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestMetrics(t *testing.T) {
}
// create metrics httpClient middleware
stack := httpware.MiddlewareStack(
middleware.Metrics(metrics.NewConfig(recorderMock)),
middleware.Metrics(recorderMock),
)

// assert recorder calls
Expand Down Expand Up @@ -65,16 +65,12 @@ func ExampleMetrics() {

recorder := prom.NewRecorder(prom.Config{}).RegisterOn(nil)

metricsConfig := metrics.NewConfig(recorder)
// you can override default identifier provider
metricsConfig.IdentifierProvider = func(req *http.Request) string {
return req.URL.Host + " -> " + req.URL.Path
}

// we recommend to use MiddlewareStack to simplify managing all wanted middleware
// caution middleware order matter
stack := httpware.MiddlewareStack(
middleware.Metrics(metricsConfig),
middleware.Metrics(recorder, metrics.WithIdentifierProvider(func(req *http.Request) string {
return req.URL.Host + " -> " + req.URL.Path
})),
)

srv := http.NewServeMux()
Expand Down
6 changes: 6 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package httpware_test

import (
"fmt"
"github.com/gol4ng/httpware/correlation_id"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -207,6 +208,11 @@ func ExampleMiddlewareStack() {
)
// create a server
srv := http.NewServeMux()
srv.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
request.Header.Get(correlation_id.HeaderName)
writer.WriteHeader(http.StatusOK)
})

// apply the middlewares on the server
// note: this part is normally done on `http.ListenAndServe(":<serverPort>", stack.DecorateHandler(srv))`
h := stack.DecorateHandler(srv)
Expand Down
3 changes: 2 additions & 1 deletion tripperware/correlation_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

// CorrelationId tripperware gets request id header if provided or generates a request id
// It will add the request ID to request context
func CorrelationId(config *correlation_id.Config) httpware.Tripperware {
func CorrelationId(options ...correlation_id.Option) httpware.Tripperware {
config := correlation_id.GetConfig(options...)
return func(next http.RoundTripper) http.RoundTripper {
return httpware.RoundTripFunc(func(req *http.Request) (resp *http.Response, err error) {
if v, ok := req.Context().Value(config.HeaderName).(string); ok {
Expand Down
Loading

0 comments on commit 37ad04d

Please sign in to comment.