Repository: labstack/echo-contrib Branch: master Commit: 0203010ac803 Files: 33 Total size: 116.9 KB Directory structure: gitextract_lu6se63q/ ├── .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 ├── internal/ │ └── helpers/ │ └── statuscode.go ├── jaegertracing/ │ ├── jaegertracing.go │ ├── jaegertracing_test.go │ └── response_dumper.go ├── pprof/ │ ├── README.md │ ├── pprof.go │ └── pprof_test.go ├── session/ │ ├── session.go │ └── session_test.go └── zipkintracing/ ├── README.md ├── response_writer.go ├── tracing.go └── tracing_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Issue Description ### Checklist - [ ] Dependencies installed - [ ] No typos - [ ] Searched existing issues and docs ### Expected behaviour ### Actual behaviour ### Steps to reproduce ### Working code to debug ```go package main func main() { } ``` ### Version/commit ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 30 # Issues with these labels will never be considered stale exemptLabels: - pinned - security - bug - enhancement # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed within a month if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/echo-contrib.yml ================================================ name: Run Tests on: push: branches: - master pull_request: branches: - master workflow_dispatch: permissions: contents: read # to fetch code (actions/checkout) env: # run coverage and benchmarks only with the latest Go version LATEST_GO_VERSION: "1.26" jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy # Echo CORE tests with last four major releases (unless there are pressing vulnerabilities) # As we depend on MANY DIFFERENT libraries which of SOME support last 2 Go releases we could have situations when # we derive from last four major releases promise. go: ["1.25", "1.26"] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - name: Checkout Code uses: actions/checkout@v6 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} - name: Run Tests run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v5 with: token: fail_ci_if_error: false benchmark: needs: test name: Benchmark comparison runs-on: ubuntu-latest steps: - name: Checkout Code (Previous) uses: actions/checkout@v6 with: ref: ${{ github.base_ref }} path: previous - name: Checkout Code (New) uses: actions/checkout@v6 with: path: new - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v6 with: go-version: ${{ env.LATEST_GO_VERSION }} - name: Install Dependencies run: go install golang.org/x/perf/cmd/benchstat@latest - name: Run Benchmark (Previous) run: | cd previous go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt - name: Run Benchmark (New) run: | cd new go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt - name: Run Benchstat run: | benchstat previous/benchmark.txt new/benchmark.txt ================================================ FILE: .gitignore ================================================ vendor _test coverage.txt .idea ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 LabStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ PKG := "github.com/labstack/echo-contrib" PKG_LIST := $(shell go list ${PKG}/...) .DEFAULT_GOAL := check check: lint vet race ## Check project init: @go install honnef.co/go/tools/cmd/staticcheck@latest format: ## Format the source code @find ./ -type f -name "*.go" -exec gofmt -w {} \; lint: ## Lint the files @staticcheck -tests=false ${PKG_LIST} vet: ## Vet the files @go vet ${PKG_LIST} test: ## Run tests @go test -short ${PKG_LIST} race: ## Run tests with data race detector @go test -race ${PKG_LIST} benchmark: ## Run benchmarks @go test -run="-" -bench=".*" ${PKG_LIST} help: ## Display this help screen @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' goversion ?= "1.18" test_version: ## Run tests inside Docker with given version (defaults to 1.18 oldest supported). Example: make test_version goversion=1.18 @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make race" ================================================ FILE: README.md ================================================ # Echo Community Contribution middlewares [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo-contrib) [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo-contrib.svg?style=flat-square)](https://codecov.io/gh/labstack/echo-contrib) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) * [Official website](https://echo.labstack.com) * [All middleware docs](https://echo.labstack.com/docs/category/middleware) ## Deprecations / alternatives 1. Prometheus middleware has a new separate repository - https://github.com/labstack/echo-prometheus 2. Jaeger middleware is deprecated, use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + [OTLP exporters](https://opentelemetry.io/docs/languages/go/exporters/). 3. Zipkin middleware is deprecated, use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + [OTLP exporters](https://opentelemetry.io/docs/languages/go/exporters/). ## Usage For Echo `v5` support: ```bash go get github.com/labstack/echo-contrib/v5 ``` ## Versioning This repository does not use semantic versioning. MAJOR version tracks which Echo version should be used. MINOR version tracks API changes (possibly backwards incompatible, which is a very rare occasion), and a PATCH version is incremented for fixes. > **Always add at least one integration test in your project.** Minimal needed Echo versions: * `v5.x.y` needs Echo `v5.0.0+`, use `go get github.com/labstack/echo-contrib/v5@latest` * `v0.18.0` needs Echo `v4.15.0+`, use `go get github.com/labstack/echo-contrib@v0` For `v0.x.y` releases the code is located in `v4` branch. # Supported Go version Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy [Echo CORE](https://github.com/labstack/echo) tests with last FOUR major releases (unless there are pressing vulnerabilities) As this library depends on MANY DIFFERENT libraries which of SOME support only last 2 Go releases we could have situations when we derive from last four major releases promise. 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/ ================================================ FILE: casbin/README.md ================================================ # Usage Simple example: ```go package main import ( "log/slog" "github.com/casbin/casbin/v2" casbin_mw "github.com/labstack/echo-contrib/v5/casbin" "github.com/labstack/echo/v5" ) func main() { e := echo.New() // Mediate the access for every request enforcer, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") if err != nil { slog.Error("failed to load casbin enforcer", "error", err) } e.Use(casbin_mw.Middleware(enforcer)) if err := e.Start(":1323"); err != nil { slog.Error("failed to start server", "error", err) } } ``` Advanced example: ```go package main import ( "log/slog" "github.com/casbin/casbin/v2" casbin_mw "github.com/labstack/echo-contrib/v5/casbin" "github.com/labstack/echo/v5" ) func main() { ce, _ := casbin.NewEnforcer("auth_model.conf", "") ce.AddRoleForUser("alice", "admin") ce.AddPolicy(...) e := echo.New() e.Use(casbin_mw.Middleware(ce)) if err := e.Start(":1323"); err != nil { slog.Error("failed to start server", "error", err) } } ``` # API Reference See [API Overview](https://casbin.org/docs/api-overview). ================================================ FILE: casbin/auth_model.conf ================================================ [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") ================================================ FILE: casbin/auth_policy.csv ================================================ p, alice, /dataset1/*, GET p, alice, /dataset1/resource1, POST p, bob, /dataset2/resource1, * p, bob, /dataset2/resource2, GET p, bob, /dataset2/folder1/*, POST p, dataset1_admin, /dataset1/*, * g, cathy, dataset1_admin ================================================ FILE: casbin/broken_auth_model.conf ================================================ [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") ================================================ FILE: casbin/casbin.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors /* Package casbin provides middleware to enable ACL, RBAC, ABAC authorization support. Simple example: package main import ( "github.com/casbin/casbin/v2" "github.com/labstack/echo/v5" casbin_mw "github.com/labstack/echo-contrib/v5/casbin" ) func main() { e := echo.New() // Mediate the access for every request e.Use(casbin_mw.Middleware(casbin.NewEnforcer("auth_model.conf", "auth_policy.csv"))) e.Logger.Fatal(e.Start(":1323")) } Advanced example: package main import ( "github.com/casbin/casbin/v2" "github.com/labstack/echo/v5" casbin_mw "github.com/labstack/echo-contrib/v5/casbin" ) func main() { ce, _ := casbin.NewEnforcer("auth_model.conf", "") ce.AddRoleForUser("alice", "admin") ce.AddPolicy(...) e := echo.New() e.Use(casbin_mw.Middleware(ce)) e.Logger.Fatal(e.Start(":1323")) } */ package casbin import ( "errors" "net/http" "github.com/casbin/casbin/v2" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" ) type ( // Config defines the config for CasbinAuth middleware. Config struct { // Skipper defines a function to skip middleware. Skipper middleware.Skipper // Enforcer CasbinAuth main rule. // One of Enforcer or EnforceHandler fields is required. Enforcer *casbin.Enforcer // EnforceHandler is custom callback to handle enforcing. // One of Enforcer or EnforceHandler fields is required. EnforceHandler func(c *echo.Context, user string) (bool, error) // Method to get the username - defaults to using basic auth UserGetter func(c *echo.Context) (string, error) // Method to handle errors ErrorHandler func(c *echo.Context, internal error, proposedStatus int) error } ) var ( // DefaultConfig is the default CasbinAuth middleware config. DefaultConfig = Config{ Skipper: middleware.DefaultSkipper, UserGetter: func(c *echo.Context) (string, error) { username, _, _ := c.Request().BasicAuth() return username, nil }, ErrorHandler: func(c *echo.Context, internal error, proposedStatus int) error { return echo.NewHTTPError(proposedStatus, internal.Error()).Wrap(internal) }, } ) // Middleware returns a CasbinAuth middleware. // // For valid credentials it calls the next handler. // For missing or invalid credentials, it sends "401 - Unauthorized" response. func Middleware(ce *casbin.Enforcer) echo.MiddlewareFunc { c := DefaultConfig c.Enforcer = ce return MiddlewareWithConfig(c) } // MiddlewareWithConfig returns a CasbinAuth middleware with config. // See `Middleware()`. func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { if config.Enforcer == nil && config.EnforceHandler == nil { panic("one of casbin middleware Enforcer or EnforceHandler fields must be set") } if config.Skipper == nil { config.Skipper = DefaultConfig.Skipper } if config.UserGetter == nil { config.UserGetter = DefaultConfig.UserGetter } if config.ErrorHandler == nil { config.ErrorHandler = DefaultConfig.ErrorHandler } if config.EnforceHandler == nil { config.EnforceHandler = func(c *echo.Context, user string) (bool, error) { return config.Enforcer.Enforce(user, c.Request().URL.Path, c.Request().Method) } } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } user, err := config.UserGetter(c) if err != nil { return config.ErrorHandler(c, err, http.StatusForbidden) } pass, err := config.EnforceHandler(c, user) if err != nil { return config.ErrorHandler(c, err, http.StatusInternalServerError) } if !pass { return config.ErrorHandler(c, errors.New("enforce did not pass"), http.StatusForbidden) } return next(c) } } } ================================================ FILE: casbin/casbin_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package casbin import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/casbin/casbin/v2" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" ) func testRequest(t *testing.T, h echo.HandlerFunc, user string, path string, method string, code int) { e := echo.New() req := httptest.NewRequest(method, path, nil) req.SetBasicAuth(user, "secret") res := httptest.NewRecorder() c := e.NewContext(req, res) err := h(c) if err != nil { var errObj *echo.HTTPError if errors.As(err, &errObj) { if errObj.Code != code { t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, errObj.Code, code) } } } else { status := 0 if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { status = eResp.Status } if status != code { t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, status, code) } } } func TestAuth(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "alice", "/dataset1/resource1", http.MethodGet, http.StatusOK) testRequest(t, h, "alice", "/dataset1/resource1", http.MethodPost, http.StatusOK) testRequest(t, h, "alice", "/dataset1/resource2", http.MethodGet, http.StatusOK) testRequest(t, h, "alice", "/dataset1/resource2", http.MethodPost, http.StatusForbidden) } func TestPathWildcard(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "bob", "/dataset2/resource1", http.MethodGet, http.StatusOK) testRequest(t, h, "bob", "/dataset2/resource1", http.MethodPost, http.StatusOK) testRequest(t, h, "bob", "/dataset2/resource1", http.MethodDelete, http.StatusOK) testRequest(t, h, "bob", "/dataset2/resource2", http.MethodGet, http.StatusOK) testRequest(t, h, "bob", "/dataset2/resource2", http.MethodPost, http.StatusForbidden) testRequest(t, h, "bob", "/dataset2/resource2", http.MethodDelete, http.StatusForbidden) testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodGet, http.StatusForbidden) testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodPost, http.StatusOK) testRequest(t, h, "bob", "/dataset2/folder1/item1", http.MethodDelete, http.StatusForbidden) testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodGet, http.StatusForbidden) testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodPost, http.StatusOK) testRequest(t, h, "bob", "/dataset2/folder1/item2", http.MethodDelete, http.StatusForbidden) } func TestRBAC(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) // cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role. testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusOK) testRequest(t, h, "cathy", "/dataset1/item", http.MethodPost, http.StatusOK) testRequest(t, h, "cathy", "/dataset1/item", http.MethodDelete, http.StatusOK) testRequest(t, h, "cathy", "/dataset2/item", http.MethodGet, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset2/item", http.MethodPost, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset2/item", http.MethodDelete, http.StatusForbidden) // delete all roles on user cathy, so cathy cannot access any resources now. ce.DeleteRolesForUser("cathy") testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset1/item", http.MethodPost, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset1/item", http.MethodDelete, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset2/item", http.MethodGet, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset2/item", http.MethodPost, http.StatusForbidden) testRequest(t, h, "cathy", "/dataset2/item", http.MethodDelete, http.StatusForbidden) } func TestEnforceError(t *testing.T) { ce, _ := casbin.NewEnforcer("broken_auth_model.conf", "auth_policy.csv") h := Middleware(ce)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusInternalServerError) } func TestCustomUserGetter(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") cnf := Config{ Skipper: middleware.DefaultSkipper, Enforcer: ce, UserGetter: func(c *echo.Context) (string, error) { return "not_cathy_at_all", nil }, } h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) } func TestUserGetterError(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") cnf := Config{ Skipper: middleware.DefaultSkipper, Enforcer: ce, UserGetter: func(c *echo.Context) (string, error) { return "", errors.New("no idea who you are") }, } h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "cathy", "/dataset1/item", http.MethodGet, http.StatusForbidden) } func TestCustomEnforceHandler(t *testing.T) { ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") assert.NoError(t, err) _, err = ce.AddPolicy("bob", "/user/bob", "PATCH_SELF") assert.NoError(t, err) cnf := Config{ EnforceHandler: func(c *echo.Context, user string) (bool, error) { method := c.Request().Method if strings.HasPrefix(c.Request().URL.Path, "/user/bob") { method += "_SELF" } return ce.Enforce(user, c.Request().URL.Path, method) }, } h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "bob", "/dataset2/resource1", http.MethodGet, http.StatusOK) testRequest(t, h, "bob", "/user/alice", http.MethodPatch, http.StatusForbidden) testRequest(t, h, "bob", "/user/bob", http.MethodPatch, http.StatusOK) } func TestCustomSkipper(t *testing.T) { ce, _ := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") cnf := Config{ Skipper: func(c *echo.Context) bool { return c.Request().URL.Path == "/dataset1/resource1" }, Enforcer: ce, } h := MiddlewareWithConfig(cnf)(func(c *echo.Context) error { return c.String(http.StatusOK, "test") }) testRequest(t, h, "alice", "/dataset1/resource1", http.MethodGet, http.StatusOK) testRequest(t, h, "alice", "/dataset1/resource2", http.MethodPost, http.StatusForbidden) } ================================================ FILE: codecov.yml ================================================ coverage: status: project: default: threshold: 1% patch: default: threshold: 1% comment: require_changes: true ================================================ FILE: echo.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package echo ================================================ FILE: echoprometheus/README.md ================================================ # Usage Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters ```go package main import ( "log/slog" "github.com/labstack/echo-contrib/v5/echoprometheus" "github.com/labstack/echo/v5" ) func main() { e := echo.New() // Enable metrics middleware e.Use(echoprometheus.NewMiddleware("myapp")) e.GET("/metrics", echoprometheus.NewHandler()) if err := e.Start(":1323"); err != nil { slog.Error("failed to start server", "error", err) } } ``` ================================================ FILE: echoprometheus/prometheus.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors /* Package echoprometheus provides middleware to add Prometheus metrics. Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters */ package echoprometheus import ( "bytes" "context" "errors" "fmt" "io" "net/http" "sort" "strconv" "strings" "time" "github.com/labstack/echo-contrib/v5/internal/helpers" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/expfmt" ) const ( defaultSubsystem = "echo" ) const ( _ = iota // ignore first value by assigning to blank identifier bKB float64 = 1 << (10 * iota) bMB ) // sizeBuckets is the buckets for request/response size. Here we define a spectrum from 1KB through 1NB up to 10MB. 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} // MiddlewareConfig contains the configuration for creating prometheus middleware collecting several default metrics. type MiddlewareConfig struct { // Skipper defines a function to skip middleware. Skipper middleware.Skipper // Namespace is components of the fully-qualified name of the Metric (created by joining Namespace,Subsystem and Name components with "_") // Optional Namespace string // Subsystem is components of the fully-qualified name of the Metric (created by joining Namespace,Subsystem and Name components with "_") // Defaults to: "echo" Subsystem string // LabelFuncs allows adding custom labels in addition to default labels. When key has same name with default label // it replaces default one. LabelFuncs map[string]LabelValueFunc // HistogramOptsFunc allows to change options for metrics of type histogram before metric is registered to Registerer HistogramOptsFunc func(opts prometheus.HistogramOpts) prometheus.HistogramOpts // CounterOptsFunc allows to change options for metrics of type counter before metric is registered to Registerer CounterOptsFunc func(opts prometheus.CounterOpts) prometheus.CounterOpts // Registerer sets the prometheus.Registerer instance the middleware will register these metrics with. // Defaults to: prometheus.DefaultRegisterer Registerer prometheus.Registerer // BeforeNext is callback that is executed before next middleware/handler is called. Useful for case when you have own // metrics that need data to be stored for AfterNext. BeforeNext func(c *echo.Context) // AfterNext is callback that is executed after next middleware/handler returns. Useful for case when you have own // metrics that need incremented/observed. AfterNext func(c *echo.Context, err error) timeNow func() time.Time // If DoNotUseRequestPathFor404 is true, all 404 responses (due to non-matching route) will have the same `url` label and // thus won't generate new metrics. DoNotUseRequestPathFor404 bool // StatusCodeResolver resolves err & context into http status code. Default is to use context.Response().Status StatusCodeResolver func(c *echo.Context, err error) int } type LabelValueFunc func(c *echo.Context, err error) string // HandlerConfig contains the configuration for creating HTTP handler for metrics. type HandlerConfig struct { // Gatherer sets the prometheus.Gatherer instance the middleware will use when generating the metric endpoint handler. // Defaults to: prometheus.DefaultGatherer Gatherer prometheus.Gatherer } // PushGatewayConfig contains the configuration for pushing to a Prometheus push gateway. type PushGatewayConfig struct { // PushGatewayURL is push gateway URL in format http://domain:port PushGatewayURL string // PushInterval in ticker interval for pushing gathered metrics to the Gateway // Defaults to: 1 minute PushInterval time.Duration // Gatherer sets the prometheus.Gatherer instance the middleware will use when generating the metric endpoint handler. // Defaults to: prometheus.DefaultGatherer Gatherer prometheus.Gatherer // ErrorHandler is function that is called when errors occur. When callback returns error StartPushGateway also returns. ErrorHandler func(err error) error // ClientTransport specifies the mechanism by which individual HTTP POST requests are made. // Defaults to: http.DefaultTransport ClientTransport http.RoundTripper } // NewHandler creates new instance of Handler using Prometheus default registry. // // Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters func NewHandler() echo.HandlerFunc { return NewHandlerWithConfig(HandlerConfig{}) } // NewHandlerWithConfig creates new instance of Handler using given configuration. func NewHandlerWithConfig(config HandlerConfig) echo.HandlerFunc { if config.Gatherer == nil { config.Gatherer = prometheus.DefaultGatherer } h := promhttp.HandlerFor(config.Gatherer, promhttp.HandlerOpts{DisableCompression: true}) if r, ok := config.Gatherer.(prometheus.Registerer); ok { h = promhttp.InstrumentMetricHandler(r, h) } return func(c *echo.Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } } // NewMiddleware creates new instance of middleware using Prometheus default registry. // // Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters func NewMiddleware(subsystem string) echo.MiddlewareFunc { return NewMiddlewareWithConfig(MiddlewareConfig{Subsystem: subsystem}) } // NewMiddlewareWithConfig creates new instance of middleware using given configuration. // // Deprecated: use [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters func NewMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc { mw, err := config.ToMiddleware() if err != nil { panic(err) } return mw } // ToMiddleware converts configuration to middleware or returns an error. // // Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if conf.timeNow == nil { conf.timeNow = time.Now } if conf.Subsystem == "" { conf.Subsystem = defaultSubsystem } if conf.Registerer == nil { conf.Registerer = prometheus.DefaultRegisterer } if conf.CounterOptsFunc == nil { conf.CounterOptsFunc = func(opts prometheus.CounterOpts) prometheus.CounterOpts { return opts } } if conf.HistogramOptsFunc == nil { conf.HistogramOptsFunc = func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { return opts } } if conf.StatusCodeResolver == nil { conf.StatusCodeResolver = helpers.DefaultStatusResolver } labelNames, customValuers := createLabels(conf.LabelFuncs) requestCount := prometheus.NewCounterVec( conf.CounterOptsFunc(prometheus.CounterOpts{ Namespace: conf.Namespace, Subsystem: conf.Subsystem, Name: "requests_total", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", }), labelNames, ) // we do not allow skipping or replacing default collector but developer can use `conf.CounterOptsFunc` to rename // this middleware default collector, so they can have own collector with that same name. // and we treat all register errors as returnable failures if err := conf.Registerer.Register(requestCount); err != nil { return nil, err } requestDuration := prometheus.NewHistogramVec( conf.HistogramOptsFunc(prometheus.HistogramOpts{ Namespace: conf.Namespace, Subsystem: conf.Subsystem, Name: "request_duration_seconds", Help: "The HTTP request latencies in seconds.", // 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} Buckets: prometheus.DefBuckets, }), labelNames, ) if rErr := conf.Registerer.Register(requestDuration); rErr != nil { return nil, rErr } responseSize := prometheus.NewHistogramVec( conf.HistogramOptsFunc(prometheus.HistogramOpts{ Namespace: conf.Namespace, Subsystem: conf.Subsystem, Name: "response_size_bytes", Help: "The HTTP response sizes in bytes.", Buckets: sizeBuckets, }), labelNames, ) if err := conf.Registerer.Register(responseSize); err != nil { return nil, err } requestSize := prometheus.NewHistogramVec( conf.HistogramOptsFunc(prometheus.HistogramOpts{ Namespace: conf.Namespace, Subsystem: conf.Subsystem, Name: "request_size_bytes", Help: "The HTTP request sizes in bytes.", Buckets: sizeBuckets, }), labelNames, ) if rErr := conf.Registerer.Register(requestSize); rErr != nil { return nil, rErr } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { // NB: we do not skip metrics handler path by default. This can be added with custom Skipper but for default // behaviour we measure metrics path request/response metrics also if conf.Skipper != nil && conf.Skipper(c) { return next(c) } if conf.BeforeNext != nil { conf.BeforeNext(c) } reqSz := computeApproximateRequestSize(c.Request()) start := conf.timeNow() err := next(c) elapsed := float64(conf.timeNow().Sub(start)) / float64(time.Second) if conf.AfterNext != nil { conf.AfterNext(c, err) } url := c.Path() // contains route path ala `/users/:id` if url == "" && !conf.DoNotUseRequestPathFor404 { // as of Echo v4.10.1 path is empty for 404 cases (when router did not find any matching routes) // in this case we use actual path from request to have some distinction in Prometheus url = c.Request().URL.Path } status := conf.StatusCodeResolver(c, err) values := make([]string, len(labelNames)) values[0] = strconv.Itoa(status) values[1] = c.Request().Method values[2] = c.Request().Host values[3] = strings.ToValidUTF8(url, "\uFFFD") // \uFFFD is � https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character for _, cv := range customValuers { values[cv.index] = cv.valueFunc(c, err) } if obs, err := requestDuration.GetMetricWithLabelValues(values...); err == nil { obs.Observe(elapsed) } else { return fmt.Errorf("failed to label request duration metric with values, err: %w", err) } if obs, err := requestCount.GetMetricWithLabelValues(values...); err == nil { obs.Inc() } else { return fmt.Errorf("failed to label request count metric with values, err: %w", err) } if obs, err := requestSize.GetMetricWithLabelValues(values...); err == nil { obs.Observe(float64(reqSz)) } else { return fmt.Errorf("failed to label request size metric with values, err: %w", err) } if obs, err := responseSize.GetMetricWithLabelValues(values...); err == nil { if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { obs.Observe(float64(eResp.Size)) } } else { return fmt.Errorf("failed to label response size metric with values, err: %w", err) } return err } }, nil } type customLabelValuer struct { index int label string valueFunc LabelValueFunc } func createLabels(customLabelFuncs map[string]LabelValueFunc) ([]string, []customLabelValuer) { labelNames := []string{"code", "method", "host", "url"} if len(customLabelFuncs) == 0 { return labelNames, nil } customValuers := make([]customLabelValuer, 0) // we create valuers in two passes for a reason - first to get fixed order, and then we know to assign correct indexes for label, labelFunc := range customLabelFuncs { customValuers = append(customValuers, customLabelValuer{ label: label, valueFunc: labelFunc, }) } sort.Slice(customValuers, func(i, j int) bool { return customValuers[i].label < customValuers[j].label }) for cvIdx, cv := range customValuers { idx := containsAt(labelNames, cv.label) if idx == -1 { idx = len(labelNames) labelNames = append(labelNames, cv.label) } customValuers[cvIdx].index = idx } return labelNames, customValuers } func containsAt[K comparable](haystack []K, needle K) int { for i, v := range haystack { if v == needle { return i } } return -1 } func computeApproximateRequestSize(r *http.Request) int { s := 0 if r.URL != nil { s = len(r.URL.Path) } s += len(r.Method) s += len(r.Proto) for name, values := range r.Header { s += len(name) for _, value := range values { s += len(value) } } s += len(r.Host) // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. if r.ContentLength != -1 { s += int(r.ContentLength) } return s } // RunPushGatewayGatherer starts pushing collected metrics and waits for it context to complete or ErrorHandler to return error. // // Example: // ``` // // go func() { // config := echoprometheus.PushGatewayConfig{ // PushGatewayURL: "https://host:9080", // PushInterval: 10 * time.Millisecond, // } // if err := echoprometheus.RunPushGatewayGatherer(context.Background(), config); !errors.Is(err, context.Canceled) { // log.Fatal(err) // } // }() // // ``` // // Deprecated: use new repository [echo-prometheus middleware](https://github.com/labstack/echo-prometheus) or [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters func RunPushGatewayGatherer(ctx context.Context, config PushGatewayConfig) error { if config.PushGatewayURL == "" { return errors.New("push gateway URL is missing") } if config.PushInterval <= 0 { config.PushInterval = 1 * time.Minute } if config.Gatherer == nil { config.Gatherer = prometheus.DefaultGatherer } if config.ErrorHandler == nil { config.ErrorHandler = func(err error) error { return nil } } client := &http.Client{ Transport: config.ClientTransport, } out := &bytes.Buffer{} ticker := time.NewTicker(config.PushInterval) defer ticker.Stop() for { select { case <-ticker.C: out.Reset() err := WriteGatheredMetrics(out, config.Gatherer) if err != nil { if hErr := config.ErrorHandler(fmt.Errorf("failed to create metrics: %w", err)); hErr != nil { return hErr } continue } req, err := http.NewRequestWithContext(ctx, http.MethodPost, config.PushGatewayURL, out) if err != nil { if hErr := config.ErrorHandler(fmt.Errorf("failed to create push gateway request: %w", err)); hErr != nil { return hErr } continue } res, err := client.Do(req) if err != nil { if hErr := config.ErrorHandler(fmt.Errorf("error sending to push gateway: %w", err)); hErr != nil { return hErr } } if res.StatusCode != http.StatusOK { if hErr := config.ErrorHandler(echo.NewHTTPError(res.StatusCode, "post metrics request did not succeed")); hErr != nil { return hErr } } case <-ctx.Done(): return ctx.Err() } } } // WriteGatheredMetrics gathers collected metrics and writes them to given writer func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer) error { metricFamilies, err := gatherer.Gather() if err != nil { return err } for _, mf := range metricFamilies { if _, err := expfmt.MetricFamilyToText(writer, mf); err != nil { return err } } return nil } ================================================ FILE: echoprometheus/prometheus_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package echoprometheus import ( "bytes" "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/labstack/echo/v5" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" ) func TestCustomRegistryMetrics(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Registerer: customRegistry})) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) assert.Equal(t, http.StatusNotFound, request(e, "/ping?test=1")) s, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, s, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) } func TestDefaultRegistryMetrics(t *testing.T) { e := echo.New() e.Use(NewMiddleware("myapp")) e.GET("/metrics", NewHandler()) assert.Equal(t, http.StatusNotFound, request(e, "/ping?test=1")) s, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, s, `myapp_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) unregisterDefaults("myapp") } func TestPrometheus_Buckets(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Registerer: customRegistry})) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) assert.Equal(t, http.StatusNotFound, request(e, "/ping")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) 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)") 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)") 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") 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)") 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") 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)") } func TestMiddlewareConfig_Skipper(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ Skipper: func(c *echo.Context) bool { hasSuffix := strings.HasSuffix(c.Path(), "ignore") return hasSuffix }, Registerer: customRegistry, })) e.GET("/test", func(c *echo.Context) error { return c.String(http.StatusOK, "OK") }) e.GET("/test_ignore", func(c *echo.Context) error { return c.String(http.StatusOK, "OK") }) assert.Equal(t, http.StatusNotFound, request(e, "/ping")) assert.Equal(t, http.StatusOK, request(e, "/test")) assert.Equal(t, http.StatusOK, request(e, "/test_ignore")) out := &bytes.Buffer{} assert.NoError(t, WriteGatheredMetrics(out, customRegistry)) body := out.String() assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="GET",url="/test"} 1`) assert.Contains(t, body, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) assert.Contains(t, body, `echo_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/ping"} 1`) assert.NotContains(t, body, `test_ignore`) // because we skipped } func TestMetricsForErrors(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ Skipper: func(c *echo.Context) bool { return strings.HasSuffix(c.Path(), "ignore") }, Subsystem: "myapp", Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/handler_for_ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) e.GET("/handler_for_nok", func(c *echo.Context) error { return c.JSON(http.StatusConflict, "NOK") }) e.GET("/handler_for_error", func(c *echo.Context) error { return echo.NewHTTPError(http.StatusBadGateway, "BAD") }) assert.Equal(t, http.StatusOK, request(e, "/handler_for_ok")) assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) assert.Equal(t, http.StatusBadGateway, request(e, "/handler_for_error")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, body, fmt.Sprintf("%s_requests_total", "myapp")) assert.Contains(t, body, `myapp_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) assert.Contains(t, body, `myapp_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 2`) assert.Contains(t, body, `myapp_requests_total{code="502",host="example.com",method="GET",url="/handler_for_error"} 1`) } func TestMiddlewareConfig_LabelFuncs(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ LabelFuncs: map[string]LabelValueFunc{ "scheme": func(c *echo.Context, err error) string { // additional custom label return c.Scheme() }, "method": func(c *echo.Context, err error) string { // overrides default 'method' label value return "overridden_" + c.Request().Method }, }, Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) assert.Equal(t, http.StatusOK, request(e, "/ok")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="overridden_GET",scheme="http",url="/ok"} 1`) } func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() customResolver := func(c *echo.Context, err error) int { if err == nil { if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { return eResp.Status } return http.StatusOK } msg := err.Error() if strings.Contains(msg, "NOT FOUND") { return http.StatusNotFound } if strings.Contains(msg, "NOT Authorized") { return http.StatusUnauthorized } return http.StatusInternalServerError } e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ Skipper: func(c *echo.Context) bool { return strings.HasSuffix(c.Path(), "ignore") }, Subsystem: "myapp", Registerer: customRegistry, StatusCodeResolver: customResolver, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/handler_for_ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) e.GET("/handler_for_nok", func(c *echo.Context) error { return c.JSON(http.StatusConflict, "NOK") }) e.GET("/handler_for_not_found", func(c *echo.Context) error { return errors.New("NOT FOUND") }) e.GET("/handler_for_not_authorized", func(c *echo.Context) error { return errors.New("NOT Authorized") }) e.GET("/handler_for_unknown_error", func(c *echo.Context) error { return errors.New("i do not know") }) assert.Equal(t, http.StatusOK, request(e, "/handler_for_ok")) assert.Equal(t, http.StatusConflict, request(e, "/handler_for_nok")) assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_found")) assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_not_authorized")) assert.Equal(t, http.StatusInternalServerError, request(e, "/handler_for_unknown_error")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, body, fmt.Sprintf("%s_requests_total", "myapp")) assert.Contains(t, body, `myapp_requests_total{code="200",host="example.com",method="GET",url="/handler_for_ok"} 1`) assert.Contains(t, body, `myapp_requests_total{code="409",host="example.com",method="GET",url="/handler_for_nok"} 1`) assert.Contains(t, body, `myapp_requests_total{code="404",host="example.com",method="GET",url="/handler_for_not_found"} 1`) assert.Contains(t, body, `myapp_requests_total{code="401",host="example.com",method="GET",url="/handler_for_not_authorized"} 1`) assert.Contains(t, body, `myapp_requests_total{code="500",host="example.com",method="GET",url="/handler_for_unknown_error"} 1`) } func TestMiddlewareConfig_HistogramOptsFunc(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { if opts.Name == "request_duration_seconds" { opts.ConstLabels = prometheus.Labels{"my_const": "123"} } return opts }, Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) assert.Equal(t, http.StatusOK, request(e, "/ok")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) // has const label assert.Contains(t, body, `echo_request_duration_seconds_count{code="200",host="example.com",method="GET",my_const="123",url="/ok"} 1`) // does not have const label assert.Contains(t, body, `echo_request_size_bytes_count{code="200",host="example.com",method="GET",url="/ok"} 1`) } func TestMiddlewareConfig_CounterOptsFunc(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { if opts.Name == "requests_total" { opts.ConstLabels = prometheus.Labels{"my_const": "123"} } return opts }, Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) assert.Equal(t, http.StatusOK, request(e, "/ok")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) // has const label assert.Contains(t, body, `echo_requests_total{code="200",host="example.com",method="GET",my_const="123",url="/ok"} 1`) // does not have const label assert.Contains(t, body, `echo_request_size_bytes_count{code="200",host="example.com",method="GET",url="/ok"} 1`) } func TestMiddlewareConfig_AfterNextFuncs(t *testing.T) { e := echo.New() customRegistry := prometheus.NewRegistry() customCounter := prometheus.NewCounter( prometheus.CounterOpts{ Name: "custom_requests_total", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", }, ) if err := customRegistry.Register(customCounter); err != nil { t.Fatal(err) } e.Use(NewMiddlewareWithConfig(MiddlewareConfig{ AfterNext: func(c *echo.Context, err error) { customCounter.Inc() // use our custom metric in middleware }, Registerer: customRegistry, })) e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) e.GET("/ok", func(c *echo.Context) error { return c.JSON(http.StatusOK, "OK") }) assert.Equal(t, http.StatusOK, request(e, "/ok")) body, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, body, `custom_requests_total 1`) } func TestRunPushGatewayGatherer(t *testing.T) { receivedMetrics := false svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedMetrics = true w.WriteHeader(http.StatusBadRequest) w.Write([]byte("OK")) })) defer svr.Close() ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() config := PushGatewayConfig{ PushGatewayURL: svr.URL, PushInterval: 10 * time.Millisecond, ErrorHandler: func(err error) error { return err // to force return after first request }, } err := RunPushGatewayGatherer(ctx, config) assert.EqualError(t, err, "code=400, message=post metrics request did not succeed") assert.True(t, receivedMetrics) unregisterDefaults("myapp") } // TestSetPathFor404NoMatchingRoute tests that the url is not included in the metric when // the 404 response is due to no matching route func TestSetPathFor404NoMatchingRoute(t *testing.T) { e := echo.New() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{DoNotUseRequestPathFor404: true, Subsystem: defaultSubsystem})) e.GET("/metrics", NewHandler()) assert.Equal(t, http.StatusNotFound, request(e, "/nonExistentPath")) s, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.Contains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url=""} 1`, defaultSubsystem)) assert.NotContains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/nonExistentPath"} 1`, defaultSubsystem)) unregisterDefaults(defaultSubsystem) } // TestSetPathFor404Logic tests that the url is included in the metric when the 404 response is due to logic func TestSetPathFor404Logic(t *testing.T) { unregisterDefaults("myapp") e := echo.New() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{DoNotUseRequestPathFor404: true, Subsystem: defaultSubsystem})) e.GET("/metrics", NewHandler()) e.GET("/sample", func(c *echo.Context) error { return echo.ErrNotFound }) assert.Equal(t, http.StatusNotFound, request(e, "/sample")) s, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) assert.NotContains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url=""} 1`, defaultSubsystem)) assert.Contains(t, s, fmt.Sprintf(`%s_request_duration_seconds_count{code="404",host="example.com",method="GET",url="/sample"} 1`, defaultSubsystem)) unregisterDefaults(defaultSubsystem) } func TestInvalidUTF8PathIsFixed(t *testing.T) { e := echo.New() e.Use(NewMiddlewareWithConfig(MiddlewareConfig{Subsystem: defaultSubsystem})) e.GET("/metrics", NewHandler()) assert.Equal(t, http.StatusNotFound, request(e, "/../../WEB-INF/web.xml\xc0\x80.jsp")) s, code := requestBody(e, "/metrics") assert.Equal(t, http.StatusOK, code) 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)) unregisterDefaults(defaultSubsystem) } func requestBody(e *echo.Echo, path string) (string, int) { req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) return rec.Body.String(), rec.Code } func request(e *echo.Echo, path string) int { _, code := requestBody(e, path) return code } func unregisterDefaults(subsystem string) { // this is extremely hacky way to unregister our middleware metrics that it registers to prometheus default registry // Metrics/collector can be unregistered only by their instance but we do not have their instance, so we need to // create similar collector to register it and get error back with that existing collector we actually want to // unregister p := prometheus.DefaultRegisterer unRegisterCollector := func(opts prometheus.Opts) { dummyDuplicate := prometheus.NewCounterVec(prometheus.CounterOpts(opts), []string{"code", "method", "host", "url"}) err := p.Register(dummyDuplicate) if err == nil { return } var arErr prometheus.AlreadyRegisteredError if errors.As(err, &arErr) { p.Unregister(arErr.ExistingCollector) } } unRegisterCollector(prometheus.Opts{ Subsystem: subsystem, Name: "requests_total", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", }) unRegisterCollector(prometheus.Opts{ Subsystem: subsystem, Name: "request_duration_seconds", Help: "The HTTP request latencies in seconds.", }) unRegisterCollector(prometheus.Opts{ Subsystem: subsystem, Name: "response_size_bytes", Help: "The HTTP response sizes in bytes.", }) unRegisterCollector(prometheus.Opts{ Subsystem: subsystem, Name: "request_size_bytes", Help: "The HTTP request sizes in bytes.", }) } ================================================ FILE: go.mod ================================================ module github.com/labstack/echo-contrib/v5 go 1.25.0 require ( github.com/casbin/casbin/v2 v2.135.0 github.com/gorilla/sessions v1.4.0 github.com/labstack/echo/v5 v5.0.4 github.com/opentracing/opentracing-go v1.2.0 github.com/openzipkin/zipkin-go v0.4.3 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.5 github.com/stretchr/testify v1.11.1 github.com/uber/jaeger-client-go v2.30.0+incompatible ) require ( github.com/HdrHistogram/hdrhistogram-go v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/HdrHistogram/hdrhistogram-go v1.2.0 h1:XMJkDWuz6bM9Fzy7zORuVFKH7ZJY41G2q8KWhVGkNiY= github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/helpers/statuscode.go ================================================ package helpers import ( "errors" "net/http" "github.com/labstack/echo/v5" ) // DefaultStatusResolver resolves http status code from given err or Response. func DefaultStatusResolver(c *echo.Context, err error) int { status := 0 var sc echo.HTTPStatusCoder if errors.As(err, &sc) { return sc.StatusCode() } if eResp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { if eResp.Committed { status = eResp.Status } } if err != nil && status == 0 { status = http.StatusInternalServerError } return status } ================================================ FILE: jaegertracing/jaegertracing.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors /* Package jaegertracing provides middleware to Opentracing using Jaeger. Example: ``` package main import ( "github.com/labstack/echo-contrib/v5/jaegertracing" "github.com/labstack/echo/v5" ) func main() { e := echo.New() // Enable tracing middleware c := jaegertracing.New(e, nil) defer c.Close() e.Logger.Fatal(e.Start(":1323")) } ``` Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go */ package jaegertracing import ( "bytes" "crypto/rand" "errors" "fmt" "io" "net/http" "reflect" "runtime" "time" "github.com/labstack/echo-contrib/v5/internal/helpers" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/uber/jaeger-client-go/config" ) const defaultComponentName = "echo/v5" type ( // TraceConfig defines the config for Trace middleware. TraceConfig struct { // Skipper defines a function to skip middleware. Skipper middleware.Skipper // OpenTracing Tracer instance which should be got before Tracer opentracing.Tracer // ComponentName used for describing the tracing component name ComponentName string // add req body & resp body to tracing tags IsBodyDump bool // prevent logging long http request bodies LimitHTTPBody bool // http body limit size (in bytes) // NOTE: don't specify values larger than 60000 as jaeger can't handle values in span.LogKV larger than 60000 bytes LimitSize int // OperationNameFunc composes operation name based on context. Can be used to override default naming OperationNameFunc func(c *echo.Context) string } ) var ( // DefaultTraceConfig is the default Trace middleware config. DefaultTraceConfig = TraceConfig{ Skipper: middleware.DefaultSkipper, ComponentName: defaultComponentName, IsBodyDump: false, LimitHTTPBody: true, LimitSize: 60_000, OperationNameFunc: defaultOperationName, } ) // New creates an Opentracing tracer and attaches it to Echo middleware. // Returns Closer do be added to caller function as `defer closer.Close()` // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func New(e *echo.Echo, skipper middleware.Skipper) io.Closer { // Add Opentracing instrumentation defcfg := config.Configuration{ ServiceName: "echo-tracer", Sampler: &config.SamplerConfig{ Type: "const", Param: 1, }, Reporter: &config.ReporterConfig{ LogSpans: true, BufferFlushInterval: 1 * time.Second, }, } cfg, err := defcfg.FromEnv() if err != nil { panic("Could not parse Jaeger env vars: " + err.Error()) } tracer, closer, err := cfg.NewTracer() if err != nil { panic("Could not initialize jaeger tracer: " + err.Error()) } opentracing.SetGlobalTracer(tracer) e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, Skipper: skipper, })) return closer } // Trace returns a Trace middleware. // Trace middleware traces http requests and reporting errors. // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func Trace(tracer opentracing.Tracer) echo.MiddlewareFunc { c := DefaultTraceConfig c.Tracer = tracer c.ComponentName = defaultComponentName return TraceWithConfig(c) } // TraceWithConfig returns a Trace middleware with config. // See: `Trace()`. // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc { if config.Tracer == nil { panic("echo: trace middleware requires opentracing tracer") } if config.Skipper == nil { config.Skipper = middleware.DefaultSkipper } if config.ComponentName == "" { config.ComponentName = defaultComponentName } if config.OperationNameFunc == nil { config.OperationNameFunc = defaultOperationName } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } req := c.Request() opname := config.OperationNameFunc(c) realIP := c.RealIP() requestID := getRequestID(c) // request-id generated by reverse-proxy var sp opentracing.Span var err error ctx, err := config.Tracer.Extract( opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header), ) if err != nil { sp = config.Tracer.StartSpan(opname) } else { sp = config.Tracer.StartSpan(opname, ext.RPCServerOption(ctx)) } defer sp.Finish() ext.HTTPMethod.Set(sp, req.Method) ext.HTTPUrl.Set(sp, req.URL.String()) ext.Component.Set(sp, config.ComponentName) sp.SetTag("client_ip", realIP) sp.SetTag("request_id", requestID) // Dump request & response body var respDumper *responseDumper if config.IsBodyDump { // request reqBody := []byte{} if c.Request().Body != nil { reqBody, _ = io.ReadAll(c.Request().Body) if config.LimitHTTPBody { sp.LogKV("http.req.body", limitString(string(reqBody), config.LimitSize)) } else { sp.LogKV("http.req.body", string(reqBody)) } } req.Body = io.NopCloser(bytes.NewBuffer(reqBody)) // reset original request body // response respDumper = newResponseDumper(c.Response()) c.SetResponse(respDumper) } // setup request context - add opentracing span reqSpan := req.WithContext(opentracing.ContextWithSpan(req.Context(), sp)) c.SetRequest(reqSpan) defer func() { // as we have created new http.Request object we need to make sure that temporary files created to hold MultipartForm // files are cleaned up. This is done by http.Server at the end of request lifecycle but Server does not // have reference to our new Request instance therefore it is our responsibility to fix the mess we caused. // // This means that when we are on returning path from handler middlewares up in chain from this middleware // can not access these temporary files anymore because we deleted them here. if reqSpan.MultipartForm != nil { reqSpan.MultipartForm.RemoveAll() } }() // inject Jaeger context into request header config.Tracer.Inject(sp.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request().Header)) // call next middleware / controller err = next(c) status := helpers.DefaultStatusResolver(c, err) ext.HTTPStatusCode.Set(sp, uint16(status)) if err != nil { logError(sp, err) } // Dump response body if config.IsBodyDump { if config.LimitHTTPBody { sp.LogKV("http.resp.body", limitString(respDumper.GetResponse(), config.LimitSize)) } else { sp.LogKV("http.resp.body", respDumper.GetResponse()) } } return nil // error was already processed with ctx.Error(err) } } } func limitString(str string, size int) string { if len(str) > size { return str[:size/2] + "\n---- skipped ----\n" + str[len(str)-size/2:] } return str } func logError(span opentracing.Span, err error) { var httpError *echo.HTTPError if errors.As(err, &httpError) { span.LogKV("error.message", httpError.Message) } else { span.LogKV("error.message", err.Error()) } span.SetTag("error", true) } func getRequestID(ctx *echo.Context) string { requestID := ctx.Request().Header.Get(echo.HeaderXRequestID) // request-id generated by reverse-proxy if requestID == "" { requestID = generateToken() // missed request-id from proxy, we generate it manually } return requestID } func generateToken() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%x", b) } func defaultOperationName(c *echo.Context) string { req := c.Request() return "HTTP " + req.Method + " URL: " + c.Path() } // TraceFunction wraps funtion with opentracing span adding tags for the function name and caller details // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func TraceFunction(ctx *echo.Context, fn interface{}, params ...interface{}) (result []reflect.Value) { // Get function name name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() // Create child span parentSpan := opentracing.SpanFromContext(ctx.Request().Context()) sp := opentracing.StartSpan( "Function - "+name, opentracing.ChildOf(parentSpan.Context())) defer sp.Finish() sp.SetTag("function", name) // Get caller function name, file and line pc := make([]uintptr, 15) n := runtime.Callers(2, pc) frames := runtime.CallersFrames(pc[:n]) frame, _ := frames.Next() callerDetails := fmt.Sprintf("%s - %s#%d", frame.Function, frame.File, frame.Line) sp.SetTag("caller", callerDetails) // Check params and call function f := reflect.ValueOf(fn) if f.Type().NumIn() != len(params) { e := fmt.Sprintf("Incorrect number of parameters calling wrapped function %s", name) panic(e) } inputs := make([]reflect.Value, len(params)) for k, in := range params { inputs[k] = reflect.ValueOf(in) } return f.Call(inputs) } // CreateChildSpan creates a new opentracing span adding tags for the span name and caller details. // User must call defer `sp.Finish()` // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func CreateChildSpan(ctx *echo.Context, name string) opentracing.Span { parentSpan := opentracing.SpanFromContext(ctx.Request().Context()) sp := opentracing.StartSpan( name, opentracing.ChildOf(parentSpan.Context())) sp.SetTag("name", name) // Get caller function name, file and line pc := make([]uintptr, 15) n := runtime.Callers(2, pc) frames := runtime.CallersFrames(pc[:n]) frame, _ := frames.Next() callerDetails := fmt.Sprintf("%s - %s#%d", frame.Function, frame.File, frame.Line) sp.SetTag("caller", callerDetails) return sp } // NewTracedRequest generates a new traced HTTP request with opentracing headers injected into it // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/jaegertracing/jaeger-client-go func NewTracedRequest(method string, url string, body io.Reader, span opentracing.Span) (*http.Request, error) { req, err := http.NewRequest(method, url, body) if err != nil { panic(err.Error()) } ext.SpanKindRPCClient.Set(span) ext.HTTPUrl.Set(span, url) ext.HTTPMethod.Set(span, method) span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)) return req, err } ================================================ FILE: jaegertracing/jaegertracing_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package jaegertracing import ( "bytes" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v5" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/log" "github.com/stretchr/testify/assert" ) // Mock opentracing.Span type mockSpan struct { tracer opentracing.Tracer tags map[string]interface{} logs map[string]interface{} opName string finished bool } func createSpan(tracer opentracing.Tracer) *mockSpan { return &mockSpan{ tracer: tracer, tags: make(map[string]interface{}), logs: make(map[string]interface{}), } } func (sp *mockSpan) isFinished() bool { return sp.finished } func (sp *mockSpan) getOpName() string { return sp.opName } func (sp *mockSpan) getTag(key string) interface{} { return sp.tags[key] } func (sp *mockSpan) getLog(key string) interface{} { return sp.logs[key] } func (sp *mockSpan) Finish() { sp.finished = true } func (sp *mockSpan) FinishWithOptions(opts opentracing.FinishOptions) { } func (sp *mockSpan) Context() opentracing.SpanContext { return nil } func (sp *mockSpan) SetOperationName(operationName string) opentracing.Span { sp.opName = operationName return sp } func (sp *mockSpan) SetTag(key string, value interface{}) opentracing.Span { sp.tags[key] = value return sp } func (sp *mockSpan) LogFields(fields ...log.Field) { } func (sp *mockSpan) LogKV(alternatingKeyValues ...interface{}) { for i := 0; i < len(alternatingKeyValues); i += 2 { ikey := alternatingKeyValues[i] value := alternatingKeyValues[i+1] if key, ok := ikey.(string); ok { sp.logs[key] = value } } } func (sp *mockSpan) SetBaggageItem(restrictedKey, value string) opentracing.Span { return sp } func (sp *mockSpan) BaggageItem(restrictedKey string) string { return "" } func (sp *mockSpan) Tracer() opentracing.Tracer { return sp.tracer } func (sp *mockSpan) LogEvent(event string) { } func (sp *mockSpan) LogEventWithPayload(event string, payload interface{}) { } func (sp *mockSpan) Log(data opentracing.LogData) { } // Mock opentracing.Tracer type mockTracer struct { span *mockSpan hasStartSpanWithOption bool } func (tr *mockTracer) currentSpan() *mockSpan { return tr.span } func (tr *mockTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span { tr.hasStartSpanWithOption = len(opts) > 0 if tr.span != nil { tr.span.opName = operationName return tr.span } span := createSpan(tr) span.opName = operationName return span } func (tr *mockTracer) Inject(sm opentracing.SpanContext, format interface{}, carrier interface{}) error { return nil } func (tr *mockTracer) Extract(format interface{}, carrier interface{}) (opentracing.SpanContext, error) { if tr.span != nil { return nil, nil } return nil, errors.New("no span") } func createMockTracer() *mockTracer { tracer := mockTracer{} span := createSpan(&tracer) tracer.span = span return &tracer } func TestTraceWithDefaultConfig(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(Trace(tracer)) e.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "world") }) e.GET("/giveme400", func(c *echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "baaaad request") }) e.GET("/givemeerror", func(c *echo.Context) error { return fmt.Errorf("internal stuff went wrong") }) t.Run("successful call", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/hello", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "GET", tracer.currentSpan().getTag("http.method")) assert.Equal(t, "/hello", tracer.currentSpan().getTag("http.url")) assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) assert.Equal(t, uint16(200), tracer.currentSpan().getTag("http.status_code")) assert.NotEqual(t, true, tracer.currentSpan().getTag("error")) }) t.Run("error from echo", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/idontexist", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "GET", tracer.currentSpan().getTag("http.method")) assert.Equal(t, "/idontexist", tracer.currentSpan().getTag("http.url")) assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) assert.Equal(t, uint16(404), tracer.currentSpan().getTag("http.status_code")) assert.Equal(t, true, tracer.currentSpan().getTag("error")) }) t.Run("custom http error", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/giveme400", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, uint16(400), tracer.currentSpan().getTag("http.status_code")) assert.Equal(t, true, tracer.currentSpan().getTag("error")) assert.Equal(t, "baaaad request", tracer.currentSpan().getLog("error.message")) }) t.Run("unknown error", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/givemeerror", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, uint16(500), tracer.currentSpan().getTag("http.status_code")) assert.Equal(t, true, tracer.currentSpan().getTag("error")) assert.Equal(t, "internal stuff went wrong", tracer.currentSpan().getLog("error.message")) }) } func TestTraceWithConfig(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", })) req := httptest.NewRequest(http.MethodGet, "/trace", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, "/trace", tracer.currentSpan().getTag("http.url")) assert.Equal(t, "EchoTracer", tracer.currentSpan().getTag("component")) assert.Equal(t, true, tracer.hasStartSpanWithOption) } func TestTraceWithConfigOfBodyDump(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", IsBodyDump: true, })) e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi") }) req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString(`{"name": "Lorem"}`)) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, "EchoTracer", tracer.currentSpan().getTag("component")) assert.Equal(t, "/trace", tracer.currentSpan().getTag("http.url")) assert.Equal(t, `{"name": "Lorem"}`, tracer.currentSpan().getLog("http.req.body")) assert.Equal(t, `Hi`, tracer.currentSpan().getLog("http.resp.body")) assert.Equal(t, uint16(200), tracer.currentSpan().getTag("http.status_code")) assert.Equal(t, nil, tracer.currentSpan().getTag("error")) assert.Equal(t, true, tracer.hasStartSpanWithOption) } func TestTraceWithConfigOfNoneComponentName(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, })) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, defaultComponentName, tracer.currentSpan().getTag("component")) } func TestTraceWithConfigOfSkip(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Skipper: func(*echo.Context) bool { return true }, Tracer: tracer, })) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, false, tracer.currentSpan().isFinished()) } func TestTraceOfNoCurrentSpan(t *testing.T) { tracer := &mockTracer{} e := echo.New() e.Use(Trace(tracer)) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, false, tracer.hasStartSpanWithOption) } func TestTraceWithLimitHTTPBody(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", IsBodyDump: true, LimitHTTPBody: true, LimitSize: 10, })) e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi 123456789012345678901234567890") }) req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890")) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, "12345\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.req.body")) assert.Equal(t, "Hi 12\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.resp.body")) } func TestTraceWithoutLimitHTTPBody(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", IsBodyDump: true, LimitHTTPBody: false, // disabled LimitSize: 10, })) e.POST("/trace", func(c *echo.Context) error { return c.String(200, "Hi 123456789012345678901234567890") }) req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890")) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, "123456789012345678901234567890", tracer.currentSpan().getLog("http.req.body")) assert.Equal(t, "Hi 123456789012345678901234567890", tracer.currentSpan().getLog("http.resp.body")) } func TestTraceWithDefaultOperationName(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(Trace(tracer)) e.GET("/trace", func(c *echo.Context) error { return c.String(http.StatusOK, "Hi") }) req := httptest.NewRequest(http.MethodGet, "/trace", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, "HTTP GET URL: /trace", tracer.currentSpan().getOpName()) } func TestTraceWithCustomOperationName(t *testing.T) { tracer := createMockTracer() e := echo.New() e.Use(TraceWithConfig(TraceConfig{ Tracer: tracer, ComponentName: "EchoTracer", OperationNameFunc: func(c *echo.Context) string { // This is an example of operation name customization // In most cases default formatting is more than enough req := c.Request() opName := "HTTP " + req.Method path := c.Path() for _, pv := range c.PathValues() { from := ":" + pv.Name to := "{" + pv.Name + "}" path = strings.ReplaceAll(path, from, to) } return opName + " " + path }, })) e.GET("/trace/:traceID/spans/:spanID", func(c *echo.Context) error { return c.String(http.StatusOK, "Hi") }) req := httptest.NewRequest(http.MethodGet, "/trace/123456/spans/123", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, true, tracer.currentSpan().isFinished()) assert.Equal(t, "HTTP GET /trace/{traceID}/spans/{spanID}", tracer.currentSpan().getOpName()) } ================================================ FILE: jaegertracing/response_dumper.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package jaegertracing import ( "bytes" "io" "net/http" ) type responseDumper struct { http.ResponseWriter mw io.Writer buf *bytes.Buffer } func newResponseDumper(resp http.ResponseWriter) *responseDumper { buf := new(bytes.Buffer) return &responseDumper{ ResponseWriter: resp, mw: io.MultiWriter(resp, buf), buf: buf, } } func (d *responseDumper) Write(b []byte) (int, error) { return d.mw.Write(b) } func (d *responseDumper) GetResponse() string { return d.buf.String() } func (d *responseDumper) Unwrap() http.ResponseWriter { return d.ResponseWriter } ================================================ FILE: pprof/README.md ================================================ Usage ```go package main import ( "log/slog" "github.com/labstack/echo-contrib/v5/pprof" "github.com/labstack/echo/v5" ) func main() { e := echo.New() pprof.Register(e) //...... if err := e.Start(":1323"); err != nil { slog.Error("failed to start server", "error", err) } } ``` - Then use the pprof tool to look at the heap profile: `go tool pprof http://localhost:1323/debug/pprof/heap` - Or to look at a 30-second CPU profile: `go tool pprof http://localhost:1323/debug/pprof/profile?seconds=30` - Or to look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate in your program: `go tool pprof http://localhost:1323/debug/pprof/block` - Or to look at the holders of contended mutexes, after calling runtime.SetMutexProfileFraction in your program: `go tool pprof http://localhost:1323/debug/pprof/mutex` ================================================ FILE: pprof/pprof.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package pprof import ( "net/http" "net/http/pprof" "github.com/labstack/echo/v5" ) const ( // DefaultPrefix url prefix of pprof DefaultPrefix = "/debug/pprof" ) func getPrefix(prefixOptions ...string) string { if len(prefixOptions) > 0 { return prefixOptions[0] } return DefaultPrefix } // Register middleware for net/http/pprof func Register(e *echo.Echo, prefixOptions ...string) { prefix := getPrefix(prefixOptions...) prefixRouter := e.Group(prefix) { prefixRouter.GET("/", handler(pprof.Index)) prefixRouter.GET("/allocs", handler(pprof.Handler("allocs").ServeHTTP)) prefixRouter.GET("/block", handler(pprof.Handler("block").ServeHTTP)) prefixRouter.GET("/cmdline", handler(pprof.Cmdline)) prefixRouter.GET("/goroutine", handler(pprof.Handler("goroutine").ServeHTTP)) prefixRouter.GET("/heap", handler(pprof.Handler("heap").ServeHTTP)) prefixRouter.GET("/mutex", handler(pprof.Handler("mutex").ServeHTTP)) prefixRouter.GET("/profile", handler(pprof.Profile)) prefixRouter.POST("/symbol", handler(pprof.Symbol)) prefixRouter.GET("/symbol", handler(pprof.Symbol)) prefixRouter.GET("/threadcreate", handler(pprof.Handler("threadcreate").ServeHTTP)) prefixRouter.GET("/trace", handler(pprof.Trace)) } } func handler(h http.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } } ================================================ FILE: pprof/pprof_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package pprof import ( "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v5" ) func TestPProfRegisterDefaualtPrefix(t *testing.T) { var pprofPaths = []struct { path string }{ {"/"}, {"/allocs"}, {"/block"}, {"/cmdline"}, {"/goroutine"}, {"/heap"}, {"/mutex"}, {"/profile?seconds=1"}, {"/symbol"}, {"/symbol"}, {"/threadcreate"}, {"/trace"}, } for _, tt := range pprofPaths { t.Run(tt.path, func(t *testing.T) { e := echo.New() Register(e) req, _ := http.NewRequest(http.MethodGet, DefaultPrefix+tt.path, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, rec.Code, http.StatusOK) }) } } func TestPProfRegisterCustomPrefix(t *testing.T) { var pprofPaths = []struct { path string }{ {"/"}, {"/allocs"}, {"/block"}, {"/cmdline"}, {"/goroutine"}, {"/heap"}, {"/mutex"}, {"/profile?seconds=1"}, {"/symbol"}, {"/symbol"}, {"/threadcreate"}, {"/trace"}, } for _, tt := range pprofPaths { t.Run(tt.path, func(t *testing.T) { e := echo.New() pprofPrefix := "/myapp/pprof" Register(e, pprofPrefix) req, _ := http.NewRequest(http.MethodGet, pprofPrefix+tt.path, nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, rec.Code, http.StatusOK) }) } } ================================================ FILE: session/session.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package session import ( "fmt" "github.com/gorilla/sessions" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" ) type ( // Config defines the config for Session middleware. Config struct { // Skipper defines a function to skip middleware. Skipper middleware.Skipper // Session store. // Required. Store sessions.Store } ) const ( key = "_session_store" ) var ( // DefaultConfig is the default Session middleware config. DefaultConfig = Config{ Skipper: middleware.DefaultSkipper, } ) // Get returns a named session. func Get(name string, c *echo.Context) (*sessions.Session, error) { s := c.Get(key) if s == nil { return nil, fmt.Errorf("%q session store not found", key) } store := s.(sessions.Store) return store.Get(c.Request(), name) } // Middleware returns a Session middleware. func Middleware(store sessions.Store) echo.MiddlewareFunc { c := DefaultConfig c.Store = store return MiddlewareWithConfig(c) } // MiddlewareWithConfig returns a Sessions middleware with config. // See `Middleware()`. func MiddlewareWithConfig(config Config) echo.MiddlewareFunc { // Defaults if config.Skipper == nil { config.Skipper = DefaultConfig.Skipper } if config.Store == nil { panic("echo: session middleware requires store") } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } c.Set(key, config.Store) return next(c) } } } ================================================ FILE: session/session_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package session import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/gorilla/sessions" "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) func TestMiddleware(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) handler := func(c *echo.Context) error { sess, _ := Get("test", c) sess.Options.Domain = "labstack.com" sess.Values["foo"] = "bar" if err := sess.Save(c.Request(), c.Response()); err != nil { return err } return c.String(http.StatusOK, "test") } store := sessions.NewCookieStore([]byte("secret")) config := Config{ Skipper: func(c *echo.Context) bool { return true }, Store: store, } // Skipper mw := MiddlewareWithConfig(config) h := mw(func(c *echo.Context) error { return echo.ErrNotFound }) assert.Error(t, h(c)) // 404 assert.Nil(t, c.Get(key)) // Panic config.Skipper = nil config.Store = nil assert.Panics(t, func() { MiddlewareWithConfig(config) }) // Core mw = Middleware(store) h = mw(handler) assert.NoError(t, h(c)) assert.Contains(t, rec.Header().Get(echo.HeaderSetCookie), "labstack.com") } func TestGetSessionMissingStore(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _, err := Get("test", c) assert.EqualError(t, err, fmt.Sprintf("%q session store not found", key)) } ================================================ FILE: zipkintracing/README.md ================================================ # Tracing Library for Go > Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter > `Go app -> OTLP -> OpenTelemetry Collector -> Zipkin` This library provides tracing for go using [Zipkin](https://zipkin.io/) ## Usage ### Server Tracing Middleware & http client tracing ```go package main import ( "io" "log/slog" "net/http" "github.com/labstack/echo-contrib/v5/zipkintracing" "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" ) func main() { e := echo.New() endpoint, err := zipkin.NewEndpoint("echo-service", "") if err != nil { slog.Error("failed to create zipkin endpoint", "error", err) } reporter := zipkinHttpReporter.NewReporter("http://localhost:9411/api/v2/spans") traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(traceTags)) client, _ := zipkinhttp.NewClient(tracer, zipkinhttp.ClientTrace(true)) if err != nil { slog.Error("failed to create tracer", "error", err) } //Wrap & Use trace server middleware, this traces all server calls e.Use(zipkintracing.TraceServer(tracer)) //.... e.GET("/echo", func(c *echo.Context) error { //trace http request calls. req, _ := http.NewRequest("GET", "https://echo.labstack.com/", nil) resp, _ := zipkintracing.DoHTTP(c, req, client) body, _ := io.ReadAll(resp.Body) return c.String(http.StatusOK, string(body)) }) defer reporter.Close() //defer close reporter if err := e.Start(":8080"); err != nil { slog.Error("failed to start server", "error", err) } } ``` ### Reverse Proxy Tracing ```go package main import ( "log/slog" "net/http/httputil" "net/url" "github.com/labstack/echo-contrib/v5/zipkintracing" "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" ) func main() { e := echo.New() //new tracing instance endpoint, err := zipkin.NewEndpoint("echo-service", "") if err != nil { slog.Error("failed to create endpoint", "error", err) } reporter := zipkinHttpReporter.NewReporter("http://localhost:9411/api/v2/spans") traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(traceTags)) if err != nil { slog.Error("failed to create tracer", "error", err) } //.... e.GET("/echo", func(c *echo.Context) error { proxyURL, _ := url.Parse("https://echo.labstack.com/") httputil.NewSingleHostReverseProxy(proxyURL) return nil }, zipkintracing.TraceProxy(tracer)) defer reporter.Close() //close reporter if err := e.Start(":8080"); err != nil { slog.Error("failed to start server", "error", err) } } ``` ### Trace function calls To trace function calls e.g. to trace `s3Func` ```go package main import ( "github.com/labstack/echo-contrib/v5/zipkintracing" "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" ) func s3Func(c *echo.Context, tracer *zipkin.Tracer) { defer zipkintracing.TraceFunc(c, "s3_read", zipkintracing.DefaultSpanTags, tracer)() //s3Func logic here... } ``` ### Create Child Span ```go package main import ( "github.com/labstack/echo-contrib/v5/zipkintracing" "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" ) func traceWithChildSpan(c *echo.Context, tracer *zipkin.Tracer) { span := zipkintracing.StartChildSpan(c, "someMethod", tracer) //func logic..... span.Finish() } ``` ================================================ FILE: zipkintracing/response_writer.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package zipkintracing import ( "bufio" "errors" "net" "net/http" ) // ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about // the response. It is recommended that middleware handlers use this construct to wrap a response writer // if the functionality calls for it. type ResponseWriter interface { http.ResponseWriter http.Flusher // Status returns the status code of the response or 0 if the response has // not been written Status() int // Written returns whether or not the ResponseWriter has been written. Written() bool // Size returns the size of the response body. Size() int // Before allows for a function to be called before the ResponseWriter has been written to. This is // useful for setting headers or any other operations that must happen before a response has been written. Before(func(ResponseWriter)) } type beforeFunc func(ResponseWriter) // NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter func NewResponseWriter(rw http.ResponseWriter) ResponseWriter { nrw := &responseWriter{ ResponseWriter: rw, } return nrw } type responseWriter struct { http.ResponseWriter status int size int beforeFuncs []beforeFunc } func (rw *responseWriter) WriteHeader(s int) { rw.status = s rw.callBefore() rw.ResponseWriter.WriteHeader(s) } func (rw *responseWriter) Write(b []byte) (int, error) { if !rw.Written() { // The status will be StatusOK if WriteHeader has not been called yet rw.WriteHeader(http.StatusOK) } size, err := rw.ResponseWriter.Write(b) rw.size += size return size, err } func (rw *responseWriter) Status() int { return rw.status } func (rw *responseWriter) Size() int { return rw.size } func (rw *responseWriter) Written() bool { return rw.status != 0 } func (rw *responseWriter) Before(before func(ResponseWriter)) { rw.beforeFuncs = append(rw.beforeFuncs, before) } func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker, ok := rw.ResponseWriter.(http.Hijacker) if !ok { return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") } return hijacker.Hijack() } func (rw *responseWriter) callBefore() { for i := len(rw.beforeFuncs) - 1; i >= 0; i-- { rw.beforeFuncs[i](rw) } } func (rw *responseWriter) Flush() { flusher, ok := rw.ResponseWriter.(http.Flusher) if ok { if !rw.Written() { // The status will be StatusOK if WriteHeader has not been called yet rw.WriteHeader(http.StatusOK) } flusher.Flush() } } func (rw *responseWriter) CloseNotify() <-chan bool { //lint:ignore SA1019 we support it for backwards compatibility reasons return rw.ResponseWriter.(http.CloseNotifier).CloseNotify() } func (rw *responseWriter) Unwrap() http.ResponseWriter { return rw.ResponseWriter } ================================================ FILE: zipkintracing/tracing.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package zipkintracing import ( "fmt" "net/http" "strconv" "github.com/labstack/echo/v5/middleware" "github.com/labstack/echo/v5" "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" "github.com/openzipkin/zipkin-go/model" "github.com/openzipkin/zipkin-go/propagation/b3" ) type ( //Tags func to adds span tags Tags func(c *echo.Context) map[string]string //TraceProxyConfig config for TraceProxyWithConfig TraceProxyConfig struct { Skipper middleware.Skipper Tracer *zipkin.Tracer SpanTags Tags } //TraceServerConfig config for TraceServerWithConfig TraceServerConfig struct { Skipper middleware.Skipper Tracer *zipkin.Tracer SpanTags Tags } ) var ( //DefaultSpanTags default span tags DefaultSpanTags = func(c *echo.Context) map[string]string { return make(map[string]string) } //DefaultTraceProxyConfig default config for Trace Proxy DefaultTraceProxyConfig = TraceProxyConfig{Skipper: middleware.DefaultSkipper, SpanTags: DefaultSpanTags} //DefaultTraceServerConfig default config for Trace Server DefaultTraceServerConfig = TraceServerConfig{Skipper: middleware.DefaultSkipper, SpanTags: DefaultSpanTags} ) // DoHTTP is a http zipkin tracer implementation of HTTPDoer // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func DoHTTP(c *echo.Context, r *http.Request, client *zipkinhttp.Client) (*http.Response, error) { req := r.WithContext(c.Request().Context()) return client.DoWithAppSpan(req, req.Method) } // 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 // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func TraceFunc(c *echo.Context, spanName string, spanTags Tags, tracer *zipkin.Tracer) func() { span, _ := tracer.StartSpanFromContext(c.Request().Context(), spanName) for key, value := range spanTags(c) { span.Tag(key, value) } finishSpan := func() { span.Finish() } return finishSpan } // TraceProxy middleware that traces reverse proxy // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func TraceProxy(tracer *zipkin.Tracer) echo.MiddlewareFunc { config := DefaultTraceProxyConfig config.Tracer = tracer return TraceProxyWithConfig(config) } // TraceProxyWithConfig middleware that traces reverse proxy // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func TraceProxyWithConfig(config TraceProxyConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } var parentContext model.SpanContext if span := zipkin.SpanFromContext(c.Request().Context()); span != nil { parentContext = span.Context() } span := config.Tracer.StartSpan(fmt.Sprintf("C %s %s", c.Request().Method, "reverse proxy"), zipkin.Parent(parentContext)) for key, value := range config.SpanTags(c) { span.Tag(key, value) } defer span.Finish() ctx := zipkin.NewContext(c.Request().Context(), span) c.SetRequest(c.Request().WithContext(ctx)) b3.InjectHTTP(c.Request())(span.Context()) nrw := NewResponseWriter(c.Response()) if err := next(c); err != nil { c.Echo().HTTPErrorHandler(c, err) } if nrw.Size() > 0 { zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(int64(nrw.Size()), 10)) } if nrw.Status() < 200 || nrw.Status() > 299 { statusCode := strconv.FormatInt(int64(nrw.Status()), 10) zipkin.TagHTTPStatusCode.Set(span, statusCode) if nrw.Status() > 399 { zipkin.TagError.Set(span, statusCode) } } return nil } } } // TraceServer middleware that traces server calls // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func TraceServer(tracer *zipkin.Tracer) echo.MiddlewareFunc { config := DefaultTraceServerConfig config.Tracer = tracer return TraceServerWithConfig(config) } // TraceServerWithConfig middleware that traces server calls // // Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) instead + OTLP exporters. Read this: https://github.com/openzipkin-contrib/zipkin-otel and https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/zipkinexporter func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { if config.Skipper(c) { return next(c) } sc := config.Tracer.Extract(b3.ExtractHTTP(c.Request())) span := config.Tracer.StartSpan(fmt.Sprintf("S %s %s", c.Request().Method, c.Request().URL.Path), zipkin.Parent(sc)) for key, value := range config.SpanTags(c) { span.Tag(key, value) } defer span.Finish() ctx := zipkin.NewContext(c.Request().Context(), span) c.SetRequest(c.Request().WithContext(ctx)) nrw := NewResponseWriter(c.Response()) if err := next(c); err != nil { c.Echo().HTTPErrorHandler(c, err) } if nrw.Size() > 0 { zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(int64(nrw.Size()), 10)) } if nrw.Status() < 200 || nrw.Status() > 299 { statusCode := strconv.FormatInt(int64(nrw.Status()), 10) zipkin.TagHTTPStatusCode.Set(span, statusCode) if nrw.Status() > 399 { zipkin.TagError.Set(span, statusCode) } } return nil } } } // StartChildSpan starts a new child span as child of parent span from context // user must call defer childSpan.Finish() func StartChildSpan(c *echo.Context, spanName string, tracer *zipkin.Tracer) (childSpan zipkin.Span) { var parentContext model.SpanContext if span := zipkin.SpanFromContext(c.Request().Context()); span != nil { parentContext = span.Context() } childSpan = tracer.StartSpan(spanName, zipkin.Parent(parentContext)) return childSpan } ================================================ FILE: zipkintracing/tracing_test.go ================================================ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors package zipkintracing import ( "encoding/json" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/openzipkin/zipkin-go" zipkinhttp "github.com/openzipkin/zipkin-go/middleware/http" "github.com/openzipkin/zipkin-go/propagation/b3" "github.com/openzipkin/zipkin-go/reporter" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" zipkinHttpReporter "github.com/openzipkin/zipkin-go/reporter/http" "github.com/stretchr/testify/assert" ) type zipkinSpanRequest struct { ID string TraceID string Timestamp uint64 Name string LocalEndpoint struct { ServiceName string } Tags map[string]string } // DefaultTracer returns zipkin tracer with defaults for testing func DefaultTracer(reportingURL, serviceName string, tags map[string]string) (*zipkin.Tracer, reporter.Reporter, error) { endpoint, err := zipkin.NewEndpoint(serviceName, "") if err != nil { return nil, nil, err } reporter := zipkinHttpReporter.NewReporter(reportingURL) tracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithTags(tags)) if err != nil { return nil, nil, err } return tracer, reporter, nil } func TestDoHTTTP(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "http/get", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) })) defer ts.Close() echoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, http.MethodGet) assert.NotEmpty(t, r.Header.Get(b3.TraceID)) assert.NotEmpty(t, r.Header.Get(b3.SpanID)) })) defer echoServer.Close() tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", nil) req := httptest.NewRequest(http.MethodGet, echoServer.URL, nil) req.RequestURI = "" rec := httptest.NewRecorder() assert.NoError(t, err) e := echo.New() c := e.NewContext(req, rec) client, err := zipkinhttp.NewClient(tracer) assert.NoError(t, err) _, err = DoHTTP(c, req, client) assert.NoError(t, err) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 1500): t.Fatalf("Test server did not receive spans") } } func TestTraceFunc(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "s3_read", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) assert.NotNil(t, spans[0].Tags["availability_zone"]) assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) })) defer ts.Close() e := echo.New() req := httptest.NewRequest("GET", "http://localhost:8080/echo", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) assert.NoError(t, err) s3func := func(name string) { TraceFunc(c, "s3_read", DefaultSpanTags, tracer)() assert.Equal(t, "s3Test", name) } s3func("s3Test") err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 15500): t.Fatalf("Test server did not receive spans") } } func TestTraceProxy(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "c get reverse proxy", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) assert.NotNil(t, spans[0].Tags["availability_zone"]) assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) })) defer ts.Close() traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) mw := TraceProxy(tracer) h := mw(func(c *echo.Context) error { return nil }) err = h(c) assert.NoError(t, err) assert.NotEmpty(t, req.Header.Get(b3.TraceID)) assert.NotEmpty(t, req.Header.Get(b3.SpanID)) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 1500): t.Fatalf("Test server did not receive spans") } } func TestTraceServer(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "s get /accounts/acctrefid/transactions", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) assert.NotNil(t, spans[0].Tags["availability_zone"]) assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) })) defer ts.Close() traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) rec := httptest.NewRecorder() mw := TraceServer(tracer) h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err) e := echo.New() c := e.NewContext(req, rec) err = h(c) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 1500): t.Fatalf("Test server did not receive spans") } } func TestTraceServerWithConfig(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "s get /accounts/acctrefid/transactions", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) assert.NotNil(t, spans[0].Tags["availability_zone"]) assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) assert.NotNil(t, spans[0].Tags["Client-Correlation-Id"]) assert.Equal(t, "c98404736319", spans[0].Tags["Client-Correlation-Id"]) })) defer ts.Close() traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) req := httptest.NewRequest("GET", "http://localhost:8080/accounts/acctrefid/transactions", nil) req.Header.Add("Client-Correlation-Id", "c98404736319") rec := httptest.NewRecorder() tags := func(c *echo.Context) map[string]string { tags := make(map[string]string) correlationID := c.Request().Header.Get("Client-Correlation-Id") tags["Client-Correlation-Id"] = correlationID return tags } config := TraceServerConfig{Skipper: middleware.DefaultSkipper, SpanTags: tags, Tracer: tracer} mw := TraceServerWithConfig(config) h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err) e := echo.New() c := e.NewContext(req, rec) err = h(c) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 1500): t.Fatalf("Test server did not receive spans") } } func TestTraceServerWithConfigSkipper(t *testing.T) { done := make(chan struct{}) neverCalled := false ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) })) defer ts.Close() traceTags := make(map[string]string) tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) traceTags["availability_zone"] = "us-east-1" req := httptest.NewRequest("GET", "http://localhost:8080/health", nil) rec := httptest.NewRecorder() config := TraceServerConfig{Skipper: func(c *echo.Context) bool { return c.Request().URL.Path == "/health" }, Tracer: tracer} mw := TraceServerWithConfig(config) h := mw(func(c *echo.Context) error { return nil }) assert.NoError(t, err) e := echo.New() c := e.NewContext(req, rec) err = h(c) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 500): neverCalled = true } assert.True(t, neverCalled) } func TestStartChildSpan(t *testing.T) { done := make(chan struct{}) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer close(done) body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) var spans []zipkinSpanRequest err = json.Unmarshal(body, &spans) assert.NoError(t, err) assert.NotEmpty(t, spans[0].ID) assert.NotEmpty(t, spans[0].TraceID) assert.Equal(t, "kinesis-test", spans[0].Name) assert.Equal(t, "echo-service", spans[0].LocalEndpoint.ServiceName) assert.NotNil(t, spans[0].Tags["availability_zone"]) assert.Equal(t, "us-east-1", spans[0].Tags["availability_zone"]) })) defer ts.Close() traceTags := make(map[string]string) traceTags["availability_zone"] = "us-east-1" tracer, reporter, err := DefaultTracer(ts.URL, "echo-service", traceTags) assert.NoError(t, err) req := httptest.NewRequest("GET", "http://localhost:8080/health", nil) rec := httptest.NewRecorder() e := echo.New() c := e.NewContext(req, rec) childSpan := StartChildSpan(c, "kinesis-test", tracer) time.Sleep(500) childSpan.Finish() assert.NoError(t, err) err = reporter.Close() assert.NoError(t, err) select { case <-done: case <-time.After(time.Millisecond * 15500): t.Fatalf("Test server did not receive spans") } }