diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 412ed8a..f144253 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,19 @@ name: CI -on: [push] +on: [push, pull_request] jobs: check: name: Check runs-on: ubuntu-latest - # Execute the checks inside the contianer instead the VM. + # Execute the checks inside the container instead the VM. container: golangci/golangci-lint:v1.27.0-alpine steps: - uses: actions/checkout@v1 - run: golangci-lint run -E goimports - test: - name: Test + unit-test: + name: Unit test runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -21,3 +21,14 @@ jobs: with: go-version: 1.14 - run: make test + + integration-test: + name: Integration test + runs-on: ubuntu-latest + needs: [check, unit-test] + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-go@v1 + with: + go-version: 1.14 + - run: make integration-test diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1e51c29 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,5 @@ +--- + +run: + build-tags: + - integration diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fee89..cfedd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,23 @@ ## [Unreleased] +Breaking change: The library has been refactored to be more flexible when adding new framework/libraries. + ### Added -- New middleware helper for the Echo framework +- New middleware helper for the Echo framework. + +### Changed + +- Refactored internally how the Middleware works and gets the data to make it easier to extend and more reliable. + - Added `Reporter` interface as the service responsible of getting the data to be measured. + - All different framwork helpers now implement with the new Reporter way. +- Fixed Gin returning duplicated data (#31). +- (Breaking) Standard handler now is on `middleware/std` instead of `middleware`. + +### Removed + +- Middleware interface in favor of a struct. ## [0.6.1] - 2020-02-07 diff --git a/Makefile b/Makefile index badbaf0..79a3ec0 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +1,46 @@ -UNIT_TEST_CMD := go test `go list ./... | grep -v vendor` -race -INTEGRATION_TEST_CMD := go test `go list ./... | grep -v vendor` -race -tags='integration' +UNIT_TEST_CMD := go test `go list ./... | grep -v test\/integration` -race +INTEGRATION_TEST_CMD := go test ./test/integration -race -tags='integration' BENCHMARK_CMD := go test `go list ./... | grep -v vendor` -benchmem -bench=. CHECK_CMD = golangci-lint run -E goimports DEPS_CMD := go mod tidy MOCKS_CMD := go generate ./internal/mocks +help: ## Show this help. + @echo "Help" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}' + + .PHONY: default default: test .PHONY: unit-test -unit-test: +unit-test: ## Execute unit tests. $(UNIT_TEST_CMD) .PHONY: integration-test -integration-test: +integration-test: ## Execute unit tests. $(INTEGRATION_TEST_CMD) -.PHONY: test -test: integration-test +.PHONY: test ## Alias for unit tests. +test: unit-test .PHONY: benchmark -benchmark: +benchmark: ## Execute benchmarks. $(BENCHMARK_CMD) .PHONY: check -check: +check: ## Execute check. $(CHECK_CMD) .PHONY: deps -deps: +deps: ## Tidy dependencies. $(DEPS_CMD) .PHONY: mocks -mocks: +mocks: ## Generates mocks. $(MOCKS_CMD) .PHONY: docs -docs: +docs: ## Runs docs example on :6060. godoc -http=":6060" diff --git a/Readme.md b/Readme.md index 1aa8ba2..5a2a414 100644 --- a/Readme.md +++ b/Readme.md @@ -1,8 +1,6 @@ # go-http-metrics [![Build Status][github-actions-image]][github-actions-url] [![Go Report Card][goreport-image]][goreport-url] [![GoDoc][godoc-image]][godoc-url] -go-http-metrics knows how to measure http metrics in different metric formats. The metrics measured are based on [RED] and/or [Four golden signals], follow standards and try to be measured in a efficient way. - -It measures based on a middleware that is compatible with Go core net/http handler, if you are using a framework that isn't directly compatible with go's `http.Handler` interface, do not worry, there are multiple helpers available to get middlewares for the most used http Go frameworks. If there isn't you can open an issue or a PR. +go-http-metrics knows how to measure http metrics in different metric formats a different Go HTTP framework/libs. The metrics measured are based on [RED] and/or [Four golden signals], follow standards and try to be measured in a efficient way. ## Table of contents @@ -58,6 +56,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" + middlewarestd "github.com/slok/go-http-metrics/middleware/std" ) func main() { @@ -67,11 +66,11 @@ func main() { }) // Our handler. - myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("hello world!")) }) - h := mdlw.Handler("", myHandler) + h = middlewarestd.Handler("", mdlw, h) // Serve metrics. log.Printf("serving metrics at: %s", ":9090") @@ -196,18 +195,6 @@ Same options as the Prometheus recorder. This Option is used to unregister the Recorder views before are being registered, this is option is mainly due to the nature of OpenCensus implementation and the huge usage fo global state making impossible to run multiple tests. On regular usage of the library this setting is very rare that needs to be used. -## Benchmarks - -```text -pkg: github.com/slok/go-http-metrics/middleware - -BenchmarkMiddlewareHandler/benchmark_with_default_settings.-4 1000000 1206 ns/op 256 B/op 6 allocs/op -BenchmarkMiddlewareHandler/benchmark_disabling_measuring_size.-4 1000000 1198 ns/op 256 B/op 6 allocs/op -BenchmarkMiddlewareHandler/benchmark_disabling_inflights.-4 1000000 1139 ns/op 256 B/op 6 allocs/op -BenchmarkMiddlewareHandler/benchmark_with_grouped_status_code.-4 1000000 1534 ns/op 256 B/op 7 allocs/op -BenchmarkMiddlewareHandler/benchmark_with_predefined_handler_ID-4 1000000 1258 ns/op 256 B/op 6 allocs/op -``` - [github-actions-image]: https://github.com/slok/go-http-metrics/workflows/CI/badge.svg [github-actions-url]: https://github.com/slok/go-http-metrics/actions [goreport-image]: https://goreportcard.com/badge/github.com/slok/go-http-metrics diff --git a/doc.go b/doc.go index ae6b668..a8f6d6b 100644 --- a/doc.go +++ b/doc.go @@ -11,6 +11,7 @@ the main Go net/http handler: "github.com/prometheus/client_golang/prometheus/promhttp" httpmetrics "github.com/slok/go-http-metrics/metrics/prometheus" httpmiddleware "github.com/slok/go-http-metrics/middleware" + httpstdmiddleware "github.com/slok/go-http-metrics/middleware/std" ) func main() { @@ -24,7 +25,7 @@ the main Go net/http handler: w.WriteHeader(http.StatusOK) w.Write([]byte("hello world!")) }) - h := mdlw.Handler("", myHandler) + h := httpstdmiddleware.Handler("", mdlw, myHandler) // Serve metrics. log.Printf("serving metrics at: %s", ":9090") diff --git a/examples/custom/main.go b/examples/custom/main.go index e3b6b63..4a8e0dd 100644 --- a/examples/custom/main.go +++ b/examples/custom/main.go @@ -11,6 +11,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" + "github.com/slok/go-http-metrics/middleware/std" ) const ( @@ -50,10 +51,10 @@ func main() { // Wrape our middleware on each of the different handlers with the ID of the handler // this way we reduce the cardinality, for example: `/test/2` and `/test/4` will // have the same `handler` label on the metric: `/test/:testID` - mux.Handle("/", mdlw.Handler("/", rooth)) - mux.Handle("/test/1", mdlw.Handler("/test/:testID", testh)) - mux.Handle("/test/2", mdlw.Handler("/test/:testID", testh2)) - mux.Handle("/other-test", mdlw.Handler("/other-test", othetesth)) + mux.Handle("/", std.Handler("/", mdlw, rooth)) + mux.Handle("/test/1", std.Handler("/test/:testID", mdlw, testh)) + mux.Handle("/test/2", std.Handler("/test/:testID", mdlw, testh2)) + mux.Handle("/other-test", std.Handler("/other-test", mdlw, othetesth)) // Serve our handler. go func() { diff --git a/examples/default/main.go b/examples/default/main.go index 6f0102e..b5bcc33 100644 --- a/examples/default/main.go +++ b/examples/default/main.go @@ -11,6 +11,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" + "github.com/slok/go-http-metrics/middleware/std" ) const ( @@ -45,7 +46,7 @@ func main() { // Wrap our main handler, we pass empty handler ID so the middleware inferes // the handler label from the URL. - h := mdlw.Handler("", mux) + h := std.Handler("", mdlw, mux) // Serve our handler. go func() { diff --git a/examples/echo/main.go b/examples/echo/main.go index 47763bd..0476027 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -12,7 +12,7 @@ import ( metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" - echoMiddleware "github.com/slok/go-http-metrics/middleware/echo" + echomiddleware "github.com/slok/go-http-metrics/middleware/echo" ) const ( @@ -28,12 +28,15 @@ func main() { // Create Echo instance and global middleware. e := echo.New() - e.Use(echoMiddleware.Handler("", mdlw)) + e.Use(echomiddleware.Handler("", mdlw)) // Add our handler. e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello world") }) + e.GET("/json", func(c echo.Context) error { + return c.JSON(http.StatusAccepted, map[string]string{"hello": "world"}) + }) e.GET("/wrong", func(c echo.Context) error { return c.String(http.StatusTooManyRequests, "oops") }) diff --git a/examples/gin/main.go b/examples/gin/main.go index 7f2581b..d01098b 100644 --- a/examples/gin/main.go +++ b/examples/gin/main.go @@ -31,7 +31,13 @@ func main() { // Add our handler. engine.GET("/", func(c *gin.Context) { - c.String(http.StatusOK, "Hello world") + c.String(http.StatusOK, "Hello %s", "world") + }) + engine.GET("/json", func(c *gin.Context) { + c.JSON(http.StatusAccepted, map[string]string{"hello": "world"}) + }) + engine.GET("/yaml", func(c *gin.Context) { + c.YAML(http.StatusCreated, map[string]string{"hello": "world"}) }) engine.GET("/wrong", func(c *gin.Context) { c.String(http.StatusTooManyRequests, "oops") diff --git a/examples/opencensus/main.go b/examples/opencensus/main.go index 1435459..be71bf7 100644 --- a/examples/opencensus/main.go +++ b/examples/opencensus/main.go @@ -10,8 +10,10 @@ import ( ocprometheus "contrib.go.opencensus.io/exporter/prometheus" ocmmetrics "github.com/slok/go-http-metrics/metrics/opencensus" - "github.com/slok/go-http-metrics/middleware" "go.opencensus.io/stats/view" + + "github.com/slok/go-http-metrics/middleware" + "github.com/slok/go-http-metrics/middleware/std" ) const ( @@ -52,7 +54,7 @@ func main() { // Wrap our main handler, we pass empty handler ID so the middleware inferes // the handler label from the URL. - h := mdlw.Handler("", mux) + h := std.Handler("", mdlw, mux) // Serve our handler. go func() { diff --git a/go.mod b/go.mod index a61a038..8121f1a 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,14 @@ module github.com/slok/go-http-metrics require ( contrib.go.opencensus.io/exporter/prometheus v0.1.0 - github.com/emicklei/go-restful v2.11.1+incompatible - github.com/gin-gonic/gin v1.5.0 + github.com/emicklei/go-restful v2.12.0+incompatible + github.com/gin-gonic/gin v1.6.3 github.com/julienschmidt/httprouter v1.3.0 github.com/labstack/echo/v4 v4.1.16 - github.com/prometheus/client_golang v1.2.1 - github.com/stretchr/testify v1.4.0 + github.com/prometheus/client_golang v1.6.0 + github.com/stretchr/testify v1.5.1 github.com/urfave/negroni v1.0.0 - go.opencensus.io v0.22.0 + go.opencensus.io v0.22.3 ) go 1.13 diff --git a/go.sum b/go.sum index 3ff4b68..ebdae8f 100644 --- a/go.sum +++ b/go.sum @@ -12,30 +12,36 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= -github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/emicklei/go-restful v2.11.1+incompatible h1:CjKsv3uWcCMvySPQYKxO8XX3f9zD4FeZRsW4G0B4ffE= -github.com/emicklei/go-restful v2.11.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.12.0+incompatible h1:SIvoTSbsMEwuM3dzFirLwKc4BH6VXP5CNf+G1FfJVr4= +github.com/emicklei/go-restful v2.12.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -43,31 +49,45 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -93,25 +113,25 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= -github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= +github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -123,6 +143,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -136,8 +158,8 @@ github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= @@ -154,8 +176,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -165,6 +187,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -177,11 +200,12 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd h1:r7DufRZuZbWB7j439YfAzP8RP golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= -golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -189,6 +213,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -196,15 +222,23 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/mocks/doc.go b/internal/mocks/doc.go index b86fdfc..74a6c32 100644 --- a/internal/mocks/doc.go +++ b/internal/mocks/doc.go @@ -4,3 +4,4 @@ Package mocks will have all the mocks of the library. package mocks // import "github.com/slok/go-http-metrics/internal/mocks" //go:generate mockery -output ./metrics -outpkg metrics -dir ../../metrics -name Recorder +//go:generate mockery -output ./middleware -outpkg middleware -dir ../../middleware -name Reporter diff --git a/internal/mocks/metrics/Recorder.go b/internal/mocks/metrics/Recorder.go index 7fd8d2e..2e04256 100644 --- a/internal/mocks/metrics/Recorder.go +++ b/internal/mocks/metrics/Recorder.go @@ -2,10 +2,14 @@ package metrics -import context "context" -import metrics "github.com/slok/go-http-metrics/metrics" -import mock "github.com/stretchr/testify/mock" -import time "time" +import ( + context "context" + + metrics "github.com/slok/go-http-metrics/metrics" + mock "github.com/stretchr/testify/mock" + + time "time" +) // Recorder is an autogenerated mock type for the Recorder type type Recorder struct { diff --git a/internal/mocks/middleware/Reporter.go b/internal/mocks/middleware/Reporter.go new file mode 100644 index 0000000..7ed1742 --- /dev/null +++ b/internal/mocks/middleware/Reporter.go @@ -0,0 +1,86 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package middleware + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// Reporter is an autogenerated mock type for the Reporter type +type Reporter struct { + mock.Mock +} + +// BytesWritten provides a mock function with given fields: +func (_m *Reporter) BytesWritten() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Context provides a mock function with given fields: +func (_m *Reporter) Context() context.Context { + ret := _m.Called() + + var r0 context.Context + if rf, ok := ret.Get(0).(func() context.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.Context) + } + } + + return r0 +} + +// Method provides a mock function with given fields: +func (_m *Reporter) Method() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// StatusCode provides a mock function with given fields: +func (_m *Reporter) StatusCode() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// URLPath provides a mock function with given fields: +func (_m *Reporter) URLPath() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} diff --git a/middleware/echo/echo.go b/middleware/echo/echo.go index 4b5ad93..8fcc1c3 100644 --- a/middleware/echo/echo.go +++ b/middleware/echo/echo.go @@ -3,17 +3,36 @@ package echo import ( - "net/http" + "context" "github.com/labstack/echo/v4" "github.com/slok/go-http-metrics/middleware" ) -// Handler returns a Echo compatible middleware from a Middleware factory instance. -// The first handlerID argument is the same argument passed on Middleware.Handler method. +// Handler returns a Echo measuring middleware. func Handler(handlerID string, m middleware.Middleware) echo.MiddlewareFunc { - // Wrap wrapping handler with echo's WrapMiddleware helper - return echo.WrapMiddleware(func(next http.Handler) http.Handler { - return m.Handler(handlerID, next) - }) + return func(h echo.HandlerFunc) echo.HandlerFunc { + return echo.HandlerFunc(func(c echo.Context) error { + r := &reporter{c: c} + var err error + m.Measure(handlerID, r, func() { + err = h(c) + }) + return err + }) + } } + +type reporter struct { + c echo.Context +} + +func (r *reporter) Method() string { return r.c.Request().Method } + +func (r *reporter) Context() context.Context { return r.c.Request().Context() } + +func (r *reporter) URLPath() string { return r.c.Request().URL.Path } + +func (r *reporter) StatusCode() int { return r.c.Response().Status } + +func (r *reporter) BytesWritten() int64 { return r.c.Response().Size } diff --git a/middleware/echo/echo_test.go b/middleware/echo/echo_test.go index 30e0d2f..cccc00e 100644 --- a/middleware/echo/echo_test.go +++ b/middleware/echo/echo_test.go @@ -21,9 +21,8 @@ func getTestHandler(statusCode int) echo.HandlerFunc { } } -func TestMiddlewareIntegration(t *testing.T) { - tests := []struct { - name string +func TestMiddleware(t *testing.T) { + tests := map[string]struct { handlerID string statusCode int req *http.Request @@ -33,8 +32,7 @@ func TestMiddlewareIntegration(t *testing.T) { expMethod string expStatusCode string }{ - { - name: "A default HTTP middleware should call the recorder to measure.", + "A default HTTP middleware should call the recorder to measure.": { statusCode: http.StatusAccepted, req: httptest.NewRequest(http.MethodPost, "/test", nil), expHandlerID: "/test", @@ -43,8 +41,8 @@ func TestMiddlewareIntegration(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { assert := assert.New(t) // Mocks. diff --git a/middleware/gin/gin.go b/middleware/gin/gin.go index e1a4f5d..863d6bd 100644 --- a/middleware/gin/gin.go +++ b/middleware/gin/gin.go @@ -3,53 +3,33 @@ package gin import ( - "net/http" + "context" "github.com/gin-gonic/gin" "github.com/slok/go-http-metrics/middleware" ) -// Handler returns a Gin compatible middleware from a Middleware factory instance. -// The first handlerID argument is the same argument passed on Middleware.Handler method. +// Handler returns a Gin measuring middleware. func Handler(handlerID string, m middleware.Middleware) gin.HandlerFunc { - // Create a dummy handler to wrap the middleware chain of Gin, this way Middleware - // interface can wrap the Gin chain. return func(c *gin.Context) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Writer = &ginResponseWriter{ - ResponseWriter: c.Writer, - middlewareRW: w, - } + r := &reporter{c: c} + m.Measure(handlerID, r, func() { c.Next() }) - m.Handler(handlerID, h).ServeHTTP(c.Writer, c.Request) } } -// ginResponseWriter is a helper type that intercepts the middleware ResponseWriter -// interceptor. -// This is required because gin's context Writer (c.Writer) is a gin.ResponseWriter -// interface and we can't access to the internal object http.ResponseWriter, so -// we already know that our middleware intercepts the regular http.ResponseWriter, -// and doesn't change anything, just intercepts to read information. So in order to -// get this information on our interceptor we create a gin.ResponseWriter implementation -// that will call the real gin.Context.Writer and our interceptor. This way Gin gets the -// information and our interceptor also. -type ginResponseWriter struct { - middlewareRW http.ResponseWriter - gin.ResponseWriter +type reporter struct { + c *gin.Context } -func (w *ginResponseWriter) WriteHeader(statusCode int) { - w.middlewareRW.WriteHeader(statusCode) - w.ResponseWriter.WriteHeader(statusCode) -} +func (r *reporter) Method() string { return r.c.Request.Method } -func (w *ginResponseWriter) Write(p []byte) (int, error) { - b, err := w.middlewareRW.Write(p) - if err != nil { - return b, err - } - return w.ResponseWriter.Write(p) -} +func (r *reporter) Context() context.Context { return r.c.Request.Context() } + +func (r *reporter) URLPath() string { return r.c.Request.URL.Path } + +func (r *reporter) StatusCode() int { return r.c.Writer.Status() } + +func (r *reporter) BytesWritten() int64 { return int64(r.c.Writer.Size()) } diff --git a/middleware/gin/gin_test.go b/middleware/gin/gin_test.go index f8302d4..835add3 100644 --- a/middleware/gin/gin_test.go +++ b/middleware/gin/gin_test.go @@ -21,9 +21,8 @@ func getTestHandler(statusCode int) gin.HandlerFunc { } } -func TestMiddlewareIntegration(t *testing.T) { - tests := []struct { - name string +func TestMiddleware(t *testing.T) { + tests := map[string]struct { handlerID string statusCode int req *http.Request @@ -33,8 +32,7 @@ func TestMiddlewareIntegration(t *testing.T) { expMethod string expStatusCode string }{ - { - name: "A default HTTP middleware should call the recorder to measure.", + "A default HTTP middleware should call the recorder to measure.": { statusCode: http.StatusAccepted, req: httptest.NewRequest(http.MethodPost, "/test", nil), expHandlerID: "/test", @@ -43,8 +41,8 @@ func TestMiddlewareIntegration(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { assert := assert.New(t) // Mocks. diff --git a/middleware/gorestful/gorestful.go b/middleware/gorestful/gorestful.go index 0e2ab41..fec6a93 100644 --- a/middleware/gorestful/gorestful.go +++ b/middleware/gorestful/gorestful.go @@ -3,25 +3,34 @@ package gorestful import ( - "net/http" + "context" gorestful "github.com/emicklei/go-restful" "github.com/slok/go-http-metrics/middleware" ) -// Handler returns a gorestful compatible middleware from a Middleware factory instance. -// The first handlerID argument is the same argument passed on Middleware.Handler method. +// Handler returns a gorestful measuring middleware. func Handler(handlerID string, m middleware.Middleware) gorestful.FilterFunction { - // Create a dummy handler to wrap the middleware chain of gorestful, this way Middleware - // interface can wrap the gorestful chain. return func(req *gorestful.Request, resp *gorestful.Response, chain *gorestful.FilterChain) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - req.Request = r - resp.ResponseWriter = w + r := &reporter{req: req, resp: resp} + m.Measure(handlerID, r, func() { chain.ProcessFilter(req, resp) }) - - m.Handler(handlerID, h).ServeHTTP(resp.ResponseWriter, req.Request) } } + +type reporter struct { + req *gorestful.Request + resp *gorestful.Response +} + +func (r *reporter) Method() string { return r.req.Request.Method } + +func (r *reporter) Context() context.Context { return r.req.Request.Context() } + +func (r *reporter) URLPath() string { return r.req.Request.URL.Path } + +func (r *reporter) StatusCode() int { return r.resp.StatusCode() } + +func (r *reporter) BytesWritten() int64 { return int64(r.resp.ContentLength()) } diff --git a/middleware/gorestful/gorestful_test.go b/middleware/gorestful/gorestful_test.go index 3db990d..fa7396c 100644 --- a/middleware/gorestful/gorestful_test.go +++ b/middleware/gorestful/gorestful_test.go @@ -21,9 +21,8 @@ func getTestHandler(statusCode int) gorestful.RouteFunction { }) } -func TestMiddlewareIntegration(t *testing.T) { - tests := []struct { - name string +func TestMiddleware(t *testing.T) { + tests := map[string]struct { handlerID string statusCode int req *http.Request @@ -33,8 +32,7 @@ func TestMiddlewareIntegration(t *testing.T) { expMethod string expStatusCode string }{ - { - name: "A default HTTP middleware should call the recorder to measure.", + "A default HTTP middleware should call the recorder to measure.": { statusCode: http.StatusAccepted, req: httptest.NewRequest(http.MethodPost, "/test", nil), expHandlerID: "/test", @@ -43,8 +41,8 @@ func TestMiddlewareIntegration(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { assert := assert.New(t) // Mocks. diff --git a/middleware/httprouter/example_test.go b/middleware/httprouter/example_test.go index cfbb242..9aa058a 100644 --- a/middleware/httprouter/example_test.go +++ b/middleware/httprouter/example_test.go @@ -10,6 +10,7 @@ import ( metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" httproutermiddleware "github.com/slok/go-http-metrics/middleware/httprouter" + stdmiddleware "github.com/slok/go-http-metrics/middleware/std" ) // HTTPRouterMiddlewareByHandler shows how you would create a default middleware factory @@ -75,7 +76,7 @@ func Example_httprouterMiddlewareOnRouter() { }() // Wrap the router with the middleware. - h := mdlw.Handler("", r) + h := stdmiddleware.Handler("", mdlw, r) // Serve our handler. log.Printf("listening at: %s", ":8080") diff --git a/middleware/httprouter/httprouter.go b/middleware/httprouter/httprouter.go index be937cf..edece1b 100644 --- a/middleware/httprouter/httprouter.go +++ b/middleware/httprouter/httprouter.go @@ -8,11 +8,10 @@ import ( "github.com/julienschmidt/httprouter" "github.com/slok/go-http-metrics/middleware" + "github.com/slok/go-http-metrics/middleware/std" ) -// Handler returns a httprouter.Handler compatible middleware from a Middleware factory instance. -// The first handlerID argument is the same argument passed on Middleware.Handler method. -// The second argument is the handler that wants to be wrapped. +// Handler returns a httprouter.Handler measuring middleware. func Handler(handlerID string, next httprouter.Handle, m middleware.Middleware) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { // Dummy handler to wrap httprouter Handle type @@ -20,6 +19,6 @@ func Handler(handlerID string, next httprouter.Handle, m middleware.Middleware) next(w, r, p) }) - m.Handler(handlerID, h).ServeHTTP(w, r) + std.Handler(handlerID, m, h).ServeHTTP(w, r) } } diff --git a/middleware/httprouter/httprouter_test.go b/middleware/httprouter/httprouter_test.go index 431a1f1..1e12df3 100644 --- a/middleware/httprouter/httprouter_test.go +++ b/middleware/httprouter/httprouter_test.go @@ -21,9 +21,8 @@ func getTestHandler(statusCode int) httprouter.Handle { }) } -func TestMiddlewareIntegration(t *testing.T) { - tests := []struct { - name string +func TestMiddleware(t *testing.T) { + tests := map[string]struct { handlerID string statusCode int req *http.Request @@ -33,8 +32,7 @@ func TestMiddlewareIntegration(t *testing.T) { expMethod string expStatusCode string }{ - { - name: "A default HTTP middleware should call the recorder to measure.", + "A default HTTP middleware should call the recorder to measure.": { statusCode: http.StatusAccepted, req: httptest.NewRequest(http.MethodPost, "/test", nil), expHandlerID: "/test", @@ -43,8 +41,8 @@ func TestMiddlewareIntegration(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { assert := assert.New(t) // Mocks. diff --git a/middleware/middleware.go b/middleware/middleware.go index 80f5318..079e77c 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -1,15 +1,12 @@ -// Package middleware will measure metrics of a Go net/http -// handler using a `metrics.Recorder`. -// The metrics measured are based on RED and/or Four golden signals and -// try to be measured in a efficient way. +// Package middleware will measure metrics of different http handler types +// using a `metrics.Recorder`. +// +// The metrics measured are based on RED and/or Four golden signals. package middleware import ( - "bufio" - "errors" + "context" "fmt" - "net" - "net/http" "strconv" "time" @@ -37,141 +34,95 @@ type Config struct { DisableMeasureInflight bool } -func (c *Config) validate() { +func (c *Config) defaults() { if c.Recorder == nil { c.Recorder = metrics.Dummy } } -// Middleware is a factory that creates middlewares or wrappers that -// measure requests to the wrapped handler using different metrics -// backends using a `metrics.Recorder` implementation. -type Middleware interface { - // Handler wraps the received handler with the Prometheus middleware. - // The first argument receives the handlerID, all the metrics will have - // that handler ID as the handler label on the metrics, if an empty - // string is passed then it will get the handlerID from the request - // path. - Handler(handlerID string, h http.Handler) http.Handler -} - -// middelware is the prometheus middleware instance. -type middleware struct { +// Middleware is a service that knows how to measure an HTTP handler by wrapping +// another handler. +// +// Depending on the framework/library we want to measure, this can change a lot, +// to abstract the way how we measure on the different libraries, Middleware will +// recieve a `Reporter` that knows how to get the data the Middleware service needs +// to measure. +type Middleware struct { cfg Config } -// New returns the a Middleware factory. +// New returns the a Middleware service. func New(cfg Config) Middleware { - // Validate the configuration. - cfg.validate() + cfg.defaults() - // Create our middleware with all the configuration options. - m := &middleware{ - cfg: cfg, - } + m := Middleware{cfg: cfg} return m } -// Handler satisfies Middleware interface. -func (m *middleware) Handler(handlerID string, h http.Handler) http.Handler { +// Measure abstracts the HTTP handler implementation by only requesting a reporter, this +// reporter will return the required data to be measured. +// it accepts a next function that will be called as the wrapped logic before and after +// measurement actions. +func (m Middleware) Measure(handlerID string, reporter Reporter, next func()) { + ctx := reporter.Context() + + // If there isn't predefined handler ID we + // set that ID as the URL path. + hid := handlerID + if handlerID == "" { + hid = reporter.URLPath() + } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Intercept the writer so we can retrieve data afterwards. - wi := &responseWriterInterceptor{ - statusCode: http.StatusOK, - ResponseWriter: w, + // Measure inflights if required. + if !m.cfg.DisableMeasureInflight { + props := metrics.HTTPProperties{ + Service: m.cfg.Service, + ID: hid, } + m.cfg.Recorder.AddInflightRequests(ctx, props, 1) + defer m.cfg.Recorder.AddInflightRequests(ctx, props, -1) + } - // If there isn't predefined handler ID we - // set that ID as the URL path. - hid := handlerID - if handlerID == "" { - hid = r.URL.Path + // Start the timer and when finishing measure the duration. + start := time.Now() + defer func() { + duration := time.Since(start) + + // If we need to group the status code, it uses the + // first number of the status code because is the least + // required identification way. + var code string + if m.cfg.GroupedStatus { + code = fmt.Sprintf("%dxx", reporter.StatusCode()/100) + } else { + code = strconv.Itoa(reporter.StatusCode()) } - // Measure inflights if required. - if !m.cfg.DisableMeasureInflight { - props := metrics.HTTPProperties{ - Service: m.cfg.Service, - ID: hid, - } - m.cfg.Recorder.AddInflightRequests(r.Context(), props, 1) - defer m.cfg.Recorder.AddInflightRequests(r.Context(), props, -1) + props := metrics.HTTPReqProperties{ + Service: m.cfg.Service, + ID: hid, + Method: reporter.Method(), + Code: code, } + m.cfg.Recorder.ObserveHTTPRequestDuration(ctx, props, duration) - // Start the timer and when finishing measure the duration. - start := time.Now() - defer func() { - duration := time.Since(start) - - // If we need to group the status code, it uses the - // first number of the status code because is the least - // required identification way. - var code string - if m.cfg.GroupedStatus { - code = fmt.Sprintf("%dxx", wi.statusCode/100) - } else { - code = strconv.Itoa(wi.statusCode) - } - - props := metrics.HTTPReqProperties{ - Service: m.cfg.Service, - ID: hid, - Method: r.Method, - Code: code, - } - m.cfg.Recorder.ObserveHTTPRequestDuration(r.Context(), props, duration) - - // Measure size of response if required. - if !m.cfg.DisableMeasureSize { - m.cfg.Recorder.ObserveHTTPResponseSize(r.Context(), props, int64(wi.bytesWritten)) - } - - }() - - h.ServeHTTP(wi, r) - }) -} - -// responseWriterInterceptor is a simple wrapper to intercept set data on a -// ResponseWriter. -type responseWriterInterceptor struct { - http.ResponseWriter - statusCode int - bytesWritten int -} - -func (w *responseWriterInterceptor) WriteHeader(statusCode int) { - w.statusCode = statusCode - w.ResponseWriter.WriteHeader(statusCode) -} + // Measure size of response if required. + if !m.cfg.DisableMeasureSize { + m.cfg.Recorder.ObserveHTTPResponseSize(ctx, props, reporter.BytesWritten()) + } + }() -func (w *responseWriterInterceptor) Write(p []byte) (int, error) { - w.bytesWritten += len(p) - return w.ResponseWriter.Write(p) + // Call the wrapped logic. + next() } -func (w *responseWriterInterceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) { - h, ok := w.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("type assertion failed http.ResponseWriter not a http.Hijacker") - } - return h.Hijack() +// Reporter knows how to report the data to the Middleware so it can measure the +// different framework/libraries. +type Reporter interface { + Method() string + Context() context.Context + URLPath() string + StatusCode() int + BytesWritten() int64 } - -func (w *responseWriterInterceptor) Flush() { - f, ok := w.ResponseWriter.(http.Flusher) - if !ok { - return - } - - f.Flush() -} - -// Check interface implementations. -var ( - _ http.ResponseWriter = &responseWriterInterceptor{} - _ http.Hijacker = &responseWriterInterceptor{} - _ http.Flusher = &responseWriterInterceptor{} -) diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go index 50c454d..0e70e68 100644 --- a/middleware/middleware_test.go +++ b/middleware/middleware_test.go @@ -1,164 +1,162 @@ package middleware_test import ( - "net/http" - "net/http/httptest" + "context" "testing" - mmetrics "github.com/slok/go-http-metrics/internal/mocks/metrics" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + mockmetrics "github.com/slok/go-http-metrics/internal/mocks/metrics" + mockmiddleware "github.com/slok/go-http-metrics/internal/mocks/middleware" "github.com/slok/go-http-metrics/metrics" "github.com/slok/go-http-metrics/middleware" - "github.com/stretchr/testify/mock" ) -func getFakeHandler(statusCode int, responseBody string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(statusCode) - _, _ = w.Write([]byte(responseBody)) - }) -} - -func TestMiddlewareHandler(t *testing.T) { +func TestMiddlewareMeasure(t *testing.T) { tests := map[string]struct { - handlerID string - body string - statusCode int - req *http.Request - config middleware.Config - expHandlerID string - expService string - expMethod string - expSize int64 - expStatusCode string - }{ - "A default HTTP middleware should call the recorder to measure.": { - statusCode: http.StatusAccepted, - body: "Я бэтмен", - req: httptest.NewRequest(http.MethodGet, "/test", nil), - expHandlerID: "/test", - expService: "", - expSize: 15, - expMethod: http.MethodGet, - expStatusCode: "202", - }, - - "Using custom ID in the middleware should call the recorder to measure with that ID.": { - handlerID: "customID", - body: "I'm Batman", - statusCode: http.StatusTeapot, - req: httptest.NewRequest(http.MethodPost, "/test", nil), - expHandlerID: "customID", - expService: "", - expSize: 10, - expMethod: http.MethodPost, - expStatusCode: "418", - }, - - "Using grouped status code should group the status code.": { - config: middleware.Config{GroupedStatus: true}, - statusCode: http.StatusGatewayTimeout, - req: httptest.NewRequest(http.MethodPatch, "/test", nil), - expHandlerID: "/test", - expService: "", - expMethod: http.MethodPatch, - expStatusCode: "5xx", - }, - - "Using the service middleware option should set the service on the metrics.": { - config: middleware.Config{Service: "Yoda"}, - statusCode: http.StatusContinue, - body: "May the force be with you", - req: httptest.NewRequest(http.MethodGet, "/test", nil), - expHandlerID: "/test", - expService: "Yoda", - expSize: 25, - expMethod: http.MethodGet, - expStatusCode: "100", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - // Mocks. - mr := &mmetrics.Recorder{} - expHTTPReqProps := metrics.HTTPReqProperties{ - ID: test.expHandlerID, - Service: test.expService, - Method: test.expMethod, - Code: test.expStatusCode, - } - expHTTPProps := metrics.HTTPProperties{ - ID: test.expHandlerID, - Service: test.expService, - } - mr.On("ObserveHTTPRequestDuration", mock.Anything, expHTTPReqProps, mock.Anything).Once() - mr.On("ObserveHTTPResponseSize", mock.Anything, expHTTPReqProps, test.expSize).Once() - mr.On("AddInflightRequests", mock.Anything, expHTTPProps, 1).Once() - mr.On("AddInflightRequests", mock.Anything, expHTTPProps, -1).Once() - - // Make the request. - test.config.Recorder = mr - m := middleware.New(test.config) - h := m.Handler(test.handlerID, getFakeHandler(test.statusCode, test.body)) - h.ServeHTTP(httptest.NewRecorder(), test.req) - - mr.AssertExpectations(t) - }) - } -} - -func BenchmarkMiddlewareHandler(b *testing.B) { - b.StopTimer() - - benchs := map[string]struct { handlerID string - cfg middleware.Config + config func() middleware.Config + mock func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) }{ - "benchmark with default settings.": { - handlerID: "", - cfg: middleware.Config{}, + "Having default config with service, it should measure the metrics.": { + handlerID: "test01", + config: func() middleware.Config { + return middleware.Config{ + Service: "svc1", + } + }, + mock: func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) { + // Reporter mocks. + mrep.On("Context").Once().Return(context.TODO()) + mrep.On("StatusCode").Once().Return(418) + mrep.On("Method").Once().Return("PATCH") + mrep.On("BytesWritten").Once().Return(int64(42)) + + // Recorder mocks. + expProps := metrics.HTTPProperties{Service: "svc1", ID: "test01"} + expRepProps := metrics.HTTPReqProperties{Service: "svc1", ID: "test01", Method: "PATCH", Code: "418"} + + mrec.On("AddInflightRequests", mock.Anything, expProps, 1).Once() + mrec.On("AddInflightRequests", mock.Anything, expProps, -1).Once() + mrec.On("ObserveHTTPRequestDuration", mock.Anything, expRepProps, mock.Anything).Once() + mrec.On("ObserveHTTPResponseSize", mock.Anything, expRepProps, int64(42)).Once() + }, }, - "benchmark disabling measuring size.": { + "Without having handler ID, it should measure the metrics using the request path.": { handlerID: "", - cfg: middleware.Config{ - DisableMeasureSize: true, + config: func() middleware.Config { + return middleware.Config{} + }, + mock: func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) { + // Reporter mocks. + mrep.On("URLPath").Once().Return("/test/01") + mrep.On("Context").Once().Return(context.TODO()) + mrep.On("StatusCode").Once().Return(418) + mrep.On("Method").Once().Return("PATCH") + mrep.On("BytesWritten").Once().Return(int64(42)) + + // Recorder mocks. + expRepProps := metrics.HTTPReqProperties{ID: "/test/01", Method: "PATCH", Code: "418"} + + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("ObserveHTTPRequestDuration", mock.Anything, expRepProps, mock.Anything).Once() + mrec.On("ObserveHTTPResponseSize", mock.Anything, expRepProps, mock.Anything).Once() }, }, - "benchmark disabling inflights.": { - handlerID: "", - cfg: middleware.Config{ - DisableMeasureInflight: true, + "Having grouped status code, it should measure the metrics using grouped status codes.": { + handlerID: "test01", + config: func() middleware.Config { + return middleware.Config{ + GroupedStatus: true, + } + }, + mock: func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) { + // Reporter mocks. + mrep.On("Context").Once().Return(context.TODO()) + mrep.On("StatusCode").Once().Return(418) + mrep.On("Method").Once().Return("PATCH") + mrep.On("BytesWritten").Once().Return(int64(42)) + + // Recorder mocks. + expRepProps := metrics.HTTPReqProperties{ID: "test01", Method: "PATCH", Code: "4xx"} + + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("ObserveHTTPRequestDuration", mock.Anything, expRepProps, mock.Anything).Once() + mrec.On("ObserveHTTPResponseSize", mock.Anything, expRepProps, mock.Anything).Once() }, }, - "benchmark with grouped status code.": { - cfg: middleware.Config{ - GroupedStatus: true, + "Disabling inflight requests measuring, it shouldn't measure inflight metrics.": { + handlerID: "test01", + config: func() middleware.Config { + return middleware.Config{ + DisableMeasureInflight: true, + } + }, + mock: func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) { + // Reporter mocks. + mrep.On("Context").Once().Return(context.TODO()) + mrep.On("StatusCode").Once().Return(418) + mrep.On("Method").Once().Return("PATCH") + mrep.On("BytesWritten").Once().Return(int64(42)) + + // Recorder mocks. + expRepProps := metrics.HTTPReqProperties{ID: "test01", Method: "PATCH", Code: "418"} + + mrec.On("ObserveHTTPRequestDuration", mock.Anything, expRepProps, mock.Anything).Once() + mrec.On("ObserveHTTPResponseSize", mock.Anything, expRepProps, mock.Anything).Once() }, }, - "benchmark with predefined handler ID": { - handlerID: "benchmark1", - cfg: middleware.Config{}, + "Disabling size measuring, it shouldn't measure size metrics.": { + handlerID: "test01", + config: func() middleware.Config { + return middleware.Config{ + DisableMeasureSize: true, + } + }, + mock: func(mrec *mockmetrics.Recorder, mrep *mockmiddleware.Reporter) { + // Reporter mocks. + mrep.On("Context").Once().Return(context.TODO()) + mrep.On("StatusCode").Once().Return(418) + mrep.On("Method").Once().Return("PATCH") + + // Recorder mocks. + expRepProps := metrics.HTTPReqProperties{ID: "test01", Method: "PATCH", Code: "418"} + + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("AddInflightRequests", mock.Anything, mock.Anything, mock.Anything).Once() + mrec.On("ObserveHTTPRequestDuration", mock.Anything, expRepProps, mock.Anything).Once() + }, }, } - for name, bench := range benchs { - b.Run(name, func(b *testing.B) { - // Prepare. - bench.cfg.Recorder = metrics.Dummy - m := middleware.New(bench.cfg) - h := m.Handler(bench.handlerID, getFakeHandler(200, "")) - r := httptest.NewRequest("GET", "/test", nil) - - // Make the requests. - for n := 0; n < b.N; n++ { - b.StartTimer() - h.ServeHTTP(httptest.NewRecorder(), r) - b.StopTimer() - } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + // Mocks. + mrec := &mockmetrics.Recorder{} + mrep := &mockmiddleware.Reporter{} + test.mock(mrec, mrep) + + // Execute. + config := test.config() + config.Recorder = mrec // Set mocked recorder. + mdlw := middleware.New(config) + + calledNext := false + mdlw.Measure(test.handlerID, mrep, func() { calledNext = true }) + + // Check. + mrec.AssertExpectations(t) + mrep.AssertExpectations(t) + assert.True(calledNext) }) } } diff --git a/middleware/negroni/negroni.go b/middleware/negroni/negroni.go index 6d5761c..b374ea5 100644 --- a/middleware/negroni/negroni.go +++ b/middleware/negroni/negroni.go @@ -8,12 +8,12 @@ import ( "github.com/urfave/negroni" "github.com/slok/go-http-metrics/middleware" + "github.com/slok/go-http-metrics/middleware/std" ) -// Handler returns a Negroni compatible middleware from a Middleware factory instance. -// The first handlerID argument is the same argument passed on Middleware.Handler method. +// Handler returns a Negroni measuring middleware. func Handler(handlerID string, m middleware.Middleware) negroni.Handler { return negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - m.Handler(handlerID, next).ServeHTTP(rw, r) + std.Handler(handlerID, m, next).ServeHTTP(rw, r) }) } diff --git a/middleware/negroni/negroni_test.go b/middleware/negroni/negroni_test.go index dba524a..e9666d8 100644 --- a/middleware/negroni/negroni_test.go +++ b/middleware/negroni/negroni_test.go @@ -21,9 +21,8 @@ func getTestHandler(statusCode int) http.Handler { }) } -func TestMiddlewareIntegration(t *testing.T) { - tests := []struct { - name string +func TestMiddleware(t *testing.T) { + tests := map[string]struct { handlerID string statusCode int req *http.Request @@ -33,8 +32,7 @@ func TestMiddlewareIntegration(t *testing.T) { expMethod string expStatusCode string }{ - { - name: "A default HTTP middleware should call the recorder to measure.", + "A default HTTP middleware should call the recorder to measure.": { statusCode: http.StatusAccepted, req: httptest.NewRequest(http.MethodPost, "/test", nil), expHandlerID: "/test", @@ -43,8 +41,8 @@ func TestMiddlewareIntegration(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { assert := assert.New(t) // Mocks. diff --git a/middleware/example_test.go b/middleware/std/example_test.go similarity index 67% rename from middleware/example_test.go rename to middleware/std/example_test.go index 2c7e7b6..f92156b 100644 --- a/middleware/example_test.go +++ b/middleware/std/example_test.go @@ -1,19 +1,19 @@ -package middleware_test +package std_test import ( "log" "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" + metrics "github.com/slok/go-http-metrics/metrics/prometheus" "github.com/slok/go-http-metrics/middleware" + stdmiddleware "github.com/slok/go-http-metrics/middleware/std" ) -// PrometheusBackendMiddleware shows how you would create a middleware factory for standard -// go library `http.Handler` and wrap a handler to measure with the default settings using -// Prometheus as the metrics recorder backend, the Prometheus will use the default settings -// so it will measure using the default Prometheus registry. -func ExampleMiddleware_prometheusBackendMiddleware() { +// NegroniMiddleware shows how you would create a default middleware factory and use it +// to create a standdard `http.Handler` compatible middleware. +func Example_stdMiddleware() { // Create our middleware factory with the default settings. mdlw := middleware.New(middleware.Config{ Recorder: metrics.NewRecorder(metrics.Config{}), @@ -26,7 +26,7 @@ func ExampleMiddleware_prometheusBackendMiddleware() { }) // Wrap our handler with the middleware. - h := mdlw.Handler("", myHandler) + h := stdmiddleware.Handler("", mdlw, myHandler) // Serve metrics from the default prometheus registry. log.Printf("serving metrics at: %s", ":8081") diff --git a/middleware/std/std.go b/middleware/std/std.go new file mode 100644 index 0000000..f91b24c --- /dev/null +++ b/middleware/std/std.go @@ -0,0 +1,86 @@ +package std + +import ( + "bufio" + "context" + "errors" + "net" + "net/http" + + "github.com/slok/go-http-metrics/middleware" +) + +// Handler returns an measuring standard http.Handler. +func Handler(handlerID string, m middleware.Middleware, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wi := &responseWriterInterceptor{ + statusCode: http.StatusOK, + ResponseWriter: w, + } + reporter := &stdReporter{ + w: wi, + r: r, + } + + m.Measure(handlerID, reporter, func() { + h.ServeHTTP(wi, r) + }) + }) +} + +type stdReporter struct { + w *responseWriterInterceptor + r *http.Request +} + +func (s *stdReporter) Method() string { return s.r.Method } + +func (s *stdReporter) Context() context.Context { return s.r.Context() } + +func (s *stdReporter) URLPath() string { return s.r.URL.Path } + +func (s *stdReporter) StatusCode() int { return s.w.statusCode } + +func (s *stdReporter) BytesWritten() int64 { return int64(s.w.bytesWritten) } + +// responseWriterInterceptor is a simple wrapper to intercept set data on a +// ResponseWriter. +type responseWriterInterceptor struct { + http.ResponseWriter + statusCode int + bytesWritten int +} + +func (w *responseWriterInterceptor) WriteHeader(statusCode int) { + w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +func (w *responseWriterInterceptor) Write(p []byte) (int, error) { + w.bytesWritten += len(p) + return w.ResponseWriter.Write(p) +} + +func (w *responseWriterInterceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) { + h, ok := w.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, errors.New("type assertion failed http.ResponseWriter not a http.Hijacker") + } + return h.Hijack() +} + +func (w *responseWriterInterceptor) Flush() { + f, ok := w.ResponseWriter.(http.Flusher) + if !ok { + return + } + + f.Flush() +} + +// Check interface implementations. +var ( + _ http.ResponseWriter = &responseWriterInterceptor{} + _ http.Hijacker = &responseWriterInterceptor{} + _ http.Flusher = &responseWriterInterceptor{} +) diff --git a/middleware/std/std_test.go b/middleware/std/std_test.go new file mode 100644 index 0000000..f927bfc --- /dev/null +++ b/middleware/std/std_test.go @@ -0,0 +1,110 @@ +package std_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/mock" + + mmetrics "github.com/slok/go-http-metrics/internal/mocks/metrics" + "github.com/slok/go-http-metrics/metrics" + "github.com/slok/go-http-metrics/middleware" + stdmiddleware "github.com/slok/go-http-metrics/middleware/std" +) + +func getFakeHandler(statusCode int, responseBody string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + _, _ = w.Write([]byte(responseBody)) + }) +} + +func TestMiddleware(t *testing.T) { + tests := map[string]struct { + handlerID string + body string + statusCode int + req *http.Request + config middleware.Config + expHandlerID string + expService string + expMethod string + expSize int64 + expStatusCode string + }{ + "A default HTTP middleware should call the recorder to measure.": { + statusCode: http.StatusAccepted, + body: "Я бэтмен", + req: httptest.NewRequest(http.MethodGet, "/test", nil), + expHandlerID: "/test", + expService: "", + expSize: 15, + expMethod: http.MethodGet, + expStatusCode: "202", + }, + + "Using custom ID in the middleware should call the recorder to measure with that ID.": { + handlerID: "customID", + body: "I'm Batman", + statusCode: http.StatusTeapot, + req: httptest.NewRequest(http.MethodPost, "/test", nil), + expHandlerID: "customID", + expService: "", + expSize: 10, + expMethod: http.MethodPost, + expStatusCode: "418", + }, + + "Using grouped status code should group the status code.": { + config: middleware.Config{GroupedStatus: true}, + statusCode: http.StatusGatewayTimeout, + req: httptest.NewRequest(http.MethodPatch, "/test", nil), + expHandlerID: "/test", + expService: "", + expMethod: http.MethodPatch, + expStatusCode: "5xx", + }, + + "Using the service middleware option should set the service on the metrics.": { + config: middleware.Config{Service: "Yoda"}, + statusCode: http.StatusContinue, + body: "May the force be with you", + req: httptest.NewRequest(http.MethodGet, "/test", nil), + expHandlerID: "/test", + expService: "Yoda", + expSize: 25, + expMethod: http.MethodGet, + expStatusCode: "100", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Mocks. + mr := &mmetrics.Recorder{} + expHTTPReqProps := metrics.HTTPReqProperties{ + ID: test.expHandlerID, + Service: test.expService, + Method: test.expMethod, + Code: test.expStatusCode, + } + expHTTPProps := metrics.HTTPProperties{ + ID: test.expHandlerID, + Service: test.expService, + } + mr.On("ObserveHTTPRequestDuration", mock.Anything, expHTTPReqProps, mock.Anything).Once() + mr.On("ObserveHTTPResponseSize", mock.Anything, expHTTPReqProps, test.expSize).Once() + mr.On("AddInflightRequests", mock.Anything, expHTTPProps, 1).Once() + mr.On("AddInflightRequests", mock.Anything, expHTTPProps, -1).Once() + + // Make the request. + test.config.Recorder = mr + m := middleware.New(test.config) + h := stdmiddleware.Handler(test.handlerID, m, getFakeHandler(test.statusCode, test.body)) + h.ServeHTTP(httptest.NewRecorder(), test.req) + + mr.AssertExpectations(t) + }) + } +} diff --git a/test/integration/data_test.go b/test/integration/data_test.go new file mode 100644 index 0000000..e2bb5f3 --- /dev/null +++ b/test/integration/data_test.go @@ -0,0 +1,91 @@ +// +build integration + +package integration_test + +import "time" + +var ( + expReqs = []handlerConfig{ + {Path: "/test/1", Method: "GET", Code: 201, ReturnData: "1", SleepDuration: 45 * time.Millisecond, NumberRequests: 10}, + {Path: "/test/2", Method: "POST", Code: 202, ReturnData: "22", SleepDuration: 95 * time.Millisecond, NumberRequests: 9}, + {Path: "/test/3", Method: "PATCH", Code: 203, ReturnData: "333", SleepDuration: 145 * time.Millisecond, NumberRequests: 8}, + {Path: "/test/4", Method: "DELETE", Code: 205, ReturnData: "4444", SleepDuration: 195 * time.Millisecond, NumberRequests: 7}, + } + + expMetrics = []string{ + `# HELP http_request_duration_seconds The latency of the HTTP requests.`, + `# TYPE http_request_duration_seconds histogram`, + `http_request_duration_seconds_bucket{code="201",handler="/test/1",method="GET",service="integration",le="0.05"} 10`, + `http_request_duration_seconds_bucket{code="201",handler="/test/1",method="GET",service="integration",le="0.1"} 10`, + `http_request_duration_seconds_bucket{code="201",handler="/test/1",method="GET",service="integration",le="0.15"} 10`, + `http_request_duration_seconds_bucket{code="201",handler="/test/1",method="GET",service="integration",le="0.2"} 10`, + `http_request_duration_seconds_bucket{code="201",handler="/test/1",method="GET",service="integration",le="+Inf"} 10`, + `http_request_duration_seconds_count{code="201",handler="/test/1",method="GET",service="integration"} 10`, + + `http_request_duration_seconds_bucket{code="202",handler="/test/2",method="POST",service="integration",le="0.05"} 0`, + `http_request_duration_seconds_bucket{code="202",handler="/test/2",method="POST",service="integration",le="0.1"} 9`, + `http_request_duration_seconds_bucket{code="202",handler="/test/2",method="POST",service="integration",le="0.15"} 9`, + `http_request_duration_seconds_bucket{code="202",handler="/test/2",method="POST",service="integration",le="0.2"} 9`, + `http_request_duration_seconds_bucket{code="202",handler="/test/2",method="POST",service="integration",le="+Inf"} 9`, + `http_request_duration_seconds_count{code="202",handler="/test/2",method="POST",service="integration"} 9`, + + `http_request_duration_seconds_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="0.05"} 0`, + `http_request_duration_seconds_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="0.1"} 0`, + `http_request_duration_seconds_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="0.15"} 8`, + `http_request_duration_seconds_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="0.2"} 8`, + `http_request_duration_seconds_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="+Inf"} 8`, + `http_request_duration_seconds_count{code="203",handler="/test/3",method="PATCH",service="integration"} 8`, + + `http_request_duration_seconds_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="0.05"} 0`, + `http_request_duration_seconds_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="0.1"} 0`, + `http_request_duration_seconds_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="0.15"} 0`, + `http_request_duration_seconds_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="0.2"} 7`, + `http_request_duration_seconds_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="+Inf"} 7`, + `http_request_duration_seconds_count{code="205",handler="/test/4",method="DELETE",service="integration"} 7`, + + `# HELP http_requests_inflight The number of inflight requests being handled at the same time.`, + `# TYPE http_requests_inflight gauge`, + `http_requests_inflight{handler="/test/1",service="integration"} 0`, + `http_requests_inflight{handler="/test/2",service="integration"} 0`, + `http_requests_inflight{handler="/test/3",service="integration"} 0`, + `http_requests_inflight{handler="/test/4",service="integration"} 0`, + + `# HELP http_response_size_bytes The size of the HTTP responses.`, + `# TYPE http_response_size_bytes histogram`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="1"} 10`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="2"} 10`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="3"} 10`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="4"} 10`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="5"} 10`, + `http_response_size_bytes_bucket{code="201",handler="/test/1",method="GET",service="integration",le="+Inf"} 10`, + `http_response_size_bytes_sum{code="201",handler="/test/1",method="GET",service="integration"} 10`, + `http_response_size_bytes_count{code="201",handler="/test/1",method="GET",service="integration"} 10`, + + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="1"} 0`, + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="2"} 9`, + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="3"} 9`, + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="4"} 9`, + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="5"} 9`, + `http_response_size_bytes_bucket{code="202",handler="/test/2",method="POST",service="integration",le="+Inf"} 9`, + `http_response_size_bytes_sum{code="202",handler="/test/2",method="POST",service="integration"} 18`, + `http_response_size_bytes_count{code="202",handler="/test/2",method="POST",service="integration"} 9`, + + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="1"} 0`, + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="2"} 0`, + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="3"} 8`, + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="4"} 8`, + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="5"} 8`, + `http_response_size_bytes_bucket{code="203",handler="/test/3",method="PATCH",service="integration",le="+Inf"} 8`, + `http_response_size_bytes_sum{code="203",handler="/test/3",method="PATCH",service="integration"} 24`, + `http_response_size_bytes_count{code="203",handler="/test/3",method="PATCH",service="integration"} 8`, + + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="1"} 0`, + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="2"} 0`, + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="3"} 0`, + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="4"} 7`, + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="5"} 7`, + `http_response_size_bytes_bucket{code="205",handler="/test/4",method="DELETE",service="integration",le="+Inf"} 7`, + `http_response_size_bytes_sum{code="205",handler="/test/4",method="DELETE",service="integration"} 28`, + `http_response_size_bytes_count{code="205",handler="/test/4",method="DELETE",service="integration"} 7`, + } +) diff --git a/test/integration/doc.go b/test/integration/doc.go new file mode 100644 index 0000000..76ab1b7 --- /dev/null +++ b/test/integration/doc.go @@ -0,0 +1 @@ +package integration diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go new file mode 100644 index 0000000..0bcd144 --- /dev/null +++ b/test/integration/integration_test.go @@ -0,0 +1,252 @@ +// +build integration + +package integration_test + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + gorestful "github.com/emicklei/go-restful" + "github.com/gin-gonic/gin" + "github.com/julienschmidt/httprouter" + "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/negroni" + + metricsprometheus "github.com/slok/go-http-metrics/metrics/prometheus" + "github.com/slok/go-http-metrics/middleware" + echomiddleware "github.com/slok/go-http-metrics/middleware/echo" + ginmiddleware "github.com/slok/go-http-metrics/middleware/gin" + gorestfulmiddleware "github.com/slok/go-http-metrics/middleware/gorestful" + httproutermiddleware "github.com/slok/go-http-metrics/middleware/httprouter" + negronimiddleware "github.com/slok/go-http-metrics/middleware/negroni" + stdmiddleware "github.com/slok/go-http-metrics/middleware/std" +) + +// handlerConfig is the configuration the servers will need to set up to properly +// execute the tests. +type handlerConfig struct { + Path string + Code int + Method string + ReturnData string + SleepDuration time.Duration + NumberRequests int +} + +func TestMiddlewarePrometheus(t *testing.T) { + tests := map[string]struct { + handler func(m middleware.Middleware, hc []handlerConfig) http.Handler + }{ + "STD http.Handler": {handler: prepareHandlerSTD}, + "Negroni": {handler: prepareHandlerNegroni}, + "HTTPRouter": {handler: prepareHandlerHTTPRouter}, + "Gorestful": {handler: prepareHandlerGorestful}, + "Gin": {handler: prepareHandlerGin}, + "Echo": {handler: prepareHandlerEcho}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Setup. + reg := prometheus.NewRegistry() + rec := metricsprometheus.NewRecorder(metricsprometheus.Config{ + Registry: reg, + DurationBuckets: []float64{0.05, 0.1, 0.15, 0.2}, + SizeBuckets: []float64{1, 2, 3, 4, 5}, + }) + mdlw := middleware.New(middleware.Config{ + Service: "integration", + Recorder: rec, + }) + + serverHandler := test.handler(mdlw, expReqs) + metricsHandler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{}) + + // Test. + testMiddlewareRequests(t, serverHandler, expReqs) + testMiddlewarePrometheusMetrics(t, metricsHandler, expMetrics) + }) + } +} + +func testMiddlewareRequests(t *testing.T, h http.Handler, expReqs []handlerConfig) { + require := require.New(t) + assert := assert.New(t) + + // Setup server. + server := httptest.NewServer(h) + t.Cleanup(func() { server.Close() }) + + // Make all the requests. + for _, config := range expReqs { + for i := 0; i < config.NumberRequests; i++ { + r, err := http.NewRequest(config.Method, server.URL+config.Path, nil) + require.NoError(err) + resp, err := http.DefaultClient.Do(r) + require.NoError(err) + + // Check. + assert.Equal(config.Code, resp.StatusCode) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(err) + assert.Equal(config.ReturnData, string(b)) + } + } +} + +func testMiddlewarePrometheusMetrics(t *testing.T, h http.Handler, expMetrics []string) { + require := require.New(t) + assert := assert.New(t) + + // Setup server. + server := httptest.NewServer(h) + t.Cleanup(func() { server.Close() }) + + // Get metrics. + r, err := http.NewRequest(http.MethodGet, server.URL+"/metrics", nil) + require.NoError(err) + resp, err := http.DefaultClient.Do(r) + require.NoError(err) + + // Check. + b, err := ioutil.ReadAll(resp.Body) + require.NoError(err) + metrics := string(b) + + // Make all the requests. + for _, expMetric := range expMetrics { + assert.Contains(metrics, expMetric) + } +} + +func prepareHandlerSTD(m middleware.Middleware, hc []handlerConfig) http.Handler { + // Setup handlers. + mux := http.NewServeMux() + for _, h := range hc { + h := h + mux.Handle(h.Path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != h.Method { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + time.Sleep(h.SleepDuration) + w.WriteHeader(h.Code) + // nolint: errcheck + w.Write([]byte(h.ReturnData)) + })) + } + + // Setup server and middleware. + h := stdmiddleware.Handler("", m, mux) + + return h +} + +func prepareHandlerNegroni(m middleware.Middleware, hc []handlerConfig) http.Handler { + // Setup handlers. + mux := http.NewServeMux() + for _, h := range hc { + h := h + mux.Handle(h.Path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != h.Method { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + time.Sleep(h.SleepDuration) + w.WriteHeader(h.Code) + // nolint: errcheck + w.Write([]byte(h.ReturnData)) + })) + } + + // Setup server and middleware. + n := negroni.Classic() + n.Use(negronimiddleware.Handler("", m)) + n.UseHandler(mux) + + return n +} + +func prepareHandlerHTTPRouter(m middleware.Middleware, hc []handlerConfig) http.Handler { + r := httprouter.New() + + // Setup handlers. + for _, h := range hc { + h := h + hr := func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + time.Sleep(h.SleepDuration) + w.WriteHeader(h.Code) + // nolint: errcheck + w.Write([]byte(h.ReturnData)) + } + + // Setup middleware on each of the routes. + r.Handle(h.Method, h.Path, httproutermiddleware.Handler("", hr, m)) + } + + return r +} + +func prepareHandlerGorestful(m middleware.Middleware, hc []handlerConfig) http.Handler { + // Setup server and middleware. + c := gorestful.NewContainer() + c.Filter(gorestfulmiddleware.Handler("", m)) + + // Setup handlers. + ws := &gorestful.WebService{} + for _, h := range hc { + h := h + ws.Route(ws.Method(h.Method).Path(h.Path).To(func(_ *gorestful.Request, resp *gorestful.Response) { + time.Sleep(h.SleepDuration) + resp.WriteHeader(h.Code) + // nolint: errcheck + resp.Write([]byte(h.ReturnData)) + })) + } + c.Add(ws) + + return c +} + +func prepareHandlerGin(m middleware.Middleware, hc []handlerConfig) http.Handler { + // Setup server and middleware. + e := gin.New() + e.Use(ginmiddleware.Handler("", m)) + + // Setup handlers. + for _, h := range hc { + h := h + e.Handle(h.Method, h.Path, func(c *gin.Context) { + time.Sleep(h.SleepDuration) + c.String(h.Code, h.ReturnData) + }) + } + + return e +} + +func prepareHandlerEcho(m middleware.Middleware, hc []handlerConfig) http.Handler { + // Setup server and middleware. + e := echo.New() + e.Use(echomiddleware.Handler("", m)) + + // Setup handlers. + for _, h := range hc { + h := h + e.Add(h.Method, h.Path, func(c echo.Context) error { + time.Sleep(h.SleepDuration) + return c.String(h.Code, h.ReturnData) + }) + } + + return e +}