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
[](http://godoc.org/github.com/labstack/echo-contrib)
[](https://codecov.io/gh/labstack/echo-contrib)
[](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")
}
}
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
SYMBOL INDEX (149 symbols across 15 files)
FILE: casbin/casbin.go
type Config (line 61) | type Config struct
function Middleware (line 99) | func Middleware(ce *casbin.Enforcer) echo.MiddlewareFunc {
function MiddlewareWithConfig (line 107) | func MiddlewareWithConfig(config Config) echo.MiddlewareFunc {
FILE: casbin/casbin_test.go
function testRequest (line 20) | func testRequest(t *testing.T, h echo.HandlerFunc, user string, path str...
function TestAuth (line 47) | func TestAuth(t *testing.T) {
function TestPathWildcard (line 59) | func TestPathWildcard(t *testing.T) {
function TestRBAC (line 80) | func TestRBAC(t *testing.T) {
function TestEnforceError (line 105) | func TestEnforceError(t *testing.T) {
function TestCustomUserGetter (line 114) | func TestCustomUserGetter(t *testing.T) {
function TestUserGetterError (line 129) | func TestUserGetterError(t *testing.T) {
function TestCustomEnforceHandler (line 144) | func TestCustomEnforceHandler(t *testing.T) {
function TestCustomSkipper (line 168) | func TestCustomSkipper(t *testing.T) {
FILE: echoprometheus/prometheus.go
constant defaultSubsystem (line 32) | defaultSubsystem = "echo"
constant _ (line 36) | _ = iota
constant bKB (line 37) | bKB float64 = 1 << (10 * iota)
constant bMB (line 38) | bMB
type MiddlewareConfig (line 45) | type MiddlewareConfig struct
method ToMiddleware (line 164) | func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, erro...
type LabelValueFunc (line 89) | type LabelValueFunc
type HandlerConfig (line 92) | type HandlerConfig struct
type PushGatewayConfig (line 99) | type PushGatewayConfig struct
function NewHandler (line 122) | func NewHandler() echo.HandlerFunc {
function NewHandlerWithConfig (line 127) | func NewHandlerWithConfig(config HandlerConfig) echo.HandlerFunc {
function NewMiddleware (line 146) | func NewMiddleware(subsystem string) echo.MiddlewareFunc {
function NewMiddlewareWithConfig (line 153) | func NewMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc {
type customLabelValuer (line 315) | type customLabelValuer struct
function createLabels (line 321) | func createLabels(customLabelFuncs map[string]LabelValueFunc) ([]string,...
function containsAt (line 350) | func containsAt[K comparable](haystack []K, needle K) int {
function computeApproximateRequestSize (line 359) | func computeApproximateRequestSize(r *http.Request) int {
function RunPushGatewayGatherer (line 401) | func RunPushGatewayGatherer(ctx context.Context, config PushGatewayConfi...
function WriteGatheredMetrics (line 460) | func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer...
FILE: echoprometheus/prometheus_test.go
function TestCustomRegistryMetrics (line 22) | func TestCustomRegistryMetrics(t *testing.T) {
function TestDefaultRegistryMetrics (line 36) | func TestDefaultRegistryMetrics(t *testing.T) {
function TestPrometheus_Buckets (line 51) | func TestPrometheus_Buckets(t *testing.T) {
function TestMiddlewareConfig_Skipper (line 70) | func TestMiddlewareConfig_Skipper(t *testing.T) {
function TestMetricsForErrors (line 103) | func TestMetricsForErrors(t *testing.T) {
function TestMiddlewareConfig_LabelFuncs (line 138) | func TestMiddlewareConfig_LabelFuncs(t *testing.T) {
function TestMiddlewareConfig_StatusCodeResolver (line 165) | func TestMiddlewareConfig_StatusCodeResolver(t *testing.T) {
function TestMiddlewareConfig_HistogramOptsFunc (line 226) | func TestMiddlewareConfig_HistogramOptsFunc(t *testing.T) {
function TestMiddlewareConfig_CounterOptsFunc (line 255) | func TestMiddlewareConfig_CounterOptsFunc(t *testing.T) {
function TestMiddlewareConfig_AfterNextFuncs (line 284) | func TestMiddlewareConfig_AfterNextFuncs(t *testing.T) {
function TestRunPushGatewayGatherer (line 317) | func TestRunPushGatewayGatherer(t *testing.T) {
function TestSetPathFor404NoMatchingRoute (line 345) | func TestSetPathFor404NoMatchingRoute(t *testing.T) {
function TestSetPathFor404Logic (line 362) | func TestSetPathFor404Logic(t *testing.T) {
function TestInvalidUTF8PathIsFixed (line 383) | func TestInvalidUTF8PathIsFixed(t *testing.T) {
function requestBody (line 398) | func requestBody(e *echo.Echo, path string) (string, int) {
function request (line 406) | func request(e *echo.Echo, path string) int {
function unregisterDefaults (line 411) | func unregisterDefaults(subsystem string) {
FILE: internal/helpers/statuscode.go
function DefaultStatusResolver (line 11) | func DefaultStatusResolver(c *echo.Context, err error) int {
FILE: jaegertracing/jaegertracing.go
constant defaultComponentName (line 51) | defaultComponentName = "echo/v5"
type TraceConfig (line 55) | type TraceConfig struct
function New (line 97) | func New(e *echo.Echo, skipper middleware.Skipper) io.Closer {
function Trace (line 131) | func Trace(tracer opentracing.Tracer) echo.MiddlewareFunc {
function TraceWithConfig (line 142) | func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc {
function limitString (line 252) | func limitString(str string, size int) string {
function logError (line 260) | func logError(span opentracing.Span, err error) {
function getRequestID (line 270) | func getRequestID(ctx *echo.Context) string {
function generateToken (line 278) | func generateToken() string {
function defaultOperationName (line 284) | func defaultOperationName(c *echo.Context) string {
function TraceFunction (line 292) | func TraceFunction(ctx *echo.Context, fn interface{}, params ...interfac...
function CreateChildSpan (line 329) | func CreateChildSpan(ctx *echo.Context, name string) opentracing.Span {
function NewTracedRequest (line 350) | func NewTracedRequest(method string, url string, body io.Reader, span op...
FILE: jaegertracing/jaegertracing_test.go
type mockSpan (line 22) | type mockSpan struct
method isFinished (line 38) | func (sp *mockSpan) isFinished() bool {
method getOpName (line 42) | func (sp *mockSpan) getOpName() string {
method getTag (line 46) | func (sp *mockSpan) getTag(key string) interface{} {
method getLog (line 50) | func (sp *mockSpan) getLog(key string) interface{} {
method Finish (line 54) | func (sp *mockSpan) Finish() {
method FinishWithOptions (line 57) | func (sp *mockSpan) FinishWithOptions(opts opentracing.FinishOptions) {
method Context (line 59) | func (sp *mockSpan) Context() opentracing.SpanContext {
method SetOperationName (line 62) | func (sp *mockSpan) SetOperationName(operationName string) opentracing...
method SetTag (line 66) | func (sp *mockSpan) SetTag(key string, value interface{}) opentracing....
method LogFields (line 70) | func (sp *mockSpan) LogFields(fields ...log.Field) {
method LogKV (line 72) | func (sp *mockSpan) LogKV(alternatingKeyValues ...interface{}) {
method SetBaggageItem (line 81) | func (sp *mockSpan) SetBaggageItem(restrictedKey, value string) opentr...
method BaggageItem (line 84) | func (sp *mockSpan) BaggageItem(restrictedKey string) string {
method Tracer (line 87) | func (sp *mockSpan) Tracer() opentracing.Tracer {
method LogEvent (line 90) | func (sp *mockSpan) LogEvent(event string) {
method LogEventWithPayload (line 92) | func (sp *mockSpan) LogEventWithPayload(event string, payload interfac...
method Log (line 94) | func (sp *mockSpan) Log(data opentracing.LogData) {
function createSpan (line 30) | func createSpan(tracer opentracing.Tracer) *mockSpan {
type mockTracer (line 98) | type mockTracer struct
method currentSpan (line 103) | func (tr *mockTracer) currentSpan() *mockSpan {
method StartSpan (line 107) | func (tr *mockTracer) StartSpan(operationName string, opts ...opentrac...
method Inject (line 118) | func (tr *mockTracer) Inject(sm opentracing.SpanContext, format interf...
method Extract (line 122) | func (tr *mockTracer) Extract(format interface{}, carrier interface{})...
function createMockTracer (line 129) | func createMockTracer() *mockTracer {
function TestTraceWithDefaultConfig (line 136) | func TestTraceWithDefaultConfig(t *testing.T) {
function TestTraceWithConfig (line 199) | func TestTraceWithConfig(t *testing.T) {
function TestTraceWithConfigOfBodyDump (line 218) | func TestTraceWithConfigOfBodyDump(t *testing.T) {
function TestTraceWithConfigOfNoneComponentName (line 246) | func TestTraceWithConfigOfNoneComponentName(t *testing.T) {
function TestTraceWithConfigOfSkip (line 261) | func TestTraceWithConfigOfSkip(t *testing.T) {
function TestTraceOfNoCurrentSpan (line 277) | func TestTraceOfNoCurrentSpan(t *testing.T) {
function TestTraceWithLimitHTTPBody (line 288) | func TestTraceWithLimitHTTPBody(t *testing.T) {
function TestTraceWithoutLimitHTTPBody (line 312) | func TestTraceWithoutLimitHTTPBody(t *testing.T) {
function TestTraceWithDefaultOperationName (line 336) | func TestTraceWithDefaultOperationName(t *testing.T) {
function TestTraceWithCustomOperationName (line 353) | func TestTraceWithCustomOperationName(t *testing.T) {
FILE: jaegertracing/response_dumper.go
type responseDumper (line 12) | type responseDumper struct
method Write (line 29) | func (d *responseDumper) Write(b []byte) (int, error) {
method GetResponse (line 33) | func (d *responseDumper) GetResponse() string {
method Unwrap (line 37) | func (d *responseDumper) Unwrap() http.ResponseWriter {
function newResponseDumper (line 19) | func newResponseDumper(resp http.ResponseWriter) *responseDumper {
FILE: pprof/pprof.go
constant DefaultPrefix (line 15) | DefaultPrefix = "/debug/pprof"
function getPrefix (line 18) | func getPrefix(prefixOptions ...string) string {
function Register (line 26) | func Register(e *echo.Echo, prefixOptions ...string) {
function handler (line 46) | func handler(h http.HandlerFunc) echo.HandlerFunc {
FILE: pprof/pprof_test.go
function TestPProfRegisterDefaualtPrefix (line 15) | func TestPProfRegisterDefaualtPrefix(t *testing.T) {
function TestPProfRegisterCustomPrefix (line 44) | func TestPProfRegisterCustomPrefix(t *testing.T) {
FILE: session/session.go
type Config (line 16) | type Config struct
constant key (line 27) | key = "_session_store"
function Get (line 38) | func Get(name string, c *echo.Context) (*sessions.Session, error) {
function Middleware (line 48) | func Middleware(store sessions.Store) echo.MiddlewareFunc {
function MiddlewareWithConfig (line 56) | func MiddlewareWithConfig(config Config) echo.MiddlewareFunc {
FILE: session/session_test.go
function TestMiddleware (line 17) | func TestMiddleware(t *testing.T) {
function TestGetSessionMissingStore (line 62) | func TestGetSessionMissingStore(t *testing.T) {
FILE: zipkintracing/response_writer.go
type ResponseWriter (line 16) | type ResponseWriter interface
type beforeFunc (line 31) | type beforeFunc
function NewResponseWriter (line 34) | func NewResponseWriter(rw http.ResponseWriter) ResponseWriter {
type responseWriter (line 42) | type responseWriter struct
method WriteHeader (line 49) | func (rw *responseWriter) WriteHeader(s int) {
method Write (line 55) | func (rw *responseWriter) Write(b []byte) (int, error) {
method Status (line 65) | func (rw *responseWriter) Status() int {
method Size (line 69) | func (rw *responseWriter) Size() int {
method Written (line 73) | func (rw *responseWriter) Written() bool {
method Before (line 77) | func (rw *responseWriter) Before(before func(ResponseWriter)) {
method Hijack (line 81) | func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
method callBefore (line 89) | func (rw *responseWriter) callBefore() {
method Flush (line 95) | func (rw *responseWriter) Flush() {
method CloseNotify (line 106) | func (rw *responseWriter) CloseNotify() <-chan bool {
method Unwrap (line 111) | func (rw *responseWriter) Unwrap() http.ResponseWriter {
FILE: zipkintracing/tracing.go
type Tags (line 23) | type Tags
type TraceProxyConfig (line 26) | type TraceProxyConfig struct
type TraceServerConfig (line 33) | type TraceServerConfig struct
function DoHTTP (line 56) | func DoHTTP(c *echo.Context, r *http.Request, client *zipkinhttp.Client)...
function TraceFunc (line 64) | func TraceFunc(c *echo.Context, spanName string, spanTags Tags, tracer *...
function TraceProxy (line 80) | func TraceProxy(tracer *zipkin.Tracer) echo.MiddlewareFunc {
function TraceProxyWithConfig (line 89) | func TraceProxyWithConfig(config TraceProxyConfig) echo.MiddlewareFunc {
function TraceServer (line 129) | func TraceServer(tracer *zipkin.Tracer) echo.MiddlewareFunc {
function TraceServerWithConfig (line 138) | func TraceServerWithConfig(config TraceServerConfig) echo.MiddlewareFunc {
function StartChildSpan (line 174) | func StartChildSpan(c *echo.Context, spanName string, tracer *zipkin.Tra...
FILE: zipkintracing/tracing_test.go
type zipkinSpanRequest (line 24) | type zipkinSpanRequest struct
function DefaultTracer (line 36) | func DefaultTracer(reportingURL, serviceName string, tags map[string]str...
function TestDoHTTTP (line 49) | func TestDoHTTTP(t *testing.T) {
function TestTraceFunc (line 95) | func TestTraceFunc(t *testing.T) {
function TestTraceProxy (line 136) | func TestTraceProxy(t *testing.T) {
function TestTraceServer (line 182) | func TestTraceServer(t *testing.T) {
function TestTraceServerWithConfig (line 225) | func TestTraceServerWithConfig(t *testing.T) {
function TestTraceServerWithConfigSkipper (line 278) | func TestTraceServerWithConfigSkipper(t *testing.T) {
function TestStartChildSpan (line 316) | func TestStartChildSpan(t *testing.T) {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 280,
"preview": "### Issue Description\n\n### Checklist\n\n- [ ] Dependencies installed\n- [ ] No typos\n- [ ] Searched existing issues and doc"
},
{
"path": ".github/stale.yml",
"chars": 722,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
},
{
"path": ".github/workflows/echo-contrib.yml",
"chars": 2450,
"preview": "name: Run Tests\n\non:\n push:\n branches:\n - master\n pull_request:\n branches:\n - master\n workflow_dispat"
},
{
"path": ".gitignore",
"chars": 31,
"preview": "vendor\n_test\ncoverage.txt\n.idea"
},
{
"path": "LICENSE",
"chars": 1075,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2017 LabStack\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "Makefile",
"chars": 1030,
"preview": "PKG := \"github.com/labstack/echo-contrib\"\nPKG_LIST := $(shell go list ${PKG}/...)\n\n.DEFAULT_GOAL := check\ncheck: lint ve"
},
{
"path": "README.md",
"chars": 2393,
"preview": "# Echo Community Contribution middlewares\n\n [ or [Op"
},
{
"path": "echoprometheus/prometheus.go",
"chars": 15903,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\n/*\nPackage echoprometh"
},
{
"path": "echoprometheus/prometheus_test.go",
"chars": 16924,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage echoprometheus"
},
{
"path": "go.mod",
"chars": 1469,
"preview": "module github.com/labstack/echo-contrib/v5\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/casbin/casbin/v2 v2.135.0\n\tgithub.com/goril"
},
{
"path": "go.sum",
"chars": 8673,
"preview": "github.com/HdrHistogram/hdrhistogram-go v1.2.0 h1:XMJkDWuz6bM9Fzy7zORuVFKH7ZJY41G2q8KWhVGkNiY=\ngithub.com/HdrHistogram/h"
},
{
"path": "internal/helpers/statuscode.go",
"chars": 536,
"preview": "package helpers\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// DefaultStatusResolver resolves htt"
},
{
"path": "jaegertracing/jaegertracing.go",
"chars": 11152,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\n/*\nPackage jaegertraci"
},
{
"path": "jaegertracing/jaegertracing_test.go",
"chars": 11102,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage jaegertracing\n"
},
{
"path": "jaegertracing/response_dumper.go",
"chars": 681,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage jaegertracing\n"
},
{
"path": "pprof/README.md",
"chars": 883,
"preview": "Usage\n\n```go\npackage main\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/labstack/echo-contrib/v5/pprof\"\n\t\"github.com/labstack/echo"
},
{
"path": "pprof/pprof.go",
"chars": 1496,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage pprof\n\nimport "
},
{
"path": "pprof/pprof_test.go",
"chars": 1448,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage pprof\n\nimport "
},
{
"path": "session/session.go",
"chars": 1591,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage session\n\nimpor"
},
{
"path": "session/session_test.go",
"chars": 1588,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage session\n\nimpor"
},
{
"path": "zipkintracing/README.md",
"chars": 3880,
"preview": "# Tracing Library for Go\n\n> Deprecated: use [OpenTelemetry middleware](https://github.com/labstack/echo-opentelemetry) i"
},
{
"path": "zipkintracing/response_writer.go",
"chars": 2925,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage zipkintracing\n"
},
{
"path": "zipkintracing/tracing.go",
"chars": 7090,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage zipkintracing\n"
},
{
"path": "zipkintracing/tracing_test.go",
"chars": 10970,
"preview": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2017 LabStack and Echo contributors\n\npackage zipkintracing\n"
}
]
About this extraction
This page contains the full source code of the labstack/echo-contrib GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (116.9 KB), approximately 35.1k tokens, and a symbol index with 149 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.