├── .github ├── dependabot.yml └── workflows │ ├── linter.yaml │ ├── security.yaml │ ├── stale.yaml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── middleware.go └── middleware_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "04:00" 9 | open-pull-requests-limit: 10 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | time: "04:00" 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/workflows/linter.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: "0 7 * * *" 6 | 7 | name: Linter 8 | jobs: 9 | Golint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Fetch Repository 13 | uses: actions/checkout@v4 14 | - name: Run Golint 15 | uses: reviewdog/action-golangci-lint@v2 16 | with: 17 | golangci_lint_flags: "--tests=false" 18 | -------------------------------------------------------------------------------- /.github/workflows/security.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: "0 7 * * *" 6 | 7 | name: Security 8 | jobs: 9 | Gosec: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Fetch Repository 13 | uses: actions/checkout@v4 14 | - name: Run Gosec 15 | uses: securego/gosec@master 16 | with: 17 | args: ./... 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 14 | stale-issue-label: 'no-issue-activity' 15 | close-issue-message: 'This issue was closed because it has been stalled for 10 days with no activity.' 16 | stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 17 | stale-pr-label: 'no-pr-activity' 18 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 19 | days-before-issue-stale: 60 20 | days-before-pr-stale: 60 21 | days-before-issue-close: 10 22 | days-before-pr-close: 10 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: "0 7 * * *" 6 | 7 | name: Test 8 | jobs: 9 | Build: 10 | strategy: 11 | matrix: 12 | go-version: [1.22.x, 1.23.x, 1.24.x] 13 | platform: [ubuntu-latest, windows-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Fetch Repository 17 | uses: actions/checkout@v4 18 | - name: Install Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '${{ matrix.go-version }}' 22 | - name: Run Test 23 | run: go test -race -count=1 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## ChangeLog 2 | --- 3 | ## [2021-03-29] - v2.1.2 4 | ### Bug Fix: 5 | - Fixes #39, thanks @sunhailin-Leo 6 | 7 | ## [2021-02-08] - v2.1.1 8 | ### Enhancements: 9 | - Fix the LICENSE headers and introduce MIT License 10 | 11 | ## [2021-01-18] - v2.1.0 12 | ### Enhancements: 13 | - New method `NewWithLabels` now accepts a `map[string]string` so that users can create custom labels easily. 14 | - Bumped gofiber to v2.3.3 15 | 16 | ## [2020-11-27] - v2.0.1 17 | ### Enhancements: 18 | - Bug Fix: RequestInFlight won't decrease if ctx.Next() return error 19 | - Bumped gofiber to v2.2.1 20 | - Use go 1.15 21 | 22 | ## [2020-09-15] - v2.0.0 23 | ### Enhancements: 24 | - Support gofiber-v2 25 | - New import path would be github.com/ansrivas/fiberprometheus/v2 26 | 27 | 28 | ## [2020-07-08] - 0.3.2 29 | ### Enhancements: 30 | - Upgrade gofiber to 1.14.4 31 | 32 | ## [2020-07-08] - 0.3.0 33 | ### Enhancements: 34 | - Support a new method to provide a namespace and a subsystem for the service 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Ankur Srivastava and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE**: 🚨 We are currently migrating this middleware to the official gofiber/contrib repo, official link etc. will be posted soon. 2 | 3 | 4 | 5 | # fiberprometheus 6 | 7 | Prometheus middleware for [Fiber](https://github.com/gofiber/fiber). 8 | 9 | **Note: Requires Go 1.22 and above** 10 | 11 | ![Release](https://img.shields.io/github/release/ansrivas/fiberprometheus.svg) 12 | [![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) 13 | ![Test](https://github.com/ansrivas/fiberprometheus/workflows/Test/badge.svg) 14 | ![Security](https://github.com/ansrivas/fiberprometheus/workflows/Security/badge.svg) 15 | ![Linter](https://github.com/ansrivas/fiberprometheus/workflows/Linter/badge.svg) 16 | 17 | Following metrics are available by default: 18 | 19 | ```text 20 | http_requests_total 21 | http_request_duration_seconds 22 | http_requests_in_progress_total 23 | ``` 24 | 25 | ### Install v2 26 | 27 | ```console 28 | go get -u github.com/gofiber/fiber/v2 29 | go get -u github.com/ansrivas/fiberprometheus/v2 30 | ``` 31 | 32 | ### Example using v2 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "github.com/ansrivas/fiberprometheus/v2" 39 | "github.com/gofiber/fiber/v2" 40 | ) 41 | 42 | func main() { 43 | app := fiber.New() 44 | 45 | // This here will appear as a label, one can also use 46 | // fiberprometheus.NewWith(servicename, namespace, subsystem ) 47 | // or 48 | // labels := map[string]string{"custom_label1":"custom_value1", "custom_label2":"custom_value2"} 49 | // fiberprometheus.NewWithLabels(labels, namespace, subsystem ) 50 | prometheus := fiberprometheus.New("my-service-name") 51 | prometheus.RegisterAt(app, "/metrics") 52 | prometheus.SetSkipPaths([]string{"/ping"}) // Optional: Remove some paths from metrics 53 | prometheus.SetIgnoreStatusCodes([]int{401, 403, 404}) // Optional: Skip metrics for these status codes 54 | app.Use(prometheus.Middleware) 55 | 56 | app.Get("/", func(c *fiber.Ctx) error { 57 | return c.SendString("Hello World") 58 | }) 59 | 60 | app.Get("/ping", func(c *fiber.Ctx) error { 61 | return c.SendString("pong") 62 | }) 63 | 64 | app.Post("/some", func(c *fiber.Ctx) error { 65 | return c.SendString("Welcome!") 66 | }) 67 | 68 | app.Listen(":3000") 69 | } 70 | ``` 71 | 72 | ### Result 73 | 74 | - Hit the default url at http://localhost:3000 75 | - Navigate to http://localhost:3000/metrics 76 | - Metrics are recorded only for routes registered with Fiber; unknown routes are skipped automatically 77 | 78 | ### Grafana Dashboard 79 | 80 | - https://grafana.com/grafana/dashboards/14331 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ansrivas/fiberprometheus/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.8 7 | github.com/prometheus/client_golang v1.22.0 8 | github.com/valyala/fasthttp v1.62.0 9 | go.opentelemetry.io/otel v1.36.0 10 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 11 | go.opentelemetry.io/otel/sdk v1.36.0 12 | go.opentelemetry.io/otel/trace v1.36.0 13 | ) 14 | 15 | require ( 16 | github.com/andybalholm/brotli v1.1.1 // indirect 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/go-logr/stdr v1.2.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/klauspost/compress v1.18.0 // indirect 23 | github.com/mattn/go-colorable v0.1.13 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/mattn/go-runewidth v0.0.16 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/prometheus/client_model v0.6.1 // indirect 28 | github.com/prometheus/common v0.62.0 // indirect 29 | github.com/prometheus/procfs v0.15.1 // indirect 30 | github.com/rivo/uniseg v0.4.7 // indirect 31 | github.com/valyala/bytebufferpool v1.0.0 // indirect 32 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 33 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 34 | golang.org/x/sys v0.33.0 // indirect 35 | google.golang.org/protobuf v1.36.5 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 10 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 11 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 12 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 13 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 14 | github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= 15 | github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 16 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 17 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 18 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 19 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 21 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 22 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 23 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 24 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 25 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 26 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 27 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 28 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 29 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 30 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 36 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 37 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 38 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 39 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 40 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 41 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 42 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 44 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 45 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 46 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 47 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 48 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 49 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 50 | github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= 51 | github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= 52 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 53 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 54 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 55 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 56 | go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= 57 | go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= 58 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= 59 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= 60 | go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= 61 | go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= 62 | go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= 63 | go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= 64 | go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= 65 | go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= 66 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 67 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 68 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 71 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 72 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 73 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 74 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021-present Ankur Srivastava and Contributors 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package fiberprometheus 23 | 24 | import ( 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "time" 29 | 30 | "github.com/gofiber/fiber/v2" 31 | "github.com/gofiber/fiber/v2/middleware/adaptor" 32 | "github.com/gofiber/fiber/v2/utils" 33 | "github.com/prometheus/client_golang/prometheus" 34 | "github.com/prometheus/client_golang/prometheus/promauto" 35 | "github.com/prometheus/client_golang/prometheus/promhttp" 36 | 37 | "go.opentelemetry.io/otel/trace" 38 | ) 39 | 40 | // FiberPrometheus ... 41 | type FiberPrometheus struct { 42 | gatherer prometheus.Gatherer 43 | requestsTotal *prometheus.CounterVec 44 | requestDuration *prometheus.HistogramVec 45 | requestInFlight *prometheus.GaugeVec 46 | defaultURL string 47 | skipPaths map[string]bool 48 | ignoreStatusCodes map[int]bool 49 | registeredRoutes map[string]struct{} 50 | routesOnce sync.Once 51 | } 52 | 53 | func create(registry prometheus.Registerer, serviceName, namespace, subsystem string, labels map[string]string) *FiberPrometheus { 54 | if registry == nil { 55 | registry = prometheus.NewRegistry() 56 | } 57 | 58 | constLabels := make(prometheus.Labels) 59 | if serviceName != "" { 60 | constLabels["service"] = serviceName 61 | } 62 | for label, value := range labels { 63 | constLabels[label] = value 64 | } 65 | 66 | counter := promauto.With(registry).NewCounterVec( 67 | prometheus.CounterOpts{ 68 | Name: prometheus.BuildFQName(namespace, subsystem, "requests_total"), 69 | Help: "Count all http requests by status code, method and path.", 70 | ConstLabels: constLabels, 71 | }, 72 | []string{"status_code", "method", "path"}, 73 | ) 74 | 75 | histogram := promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ 76 | Name: prometheus.BuildFQName(namespace, subsystem, "request_duration_seconds"), 77 | Help: "Duration of all HTTP requests by status code, method and path.", 78 | ConstLabels: constLabels, 79 | Buckets: []float64{ 80 | 0.000000001, // 1ns 81 | 0.000000002, 82 | 0.000000005, 83 | 0.00000001, // 10ns 84 | 0.00000002, 85 | 0.00000005, 86 | 0.0000001, // 100ns 87 | 0.0000002, 88 | 0.0000005, 89 | 0.000001, // 1µs 90 | 0.000002, 91 | 0.000005, 92 | 0.00001, // 10µs 93 | 0.00002, 94 | 0.00005, 95 | 0.0001, // 100µs 96 | 0.0002, 97 | 0.0005, 98 | 0.001, // 1ms 99 | 0.002, 100 | 0.005, 101 | 0.01, // 10ms 102 | 0.02, 103 | 0.05, 104 | 0.1, // 100 ms 105 | 0.2, 106 | 0.5, 107 | 1.0, // 1s 108 | 2.0, 109 | 5.0, 110 | 10.0, // 10s 111 | 15.0, 112 | 20.0, 113 | 30.0, 114 | 60.0, // 1m 115 | }, 116 | }, 117 | []string{"status_code", "method", "path"}, 118 | ) 119 | 120 | gauge := promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{ 121 | Name: prometheus.BuildFQName(namespace, subsystem, "requests_in_progress_total"), 122 | Help: "All the requests in progress", 123 | ConstLabels: constLabels, 124 | }, []string{"method"}) 125 | 126 | // If the registerer is also a gatherer, use it, falling back to the 127 | // DefaultGatherer. 128 | gatherer, ok := registry.(prometheus.Gatherer) 129 | if !ok { 130 | gatherer = prometheus.DefaultGatherer 131 | } 132 | 133 | return &FiberPrometheus{ 134 | gatherer: gatherer, 135 | requestsTotal: counter, 136 | requestDuration: histogram, 137 | requestInFlight: gauge, 138 | defaultURL: "/metrics", 139 | } 140 | } 141 | 142 | // New creates a new instance of FiberPrometheus middleware 143 | // serviceName is available as a const label 144 | func New(serviceName string) *FiberPrometheus { 145 | return create(nil, serviceName, "http", "", nil) 146 | } 147 | 148 | // NewWith creates a new instance of FiberPrometheus middleware but with an ability 149 | // to pass namespace and a custom subsystem 150 | // Here serviceName is created as a constant-label for the metrics 151 | // Namespace, subsystem get prefixed to the metrics. 152 | // 153 | // For e.g. namespace = "my_app", subsystem = "http" then metrics would be 154 | // `my_app_http_requests_total{...,service= "serviceName"}` 155 | func NewWith(serviceName, namespace, subsystem string) *FiberPrometheus { 156 | return create(nil, serviceName, namespace, subsystem, nil) 157 | } 158 | 159 | // NewWithLabels creates a new instance of FiberPrometheus middleware but with an ability 160 | // to pass namespace and a custom subsystem 161 | // Here labels are created as a constant-labels for the metrics 162 | // Namespace, subsystem get prefixed to the metrics. 163 | // 164 | // For e.g. namespace = "my_app", subsystem = "http" and labels = map[string]string{"key1": "value1", "key2":"value2"} 165 | // then then metrics would become 166 | // `my_app_http_requests_total{...,key1= "value1", key2= "value2" }` 167 | func NewWithLabels(labels map[string]string, namespace, subsystem string) *FiberPrometheus { 168 | return create(nil, "", namespace, subsystem, labels) 169 | } 170 | 171 | // NewWithRegistry creates a new instance of FiberPrometheus middleware but with an ability 172 | // to pass a custom registry, serviceName, namespace, subsystem and labels 173 | // Here labels are created as a constant-labels for the metrics 174 | // Namespace, subsystem get prefixed to the metrics. 175 | // 176 | // For e.g. namespace = "my_app", subsystem = "http" and labels = map[string]string{"key1": "value1", "key2":"value2"} 177 | // then then metrics would become 178 | // `my_app_http_requests_total{...,key1= "value1", key2= "value2" }` 179 | func NewWithRegistry(registry prometheus.Registerer, serviceName, namespace, subsystem string, labels map[string]string) *FiberPrometheus { 180 | return create(registry, serviceName, namespace, subsystem, labels) 181 | } 182 | 183 | // NewWithDefaultRegistry creates a new instance of FiberPrometheus middleware using the default prometheus registry 184 | func NewWithDefaultRegistry(serviceName string) *FiberPrometheus { 185 | return create(prometheus.DefaultRegisterer, serviceName, "http", "", nil) 186 | } 187 | 188 | // RegisterAt will register the prometheus handler at a given URL 189 | func (ps *FiberPrometheus) RegisterAt(app fiber.Router, url string, handlers ...fiber.Handler) { 190 | ps.defaultURL = url 191 | 192 | h := append(handlers, adaptor.HTTPHandler(promhttp.HandlerFor(ps.gatherer, promhttp.HandlerOpts{ 193 | EnableOpenMetrics: true, 194 | }))) 195 | app.Get(ps.defaultURL, h...) 196 | } 197 | 198 | // SetSkipPaths allows to set the paths that should be skipped from the metrics 199 | func (ps *FiberPrometheus) SetSkipPaths(paths []string) { 200 | if ps.skipPaths == nil { 201 | ps.skipPaths = make(map[string]bool) 202 | } 203 | for _, path := range paths { 204 | ps.skipPaths[path] = true 205 | } 206 | } 207 | 208 | // SetIgnoreStatusCodes allows ignoring specific status codes from being recorded in metrics 209 | func (ps *FiberPrometheus) SetIgnoreStatusCodes(codes []int) { 210 | if ps.ignoreStatusCodes == nil { 211 | ps.ignoreStatusCodes = make(map[int]bool) 212 | } 213 | for _, code := range codes { 214 | ps.ignoreStatusCodes[code] = true 215 | } 216 | } 217 | 218 | // Middleware is the actual default middleware implementation 219 | func (ps *FiberPrometheus) Middleware(ctx *fiber.Ctx) error { 220 | // Retrieve the request method 221 | method := utils.CopyString(ctx.Method()) 222 | 223 | // Increment the in-flight gauge 224 | ps.requestInFlight.WithLabelValues(method).Inc() 225 | defer func() { 226 | ps.requestInFlight.WithLabelValues(method).Dec() 227 | }() 228 | 229 | // Start metrics timer 230 | start := time.Now() 231 | 232 | // Continue stack 233 | err := ctx.Next() 234 | 235 | // Get the route path 236 | routePath := utils.CopyString(ctx.Route().Path) 237 | 238 | // If the route path is empty, use the current path 239 | if routePath == "/" { 240 | routePath = utils.CopyString(ctx.Path()) 241 | } 242 | 243 | // Normalize the path 244 | if routePath != "" && routePath != "/" { 245 | routePath = normalizePath(routePath) 246 | } 247 | 248 | // Build registered routes map once 249 | ps.routesOnce.Do(func() { 250 | ps.registeredRoutes = make(map[string]struct{}) 251 | for _, r := range ctx.App().GetRoutes(true) { 252 | p := r.Path 253 | if p != "" && p != "/" { 254 | p = normalizePath(p) 255 | } 256 | ps.registeredRoutes[r.Method+" "+p] = struct{}{} 257 | } 258 | }) 259 | 260 | // Skip metrics for routes that are not registered 261 | if _, ok := ps.registeredRoutes[method+" "+routePath]; !ok { 262 | return err 263 | } 264 | 265 | // Check if the normalized path should be skipped 266 | if ps.skipPaths[routePath] { 267 | return nil 268 | } 269 | 270 | // Determine status code from stack 271 | status := fiber.StatusInternalServerError 272 | if err != nil { 273 | if e, ok := err.(*fiber.Error); ok { 274 | status = e.Code 275 | } 276 | } else { 277 | status = ctx.Response().StatusCode() 278 | } 279 | 280 | // Convert status code to string 281 | statusCode := strconv.Itoa(status) 282 | 283 | // Skip metrics for ignored status codes 284 | if ps.ignoreStatusCodes[status] { 285 | return err 286 | } 287 | 288 | // Update metrics 289 | ps.requestsTotal.WithLabelValues(statusCode, method, routePath).Inc() 290 | 291 | // Observe the Request Duration 292 | elapsed := float64(time.Since(start).Nanoseconds()) / 1e9 293 | 294 | traceID := trace.SpanContextFromContext(ctx.UserContext()).TraceID() 295 | histogram := ps.requestDuration.WithLabelValues(statusCode, method, routePath) 296 | 297 | if traceID.IsValid() { 298 | if histogramExemplar, ok := histogram.(prometheus.ExemplarObserver); ok { 299 | histogramExemplar.ObserveWithExemplar(elapsed, prometheus.Labels{"traceID": traceID.String()}) 300 | } 301 | 302 | return err 303 | } 304 | 305 | histogram.Observe(elapsed) 306 | 307 | return err 308 | } 309 | 310 | // normalizePath will remove the trailing slash from the route path 311 | func normalizePath(routePath string) string { 312 | normalized := strings.TrimRight(routePath, "/") 313 | if normalized == "" { 314 | return "/" 315 | } 316 | return normalized 317 | } 318 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021-present Ankur Srivastava and Contributors 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in all 12 | // copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | // SOFTWARE. 21 | 22 | package fiberprometheus 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | "io" 28 | "net/http/httptest" 29 | "os" 30 | "regexp" 31 | "strconv" 32 | "strings" 33 | "sync" 34 | "testing" 35 | "time" 36 | 37 | "github.com/gofiber/fiber/v2" 38 | "github.com/gofiber/fiber/v2/middleware/basicauth" 39 | "github.com/prometheus/client_golang/prometheus" 40 | "github.com/prometheus/client_golang/prometheus/collectors" 41 | "github.com/prometheus/client_golang/prometheus/promhttp" 42 | "github.com/valyala/fasthttp" 43 | 44 | "go.opentelemetry.io/otel" 45 | "go.opentelemetry.io/otel/attribute" 46 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 47 | "go.opentelemetry.io/otel/propagation" 48 | "go.opentelemetry.io/otel/sdk/resource" 49 | "go.opentelemetry.io/otel/sdk/trace" 50 | ) 51 | 52 | // Helper Functions for TestMiddlewareWithExamplar 53 | func otelTracingInit(t *testing.T) { 54 | // Add trace resource attributes 55 | res, err := resource.New( 56 | context.Background(), 57 | resource.WithTelemetrySDK(), 58 | resource.WithOS(), 59 | resource.WithHost(), 60 | resource.WithAttributes(attribute.String("service.name", "fiber")), 61 | ) 62 | if err != nil { 63 | t.Errorf("cant create otlp resource: %v", err) 64 | t.Fail() 65 | } 66 | 67 | // Create stdout exporter 68 | traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) 69 | if err != nil { 70 | t.Errorf("cant create otlp exporter: %v", err) 71 | t.Fail() 72 | } 73 | 74 | // Create OTEL trace provider 75 | tp := trace.NewTracerProvider( 76 | trace.WithBatcher(traceExporter), 77 | trace.WithResource(res), 78 | ) 79 | 80 | os.Setenv("OTEL_TRACES_EXPORTER", "otlp") 81 | os.Setenv("OTEL_TRACES_SAMPLER", "always_on") 82 | 83 | // Set OTLP Provider 84 | otel.SetTracerProvider(tp) 85 | 86 | // SetTextMapPropagator configures the OpenTelemetry text map propagator 87 | // using a composite of TraceContext and Baggage propagators. 88 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 89 | } 90 | 91 | // Helper Functions for TestMiddlewareWithExamplar 92 | func tracingMiddleware(c *fiber.Ctx) error { 93 | // Create OTLP tracer 94 | tracer := otel.Tracer("FOCUZ") 95 | 96 | // Create a new context with cancellation capability from Fiber context 97 | ctx, cancel := context.WithCancel(c.UserContext()) 98 | 99 | // Start a new span with attributes for tracing the current request 100 | ctx, span := tracer.Start(ctx, c.Route().Name) 101 | 102 | // Ensure the span is ended and context is cancelled when the request completes 103 | defer span.End() 104 | defer cancel() 105 | 106 | // Set OTLP context 107 | c.SetUserContext(ctx) 108 | 109 | // Continue with the next middleware/handler 110 | return c.Next() 111 | } 112 | 113 | func TestMiddleware(t *testing.T) { 114 | t.Parallel() 115 | 116 | app := fiber.New() 117 | prometheus := New("test-service") 118 | prometheus.RegisterAt(app, "/metrics") 119 | app.Use(prometheus.Middleware) 120 | app.Get("/", func(c *fiber.Ctx) error { 121 | return c.SendString("Hello World") 122 | }) 123 | app.Get("/error/:type", func(ctx *fiber.Ctx) error { 124 | switch ctx.Params("type") { 125 | case "fiber": 126 | return fiber.ErrBadRequest 127 | default: 128 | return fiber.ErrInternalServerError 129 | } 130 | }) 131 | req := httptest.NewRequest("GET", "/", nil) 132 | resp, _ := app.Test(req, -1) 133 | if resp.StatusCode != 200 { 134 | t.Fail() 135 | } 136 | 137 | req = httptest.NewRequest("GET", "/error/fiber", nil) 138 | resp, _ = app.Test(req, -1) 139 | if resp.StatusCode != fiber.StatusBadRequest { 140 | t.Fail() 141 | } 142 | 143 | req = httptest.NewRequest("GET", "/error/unknown", nil) 144 | resp, _ = app.Test(req, -1) 145 | if resp.StatusCode != fiber.StatusInternalServerError { 146 | t.Fail() 147 | } 148 | 149 | req = httptest.NewRequest("GET", "/metrics", nil) 150 | resp, _ = app.Test(req, -1) 151 | defer resp.Body.Close() 152 | body, _ := io.ReadAll(resp.Body) 153 | got := string(body) 154 | 155 | // Check Metrics Response 156 | want := `http_requests_total{method="GET",path="/",service="test-service",status_code="200"} 1` 157 | if !strings.Contains(got, want) { 158 | t.Errorf("got %s; want %s", got, want) 159 | } 160 | 161 | want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="400"} 1` 162 | if !strings.Contains(got, want) { 163 | t.Errorf("got %s; want %s", got, want) 164 | } 165 | 166 | want = `http_requests_total{method="GET",path="/error/:type",service="test-service",status_code="500"} 1` 167 | if !strings.Contains(got, want) { 168 | t.Errorf("got %s; want %s", got, want) 169 | } 170 | 171 | want = `http_request_duration_seconds_count{method="GET",path="/",service="test-service",status_code="200"} 1` 172 | if !strings.Contains(got, want) { 173 | t.Errorf("got %s; want %s", got, want) 174 | } 175 | 176 | want = `http_requests_in_progress_total{method="GET",service="test-service"} 0` 177 | if !strings.Contains(got, want) { 178 | t.Errorf("got %s; want %s", got, want) 179 | } 180 | } 181 | 182 | func TestMiddlewareWithExamplar(t *testing.T) { 183 | t.Parallel() 184 | otelTracingInit(t) 185 | 186 | app := fiber.New() 187 | prometheus := New("test-service") 188 | prometheus.RegisterAt(app, "/metrics") 189 | app.Use(tracingMiddleware) 190 | app.Use(prometheus.Middleware) 191 | app.Get("/", func(c *fiber.Ctx) error { 192 | return c.SendString("Hello World") 193 | }) 194 | 195 | req := httptest.NewRequest("GET", "/", nil) 196 | resp, _ := app.Test(req, -1) 197 | if resp.StatusCode != 200 { 198 | t.Fail() 199 | } 200 | 201 | req = httptest.NewRequest("GET", "/metrics", nil) 202 | req.Header.Set("Accept", "application/openmetrics-text") 203 | resp, _ = app.Test(req, -1) 204 | defer resp.Body.Close() 205 | body, _ := io.ReadAll(resp.Body) 206 | got := string(body) 207 | 208 | // Check Metrics Response 209 | want := `http_request_duration_seconds_bucket{method="GET",path="/",service="test-service",status_code="200",le=".*"} 1 # {traceID=".*"} .*` 210 | re := regexp.MustCompile(want) 211 | if !re.MatchString(got) { 212 | t.Errorf("got %s; want pattern %s", got, want) 213 | } 214 | } 215 | 216 | func TestMiddlewareWithSkipPath(t *testing.T) { 217 | t.Parallel() 218 | 219 | app := fiber.New() 220 | prometheus := New("test-service") 221 | prometheus.RegisterAt(app, "/metrics") 222 | prometheus.SetSkipPaths([]string{"/healthz", "/livez"}) 223 | app.Use(prometheus.Middleware) 224 | app.Get("/", func(c *fiber.Ctx) error { 225 | return c.SendString("Hello World") 226 | }) 227 | app.Get("/healthz", func(c *fiber.Ctx) error { 228 | return c.SendString("Hello World") 229 | }) 230 | app.Get("/livez", func(c *fiber.Ctx) error { 231 | return c.SendString("Hello World") 232 | }) 233 | 234 | // Make requests 235 | req := httptest.NewRequest("GET", "/", nil) 236 | resp, _ := app.Test(req, -1) 237 | if resp.StatusCode != 200 { 238 | t.Fail() 239 | } 240 | 241 | req = httptest.NewRequest("GET", "/healthz", nil) 242 | resp, _ = app.Test(req, -1) 243 | if resp.StatusCode != 200 { 244 | t.Fail() 245 | } 246 | 247 | req = httptest.NewRequest("GET", "/livez", nil) 248 | resp, _ = app.Test(req, -1) 249 | if resp.StatusCode != 200 { 250 | t.Fail() 251 | } 252 | 253 | // Check Metrics Response 254 | req = httptest.NewRequest("GET", "/metrics", nil) 255 | resp, _ = app.Test(req, -1) 256 | defer resp.Body.Close() 257 | 258 | body, _ := io.ReadAll(resp.Body) 259 | got := string(body) 260 | want := `http_requests_total{method="GET",path="/",service="test-service",status_code="200"} 1` 261 | if !strings.Contains(got, want) { 262 | t.Errorf("got %s; want %s", got, want) 263 | } 264 | 265 | want = `http_requests_total{method="GET",path="/healthz",service="test-service",status_code="200"} 1` 266 | if strings.Contains(got, want) { 267 | t.Errorf("got %s; not want %s", got, want) 268 | } 269 | 270 | want = `http_requests_total{method="GET",path="/metrics",service="test-service",status_code="200"}` 271 | if strings.Contains(got, want) { 272 | t.Errorf("got %s; not want %s", got, want) 273 | } 274 | 275 | want = `http_requests_total{method="GET",path="/livez",service="test-service",status_code="200"} 1` 276 | if strings.Contains(got, want) { 277 | t.Errorf("got %s; not want %s", got, want) 278 | } 279 | 280 | want = `http_request_duration_seconds_count{method="GET",path="/",service="test-service",status_code="200"} 1` 281 | if !strings.Contains(got, want) { 282 | t.Errorf("got %s; not want %s", got, want) 283 | } 284 | 285 | want = `http_requests_in_progress_total{method="GET",service="test-service"} 0` 286 | if !strings.Contains(got, want) { 287 | t.Errorf("got %s; not want %s", got, want) 288 | } 289 | } 290 | 291 | func TestMiddlewareWithGroup(t *testing.T) { 292 | t.Parallel() 293 | 294 | app := fiber.New() 295 | prometheus := New("test-service") 296 | prometheus.RegisterAt(app, "/metrics") 297 | app.Use(prometheus.Middleware) 298 | 299 | // Define Group 300 | public := app.Group("/public") 301 | 302 | // Define Group Routes 303 | public.Get("/", func(c *fiber.Ctx) error { 304 | return c.SendString("Hello World") 305 | }) 306 | public.Get("/error/:type", func(ctx *fiber.Ctx) error { 307 | switch ctx.Params("type") { 308 | case "fiber": 309 | return fiber.ErrBadRequest 310 | default: 311 | return fiber.ErrInternalServerError 312 | } 313 | }) 314 | req := httptest.NewRequest("GET", "/public", nil) 315 | resp, _ := app.Test(req, -1) 316 | if resp.StatusCode != 200 { 317 | t.Fail() 318 | } 319 | 320 | req = httptest.NewRequest("GET", "/public/error/fiber", nil) 321 | resp, _ = app.Test(req, -1) 322 | if resp.StatusCode != fiber.StatusBadRequest { 323 | t.Fail() 324 | } 325 | 326 | req = httptest.NewRequest("GET", "/public/error/unknown", nil) 327 | resp, _ = app.Test(req, -1) 328 | if resp.StatusCode != fiber.StatusInternalServerError { 329 | t.Fail() 330 | } 331 | 332 | req = httptest.NewRequest("GET", "/metrics", nil) 333 | resp, _ = app.Test(req, -1) 334 | defer resp.Body.Close() 335 | body, _ := io.ReadAll(resp.Body) 336 | got := string(body) 337 | 338 | // Check Metrics Response 339 | want := `http_requests_total{method="GET",path="/public",service="test-service",status_code="200"} 1` 340 | if !strings.Contains(got, want) { 341 | t.Errorf("got %s; want %s", got, want) 342 | } 343 | 344 | want = `http_requests_total{method="GET",path="/public/error/:type",service="test-service",status_code="400"} 1` 345 | if !strings.Contains(got, want) { 346 | t.Errorf("got %s; want %s", got, want) 347 | } 348 | 349 | want = `http_requests_total{method="GET",path="/public/error/:type",service="test-service",status_code="500"} 1` 350 | if !strings.Contains(got, want) { 351 | t.Errorf("got %s; want %s", got, want) 352 | } 353 | 354 | want = `http_request_duration_seconds_count{method="GET",path="/public",service="test-service",status_code="200"} 1` 355 | if !strings.Contains(got, want) { 356 | t.Errorf("got %s; want %s", got, want) 357 | } 358 | 359 | want = `http_requests_in_progress_total{method="GET",service="test-service"} 0` 360 | if !strings.Contains(got, want) { 361 | t.Errorf("got %s; want %s", got, want) 362 | } 363 | } 364 | 365 | func TestMiddlewareOnRoute(t *testing.T) { 366 | t.Parallel() 367 | 368 | app := fiber.New() 369 | prometheus := New("test-route") 370 | prefix := "/prefix/path" 371 | 372 | app.Route(prefix, func(route fiber.Router) { 373 | prometheus.RegisterAt(route, "/metrics") 374 | }, "Prefixed Route") 375 | app.Use(prometheus.Middleware) 376 | 377 | app.Get("/", func(c *fiber.Ctx) error { 378 | return c.SendString("Hello World") 379 | }) 380 | 381 | app.Get("/error/:type", func(ctx *fiber.Ctx) error { 382 | switch ctx.Params("type") { 383 | case "fiber": 384 | return fiber.ErrBadRequest 385 | default: 386 | return fiber.ErrInternalServerError 387 | } 388 | }) 389 | 390 | req := httptest.NewRequest("GET", "/", nil) 391 | resp, _ := app.Test(req, -1) 392 | if resp.StatusCode != 200 { 393 | t.Fail() 394 | } 395 | 396 | req = httptest.NewRequest("GET", "/error/fiber", nil) 397 | resp, _ = app.Test(req, -1) 398 | if resp.StatusCode != fiber.StatusBadRequest { 399 | t.Fail() 400 | } 401 | 402 | req = httptest.NewRequest("GET", "/error/unknown", nil) 403 | resp, _ = app.Test(req, -1) 404 | if resp.StatusCode != fiber.StatusInternalServerError { 405 | t.Fail() 406 | } 407 | 408 | req = httptest.NewRequest("GET", prefix+"/metrics", nil) 409 | resp, _ = app.Test(req, -1) 410 | defer resp.Body.Close() 411 | 412 | body, _ := io.ReadAll(resp.Body) 413 | got := string(body) 414 | want := `http_requests_total{method="GET",path="/",service="test-route",status_code="200"} 1` 415 | if !strings.Contains(got, want) { 416 | t.Errorf("got %s; want %s", got, want) 417 | } 418 | 419 | want = `http_requests_total{method="GET",path="/error/:type",service="test-route",status_code="400"} 1` 420 | if !strings.Contains(got, want) { 421 | t.Errorf("got %s; want %s", got, want) 422 | } 423 | 424 | want = `http_requests_total{method="GET",path="/error/:type",service="test-route",status_code="500"} 1` 425 | if !strings.Contains(got, want) { 426 | t.Errorf("got %s; want %s", got, want) 427 | } 428 | 429 | want = `http_request_duration_seconds_count{method="GET",path="/",service="test-route",status_code="200"} 1` 430 | if !strings.Contains(got, want) { 431 | t.Errorf("got %s; want %s", got, want) 432 | } 433 | 434 | want = `http_requests_in_progress_total{method="GET",service="test-route"} 0` 435 | if !strings.Contains(got, want) { 436 | t.Errorf("got %s; want %s", got, want) 437 | } 438 | } 439 | 440 | func TestMiddlewareWithServiceName(t *testing.T) { 441 | t.Parallel() 442 | 443 | app := fiber.New() 444 | prometheus := NewWith("unique-service", "my_service_with_name", "http") 445 | prometheus.RegisterAt(app, "/metrics") 446 | app.Use(prometheus.Middleware) 447 | 448 | app.Get("/", func(c *fiber.Ctx) error { 449 | return c.SendString("Hello World") 450 | }) 451 | req := httptest.NewRequest("GET", "/", nil) 452 | resp, _ := app.Test(req, -1) 453 | if resp.StatusCode != 200 { 454 | t.Fail() 455 | } 456 | 457 | req = httptest.NewRequest("GET", "/metrics", nil) 458 | resp, _ = app.Test(req, -1) 459 | defer resp.Body.Close() 460 | 461 | body, _ := io.ReadAll(resp.Body) 462 | got := string(body) 463 | want := `my_service_with_name_http_requests_total{method="GET",path="/",service="unique-service",status_code="200"} 1` 464 | if !strings.Contains(got, want) { 465 | t.Errorf("got %s; want %s", got, want) 466 | } 467 | 468 | want = `my_service_with_name_http_request_duration_seconds_count{method="GET",path="/",service="unique-service",status_code="200"} 1` 469 | if !strings.Contains(got, want) { 470 | t.Errorf("got %s; want %s", got, want) 471 | } 472 | 473 | want = `my_service_with_name_http_requests_in_progress_total{method="GET",service="unique-service"} 0` 474 | if !strings.Contains(got, want) { 475 | t.Errorf("got %s; want %s", got, want) 476 | } 477 | } 478 | 479 | func TestMiddlewareWithLabels(t *testing.T) { 480 | t.Parallel() 481 | 482 | app := fiber.New() 483 | constLabels := map[string]string{ 484 | "customkey1": "customvalue1", 485 | "customkey2": "customvalue2", 486 | } 487 | prometheus := NewWithLabels(constLabels, "my_service", "http") 488 | prometheus.RegisterAt(app, "/metrics") 489 | app.Use(prometheus.Middleware) 490 | 491 | app.Get("/", func(c *fiber.Ctx) error { 492 | return c.SendString("Hello World") 493 | }) 494 | req := httptest.NewRequest("GET", "/", nil) 495 | resp, _ := app.Test(req, -1) 496 | if resp.StatusCode != 200 { 497 | t.Fail() 498 | } 499 | 500 | req = httptest.NewRequest("GET", "/metrics", nil) 501 | resp, _ = app.Test(req, -1) 502 | defer resp.Body.Close() 503 | 504 | body, _ := io.ReadAll(resp.Body) 505 | got := string(body) 506 | want := `my_service_http_requests_total{customkey1="customvalue1",customkey2="customvalue2",method="GET",path="/",status_code="200"} 1` 507 | if !strings.Contains(got, want) { 508 | t.Errorf("got %s; want %s", got, want) 509 | } 510 | 511 | want = `my_service_http_request_duration_seconds_count{customkey1="customvalue1",customkey2="customvalue2",method="GET",path="/",status_code="200"} 1` 512 | if !strings.Contains(got, want) { 513 | t.Errorf("got %s; want %s", got, want) 514 | } 515 | 516 | want = `my_service_http_requests_in_progress_total{customkey1="customvalue1",customkey2="customvalue2",method="GET"} 0` 517 | if !strings.Contains(got, want) { 518 | t.Errorf("got %s; want %s", got, want) 519 | } 520 | } 521 | 522 | func TestMiddlewareWithBasicAuth(t *testing.T) { 523 | t.Parallel() 524 | 525 | app := fiber.New() 526 | prometheus := New("basic-auth") 527 | prometheus.RegisterAt(app, "/metrics", basicauth.New(basicauth.Config{ 528 | Users: map[string]string{ 529 | "prometheus": "password", 530 | }, 531 | })) 532 | 533 | app.Use(prometheus.Middleware) 534 | 535 | app.Get("/", func(c *fiber.Ctx) error { 536 | return c.SendString("Hello World") 537 | }) 538 | 539 | req := httptest.NewRequest("GET", "/", nil) 540 | resp, _ := app.Test(req, -1) 541 | if resp.StatusCode != 200 { 542 | t.Fail() 543 | } 544 | 545 | req = httptest.NewRequest("GET", "/metrics", nil) 546 | resp, _ = app.Test(req, -1) 547 | if resp.StatusCode != 401 { 548 | t.Fail() 549 | } 550 | 551 | req.SetBasicAuth("prometheus", "password") 552 | resp, _ = app.Test(req, -1) 553 | if resp.StatusCode != 200 { 554 | t.Fail() 555 | } 556 | } 557 | 558 | func TestMiddlewareWithCustomRegistry(t *testing.T) { 559 | t.Parallel() 560 | 561 | app := fiber.New() 562 | registry := prometheus.NewRegistry() 563 | srv := httptest.NewServer(promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) 564 | t.Cleanup(srv.Close) 565 | promfiber := NewWithRegistry(registry, "unique-service", "my_service_with_name", "http", nil) 566 | app.Use(promfiber.Middleware) 567 | 568 | app.Get("/", func(c *fiber.Ctx) error { 569 | return c.SendString("Hello World") 570 | }) 571 | 572 | req := httptest.NewRequest("GET", "/", nil) 573 | resp, err := app.Test(req, -1) 574 | if err != nil { 575 | t.Fail() 576 | } 577 | if resp.StatusCode != 200 { 578 | t.Fail() 579 | } 580 | 581 | resp, err = srv.Client().Get(srv.URL) 582 | if err != nil { 583 | t.Fail() 584 | } 585 | if resp == nil { 586 | t.Fatal("response is nil") 587 | } 588 | if resp.StatusCode != 200 { 589 | t.Fail() 590 | } 591 | defer resp.Body.Close() 592 | 593 | body, _ := io.ReadAll(resp.Body) 594 | got := string(body) 595 | want := `my_service_with_name_http_requests_total{method="GET",path="/",service="unique-service",status_code="200"} 1` 596 | if !strings.Contains(got, want) { 597 | t.Errorf("got %s; want %s", got, want) 598 | } 599 | 600 | want = `my_service_with_name_http_request_duration_seconds_count{method="GET",path="/",service="unique-service",status_code="200"} 1` 601 | if !strings.Contains(got, want) { 602 | t.Errorf("got %s; want %s", got, want) 603 | } 604 | 605 | want = `my_service_with_name_http_requests_in_progress_total{method="GET",service="unique-service"} 0` 606 | if !strings.Contains(got, want) { 607 | t.Errorf("got %s; want %s", got, want) 608 | } 609 | } 610 | 611 | func TestCustomRegistryRegisterAt(t *testing.T) { 612 | t.Parallel() 613 | 614 | app := fiber.New() 615 | registry := prometheus.NewRegistry() 616 | registry.Register(collectors.NewGoCollector()) 617 | registry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) 618 | fpCustom := NewWithRegistry(registry, "custom-registry", "custom_name", "http", nil) 619 | fpCustom.RegisterAt(app, "/metrics") 620 | app.Use(fpCustom.Middleware) 621 | 622 | app.Get("/", func(c *fiber.Ctx) error { 623 | return c.SendString("Hello, world!") 624 | }) 625 | req := httptest.NewRequest("GET", "/", nil) 626 | res, err := app.Test(req, -1) 627 | if err != nil { 628 | t.Fatal(fmt.Errorf("GET / failed: %w", err)) 629 | } 630 | defer res.Body.Close() 631 | if res.StatusCode != 200 { 632 | t.Fatal(fmt.Errorf("GET /: Status=%d", res.StatusCode)) 633 | } 634 | 635 | req = httptest.NewRequest("GET", "/metrics", nil) 636 | resMetr, err := app.Test(req, -1) 637 | if err != nil { 638 | t.Fatal(fmt.Errorf("GET /metrics failed: %W", err)) 639 | } 640 | defer resMetr.Body.Close() 641 | if res.StatusCode != 200 { 642 | t.Fatal(fmt.Errorf("GET /metrics: Status=%d", resMetr.StatusCode)) 643 | } 644 | body, err := io.ReadAll(resMetr.Body) 645 | if err != nil { 646 | t.Fatal(fmt.Errorf("GET /metrics: read body: %w", err)) 647 | } 648 | got := string(body) 649 | 650 | want := `custom_name_http_requests_total{method="GET",path="/",service="custom-registry",status_code="200"} 1` 651 | if !strings.Contains(got, want) { 652 | t.Errorf("got %s; want %s", got, want) 653 | } 654 | 655 | // Make sure that /metrics was skipped 656 | want = `custom_name_http_requests_total{method="GET",path="/metrics",service="custom-registry",status_code="200"} 1` 657 | if strings.Contains(got, want) { 658 | t.Errorf("got %s; not want %s", got, want) 659 | } 660 | } 661 | 662 | // TestInFlightGauge verifies that the in-flight requests gauge is updated correctly. 663 | func TestInFlightGauge(t *testing.T) { 664 | app := fiber.New() 665 | prometheus := New("inflight-service") 666 | app.Use(prometheus.Middleware) 667 | 668 | // Long-running handler to simulate in-flight requests 669 | app.Get("/long", func(c *fiber.Ctx) error { 670 | // Sleep for a short duration 671 | time.Sleep(100 * time.Millisecond) 672 | return c.SendString("Long Request") 673 | }) 674 | 675 | // Register metrics endpoint 676 | prometheus.RegisterAt(app, "/metrics") 677 | 678 | var wg sync.WaitGroup 679 | requests := 10 680 | wg.Add(requests) 681 | 682 | // Start multiple concurrent requests 683 | for i := 0; i < requests; i++ { 684 | go func() { 685 | defer wg.Done() 686 | req := httptest.NewRequest("GET", "/long", nil) 687 | app.Test(req, -1) 688 | }() 689 | } 690 | 691 | // Allow some time for requests to start 692 | time.Sleep(10 * time.Millisecond) 693 | wg.Wait() 694 | 695 | // After all requests complete, in-flight gauge should be zero 696 | req := httptest.NewRequest("GET", "/metrics", nil) 697 | resp, _ := app.Test(req, -1) 698 | defer resp.Body.Close() 699 | body, _ := io.ReadAll(resp.Body) 700 | got := string(body) 701 | 702 | // The in-flight gauge should be equal to the number of concurrent requests 703 | // Since requests are sleeping, some may have completed, so we check for at least one 704 | if !strings.Contains(got, `http_requests_in_progress_total{method="GET",service="inflight-service"} 1`) { 705 | t.Errorf("Expected in-flight gauge to be at least 1, got %s", got) 706 | } 707 | 708 | want := `http_requests_total{method="GET",path="/long",service="inflight-service",status_code="200"} 10` 709 | if !strings.Contains(got, want) { 710 | t.Errorf("Expected in-flight gauge to be 0, got %s", got) 711 | } 712 | } 713 | 714 | // TestDifferentHTTPMethods verifies that metrics are correctly recorded for various HTTP methods. 715 | func TestDifferentHTTPMethods(t *testing.T) { 716 | app := fiber.New() 717 | prometheus := New("methods-service") 718 | app.Use(prometheus.Middleware) 719 | 720 | // Define handlers for different methods 721 | app.Get("/resource", func(c *fiber.Ctx) error { 722 | return c.SendString("GET") 723 | }) 724 | app.Post("/resource", func(c *fiber.Ctx) error { 725 | return c.SendString("POST") 726 | }) 727 | app.Put("/resource", func(c *fiber.Ctx) error { 728 | return c.SendString("PUT") 729 | }) 730 | app.Delete("/resource", func(c *fiber.Ctx) error { 731 | return c.SendString("DELETE") 732 | }) 733 | 734 | // Register metrics endpoint 735 | prometheus.RegisterAt(app, "/metrics") 736 | 737 | // Make requests with different methods 738 | methods := []string{"GET", "POST", "PUT", "DELETE"} 739 | for _, method := range methods { 740 | req := httptest.NewRequest(method, "/resource", nil) 741 | resp, _ := app.Test(req, -1) 742 | if resp.StatusCode != 200 { 743 | t.Fatalf("Expected status 200 for %s, got %d", method, resp.StatusCode) 744 | } 745 | } 746 | 747 | // Check Metrics 748 | req := httptest.NewRequest("GET", "/metrics", nil) 749 | resp, _ := app.Test(req, -1) 750 | defer resp.Body.Close() 751 | body, _ := io.ReadAll(resp.Body) 752 | got := string(body) 753 | 754 | for _, method := range methods { 755 | want := `http_requests_total{method="` + method + `",path="/resource",service="methods-service",status_code="200"} 1` 756 | if !strings.Contains(got, want) { 757 | t.Errorf("Expected metric %s, got %s", want, got) 758 | } 759 | } 760 | } 761 | 762 | // TestSkipPathsWithTrailingSlash verifies that skip paths are correctly normalized and skipped even with trailing slashes. 763 | func TestSkipPathsWithTrailingSlash(t *testing.T) { 764 | app := fiber.New() 765 | prometheus := New("skip-service") 766 | prometheus.RegisterAt(app, "/metrics") 767 | prometheus.SetSkipPaths([]string{"/healthz", "/livez"}) 768 | app.Use(prometheus.Middleware) 769 | 770 | app.Get("/", func(c *fiber.Ctx) error { 771 | return c.SendString("Hello World") 772 | }) 773 | app.Get("/healthz/", func(c *fiber.Ctx) error { // Trailing slash 774 | return c.SendString("Healthz") 775 | }) 776 | app.Get("/livez/", func(c *fiber.Ctx) error { // Trailing slash 777 | return c.SendString("Livez") 778 | }) 779 | 780 | // Make requests 781 | req := httptest.NewRequest("GET", "/", nil) 782 | resp, _ := app.Test(req, -1) 783 | if resp.StatusCode != 200 { 784 | t.Fatalf("Expected status 200, got %d", resp.StatusCode) 785 | } 786 | 787 | req = httptest.NewRequest("GET", "/healthz/", nil) 788 | resp, _ = app.Test(req, -1) 789 | if resp.StatusCode != 200 { 790 | t.Fatalf("Expected status 200, got %d", resp.StatusCode) 791 | } 792 | 793 | req = httptest.NewRequest("GET", "/livez/", nil) 794 | resp, _ = app.Test(req, -1) 795 | if resp.StatusCode != 200 { 796 | t.Fatalf("Expected status 200, got %d", resp.StatusCode) 797 | } 798 | 799 | // Check Metrics 800 | req = httptest.NewRequest("GET", "/metrics", nil) 801 | resp, _ = app.Test(req, -1) 802 | defer resp.Body.Close() 803 | 804 | body, _ := io.ReadAll(resp.Body) 805 | got := string(body) 806 | 807 | // Only the root path should be recorded 808 | want := `http_requests_total{method="GET",path="/",service="skip-service",status_code="200"} 1` 809 | if !strings.Contains(got, want) { 810 | t.Errorf("Expected metric %s, got %s", want, got) 811 | } 812 | 813 | // Ensure skipped paths are not recorded 814 | skippedPaths := []string{"/healthz", "/livez"} 815 | for _, path := range skippedPaths { 816 | want := `http_requests_total{method="GET",path="` + path + `",service="skip-service",status_code="200"} 1` 817 | if strings.Contains(got, want) { 818 | t.Errorf("Did not expect metric %s, but found in %s", want, got) 819 | } 820 | } 821 | } 822 | 823 | // TestMetricsAfterError verifies that metrics are recorded correctly even when handlers return errors. 824 | func TestMetricsAfterError(t *testing.T) { 825 | app := fiber.New() 826 | prometheus := New("error-service") 827 | app.Use(prometheus.Middleware) 828 | 829 | app.Get("/badrequest", func(c *fiber.Ctx) error { 830 | return fiber.ErrBadRequest 831 | }) 832 | app.Get("/internalerror", func(c *fiber.Ctx) error { 833 | return fiber.ErrInternalServerError 834 | }) 835 | 836 | // Register metrics endpoint 837 | prometheus.RegisterAt(app, "/metrics") 838 | 839 | // Make error requests 840 | req := httptest.NewRequest("GET", "/badrequest", nil) 841 | resp, _ := app.Test(req, -1) 842 | if resp.StatusCode != fiber.StatusBadRequest { 843 | t.Fatalf("Expected status 400, got %d", resp.StatusCode) 844 | } 845 | 846 | req = httptest.NewRequest("GET", "/internalerror", nil) 847 | resp, _ = app.Test(req, -1) 848 | if resp.StatusCode != fiber.StatusInternalServerError { 849 | t.Fatalf("Expected status 500, got %d", resp.StatusCode) 850 | } 851 | 852 | // Check Metrics 853 | req = httptest.NewRequest("GET", "/metrics", nil) 854 | resp, _ = app.Test(req, -1) 855 | defer resp.Body.Close() 856 | body, _ := io.ReadAll(resp.Body) 857 | got := string(body) 858 | 859 | want400 := `http_requests_total{method="GET",path="/badrequest",service="error-service",status_code="400"} 1` 860 | if !strings.Contains(got, want400) { 861 | t.Errorf("Expected metric %s, got %s", want400, got) 862 | } 863 | 864 | want500 := `http_requests_total{method="GET",path="/internalerror",service="error-service",status_code="500"} 1` 865 | if !strings.Contains(got, want500) { 866 | t.Errorf("Expected metric %s, got %s", want500, got) 867 | } 868 | } 869 | 870 | // TestMultipleRegistrations ensures that calling RegisterAt multiple times does not duplicate handlers. 871 | func TestMultipleRegistrations(t *testing.T) { 872 | app := fiber.New() 873 | prometheus := New("multi-register-service") 874 | app.Use(prometheus.Middleware) 875 | prometheus.RegisterAt(app, "/metrics") 876 | prometheus.RegisterAt(app, "/metrics") // Register again 877 | 878 | app.Get("/", func(c *fiber.Ctx) error { 879 | return c.SendString("Hello World") 880 | }) 881 | 882 | // Make requests 883 | req := httptest.NewRequest("GET", "/", nil) 884 | resp, _ := app.Test(req, -1) 885 | if resp.StatusCode != 200 { 886 | t.Fatalf("Expected status 200, got %d", resp.StatusCode) 887 | } 888 | 889 | // Make a request to /metrics 890 | req = httptest.NewRequest("GET", "/metrics", nil) 891 | resp, _ = app.Test(req, -1) 892 | if resp.StatusCode != 200 { 893 | t.Fatalf("Expected status 200, got %d", resp.StatusCode) 894 | } 895 | 896 | body, _ := io.ReadAll(resp.Body) 897 | got := string(body) 898 | 899 | // Expect metrics to be registered only once 900 | want := `http_requests_total{method="GET",path="/",service="multi-register-service",status_code="200"} 1` 901 | if strings.Count(got, want) != 1 { 902 | t.Errorf("Expected metric %s to appear once, got %s occurrences", want, got) 903 | } 904 | } 905 | 906 | // TestMetricsHandlerConcurrentAccess verifies that the metrics handler can handle concurrent access without issues. 907 | func TestMetricsHandlerConcurrentAccess(t *testing.T) { 908 | app := fiber.New() 909 | prometheus := New("concurrent-service") 910 | app.Use(prometheus.Middleware) 911 | 912 | app.Get("/resource", func(c *fiber.Ctx) error { 913 | return c.SendString("Resource") 914 | }) 915 | 916 | // Register metrics endpoint 917 | prometheus.RegisterAt(app, "/metrics") 918 | 919 | // Make multiple concurrent requests 920 | var wg sync.WaitGroup 921 | requests := 100 922 | wg.Add(requests) 923 | 924 | for i := 0; i < requests; i++ { 925 | go func() { 926 | defer wg.Done() 927 | req := httptest.NewRequest("GET", "/resource", nil) 928 | app.Test(req, -1) 929 | }() 930 | } 931 | 932 | wg.Wait() 933 | 934 | // Check Metrics 935 | req := httptest.NewRequest("GET", "/metrics", nil) 936 | resp, _ := app.Test(req, -1) 937 | defer resp.Body.Close() 938 | 939 | body, _ := io.ReadAll(resp.Body) 940 | got := string(body) 941 | 942 | // Verify that requests_total is incremented correctly 943 | wantTotal := `http_requests_total{method="GET",path="/resource",service="concurrent-service",status_code="200"} ` + strconv.Itoa(requests) 944 | if !strings.Contains(got, wantTotal) { 945 | t.Errorf("Expected metric %s, got %s", wantTotal, got) 946 | } 947 | } 948 | 949 | func TestIgnoreStatusCodes(t *testing.T) { 950 | t.Parallel() 951 | 952 | app := fiber.New() 953 | prometheus := New("ignore-status") 954 | prometheus.RegisterAt(app, "/metrics") 955 | prometheus.SetIgnoreStatusCodes([]int{401, 403}) 956 | app.Use(prometheus.Middleware) 957 | 958 | app.Get("/", func(c *fiber.Ctx) error { return c.SendString("OK") }) 959 | app.Get("/unauthorized", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) }) 960 | app.Get("/forbidden", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusForbidden) }) 961 | 962 | app.Test(httptest.NewRequest("GET", "/", nil), -1) 963 | app.Test(httptest.NewRequest("GET", "/unauthorized", nil), -1) 964 | app.Test(httptest.NewRequest("GET", "/forbidden", nil), -1) 965 | 966 | resp, _ := app.Test(httptest.NewRequest("GET", "/metrics", nil), -1) 967 | defer resp.Body.Close() 968 | body, _ := io.ReadAll(resp.Body) 969 | got := string(body) 970 | 971 | want := `http_requests_total{method="GET",path="/",service="ignore-status",status_code="200"} 1` 972 | if !strings.Contains(got, want) { 973 | t.Errorf("got %s; want %s", got, want) 974 | } 975 | 976 | if strings.Contains(got, `/unauthorized`) { 977 | t.Errorf("metrics should ignore status 401: %s", got) 978 | } 979 | 980 | if strings.Contains(got, `/forbidden`) { 981 | t.Errorf("metrics should ignore status 403: %s", got) 982 | } 983 | } 984 | 985 | // TestIgnoreStatusCodesWithGroup replicates TestMiddlewareWithGroup but ignores specific status codes. 986 | func TestIgnoreStatusCodesWithGroup(t *testing.T) { 987 | t.Parallel() 988 | 989 | app := fiber.New() 990 | prometheus := New("ignore-group") 991 | prometheus.RegisterAt(app, "/metrics") 992 | prometheus.SetIgnoreStatusCodes([]int{400, 500}) 993 | app.Use(prometheus.Middleware) 994 | 995 | public := app.Group("/public") 996 | public.Get("/", func(c *fiber.Ctx) error { return c.SendString("Hello World") }) 997 | public.Get("/error/:type", func(ctx *fiber.Ctx) error { 998 | switch ctx.Params("type") { 999 | case "fiber": 1000 | return fiber.ErrBadRequest 1001 | default: 1002 | return fiber.ErrInternalServerError 1003 | } 1004 | }) 1005 | 1006 | app.Test(httptest.NewRequest("GET", "/public", nil), -1) 1007 | app.Test(httptest.NewRequest("GET", "/public/error/fiber", nil), -1) 1008 | app.Test(httptest.NewRequest("GET", "/public/error/unknown", nil), -1) 1009 | 1010 | resp, _ := app.Test(httptest.NewRequest("GET", "/metrics", nil), -1) 1011 | defer resp.Body.Close() 1012 | body, _ := io.ReadAll(resp.Body) 1013 | got := string(body) 1014 | 1015 | want := `http_requests_total{method="GET",path="/public",service="ignore-group",status_code="200"} 1` 1016 | if !strings.Contains(got, want) { 1017 | t.Errorf("got %s; want %s", got, want) 1018 | } 1019 | if strings.Contains(got, `/public/error/:type`) { 1020 | t.Errorf("metrics should ignore 400 and 500 status codes: %s", got) 1021 | } 1022 | } 1023 | 1024 | func TestSkipUnregisteredRoute(t *testing.T) { 1025 | t.Parallel() 1026 | 1027 | app := fiber.New() 1028 | prometheus := New("skip-unregistered") 1029 | prometheus.RegisterAt(app, "/metrics") 1030 | app.Use(prometheus.Middleware) 1031 | 1032 | app.Get("/registered", func(c *fiber.Ctx) error { return c.SendString("OK") }) 1033 | 1034 | app.Test(httptest.NewRequest("GET", "/registered", nil), -1) 1035 | app.Test(httptest.NewRequest("GET", "/not-found", nil), -1) 1036 | 1037 | resp, _ := app.Test(httptest.NewRequest("GET", "/metrics", nil), -1) 1038 | defer resp.Body.Close() 1039 | body, _ := io.ReadAll(resp.Body) 1040 | got := string(body) 1041 | 1042 | want := `http_requests_total{method="GET",path="/registered",service="skip-unregistered",status_code="200"} 1` 1043 | if !strings.Contains(got, want) { 1044 | t.Errorf("got %s; want %s", got, want) 1045 | } 1046 | 1047 | if strings.Contains(got, "/not-found") { 1048 | t.Errorf("metrics should skip unregistered route: %s", got) 1049 | } 1050 | } 1051 | 1052 | // TestSkipUnregisteredRouteWithGroup replicates TestMiddlewareWithGroup but ensures unregistered routes are skipped. 1053 | func TestSkipUnregisteredRouteWithGroup(t *testing.T) { 1054 | t.Parallel() 1055 | 1056 | app := fiber.New() 1057 | prometheus := New("skip-group") 1058 | prometheus.RegisterAt(app, "/metrics") 1059 | app.Use(prometheus.Middleware) 1060 | 1061 | public := app.Group("/public") 1062 | public.Get("/", func(c *fiber.Ctx) error { return c.SendString("OK") }) 1063 | 1064 | app.Test(httptest.NewRequest("GET", "/public", nil), -1) 1065 | app.Test(httptest.NewRequest("GET", "/unknown", nil), -1) 1066 | 1067 | resp, _ := app.Test(httptest.NewRequest("GET", "/metrics", nil), -1) 1068 | defer resp.Body.Close() 1069 | body, _ := io.ReadAll(resp.Body) 1070 | got := string(body) 1071 | 1072 | want := `http_requests_total{method="GET",path="/public",service="skip-group",status_code="200"} 1` 1073 | if !strings.Contains(got, want) { 1074 | t.Errorf("got %s; want %s", got, want) 1075 | } 1076 | if strings.Contains(got, "/unknown") { 1077 | t.Errorf("metrics should skip unregistered route: %s", got) 1078 | } 1079 | } 1080 | 1081 | func Benchmark_Middleware(b *testing.B) { 1082 | app := fiber.New() 1083 | 1084 | prometheus := New("test-benchmark") 1085 | prometheus.RegisterAt(app, "/metrics") 1086 | app.Use(prometheus.Middleware) 1087 | 1088 | app.Get("/", func(c *fiber.Ctx) error { 1089 | return c.SendString("Hello World") 1090 | }) 1091 | 1092 | h := app.Handler() 1093 | ctx := &fasthttp.RequestCtx{} 1094 | 1095 | req := &fasthttp.Request{} 1096 | req.Header.SetMethod(fiber.MethodOptions) 1097 | req.SetRequestURI("/") 1098 | ctx.Init(req, nil, nil) 1099 | 1100 | b.ReportAllocs() 1101 | b.ResetTimer() 1102 | 1103 | for i := 0; i < b.N; i++ { 1104 | h(ctx) 1105 | } 1106 | } 1107 | 1108 | func Benchmark_Middleware_Parallel(b *testing.B) { 1109 | app := fiber.New() 1110 | 1111 | prometheus := New("test-benchmark") 1112 | prometheus.RegisterAt(app, "/metrics") 1113 | app.Use(prometheus.Middleware) 1114 | 1115 | app.Get("/", func(c *fiber.Ctx) error { 1116 | return c.SendString("Hello World") 1117 | }) 1118 | 1119 | h := app.Handler() 1120 | 1121 | b.ReportAllocs() 1122 | b.ResetTimer() 1123 | 1124 | b.RunParallel(func(pb *testing.PB) { 1125 | ctx := &fasthttp.RequestCtx{} 1126 | req := &fasthttp.Request{} 1127 | req.Header.SetMethod(fiber.MethodOptions) 1128 | req.SetRequestURI("/metrics") 1129 | ctx.Init(req, nil, nil) 1130 | 1131 | for pb.Next() { 1132 | h(ctx) 1133 | } 1134 | }) 1135 | } 1136 | --------------------------------------------------------------------------------