diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7793fa..633f421 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: Build: strategy: matrix: - go-version: [1.20.x, 1.21.x, 1.22.x] + go-version: [1.21.x, 1.22.x] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: @@ -20,5 +20,5 @@ jobs: with: go-version: '${{ matrix.go-version }}' - name: Run Test - run: go test -race + run: go test -race -count=1 diff --git a/README.md b/README.md index b4b0181..0638107 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Prometheus middleware for gofiber. +**Note: Requires Go 1.21 and above** + ![Release](https://img.shields.io/github/release/ansrivas/fiberprometheus.svg) [![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) ![Test](https://github.com/ansrivas/fiberprometheus/workflows/Test/badge.svg) @@ -14,6 +16,7 @@ Following metrics are available by default: http_requests_total http_request_duration_seconds http_requests_in_progress_total +http_cache_results ``` ### Install v2 @@ -39,7 +42,6 @@ func main() { // This here will appear as a label, one can also use // fiberprometheus.NewWith(servicename, namespace, subsystem ) // or - // NOTE: Following is not available in v1 // labels := map[string]string{"custom_label1":"custom_value1", "custom_label2":"custom_value2"} // fiberprometheus.NewWithLabels(labels, namespace, subsystem ) prometheus := fiberprometheus.New("my-service-name") @@ -58,37 +60,6 @@ func main() { } ``` -### Example using V1 - -```go -package main - -import ( - "github.com/gofiber/fiber" - "github.com/ansrivas/fiberprometheus" -) - -func main() { - app := fiber.New() - - // This here will appear as a label, one can also use - // fiberprometheus.NewWith(servicename, namespace, subsystem ) - prometheus := fiberprometheus.New("my-service-name") - prometheus.RegisterAt(app, "/metrics") - app.Use(prometheus.Middleware) - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello World") - }) - - app.Post("/some", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - app.Listen(3000) -} -``` - ### Result - Hit the default url at http://localhost:3000 @@ -96,4 +67,4 @@ func main() { ### Grafana Board -- https://grafana.com/grafana/dashboards/14331 \ No newline at end of file +- https://grafana.com/grafana/dashboards/14331 diff --git a/go.mod b/go.mod index be24c1a..68e471e 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,30 @@ module github.com/ansrivas/fiberprometheus/v2 -go 1.20 +go 1.21 require ( - github.com/gofiber/adaptor/v2 v2.2.1 github.com/gofiber/fiber/v2 v2.52.2 github.com/prometheus/client_golang v1.19.0 + github.com/valyala/fasthttp v1.52.0 ) require ( - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.50.0 // indirect + github.com/prometheus/procfs v0.13.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.16.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + golang.org/x/sys v0.18.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index 0b28397..ee05d5d 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,19 @@ -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/gofiber/adaptor/v2 v2.2.1 h1:givE7iViQWlsTR4Jh7tB4iXzrlKBgiraB/yTdHs9Lv4= -github.com/gofiber/adaptor/v2 v2.2.1/go.mod h1:AhR16dEqs25W2FY/l8gSj1b51Azg5dtPDmm+pruNOrc= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -21,26 +21,60 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= +github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/middleware.go b/middleware.go index 98a01ac..f512ff6 100644 --- a/middleware.go +++ b/middleware.go @@ -25,8 +25,9 @@ import ( "strconv" "time" - "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" + "github.com/gofiber/fiber/v2/utils" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -38,10 +39,16 @@ type FiberPrometheus struct { requestsTotal *prometheus.CounterVec requestDuration *prometheus.HistogramVec requestInFlight *prometheus.GaugeVec + cacheHeaderKey string + cacheCounter *prometheus.CounterVec defaultURL string } func create(registry prometheus.Registerer, serviceName, namespace, subsystem string, labels map[string]string) *FiberPrometheus { + if registry == nil { + registry = prometheus.NewRegistry() + } + constLabels := make(prometheus.Labels) if serviceName != "" { constLabels["service"] = serviceName @@ -58,6 +65,16 @@ func create(registry prometheus.Registerer, serviceName, namespace, subsystem st }, []string{"status_code", "method", "path"}, ) + + cacheCounter := promauto.With(registry).NewCounterVec( + prometheus.CounterOpts{ + Name: prometheus.BuildFQName(namespace, subsystem, "cache_results"), + Help: "Counts all cache hits by status code, method, and path", + ConstLabels: constLabels, + }, + []string{"status_code", "method", "path", "cache_result"}, + ) + histogram := promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ Name: prometheus.BuildFQName(namespace, subsystem, "request_duration_seconds"), Help: "Duration of all HTTP requests by status code, method and path.", @@ -120,14 +137,22 @@ func create(registry prometheus.Registerer, serviceName, namespace, subsystem st requestsTotal: counter, requestDuration: histogram, requestInFlight: gauge, + cacheHeaderKey: "X-Cache", + cacheCounter: cacheCounter, defaultURL: "/metrics", } } +// CustomCacheKey allows to set a custom header key for caching +// By default it is set to "X-Cache", the fiber default +func (ps *FiberPrometheus) CustomCacheKey(cacheHeaderKey string) { + ps.cacheHeaderKey = cacheHeaderKey +} + // New creates a new instance of FiberPrometheus middleware // serviceName is available as a const label func New(serviceName string) *FiberPrometheus { - return create(prometheus.DefaultRegisterer, serviceName, "http", "", nil) + return create(nil, serviceName, "http", "", nil) } // NewWith creates a new instance of FiberPrometheus middleware but with an ability @@ -138,7 +163,7 @@ func New(serviceName string) *FiberPrometheus { // For e.g. namespace = "my_app", subsystem = "http" then metrics would be // `my_app_http_requests_total{...,service= "serviceName"}` func NewWith(serviceName, namespace, subsystem string) *FiberPrometheus { - return create(prometheus.DefaultRegisterer, serviceName, namespace, subsystem, nil) + return create(nil, serviceName, namespace, subsystem, nil) } // NewWithLabels creates a new instance of FiberPrometheus middleware but with an ability @@ -150,7 +175,7 @@ func NewWith(serviceName, namespace, subsystem string) *FiberPrometheus { // then then metrics would become // `my_app_http_requests_total{...,key1= "value1", key2= "value2" }` func NewWithLabels(labels map[string]string, namespace, subsystem string) *FiberPrometheus { - return create(prometheus.DefaultRegisterer, "", namespace, subsystem, labels) + return create(nil, "", namespace, subsystem, labels) } // NewWithRegistry creates a new instance of FiberPrometheus middleware but with an ability @@ -176,12 +201,13 @@ func (ps *FiberPrometheus) RegisterAt(app fiber.Router, url string, handlers ... // Middleware is the actual default middleware implementation func (ps *FiberPrometheus) Middleware(ctx *fiber.Ctx) error { start := time.Now() - method := ctx.Route().Method + path := string(ctx.Request().RequestURI()) - if ctx.Route().Path == ps.defaultURL { + if path == ps.defaultURL { return ctx.Next() } + method := ctx.Route().Method ps.requestInFlight.WithLabelValues(method).Inc() defer func() { ps.requestInFlight.WithLabelValues(method).Dec() @@ -200,11 +226,19 @@ func (ps *FiberPrometheus) Middleware(ctx *fiber.Ctx) error { status = ctx.Response().StatusCode() } - path := ctx.Route().Path - + // Get status as string statusCode := strconv.Itoa(status) + + // Update total requests counter ps.requestsTotal.WithLabelValues(statusCode, method, path).Inc() + // Update the cache counter + cacheResult := utils.CopyString(ctx.GetRespHeader(ps.cacheHeaderKey, "")) + if cacheResult != "" { + ps.cacheCounter.WithLabelValues(statusCode, method, path, cacheResult).Inc() + } + + // Update the request duration histogram elapsed := float64(time.Since(start).Nanoseconds()) / 1e9 ps.requestDuration.WithLabelValues(statusCode, method, path).Observe(elapsed) diff --git a/middleware_test.go b/middleware_test.go index 5ee1b55..29f6e1a 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -30,12 +30,15 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/basicauth" + "github.com/gofiber/fiber/v2/middleware/cache" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/valyala/fasthttp" ) func TestMiddleware(t *testing.T) { + t.Parallel() app := fiber.New() prometheus := New("test-service") @@ -81,12 +84,12 @@ func TestMiddleware(t *testing.T) { t.Errorf("got %s; want %s", got, want) } - want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="400"} 1` + want = `http_requests_total{method="GET",path="/error/fiber",service="test-service",status_code="400"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } - want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="500"} 1` + want = `http_requests_total{method="GET",path="/error/unknown",service="test-service",status_code="500"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } @@ -102,16 +105,94 @@ func TestMiddleware(t *testing.T) { } } +func TestMiddlewareWithGroup(t *testing.T) { + t.Parallel() + app := fiber.New() + + prometheus := New("test-service") + prometheus.RegisterAt(app, "/metrics") + app.Use(prometheus.Middleware) + + // Define Group + public := app.Group("/public") + + // Define Group Routes + public.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello World") + }) + public.Get("/error/:type", func(ctx *fiber.Ctx) error { + switch ctx.Params("type") { + case "fiber": + return fiber.ErrBadRequest + default: + return fiber.ErrInternalServerError + } + }) + req := httptest.NewRequest("GET", "/public", nil) + resp, _ := app.Test(req, -1) + if resp.StatusCode != 200 { + t.Fail() + } + + req = httptest.NewRequest("GET", "/public/error/fiber", nil) + resp, _ = app.Test(req, -1) + if resp.StatusCode != fiber.StatusBadRequest { + t.Fail() + } + + req = httptest.NewRequest("GET", "/public/error/unknown", nil) + resp, _ = app.Test(req, -1) + if resp.StatusCode != fiber.StatusInternalServerError { + t.Fail() + } + + req = httptest.NewRequest("GET", "/metrics", nil) + resp, _ = app.Test(req, -1) + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + got := string(body) + want := `http_requests_total{method="GET",path="/public",service="test-service",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `http_requests_total{method="GET",path="/public/error/fiber",service="test-service",status_code="400"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `http_requests_total{method="GET",path="/public/error/unknown",service="test-service",status_code="500"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `http_request_duration_seconds_count{method="GET",path="/public",service="test-service",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `http_requests_in_progress_total{method="GET",service="test-service"} 0` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } +} + func TestMiddlewareOnRoute(t *testing.T) { + t.Parallel() app := fiber.New() prometheus := New("test-route") prefix := "/prefix/path" + app.Route(prefix, func(route fiber.Router) { prometheus.RegisterAt(route, "/metrics") }, "Prefixed Route") + app.Use(prometheus.Middleware) + app.Get("/", func(c *fiber.Ctx) error { return c.SendString("Hello World") }) + app.Get("/error/:type", func(ctx *fiber.Ctx) error { switch ctx.Params("type") { case "fiber": @@ -120,6 +201,7 @@ func TestMiddlewareOnRoute(t *testing.T) { return fiber.ErrInternalServerError } }) + req := httptest.NewRequest("GET", "/", nil) resp, _ := app.Test(req, -1) if resp.StatusCode != 200 { @@ -144,33 +226,34 @@ func TestMiddlewareOnRoute(t *testing.T) { body, _ := io.ReadAll(resp.Body) got := string(body) - want := `http_requests_total{method="GET",path="/",service="test-service",status_code="200"} 1` + want := `http_requests_total{method="GET",path="/",service="test-route",status_code="200"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } - want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="400"} 1` + want = `http_requests_total{method="GET",path="/error/fiber",service="test-route",status_code="400"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } - want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="500"} 1` + want = `http_requests_total{method="GET",path="/error/unknown",service="test-route",status_code="500"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } - want = `http_request_duration_seconds_count{method="GET",path="/",service="test-service",status_code="200"} 1` + want = `http_request_duration_seconds_count{method="GET",path="/",service="test-route",status_code="200"} 1` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } - want = `http_requests_in_progress_total{method="GET",service="test-service"} 0` + want = `http_requests_in_progress_total{method="GET",service="test-route"} 0` if !strings.Contains(got, want) { t.Errorf("got %s; want %s", got, want) } } func TestMiddlewareWithServiceName(t *testing.T) { + t.Parallel() app := fiber.New() prometheus := NewWith("unique-service", "my_service_with_name", "http") @@ -208,6 +291,7 @@ func TestMiddlewareWithServiceName(t *testing.T) { } func TestMiddlewareWithLabels(t *testing.T) { + t.Parallel() app := fiber.New() constLabels := map[string]string{ @@ -249,6 +333,7 @@ func TestMiddlewareWithLabels(t *testing.T) { } func TestMiddlewareWithBasicAuth(t *testing.T) { + t.Parallel() app := fiber.New() prometheus := New("basic-auth") @@ -284,6 +369,7 @@ func TestMiddlewareWithBasicAuth(t *testing.T) { } func TestMiddlewareWithCustomRegistry(t *testing.T) { + t.Parallel() app := fiber.New() registry := prometheus.NewRegistry() @@ -336,6 +422,7 @@ func TestMiddlewareWithCustomRegistry(t *testing.T) { } func TestCustomRegistryRegisterAt(t *testing.T) { + t.Parallel() app := fiber.New() registry := prometheus.NewRegistry() registry.Register(collectors.NewGoCollector()) @@ -378,3 +465,180 @@ func TestCustomRegistryRegisterAt(t *testing.T) { t.Errorf("got %s; want %s", got, want) } } + +func TestWithCacheMiddleware(t *testing.T) { + t.Parallel() + app := fiber.New() + registry := prometheus.NewRegistry() + registry.Register(collectors.NewGoCollector()) + registry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + fpCustom := NewWithRegistry(registry, "custom-registry", "custom_name", "http", nil) + fpCustom.RegisterAt(app, "/metrics") + + app.Use(fpCustom.Middleware) + app.Use(cache.New()) + + app.Get("/myPath", func(c *fiber.Ctx) error { + return c.SendString("Hello, world!") + }) + + for i := 0; i < 2; i++ { + req := httptest.NewRequest("GET", "/myPath", nil) + res, err := app.Test(req, -1) + if err != nil { + t.Fatal(fmt.Errorf("GET / failed: %w", err)) + } + defer res.Body.Close() + if res.StatusCode != 200 { + t.Fatal(fmt.Errorf("GET /: Status=%d", res.StatusCode)) + } + } + + req := httptest.NewRequest("GET", "/metrics", nil) + res, err := app.Test(req, -1) + if err != nil { + t.Fatal(fmt.Errorf("GET /metrics failed: %W", err)) + } + defer res.Body.Close() + if res.StatusCode != 200 { + t.Fatal(fmt.Errorf("GET /metrics: Status=%d", res.StatusCode)) + } + body, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(fmt.Errorf("GET /metrics: read body: %w", err)) + } + got := string(body) + want := `custom_name_http_requests_total{method="GET",path="/myPath",service="custom-registry",status_code="200"} 2` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `custom_name_http_cache_results{cache_result="hit",method="GET",path="/myPath",service="custom-registry",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `custom_name_http_cache_results{cache_result="miss",method="GET",path="/myPath",service="custom-registry",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } +} + +func TestWithCacheMiddlewareWithCustomKey(t *testing.T) { + t.Parallel() + app := fiber.New() + registry := prometheus.NewRegistry() + registry.Register(collectors.NewGoCollector()) + registry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + fpCustom := NewWithRegistry(registry, "custom-registry", "custom_name", "http", nil) + fpCustom.RegisterAt(app, "/metrics") + fpCustom.CustomCacheKey("my-custom-cache-header") + + app.Use(fpCustom.Middleware) + app.Use(cache.New( + cache.Config{ + CacheHeader: "my-custom-cache-header", + }, + )) + + app.Get("/myPath", func(c *fiber.Ctx) error { + return c.SendString("Hello, world!") + }) + + for i := 0; i < 2; i++ { + req := httptest.NewRequest("GET", "/myPath", nil) + res, err := app.Test(req, -1) + if err != nil { + t.Fatal(fmt.Errorf("GET / failed: %w", err)) + } + defer res.Body.Close() + if res.StatusCode != 200 { + t.Fatal(fmt.Errorf("GET /: Status=%d", res.StatusCode)) + } + } + + req := httptest.NewRequest("GET", "/metrics", nil) + res, err := app.Test(req, -1) + if err != nil { + t.Fatal(fmt.Errorf("GET /metrics failed: %W", err)) + } + defer res.Body.Close() + if res.StatusCode != 200 { + t.Fatal(fmt.Errorf("GET /metrics: Status=%d", res.StatusCode)) + } + body, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(fmt.Errorf("GET /metrics: read body: %w", err)) + } + got := string(body) + want := `custom_name_http_requests_total{method="GET",path="/myPath",service="custom-registry",status_code="200"} 2` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `custom_name_http_cache_results{cache_result="hit",method="GET",path="/myPath",service="custom-registry",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } + + want = `custom_name_http_cache_results{cache_result="miss",method="GET",path="/myPath",service="custom-registry",status_code="200"} 1` + if !strings.Contains(got, want) { + t.Errorf("got %s; want %s", got, want) + } +} + +func Benchmark_Middleware(b *testing.B) { + app := fiber.New() + + prometheus := New("test-benchmark") + prometheus.RegisterAt(app, "/metrics") + app.Use(prometheus.Middleware) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello World") + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +func Benchmark_Middleware_Parallel(b *testing.B) { + app := fiber.New() + + prometheus := New("test-benchmark") + prometheus.RegisterAt(app, "/metrics") + app.Use(prometheus.Middleware) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello World") + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/metrics") + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +}