├── .github ├── ISSUE_TEMPLATE.md ├── stale.yml └── workflows │ └── echo-contrib.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── casbin ├── README.md ├── auth_model.conf ├── auth_policy.csv ├── broken_auth_model.conf ├── casbin.go └── casbin_test.go ├── codecov.yml ├── echo.go ├── echoprometheus ├── README.md ├── prometheus.go └── prometheus_test.go ├── go.mod ├── go.sum ├── jaegertracing ├── jaegertracing.go ├── jaegertracing_test.go └── response_dumper.go ├── pprof ├── README.md ├── pprof.go └── pprof_test.go ├── prometheus ├── prometheus.go └── prometheus_test.go ├── session ├── session.go └── session_test.go └── zipkintracing ├── README.md ├── response_writer.go ├── tracing.go └── tracing_test.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Issue Description 2 | 3 | ### Checklist 4 | 5 | - [ ] Dependencies installed 6 | - [ ] No typos 7 | - [ ] Searched existing issues and docs 8 | 9 | ### Expected behaviour 10 | 11 | ### Actual behaviour 12 | 13 | ### Steps to reproduce 14 | 15 | ### Working code to debug 16 | 17 | ```go 18 | package main 19 | 20 | func main() { 21 | } 22 | ``` 23 | 24 | ### Version/commit 25 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 30 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed within a month if no further activity occurs. 17 | Thank you for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /.github/workflows/echo-contrib.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read # to fetch code (actions/checkout) 14 | 15 | env: 16 | # run coverage and benchmarks only with the latest Go version 17 | LATEST_GO_VERSION: "1.24" 18 | 19 | jobs: 20 | test: 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy 25 | # Echo CORE tests with last four major releases (unless there are pressing vulnerabilities) 26 | # As we depend on MANY DIFFERENT libraries which of SOME support last 2 Go releases we could have situations when 27 | # we derive from last four major releases promise. 28 | go: ["1.23", "1.24"] 29 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - name: Checkout Code 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Go ${{ matrix.go }} 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version: ${{ matrix.go }} 39 | 40 | - name: Run Tests 41 | run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... 42 | 43 | - name: Upload coverage to Codecov 44 | if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest' 45 | uses: codecov/codecov-action@v5 46 | with: 47 | token: 48 | fail_ci_if_error: false 49 | 50 | benchmark: 51 | needs: test 52 | name: Benchmark comparison 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout Code (Previous) 56 | uses: actions/checkout@v4 57 | with: 58 | ref: ${{ github.base_ref }} 59 | path: previous 60 | 61 | - name: Checkout Code (New) 62 | uses: actions/checkout@v4 63 | with: 64 | path: new 65 | 66 | - name: Set up Go ${{ matrix.go }} 67 | uses: actions/setup-go@v5 68 | with: 69 | go-version: ${{ env.LATEST_GO_VERSION }} 70 | 71 | - name: Install Dependencies 72 | run: go install golang.org/x/perf/cmd/benchstat@latest 73 | 74 | - name: Run Benchmark (Previous) 75 | run: | 76 | cd previous 77 | go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt 78 | 79 | - name: Run Benchmark (New) 80 | run: | 81 | cd new 82 | go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt 83 | 84 | - name: Run Benchstat 85 | run: | 86 | benchstat previous/benchmark.txt new/benchmark.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | _test 3 | coverage.txt 4 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 LabStack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := "github.com/labstack/echo-contrib" 2 | PKG_LIST := $(shell go list ${PKG}/...) 3 | 4 | .DEFAULT_GOAL := check 5 | check: lint vet race ## Check project 6 | 7 | init: 8 | @go install honnef.co/go/tools/cmd/staticcheck@latest 9 | 10 | format: ## Format the source code 11 | @find ./ -type f -name "*.go" -exec gofmt -w {} \; 12 | 13 | lint: ## Lint the files 14 | @staticcheck -tests=false ${PKG_LIST} 15 | 16 | vet: ## Vet the files 17 | @go vet ${PKG_LIST} 18 | 19 | test: ## Run tests 20 | @go test -short ${PKG_LIST} 21 | 22 | race: ## Run tests with data race detector 23 | @go test -race ${PKG_LIST} 24 | 25 | benchmark: ## Run benchmarks 26 | @go test -run="-" -bench=".*" ${PKG_LIST} 27 | 28 | help: ## Display this help screen 29 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 30 | 31 | goversion ?= "1.18" 32 | test_version: ## Run tests inside Docker with given version (defaults to 1.18 oldest supported). Example: make test_version goversion=1.18 33 | @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make race" 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Echo Community Contribution middlewares 2 | 3 | [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo-contrib) 4 | [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo-contrib.svg?style=flat-square)](https://codecov.io/gh/labstack/echo-contrib) 5 | [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) 6 | 7 | * [Official website](https://echo.labstack.com) 8 | * [All middleware docs](https://echo.labstack.com/docs/category/middleware) 9 | 10 | # Supported Go version 11 | 12 | Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy 13 | 14 | 15 | [Echo CORE](https://github.com/labstack/echo) tests with last FOUR major releases (unless there are pressing vulnerabilities) 16 | As this library depends on MANY DIFFERENT libraries which of SOME support only last 2 Go releases we could have situations when 17 | we derive from last four major releases promise. 18 | 19 | p.s. you really should use latest versions of Go as there are many vulnerebilites fixed only in supported versions. Please see https://pkg.go.dev/vuln/ 20 | -------------------------------------------------------------------------------- /casbin/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | Simple example: 3 | ```go 4 | package main 5 | 6 | import ( 7 | "github.com/casbin/casbin/v2" 8 | "github.com/labstack/echo/v4" 9 | casbin_mw "github.com/labstack/echo-contrib/casbin" 10 | ) 11 | 12 | func main() { 13 | e := echo.New() 14 | 15 | // Mediate the access for every request 16 | e.Use(casbin_mw.Middleware(casbin.NewEnforcer("auth_model.conf", "auth_policy.csv"))) 17 | 18 | e.Logger.Fatal(e.Start(":1323")) 19 | } 20 | ``` 21 | 22 | Advanced example: 23 | ```go 24 | package main 25 | 26 | import ( 27 | "github.com/casbin/casbin/v2" 28 | "github.com/labstack/echo/v4" 29 | casbin_mw "github.com/labstack/echo-contrib/casbin" 30 | ) 31 | 32 | func main() { 33 | ce, _ := casbin.NewEnforcer("auth_model.conf", "") 34 | ce.AddRoleForUser("alice", "admin") 35 | ce.AddPolicy("added_user", "data1", "read") 36 | 37 | e := echo.New() 38 | 39 | e.Use(casbin_mw.Middleware(ce)) 40 | 41 | e.Logger.Fatal(e.Start(":1323")) 42 | } 43 | ``` 44 | 45 | # API Reference 46 | See [API Overview](https://casbin.org/docs/api-overview). -------------------------------------------------------------------------------- /casbin/auth_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") 15 | -------------------------------------------------------------------------------- /casbin/auth_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, /dataset1/*, GET 2 | p, alice, /dataset1/resource1, POST 3 | p, bob, /dataset2/resource1, * 4 | p, bob, /dataset2/resource2, GET 5 | p, bob, /dataset2/folder1/*, POST 6 | p, dataset1_admin, /dataset1/*, * 7 | g, cathy, dataset1_admin 8 | -------------------------------------------------------------------------------- /casbin/broken_auth_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") 15 | -------------------------------------------------------------------------------- /casbin/casbin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | /* Package casbin provides middleware to enable ACL, RBAC, ABAC authorization support. 5 | 6 | Simple example: 7 | 8 | package main 9 | 10 | import ( 11 | "github.com/casbin/casbin/v2" 12 | "github.com/labstack/echo/v4" 13 | casbin_mw "github.com/labstack/echo-contrib/casbin" 14 | ) 15 | 16 | func main() { 17 | e := echo.New() 18 | 19 | // Mediate the access for every request 20 | e.Use(casbin_mw.Middleware(casbin.NewEnforcer("auth_model.conf", "auth_policy.csv"))) 21 | 22 | e.Logger.Fatal(e.Start(":1323")) 23 | } 24 | 25 | Advanced example: 26 | 27 | package main 28 | 29 | import ( 30 | "github.com/casbin/casbin/v2" 31 | "github.com/labstack/echo/v4" 32 | casbin_mw "github.com/labstack/echo-contrib/casbin" 33 | ) 34 | 35 | func main() { 36 | ce, _ := casbin.NewEnforcer("auth_model.conf", "") 37 | ce.AddRoleForUser("alice", "admin") 38 | ce.AddPolicy(...) 39 | 40 | e := echo.New() 41 | 42 | e.Use(casbin_mw.Middleware(ce)) 43 | 44 | e.Logger.Fatal(e.Start(":1323")) 45 | } 46 | */ 47 | 48 | package casbin 49 | 50 | import ( 51 | "errors" 52 | "github.com/casbin/casbin/v2" 53 | "github.com/labstack/echo/v4" 54 | "github.com/labstack/echo/v4/middleware" 55 | "net/http" 56 | ) 57 | 58 | type ( 59 | // Config defines the config for CasbinAuth middleware. 60 | Config struct { 61 | // Skipper defines a function to skip middleware. 62 | Skipper middleware.Skipper 63 | 64 | // Enforcer CasbinAuth main rule. 65 | // One of Enforcer or EnforceHandler fields is required. 66 | Enforcer *casbin.Enforcer 67 | 68 | // EnforceHandler is custom callback to handle enforcing. 69 | // One of Enforcer or EnforceHandler fields is required. 70 | EnforceHandler func(c echo.Context, user string) (bool, error) 71 | 72 | // Method to get the username - defaults to using basic auth 73 | UserGetter func(c echo.Context) (string, error) 74 | 75 | // Method to handle errors 76 | ErrorHandler func(c echo.Context, internal error, proposedStatus int) error 77 | } 78 | ) 79 | 80 | var ( 81 | // DefaultConfig is the default CasbinAuth middleware config. 82 | DefaultConfig = Config{ 83 | Skipper: middleware.DefaultSkipper, 84 | UserGetter: func(c echo.Context) (string, error) { 85 | username, _, _ := c.Request().BasicAuth() 86 | return username, nil 87 | }, 88 | ErrorHandler: func(c echo.Context, internal error, proposedStatus int) error { 89 | err := echo.NewHTTPError(proposedStatus, internal.Error()) 90 | err.Internal = internal 91 | return err 92 | }, 93 | } 94 | ) 95 | 96 | // Middleware returns a CasbinAuth middleware. 97 | // 98 | // For valid credentials it calls the next handler. 99 | // For missing or invalid credentials, it sends "401 - Unauthorized" response. 100 | func Middleware(ce *casbin.Enforcer) echo.MiddlewareFunc { 101 | c := DefaultConfig 102 | c.Enforcer = ce 103 | return MiddlewareWithConfig(c) 104 | } 105 | 106 | // MiddlewareWithConfig returns a CasbinAuth middleware with config. 107 | // See `Middleware()`. 108 | func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { 109 | if config.Enforcer == nil && config.EnforceHandler == nil { 110 | panic("one of casbin middleware Enforcer or EnforceHandler fields must be set") 111 | } 112 | if config.Skipper == nil { 113 | config.Skipper = DefaultConfig.Skipper 114 | } 115 | if config.UserGetter == nil { 116 | config.UserGetter = DefaultConfig.UserGetter 117 | } 118 | if config.ErrorHandler == nil { 119 | config.ErrorHandler = DefaultConfig.ErrorHandler 120 | } 121 | if config.EnforceHandler == nil { 122 | config.EnforceHandler = func(c echo.Context, user string) (bool, error) { 123 | return config.Enforcer.Enforce(user, c.Request().URL.Path, c.Request().Method) 124 | } 125 | } 126 | 127 | return func(next echo.HandlerFunc) echo.HandlerFunc { 128 | return func(c echo.Context) error { 129 | if config.Skipper(c) { 130 | return next(c) 131 | } 132 | 133 | user, err := config.UserGetter(c) 134 | if err != nil { 135 | return config.ErrorHandler(c, err, http.StatusForbidden) 136 | } 137 | pass, err := config.EnforceHandler(c, user) 138 | if err != nil { 139 | return config.ErrorHandler(c, err, http.StatusInternalServerError) 140 | } 141 | if !pass { 142 | return config.ErrorHandler(c, errors.New("enforce did not pass"), http.StatusForbidden) 143 | } 144 | return next(c) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /casbin/casbin_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package casbin 5 | 6 | import ( 7 | "errors" 8 | "github.com/stretchr/testify/assert" 9 | "net/http" 10 | "net/http/httptest" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/casbin/casbin/v2" 15 | "github.com/labstack/echo/v4" 16 | "github.com/labstack/echo/v4/middleware" 17 | ) 18 | 19 | func testRequest(t *testing.T, h echo.HandlerFunc, user string, path string, method string, code int) { 20 | e := echo.New() 21 | req := httptest.NewRequest(method, path, nil) 22 | req.SetBasicAuth(user, "secret") 23 | res := httptest.NewRecorder() 24 | c := e.NewContext(req, res) 25 | 26 | err := h(c) 27 | 28 | if err != nil { 29 | var errObj *echo.HTTPError 30 | if errors.As(err, &errObj) { 31 | if errObj.Code != code { 32 | t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, errObj.Code, code) 33 | } 34 | } 35 | } else { 36 | if c.Response().Status != code { 37 | t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response().Status, code) 38 | } 39 | } 40 | } 41 | 42 | func TestAuth(t *testing.T) { 43 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 44 | h := Middleware(ce)(func(c echo.Context) error { 45 | return c.String(http.StatusOK, "test") 46 | }) 47 | 48 | testRequest(t, h, "alice", "/dataset1/resource1", echo.GET, http.StatusOK) 49 | testRequest(t, h, "alice", "/dataset1/resource1", echo.POST, http.StatusOK) 50 | testRequest(t, h, "alice", "/dataset1/resource2", echo.GET, http.StatusOK) 51 | testRequest(t, h, "alice", "/dataset1/resource2", echo.POST, http.StatusForbidden) 52 | } 53 | 54 | func TestPathWildcard(t *testing.T) { 55 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 56 | h := Middleware(ce)(func(c echo.Context) error { 57 | return c.String(http.StatusOK, "test") 58 | }) 59 | 60 | testRequest(t, h, "bob", "/dataset2/resource1", echo.GET, http.StatusOK) 61 | testRequest(t, h, "bob", "/dataset2/resource1", echo.POST, http.StatusOK) 62 | testRequest(t, h, "bob", "/dataset2/resource1", echo.DELETE, http.StatusOK) 63 | testRequest(t, h, "bob", "/dataset2/resource2", echo.GET, http.StatusOK) 64 | testRequest(t, h, "bob", "/dataset2/resource2", echo.POST, http.StatusForbidden) 65 | testRequest(t, h, "bob", "/dataset2/resource2", echo.DELETE, http.StatusForbidden) 66 | 67 | testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.GET, http.StatusForbidden) 68 | testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.POST, http.StatusOK) 69 | testRequest(t, h, "bob", "/dataset2/folder1/item1", echo.DELETE, http.StatusForbidden) 70 | testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.GET, http.StatusForbidden) 71 | testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.POST, http.StatusOK) 72 | testRequest(t, h, "bob", "/dataset2/folder1/item2", echo.DELETE, http.StatusForbidden) 73 | } 74 | 75 | func TestRBAC(t *testing.T) { 76 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 77 | h := Middleware(ce)(func(c echo.Context) error { 78 | return c.String(http.StatusOK, "test") 79 | }) 80 | 81 | // cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role. 82 | testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusOK) 83 | testRequest(t, h, "cathy", "/dataset1/item", echo.POST, http.StatusOK) 84 | testRequest(t, h, "cathy", "/dataset1/item", echo.DELETE, http.StatusOK) 85 | testRequest(t, h, "cathy", "/dataset2/item", echo.GET, http.StatusForbidden) 86 | testRequest(t, h, "cathy", "/dataset2/item", echo.POST, http.StatusForbidden) 87 | testRequest(t, h, "cathy", "/dataset2/item", echo.DELETE, http.StatusForbidden) 88 | 89 | // delete all roles on user cathy, so cathy cannot access any resources now. 90 | ce.DeleteRolesForUser("cathy") 91 | 92 | testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) 93 | testRequest(t, h, "cathy", "/dataset1/item", echo.POST, http.StatusForbidden) 94 | testRequest(t, h, "cathy", "/dataset1/item", echo.DELETE, http.StatusForbidden) 95 | testRequest(t, h, "cathy", "/dataset2/item", echo.GET, http.StatusForbidden) 96 | testRequest(t, h, "cathy", "/dataset2/item", echo.POST, http.StatusForbidden) 97 | testRequest(t, h, "cathy", "/dataset2/item", echo.DELETE, http.StatusForbidden) 98 | } 99 | 100 | func TestEnforceError(t *testing.T) { 101 | ce, _ := casbin.NewEnforcer("broken_auth_model.conf", "auth_policy.csv") 102 | h := Middleware(ce)(func(c echo.Context) error { 103 | return c.String(http.StatusOK, "test") 104 | }) 105 | 106 | testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusInternalServerError) 107 | } 108 | 109 | func TestCustomUserGetter(t *testing.T) { 110 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 111 | cnf := Config{ 112 | Skipper: middleware.DefaultSkipper, 113 | Enforcer: ce, 114 | UserGetter: func(c echo.Context) (string, error) { 115 | return "not_cathy_at_all", nil 116 | }, 117 | } 118 | h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { 119 | return c.String(http.StatusOK, "test") 120 | }) 121 | testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) 122 | } 123 | 124 | func TestUserGetterError(t *testing.T) { 125 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 126 | cnf := Config{ 127 | Skipper: middleware.DefaultSkipper, 128 | Enforcer: ce, 129 | UserGetter: func(c echo.Context) (string, error) { 130 | return "", errors.New("no idea who you are") 131 | }, 132 | } 133 | h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { 134 | return c.String(http.StatusOK, "test") 135 | }) 136 | testRequest(t, h, "cathy", "/dataset1/item", echo.GET, http.StatusForbidden) 137 | } 138 | 139 | func TestCustomEnforceHandler(t *testing.T) { 140 | ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 141 | assert.NoError(t, err) 142 | 143 | _, err = ce.AddPolicy("bob", "/user/bob", "PATCH_SELF") 144 | assert.NoError(t, err) 145 | 146 | cnf := Config{ 147 | EnforceHandler: func(c echo.Context, user string) (bool, error) { 148 | method := c.Request().Method 149 | if strings.HasPrefix(c.Request().URL.Path, "/user/bob") { 150 | method += "_SELF" 151 | } 152 | return ce.Enforce(user, c.Request().URL.Path, method) 153 | }, 154 | } 155 | h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { 156 | return c.String(http.StatusOK, "test") 157 | }) 158 | testRequest(t, h, "bob", "/dataset2/resource1", echo.GET, http.StatusOK) 159 | testRequest(t, h, "bob", "/user/alice", echo.PATCH, http.StatusForbidden) 160 | testRequest(t, h, "bob", "/user/bob", echo.PATCH, http.StatusOK) 161 | } 162 | 163 | func TestCustomSkipper(t *testing.T) { 164 | ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") 165 | cnf := Config{ 166 | Skipper: func(c echo.Context) bool { 167 | return c.Request().URL.Path == "/dataset1/resource1" 168 | }, 169 | Enforcer: ce, 170 | } 171 | h := MiddlewareWithConfig(cnf)(func(c echo.Context) error { 172 | return c.String(http.StatusOK, "test") 173 | }) 174 | testRequest(t, h, "alice", "/dataset1/resource1", echo.GET, http.StatusOK) 175 | testRequest(t, h, "alice", "/dataset1/resource2", echo.POST, http.StatusForbidden) 176 | } 177 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 1% 6 | patch: 7 | default: 8 | threshold: 1% 9 | 10 | comment: 11 | require_changes: true -------------------------------------------------------------------------------- /echo.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package echo 5 | -------------------------------------------------------------------------------- /echoprometheus/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ``` 4 | package main 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/echo-contrib/echoprometheus" 9 | ) 10 | 11 | func main() { 12 | e := echo.New() 13 | // Enable metrics middleware 14 | e.Use(echoprometheus.NewMiddleware("myapp")) 15 | e.GET("/metrics", echoprometheus.NewHandler()) 16 | 17 | e.Logger.Fatal(e.Start(":1323")) 18 | } 19 | ``` 20 | 21 | 22 | # How to migrate 23 | 24 | ## Creating and adding middleware to the application 25 | 26 | Older `prometheus` middleware 27 | ```go 28 | e := echo.New() 29 | p := prometheus.NewPrometheus("echo", nil) 30 | p.Use(e) 31 | ``` 32 | 33 | With the new `echoprometheus` middleware 34 | ```go 35 | e := echo.New() 36 | e.Use(echoprometheus.NewMiddleware("myapp")) // register middleware to gather metrics from requests 37 | e.GET("/metrics", echoprometheus.NewHandler()) // register route to serve gathered metrics in Prometheus format 38 | ``` 39 | 40 | ## Replacement for `Prometheus.MetricsList` field, `NewMetric(m *Metric, subsystem string)` function and `prometheus.Metric` struct 41 | 42 | The `NewMetric` function allowed to create custom metrics with the old `prometheus` middleware. This helper is no longer available 43 | to avoid the added complexity. It is recommended to use native Prometheus metrics and register those yourself. 44 | 45 | This can be done now as follows: 46 | ```go 47 | e := echo.New() 48 | 49 | customRegistry := prometheus.NewRegistry() // create custom registry for your custom metrics 50 | customCounter := prometheus.NewCounter( // create new counter metric. This is replacement for `prometheus.Metric` struct 51 | prometheus.CounterOpts{ 52 | Name: "custom_requests_total", 53 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 54 | }, 55 | ) 56 | if err := customRegistry.Register(customCounter); err != nil { // register your new counter metric with metrics registry 57 | log.Fatal(err) 58 | } 59 | 60 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 61 | AfterNext: func(c echo.Context, err error) { 62 | customCounter.Inc() // use our custom metric in middleware. after every request increment the counter 63 | }, 64 | Registerer: customRegistry, // use our custom registry instead of default Prometheus registry 65 | })) 66 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) // register route for getting gathered metrics data from our custom Registry 67 | ``` 68 | 69 | ## Replacement for `Prometheus.MetricsPath` 70 | 71 | `MetricsPath` was used to skip metrics own route from Prometheus metrics. Skipping is no longer done and requests to Prometheus 72 | route will be included in gathered metrics. 73 | 74 | To restore the old behaviour the `/metrics` path needs to be excluded from counting using the Skipper function: 75 | ```go 76 | conf := echoprometheus.MiddlewareConfig{ 77 | Skipper: func(c echo.Context) bool { 78 | return c.Path() == "/metrics" 79 | }, 80 | } 81 | e.Use(echoprometheus.NewMiddlewareWithConfig(conf)) 82 | ``` 83 | 84 | ## Replacement for `Prometheus.RequestCounterURLLabelMappingFunc` and `Prometheus.RequestCounterHostLabelMappingFunc` 85 | 86 | These function fields were used to define how "URL" or "Host" attribute in Prometheus metric lines are created. 87 | 88 | These can now be substituted by using `LabelFuncs`: 89 | ```go 90 | e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ 91 | LabelFuncs: map[string]echoprometheus.LabelValueFunc{ 92 | "scheme": func(c echo.Context, err error) string { // additional custom label 93 | return c.Scheme() 94 | }, 95 | "url": func(c echo.Context, err error) string { // overrides default 'url' label value 96 | return "x_" + c.Request().URL.Path 97 | }, 98 | "host": func(c echo.Context, err error) string { // overrides default 'host' label value 99 | return "y_" + c.Request().Host 100 | }, 101 | }, 102 | })) 103 | ``` 104 | 105 | Will produce Prometheus line as 106 | `echo_request_duration_seconds_count{code="200",host="y_example.com",method="GET",scheme="http",url="x_/ok",scheme="http"} 1` 107 | 108 | 109 | ## Replacement for `Metric.Buckets` and modifying default metrics 110 | 111 | The `echoprometheus` middleware registers the following metrics by default: 112 | 113 | * Counter `requests_total` 114 | * Histogram `request_duration_seconds` 115 | * Histogram `response_size_bytes` 116 | * Histogram `request_size_bytes` 117 | 118 | You can modify their definition before these metrics are registed with `CounterOptsFunc` and `HistogramOptsFunc` callbacks 119 | 120 | Example: 121 | ```go 122 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 123 | HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { 124 | if opts.Name == "request_duration_seconds" { 125 | opts.Buckets = []float64{1.0 * bKB, 2.0 * bKB, 5.0 * bKB, 10.0 * bKB, 100 * bKB, 500 * bKB, 1.0 * bMB, 2.5 * bMB, 5.0 * bMB, 10.0 * bMB} 126 | } 127 | return opts 128 | }, 129 | CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { 130 | if opts.Name == "requests_total" { 131 | opts.ConstLabels = prometheus.Labels{"my_const": "123"} 132 | } 133 | return opts 134 | }, 135 | })) 136 | ``` 137 | 138 | ## Replacement for `PushGateway` struct and related methods 139 | 140 | Function `RunPushGatewayGatherer` starts pushing collected metrics and block until context completes or ErrorHandler returns an error. 141 | This function should be run in separate goroutine. 142 | 143 | Example: 144 | ```go 145 | go func() { 146 | config := echoprometheus.PushGatewayConfig{ 147 | PushGatewayURL: "https://host:9080", 148 | PushInterval: 10 * time.Millisecond, 149 | } 150 | if err := echoprometheus.RunPushGatewayGatherer(context.Background(), config); !errors.Is(err, context.Canceled) { 151 | log.Fatal(err) 152 | } 153 | }() 154 | ``` -------------------------------------------------------------------------------- /echoprometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | /* 5 | Package echoprometheus provides middleware to add Prometheus metrics. 6 | */ 7 | package echoprometheus 8 | 9 | import ( 10 | "bytes" 11 | "context" 12 | "errors" 13 | "fmt" 14 | "github.com/labstack/echo/v4" 15 | "github.com/labstack/echo/v4/middleware" 16 | "github.com/labstack/gommon/log" 17 | "github.com/prometheus/client_golang/prometheus" 18 | "github.com/prometheus/client_golang/prometheus/promhttp" 19 | "github.com/prometheus/common/expfmt" 20 | "io" 21 | "net/http" 22 | "sort" 23 | "strconv" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const ( 29 | defaultSubsystem = "echo" 30 | ) 31 | 32 | const ( 33 | _ = iota // ignore first value by assigning to blank identifier 34 | bKB float64 = 1 << (10 * iota) 35 | bMB 36 | ) 37 | 38 | // sizeBuckets is the buckets for request/response size. Here we define a spectrum from 1KB through 1NB up to 10MB. 39 | var sizeBuckets = []float64{1.0 * bKB, 2.0 * bKB, 5.0 * bKB, 10.0 * bKB, 100 * bKB, 500 * bKB, 1.0 * bMB, 2.5 * bMB, 5.0 * bMB, 10.0 * bMB} 40 | 41 | // MiddlewareConfig contains the configuration for creating prometheus middleware collecting several default metrics. 42 | type MiddlewareConfig struct { 43 | // Skipper defines a function to skip middleware. 44 | Skipper middleware.Skipper 45 | 46 | // Namespace is components of the fully-qualified name of the Metric (created by joining Namespace,Subsystem and Name components with "_") 47 | // Optional 48 | Namespace string 49 | 50 | // Subsystem is components of the fully-qualified name of the Metric (created by joining Namespace,Subsystem and Name components with "_") 51 | // Defaults to: "echo" 52 | Subsystem string 53 | 54 | // LabelFuncs allows adding custom labels in addition to default labels. When key has same name with default label 55 | // it replaces default one. 56 | LabelFuncs map[string]LabelValueFunc 57 | 58 | // HistogramOptsFunc allows to change options for metrics of type histogram before metric is registered to Registerer 59 | HistogramOptsFunc func(opts prometheus.HistogramOpts) prometheus.HistogramOpts 60 | 61 | // CounterOptsFunc allows to change options for metrics of type counter before metric is registered to Registerer 62 | CounterOptsFunc func(opts prometheus.CounterOpts) prometheus.CounterOpts 63 | 64 | // Registerer sets the prometheus.Registerer instance the middleware will register these metrics with. 65 | // Defaults to: prometheus.DefaultRegisterer 66 | Registerer prometheus.Registerer 67 | 68 | // BeforeNext is callback that is executed before next middleware/handler is called. Useful for case when you have own 69 | // metrics that need data to be stored for AfterNext. 70 | BeforeNext func(c echo.Context) 71 | 72 | // AfterNext is callback that is executed after next middleware/handler returns. Useful for case when you have own 73 | // metrics that need incremented/observed. 74 | AfterNext func(c echo.Context, err error) 75 | 76 | timeNow func() time.Time 77 | 78 | // If DoNotUseRequestPathFor404 is true, all 404 responses (due to non-matching route) will have the same `url` label and 79 | // thus won't generate new metrics. 80 | DoNotUseRequestPathFor404 bool 81 | 82 | // StatusCodeResolver resolves err & context into http status code. Default is to use context.Response().Status 83 | StatusCodeResolver func(c echo.Context, err error) int 84 | } 85 | 86 | type LabelValueFunc func(c echo.Context, err error) string 87 | 88 | // HandlerConfig contains the configuration for creating HTTP handler for metrics. 89 | type HandlerConfig struct { 90 | // Gatherer sets the prometheus.Gatherer instance the middleware will use when generating the metric endpoint handler. 91 | // Defaults to: prometheus.DefaultGatherer 92 | Gatherer prometheus.Gatherer 93 | } 94 | 95 | // PushGatewayConfig contains the configuration for pushing to a Prometheus push gateway. 96 | type PushGatewayConfig struct { 97 | // PushGatewayURL is push gateway URL in format http://domain:port 98 | PushGatewayURL string 99 | 100 | // PushInterval in ticker interval for pushing gathered metrics to the Gateway 101 | // Defaults to: 1 minute 102 | PushInterval time.Duration 103 | 104 | // Gatherer sets the prometheus.Gatherer instance the middleware will use when generating the metric endpoint handler. 105 | // Defaults to: prometheus.DefaultGatherer 106 | Gatherer prometheus.Gatherer 107 | 108 | // ErrorHandler is function that is called when errors occur. When callback returns error StartPushGateway also returns. 109 | ErrorHandler func(err error) error 110 | 111 | // ClientTransport specifies the mechanism by which individual HTTP POST requests are made. 112 | // Defaults to: http.DefaultTransport 113 | ClientTransport http.RoundTripper 114 | } 115 | 116 | // NewHandler creates new instance of Handler using Prometheus default registry. 117 | func NewHandler() echo.HandlerFunc { 118 | return NewHandlerWithConfig(HandlerConfig{}) 119 | } 120 | 121 | // NewHandlerWithConfig creates new instance of Handler using given configuration. 122 | func NewHandlerWithConfig(config HandlerConfig) echo.HandlerFunc { 123 | if config.Gatherer == nil { 124 | config.Gatherer = prometheus.DefaultGatherer 125 | } 126 | h := promhttp.HandlerFor(config.Gatherer, promhttp.HandlerOpts{DisableCompression: true}) 127 | 128 | if r, ok := config.Gatherer.(prometheus.Registerer); ok { 129 | h = promhttp.InstrumentMetricHandler(r, h) 130 | } 131 | 132 | return func(c echo.Context) error { 133 | h.ServeHTTP(c.Response(), c.Request()) 134 | return nil 135 | } 136 | } 137 | 138 | // NewMiddleware creates new instance of middleware using Prometheus default registry. 139 | func NewMiddleware(subsystem string) echo.MiddlewareFunc { 140 | return NewMiddlewareWithConfig(MiddlewareConfig{Subsystem: subsystem}) 141 | } 142 | 143 | // NewMiddlewareWithConfig creates new instance of middleware using given configuration. 144 | func NewMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc { 145 | mw, err := config.ToMiddleware() 146 | if err != nil { 147 | panic(err) 148 | } 149 | return mw 150 | } 151 | 152 | // ToMiddleware converts configuration to middleware or returns an error. 153 | func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { 154 | if conf.timeNow == nil { 155 | conf.timeNow = time.Now 156 | } 157 | if conf.Subsystem == "" { 158 | conf.Subsystem = defaultSubsystem 159 | } 160 | if conf.Registerer == nil { 161 | conf.Registerer = prometheus.DefaultRegisterer 162 | } 163 | if conf.CounterOptsFunc == nil { 164 | conf.CounterOptsFunc = func(opts prometheus.CounterOpts) prometheus.CounterOpts { 165 | return opts 166 | } 167 | } 168 | if conf.HistogramOptsFunc == nil { 169 | conf.HistogramOptsFunc = func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { 170 | return opts 171 | } 172 | } 173 | if conf.StatusCodeResolver == nil { 174 | conf.StatusCodeResolver = defaultStatusResolver 175 | } 176 | 177 | labelNames, customValuers := createLabels(conf.LabelFuncs) 178 | 179 | requestCount := prometheus.NewCounterVec( 180 | conf.CounterOptsFunc(prometheus.CounterOpts{ 181 | Namespace: conf.Namespace, 182 | Subsystem: conf.Subsystem, 183 | Name: "requests_total", 184 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 185 | }), 186 | labelNames, 187 | ) 188 | // we do not allow skipping or replacing default collector but developer can use `conf.CounterOptsFunc` to rename 189 | // this middleware default collector, so they can have own collector with that same name. 190 | // and we treat all register errors as returnable failures 191 | if err := conf.Registerer.Register(requestCount); err != nil { 192 | return nil, err 193 | } 194 | 195 | requestDuration := prometheus.NewHistogramVec( 196 | conf.HistogramOptsFunc(prometheus.HistogramOpts{ 197 | Namespace: conf.Namespace, 198 | Subsystem: conf.Subsystem, 199 | Name: "request_duration_seconds", 200 | Help: "The HTTP request latencies in seconds.", 201 | // Here, we use the prometheus defaults which are for ~10s request length max: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 202 | Buckets: prometheus.DefBuckets, 203 | }), 204 | labelNames, 205 | ) 206 | if err := conf.Registerer.Register(requestDuration); err != nil { 207 | return nil, err 208 | } 209 | 210 | responseSize := prometheus.NewHistogramVec( 211 | conf.HistogramOptsFunc(prometheus.HistogramOpts{ 212 | Namespace: conf.Namespace, 213 | Subsystem: conf.Subsystem, 214 | Name: "response_size_bytes", 215 | Help: "The HTTP response sizes in bytes.", 216 | Buckets: sizeBuckets, 217 | }), 218 | labelNames, 219 | ) 220 | if err := conf.Registerer.Register(responseSize); err != nil { 221 | return nil, err 222 | } 223 | 224 | requestSize := prometheus.NewHistogramVec( 225 | conf.HistogramOptsFunc(prometheus.HistogramOpts{ 226 | Namespace: conf.Namespace, 227 | Subsystem: conf.Subsystem, 228 | Name: "request_size_bytes", 229 | Help: "The HTTP request sizes in bytes.", 230 | Buckets: sizeBuckets, 231 | }), 232 | labelNames, 233 | ) 234 | if err := conf.Registerer.Register(requestSize); err != nil { 235 | return nil, err 236 | } 237 | 238 | return func(next echo.HandlerFunc) echo.HandlerFunc { 239 | return func(c echo.Context) error { 240 | // NB: we do not skip metrics handler path by default. This can be added with custom Skipper but for default 241 | // behaviour we measure metrics path request/response metrics also 242 | if conf.Skipper != nil && conf.Skipper(c) { 243 | return next(c) 244 | } 245 | 246 | if conf.BeforeNext != nil { 247 | conf.BeforeNext(c) 248 | } 249 | reqSz := computeApproximateRequestSize(c.Request()) 250 | 251 | start := conf.timeNow() 252 | err := next(c) 253 | elapsed := float64(conf.timeNow().Sub(start)) / float64(time.Second) 254 | 255 | if conf.AfterNext != nil { 256 | conf.AfterNext(c, err) 257 | } 258 | 259 | url := c.Path() // contains route path ala `/users/:id` 260 | if url == "" && !conf.DoNotUseRequestPathFor404 { 261 | // as of Echo v4.10.1 path is empty for 404 cases (when router did not find any matching routes) 262 | // in this case we use actual path from request to have some distinction in Prometheus 263 | url = c.Request().URL.Path 264 | } 265 | 266 | status := conf.StatusCodeResolver(c, err) 267 | 268 | values := make([]string, len(labelNames)) 269 | values[0] = strconv.Itoa(status) 270 | values[1] = c.Request().Method 271 | values[2] = c.Request().Host 272 | values[3] = strings.ToValidUTF8(url, "\uFFFD") // \uFFFD is � https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character 273 | for _, cv := range customValuers { 274 | values[cv.index] = cv.valueFunc(c, err) 275 | } 276 | if obs, err := requestDuration.GetMetricWithLabelValues(values...); err == nil { 277 | obs.Observe(elapsed) 278 | } else { 279 | return fmt.Errorf("failed to label request duration metric with values, err: %w", err) 280 | } 281 | if obs, err := requestCount.GetMetricWithLabelValues(values...); err == nil { 282 | obs.Inc() 283 | } else { 284 | return fmt.Errorf("failed to label request count metric with values, err: %w", err) 285 | } 286 | if obs, err := requestSize.GetMetricWithLabelValues(values...); err == nil { 287 | obs.Observe(float64(reqSz)) 288 | } else { 289 | return fmt.Errorf("failed to label request size metric with values, err: %w", err) 290 | } 291 | if obs, err := responseSize.GetMetricWithLabelValues(values...); err == nil { 292 | obs.Observe(float64(c.Response().Size)) 293 | } else { 294 | return fmt.Errorf("failed to label response size metric with values, err: %w", err) 295 | } 296 | 297 | return err 298 | } 299 | }, nil 300 | } 301 | 302 | type customLabelValuer struct { 303 | index int 304 | label string 305 | valueFunc LabelValueFunc 306 | } 307 | 308 | func createLabels(customLabelFuncs map[string]LabelValueFunc) ([]string, []customLabelValuer) { 309 | labelNames := []string{"code", "method", "host", "url"} 310 | if len(customLabelFuncs) == 0 { 311 | return labelNames, nil 312 | } 313 | 314 | customValuers := make([]customLabelValuer, 0) 315 | // we create valuers in two passes for a reason - first to get fixed order, and then we know to assign correct indexes 316 | for label, labelFunc := range customLabelFuncs { 317 | customValuers = append(customValuers, customLabelValuer{ 318 | label: label, 319 | valueFunc: labelFunc, 320 | }) 321 | } 322 | sort.Slice(customValuers, func(i, j int) bool { 323 | return customValuers[i].label < customValuers[j].label 324 | }) 325 | 326 | for cvIdx, cv := range customValuers { 327 | idx := containsAt(labelNames, cv.label) 328 | if idx == -1 { 329 | idx = len(labelNames) 330 | labelNames = append(labelNames, cv.label) 331 | } 332 | customValuers[cvIdx].index = idx 333 | } 334 | return labelNames, customValuers 335 | } 336 | 337 | func containsAt[K comparable](haystack []K, needle K) int { 338 | for i, v := range haystack { 339 | if v == needle { 340 | return i 341 | } 342 | } 343 | return -1 344 | } 345 | 346 | func computeApproximateRequestSize(r *http.Request) int { 347 | s := 0 348 | if r.URL != nil { 349 | s = len(r.URL.Path) 350 | } 351 | 352 | s += len(r.Method) 353 | s += len(r.Proto) 354 | for name, values := range r.Header { 355 | s += len(name) 356 | for _, value := range values { 357 | s += len(value) 358 | } 359 | } 360 | s += len(r.Host) 361 | 362 | // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 363 | 364 | if r.ContentLength != -1 { 365 | s += int(r.ContentLength) 366 | } 367 | return s 368 | } 369 | 370 | // RunPushGatewayGatherer starts pushing collected metrics and waits for it context to complete or ErrorHandler to return error. 371 | // 372 | // Example: 373 | // ``` 374 | // 375 | // go func() { 376 | // config := echoprometheus.PushGatewayConfig{ 377 | // PushGatewayURL: "https://host:9080", 378 | // PushInterval: 10 * time.Millisecond, 379 | // } 380 | // if err := echoprometheus.RunPushGatewayGatherer(context.Background(), config); !errors.Is(err, context.Canceled) { 381 | // log.Fatal(err) 382 | // } 383 | // }() 384 | // 385 | // ``` 386 | func RunPushGatewayGatherer(ctx context.Context, config PushGatewayConfig) error { 387 | if config.PushGatewayURL == "" { 388 | return errors.New("push gateway URL is missing") 389 | } 390 | if config.PushInterval <= 0 { 391 | config.PushInterval = 1 * time.Minute 392 | } 393 | if config.Gatherer == nil { 394 | config.Gatherer = prometheus.DefaultGatherer 395 | } 396 | if config.ErrorHandler == nil { 397 | config.ErrorHandler = func(err error) error { 398 | log.Error(err) 399 | return nil 400 | } 401 | } 402 | 403 | client := &http.Client{ 404 | Transport: config.ClientTransport, 405 | } 406 | out := &bytes.Buffer{} 407 | 408 | ticker := time.NewTicker(config.PushInterval) 409 | defer ticker.Stop() 410 | for { 411 | select { 412 | case <-ticker.C: 413 | out.Reset() 414 | err := WriteGatheredMetrics(out, config.Gatherer) 415 | if err != nil { 416 | if hErr := config.ErrorHandler(fmt.Errorf("failed to create metrics: %w", err)); hErr != nil { 417 | return hErr 418 | } 419 | continue 420 | } 421 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, config.PushGatewayURL, out) 422 | if err != nil { 423 | if hErr := config.ErrorHandler(fmt.Errorf("failed to create push gateway request: %w", err)); hErr != nil { 424 | return hErr 425 | } 426 | continue 427 | } 428 | res, err := client.Do(req) 429 | if err != nil { 430 | if hErr := config.ErrorHandler(fmt.Errorf("error sending to push gateway: %w", err)); hErr != nil { 431 | return hErr 432 | } 433 | } 434 | if res.StatusCode != http.StatusOK { 435 | if hErr := config.ErrorHandler(echo.NewHTTPError(res.StatusCode, "post metrics request did not succeed")); hErr != nil { 436 | return hErr 437 | } 438 | } 439 | case <-ctx.Done(): 440 | return ctx.Err() 441 | } 442 | } 443 | } 444 | 445 | // WriteGatheredMetrics gathers collected metrics and writes them to given writer 446 | func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer) error { 447 | metricFamilies, err := gatherer.Gather() 448 | if err != nil { 449 | return err 450 | } 451 | for _, mf := range metricFamilies { 452 | if _, err := expfmt.MetricFamilyToText(writer, mf); err != nil { 453 | return err 454 | } 455 | } 456 | return nil 457 | } 458 | 459 | // defaultStatusResolver resolves http status code by referencing echo.HTTPError. 460 | func defaultStatusResolver(c echo.Context, err error) int { 461 | status := c.Response().Status 462 | if err != nil { 463 | var httpError *echo.HTTPError 464 | if errors.As(err, &httpError) { 465 | status = httpError.Code 466 | } 467 | if status == 0 || status == http.StatusOK { 468 | status = http.StatusInternalServerError 469 | } 470 | } 471 | return status 472 | } 473 | -------------------------------------------------------------------------------- /echoprometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package echoprometheus 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "errors" 10 | "fmt" 11 | "github.com/labstack/echo/v4" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/stretchr/testify/assert" 14 | "net/http" 15 | "net/http/httptest" 16 | "strings" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | func TestCustomRegistryMetrics(t *testing.T) { 22 | e := echo.New() 23 | 24 | customRegistry := prometheus.NewRegistry() 25 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Registerer: customRegistry})) 26 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 27 | 28 | assert.Equal(t, http.StatusNotFound, request(e, "/ping?test=1")) 29 | 30 | s, code := requestBody(e, "/metrics") 31 | assert.Equal(t, http.StatusOK, code) 32 | assert.Contains(t, s, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) 33 | } 34 | 35 | func TestDefaultRegistryMetrics(t *testing.T) { 36 | e := echo.New() 37 | 38 | e.Use(NewMiddleware("myapp")) 39 | e.GET("/metrics", NewHandler()) 40 | 41 | assert.Equal(t, http.StatusNotFound, request(e, "/ping?test=1")) 42 | 43 | s, code := requestBody(e, "/metrics") 44 | assert.Equal(t, http.StatusOK, code) 45 | assert.Contains(t, s, `myapp_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) 46 | 47 | unregisterDefaults("myapp") 48 | } 49 | 50 | func TestPrometheus_Buckets(t *testing.T) { 51 | e := echo.New() 52 | 53 | customRegistry := prometheus.NewRegistry() 54 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Registerer: customRegistry})) 55 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 56 | 57 | assert.Equal(t, http.StatusNotFound, request(e, "/ping")) 58 | 59 | body, code := requestBody(e, "/metrics") 60 | assert.Equal(t, http.StatusOK, code) 61 | assert.Contains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "duration should have time bucket (like, 0.005s)") 62 | assert.NotContains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="512000"}`, "duration should NOT have a size bucket (like, 512K)") 63 | assert.Contains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "request size should have a 1024k (size) bucket") 64 | assert.NotContains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "request size should NOT have time bucket (like, 0.005s)") 65 | assert.Contains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "response size should have a 1024k (size) bucket") 66 | assert.NotContains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "response size should NOT have time bucket (like, 0.005s)") 67 | } 68 | 69 | func TestMiddlewareConfig_Skipper(t *testing.T) { 70 | e := echo.New() 71 | 72 | customRegistry := prometheus.NewRegistry() 73 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 74 | Skipper: func(c echo.Context) bool { 75 | hasSuffix := strings.HasSuffix(c.Path(), "ignore") 76 | return hasSuffix 77 | }, 78 | Registerer: customRegistry, 79 | })) 80 | 81 | e.GET("/test", func(c echo.Context) error { 82 | return c.String(http.StatusOK, "OK") 83 | }) 84 | e.GET("/test_ignore", func(c echo.Context) error { 85 | return c.String(http.StatusOK, "OK") 86 | }) 87 | 88 | assert.Equal(t, http.StatusNotFound, request(e, "/ping")) 89 | assert.Equal(t, http.StatusOK, request(e, "/test")) 90 | assert.Equal(t, http.StatusOK, request(e, "/test_ignore")) 91 | 92 | out := &bytes.Buffer{} 93 | assert.NoError(t, WriteGatheredMetrics(out, customRegistry)) 94 | 95 | body := out.String() 96 | assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="GET",url="/test"} 1`) 97 | assert.Contains(t, body, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) 98 | assert.Contains(t, body, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) 99 | assert.NotContains(t, body, `test_ignore`) // because we skipped 100 | } 101 | 102 | func TestMetricsForErrors(t *testing.T) { 103 | e := echo.New() 104 | customRegistry := prometheus.NewRegistry() 105 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 106 | Skipper: func(c echo.Context) bool { 107 | return strings.HasSuffix(c.Path(), "ignore") 108 | }, 109 | Subsystem: "myapp", 110 | Registerer: customRegistry, 111 | })) 112 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 113 | 114 | e.GET("/handler_for_ok", func(c echo.Context) error { 115 | return c.JSON(http.StatusOK, "OK") 116 | }) 117 | e.GET("/handler_for_nok", func(c echo.Context) error { 118 | return c.JSON(http.StatusConflict, "NOK") 119 | }) 120 | e.GET("/handler_for_error", func(c echo.Context) error { 121 | return echo.NewHTTPError(http.StatusBadGateway, "BAD") 122 | }) 123 | 124 | assert.Equal(t, http.StatusOK, request(e, "/handler_for_ok")) 125 | assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) 126 | assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) 127 | assert.Equal(t, http.StatusBadGateway, request(e, "/handler_for_error")) 128 | 129 | body, code := requestBody(e, "/metrics") 130 | assert.Equal(t, http.StatusOK, code) 131 | assert.Contains(t, body, fmt.Sprintf("%s_requests_total", "myapp")) 132 | assert.Contains(t, body, `myapp_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) 133 | assert.Contains(t, body, `myapp_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 2`) 134 | assert.Contains(t, body, `myapp_requests_total{code="502",host="example.com",method="GET",url="/handler_for_error"} 1`) 135 | } 136 | 137 | func TestMiddlewareConfig_LabelFuncs(t *testing.T) { 138 | e := echo.New() 139 | customRegistry := prometheus.NewRegistry() 140 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 141 | LabelFuncs: map[string]LabelValueFunc{ 142 | "scheme": func(c echo.Context, err error) string { // additional custom label 143 | return c.Scheme() 144 | }, 145 | "method": func(c echo.Context, err error) string { // overrides default 'method' label value 146 | return "overridden_" + c.Request().Method 147 | }, 148 | }, 149 | Registerer: customRegistry, 150 | })) 151 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 152 | 153 | e.GET("/ok", func(c echo.Context) error { 154 | return c.JSON(http.StatusOK, "OK") 155 | }) 156 | 157 | assert.Equal(t, http.StatusOK, request(e, "/ok")) 158 | 159 | body, code := requestBody(e, "/metrics") 160 | assert.Equal(t, http.StatusOK, code) 161 | assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="overridden_GET",scheme="http",url="/ok"} 1`) 162 | } 163 | 164 | func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) { 165 | e := echo.New() 166 | customRegistry := prometheus.NewRegistry() 167 | customResolver := func(c echo.Context, err error) int { 168 | if err == nil { 169 | return c.Response().Status 170 | } 171 | msg := err.Error() 172 | if strings.Contains(msg, "NOT FOUND") { 173 | return http.StatusNotFound 174 | } 175 | if strings.Contains(msg, "NOT Authorized") { 176 | return http.StatusUnauthorized 177 | } 178 | return http.StatusInternalServerError 179 | } 180 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 181 | Skipper: func(c echo.Context) bool { 182 | return strings.HasSuffix(c.Path(), "ignore") 183 | }, 184 | Subsystem: "myapp", 185 | Registerer: customRegistry, 186 | StatusCodeResolver: customResolver, 187 | })) 188 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 189 | 190 | e.GET("/handler_for_ok", func(c echo.Context) error { 191 | return c.JSON(http.StatusOK, "OK") 192 | }) 193 | e.GET("/handler_for_nok", func(c echo.Context) error { 194 | return c.JSON(http.StatusConflict, "NOK") 195 | }) 196 | e.GET("/handler_for_not_found", func(c echo.Context) error { 197 | return errors.New("NOT FOUND") 198 | }) 199 | e.GET("/handler_for_not_authorized", func(c echo.Context) error { 200 | return errors.New("NOT Authorized") 201 | }) 202 | e.GET("/handler_for_unknown_error", func(c echo.Context) error { 203 | return errors.New("i do not know") 204 | }) 205 | 206 | assert.Equal(t, http.StatusOK, request(e, "/handler_for_ok")) 207 | assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) 208 | assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_found")) 209 | assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_authorized")) 210 | assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_unknown_error")) 211 | 212 | body, code := requestBody(e, "/metrics") 213 | assert.Equal(t, http.StatusOK, code) 214 | assert.Contains(t, body, fmt.Sprintf("%s_requests_total", "myapp")) 215 | assert.Contains(t, body, `myapp_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) 216 | assert.Contains(t, body, `myapp_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 1`) 217 | assert.Contains(t, body, `myapp_requests_total{code="404",host="example.com",method="GET",url="/handler_for_not_found"} 1`) 218 | assert.Contains(t, body, `myapp_requests_total{code="401",host="example.com",method="GET",url="/handler_for_not_authorized"} 1`) 219 | assert.Contains(t, body, `myapp_requests_total{code="500",host="example.com",method="GET",url="/handler_for_unknown_error"} 1`) 220 | } 221 | 222 | func TestMiddlewareConfig_HistogramOptsFunc(t *testing.T) { 223 | e := echo.New() 224 | customRegistry := prometheus.NewRegistry() 225 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 226 | HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { 227 | if opts.Name == "request_duration_seconds" { 228 | opts.ConstLabels = prometheus.Labels{"my_const": "123"} 229 | } 230 | return opts 231 | }, 232 | Registerer: customRegistry, 233 | })) 234 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 235 | 236 | e.GET("/ok", func(c echo.Context) error { 237 | return c.JSON(http.StatusOK, "OK") 238 | }) 239 | 240 | assert.Equal(t, http.StatusOK, request(e, "/ok")) 241 | 242 | body, code := requestBody(e, "/metrics") 243 | assert.Equal(t, http.StatusOK, code) 244 | 245 | // has const label 246 | assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="GET",my_const="123",url="/ok"} 1`) 247 | // does not have const label 248 | assert.Contains(t, body, `echo_request_size_bytes_count{code="200",host="example.com",method="GET",url="/ok"} 1`) 249 | } 250 | 251 | func TestMiddlewareConfig_CounterOptsFunc(t *testing.T) { 252 | e := echo.New() 253 | customRegistry := prometheus.NewRegistry() 254 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 255 | CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { 256 | if opts.Name == "requests_total" { 257 | opts.ConstLabels = prometheus.Labels{"my_const": "123"} 258 | } 259 | return opts 260 | }, 261 | Registerer: customRegistry, 262 | })) 263 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 264 | 265 | e.GET("/ok", func(c echo.Context) error { 266 | return c.JSON(http.StatusOK, "OK") 267 | }) 268 | 269 | assert.Equal(t, http.StatusOK, request(e, "/ok")) 270 | 271 | body, code := requestBody(e, "/metrics") 272 | assert.Equal(t, http.StatusOK, code) 273 | 274 | // has const label 275 | assert.Contains(t, body, `echo_requests_total{code="200",host="example.com",method="GET",my_const="123",url="/ok"} 1`) 276 | // does not have const label 277 | assert.Contains(t, body, `echo_request_size_bytes_count{code="200",host="example.com",method="GET",url="/ok"} 1`) 278 | } 279 | 280 | func TestMiddlewareConfig_AfterNextFuncs(t *testing.T) { 281 | e := echo.New() 282 | 283 | customRegistry := prometheus.NewRegistry() 284 | customCounter := prometheus.NewCounter( 285 | prometheus.CounterOpts{ 286 | Name: "custom_requests_total", 287 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 288 | }, 289 | ) 290 | if err := customRegistry.Register(customCounter); err != nil { 291 | t.Fatal(err) 292 | } 293 | 294 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ 295 | AfterNext: func(c echo.Context, err error) { 296 | customCounter.Inc() // use our custom metric in middleware 297 | }, 298 | Registerer: customRegistry, 299 | })) 300 | e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) 301 | 302 | e.GET("/ok", func(c echo.Context) error { 303 | return c.JSON(http.StatusOK, "OK") 304 | }) 305 | 306 | assert.Equal(t, http.StatusOK, request(e, "/ok")) 307 | 308 | body, code := requestBody(e, "/metrics") 309 | assert.Equal(t, http.StatusOK, code) 310 | assert.Contains(t, body, `custom_requests_total 1`) 311 | } 312 | 313 | func TestRunPushGatewayGatherer(t *testing.T) { 314 | receivedMetrics := false 315 | svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 316 | receivedMetrics = true 317 | w.WriteHeader(http.StatusBadRequest) 318 | w.Write([]byte("OK")) 319 | })) 320 | defer svr.Close() 321 | 322 | ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) 323 | defer cancel() 324 | 325 | config := PushGatewayConfig{ 326 | PushGatewayURL: svr.URL, 327 | PushInterval: 10 * time.Millisecond, 328 | ErrorHandler: func(err error) error { 329 | return err // to force return after first request 330 | }, 331 | } 332 | err := RunPushGatewayGatherer(ctx, config) 333 | 334 | assert.EqualError(t, err, "code=400, message=post metrics request did not succeed") 335 | assert.True(t, receivedMetrics) 336 | unregisterDefaults("myapp") 337 | } 338 | 339 | // TestSetPathFor404NoMatchingRoute tests that the url is not included in the metric when 340 | // the 404 response is due to no matching route 341 | func TestSetPathFor404NoMatchingRoute(t *testing.T) { 342 | e := echo.New() 343 | 344 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{DoNotUseRequestPathFor404: true, Subsystem: defaultSubsystem})) 345 | e.GET("/metrics", NewHandler()) 346 | 347 | assert.Equal(t, http.StatusNotFound, request(e, "/nonExistentPath")) 348 | 349 | s, code := requestBody(e, "/metrics") 350 | assert.Equal(t, http.StatusOK, code) 351 | assert.Contains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url=""} 1`, defaultSubsystem)) 352 | assert.NotContains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/nonExistentPath"} 1`, defaultSubsystem)) 353 | 354 | unregisterDefaults(defaultSubsystem) 355 | } 356 | 357 | // TestSetPathFor404Logic tests that the url is included in the metric when the 404 response is due to logic 358 | func TestSetPathFor404Logic(t *testing.T) { 359 | unregisterDefaults("myapp") 360 | e := echo.New() 361 | 362 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{DoNotUseRequestPathFor404: true, Subsystem: defaultSubsystem})) 363 | e.GET("/metrics", NewHandler()) 364 | 365 | e.GET("/sample", echo.NotFoundHandler) 366 | 367 | assert.Equal(t, http.StatusNotFound, request(e, "/sample")) 368 | 369 | s, code := requestBody(e, "/metrics") 370 | assert.Equal(t, http.StatusOK, code) 371 | assert.NotContains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url=""} 1`, defaultSubsystem)) 372 | assert.Contains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/sample"} 1`, defaultSubsystem)) 373 | 374 | unregisterDefaults(defaultSubsystem) 375 | } 376 | 377 | func TestInvalidUTF8PathIsFixed(t *testing.T) { 378 | e := echo.New() 379 | 380 | e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Subsystem: defaultSubsystem})) 381 | e.GET("/metrics", NewHandler()) 382 | 383 | assert.Equal(t, http.StatusNotFound, request(e, "/../../WEB-INF/web.xml\xc0\x80.jsp")) 384 | 385 | s, code := requestBody(e, "/metrics") 386 | assert.Equal(t, http.StatusOK, code) 387 | assert.Contains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/../../WEB-INF/web.xml�.jsp"} 1`, defaultSubsystem)) 388 | 389 | unregisterDefaults(defaultSubsystem) 390 | } 391 | 392 | func requestBody(e *echo.Echo, path string) (string, int) { 393 | req := httptest.NewRequest(http.MethodGet, path, nil) 394 | rec := httptest.NewRecorder() 395 | e.ServeHTTP(rec, req) 396 | 397 | return rec.Body.String(), rec.Code 398 | } 399 | 400 | func request(e *echo.Echo, path string) int { 401 | _, code := requestBody(e, path) 402 | return code 403 | } 404 | 405 | func unregisterDefaults(subsystem string) { 406 | // this is extremely hacky way to unregister our middleware metrics that it registers to prometheus default registry 407 | // Metrics/collector can be unregistered only by their instance but we do not have their instance, so we need to 408 | // create similar collector to register it and get error back with that existing collector we actually want to 409 | // unregister 410 | p := prometheus.DefaultRegisterer 411 | 412 | unRegisterCollector := func(opts prometheus.Opts) { 413 | dummyDuplicate := prometheus.NewCounterVec(prometheus.CounterOpts(opts), []string{"code", "method", "host", "url"}) 414 | err := p.Register(dummyDuplicate) 415 | if err == nil { 416 | return 417 | } 418 | var arErr prometheus.AlreadyRegisteredError 419 | if errors.As(err, &arErr) { 420 | p.Unregister(arErr.ExistingCollector) 421 | } 422 | } 423 | 424 | unRegisterCollector(prometheus.Opts{ 425 | Subsystem: subsystem, 426 | Name: "requests_total", 427 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 428 | }) 429 | unRegisterCollector(prometheus.Opts{ 430 | Subsystem: subsystem, 431 | Name: "request_duration_seconds", 432 | Help: "The HTTP request latencies in seconds.", 433 | }) 434 | unRegisterCollector(prometheus.Opts{ 435 | Subsystem: subsystem, 436 | Name: "response_size_bytes", 437 | Help: "The HTTP response sizes in bytes.", 438 | }) 439 | unRegisterCollector(prometheus.Opts{ 440 | Subsystem: subsystem, 441 | Name: "request_size_bytes", 442 | Help: "The HTTP request sizes in bytes.", 443 | }) 444 | } 445 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/labstack/echo-contrib 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.105.0 7 | github.com/gorilla/context v1.1.2 8 | github.com/gorilla/sessions v1.4.0 9 | github.com/labstack/echo/v4 v4.13.3 10 | github.com/labstack/gommon v0.4.2 11 | github.com/opentracing/opentracing-go v1.2.0 12 | github.com/openzipkin/zipkin-go v0.4.3 13 | github.com/prometheus/client_golang v1.22.0 14 | github.com/prometheus/common v0.63.0 15 | github.com/stretchr/testify v1.10.0 16 | github.com/uber/jaeger-client-go v2.30.0+incompatible 17 | ) 18 | 19 | require ( 20 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect 23 | github.com/casbin/govaluate v1.3.0 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/gorilla/securecookie v1.1.2 // indirect 27 | github.com/mattn/go-colorable v0.1.14 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 30 | github.com/pkg/errors v0.9.1 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | github.com/prometheus/client_model v0.6.2 // indirect 33 | github.com/prometheus/procfs v0.16.1 // indirect 34 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 35 | github.com/valyala/bytebufferpool v1.0.0 // indirect 36 | github.com/valyala/fasttemplate v1.2.2 // indirect 37 | go.uber.org/atomic v1.11.0 // indirect 38 | golang.org/x/crypto v0.38.0 // indirect 39 | golang.org/x/net v0.40.0 // indirect 40 | golang.org/x/sys v0.33.0 // indirect 41 | golang.org/x/text v0.25.0 // indirect 42 | golang.org/x/time v0.11.0 // indirect 43 | google.golang.org/grpc v1.72.0 // indirect 44 | google.golang.org/protobuf v1.36.6 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= 4 | github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= 5 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 6 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 7 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 8 | github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 9 | github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= 10 | github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 11 | github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I= 12 | github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= 13 | github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= 14 | github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= 15 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 16 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 22 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 23 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 24 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 25 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 26 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 28 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 29 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 30 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= 32 | github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= 33 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 34 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 35 | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 36 | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 37 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 38 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 39 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 40 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 41 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 42 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 43 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 44 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 46 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 47 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 48 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 49 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 50 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 51 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 52 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 53 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 54 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 55 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 58 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 59 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 60 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 61 | github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= 62 | github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 68 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 69 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 70 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 71 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 72 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 73 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 74 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 75 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 76 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 79 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 83 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 84 | github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= 85 | github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 86 | github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= 87 | github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 88 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 89 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 90 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 91 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 92 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 93 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 94 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 95 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 96 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 97 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 98 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 99 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 100 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 101 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 102 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 103 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 104 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 105 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 106 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 107 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 108 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 109 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 110 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 111 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 112 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 113 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 115 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 119 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 120 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 121 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 122 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 123 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 124 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 125 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 127 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 128 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 129 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 133 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 134 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 135 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 136 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 137 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 138 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 139 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 142 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 143 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 144 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 148 | -------------------------------------------------------------------------------- /jaegertracing/jaegertracing.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | /* 5 | Package jaegertracing provides middleware to Opentracing using Jaeger. 6 | 7 | Example: 8 | ``` 9 | package main 10 | import ( 11 | 12 | "github.com/labstack/echo-contrib/jaegertracing" 13 | "github.com/labstack/echo/v4" 14 | 15 | ) 16 | 17 | func main() { 18 | e := echo.New() 19 | // Enable tracing middleware 20 | c := jaegertracing.New(e, nil) 21 | defer c.Close() 22 | 23 | e.Logger.Fatal(e.Start(":1323")) 24 | } 25 | 26 | ``` 27 | */ 28 | package jaegertracing 29 | 30 | import ( 31 | "bytes" 32 | "crypto/rand" 33 | "errors" 34 | "fmt" 35 | "io" 36 | "net/http" 37 | "reflect" 38 | "runtime" 39 | "time" 40 | 41 | "github.com/labstack/echo/v4" 42 | "github.com/labstack/echo/v4/middleware" 43 | "github.com/opentracing/opentracing-go" 44 | "github.com/opentracing/opentracing-go/ext" 45 | "github.com/uber/jaeger-client-go/config" 46 | ) 47 | 48 | const defaultComponentName = "echo/v4" 49 | 50 | type ( 51 | // TraceConfig defines the config for Trace middleware. 52 | TraceConfig struct { 53 | // Skipper defines a function to skip middleware. 54 | Skipper middleware.Skipper 55 | 56 | // OpenTracing Tracer instance which should be got before 57 | Tracer opentracing.Tracer 58 | 59 | // ComponentName used for describing the tracing component name 60 | ComponentName string 61 | 62 | // add req body & resp body to tracing tags 63 | IsBodyDump bool 64 | 65 | // prevent logging long http request bodies 66 | LimitHTTPBody bool 67 | 68 | // http body limit size (in bytes) 69 | // NOTE: don't specify values larger than 60000 as jaeger can't handle values in span.LogKV larger than 60000 bytes 70 | LimitSize int 71 | 72 | // OperationNameFunc composes operation name based on context. Can be used to override default naming 73 | OperationNameFunc func(c echo.Context) string 74 | } 75 | ) 76 | 77 | var ( 78 | // DefaultTraceConfig is the default Trace middleware config. 79 | DefaultTraceConfig = TraceConfig{ 80 | Skipper: middleware.DefaultSkipper, 81 | ComponentName: defaultComponentName, 82 | IsBodyDump: false, 83 | 84 | LimitHTTPBody: true, 85 | LimitSize: 60_000, 86 | OperationNameFunc: defaultOperationName, 87 | } 88 | ) 89 | 90 | // New creates an Opentracing tracer and attaches it to Echo middleware. 91 | // Returns Closer do be added to caller function as `defer closer.Close()` 92 | func New(e *echo.Echo, skipper middleware.Skipper) io.Closer { 93 | // Add Opentracing instrumentation 94 | defcfg := config.Configuration{ 95 | ServiceName: "echo-tracer", 96 | Sampler: &config.SamplerConfig{ 97 | Type: "const", 98 | Param: 1, 99 | }, 100 | Reporter: &config.ReporterConfig{ 101 | LogSpans: true, 102 | BufferFlushInterval: 1 * time.Second, 103 | }, 104 | } 105 | cfg, err := defcfg.FromEnv() 106 | if err != nil { 107 | panic("Could not parse Jaeger env vars: " + err.Error()) 108 | } 109 | tracer, closer, err := cfg.NewTracer() 110 | if err != nil { 111 | panic("Could not initialize jaeger tracer: " + err.Error()) 112 | } 113 | 114 | opentracing.SetGlobalTracer(tracer) 115 | e.Use(TraceWithConfig(TraceConfig{ 116 | Tracer: tracer, 117 | Skipper: skipper, 118 | })) 119 | return closer 120 | } 121 | 122 | // Trace returns a Trace middleware. 123 | // Trace middleware traces http requests and reporting errors. 124 | func Trace(tracer opentracing.Tracer) echo.MiddlewareFunc { 125 | c := DefaultTraceConfig 126 | c.Tracer = tracer 127 | c.ComponentName = defaultComponentName 128 | return TraceWithConfig(c) 129 | } 130 | 131 | // TraceWithConfig returns a Trace middleware with config. 132 | // See: `Trace()`. 133 | func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc { 134 | if config.Tracer == nil { 135 | panic("echo: trace middleware requires opentracing tracer") 136 | } 137 | if config.Skipper == nil { 138 | config.Skipper = middleware.DefaultSkipper 139 | } 140 | if config.ComponentName == "" { 141 | config.ComponentName = defaultComponentName 142 | } 143 | if config.OperationNameFunc == nil { 144 | config.OperationNameFunc = defaultOperationName 145 | } 146 | 147 | return func(next echo.HandlerFunc) echo.HandlerFunc { 148 | return func(c echo.Context) error { 149 | if config.Skipper(c) { 150 | return next(c) 151 | } 152 | 153 | req := c.Request() 154 | opname := config.OperationNameFunc(c) 155 | realIP := c.RealIP() 156 | requestID := getRequestID(c) // request-id generated by reverse-proxy 157 | 158 | var sp opentracing.Span 159 | var err error 160 | 161 | ctx, err := config.Tracer.Extract( 162 | opentracing.HTTPHeaders, 163 | opentracing.HTTPHeadersCarrier(req.Header), 164 | ) 165 | 166 | if err != nil { 167 | sp = config.Tracer.StartSpan(opname) 168 | } else { 169 | sp = config.Tracer.StartSpan(opname, ext.RPCServerOption(ctx)) 170 | } 171 | defer sp.Finish() 172 | 173 | ext.HTTPMethod.Set(sp, req.Method) 174 | ext.HTTPUrl.Set(sp, req.URL.String()) 175 | ext.Component.Set(sp, config.ComponentName) 176 | sp.SetTag("client_ip", realIP) 177 | sp.SetTag("request_id", requestID) 178 | 179 | // Dump request & response body 180 | var respDumper *responseDumper 181 | if config.IsBodyDump { 182 | // request 183 | reqBody := []byte{} 184 | if c.Request().Body != nil { 185 | reqBody, _ = io.ReadAll(c.Request().Body) 186 | 187 | if config.LimitHTTPBody { 188 | sp.LogKV("http.req.body", limitString(string(reqBody), config.LimitSize)) 189 | } else { 190 | sp.LogKV("http.req.body", string(reqBody)) 191 | } 192 | } 193 | 194 | req.Body = io.NopCloser(bytes.NewBuffer(reqBody)) // reset original request body 195 | 196 | // response 197 | respDumper = newResponseDumper(c.Response()) 198 | c.Response().Writer = respDumper 199 | } 200 | 201 | // setup request context - add opentracing span 202 | reqSpan := req.WithContext(opentracing.ContextWithSpan(req.Context(), sp)) 203 | c.SetRequest(reqSpan) 204 | defer func() { 205 | // as we have created new http.Request object we need to make sure that temporary files created to hold MultipartForm 206 | // files are cleaned up. This is done by http.Server at the end of request lifecycle but Server does not 207 | // have reference to our new Request instance therefore it is our responsibility to fix the mess we caused. 208 | // 209 | // This means that when we are on returning path from handler middlewares up in chain from this middleware 210 | // can not access these temporary files anymore because we deleted them here. 211 | if reqSpan.MultipartForm != nil { 212 | reqSpan.MultipartForm.RemoveAll() 213 | } 214 | }() 215 | 216 | // inject Jaeger context into request header 217 | config.Tracer.Inject(sp.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request().Header)) 218 | 219 | // call next middleware / controller 220 | err = next(c) 221 | if err != nil { 222 | c.Error(err) // call custom registered error handler 223 | } 224 | 225 | status := c.Response().Status 226 | ext.HTTPStatusCode.Set(sp, uint16(status)) 227 | 228 | if err != nil { 229 | logError(sp, err) 230 | } 231 | 232 | // Dump response body 233 | if config.IsBodyDump { 234 | if config.LimitHTTPBody { 235 | sp.LogKV("http.resp.body", limitString(respDumper.GetResponse(), config.LimitSize)) 236 | } else { 237 | sp.LogKV("http.resp.body", respDumper.GetResponse()) 238 | } 239 | } 240 | 241 | return nil // error was already processed with ctx.Error(err) 242 | } 243 | } 244 | } 245 | 246 | func limitString(str string, size int) string { 247 | if len(str) > size { 248 | return str[:size/2] + "\n---- skipped ----\n" + str[len(str)-size/2:] 249 | } 250 | 251 | return str 252 | } 253 | 254 | func logError(span opentracing.Span, err error) { 255 | var httpError *echo.HTTPError 256 | if errors.As(err, &httpError) { 257 | span.LogKV("error.message", httpError.Message) 258 | } else { 259 | span.LogKV("error.message", err.Error()) 260 | } 261 | span.SetTag("error", true) 262 | } 263 | 264 | func getRequestID(ctx echo.Context) string { 265 | requestID := ctx.Request().Header.Get(echo.HeaderXRequestID) // request-id generated by reverse-proxy 266 | if requestID == "" { 267 | requestID = generateToken() // missed request-id from proxy, we generate it manually 268 | } 269 | return requestID 270 | } 271 | 272 | func generateToken() string { 273 | b := make([]byte, 16) 274 | rand.Read(b) 275 | return fmt.Sprintf("%x", b) 276 | } 277 | 278 | func defaultOperationName(c echo.Context) string { 279 | req := c.Request() 280 | return "HTTP " + req.Method + " URL: " + c.Path() 281 | } 282 | 283 | // TraceFunction wraps funtion with opentracing span adding tags for the function name and caller details 284 | func TraceFunction(ctx echo.Context, fn interface{}, params ...interface{}) (result []reflect.Value) { 285 | // Get function name 286 | name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() 287 | // Create child span 288 | parentSpan := opentracing.SpanFromContext(ctx.Request().Context()) 289 | sp := opentracing.StartSpan( 290 | "Function - "+name, 291 | opentracing.ChildOf(parentSpan.Context())) 292 | defer sp.Finish() 293 | 294 | sp.SetTag("function", name) 295 | 296 | // Get caller function name, file and line 297 | pc := make([]uintptr, 15) 298 | n := runtime.Callers(2, pc) 299 | frames := runtime.CallersFrames(pc[:n]) 300 | frame, _ := frames.Next() 301 | callerDetails := fmt.Sprintf("%s - %s#%d", frame.Function, frame.File, frame.Line) 302 | sp.SetTag("caller", callerDetails) 303 | 304 | // Check params and call function 305 | f := reflect.ValueOf(fn) 306 | if f.Type().NumIn() != len(params) { 307 | e := fmt.Sprintf("Incorrect number of parameters calling wrapped function %s", name) 308 | panic(e) 309 | } 310 | inputs := make([]reflect.Value, len(params)) 311 | for k, in := range params { 312 | inputs[k] = reflect.ValueOf(in) 313 | } 314 | return f.Call(inputs) 315 | } 316 | 317 | // CreateChildSpan creates a new opentracing span adding tags for the span name and caller details. 318 | // User must call defer `sp.Finish()` 319 | func CreateChildSpan(ctx echo.Context, name string) opentracing.Span { 320 | parentSpan := opentracing.SpanFromContext(ctx.Request().Context()) 321 | sp := opentracing.StartSpan( 322 | name, 323 | opentracing.ChildOf(parentSpan.Context())) 324 | sp.SetTag("name", name) 325 | 326 | // Get caller function name, file and line 327 | pc := make([]uintptr, 15) 328 | n := runtime.Callers(2, pc) 329 | frames := runtime.CallersFrames(pc[:n]) 330 | frame, _ := frames.Next() 331 | callerDetails := fmt.Sprintf("%s - %s#%d", frame.Function, frame.File, frame.Line) 332 | sp.SetTag("caller", callerDetails) 333 | 334 | return sp 335 | } 336 | 337 | // NewTracedRequest generates a new traced HTTP request with opentracing headers injected into it 338 | func NewTracedRequest(method string, url string, body io.Reader, span opentracing.Span) (*http.Request, error) { 339 | req, err := http.NewRequest(method, url, body) 340 | if err != nil { 341 | panic(err.Error()) 342 | } 343 | 344 | ext.SpanKindRPCClient.Set(span) 345 | ext.HTTPUrl.Set(span, url) 346 | ext.HTTPMethod.Set(span, method) 347 | span.Tracer().Inject(span.Context(), 348 | opentracing.HTTPHeaders, 349 | opentracing.HTTPHeadersCarrier(req.Header)) 350 | 351 | return req, err 352 | } 353 | -------------------------------------------------------------------------------- /jaegertracing/jaegertracing_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package jaegertracing 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/labstack/echo/v4" 16 | "github.com/opentracing/opentracing-go" 17 | "github.com/opentracing/opentracing-go/log" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | // Mock opentracing.Span 22 | type mockSpan struct { 23 | tracer opentracing.Tracer 24 | tags map[string]interface{} 25 | logs map[string]interface{} 26 | opName string 27 | finished bool 28 | } 29 | 30 | func createSpan(tracer opentracing.Tracer) *mockSpan { 31 | return &mockSpan{ 32 | tracer: tracer, 33 | tags: make(map[string]interface{}), 34 | logs: make(map[string]interface{}), 35 | } 36 | } 37 | 38 | func (sp *mockSpan) isFinished() bool { 39 | return sp.finished 40 | } 41 | 42 | func (sp *mockSpan) getOpName() string { 43 | return sp.opName 44 | } 45 | 46 | func (sp *mockSpan) getTag(key string) interface{} { 47 | return sp.tags[key] 48 | } 49 | 50 | func (sp *mockSpan) getLog(key string) interface{} { 51 | return sp.logs[key] 52 | } 53 | 54 | func (sp *mockSpan) Finish() { 55 | sp.finished = true 56 | } 57 | func (sp *mockSpan) FinishWithOptions(opts opentracing.FinishOptions) { 58 | } 59 | func (sp *mockSpan) Context() opentracing.SpanContext { 60 | return nil 61 | } 62 | func (sp *mockSpan) SetOperationName(operationName string) opentracing.Span { 63 | sp.opName = operationName 64 | return sp 65 | } 66 | func (sp *mockSpan) SetTag(key string, value interface{}) opentracing.Span { 67 | sp.tags[key] = value 68 | return sp 69 | } 70 | func (sp *mockSpan) LogFields(fields ...log.Field) { 71 | } 72 | func (sp *mockSpan) LogKV(alternatingKeyValues ...interface{}) { 73 | for i := 0; i < len(alternatingKeyValues); i += 2 { 74 | ikey := alternatingKeyValues[i] 75 | value := alternatingKeyValues[i+1] 76 | if key, ok := ikey.(string); ok { 77 | sp.logs[key] = value 78 | } 79 | } 80 | } 81 | func (sp *mockSpan) SetBaggageItem(restrictedKey, value string) opentracing.Span { 82 | return sp 83 | } 84 | func (sp *mockSpan) BaggageItem(restrictedKey string) string { 85 | return "" 86 | } 87 | func (sp *mockSpan) Tracer() opentracing.Tracer { 88 | return sp.tracer 89 | } 90 | func (sp *mockSpan) LogEvent(event string) { 91 | } 92 | func (sp *mockSpan) LogEventWithPayload(event string, payload interface{}) { 93 | } 94 | func (sp *mockSpan) Log(data opentracing.LogData) { 95 | } 96 | 97 | // Mock opentracing.Tracer 98 | type mockTracer struct { 99 | span *mockSpan 100 | hasStartSpanWithOption bool 101 | } 102 | 103 | func (tr *mockTracer) currentSpan() *mockSpan { 104 | return tr.span 105 | } 106 | 107 | func (tr *mockTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span { 108 | tr.hasStartSpanWithOption = len(opts) > 0 109 | if tr.span != nil { 110 | tr.span.opName = operationName 111 | return tr.span 112 | } 113 | span := createSpan(tr) 114 | span.opName = operationName 115 | return span 116 | } 117 | 118 | func (tr *mockTracer) Inject(sm opentracing.SpanContext, format interface{}, carrier interface{}) error { 119 | return nil 120 | } 121 | 122 | func (tr *mockTracer) Extract(format interface{}, carrier interface{}) (opentracing.SpanContext, error) { 123 | if tr.span != nil { 124 | return nil, nil 125 | } 126 | return nil, errors.New("no span") 127 | } 128 | 129 | func createMockTracer() *mockTracer { 130 | tracer := mockTracer{} 131 | span := createSpan(&tracer) 132 | tracer.span = span 133 | return &tracer 134 | } 135 | 136 | func TestTraceWithDefaultConfig(t *testing.T) { 137 | tracer := createMockTracer() 138 | 139 | e := echo.New() 140 | e.Use(Trace(tracer)) 141 | 142 | e.GET("/hello", func(c echo.Context) error { 143 | return c.String(http.StatusOK, "world") 144 | }) 145 | 146 | e.GET("/giveme400", func(c echo.Context) error { 147 | return echo.NewHTTPError(http.StatusBadRequest, "baaaad request") 148 | }) 149 | 150 | e.GET("/givemeerror", func(c echo.Context) error { 151 | return fmt.Errorf("internal stuff went wrong") 152 | }) 153 | 154 | t.Run("successful call", func(t *testing.T) { 155 | req := httptest.NewRequest(http.MethodGet, "/hello", nil) 156 | rec := httptest.NewRecorder() 157 | e.ServeHTTP(rec, req) 158 | 159 | assert.Equal(t, "GET", tracer.currentSpan().getTag("http.method")) 160 | assert.Equal(t, "/hello", tracer.currentSpan().getTag("http.url")) 161 | assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) 162 | assert.Equal(t, uint16(200), tracer.currentSpan().getTag("http.status_code")) 163 | assert.NotEqual(t, true, tracer.currentSpan().getTag("error")) 164 | }) 165 | 166 | t.Run("error from echo", func(t *testing.T) { 167 | req := httptest.NewRequest(http.MethodGet, "/idontexist", nil) 168 | rec := httptest.NewRecorder() 169 | e.ServeHTTP(rec, req) 170 | 171 | assert.Equal(t, "GET", tracer.currentSpan().getTag("http.method")) 172 | assert.Equal(t, "/idontexist", tracer.currentSpan().getTag("http.url")) 173 | assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) 174 | assert.Equal(t, uint16(404), tracer.currentSpan().getTag("http.status_code")) 175 | assert.Equal(t, true, tracer.currentSpan().getTag("error")) 176 | }) 177 | 178 | t.Run("custom http error", func(t *testing.T) { 179 | req := httptest.NewRequest(http.MethodGet, "/giveme400", nil) 180 | rec := httptest.NewRecorder() 181 | e.ServeHTTP(rec, req) 182 | 183 | assert.Equal(t, uint16(400), tracer.currentSpan().getTag("http.status_code")) 184 | assert.Equal(t, true, tracer.currentSpan().getTag("error")) 185 | assert.Equal(t, "baaaad request", tracer.currentSpan().getLog("error.message")) 186 | }) 187 | 188 | t.Run("unknown error", func(t *testing.T) { 189 | req := httptest.NewRequest(http.MethodGet, "/givemeerror", nil) 190 | rec := httptest.NewRecorder() 191 | e.ServeHTTP(rec, req) 192 | 193 | assert.Equal(t, uint16(500), tracer.currentSpan().getTag("http.status_code")) 194 | assert.Equal(t, true, tracer.currentSpan().getTag("error")) 195 | assert.Equal(t, "internal stuff went wrong", tracer.currentSpan().getLog("error.message")) 196 | }) 197 | } 198 | 199 | func TestTraceWithConfig(t *testing.T) { 200 | tracer := createMockTracer() 201 | 202 | e := echo.New() 203 | e.Use(TraceWithConfig(TraceConfig{ 204 | Tracer: tracer, 205 | ComponentName: "EchoTracer", 206 | })) 207 | req := httptest.NewRequest(http.MethodGet, "/trace", nil) 208 | rec := httptest.NewRecorder() 209 | e.ServeHTTP(rec, req) 210 | 211 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 212 | assert.Equal(t, "/trace", tracer.currentSpan().getTag("http.url")) 213 | assert.Equal(t, "EchoTracer", tracer.currentSpan().getTag("component")) 214 | assert.Equal(t, true, tracer.hasStartSpanWithOption) 215 | 216 | } 217 | 218 | func TestTraceWithConfigOfBodyDump(t *testing.T) { 219 | tracer := createMockTracer() 220 | 221 | e := echo.New() 222 | e.Use(TraceWithConfig(TraceConfig{ 223 | Tracer: tracer, 224 | ComponentName: "EchoTracer", 225 | IsBodyDump: true, 226 | })) 227 | e.POST("/trace", func(c echo.Context) error { 228 | return c.String(200, "Hi") 229 | }) 230 | 231 | req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString(`{"name": "Lorem"}`)) 232 | rec := httptest.NewRecorder() 233 | e.ServeHTTP(rec, req) 234 | 235 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 236 | assert.Equal(t, "EchoTracer", tracer.currentSpan().getTag("component")) 237 | assert.Equal(t, "/trace", tracer.currentSpan().getTag("http.url")) 238 | assert.Equal(t, `{"name": "Lorem"}`, tracer.currentSpan().getLog("http.req.body")) 239 | assert.Equal(t, `Hi`, tracer.currentSpan().getLog("http.resp.body")) 240 | assert.Equal(t, uint16(200), tracer.currentSpan().getTag("http.status_code")) 241 | assert.Equal(t, nil, tracer.currentSpan().getTag("error")) 242 | assert.Equal(t, true, tracer.hasStartSpanWithOption) 243 | 244 | } 245 | 246 | func TestTraceWithConfigOfNoneComponentName(t *testing.T) { 247 | tracer := createMockTracer() 248 | 249 | e := echo.New() 250 | e.Use(TraceWithConfig(TraceConfig{ 251 | Tracer: tracer, 252 | })) 253 | req := httptest.NewRequest(http.MethodGet, "/", nil) 254 | rec := httptest.NewRecorder() 255 | e.ServeHTTP(rec, req) 256 | 257 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 258 | assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) 259 | } 260 | 261 | func TestTraceWithConfigOfSkip(t *testing.T) { 262 | tracer := createMockTracer() 263 | e := echo.New() 264 | e.Use(TraceWithConfig(TraceConfig{ 265 | Skipper: func(echo.Context) bool { 266 | return true 267 | }, 268 | Tracer: tracer, 269 | })) 270 | req := httptest.NewRequest(http.MethodGet, "/", nil) 271 | rec := httptest.NewRecorder() 272 | e.ServeHTTP(rec, req) 273 | 274 | assert.Equal(t, false, tracer.currentSpan().isFinished()) 275 | } 276 | 277 | func TestTraceOfNoCurrentSpan(t *testing.T) { 278 | tracer := &mockTracer{} 279 | e := echo.New() 280 | e.Use(Trace(tracer)) 281 | req := httptest.NewRequest(http.MethodGet, "/", nil) 282 | rec := httptest.NewRecorder() 283 | e.ServeHTTP(rec, req) 284 | 285 | assert.Equal(t, false, tracer.hasStartSpanWithOption) 286 | } 287 | 288 | func TestTraceWithLimitHTTPBody(t *testing.T) { 289 | tracer := createMockTracer() 290 | 291 | e := echo.New() 292 | e.Use(TraceWithConfig(TraceConfig{ 293 | Tracer: tracer, 294 | ComponentName: "EchoTracer", 295 | IsBodyDump: true, 296 | LimitHTTPBody: true, 297 | LimitSize: 10, 298 | })) 299 | e.POST("/trace", func(c echo.Context) error { 300 | return c.String(200, "Hi 123456789012345678901234567890") 301 | }) 302 | 303 | req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890")) 304 | rec := httptest.NewRecorder() 305 | e.ServeHTTP(rec, req) 306 | 307 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 308 | assert.Equal(t, "12345\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.req.body")) 309 | assert.Equal(t, "Hi 12\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.resp.body")) 310 | } 311 | 312 | func TestTraceWithoutLimitHTTPBody(t *testing.T) { 313 | tracer := createMockTracer() 314 | 315 | e := echo.New() 316 | e.Use(TraceWithConfig(TraceConfig{ 317 | Tracer: tracer, 318 | ComponentName: "EchoTracer", 319 | IsBodyDump: true, 320 | LimitHTTPBody: false, // disabled 321 | LimitSize: 10, 322 | })) 323 | e.POST("/trace", func(c echo.Context) error { 324 | return c.String(200, "Hi 123456789012345678901234567890") 325 | }) 326 | 327 | req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890")) 328 | rec := httptest.NewRecorder() 329 | e.ServeHTTP(rec, req) 330 | 331 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 332 | assert.Equal(t, "123456789012345678901234567890", tracer.currentSpan().getLog("http.req.body")) 333 | assert.Equal(t, "Hi 123456789012345678901234567890", tracer.currentSpan().getLog("http.resp.body")) 334 | } 335 | 336 | func TestTraceWithDefaultOperationName(t *testing.T) { 337 | tracer := createMockTracer() 338 | 339 | e := echo.New() 340 | e.Use(Trace(tracer)) 341 | 342 | e.GET("/trace", func(c echo.Context) error { 343 | return c.String(http.StatusOK, "Hi") 344 | }) 345 | 346 | req := httptest.NewRequest(http.MethodGet, "/trace", nil) 347 | rec := httptest.NewRecorder() 348 | e.ServeHTTP(rec, req) 349 | 350 | assert.Equal(t, "HTTP GET URL: /trace", tracer.currentSpan().getOpName()) 351 | } 352 | 353 | func TestTraceWithCustomOperationName(t *testing.T) { 354 | tracer := createMockTracer() 355 | 356 | e := echo.New() 357 | e.Use(TraceWithConfig(TraceConfig{ 358 | Tracer: tracer, 359 | ComponentName: "EchoTracer", 360 | OperationNameFunc: func(c echo.Context) string { 361 | // This is an example of operation name customization 362 | // In most cases default formatting is more than enough 363 | req := c.Request() 364 | opName := "HTTP " + req.Method 365 | 366 | path := c.Path() 367 | paramNames := c.ParamNames() 368 | 369 | for _, name := range paramNames { 370 | from := ":" + name 371 | to := "{" + name + "}" 372 | path = strings.ReplaceAll(path, from, to) 373 | } 374 | 375 | return opName + " " + path 376 | }, 377 | })) 378 | 379 | e.GET("/trace/:traceID/spans/:spanID", func(c echo.Context) error { 380 | return c.String(http.StatusOK, "Hi") 381 | }) 382 | 383 | req := httptest.NewRequest(http.MethodGet, "/trace/123456/spans/123", nil) 384 | rec := httptest.NewRecorder() 385 | e.ServeHTTP(rec, req) 386 | 387 | assert.Equal(t, true, tracer.currentSpan().isFinished()) 388 | assert.Equal(t, "HTTP GET /trace/{traceID}/spans/{spanID}", tracer.currentSpan().getOpName()) 389 | } 390 | -------------------------------------------------------------------------------- /jaegertracing/response_dumper.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package jaegertracing 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | type responseDumper struct { 15 | http.ResponseWriter 16 | 17 | mw io.Writer 18 | buf *bytes.Buffer 19 | } 20 | 21 | func newResponseDumper(resp *echo.Response) *responseDumper { 22 | buf := new(bytes.Buffer) 23 | return &responseDumper{ 24 | ResponseWriter: resp.Writer, 25 | 26 | mw: io.MultiWriter(resp.Writer, buf), 27 | buf: buf, 28 | } 29 | } 30 | 31 | func (d *responseDumper) Write(b []byte) (int, error) { 32 | return d.mw.Write(b) 33 | } 34 | 35 | func (d *responseDumper) GetResponse() string { 36 | return d.buf.String() 37 | } 38 | -------------------------------------------------------------------------------- /pprof/README.md: -------------------------------------------------------------------------------- 1 | Usage 2 | 3 | ```code go 4 | package main 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo-contrib/pprof" 11 | 12 | ) 13 | 14 | func main() { 15 | e := echo.New() 16 | pprof.Register(e) 17 | ...... 18 | e.Logger.Fatal(e.Start(":1323")) 19 | } 20 | ``` 21 | 22 | - Then use the pprof tool to look at the heap profile: 23 | 24 | `go tool pprof http://localhost:1323/debug/pprof/heap` 25 | 26 | - Or to look at a 30-second CPU profile: 27 | 28 | `go tool pprof http://localhost:1323/debug/pprof/profile?seconds=30` 29 | 30 | - Or to look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate in your program: 31 | 32 | `go tool pprof http://localhost:1323/debug/pprof/block` 33 | 34 | - Or to look at the holders of contended mutexes, after calling runtime.SetMutexProfileFraction in your program: 35 | 36 | `go tool pprof http://localhost:1323/debug/pprof/mutex` 37 | 38 | 39 | -------------------------------------------------------------------------------- /pprof/pprof.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package pprof 5 | 6 | import ( 7 | "net/http" 8 | "net/http/pprof" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | const ( 14 | // DefaultPrefix url prefix of pprof 15 | DefaultPrefix = "/debug/pprof" 16 | ) 17 | 18 | func getPrefix(prefixOptions ...string) string { 19 | if len(prefixOptions) > 0 { 20 | return prefixOptions[0] 21 | } 22 | return DefaultPrefix 23 | } 24 | 25 | // Register middleware for net/http/pprof 26 | func Register(e *echo.Echo, prefixOptions ...string) { 27 | prefix := getPrefix(prefixOptions...) 28 | 29 | prefixRouter := e.Group(prefix) 30 | { 31 | prefixRouter.GET("/", handler(pprof.Index)) 32 | prefixRouter.GET("/allocs", handler(pprof.Handler("allocs").ServeHTTP)) 33 | prefixRouter.GET("/block", handler(pprof.Handler("block").ServeHTTP)) 34 | prefixRouter.GET("/cmdline", handler(pprof.Cmdline)) 35 | prefixRouter.GET("/goroutine", handler(pprof.Handler("goroutine").ServeHTTP)) 36 | prefixRouter.GET("/heap", handler(pprof.Handler("heap").ServeHTTP)) 37 | prefixRouter.GET("/mutex", handler(pprof.Handler("mutex").ServeHTTP)) 38 | prefixRouter.GET("/profile", handler(pprof.Profile)) 39 | prefixRouter.POST("/symbol", handler(pprof.Symbol)) 40 | prefixRouter.GET("/symbol", handler(pprof.Symbol)) 41 | prefixRouter.GET("/threadcreate", handler(pprof.Handler("threadcreate").ServeHTTP)) 42 | prefixRouter.GET("/trace", handler(pprof.Trace)) 43 | } 44 | } 45 | 46 | func handler(h http.HandlerFunc) echo.HandlerFunc { 47 | return func(c echo.Context) error { 48 | h.ServeHTTP(c.Response().Writer, c.Request()) 49 | return nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pprof/pprof_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package pprof 5 | 6 | import ( 7 | "github.com/stretchr/testify/assert" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func TestPProfRegisterDefaualtPrefix(t *testing.T) { 16 | var pprofPaths = []struct { 17 | path string 18 | }{ 19 | {"/"}, 20 | {"/allocs"}, 21 | {"/block"}, 22 | {"/cmdline"}, 23 | {"/goroutine"}, 24 | {"/heap"}, 25 | {"/mutex"}, 26 | {"/profile?seconds=1"}, 27 | {"/symbol"}, 28 | {"/symbol"}, 29 | {"/threadcreate"}, 30 | {"/trace"}, 31 | } 32 | for _, tt := range pprofPaths { 33 | t.Run(tt.path, func(t *testing.T) { 34 | e := echo.New() 35 | Register(e) 36 | req, _ := http.NewRequest(http.MethodGet, DefaultPrefix+tt.path, nil) 37 | rec := httptest.NewRecorder() 38 | e.ServeHTTP(rec, req) 39 | assert.Equal(t, rec.Code, http.StatusOK) 40 | }) 41 | } 42 | } 43 | 44 | func TestPProfRegisterCustomPrefix(t *testing.T) { 45 | var pprofPaths = []struct { 46 | path string 47 | }{ 48 | {"/"}, 49 | {"/allocs"}, 50 | {"/block"}, 51 | {"/cmdline"}, 52 | {"/goroutine"}, 53 | {"/heap"}, 54 | {"/mutex"}, 55 | {"/profile?seconds=1"}, 56 | {"/symbol"}, 57 | {"/symbol"}, 58 | {"/threadcreate"}, 59 | {"/trace"}, 60 | } 61 | for _, tt := range pprofPaths { 62 | t.Run(tt.path, func(t *testing.T) { 63 | e := echo.New() 64 | pprofPrefix := "/myapp/pprof" 65 | Register(e, pprofPrefix) 66 | req, _ := http.NewRequest(http.MethodGet, pprofPrefix+tt.path, nil) 67 | rec := httptest.NewRecorder() 68 | e.ServeHTTP(rec, req) 69 | assert.Equal(t, rec.Code, http.StatusOK) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | /* 5 | Package prometheus provides middleware to add Prometheus metrics. 6 | 7 | Example: 8 | ``` 9 | package main 10 | import ( 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo-contrib/prometheus" 14 | 15 | ) 16 | 17 | func main() { 18 | e := echo.New() 19 | // Enable metrics middleware 20 | p := prometheus.NewPrometheus("echo", nil) 21 | p.Use(e) 22 | 23 | e.Logger.Fatal(e.Start(":1323")) 24 | } 25 | 26 | ``` 27 | */ 28 | package prometheus 29 | 30 | import ( 31 | "bytes" 32 | "errors" 33 | "net/http" 34 | "os" 35 | "strconv" 36 | "time" 37 | 38 | "github.com/labstack/echo/v4" 39 | "github.com/labstack/echo/v4/middleware" 40 | "github.com/labstack/gommon/log" 41 | "github.com/prometheus/client_golang/prometheus" 42 | "github.com/prometheus/client_golang/prometheus/promhttp" 43 | "github.com/prometheus/common/expfmt" 44 | ) 45 | 46 | var defaultMetricPath = "/metrics" 47 | var defaultSubsystem = "echo" 48 | 49 | const ( 50 | _ = iota // ignore first value by assigning to blank identifier 51 | KB float64 = 1 << (10 * iota) 52 | MB 53 | GB 54 | TB 55 | ) 56 | 57 | // reqDurBuckets is the buckets for request duration. Here, we use the prometheus defaults 58 | // which are for ~10s request length max: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 59 | var reqDurBuckets = prometheus.DefBuckets 60 | 61 | // reqSzBuckets is the buckets for request size. Here we define a spectrom from 1KB thru 1NB up to 10MB. 62 | var reqSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} 63 | 64 | // resSzBuckets is the buckets for response size. Here we define a spectrom from 1KB thru 1NB up to 10MB. 65 | var resSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} 66 | 67 | // Standard default metrics 68 | // 69 | // counter, counter_vec, gauge, gauge_vec, 70 | // histogram, histogram_vec, summary, summary_vec 71 | var reqCnt = &Metric{ 72 | ID: "reqCnt", 73 | Name: "requests_total", 74 | Description: "How many HTTP requests processed, partitioned by status code and HTTP method.", 75 | Type: "counter_vec", 76 | Args: []string{"code", "method", "host", "url"}} 77 | 78 | var reqDur = &Metric{ 79 | ID: "reqDur", 80 | Name: "request_duration_seconds", 81 | Description: "The HTTP request latencies in seconds.", 82 | Args: []string{"code", "method", "host", "url"}, 83 | Type: "histogram_vec", 84 | Buckets: reqDurBuckets} 85 | 86 | var resSz = &Metric{ 87 | ID: "resSz", 88 | Name: "response_size_bytes", 89 | Description: "The HTTP response sizes in bytes.", 90 | Args: []string{"code", "method", "host", "url"}, 91 | Type: "histogram_vec", 92 | Buckets: resSzBuckets} 93 | 94 | var reqSz = &Metric{ 95 | ID: "reqSz", 96 | Name: "request_size_bytes", 97 | Description: "The HTTP request sizes in bytes.", 98 | Args: []string{"code", "method", "host", "url"}, 99 | Type: "histogram_vec", 100 | Buckets: reqSzBuckets} 101 | 102 | var standardMetrics = []*Metric{ 103 | reqCnt, 104 | reqDur, 105 | resSz, 106 | reqSz, 107 | } 108 | 109 | /* 110 | RequestCounterLabelMappingFunc is a function which can be supplied to the middleware to control 111 | the cardinality of the request counter's "url" label, which might be required in some contexts. 112 | For instance, if for a "/customer/:name" route you don't want to generate a time series for every 113 | possible customer name, you could use this function: 114 | 115 | func(c echo.Context) string { 116 | url := c.Request.URL.Path 117 | for _, p := range c.Params { 118 | if p.Key == "name" { 119 | url = strings.Replace(url, p.Value, ":name", 1) 120 | break 121 | } 122 | } 123 | return url 124 | } 125 | 126 | which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". 127 | It can also be applied for the "Host" label 128 | */ 129 | type RequestCounterLabelMappingFunc func(c echo.Context) string 130 | 131 | // Metric is a definition for the name, description, type, ID, and 132 | // prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric 133 | type Metric struct { 134 | MetricCollector prometheus.Collector 135 | ID string 136 | Name string 137 | Description string 138 | Type string 139 | Args []string 140 | Buckets []float64 141 | } 142 | 143 | // Prometheus contains the metrics gathered by the instance and its path 144 | // Deprecated: use echoprometheus package instead 145 | type Prometheus struct { 146 | reqCnt *prometheus.CounterVec 147 | reqDur, reqSz, resSz *prometheus.HistogramVec 148 | router *echo.Echo 149 | listenAddress string 150 | Ppg PushGateway 151 | 152 | MetricsList []*Metric 153 | MetricsPath string 154 | Subsystem string 155 | Skipper middleware.Skipper 156 | 157 | RequestCounterURLLabelMappingFunc RequestCounterLabelMappingFunc 158 | RequestCounterHostLabelMappingFunc RequestCounterLabelMappingFunc 159 | 160 | // Context string to use as a prometheus URL label 161 | URLLabelFromContext string 162 | } 163 | 164 | // PushGateway contains the configuration for pushing to a Prometheus pushgateway (optional) 165 | type PushGateway struct { 166 | // Push interval in seconds 167 | //lint:ignore ST1011 renaming would be breaking change 168 | PushIntervalSeconds time.Duration 169 | 170 | // Push Gateway URL in format http://domain:port 171 | // where JOBNAME can be any string of your choice 172 | PushGatewayURL string 173 | 174 | // pushgateway job name, defaults to "echo" 175 | Job string 176 | } 177 | 178 | // NewPrometheus generates a new set of metrics with a certain subsystem name 179 | // Deprecated: use echoprometheus package instead 180 | func NewPrometheus(subsystem string, skipper middleware.Skipper, customMetricsList ...[]*Metric) *Prometheus { 181 | var metricsList []*Metric 182 | if skipper == nil { 183 | skipper = middleware.DefaultSkipper 184 | } 185 | 186 | if len(customMetricsList) > 1 { 187 | panic("Too many args. NewPrometheus( string, ).") 188 | } else if len(customMetricsList) == 1 { 189 | metricsList = customMetricsList[0] 190 | } 191 | 192 | metricsList = append(metricsList, standardMetrics...) 193 | 194 | p := &Prometheus{ 195 | MetricsList: metricsList, 196 | MetricsPath: defaultMetricPath, 197 | Subsystem: defaultSubsystem, 198 | Skipper: skipper, 199 | RequestCounterURLLabelMappingFunc: func(c echo.Context) string { 200 | p := c.Path() // contains route path ala `/users/:id` 201 | if p != "" { 202 | return p 203 | } 204 | // as of Echo v4.10.1 path is empty for 404 cases (when router did not find any matching routes) 205 | // in this case we use actual path from request to have some distinction in Prometheus 206 | return c.Request().URL.Path 207 | }, 208 | RequestCounterHostLabelMappingFunc: func(c echo.Context) string { 209 | return c.Request().Host 210 | }, 211 | } 212 | 213 | p.registerMetrics(subsystem) 214 | 215 | return p 216 | } 217 | 218 | // SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL 219 | // every pushInterval. Metrics are fetched from 220 | func (p *Prometheus) SetPushGateway(pushGatewayURL string, pushInterval time.Duration) { 221 | p.Ppg.PushGatewayURL = pushGatewayURL 222 | p.Ppg.PushIntervalSeconds = pushInterval 223 | p.startPushTicker() 224 | } 225 | 226 | // SetPushGatewayJob job name, defaults to "echo" 227 | func (p *Prometheus) SetPushGatewayJob(j string) { 228 | p.Ppg.Job = j 229 | } 230 | 231 | // SetListenAddress for exposing metrics on address. If not set, it will be exposed at the 232 | // same address of the echo engine that is being used 233 | // func (p *Prometheus) SetListenAddress(address string) { 234 | // p.listenAddress = address 235 | // if p.listenAddress != "" { 236 | // p.router = echo.Echo().Router() 237 | // } 238 | // } 239 | 240 | // SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of 241 | // your content's access log). 242 | // func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *echo.Echo) { 243 | // p.listenAddress = listenAddress 244 | // if len(p.listenAddress) > 0 { 245 | // p.router = r 246 | // } 247 | // } 248 | 249 | // SetMetricsPath set metrics paths 250 | func (p *Prometheus) SetMetricsPath(e *echo.Echo) { 251 | if p.listenAddress != "" { 252 | p.router.GET(p.MetricsPath, prometheusHandler()) 253 | p.runServer() 254 | } else { 255 | e.GET(p.MetricsPath, prometheusHandler()) 256 | } 257 | } 258 | 259 | func (p *Prometheus) runServer() { 260 | if p.listenAddress != "" { 261 | go p.router.Start(p.listenAddress) 262 | } 263 | } 264 | 265 | func (p *Prometheus) getMetrics() []byte { 266 | out := &bytes.Buffer{} 267 | metricFamilies, _ := prometheus.DefaultGatherer.Gather() 268 | for i := range metricFamilies { 269 | expfmt.MetricFamilyToText(out, metricFamilies[i]) 270 | 271 | } 272 | return out.Bytes() 273 | } 274 | 275 | func (p *Prometheus) getPushGatewayURL() string { 276 | h, _ := os.Hostname() 277 | if p.Ppg.Job == "" { 278 | p.Ppg.Job = "echo" 279 | } 280 | return p.Ppg.PushGatewayURL + "/metrics/job/" + p.Ppg.Job + "/instance/" + h 281 | } 282 | 283 | func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) { 284 | req, err := http.NewRequest("POST", p.getPushGatewayURL(), bytes.NewBuffer(metrics)) 285 | if err != nil { 286 | log.Errorf("failed to create push gateway request: %v", err) 287 | return 288 | } 289 | client := &http.Client{} 290 | if _, err = client.Do(req); err != nil { 291 | log.Errorf("Error sending to push gateway: %v", err) 292 | } 293 | } 294 | 295 | func (p *Prometheus) startPushTicker() { 296 | ticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds) 297 | go func() { 298 | for range ticker.C { 299 | p.sendMetricsToPushGateway(p.getMetrics()) 300 | } 301 | }() 302 | } 303 | 304 | // NewMetric associates prometheus.Collector based on Metric.Type 305 | // Deprecated: use echoprometheus package instead 306 | func NewMetric(m *Metric, subsystem string) prometheus.Collector { 307 | var metric prometheus.Collector 308 | switch m.Type { 309 | case "counter_vec": 310 | metric = prometheus.NewCounterVec( 311 | prometheus.CounterOpts{ 312 | Subsystem: subsystem, 313 | Name: m.Name, 314 | Help: m.Description, 315 | }, 316 | m.Args, 317 | ) 318 | case "counter": 319 | metric = prometheus.NewCounter( 320 | prometheus.CounterOpts{ 321 | Subsystem: subsystem, 322 | Name: m.Name, 323 | Help: m.Description, 324 | }, 325 | ) 326 | case "gauge_vec": 327 | metric = prometheus.NewGaugeVec( 328 | prometheus.GaugeOpts{ 329 | Subsystem: subsystem, 330 | Name: m.Name, 331 | Help: m.Description, 332 | }, 333 | m.Args, 334 | ) 335 | case "gauge": 336 | metric = prometheus.NewGauge( 337 | prometheus.GaugeOpts{ 338 | Subsystem: subsystem, 339 | Name: m.Name, 340 | Help: m.Description, 341 | }, 342 | ) 343 | case "histogram_vec": 344 | metric = prometheus.NewHistogramVec( 345 | prometheus.HistogramOpts{ 346 | Subsystem: subsystem, 347 | Name: m.Name, 348 | Help: m.Description, 349 | Buckets: m.Buckets, 350 | }, 351 | m.Args, 352 | ) 353 | case "histogram": 354 | metric = prometheus.NewHistogram( 355 | prometheus.HistogramOpts{ 356 | Subsystem: subsystem, 357 | Name: m.Name, 358 | Help: m.Description, 359 | Buckets: m.Buckets, 360 | }, 361 | ) 362 | case "summary_vec": 363 | metric = prometheus.NewSummaryVec( 364 | prometheus.SummaryOpts{ 365 | Subsystem: subsystem, 366 | Name: m.Name, 367 | Help: m.Description, 368 | }, 369 | m.Args, 370 | ) 371 | case "summary": 372 | metric = prometheus.NewSummary( 373 | prometheus.SummaryOpts{ 374 | Subsystem: subsystem, 375 | Name: m.Name, 376 | Help: m.Description, 377 | }, 378 | ) 379 | } 380 | return metric 381 | } 382 | 383 | func (p *Prometheus) registerMetrics(subsystem string) { 384 | 385 | for _, metricDef := range p.MetricsList { 386 | metric := NewMetric(metricDef, subsystem) 387 | if err := prometheus.Register(metric); err != nil { 388 | log.Errorf("%s could not be registered in Prometheus: %v", metricDef.Name, err) 389 | } 390 | switch metricDef { 391 | case reqCnt: 392 | p.reqCnt = metric.(*prometheus.CounterVec) 393 | case reqDur: 394 | p.reqDur = metric.(*prometheus.HistogramVec) 395 | case resSz: 396 | p.resSz = metric.(*prometheus.HistogramVec) 397 | case reqSz: 398 | p.reqSz = metric.(*prometheus.HistogramVec) 399 | } 400 | metricDef.MetricCollector = metric 401 | } 402 | } 403 | 404 | // Use adds the middleware to the Echo engine. 405 | func (p *Prometheus) Use(e *echo.Echo) { 406 | e.Use(p.HandlerFunc) 407 | p.SetMetricsPath(e) 408 | } 409 | 410 | // HandlerFunc defines handler function for middleware 411 | func (p *Prometheus) HandlerFunc(next echo.HandlerFunc) echo.HandlerFunc { 412 | return func(c echo.Context) error { 413 | if c.Path() == p.MetricsPath { 414 | return next(c) 415 | } 416 | if p.Skipper(c) { 417 | return next(c) 418 | } 419 | 420 | start := time.Now() 421 | reqSz := computeApproximateRequestSize(c.Request()) 422 | 423 | err := next(c) 424 | 425 | status := c.Response().Status 426 | if err != nil { 427 | var httpError *echo.HTTPError 428 | if errors.As(err, &httpError) { 429 | status = httpError.Code 430 | } 431 | if status == 0 || status == http.StatusOK { 432 | status = http.StatusInternalServerError 433 | } 434 | } 435 | 436 | elapsed := float64(time.Since(start)) / float64(time.Second) 437 | 438 | url := p.RequestCounterURLLabelMappingFunc(c) 439 | if len(p.URLLabelFromContext) > 0 { 440 | u := c.Get(p.URLLabelFromContext) 441 | if u == nil { 442 | u = "unknown" 443 | } 444 | url = u.(string) 445 | } 446 | 447 | statusStr := strconv.Itoa(status) 448 | p.reqDur.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(elapsed) 449 | p.reqCnt.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Inc() 450 | p.reqSz.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(float64(reqSz)) 451 | 452 | resSz := float64(c.Response().Size) 453 | p.resSz.WithLabelValues(statusStr, c.Request().Method, p.RequestCounterHostLabelMappingFunc(c), url).Observe(resSz) 454 | 455 | return err 456 | } 457 | } 458 | 459 | func prometheusHandler() echo.HandlerFunc { 460 | h := promhttp.Handler() 461 | return func(c echo.Context) error { 462 | h.ServeHTTP(c.Response(), c.Request()) 463 | return nil 464 | } 465 | } 466 | 467 | func computeApproximateRequestSize(r *http.Request) int { 468 | s := 0 469 | if r.URL != nil { 470 | s = len(r.URL.Path) 471 | } 472 | 473 | s += len(r.Method) 474 | s += len(r.Proto) 475 | for name, values := range r.Header { 476 | s += len(name) 477 | for _, value := range values { 478 | s += len(value) 479 | } 480 | } 481 | s += len(r.Host) 482 | 483 | // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 484 | 485 | if r.ContentLength != -1 { 486 | s += int(r.ContentLength) 487 | } 488 | return s 489 | } 490 | -------------------------------------------------------------------------------- /prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package prometheus 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/labstack/echo/v4" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func unregister(p *Prometheus) { 19 | prometheus.Unregister(p.reqCnt) 20 | prometheus.Unregister(p.reqDur) 21 | prometheus.Unregister(p.reqSz) 22 | prometheus.Unregister(p.resSz) 23 | } 24 | 25 | func TestPrometheus_Use(t *testing.T) { 26 | e := echo.New() 27 | p := NewPrometheus("echo", nil) 28 | p.Use(e) 29 | 30 | assert.Equal(t, 1, len(e.Routes()), "only one route should be added") 31 | assert.NotNil(t, e, "the engine should not be empty") 32 | assert.Equal(t, e.Routes()[0].Path, p.MetricsPath, "the path should match the metrics path") 33 | unregister(p) 34 | } 35 | 36 | func TestPrometheus_Buckets(t *testing.T) { 37 | e := echo.New() 38 | p := NewPrometheus("echo", nil) 39 | p.Use(e) 40 | 41 | path := "/ping" 42 | 43 | req := httptest.NewRequest(http.MethodGet, path, nil) 44 | rec := httptest.NewRecorder() 45 | e.ServeHTTP(rec, req) 46 | assert.Equal(t, http.StatusNotFound, rec.Code) 47 | 48 | req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 49 | rec = httptest.NewRecorder() 50 | e.ServeHTTP(rec, req) 51 | 52 | assert.Equal(t, http.StatusOK, rec.Code) 53 | assert.Contains(t, rec.Body.String(), fmt.Sprintf("%s_request_duration_seconds", p.Subsystem)) 54 | 55 | body := rec.Body.String() 56 | assert.Contains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "duration should have time bucket (like, 0.005s)") 57 | assert.NotContains(t, body, `echo_request_duration_seconds_bucket{code="404",host="example.com",method="GET",url="/ping",le="512000"}`, "duration should NOT have a size bucket (like, 512K)") 58 | assert.Contains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "request size should have a 1024k (size) bucket") 59 | assert.NotContains(t, body, `echo_request_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "request size should NOT have time bucket (like, 0.005s)") 60 | assert.Contains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="1024"}`, "response size should have a 1024k (size) bucket") 61 | assert.NotContains(t, body, `echo_response_size_bytes_bucket{code="404",host="example.com",method="GET",url="/ping",le="0.005"}`, "response size should NOT have time bucket (like, 0.005s)") 62 | 63 | unregister(p) 64 | } 65 | 66 | func TestPath(t *testing.T) { 67 | p := NewPrometheus("echo", nil) 68 | assert.Equal(t, p.MetricsPath, defaultMetricPath, "no usage of path should yield default path") 69 | unregister(p) 70 | } 71 | 72 | func TestSubsystem(t *testing.T) { 73 | p := NewPrometheus("echo", nil) 74 | assert.Equal(t, p.Subsystem, "echo", "subsystem should be default") 75 | unregister(p) 76 | } 77 | 78 | func TestUse(t *testing.T) { 79 | e := echo.New() 80 | p := NewPrometheus("echo", nil) 81 | 82 | req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 83 | rec := httptest.NewRecorder() 84 | e.ServeHTTP(rec, req) 85 | assert.Equal(t, http.StatusNotFound, rec.Code) 86 | 87 | p.Use(e) 88 | 89 | req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 90 | rec = httptest.NewRecorder() 91 | e.ServeHTTP(rec, req) 92 | assert.Equal(t, http.StatusOK, rec.Code) 93 | 94 | unregister(p) 95 | } 96 | 97 | func TestIgnore(t *testing.T) { 98 | e := echo.New() 99 | 100 | ipath := "/ping" 101 | lipath := fmt.Sprintf(`path="%s"`, ipath) 102 | ignore := func(c echo.Context) bool { 103 | if strings.HasPrefix(c.Path(), ipath) { 104 | return true 105 | } 106 | return false 107 | } 108 | p := NewPrometheus("echo", ignore) 109 | p.Use(e) 110 | 111 | req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 112 | rec := httptest.NewRecorder() 113 | e.ServeHTTP(rec, req) 114 | assert.Equal(t, http.StatusOK, rec.Code) 115 | assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_requests_total", p.Subsystem)) 116 | 117 | req = httptest.NewRequest(http.MethodGet, "/ping", nil) 118 | rec = httptest.NewRecorder() 119 | e.ServeHTTP(rec, req) 120 | assert.Equal(t, http.StatusNotFound, rec.Code) 121 | 122 | req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 123 | rec = httptest.NewRecorder() 124 | e.ServeHTTP(rec, req) 125 | 126 | assert.Equal(t, http.StatusOK, rec.Code) 127 | assert.NotContains(t, rec.Body.String(), lipath, "ignored path must not be present") 128 | 129 | unregister(p) 130 | } 131 | 132 | func TestMetricsGenerated(t *testing.T) { 133 | e := echo.New() 134 | p := NewPrometheus("echo", nil) 135 | p.Use(e) 136 | 137 | req := httptest.NewRequest(http.MethodGet, "/ping?test=1", nil) 138 | rec := httptest.NewRecorder() 139 | e.ServeHTTP(rec, req) 140 | assert.Equal(t, http.StatusNotFound, rec.Code) 141 | 142 | req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 143 | rec = httptest.NewRecorder() 144 | e.ServeHTTP(rec, req) 145 | 146 | assert.Equal(t, http.StatusOK, rec.Code) 147 | s := rec.Body.String() 148 | assert.Contains(t, s, `url="/ping"`, "path must be present") 149 | assert.Contains(t, s, `host="example.com"`, "host must be present") 150 | 151 | unregister(p) 152 | } 153 | 154 | func TestMetricsPathIgnored(t *testing.T) { 155 | e := echo.New() 156 | p := NewPrometheus("echo", nil) 157 | p.Use(e) 158 | 159 | req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 160 | rec := httptest.NewRecorder() 161 | e.ServeHTTP(rec, req) 162 | 163 | assert.Equal(t, http.StatusOK, rec.Code) 164 | assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_requests_total", p.Subsystem)) 165 | unregister(p) 166 | } 167 | 168 | func TestMetricsPushGateway(t *testing.T) { 169 | e := echo.New() 170 | p := NewPrometheus("echo", nil) 171 | p.Use(e) 172 | 173 | req := httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 174 | rec := httptest.NewRecorder() 175 | e.ServeHTTP(rec, req) 176 | assert.Equal(t, http.StatusOK, rec.Code) 177 | assert.NotContains(t, rec.Body.String(), fmt.Sprintf("%s_request_duration", p.Subsystem)) 178 | 179 | unregister(p) 180 | } 181 | 182 | func TestMetricsForErrors(t *testing.T) { 183 | e := echo.New() 184 | p := NewPrometheus("echo", nil) 185 | p.Use(e) 186 | 187 | e.GET("/handler_for_ok", func(c echo.Context) error { 188 | return c.JSON(http.StatusOK, "OK") 189 | }) 190 | e.GET("/handler_for_nok", func(c echo.Context) error { 191 | return c.JSON(http.StatusConflict, "NOK") 192 | }) 193 | e.GET("/handler_for_error", func(c echo.Context) error { 194 | return echo.NewHTTPError(http.StatusBadGateway, "BAD") 195 | }) 196 | 197 | req := httptest.NewRequest(http.MethodGet, "/handler_for_ok", nil) 198 | rec := httptest.NewRecorder() 199 | e.ServeHTTP(rec, req) 200 | assert.Equal(t, http.StatusOK, rec.Code) 201 | 202 | req = httptest.NewRequest(http.MethodGet, "/handler_for_nok", nil) 203 | rec = httptest.NewRecorder() 204 | e.ServeHTTP(rec, req) 205 | assert.Equal(t, http.StatusConflict, rec.Code) 206 | 207 | req = httptest.NewRequest(http.MethodGet, "/handler_for_nok", nil) 208 | rec = httptest.NewRecorder() 209 | e.ServeHTTP(rec, req) 210 | assert.Equal(t, http.StatusConflict, rec.Code) 211 | 212 | req = httptest.NewRequest(http.MethodGet, "/handler_for_error", nil) 213 | rec = httptest.NewRecorder() 214 | e.ServeHTTP(rec, req) 215 | assert.Equal(t, http.StatusBadGateway, rec.Code) 216 | 217 | req = httptest.NewRequest(http.MethodGet, p.MetricsPath, nil) 218 | rec = httptest.NewRecorder() 219 | e.ServeHTTP(rec, req) 220 | 221 | assert.Equal(t, http.StatusOK, rec.Code) 222 | body := rec.Body.String() 223 | assert.Contains(t, body, fmt.Sprintf("%s_requests_total", p.Subsystem)) 224 | assert.Contains(t, body, `echo_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) 225 | assert.Contains(t, body, `echo_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 2`) 226 | assert.Contains(t, body, `echo_requests_total{code="502",host="example.com",method="GET",url="/handler_for_error"} 1`) 227 | 228 | unregister(p) 229 | } 230 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package session 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/gorilla/context" 10 | "github.com/gorilla/sessions" 11 | "github.com/labstack/echo/v4" 12 | "github.com/labstack/echo/v4/middleware" 13 | ) 14 | 15 | type ( 16 | // Config defines the config for Session middleware. 17 | Config struct { 18 | // Skipper defines a function to skip middleware. 19 | Skipper middleware.Skipper 20 | 21 | // Session store. 22 | // Required. 23 | Store sessions.Store 24 | } 25 | ) 26 | 27 | const ( 28 | key = "_session_store" 29 | ) 30 | 31 | var ( 32 | // DefaultConfig is the default Session middleware config. 33 | DefaultConfig = Config{ 34 | Skipper: middleware.DefaultSkipper, 35 | } 36 | ) 37 | 38 | // Get returns a named session. 39 | func Get(name string, c echo.Context) (*sessions.Session, error) { 40 | s := c.Get(key) 41 | if s == nil { 42 | return nil, fmt.Errorf("%q session store not found", key) 43 | } 44 | store := s.(sessions.Store) 45 | return store.Get(c.Request(), name) 46 | } 47 | 48 | // Middleware returns a Session middleware. 49 | func Middleware(store sessions.Store) echo.MiddlewareFunc { 50 | c := DefaultConfig 51 | c.Store = store 52 | return MiddlewareWithConfig(c) 53 | } 54 | 55 | // MiddlewareWithConfig returns a Sessions middleware with config. 56 | // See `Middleware()`. 57 | func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { 58 | // Defaults 59 | if config.Skipper == nil { 60 | config.Skipper = DefaultConfig.Skipper 61 | } 62 | if config.Store == nil { 63 | panic("echo: session middleware requires store") 64 | } 65 | 66 | return func(next echo.HandlerFunc) echo.HandlerFunc { 67 | return func(c echo.Context) error { 68 | if config.Skipper(c) { 69 | return next(c) 70 | } 71 | defer context.Clear(c.Request()) 72 | c.Set(key, config.Store) 73 | return next(c) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /session/session_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package session 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gorilla/sessions" 13 | "github.com/labstack/echo/v4" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestMiddleware(t *testing.T) { 18 | e := echo.New() 19 | req := httptest.NewRequest(echo.GET, "/", nil) 20 | rec := httptest.NewRecorder() 21 | c := e.NewContext(req, rec) 22 | handler := func(c echo.Context) error { 23 | sess, _ := Get("test", c) 24 | sess.Options.Domain = "labstack.com" 25 | sess.Values["foo"] = "bar" 26 | if err := sess.Save(c.Request(), c.Response()); err != nil { 27 | return err 28 | } 29 | return c.String(http.StatusOK, "test") 30 | } 31 | store := sessions.NewCookieStore([]byte("secret")) 32 | config := Config{ 33 | Skipper: func(c echo.Context) bool { 34 | return true 35 | }, 36 | Store: store, 37 | } 38 | 39 | // Skipper 40 | mw := MiddlewareWithConfig(config) 41 | h := mw(echo.NotFoundHandler) 42 | assert.Error(t, h(c)) // 404 43 | assert.Nil(t, c.Get(key)) 44 | 45 | // Panic 46 | config.Skipper = nil 47 | config.Store = nil 48 | assert.Panics(t, func() { 49 | MiddlewareWithConfig(config) 50 | }) 51 | 52 | // Core 53 | mw = Middleware(store) 54 | h = mw(handler) 55 | assert.NoError(t, h(c)) 56 | assert.Contains(t, rec.Header().Get(echo.HeaderSetCookie), "labstack.com") 57 | 58 | } 59 | 60 | func TestGetSessionMissingStore(t *testing.T) { 61 | e := echo.New() 62 | req := httptest.NewRequest(echo.GET, "/", nil) 63 | rec := httptest.NewRecorder() 64 | c := e.NewContext(req, rec) 65 | _, err := Get("test", c) 66 | 67 | assert.EqualError(t, err, fmt.Sprintf("%q session store not found", key)) 68 | } 69 | -------------------------------------------------------------------------------- /zipkintracing/README.md: -------------------------------------------------------------------------------- 1 | # Tracing Library for Go 2 | 3 | This library provides tracing for go using [Zipkin](https://zipkin.io/) 4 | 5 | ## Usage 6 | 7 | ### Server Tracing Middleware & http client tracing 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "github.com/labstack/echo-contrib/zipkintracing" 14 | "github.com/labstack/echo/v4" 15 | "github.com/openzipkin/zipkin-go" 16 | zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" 17 | zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" 18 | "io/ioutil" 19 | "net/http" 20 | ) 21 | 22 | func main() { 23 | e := echo.New() 24 | endpoint, err := zipkin.NewEndpoint("echo-service", "") 25 | if err != nil { 26 | e.Logger.Fatalf("error creating zipkin endpoint: %s", err.Error()) 27 | } 28 | reporter := zipkinHttpReporter.NewReporter("http://localhost:9411/api/v2/spans") 29 | traceTags := make(map[string]string) 30 | traceTags["availability_zone"] = "us-east-1" 31 | tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(traceTags)) 32 | client, _ := zipkinhttp.NewClient(tracer, zipkinhttp.ClientTrace(true)) 33 | if err != nil { 34 | e.Logger.Fatalf("tracing init failed: %s", err.Error()) 35 | } 36 | //Wrap & Use trace server middleware, this traces all server calls 37 | e.Use(zipkintracing.TraceServer(tracer)) 38 | //.... 39 | e.GET("/echo", func(c echo.Context) error { 40 | //trace http request calls. 41 | req, _ := http.NewRequest("GET", "https://echo.labstack.com/", nil) 42 | resp, _ := zipkintracing.DoHTTP(c, req, client) 43 | body, _ := ioutil.ReadAll(resp.Body) 44 | return c.String(http.StatusOK, string(body)) 45 | }) 46 | 47 | defer reporter.Close() //defer close reporter 48 | e.Logger.Fatal(e.Start(":8080")) 49 | } 50 | ``` 51 | ### Reverse Proxy Tracing 52 | 53 | ```go 54 | package main 55 | 56 | import ( 57 | "github.com/labstack/echo-contrib/zipkintracing" 58 | "github.com/labstack/echo/v4" 59 | "github.com/labstack/echo/v4/middleware" 60 | "github.com/openzipkin/zipkin-go" 61 | zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" 62 | "net/http/httputil" 63 | "net/url" 64 | ) 65 | 66 | func main() { 67 | e := echo.New() 68 | //new tracing instance 69 | endpoint, err := zipkin.NewEndpoint("echo-service", "") 70 | if err != nil { 71 | e.Logger.Fatalf("error creating zipkin endpoint: %s", err.Error()) 72 | } 73 | reporter := zipkinHttpReporter.NewReporter("http://localhost:9411/api/v2/spans") 74 | traceTags := make(map[string]string) 75 | traceTags["availability_zone"] = "us-east-1" 76 | tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(traceTags)) 77 | if err != nil { 78 | e.Logger.Fatalf("tracing init failed: %s", err.Error()) 79 | } 80 | //.... 81 | e.GET("/echo", func(c echo.Context) error { 82 | proxyURL, _ := url.Parse("https://echo.labstack.com/") 83 | httputil.NewSingleHostReverseProxy(proxyURL) 84 | return nil 85 | }, zipkintracing.TraceProxy(tracer)) 86 | 87 | defer reporter.Close() //close reporter 88 | e.Logger.Fatal(e.Start(":8080")) 89 | 90 | } 91 | ``` 92 | 93 | ### Trace function calls 94 | 95 | To trace function calls e.g. to trace `s3Func` 96 | 97 | ```go 98 | package main 99 | 100 | import ( 101 | "github.com/labstack/echo-contrib/zipkintracing" 102 | "github.com/labstack/echo/v4" 103 | "github.com/openzipkin/zipkin-go" 104 | ) 105 | 106 | func s3Func(c echo.Context, tracer *zipkin.Tracer) { 107 | defer zipkintracing.TraceFunc(c, "s3_read", zipkintracing.DefaultSpanTags, tracer)() 108 | //s3Func logic here... 109 | } 110 | ``` 111 | 112 | ### Create Child Span 113 | 114 | ```go 115 | package main 116 | 117 | import ( 118 | "github.com/labstack/echo-contrib/zipkintracing" 119 | "github.com/labstack/echo/v4" 120 | "github.com/openzipkin/zipkin-go" 121 | ) 122 | 123 | func traceWithChildSpan(c echo.Context, tracer *zipkin.Tracer) { 124 | span := zipkintracing.StartChildSpan(c, "someMethod", tracer) 125 | //func logic..... 126 | span.Finish() 127 | } 128 | ``` -------------------------------------------------------------------------------- /zipkintracing/response_writer.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package zipkintracing 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "net" 10 | "net/http" 11 | ) 12 | 13 | // ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about 14 | // the response. It is recommended that middleware handlers use this construct to wrap a response writer 15 | // if the functionality calls for it. 16 | type ResponseWriter interface { 17 | http.ResponseWriter 18 | http.Flusher 19 | // Status returns the status code of the response or 0 if the response has 20 | // not been written 21 | Status() int 22 | // Written returns whether or not the ResponseWriter has been written. 23 | Written() bool 24 | // Size returns the size of the response body. 25 | Size() int 26 | // Before allows for a function to be called before the ResponseWriter has been written to. This is 27 | // useful for setting headers or any other operations that must happen before a response has been written. 28 | Before(func(ResponseWriter)) 29 | } 30 | 31 | type beforeFunc func(ResponseWriter) 32 | 33 | // NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter 34 | func NewResponseWriter(rw http.ResponseWriter) ResponseWriter { 35 | nrw := &responseWriter{ 36 | ResponseWriter: rw, 37 | } 38 | 39 | return nrw 40 | } 41 | 42 | type responseWriter struct { 43 | http.ResponseWriter 44 | status int 45 | size int 46 | beforeFuncs []beforeFunc 47 | } 48 | 49 | func (rw *responseWriter) WriteHeader(s int) { 50 | rw.status = s 51 | rw.callBefore() 52 | rw.ResponseWriter.WriteHeader(s) 53 | } 54 | 55 | func (rw *responseWriter) Write(b []byte) (int, error) { 56 | if !rw.Written() { 57 | // The status will be StatusOK if WriteHeader has not been called yet 58 | rw.WriteHeader(http.StatusOK) 59 | } 60 | size, err := rw.ResponseWriter.Write(b) 61 | rw.size += size 62 | return size, err 63 | } 64 | 65 | func (rw *responseWriter) Status() int { 66 | return rw.status 67 | } 68 | 69 | func (rw *responseWriter) Size() int { 70 | return rw.size 71 | } 72 | 73 | func (rw *responseWriter) Written() bool { 74 | return rw.status != 0 75 | } 76 | 77 | func (rw *responseWriter) Before(before func(ResponseWriter)) { 78 | rw.beforeFuncs = append(rw.beforeFuncs, before) 79 | } 80 | 81 | func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 82 | hijacker, ok := rw.ResponseWriter.(http.Hijacker) 83 | if !ok { 84 | return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") 85 | } 86 | return hijacker.Hijack() 87 | } 88 | 89 | func (rw *responseWriter) callBefore() { 90 | for i := len(rw.beforeFuncs) - 1; i >= 0; i-- { 91 | rw.beforeFuncs[i](rw) 92 | } 93 | } 94 | 95 | func (rw *responseWriter) Flush() { 96 | flusher, ok := rw.ResponseWriter.(http.Flusher) 97 | if ok { 98 | if !rw.Written() { 99 | // The status will be StatusOK if WriteHeader has not been called yet 100 | rw.WriteHeader(http.StatusOK) 101 | } 102 | flusher.Flush() 103 | } 104 | } 105 | 106 | func (rw *responseWriter) CloseNotify() <-chan bool { 107 | //lint:ignore SA1019 we support it for backwards compatibility reasons 108 | return rw.ResponseWriter.(http.CloseNotifier).CloseNotify() 109 | } 110 | -------------------------------------------------------------------------------- /zipkintracing/tracing.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package zipkintracing 5 | 6 | import ( 7 | "fmt" 8 | "github.com/labstack/echo/v4/middleware" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/openzipkin/zipkin-go" 14 | zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" 15 | "github.com/openzipkin/zipkin-go/model" 16 | "github.com/openzipkin/zipkin-go/propagation/b3" 17 | ) 18 | 19 | type ( 20 | 21 | //Tags func to adds span tags 22 | Tags func(c echo.Context) map[string]string 23 | 24 | //TraceProxyConfig config for TraceProxyWithConfig 25 | TraceProxyConfig struct { 26 | Skipper middleware.Skipper 27 | Tracer *zipkin.Tracer 28 | SpanTags Tags 29 | } 30 | 31 | //TraceServerConfig config for TraceServerWithConfig 32 | TraceServerConfig struct { 33 | Skipper middleware.Skipper 34 | Tracer *zipkin.Tracer 35 | SpanTags Tags 36 | } 37 | ) 38 | 39 | var ( 40 | //DefaultSpanTags default span tags 41 | DefaultSpanTags = func(c echo.Context) map[string]string { 42 | return make(map[string]string) 43 | } 44 | 45 | //DefaultTraceProxyConfig default config for Trace Proxy 46 | DefaultTraceProxyConfig = TraceProxyConfig{Skipper: middleware.DefaultSkipper, SpanTags: DefaultSpanTags} 47 | 48 | //DefaultTraceServerConfig default config for Trace Server 49 | DefaultTraceServerConfig = TraceServerConfig{Skipper: middleware.DefaultSkipper, SpanTags: DefaultSpanTags} 50 | ) 51 | 52 | // DoHTTP is a http zipkin tracer implementation of HTTPDoer 53 | func DoHTTP(c echo.Context, r *http.Request, client *zipkinhttp.Client) (*http.Response, error) { 54 | req := r.WithContext(c.Request().Context()) 55 | return client.DoWithAppSpan(req, req.Method) 56 | } 57 | 58 | // TraceFunc wraps function call with span so that we can trace time taken by func, eventContext only provided if we want to store trace headers 59 | func TraceFunc(c echo.Context, spanName string, spanTags Tags, tracer *zipkin.Tracer) func() { 60 | span, _ := tracer.StartSpanFromContext(c.Request().Context(), spanName) 61 | for key, value := range spanTags(c) { 62 | span.Tag(key, value) 63 | } 64 | 65 | finishSpan := func() { 66 | span.Finish() 67 | } 68 | 69 | return finishSpan 70 | } 71 | 72 | // TraceProxy middleware that traces reverse proxy 73 | func TraceProxy(tracer *zipkin.Tracer) echo.MiddlewareFunc { 74 | config := DefaultTraceProxyConfig 75 | config.Tracer = tracer 76 | return TraceProxyWithConfig(config) 77 | } 78 | 79 | // TraceProxyWithConfig middleware that traces reverse proxy 80 | func TraceProxyWithConfig(config TraceProxyConfig) echo.MiddlewareFunc { 81 | return func(next echo.HandlerFunc) echo.HandlerFunc { 82 | return func(c echo.Context) error { 83 | if config.Skipper(c) { 84 | return next(c) 85 | } 86 | var parentContext model.SpanContext 87 | if span := zipkin.SpanFromContext(c.Request().Context()); span != nil { 88 | parentContext = span.Context() 89 | } 90 | span := config.Tracer.StartSpan(fmt.Sprintf("C %s %s", c.Request().Method, "reverse proxy"), zipkin.Parent(parentContext)) 91 | for key, value := range config.SpanTags(c) { 92 | span.Tag(key, value) 93 | } 94 | defer span.Finish() 95 | ctx := zipkin.NewContext(c.Request().Context(), span) 96 | c.SetRequest(c.Request().WithContext(ctx)) 97 | b3.InjectHTTP(c.Request())(span.Context()) 98 | nrw := NewResponseWriter(c.Response().Writer) 99 | if err := next(c); err != nil { 100 | c.Error(err) 101 | } 102 | if nrw.Size() > 0 { 103 | zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(int64(nrw.Size()), 10)) 104 | } 105 | if nrw.Status() < 200 || nrw.Status() > 299 { 106 | statusCode := strconv.FormatInt(int64(nrw.Status()), 10) 107 | zipkin.TagHTTPStatusCode.Set(span, statusCode) 108 | if nrw.Status() > 399 { 109 | zipkin.TagError.Set(span, statusCode) 110 | } 111 | } 112 | return nil 113 | } 114 | } 115 | } 116 | 117 | // TraceServer middleware that traces server calls 118 | func TraceServer(tracer *zipkin.Tracer) echo.MiddlewareFunc { 119 | config := DefaultTraceServerConfig 120 | config.Tracer = tracer 121 | return TraceServerWithConfig(config) 122 | } 123 | 124 | // TraceServerWithConfig middleware that traces server calls 125 | func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc { 126 | return func(next echo.HandlerFunc) echo.HandlerFunc { 127 | return func(c echo.Context) error { 128 | if config.Skipper(c) { 129 | return next(c) 130 | } 131 | sc := config.Tracer.Extract(b3.ExtractHTTP(c.Request())) 132 | span := config.Tracer.StartSpan(fmt.Sprintf("S %s %s", c.Request().Method, c.Request().URL.Path), zipkin.Parent(sc)) 133 | for key, value := range config.SpanTags(c) { 134 | span.Tag(key, value) 135 | } 136 | defer span.Finish() 137 | ctx := zipkin.NewContext(c.Request().Context(), span) 138 | c.SetRequest(c.Request().WithContext(ctx)) 139 | nrw := NewResponseWriter(c.Response().Writer) 140 | if err := next(c); err != nil { 141 | c.Error(err) 142 | } 143 | 144 | if nrw.Size() > 0 { 145 | zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(int64(nrw.Size()), 10)) 146 | } 147 | if nrw.Status() < 200 || nrw.Status() > 299 { 148 | statusCode := strconv.FormatInt(int64(nrw.Status()), 10) 149 | zipkin.TagHTTPStatusCode.Set(span, statusCode) 150 | if nrw.Status() > 399 { 151 | zipkin.TagError.Set(span, statusCode) 152 | } 153 | } 154 | return nil 155 | } 156 | } 157 | } 158 | 159 | // StartChildSpan starts a new child span as child of parent span from context 160 | // user must call defer childSpan.Finish() 161 | func StartChildSpan(c echo.Context, spanName string, tracer *zipkin.Tracer) (childSpan zipkin.Span) { 162 | var parentContext model.SpanContext 163 | 164 | if span := zipkin.SpanFromContext(c.Request().Context()); span != nil { 165 | parentContext = span.Context() 166 | } 167 | childSpan = tracer.StartSpan(spanName, zipkin.Parent(parentContext)) 168 | return childSpan 169 | } 170 | -------------------------------------------------------------------------------- /zipkintracing/tracing_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors 3 | 4 | package zipkintracing 5 | 6 | import ( 7 | "encoding/json" 8 | "github.com/labstack/echo/v4" 9 | "github.com/labstack/echo/v4/middleware" 10 | "github.com/openzipkin/zipkin-go" 11 | zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" 12 | "github.com/openzipkin/zipkin-go/propagation/b3" 13 | "github.com/openzipkin/zipkin-go/reporter" 14 | "io/ioutil" 15 | "net/http" 16 | "net/http/httptest" 17 | "testing" 18 | "time" 19 | 20 | zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | type zipkinSpanRequest struct { 25 | ID string 26 | TraceID string 27 | Timestamp uint64 28 | Name string 29 | LocalEndpoint struct { 30 | ServiceName string 31 | } 32 | Tags map[string]string 33 | } 34 | 35 | // DefaultTracer returns zipkin tracer with defaults for testing 36 | func DefaultTracer(reportingURL, serviceName string, tags map[string]string) (*zipkin.Tracer, reporter.Reporter, error) { 37 | endpoint, err := zipkin.NewEndpoint(serviceName, "") 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | reporter := zipkinHttpReporter.NewReporter(reportingURL) 42 | tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(tags)) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | return tracer, reporter, nil 47 | } 48 | 49 | func TestDoHTTTP(t *testing.T) { 50 | done := make(chan struct{}) 51 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | defer close(done) 53 | 54 | body, err := ioutil.ReadAll(r.Body) 55 | assert.NoError(t, err) 56 | 57 | var spans []zipkinSpanRequest 58 | err = json.Unmarshal(body, &spans) 59 | assert.NoError(t, err) 60 | 61 | assert.NotEmpty(t, spans[0].ID) 62 | assert.NotEmpty(t, spans[0].TraceID) 63 | assert.Equal(t, "http/get", spans[0].Name) 64 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 65 | })) 66 | defer ts.Close() 67 | 68 | echoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | assert.Equal(t, r.Method, http.MethodGet) 70 | assert.NotEmpty(t, r.Header.Get(b3.TraceID)) 71 | assert.NotEmpty(t, r.Header.Get(b3.SpanID)) 72 | })) 73 | defer echoServer.Close() 74 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", nil) 75 | req := httptest.NewRequest(http.MethodGet, echoServer.URL, nil) 76 | req.RequestURI = "" 77 | rec := httptest.NewRecorder() 78 | assert.NoError(t, err) 79 | e := echo.New() 80 | c := e.NewContext(req, rec) 81 | client, err := zipkinhttp.NewClient(tracer) 82 | assert.NoError(t, err) 83 | _, err = DoHTTP(c, req, client) 84 | assert.NoError(t, err) 85 | err = reporter.Close() 86 | assert.NoError(t, err) 87 | 88 | select { 89 | case <-done: 90 | case <-time.After(time.Millisecond * 1500): 91 | t.Fatalf("Test server did not receive spans") 92 | } 93 | } 94 | 95 | func TestTraceFunc(t *testing.T) { 96 | done := make(chan struct{}) 97 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 | defer close(done) 99 | 100 | body, err := ioutil.ReadAll(r.Body) 101 | assert.NoError(t, err) 102 | 103 | var spans []zipkinSpanRequest 104 | err = json.Unmarshal(body, &spans) 105 | assert.NoError(t, err) 106 | assert.NotEmpty(t, spans[0].ID) 107 | assert.NotEmpty(t, spans[0].TraceID) 108 | assert.Equal(t, "s3_read", spans[0].Name) 109 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 110 | assert.NotNil(t, spans[0].Tags["availability_zone"]) 111 | assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) 112 | })) 113 | defer ts.Close() 114 | e := echo.New() 115 | req := httptest.NewRequest("GET", "http://localhost:8080/echo", nil) 116 | rec := httptest.NewRecorder() 117 | c := e.NewContext(req, rec) 118 | traceTags := make(map[string]string) 119 | traceTags["availability_zone"] = "us-east-1" 120 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 121 | assert.NoError(t, err) 122 | s3func := func(name string) { 123 | TraceFunc(c, "s3_read", DefaultSpanTags, tracer)() 124 | assert.Equal(t, "s3Test", name) 125 | } 126 | s3func("s3Test") 127 | err = reporter.Close() 128 | assert.NoError(t, err) 129 | select { 130 | case <-done: 131 | case <-time.After(time.Millisecond * 15500): 132 | t.Fatalf("Test server did not receive spans") 133 | } 134 | } 135 | 136 | func TestTraceProxy(t *testing.T) { 137 | done := make(chan struct{}) 138 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 139 | defer close(done) 140 | 141 | body, err := ioutil.ReadAll(r.Body) 142 | assert.NoError(t, err) 143 | 144 | var spans []zipkinSpanRequest 145 | err = json.Unmarshal(body, &spans) 146 | assert.NoError(t, err) 147 | 148 | assert.NotEmpty(t, spans[0].ID) 149 | assert.NotEmpty(t, spans[0].TraceID) 150 | assert.Equal(t, "c get reverse proxy", spans[0].Name) 151 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 152 | assert.NotNil(t, spans[0].Tags["availability_zone"]) 153 | assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) 154 | })) 155 | defer ts.Close() 156 | traceTags := make(map[string]string) 157 | traceTags["availability_zone"] = "us-east-1" 158 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 159 | req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) 160 | rec := httptest.NewRecorder() 161 | e := echo.New() 162 | c := e.NewContext(req, rec) 163 | mw := TraceProxy(tracer) 164 | h := mw(func(c echo.Context) error { 165 | return nil 166 | }) 167 | err = h(c) 168 | assert.NoError(t, err) 169 | assert.NotEmpty(t, req.Header.Get(b3.TraceID)) 170 | assert.NotEmpty(t, req.Header.Get(b3.SpanID)) 171 | 172 | err = reporter.Close() 173 | assert.NoError(t, err) 174 | 175 | select { 176 | case <-done: 177 | case <-time.After(time.Millisecond * 1500): 178 | t.Fatalf("Test server did not receive spans") 179 | } 180 | } 181 | 182 | func TestTraceServer(t *testing.T) { 183 | done := make(chan struct{}) 184 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 | defer close(done) 186 | 187 | body, err := ioutil.ReadAll(r.Body) 188 | assert.NoError(t, err) 189 | 190 | var spans []zipkinSpanRequest 191 | err = json.Unmarshal(body, &spans) 192 | assert.NoError(t, err) 193 | 194 | assert.NotEmpty(t, spans[0].ID) 195 | assert.NotEmpty(t, spans[0].TraceID) 196 | assert.Equal(t, "s get /accounts/acctrefid/transactions", spans[0].Name) 197 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 198 | assert.NotNil(t, spans[0].Tags["availability_zone"]) 199 | assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) 200 | })) 201 | defer ts.Close() 202 | traceTags := make(map[string]string) 203 | traceTags["availability_zone"] = "us-east-1" 204 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 205 | req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) 206 | rec := httptest.NewRecorder() 207 | mw := TraceServer(tracer) 208 | h := mw(func(c echo.Context) error { 209 | return nil 210 | }) 211 | assert.NoError(t, err) 212 | e := echo.New() 213 | c := e.NewContext(req, rec) 214 | err = h(c) 215 | err = reporter.Close() 216 | assert.NoError(t, err) 217 | 218 | select { 219 | case <-done: 220 | case <-time.After(time.Millisecond * 1500): 221 | t.Fatalf("Test server did not receive spans") 222 | } 223 | } 224 | 225 | func TestTraceServerWithConfig(t *testing.T) { 226 | done := make(chan struct{}) 227 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 228 | defer close(done) 229 | 230 | body, err := ioutil.ReadAll(r.Body) 231 | assert.NoError(t, err) 232 | 233 | var spans []zipkinSpanRequest 234 | err = json.Unmarshal(body, &spans) 235 | assert.NoError(t, err) 236 | 237 | assert.NotEmpty(t, spans[0].ID) 238 | assert.NotEmpty(t, spans[0].TraceID) 239 | assert.Equal(t, "s get /accounts/acctrefid/transactions", spans[0].Name) 240 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 241 | assert.NotNil(t, spans[0].Tags["availability_zone"]) 242 | assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) 243 | assert.NotNil(t, spans[0].Tags["Client-Correlation-Id"]) 244 | assert.Equal(t, "c98404736319", spans[0].Tags["Client-Correlation-Id"]) 245 | 246 | })) 247 | defer ts.Close() 248 | traceTags := make(map[string]string) 249 | traceTags["availability_zone"] = "us-east-1" 250 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 251 | req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) 252 | req.Header.Add("Client-Correlation-Id", "c98404736319") 253 | rec := httptest.NewRecorder() 254 | tags := func(c echo.Context) map[string]string { 255 | tags := make(map[string]string) 256 | correlationID := c.Request().Header.Get("Client-Correlation-Id") 257 | tags["Client-Correlation-Id"] = correlationID 258 | return tags 259 | } 260 | config := TraceServerConfig{Skipper: middleware.DefaultSkipper, SpanTags: tags, Tracer: tracer} 261 | mw := TraceServerWithConfig(config) 262 | h := mw(func(c echo.Context) error { 263 | return nil 264 | }) 265 | assert.NoError(t, err) 266 | e := echo.New() 267 | c := e.NewContext(req, rec) 268 | err = h(c) 269 | err = reporter.Close() 270 | assert.NoError(t, err) 271 | select { 272 | case <-done: 273 | case <-time.After(time.Millisecond * 1500): 274 | t.Fatalf("Test server did not receive spans") 275 | } 276 | } 277 | 278 | func TestTraceServerWithConfigSkipper(t *testing.T) { 279 | done := make(chan struct{}) 280 | neverCalled := false 281 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 282 | defer close(done) 283 | body, err := ioutil.ReadAll(r.Body) 284 | assert.NoError(t, err) 285 | var spans []zipkinSpanRequest 286 | err = json.Unmarshal(body, &spans) 287 | assert.NoError(t, err) 288 | })) 289 | defer ts.Close() 290 | traceTags := make(map[string]string) 291 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 292 | traceTags["availability_zone"] = "us-east-1" 293 | req := httptest.NewRequest("GET", "http://localhost:8080/health", nil) 294 | rec := httptest.NewRecorder() 295 | config := TraceServerConfig{Skipper: func(c echo.Context) bool { 296 | return c.Request().URL.Path == "/health" 297 | }, Tracer: tracer} 298 | mw := TraceServerWithConfig(config) 299 | h := mw(func(c echo.Context) error { 300 | return nil 301 | }) 302 | assert.NoError(t, err) 303 | e := echo.New() 304 | c := e.NewContext(req, rec) 305 | err = h(c) 306 | err = reporter.Close() 307 | assert.NoError(t, err) 308 | select { 309 | case <-done: 310 | case <-time.After(time.Millisecond * 500): 311 | neverCalled = true 312 | } 313 | assert.True(t, neverCalled) 314 | } 315 | 316 | func TestStartChildSpan(t *testing.T) { 317 | done := make(chan struct{}) 318 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 319 | defer close(done) 320 | 321 | body, err := ioutil.ReadAll(r.Body) 322 | assert.NoError(t, err) 323 | 324 | var spans []zipkinSpanRequest 325 | err = json.Unmarshal(body, &spans) 326 | assert.NoError(t, err) 327 | assert.NotEmpty(t, spans[0].ID) 328 | assert.NotEmpty(t, spans[0].TraceID) 329 | assert.Equal(t, "kinesis-test", spans[0].Name) 330 | assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) 331 | assert.NotNil(t, spans[0].Tags["availability_zone"]) 332 | assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) 333 | })) 334 | defer ts.Close() 335 | traceTags := make(map[string]string) 336 | traceTags["availability_zone"] = "us-east-1" 337 | tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) 338 | assert.NoError(t, err) 339 | 340 | req := httptest.NewRequest("GET", "http://localhost:8080/health", nil) 341 | rec := httptest.NewRecorder() 342 | e := echo.New() 343 | c := e.NewContext(req, rec) 344 | 345 | childSpan := StartChildSpan(c, "kinesis-test", tracer) 346 | time.Sleep(500) 347 | childSpan.Finish() 348 | assert.NoError(t, err) 349 | err = reporter.Close() 350 | assert.NoError(t, err) 351 | select { 352 | case <-done: 353 | case <-time.After(time.Millisecond * 15500): 354 | t.Fatalf("Test server did not receive spans") 355 | } 356 | } 357 | --------------------------------------------------------------------------------