Skip to content

Commit c764849

Browse files
authored
new prometheus middleware (#94)
new prometheus middleware
1 parent b6855c2 commit c764849

File tree

7 files changed

+955
-50
lines changed

7 files changed

+955
-50
lines changed

.github/workflows/echo-contrib.yml

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,54 @@ on:
44
push:
55
branches:
66
- master
7-
paths:
8-
- '**.go'
9-
- 'go.*'
10-
- '_fixture/**'
11-
- '.github/**'
12-
- 'codecov.yml'
137
pull_request:
148
branches:
159
- master
16-
paths:
17-
- '**.go'
18-
- 'go.*'
19-
- '_fixture/**'
20-
- '.github/**'
21-
- 'codecov.yml'
2210
workflow_dispatch:
2311

12+
permissions:
13+
contents: read # to fetch code (actions/checkout)
14+
15+
env:
16+
# run coverage and benchmarks only with the latest Go version
17+
LATEST_GO_VERSION: "1.20"
18+
2419
jobs:
2520
test:
26-
env:
27-
latest: '1.19'
2821
strategy:
2922
matrix:
3023
os: [ubuntu-latest, macos-latest, windows-latest]
31-
go: [ '1.17', '1.18', '1.19']
24+
# Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy
25+
# Echo tests with last four major releases (unless there are pressing vulnerabilities)
26+
# As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
27+
# we derive from last four major releases promise.
28+
go: ["1.18", "1.19", "1.20"]
3229
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
3330
runs-on: ${{ matrix.os }}
3431
steps:
35-
- name: Set up Go ${{ matrix.go }}
36-
uses: actions/setup-go@v3
37-
with:
38-
go-version: ${{ matrix.go }}
39-
4032
- name: Checkout Code
4133
uses: actions/checkout@v3
42-
with:
43-
ref: ${{ github.ref }}
4434

45-
- name: Run static checks
46-
if: matrix.go == env.latest && matrix.os == 'ubuntu-latest'
47-
run: |
48-
go install honnef.co/go/tools/cmd/staticcheck@latest
49-
staticcheck -tests=false ./...
35+
- name: Set up Go ${{ matrix.go }}
36+
uses: actions/setup-go@v4
37+
with:
38+
go-version: ${{ matrix.go }}
5039

5140
- name: Run Tests
52-
run: |
53-
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
41+
run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
5442

5543
- name: Upload coverage to Codecov
56-
if: success() && matrix.go == env.latest && matrix.os == 'ubuntu-latest'
44+
if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest'
5745
uses: codecov/codecov-action@v3
5846
with:
47+
token:
5948
fail_ci_if_error: false
49+
6050
benchmark:
6151
needs: test
62-
strategy:
63-
matrix:
64-
os: [ubuntu-latest]
65-
go: [1.19]
66-
name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
67-
runs-on: ${{ matrix.os }}
52+
name: Benchmark comparison
53+
runs-on: ubuntu-latest
6854
steps:
69-
- name: Set up Go ${{ matrix.go }}
70-
uses: actions/setup-go@v3
71-
with:
72-
go-version: ${{ matrix.go }}
73-
7455
- name: Checkout Code (Previous)
7556
uses: actions/checkout@v3
7657
with:
@@ -82,6 +63,11 @@ jobs:
8263
with:
8364
path: new
8465

66+
- name: Set up Go ${{ matrix.go }}
67+
uses: actions/setup-go@v4
68+
with:
69+
go-version: ${{ env.LATEST_GO_VERSION }}
70+
8571
- name: Install Dependencies
8672
run: go install golang.org/x/perf/cmd/benchstat@latest
8773

@@ -97,4 +83,4 @@ jobs:
9783
9884
- name: Run Benchstat
9985
run: |
100-
benchstat previous/benchmark.txt new/benchmark.txt
86+
benchstat previous/benchmark.txt new/benchmark.txt

Makefile

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
PKG := "github.com/labstack/echo-contrib"
22
PKG_LIST := $(shell go list ${PKG}/...)
33

4-
tag:
5-
@git tag `grep -P '^\tversion = ' echo.go|cut -f2 -d'"'`
6-
@git tag|grep -v ^v
7-
84
.DEFAULT_GOAL := check
95
check: lint vet race ## Check project
106

117
init:
12-
@go get -u honnef.co/go/tools/cmd/staticcheck@latest
8+
@go install honnef.co/go/tools/cmd/staticcheck@latest
139

1410
format: ## Format the source code
1511
@find ./ -type f -name "*.go" -exec gofmt -w {} \;
@@ -32,6 +28,6 @@ benchmark: ## Run benchmarks
3228
help: ## Display this help screen
3329
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
3430

35-
goversion ?= "1.16"
36-
test_version: ## Run tests inside Docker with given version (defaults to 1.15 oldest supported). Example: make test_version goversion=1.15
31+
goversion ?= "1.18"
32+
test_version: ## Run tests inside Docker with given version (defaults to 1.18 oldest supported). Example: make test_version goversion=1.18
3733
@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make race"

echoprometheus/README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Usage
2+
3+
```
4+
package main
5+
6+
import (
7+
"github.com/labstack/echo/v4"
8+
"github.com/labstack/echo-contrib/prometheus/echoprometheus"
9+
)
10+
11+
func main() {
12+
e := echo.New()
13+
// Enable metrics middleware
14+
e.Use(echoprometheus.NewMiddleware("myapp"))
15+
e.GET("/metrics", echoprometheus.NewHandler())
16+
17+
e.Logger.Fatal(e.Start(":1323"))
18+
}
19+
```
20+
21+
22+
# How to migrate
23+
24+
## Creating and adding middleware to the application
25+
26+
Older `prometheus` middleware
27+
```go
28+
e := echo.New()
29+
p := prometheus.NewPrometheus("echo", nil)
30+
p.Use(e)
31+
```
32+
33+
With the new `echoprometheus` middleware
34+
```go
35+
e := echo.New()
36+
e.Use(echoprometheus.NewMiddleware("myapp")) // register middleware to gather metrics from requests
37+
e.GET("/metrics", echoprometheus.NewHandler()) // register route to serve gathered metrics in Prometheus format
38+
```
39+
40+
## Replacement for `Prometheus.MetricsList` field, `NewMetric(m *Metric, subsystem string)` function and `prometheus.Metric` struct
41+
42+
The `NewMetric` function allowed to create custom metrics with the old `prometheus` middleware. This helper is no longer available
43+
to avoid the added complexity. It is recommended to use native Prometheus metrics and register those yourself.
44+
45+
This can be done now as follows:
46+
```go
47+
e := echo.New()
48+
49+
customRegistry := prometheus.NewRegistry() // create custom registry for your custom metrics
50+
customCounter := prometheus.NewCounter( // create new counter metric. This is replacement for `prometheus.Metric` struct
51+
prometheus.CounterOpts{
52+
Name: "custom_requests_total",
53+
Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
54+
},
55+
)
56+
if err := customRegistry.Register(customCounter); err != nil { // register your new counter metric with metrics registry
57+
log.Fatal(err)
58+
}
59+
60+
e.Use(NewMiddlewareWithConfig(MiddlewareConfig{
61+
AfterNext: func(c echo.Context, err error) {
62+
customCounter.Inc() // use our custom metric in middleware. after every request increment the counter
63+
},
64+
Registerer: customRegistry, // use our custom registry instead of default Prometheus registry
65+
}))
66+
e.GET("/metrics", NewHandlerWithConfig(HandlerConfig{Gatherer: customRegistry})) // register route for getting gathered metrics data from our custom Registry
67+
```
68+
69+
## Replacement for `Prometheus.MetricsPath`
70+
71+
`MetricsPath` was used to skip metrics own route from Prometheus metrics. Skipping is no longer done and requests to Prometheus
72+
route will be included in gathered metrics.
73+
74+
To restore the old behaviour the `/metrics` path needs to be excluded from counting using the Skipper function:
75+
```go
76+
conf := echoprometheus.MiddlewareConfig{
77+
Skipper: func(c echo.Context) bool {
78+
return c.Path() == "/metrics"
79+
},
80+
}
81+
e.Use(echoprometheus.NewMiddlewareWithConfig(conf))
82+
```
83+
84+
## Replacement for `Prometheus.RequestCounterURLLabelMappingFunc` and `Prometheus.RequestCounterHostLabelMappingFunc`
85+
86+
These function fields were used to define how "URL" or "Host" attribute in Prometheus metric lines are created.
87+
88+
These can now be substituted by using `LabelFuncs`:
89+
```go
90+
e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
91+
LabelFuncs: map[string]echoprometheus.LabelValueFunc{
92+
"scheme": func(c echo.Context, err error) string { // additional custom label
93+
return c.Scheme()
94+
},
95+
"url": func(c echo.Context, err error) string { // overrides default 'url' label value
96+
return "x_" + c.Request().URL.Path
97+
},
98+
"host": func(c echo.Context, err error) string { // overrides default 'host' label value
99+
return "y_" + c.Request().Host
100+
},
101+
},
102+
}))
103+
```
104+
105+
Will produce Prometheus line as
106+
`echo_request_duration_seconds_count{code="200",host="y_example.com",method="GET",scheme="http",url="x_/ok",scheme="http"} 1`
107+
108+
109+
## Replacement for `Metric.Buckets` and modifying default metrics
110+
111+
The `echoprometheus` middleware registers the following metrics by default:
112+
113+
* Counter `requests_total`
114+
* Histogram `request_duration_seconds`
115+
* Histogram `response_size_bytes`
116+
* Histogram `request_size_bytes`
117+
118+
You can modify their definition before these metrics are registed with `CounterOptsFunc` and `HistogramOptsFunc` callbacks
119+
120+
Example:
121+
```go
122+
e.Use(NewMiddlewareWithConfig(MiddlewareConfig{
123+
HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts {
124+
if opts.Name == "request_duration_seconds" {
125+
opts.Buckets = []float64{1.0 * bKB, 2.0 * bKB, 5.0 * bKB, 10.0 * bKB, 100 * bKB, 500 * bKB, 1.0 * bMB, 2.5 * bMB, 5.0 * bMB, 10.0 * bMB}
126+
}
127+
return opts
128+
},
129+
CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts {
130+
if opts.Name == "requests_total" {
131+
opts.ConstLabels = prometheus.Labels{"my_const": "123"}
132+
}
133+
return opts
134+
},
135+
}))
136+
```
137+
138+
## Replacement for `PushGateway` struct and related methods
139+
140+
Function `RunPushGatewayGatherer` starts pushing collected metrics and block until context completes or ErrorHandler returns an error.
141+
This function should be run in separate goroutine.
142+
143+
Example:
144+
```go
145+
go func() {
146+
config := echoprometheus.PushGatewayConfig{
147+
PushGatewayURL: "https://host:9080",
148+
PushInterval: 10 * time.Millisecond,
149+
}
150+
if err := echoprometheus.RunPushGatewayGatherer(context.Background(), config); !errors.Is(err, context.Canceled) {
151+
log.Fatal(err)
152+
}
153+
}()
154+
```

0 commit comments

Comments
 (0)