├── .github ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── reqsize.go ├── LICENSE ├── go.mod ├── options.go ├── README.md ├── go.sum ├── prom.go └── prom_test.go /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:best-practices", ":automergeMinor", ":automergeDigest"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Go 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | vendor/ 17 | coverage.txt -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | golang: 12 | name: Golang 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | checks: write 17 | uses: depado/github-actions/.github/workflows/golang.yml@main 18 | with: 19 | skip-build: true 20 | -------------------------------------------------------------------------------- /reqsize.go: -------------------------------------------------------------------------------- 1 | package ginprom 2 | 3 | import "net/http" 4 | 5 | // From https://github.com/DanielHeckrath/gin-prometheus/blob/master/gin_prometheus.go 6 | func computeApproximateRequestSize(r *http.Request) int { 7 | s := 0 8 | if r.URL != nil { 9 | s = len(r.URL.String()) 10 | } 11 | 12 | s += len(r.Method) 13 | s += len(r.Proto) 14 | for name, values := range r.Header { 15 | s += len(name) 16 | for _, value := range values { 17 | s += len(value) 18 | } 19 | } 20 | s += len(r.Host) 21 | 22 | // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 23 | 24 | if r.ContentLength != -1 { 25 | s += int(r.ContentLength) 26 | } 27 | return s 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Depado/ginprom 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.25.5 6 | 7 | require ( 8 | github.com/appleboy/gofight/v2 v2.2.0 9 | github.com/gin-gonic/gin v1.11.0 10 | github.com/prometheus/client_golang v1.23.2 11 | github.com/prometheus/client_model v0.6.2 12 | github.com/stretchr/testify v1.11.1 13 | ) 14 | 15 | require ( 16 | github.com/beorn7/perks v1.0.1 // indirect 17 | github.com/bytedance/sonic v1.14.0 // indirect 18 | github.com/bytedance/sonic/loader v0.3.0 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/cloudwego/base64x v0.1.6 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 23 | github.com/gin-contrib/sse v1.1.0 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/go-playground/validator/v10 v10.27.0 // indirect 27 | github.com/goccy/go-json v0.10.5 // indirect 28 | github.com/goccy/go-yaml v1.18.0 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 31 | github.com/kr/text v0.2.0 // indirect 32 | github.com/leodido/go-urn v1.4.0 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/prometheus/common v0.66.1 // indirect 40 | github.com/prometheus/procfs v0.16.1 // indirect 41 | github.com/quic-go/qpack v0.6.0 // indirect 42 | github.com/quic-go/quic-go v0.57.0 // indirect 43 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 44 | github.com/ugorji/go/codec v1.3.0 // indirect 45 | go.yaml.in/yaml/v2 v2.4.2 // indirect 46 | golang.org/x/arch v0.20.0 // indirect 47 | golang.org/x/crypto v0.45.0 // indirect 48 | golang.org/x/net v0.47.0 // indirect 49 | golang.org/x/sys v0.38.0 // indirect 50 | golang.org/x/text v0.31.0 // indirect 51 | google.golang.org/protobuf v1.36.9 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package ginprom 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | type PrometheusOption func(*Prometheus) 12 | 13 | // Path is an option allowing to set the metrics path when initializing with New. 14 | func Path(path string) PrometheusOption { 15 | return func(p *Prometheus) { 16 | p.MetricsPath = path 17 | } 18 | } 19 | 20 | // Ignore is used to disable instrumentation on some routes. 21 | func Ignore(paths ...string) PrometheusOption { 22 | return func(p *Prometheus) { 23 | p.Ignored.Lock() 24 | defer p.Ignored.Unlock() 25 | for _, path := range paths { 26 | p.Ignored.values[path] = true 27 | } 28 | } 29 | } 30 | 31 | // BucketSize is used to define the default bucket size when initializing with 32 | // New. 33 | func BucketSize(b []float64) PrometheusOption { 34 | return func(p *Prometheus) { 35 | p.BucketsSize = b 36 | } 37 | } 38 | 39 | // Subsystem is an option allowing to set the subsystem when initializing 40 | // with New. 41 | func Subsystem(sub string) PrometheusOption { 42 | return func(p *Prometheus) { 43 | p.Subsystem = sub 44 | } 45 | } 46 | 47 | // Namespace is an option allowing to set the namespace when initializing 48 | // with New. 49 | func Namespace(ns string) PrometheusOption { 50 | return func(p *Prometheus) { 51 | p.Namespace = ns 52 | } 53 | } 54 | 55 | // Token is an option allowing to set the bearer token in prometheus 56 | // with New. 57 | // Example: ginprom.New(ginprom.Token("your_custom_token")) 58 | func Token(token string) PrometheusOption { 59 | return func(p *Prometheus) { 60 | p.Token = token 61 | } 62 | } 63 | 64 | // RequestCounterMetricName is an option allowing to set the request counter metric name. 65 | func RequestCounterMetricName(reqCntMetricName string) PrometheusOption { 66 | return func(p *Prometheus) { 67 | p.RequestCounterMetricName = reqCntMetricName 68 | } 69 | } 70 | 71 | // RequestDurationMetricName is an option allowing to set the request duration metric name. 72 | func RequestDurationMetricName(reqDurMetricName string) PrometheusOption { 73 | return func(p *Prometheus) { 74 | p.RequestDurationMetricName = reqDurMetricName 75 | } 76 | } 77 | 78 | // RequestSizeMetricName is an option allowing to set the request size metric name. 79 | func RequestSizeMetricName(reqSzMetricName string) PrometheusOption { 80 | return func(p *Prometheus) { 81 | p.RequestSizeMetricName = reqSzMetricName 82 | } 83 | } 84 | 85 | // ResponseSizeMetricName is an option allowing to set the response size metric name. 86 | func ResponseSizeMetricName(resDurMetricName string) PrometheusOption { 87 | return func(p *Prometheus) { 88 | p.ResponseSizeMetricName = resDurMetricName 89 | } 90 | } 91 | 92 | // Engine is an option allowing to set the gin engine when intializing with New. 93 | // Example: 94 | // r := gin.Default() 95 | // p := ginprom.New(Engine(r)) 96 | func Engine(e *gin.Engine) PrometheusOption { 97 | return func(p *Prometheus) { 98 | p.Engine = e 99 | } 100 | } 101 | 102 | // Registry is an option allowing to set a *prometheus.Registry with New. 103 | // Use this option if you want to use a custom Registry instead of a global one that prometheus 104 | // client uses by default 105 | // Example: 106 | // r := gin.Default() 107 | // p := ginprom.New(Registry(r)) 108 | func Registry(r *prometheus.Registry) PrometheusOption { 109 | return func(p *Prometheus) { 110 | p.Registry = r 111 | } 112 | } 113 | 114 | // HandlerNameFunc is an option allowing to set the HandlerNameFunc with New. 115 | // Use this option if you want to override the default behavior (i.e. using 116 | // (*gin.Context).HandlerName). This is useful when wanting to group different 117 | // functions under the same "handler" label or when using gin with decorated handlers 118 | // Example: 119 | // r := gin.Default() 120 | // p := ginprom.New(HandlerNameFunc(func (c *gin.Context) string { return "my handler" })) 121 | func HandlerNameFunc(f func(c *gin.Context) string) PrometheusOption { 122 | return func(p *Prometheus) { 123 | p.HandlerNameFunc = f 124 | } 125 | } 126 | 127 | // HandlerOpts is an option allowing to set the promhttp.HandlerOpts. 128 | // Use this option if you want to override the default zero value. 129 | func HandlerOpts(opts promhttp.HandlerOpts) PrometheusOption { 130 | return func(p *Prometheus) { 131 | p.HandlerOpts = opts 132 | } 133 | } 134 | 135 | // RequestPathFunc is an option allowing to set the RequestPathFunc with New. 136 | // Use this option if you want to override the default behavior (i.e. using 137 | // (*gin.Context).FullPath). This is useful when wanting to group different requests 138 | // under the same "path" label or when wanting to process unknown routes (the default 139 | // (*gin.Context).FullPath return an empty string for unregistered routes). Note that 140 | // requests for which f returns the empty string are ignored. 141 | // To specifically ignore certain paths, see the Ignore option. 142 | // Example: 143 | // 144 | // r := gin.Default() 145 | // p := ginprom.New(RequestPathFunc(func (c *gin.Context) string { 146 | // if fullpath := c.FullPath(); fullpath != "" { 147 | // return fullpath 148 | // } 149 | // return "" 150 | // })) 151 | func RequestPathFunc(f func(c *gin.Context) string) PrometheusOption { 152 | return func(p *Prometheus) { 153 | p.RequestPathFunc = f 154 | } 155 | } 156 | 157 | func CustomCounterLabels(labels []string, f func(c *gin.Context) map[string]string) PrometheusOption { 158 | return func(p *Prometheus) { 159 | p.customCounterLabelsProvider = f 160 | p.customCounterLabels = labels 161 | } 162 | } 163 | 164 | func NativeHistogram(nh bool) PrometheusOption { 165 | return func(p *Prometheus) { 166 | p.nativeHistogram = nh 167 | } 168 | } 169 | 170 | func NativeHistogramBucketFactor(nhbf float64) PrometheusOption { 171 | return func(p *Prometheus) { 172 | p.NativeHistogramBucketFactor = nhbf 173 | } 174 | } 175 | 176 | func NativeHistogramMaxBucketNumber(nhmbn uint32) PrometheusOption { 177 | return func(p *Prometheus) { 178 | p.NativeHistogramMaxBucketNumber = nhmbn 179 | } 180 | } 181 | 182 | func NativeHistogramMinResetDuration(nhmrd time.Duration) PrometheusOption { 183 | return func(p *Prometheus) { 184 | p.NativeHistogramMinResetDuration = nhmrd 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Ginprom

2 |

3 | 4 | Gin Prometheus metrics exporter 5 | 6 | [![Sourcegraph](https://sourcegraph.com/github.com/Depado/ginprom/-/badge.svg)](https://sourcegraph.com/github.com/Depado/ginprom?badge) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/Depado/ginprom)](https://goreportcard.com/report/github.com/Depado/ginprom) 8 | [![codecov](https://codecov.io/gh/Depado/ginprom/branch/master/graph/badge.svg)](https://codecov.io/gh/Depado/ginprom) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Depado/bfchroma/blob/master/LICENSE) 10 | [![godoc](https://godoc.org/github.com/Depado/ginprom?status.svg)](https://godoc.org/github.com/Depado/ginprom) 11 | 12 |

13 |

14 | 15 | Inspired by [github.com/zsais/go-gin-prometheus](https://github.com/zsais/go-gin-prometheus) 16 | 17 |

18 | 19 | - [Install](#install) 20 | - [Differences with go-gin-prometheus](#differences-with-go-gin-prometheus) 21 | - [Usage](#usage) 22 | - [Options](#options) 23 | - [Custom counters](#custom-counters) 24 | - [Custom gauges](#custom-gauges) 25 | - [Custom histograms](#custom-histograms) 26 | - [Path](#path) 27 | - [Namespace](#namespace) 28 | - [Subsystem](#subsystem) 29 | - [Engine](#engine) 30 | - [Prometheus Registry](#prometheus-registry) 31 | - [HandlerNameFunc](#handlernamefunc) 32 | - [RequestPathFunc](#requestpathfunc) 33 | - [CustomCounterLabels](#customcounterlabels) 34 | - [Ignore](#ignore) 35 | - [Token](#token) 36 | - [Bucket size](#bucket-size) 37 | - [Native histogram](#native-histogram) 38 | - [Troubleshooting](#troubleshooting) 39 | - [The instrumentation doesn't seem to work](#the-instrumentation-doesnt-seem-to-work) 40 | 41 | ## Install 42 | 43 | Simply run: 44 | `go get -u github.com/Depado/ginprom` 45 | 46 | ## Differences with go-gin-prometheus 47 | 48 | - No support for Prometheus' Push Gateway 49 | - Options on constructor 50 | - Adds a `path` label to get the matched route 51 | - Ability to ignore routes 52 | 53 | ## Usage 54 | 55 | ```go 56 | package main 57 | 58 | import ( 59 | "github.com/Depado/ginprom" 60 | "github.com/gin-gonic/gin" 61 | ) 62 | 63 | func main() { 64 | r := gin.Default() 65 | p := ginprom.New( 66 | ginprom.Engine(r), 67 | ginprom.Subsystem("gin"), 68 | ginprom.Path("/metrics"), 69 | ) 70 | r.Use(p.Instrument()) 71 | 72 | r.GET("/hello/:id", func(c *gin.Context) {}) 73 | r.GET("/world/:id", func(c *gin.Context) {}) 74 | r.Run("127.0.0.1:8080") 75 | } 76 | ``` 77 | 78 | ## Options 79 | 80 | ### Custom counters 81 | 82 | Add custom counters to add own values to the metrics 83 | 84 | ```go 85 | r := gin.New() 86 | p := ginprom.New( 87 | ginprom.Engine(r), 88 | ) 89 | p.AddCustomCounter("custom", "Some help text to provide", []string{"label"}) 90 | r.Use(p.Instrument()) 91 | ``` 92 | 93 | Save `p` and use the following functions: 94 | 95 | - IncrementCounterValue 96 | - AddCounterValue 97 | 98 | ### Custom gauges 99 | 100 | Add custom gauges to add own values to the metrics 101 | 102 | ```go 103 | r := gin.New() 104 | p := ginprom.New( 105 | ginprom.Engine(r), 106 | ) 107 | p.AddCustomGauge("custom", "Some help text to provide", []string{"label"}) 108 | r.Use(p.Instrument()) 109 | ``` 110 | 111 | Save `p` and use the following functions: 112 | 113 | - IncrementGaugeValue 114 | - DecrementGaugeValue 115 | - SetGaugeValue 116 | 117 | ### Custom histograms 118 | 119 | Add custom histograms to add own values to the metrics 120 | 121 | ```go 122 | r := gin.New() 123 | p := ginprom.New( 124 | ginprom.Engine(r), 125 | ) 126 | p.AddCustomHistogram("internal_request_latency", "Duration of internal HTTP requests", []string{"url", "method", "status"}) 127 | r.Use(p.Instrument()) 128 | ``` 129 | 130 | Save `p` and use the following functions: 131 | 132 | - AddCustomHistogramValue 133 | 134 | ### Path 135 | 136 | Override the default path (`/metrics`) on which the metrics can be accessed: 137 | 138 | ```go 139 | r := gin.New() 140 | p := ginprom.New( 141 | ginprom.Engine(r), 142 | ginprom.Path("/custom/metrics"), 143 | ) 144 | r.Use(p.Instrument()) 145 | ``` 146 | 147 | ### Namespace 148 | 149 | Override the default namespace (`gin`): 150 | 151 | ```go 152 | r := gin.New() 153 | p := ginprom.New( 154 | ginprom.Engine(r), 155 | ginprom.Namespace("custom_ns"), 156 | ) 157 | r.Use(p.Instrument()) 158 | ``` 159 | 160 | ### Subsystem 161 | 162 | Override the default (`gonic`) subsystem: 163 | 164 | ```go 165 | r := gin.New() 166 | p := ginprom.New( 167 | ginprom.Engine(r), 168 | ginprom.Subsystem("your_subsystem"), 169 | ) 170 | r.Use(p.Instrument()) 171 | ``` 172 | 173 | ### Engine 174 | 175 | The preferred way to pass the router to ginprom: 176 | 177 | ```go 178 | r := gin.New() 179 | p := ginprom.New( 180 | ginprom.Engine(r), 181 | ) 182 | r.Use(p.Instrument()) 183 | ``` 184 | 185 | The alternative being to call the `Use` method after initialization: 186 | 187 | ```go 188 | p := ginprom.New() 189 | // ... 190 | r := gin.New() 191 | p.Use(r) 192 | r.Use(p.Instrument()) 193 | 194 | ``` 195 | 196 | ### Prometheus Registry 197 | 198 | Use a custom `prometheus.Registry` instead of prometheus client's global registry. This option allows 199 | to use ginprom in multiple gin engines in the same process, or if you would like to integrate ginprom with your own 200 | prometheus `Registry`. 201 | 202 | ```go 203 | registry := prometheus.NewRegistry() // creates new prometheus metric registry 204 | r := gin.New() 205 | p := ginprom.New( 206 | ginprom.Registry(registry), 207 | ) 208 | r.Use(p.Instrument()) 209 | ``` 210 | 211 | ### HandlerNameFunc 212 | 213 | Change the way the `handler` label is computed. By default, the `(*gin.Context).HandlerName` 214 | function is used. 215 | This option is useful when wanting to group different functions under 216 | the same `handler` label or when using `gin` with decorated handlers. 217 | 218 | ```go 219 | r := gin.Default() 220 | p := ginprom.New( 221 | HandlerNameFunc(func (c *gin.Context) string { 222 | return "my handler" 223 | }), 224 | ) 225 | r.Use(p.Instrument()) 226 | ``` 227 | 228 | ### RequestPathFunc 229 | 230 | Change how the `path` label is computed. By default, the `(*gin.Context).FullPath` function 231 | is used. 232 | This option is useful when wanting to group different requests under the same `path` 233 | label or when wanting to process unknown routes (the default `(*gin.Context).FullPath` returns 234 | an empty string for unregistered routes). Note that requests for which `f` returns the empty 235 | string are ignored. 236 | 237 | To specifically ignore certain paths, see the [Ignore](#ignore) option. 238 | 239 | ```go 240 | r := gin.Default() 241 | p := ginprom.New( 242 | // record a metric for unregistered routes under the path label "" 243 | RequestPathFunc(func (c *gin.Context) string { 244 | if fullpath := c.FullPath(); fullpath != "" { 245 | return fullpath 246 | } 247 | return "" 248 | }), 249 | ) 250 | r.Use(p.Instrument()) 251 | ``` 252 | 253 | ### CustomCounterLabels 254 | 255 | Add custom labels to the counter metric. 256 | 257 | ```go 258 | r := gin.Default() 259 | p := ginprom.New( 260 | ginprom.CustomCounterLabels([]string{"client_id"}, func(c *gin.Context) map[string]string { 261 | client_id := c.GetHeader("x-client-id") 262 | if client_id == "" { 263 | client_id = "unknown" 264 | } 265 | return map[string]string{"client_id": client_id} 266 | }), 267 | ) 268 | r.Use(p.Instrument()) 269 | ``` 270 | 271 | ### Ignore 272 | 273 | Ignore allows to completely ignore some routes. Even though you can apply the 274 | middleware to the only groups you're interested in, it is sometimes useful to 275 | have routes not instrumented. 276 | 277 | ```go 278 | r := gin.New() 279 | p := ginprom.New( 280 | ginprom.Engine(r), 281 | ginprom.Ignore("/api/no/no/no", "/api/super/secret/route") 282 | ) 283 | r.Use(p.Instrument()) 284 | ``` 285 | 286 | Note that most of the time this can be solved by gin groups: 287 | 288 | ```go 289 | r := gin.New() 290 | p := ginprom.New(ginprom.Engine(r)) 291 | 292 | // Add the routes that do not need instrumentation 293 | g := r.Group("/api/") 294 | g.Use(p.Instrument()) 295 | { 296 | // Instrumented routes 297 | } 298 | ``` 299 | 300 | ### Token 301 | 302 | Specify a secret token which Prometheus will use to access the endpoint. If the 303 | token is invalid, the endpoint will return an error. 304 | 305 | ```go 306 | r := gin.New() 307 | p := ginprom.New( 308 | ginprom.Engine(r), 309 | ginprom.Token("supersecrettoken") 310 | ) 311 | r.Use(p.Instrument()) 312 | ``` 313 | 314 | ### Bucket size 315 | 316 | Specify the bucket size for the request duration histogram according to your 317 | expected durations. 318 | 319 | ```go 320 | r := gin.New() 321 | p := ginprom.New( 322 | ginprom.Engine(r), 323 | ginprom.BucketSize([]float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}), 324 | ) 325 | r.Use(p.Instrument()) 326 | ``` 327 | 328 | ### Native histogram 329 | 330 | Configure ginprom to use native histogram instead of classical histograms. 331 | Refers to: https://prometheus.io/docs/specs/native_histograms/ 332 | 333 | Default values: 334 | - BucketFactor : 1.1 335 | - MaxBucketNumber: 100 336 | - MinResetDuration : 1 Hour 337 | 338 | ```go 339 | r := gin.New() 340 | p := ginprom.New( 341 | ginprom.Engine(r), 342 | ginprom.NativeHistogram(true), 343 | ginprom.NativeHistogramBucketFactor(1.1), 344 | ginprom.NativeHistogramMaxBucketNumber(100), 345 | ginprom.NativeHistogramMinResetDuration(1 * time.Hour), 346 | ) 347 | r.Use(p.Instrument()) 348 | ``` 349 | 350 | ## Troubleshooting 351 | 352 | ### The instrumentation doesn't seem to work 353 | 354 | Make sure you have set the `gin.Engine` in the `ginprom` middleware, either when 355 | initializing it using `ginprom.New(ginprom.Engine(r))` or using the `Use` 356 | function after the initialization like this : 357 | 358 | ```go 359 | p := ginprom.New( 360 | ginprom.Namespace("gin"), 361 | ginprom.Subsystem("gonic"), 362 | ginprom.Path("/metrics"), 363 | ) 364 | p.Use(r) 365 | r.Use(p.Instrument()) 366 | ``` 367 | 368 | By design, if the middleware was to panic, it would do so when a route is 369 | called. That's why it just silently fails when no engine has been set. 370 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= 2 | github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= 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/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 6 | github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 7 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 8 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 12 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 18 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 19 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 20 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 21 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 22 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 23 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 24 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 26 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 27 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 28 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 29 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 30 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 31 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 32 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 33 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 34 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 35 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 36 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 39 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 40 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 41 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 42 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 43 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 44 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 45 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 49 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 50 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 51 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 52 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 53 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 54 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 58 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 59 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 61 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 62 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 66 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 67 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 68 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 69 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 70 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 71 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 72 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 73 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 74 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 75 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= 76 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 77 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 78 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 81 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 82 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 83 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 84 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 85 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 88 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 89 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 90 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 91 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 92 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 93 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 94 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 95 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 96 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 97 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 98 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 99 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 100 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 101 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 102 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 103 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 104 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 105 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 106 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 107 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 109 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 110 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 111 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 112 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 113 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 114 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 115 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 118 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 121 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | -------------------------------------------------------------------------------- /prom.go: -------------------------------------------------------------------------------- 1 | // Package ginprom is a library to instrument a gin server and expose a 2 | // /metrics endpoint for Prometheus to scrape, keeping a low cardinality by 3 | // preserving the path parameters name in the prometheus label 4 | package ginprom 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | ) 18 | 19 | var defaultPath = "/metrics" 20 | var defaultNs = "gin" 21 | var defaultSys = "gonic" 22 | var defaultHandlerNameFunc = (*gin.Context).HandlerName 23 | var defaultRequestPathFunc = (*gin.Context).FullPath 24 | 25 | var defaultReqCntMetricName = "requests_total" 26 | var defaultReqDurMetricName = "request_duration" 27 | var defaultReqSzMetricName = "request_size_bytes" 28 | var defaultResSzMetricName = "response_size_bytes" 29 | 30 | // ErrInvalidToken is returned when the provided token is invalid or missing. 31 | var ErrInvalidToken = errors.New("invalid or missing token") 32 | 33 | // ErrCustomGauge is returned when the custom gauge can't be found. 34 | var ErrCustomGauge = errors.New("error finding custom gauge") 35 | 36 | // ErrCustomCounter is returned when the custom counter can't be found. 37 | var ErrCustomCounter = errors.New("error finding custom counter") 38 | 39 | type pmapb struct { 40 | sync.RWMutex 41 | values map[string]bool 42 | } 43 | 44 | type pmapGauge struct { 45 | sync.RWMutex 46 | values map[string]prometheus.GaugeVec 47 | } 48 | 49 | type pmapCounter struct { 50 | sync.RWMutex 51 | values map[string]prometheus.CounterVec 52 | } 53 | 54 | type pmapHistogram struct { 55 | sync.RWMutex 56 | values map[string]prometheus.HistogramVec 57 | } 58 | 59 | // Prometheus contains the metrics gathered by the instance and its path. 60 | type Prometheus struct { 61 | reqCnt *prometheus.CounterVec 62 | reqDur *prometheus.HistogramVec 63 | reqSz, resSz prometheus.Summary 64 | 65 | customGauges pmapGauge 66 | customCounters pmapCounter 67 | customCounterLabelsProvider func(c *gin.Context) map[string]string 68 | customCounterLabels []string 69 | customHistograms pmapHistogram 70 | nativeHistogram bool 71 | 72 | MetricsPath string 73 | Namespace string 74 | Subsystem string 75 | Token string 76 | Ignored pmapb 77 | Engine *gin.Engine 78 | BucketsSize []float64 79 | Registry *prometheus.Registry 80 | HandlerNameFunc func(c *gin.Context) string 81 | RequestPathFunc func(c *gin.Context) string 82 | HandlerOpts promhttp.HandlerOpts 83 | 84 | NativeHistogramBucketFactor float64 85 | NativeHistogramMaxBucketNumber uint32 86 | NativeHistogramMinResetDuration time.Duration 87 | 88 | RequestCounterMetricName string 89 | RequestDurationMetricName string 90 | RequestSizeMetricName string 91 | ResponseSizeMetricName string 92 | } 93 | 94 | // IncrementGaugeValue increments a custom gauge. 95 | func (p *Prometheus) IncrementGaugeValue(name string, labelValues []string) error { 96 | p.customGauges.RLock() 97 | defer p.customGauges.RUnlock() 98 | 99 | if g, ok := p.customGauges.values[name]; ok { 100 | g.WithLabelValues(labelValues...).Inc() 101 | } else { 102 | return ErrCustomGauge 103 | } 104 | return nil 105 | } 106 | 107 | // SetGaugeValue sets gauge to value. 108 | func (p *Prometheus) SetGaugeValue(name string, labelValues []string, value float64) error { 109 | p.customGauges.RLock() 110 | defer p.customGauges.RUnlock() 111 | 112 | if g, ok := p.customGauges.values[name]; ok { 113 | g.WithLabelValues(labelValues...).Set(value) 114 | } else { 115 | return ErrCustomGauge 116 | } 117 | return nil 118 | } 119 | 120 | // AddGaugeValue adds value to custom gauge. 121 | func (p *Prometheus) AddGaugeValue(name string, labelValues []string, value float64) error { 122 | p.customGauges.RLock() 123 | defer p.customGauges.RUnlock() 124 | 125 | if g, ok := p.customGauges.values[name]; ok { 126 | g.WithLabelValues(labelValues...).Add(value) 127 | } else { 128 | return ErrCustomGauge 129 | } 130 | return nil 131 | } 132 | 133 | // DecrementGaugeValue decrements a custom gauge. 134 | func (p *Prometheus) DecrementGaugeValue(name string, labelValues []string) error { 135 | p.customGauges.RLock() 136 | defer p.customGauges.RUnlock() 137 | 138 | if g, ok := p.customGauges.values[name]; ok { 139 | g.WithLabelValues(labelValues...).Dec() 140 | } else { 141 | return ErrCustomGauge 142 | } 143 | return nil 144 | } 145 | 146 | // SubGaugeValue adds gauge to value. 147 | func (p *Prometheus) SubGaugeValue(name string, labelValues []string, value float64) error { 148 | p.customGauges.RLock() 149 | defer p.customGauges.RUnlock() 150 | 151 | if g, ok := p.customGauges.values[name]; ok { 152 | g.WithLabelValues(labelValues...).Sub(value) 153 | } else { 154 | return ErrCustomGauge 155 | } 156 | return nil 157 | } 158 | 159 | // AddCustomGauge adds a custom gauge and registers it. 160 | func (p *Prometheus) AddCustomGauge(name, help string, labels []string) { 161 | p.customGauges.Lock() 162 | defer p.customGauges.Unlock() 163 | 164 | g := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 165 | Namespace: p.Namespace, 166 | Subsystem: p.Subsystem, 167 | Name: name, 168 | Help: help, 169 | }, 170 | labels) 171 | p.customGauges.values[name] = *g 172 | p.mustRegister(g) 173 | } 174 | 175 | // IncrementCounterValue increments a custom counter. 176 | func (p *Prometheus) IncrementCounterValue(name string, labelValues []string) error { 177 | p.customCounters.RLock() 178 | defer p.customCounters.RUnlock() 179 | 180 | if g, ok := p.customCounters.values[name]; ok { 181 | g.WithLabelValues(labelValues...).Inc() 182 | } else { 183 | return ErrCustomCounter 184 | } 185 | return nil 186 | } 187 | 188 | // AddCounterValue adds value to custom counter. 189 | func (p *Prometheus) AddCounterValue(name string, labelValues []string, value float64) error { 190 | p.customCounters.RLock() 191 | defer p.customCounters.RUnlock() 192 | 193 | if g, ok := p.customCounters.values[name]; ok { 194 | g.WithLabelValues(labelValues...).Add(value) 195 | } else { 196 | return ErrCustomCounter 197 | } 198 | return nil 199 | } 200 | 201 | // AddCustomCounter adds a custom counter and registers it. 202 | func (p *Prometheus) AddCustomCounter(name, help string, labels []string) { 203 | p.customCounters.Lock() 204 | defer p.customCounters.Unlock() 205 | g := prometheus.NewCounterVec(prometheus.CounterOpts{ 206 | Namespace: p.Namespace, 207 | Subsystem: p.Subsystem, 208 | Name: name, 209 | Help: help, 210 | }, labels) 211 | p.customCounters.values[name] = *g 212 | p.mustRegister(g) 213 | } 214 | 215 | // AddCustomHistogramValue adds value to custom counter. 216 | func (p *Prometheus) AddCustomHistogramValue(name string, labelValues []string, value float64) error { 217 | p.customHistograms.RLock() 218 | defer p.customHistograms.RUnlock() 219 | 220 | if g, ok := p.customHistograms.values[name]; ok { 221 | g.WithLabelValues(labelValues...).Observe(value) 222 | } else { 223 | return ErrCustomCounter 224 | } 225 | return nil 226 | } 227 | 228 | // AddCustomCounter adds a custom counter and registers it. 229 | func (p *Prometheus) AddCustomHistogram(name, help string, labels []string) { 230 | p.customHistograms.Lock() 231 | defer p.customHistograms.Unlock() 232 | 233 | var reqDurOpts prometheus.HistogramOpts 234 | if p.nativeHistogram { 235 | reqDurOpts = prometheus.HistogramOpts{ 236 | Namespace: p.Namespace, 237 | Subsystem: p.Subsystem, 238 | NativeHistogramBucketFactor: p.NativeHistogramBucketFactor, 239 | NativeHistogramMaxBucketNumber: p.NativeHistogramMaxBucketNumber, 240 | NativeHistogramMinResetDuration: p.NativeHistogramMinResetDuration, 241 | Name: name, 242 | Help: help, 243 | } 244 | 245 | } else { 246 | reqDurOpts = prometheus.HistogramOpts{ 247 | Namespace: p.Namespace, 248 | Subsystem: p.Subsystem, 249 | Name: name, 250 | Help: help, 251 | } 252 | } 253 | 254 | g := prometheus.NewHistogramVec(reqDurOpts, labels) 255 | p.customHistograms.values[name] = *g 256 | p.mustRegister(g) 257 | } 258 | 259 | func (p *Prometheus) mustRegister(c ...prometheus.Collector) { 260 | registerer, _ := p.getRegistererAndGatherer() 261 | registerer.MustRegister(c...) 262 | } 263 | 264 | // New will initialize a new Prometheus instance with the given options. 265 | // If no options are passed, sane defaults are used. 266 | // If a router is passed using the Engine() option, this instance will 267 | // automatically bind to it. 268 | func New(options ...PrometheusOption) *Prometheus { 269 | p := &Prometheus{ 270 | MetricsPath: defaultPath, 271 | Namespace: defaultNs, 272 | Subsystem: defaultSys, 273 | HandlerNameFunc: defaultHandlerNameFunc, 274 | RequestPathFunc: defaultRequestPathFunc, 275 | RequestCounterMetricName: defaultReqCntMetricName, 276 | RequestDurationMetricName: defaultReqDurMetricName, 277 | RequestSizeMetricName: defaultReqSzMetricName, 278 | ResponseSizeMetricName: defaultResSzMetricName, 279 | nativeHistogram: false, 280 | // Grafana Mimir recommended parameters: https://grafana.com/docs/mimir/latest/send/native-histograms/ 281 | NativeHistogramBucketFactor: 1.1, 282 | NativeHistogramMaxBucketNumber: 100, 283 | NativeHistogramMinResetDuration: 1 * time.Hour, 284 | } 285 | p.customGauges.values = make(map[string]prometheus.GaugeVec) 286 | p.customCounters.values = make(map[string]prometheus.CounterVec) 287 | p.customCounterLabels = make([]string, 0) 288 | p.customHistograms.values = make(map[string]prometheus.HistogramVec) 289 | 290 | p.Ignored.values = make(map[string]bool) 291 | for _, option := range options { 292 | option(p) 293 | } 294 | 295 | p.register() 296 | if p.Engine != nil { 297 | p.Engine.GET(p.MetricsPath, p.prometheusHandler(p.Token)) 298 | } 299 | 300 | return p 301 | } 302 | 303 | func (p *Prometheus) getRegistererAndGatherer() (prometheus.Registerer, prometheus.Gatherer) { 304 | if p.Registry == nil { 305 | return prometheus.DefaultRegisterer, prometheus.DefaultGatherer 306 | } 307 | return p.Registry, p.Registry 308 | } 309 | 310 | func (p *Prometheus) register() { 311 | p.reqCnt = prometheus.NewCounterVec( 312 | prometheus.CounterOpts{ 313 | Namespace: p.Namespace, 314 | Subsystem: p.Subsystem, 315 | Name: p.RequestCounterMetricName, 316 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 317 | }, 318 | append([]string{"code", "method", "handler", "host", "path"}, p.customCounterLabels...), 319 | ) 320 | p.mustRegister(p.reqCnt) 321 | 322 | var reqDurOpts prometheus.HistogramOpts 323 | if p.nativeHistogram { 324 | reqDurOpts = prometheus.HistogramOpts{ 325 | Namespace: p.Namespace, 326 | Subsystem: p.Subsystem, 327 | NativeHistogramBucketFactor: p.NativeHistogramBucketFactor, 328 | NativeHistogramMaxBucketNumber: p.NativeHistogramMaxBucketNumber, 329 | NativeHistogramMinResetDuration: p.NativeHistogramMinResetDuration, 330 | Name: p.RequestDurationMetricName, 331 | Help: "The HTTP request latency bucket.", 332 | } 333 | 334 | } else { 335 | reqDurOpts = prometheus.HistogramOpts{ 336 | Namespace: p.Namespace, 337 | Subsystem: p.Subsystem, 338 | Buckets: p.BucketsSize, 339 | Name: p.RequestDurationMetricName, 340 | Help: "The HTTP request latency bucket.", 341 | } 342 | } 343 | 344 | p.reqDur = prometheus.NewHistogramVec(reqDurOpts, []string{"method", "path", "host"}) 345 | p.mustRegister(p.reqDur) 346 | 347 | p.reqSz = prometheus.NewSummary( 348 | prometheus.SummaryOpts{ 349 | Namespace: p.Namespace, 350 | Subsystem: p.Subsystem, 351 | Name: p.RequestSizeMetricName, 352 | Help: "The HTTP request sizes in bytes.", 353 | }, 354 | ) 355 | p.mustRegister(p.reqSz) 356 | 357 | p.resSz = prometheus.NewSummary( 358 | prometheus.SummaryOpts{ 359 | Namespace: p.Namespace, 360 | Subsystem: p.Subsystem, 361 | Name: p.ResponseSizeMetricName, 362 | Help: "The HTTP response sizes in bytes.", 363 | }, 364 | ) 365 | p.mustRegister(p.resSz) 366 | } 367 | 368 | func (p *Prometheus) isIgnored(path string) bool { 369 | p.Ignored.RLock() 370 | defer p.Ignored.RUnlock() 371 | _, ok := p.Ignored.values[path] 372 | return ok 373 | } 374 | 375 | // Instrument is a gin middleware that can be used to generate metrics for a 376 | // single handler 377 | func (p *Prometheus) Instrument() gin.HandlerFunc { 378 | return func(c *gin.Context) { 379 | start := time.Now() 380 | path := p.RequestPathFunc(c) 381 | 382 | if path == "" || p.isIgnored(path) { 383 | c.Next() 384 | return 385 | } 386 | 387 | reqSz := computeApproximateRequestSize(c.Request) 388 | 389 | c.Next() 390 | 391 | status := strconv.Itoa(c.Writer.Status()) 392 | elapsed := float64(time.Since(start)) / float64(time.Second) 393 | resSz := float64(c.Writer.Size()) 394 | 395 | labels := []string{status, c.Request.Method, p.HandlerNameFunc(c), c.Request.Host, path} 396 | if p.customCounterLabelsProvider != nil { 397 | extraLabels := p.customCounterLabelsProvider(c) 398 | for _, label := range p.customCounterLabels { 399 | labels = append(labels, extraLabels[label]) 400 | } 401 | } 402 | 403 | p.reqCnt.WithLabelValues(labels...).Inc() 404 | p.reqDur.WithLabelValues(c.Request.Method, path, c.Request.Host).Observe(elapsed) 405 | p.reqSz.Observe(float64(reqSz)) 406 | p.resSz.Observe(resSz) 407 | } 408 | } 409 | 410 | // Use is a method that should be used if the engine is set after middleware 411 | // initialization. 412 | func (p *Prometheus) Use(e *gin.Engine) { 413 | e.GET(p.MetricsPath, p.prometheusHandler(p.Token)) 414 | p.Engine = e 415 | } 416 | 417 | func (p *Prometheus) prometheusHandler(token string) gin.HandlerFunc { 418 | registerer, gatherer := p.getRegistererAndGatherer() 419 | h := promhttp.InstrumentMetricHandler( 420 | registerer, promhttp.HandlerFor(gatherer, p.HandlerOpts), 421 | ) 422 | return func(c *gin.Context) { 423 | if token == "" { 424 | h.ServeHTTP(c.Writer, c.Request) 425 | return 426 | } 427 | 428 | header := c.Request.Header.Get("Authorization") 429 | 430 | if header == "" { 431 | c.String(http.StatusUnauthorized, ErrInvalidToken.Error()) 432 | return 433 | } 434 | 435 | bearer := fmt.Sprintf("Bearer %s", token) 436 | 437 | if header != bearer { 438 | c.String(http.StatusUnauthorized, ErrInvalidToken.Error()) 439 | return 440 | } 441 | 442 | h.ServeHTTP(c.Writer, c.Request) 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /prom_test.go: -------------------------------------------------------------------------------- 1 | package ginprom 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/appleboy/gofight/v2" 11 | "github.com/gin-gonic/gin" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | io_prometheus_client "github.com/prometheus/client_model/go" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func unregister(p *Prometheus) { 19 | prometheus.Unregister(p.reqCnt) 20 | prometheus.Unregister(p.reqDur) 21 | prometheus.Unregister(p.reqSz) 22 | prometheus.Unregister(p.resSz) 23 | } 24 | 25 | func init() { 26 | gin.SetMode(gin.TestMode) 27 | } 28 | 29 | func TestPrometheus_Use(t *testing.T) { 30 | p := New() 31 | r := gin.New() 32 | 33 | p.Use(r) 34 | 35 | assert.Equal(t, 1, len(r.Routes()), "only one route should be added") 36 | assert.NotNil(t, p.Engine, "the engine should not be empty") 37 | assert.Equal(t, r, p.Engine, "used router should be the same") 38 | assert.Equal(t, r.Routes()[0].Path, p.MetricsPath, "the path should match the metrics path") 39 | unregister(p) 40 | } 41 | 42 | // Set the path (endpoint) where the metrics will be served 43 | func ExamplePath() { 44 | r := gin.New() 45 | p := New(Engine(r), Path("/metrics")) 46 | r.Use(p.Instrument()) 47 | } 48 | 49 | func TestPath(t *testing.T) { 50 | p := New() 51 | assert.Equal(t, p.MetricsPath, defaultPath, "no usage of path should yield default path") 52 | unregister(p) 53 | 54 | valid := []string{"/metrics", "/home", "/x/x", ""} 55 | for _, tt := range valid { 56 | p = New(Path(tt)) 57 | assert.Equal(t, p.MetricsPath, tt) 58 | unregister(p) 59 | } 60 | } 61 | 62 | // Set a secret token that is required to access the endpoint 63 | func ExampleToken() { 64 | r := gin.New() 65 | p := New(Engine(r), Token("supersecrettoken")) 66 | r.Use(p.Instrument()) 67 | } 68 | 69 | func TestToken(t *testing.T) { 70 | valid := []string{"token1", "token2", ""} 71 | for _, tt := range valid { 72 | p := New(Token(tt)) 73 | assert.Equal(t, tt, p.Token) 74 | unregister(p) 75 | } 76 | } 77 | 78 | func TestEngine(t *testing.T) { 79 | r := gin.New() 80 | p := New(Engine(r)) 81 | assert.Equal(t, 1, len(r.Routes()), "only one route should be added") 82 | assert.NotNil(t, p.Engine, "engine should not be nil") 83 | assert.Equal(t, r.Routes()[0].Path, p.MetricsPath, "the path should match the metrics path") 84 | assert.Equal(t, p.MetricsPath, defaultPath, "path should be default") 85 | unregister(p) 86 | } 87 | 88 | func TestRegistry(t *testing.T) { 89 | registry := prometheus.NewRegistry() 90 | 91 | p := New(Registry(registry)) 92 | assert.Equal(t, p.Registry, registry) 93 | } 94 | 95 | func TestHandlerNameFunc(t *testing.T) { 96 | r := gin.New() 97 | registry := prometheus.NewRegistry() 98 | handler := "handler_label_should_have_this_value" 99 | lhandler := fmt.Sprintf("handler=%q", handler) 100 | 101 | p := New( 102 | HandlerNameFunc(func(c *gin.Context) string { 103 | return handler 104 | }), 105 | Registry(registry), 106 | Engine(r), 107 | ) 108 | 109 | r.Use(p.Instrument()) 110 | 111 | r.GET("/", func(context *gin.Context) { 112 | context.Status(http.StatusOK) 113 | }) 114 | 115 | g := gofight.New() 116 | 117 | g.GET("/").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 118 | assert.Equal(t, response.Code, http.StatusOK) 119 | }) 120 | 121 | g.GET(p.MetricsPath).Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 122 | assert.Equal(t, response.Code, http.StatusOK) 123 | assert.Contains(t, response.Body.String(), lhandler) 124 | }) 125 | } 126 | 127 | func TestHandlerOpts(t *testing.T) { 128 | r := gin.New() 129 | registry := prometheus.NewRegistry() 130 | 131 | p := New( 132 | HandlerOpts(promhttp.HandlerOpts{Timeout: time.Nanosecond}), 133 | Registry(registry), 134 | Engine(r), 135 | ) 136 | 137 | r.Use(p.Instrument()) 138 | 139 | r.GET("/", func(context *gin.Context) { 140 | context.Status(http.StatusServiceUnavailable) 141 | }) 142 | 143 | g := gofight.New() 144 | 145 | g.GET("/").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 146 | assert.Equal(t, response.Code, http.StatusServiceUnavailable) 147 | }) 148 | 149 | g.GET(p.MetricsPath).Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 150 | assert.Equal(t, response.Code, http.StatusServiceUnavailable) 151 | }) 152 | } 153 | 154 | func TestRequestPathFunc(t *testing.T) { 155 | r := gin.New() 156 | registry := prometheus.NewRegistry() 157 | 158 | correctPath := fmt.Sprintf("path=%q", "/some/path") 159 | unknownPath := fmt.Sprintf("path=%q", "") 160 | 161 | p := New( 162 | RequestPathFunc(func(c *gin.Context) string { 163 | if fullpath := c.FullPath(); fullpath != "" { 164 | return fullpath 165 | } 166 | return "" 167 | }), 168 | Engine(r), 169 | Registry(registry), 170 | ) 171 | 172 | r.Use(p.Instrument()) 173 | 174 | r.GET("/some/path", func(context *gin.Context) { 175 | context.Status(http.StatusOK) 176 | }) 177 | 178 | g := gofight.New() 179 | g.GET("/some/path").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 180 | assert.Equal(t, response.Code, http.StatusOK) 181 | }) 182 | g.GET("/some/other/path").Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 183 | assert.Equal(t, response.Code, http.StatusNotFound) 184 | }) 185 | 186 | g.GET(p.MetricsPath).Run(r, func(response gofight.HTTPResponse, request gofight.HTTPRequest) { 187 | assert.Equal(t, response.Code, http.StatusOK) 188 | assert.Contains(t, response.Body.String(), correctPath) 189 | assert.Contains(t, response.Body.String(), unknownPath) 190 | }) 191 | } 192 | 193 | func TestNamespace(t *testing.T) { 194 | p := New() 195 | assert.Equal(t, p.Namespace, defaultNs, "namespace should be default") 196 | unregister(p) 197 | 198 | tests := []string{ 199 | "test", 200 | "", 201 | "_", 202 | } 203 | for _, test := range tests { 204 | p = New(Namespace(test)) 205 | assert.Equal(t, p.Namespace, test, "should match") 206 | unregister(p) 207 | } 208 | } 209 | 210 | func TestRequestCounterMetricName(t *testing.T) { 211 | p := New() 212 | assert.Equal(t, p.RequestCounterMetricName, defaultReqCntMetricName, "subsystem should be default") 213 | unregister(p) 214 | 215 | p = New(RequestCounterMetricName("another_req_cnt_metric_name")) 216 | assert.Equal(t, p.RequestCounterMetricName, "another_req_cnt_metric_name", "should match") 217 | unregister(p) 218 | } 219 | 220 | func TestRequestDurationMetricName(t *testing.T) { 221 | p := New() 222 | assert.Equal(t, p.RequestDurationMetricName, defaultReqDurMetricName, "subsystem should be default") 223 | unregister(p) 224 | 225 | p = New(RequestDurationMetricName("another_req_dur_metric_name")) 226 | assert.Equal(t, p.RequestDurationMetricName, "another_req_dur_metric_name", "should match") 227 | unregister(p) 228 | } 229 | 230 | func TestRequestSizeMetricName(t *testing.T) { 231 | p := New() 232 | assert.Equal(t, p.RequestSizeMetricName, defaultReqSzMetricName, "subsystem should be default") 233 | unregister(p) 234 | 235 | p = New(RequestSizeMetricName("another_req_sz_metric_name")) 236 | assert.Equal(t, p.RequestSizeMetricName, "another_req_sz_metric_name", "should match") 237 | unregister(p) 238 | } 239 | 240 | func TestResponseSizeMetricName(t *testing.T) { 241 | p := New() 242 | assert.Equal(t, p.ResponseSizeMetricName, defaultResSzMetricName, "subsystem should be default") 243 | unregister(p) 244 | 245 | p = New(ResponseSizeMetricName("another_res_sz_metric_name")) 246 | assert.Equal(t, p.ResponseSizeMetricName, "another_res_sz_metric_name", "should match") 247 | unregister(p) 248 | } 249 | 250 | func TestSubsystem(t *testing.T) { 251 | p := New() 252 | assert.Equal(t, p.Subsystem, defaultSys, "subsystem should be default") 253 | unregister(p) 254 | 255 | tests := []string{ 256 | "test", 257 | "", 258 | "_", 259 | } 260 | for _, test := range tests { 261 | p = New(Subsystem(test)) 262 | assert.Equal(t, p.Subsystem, test, "should match") 263 | unregister(p) 264 | } 265 | } 266 | 267 | func TestUse(t *testing.T) { 268 | r := gin.New() 269 | p := New() 270 | 271 | g := gofight.New() 272 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 273 | assert.Equal(t, http.StatusNotFound, r.Code) 274 | }) 275 | 276 | p.Use(r) 277 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 278 | assert.Equal(t, http.StatusOK, r.Code) 279 | }) 280 | unregister(p) 281 | } 282 | 283 | func TestBucketSize(t *testing.T) { 284 | p := New() 285 | assert.Nil(t, p.BucketsSize, "namespace should be default") 286 | unregister(p) 287 | 288 | bs := []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 289 | p = New(BucketSize(bs)) 290 | assert.Equal(t, p.BucketsSize, bs, "should match") 291 | unregister(p) 292 | } 293 | 294 | func TestInstrument(t *testing.T) { 295 | r := gin.New() 296 | p := New(Engine(r)) 297 | r.Use(p.Instrument()) 298 | path := "/user/:id" 299 | lpath := fmt.Sprintf(`path="%s"`, path) 300 | 301 | r.GET(path, func(c *gin.Context) { 302 | c.JSON(http.StatusOK, gin.H{"id": c.Param("id")}) 303 | }) 304 | 305 | g := gofight.New() 306 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 307 | assert.Equal(t, http.StatusOK, r.Code) 308 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 309 | assert.NotContains(t, r.Body.String(), lpath, "path must not be present in the response") 310 | }) 311 | 312 | g.GET("/user/10").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { assert.Equal(t, http.StatusOK, r.Code) }) 313 | 314 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 315 | assert.Equal(t, http.StatusOK, r.Code) 316 | assert.Contains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 317 | assert.Contains(t, r.Body.String(), lpath, "path must be present in the response") 318 | assert.NotContains(t, r.Body.String(), `path="/user/10"`, "raw path must not be present") 319 | }) 320 | 321 | unregister(p) 322 | } 323 | 324 | func TestThreadedInstrument(t *testing.T) { 325 | r := gin.New() 326 | p := New(Engine(r)) 327 | r.Use(p.Instrument()) 328 | path := "/user/:id" 329 | lpath := fmt.Sprintf(`path="%s"`, path) 330 | 331 | r.GET(path, func(c *gin.Context) { 332 | c.JSON(http.StatusOK, gin.H{"id": c.Param("id")}) 333 | }) 334 | 335 | var wg sync.WaitGroup 336 | for n := 0; n < 10; n++ { 337 | go func(wg *sync.WaitGroup) { 338 | g := gofight.New() 339 | 340 | g.GET("/user/10").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { assert.Equal(t, http.StatusOK, r.Code) }) 341 | 342 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 343 | assert.Equal(t, http.StatusOK, r.Code) 344 | assert.Contains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 345 | assert.Contains(t, r.Body.String(), lpath, "path must be present in the response") 346 | assert.NotContains(t, r.Body.String(), `path="/user/10"`, "raw path must not be present") 347 | }) 348 | wg.Done() 349 | }(&wg) 350 | wg.Add(1) 351 | } 352 | wg.Wait() 353 | unregister(p) 354 | } 355 | 356 | func TestEmptyRouter(t *testing.T) { 357 | r := gin.New() 358 | p := New() 359 | 360 | r.Use(p.Instrument()) 361 | r.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) }) 362 | 363 | g := gofight.New() 364 | assert.NotPanics(t, func() { 365 | g.GET("/").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {}) 366 | }) 367 | unregister(p) 368 | } 369 | 370 | func TestCustomCounterMetrics(t *testing.T) { 371 | r := gin.New() 372 | p := New(Engine(r), Registry(prometheus.NewRegistry()), CustomCounterLabels([]string{"client_id", "tenant_id"}, func(c *gin.Context) map[string]string { 373 | clientId := c.GetHeader("X-Client-ID") 374 | if clientId == "" { 375 | clientId = "unknown" 376 | } 377 | tenantId := c.GetHeader("X-Tenant-ID") 378 | if tenantId == "" { 379 | tenantId = "unknown" 380 | } 381 | return map[string]string{ 382 | "client_id": clientId, 383 | "tenant_id": tenantId, 384 | } 385 | })) 386 | r.Use(p.Instrument()) 387 | 388 | r.GET("/ping", func(c *gin.Context) { 389 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 390 | }) 391 | 392 | g := gofight.New() 393 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 394 | assert.Equal(t, http.StatusOK, r.Code) 395 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 396 | assert.NotContains(t, r.Body.String(), "client_id") 397 | assert.NotContains(t, r.Body.String(), "tenant_id") 398 | }) 399 | 400 | g.GET("/ping").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { assert.Equal(t, http.StatusOK, r.Code) }) 401 | 402 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 403 | body := r.Body.String() 404 | assert.Equal(t, http.StatusOK, r.Code) 405 | assert.Contains(t, body, prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 406 | assert.Contains(t, r.Body.String(), "client_id=\"unknown\"") 407 | assert.Contains(t, r.Body.String(), "tenant_id=\"unknown\"") 408 | assert.NotContains(t, r.Body.String(), "client_id=\"client-id\"") 409 | assert.NotContains(t, r.Body.String(), "tenant_id=\"tenant-id\"") 410 | }) 411 | 412 | g.GET("/ping"). 413 | SetHeader(gofight.H{"X-Client-Id": "client-id", "X-Tenant-Id": "tenant-id"}). 414 | Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { assert.Equal(t, http.StatusOK, r.Code) }) 415 | 416 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 417 | body := r.Body.String() 418 | assert.Equal(t, http.StatusOK, r.Code) 419 | assert.Contains(t, body, prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 420 | assert.Contains(t, r.Body.String(), "client_id=\"unknown\"") 421 | assert.Contains(t, r.Body.String(), "tenant_id=\"unknown\"") 422 | assert.Contains(t, r.Body.String(), "client_id=\"client-id\"") 423 | assert.Contains(t, r.Body.String(), "tenant_id=\"tenant-id\"") 424 | }) 425 | unregister(p) 426 | } 427 | 428 | func TestCustomHistogram(t *testing.T) { 429 | r := gin.New() 430 | p := New(Engine(r), Registry(prometheus.NewRegistry())) 431 | p.AddCustomHistogram("request_latency", "test histogram", []string{"url", "method"}) 432 | r.Use(p.Instrument()) 433 | defer unregister(p) 434 | 435 | r.GET("/ping", func(c *gin.Context) { 436 | err := p.AddCustomHistogramValue("request_latency", []string{"http://example.com/status", "GET"}, 0.45) 437 | assert.NoError(t, err) 438 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 439 | }) 440 | r.GET("/pong", func(c *gin.Context) { 441 | err := p.AddCustomHistogramValue("request_latency", []string{"http://example.com/status", "GET"}, 9.56) 442 | assert.NoError(t, err) 443 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 444 | }) 445 | r.GET("/error", func(c *gin.Context) { 446 | // Metric not found 447 | err := p.AddCustomHistogramValue("invalid", []string{}, 9.56) 448 | assert.Error(t, err) 449 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 450 | }) 451 | 452 | expectedLines := []string{ 453 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.005"} 0`, 454 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.01"} 0`, 455 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.025"} 0`, 456 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.05"} 0`, 457 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.1"} 0`, 458 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.25"} 0`, 459 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="0.5"} 1`, 460 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="1"} 1`, 461 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="2.5"} 1`, 462 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="5"} 1`, 463 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="10"} 2`, 464 | `gin_gonic_request_latency_bucket{method="GET",url="http://example.com/status",le="+Inf"} 2`, 465 | `gin_gonic_request_latency_sum{method="GET",url="http://example.com/status"} 10.01`, 466 | `gin_gonic_request_latency_count{method="GET",url="http://example.com/status"} 2`, 467 | } 468 | 469 | g := gofight.New() 470 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 471 | assert.Equal(t, http.StatusOK, r.Code) 472 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "request_latency")) 473 | 474 | for _, line := range expectedLines { 475 | assert.NotContains(t, r.Body.String(), line) 476 | } 477 | }) 478 | 479 | g.GET("/ping").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 480 | assert.Equal(t, http.StatusOK, r.Code) 481 | }) 482 | g.GET("/pong").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 483 | assert.Equal(t, http.StatusOK, r.Code) 484 | }) 485 | g.GET("/error").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 486 | assert.Equal(t, http.StatusOK, r.Code) 487 | }) 488 | 489 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 490 | assert.Equal(t, http.StatusOK, r.Code) 491 | assert.Contains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "request_latency")) 492 | 493 | for _, line := range expectedLines { 494 | assert.Contains(t, r.Body.String(), line) 495 | } 496 | }) 497 | } 498 | 499 | func TestCustomNativeHistogram(t *testing.T) { 500 | r := gin.New() 501 | registry := prometheus.NewRegistry() 502 | p := New(Engine(r), Registry(registry), NativeHistogram(true)) 503 | p.AddCustomHistogram("custom_histogram", "test histogram", []string{"url", "method"}) 504 | r.Use(p.Instrument()) 505 | defer unregister(p) 506 | 507 | err := p.AddCustomHistogramValue("custom_histogram", []string{"http://example.com/status", "GET"}, 0.45) 508 | assert.Nil(t, err) 509 | 510 | mfs, err := registry.Gather() 511 | assert.Nil(t, err) 512 | 513 | found := false 514 | 515 | for _, mf := range mfs { 516 | if mf.GetType() == io_prometheus_client.MetricType_HISTOGRAM { 517 | for _, m := range mf.Metric { 518 | if mf.GetName() == "gin_gonic_custom_histogram" { 519 | found = true 520 | assert.Equal(t, int32(3), m.GetHistogram().GetSchema()) 521 | assert.Equal(t, uint64(0x1), m.GetHistogram().GetSampleCount()) 522 | assert.Equal(t, 0.45, m.GetHistogram().GetSampleSum()) 523 | } 524 | } 525 | } 526 | } 527 | 528 | assert.True(t, found) 529 | } 530 | 531 | func TestIgnore(t *testing.T) { 532 | r := gin.New() 533 | ipath := "/ping" 534 | lipath := fmt.Sprintf(`path="%s"`, ipath) 535 | p := New(Engine(r), Ignore(ipath)) 536 | r.Use(p.Instrument()) 537 | 538 | r.GET(ipath, func(c *gin.Context) { 539 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 540 | }) 541 | 542 | g := gofight.New() 543 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 544 | assert.Equal(t, http.StatusOK, r.Code) 545 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 546 | }) 547 | 548 | g.GET("/ping").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { assert.Equal(t, http.StatusOK, r.Code) }) 549 | 550 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 551 | assert.Equal(t, http.StatusOK, r.Code) 552 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 553 | assert.NotContains(t, r.Body.String(), lipath, "ignored path must not be present") 554 | }) 555 | unregister(p) 556 | } 557 | 558 | func TestMetricsPathIgnored(t *testing.T) { 559 | r := gin.New() 560 | p := New(Engine(r)) 561 | r.Use(p.Instrument()) 562 | 563 | g := gofight.New() 564 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 565 | assert.Equal(t, http.StatusOK, r.Code) 566 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 567 | }) 568 | unregister(p) 569 | } 570 | 571 | func TestMetricsBearerToken(t *testing.T) { 572 | r := gin.New() 573 | p := New(Engine(r), Token("test-1234")) 574 | r.Use(p.Instrument()) 575 | 576 | g := gofight.New() 577 | 578 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 579 | assert.Equal(t, http.StatusUnauthorized, r.Code) 580 | assert.Equal(t, ErrInvalidToken.Error(), r.Body.String()) 581 | }) 582 | 583 | g.GET(p.MetricsPath). 584 | SetHeader(gofight.H{ 585 | "Authorization": "Bearer " + "test-1234-5678", 586 | }). 587 | Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 588 | assert.Equal(t, http.StatusUnauthorized, r.Code) 589 | assert.Equal(t, ErrInvalidToken.Error(), r.Body.String()) 590 | }) 591 | 592 | g.GET(p.MetricsPath). 593 | SetHeader(gofight.H{ 594 | "Authorization": "Bearer " + "test-1234", 595 | }). 596 | Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 597 | assert.Equal(t, http.StatusOK, r.Code) 598 | assert.NotContains(t, r.Body.String(), prometheus.BuildFQName(p.Namespace, p.Subsystem, "requests_total")) 599 | }) 600 | unregister(p) 601 | } 602 | 603 | func TestCustomCounterErr(t *testing.T) { 604 | p := New() 605 | assert.Equal(t, p.IncrementCounterValue("not_found", []string{"some", "labels"}), ErrCustomCounter) 606 | assert.Equal(t, p.AddCounterValue("not_found", []string{"some", "labels"}, 1.), ErrCustomCounter) 607 | unregister(p) 608 | } 609 | 610 | func TestCustomGaugeErr(t *testing.T) { 611 | p := New() 612 | assert.Equal(t, p.IncrementGaugeValue("not_found", []string{"some", "labels"}), ErrCustomGauge) 613 | assert.Equal(t, p.DecrementGaugeValue("not_found", []string{"some", "labels"}), ErrCustomGauge) 614 | assert.Equal(t, p.AddGaugeValue("not_found", []string{"some", "labels"}, 1.), ErrCustomGauge) 615 | assert.Equal(t, p.SubGaugeValue("not_found", []string{"some", "labels"}, 1.), ErrCustomGauge) 616 | assert.Equal(t, p.SetGaugeValue("not_found", []string{"some", "labels"}, 1.), ErrCustomGauge) 617 | unregister(p) 618 | } 619 | 620 | func TestInstrumentCustomCounter(t *testing.T) { 621 | var helpText = "help text" 622 | var labels = []string{"label1"} 623 | var name = "custom_counter" 624 | 625 | r := gin.New() 626 | p := New(Engine(r)) 627 | p.AddCustomCounter(name, helpText, labels) 628 | r.Use(p.Instrument()) 629 | 630 | r.GET("/inc", func(c *gin.Context) { 631 | err := p.IncrementCounterValue(name, labels) 632 | assert.NoError(t, err, "should not fail with same Counter name") 633 | c.Status(http.StatusOK) 634 | }) 635 | 636 | r.GET("/add", func(c *gin.Context) { 637 | err := p.AddCounterValue(name, labels, 10) 638 | assert.NoError(t, err, "should not fail with same Counter name") 639 | c.Status(http.StatusOK) 640 | }) 641 | 642 | g := gofight.New() 643 | 644 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 645 | assert.NotContains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 646 | assert.NotContains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 0`, name, labels[0], labels[0])) 647 | assert.Equal(t, http.StatusOK, r.Code) 648 | }) 649 | 650 | g.GET("/inc").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 651 | assert.Equal(t, http.StatusOK, r.Code) 652 | }) 653 | 654 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 655 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 656 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 1`, name, labels[0], labels[0])) 657 | assert.Equal(t, http.StatusOK, r.Code) 658 | }) 659 | 660 | g.GET("/add").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 661 | assert.Equal(t, http.StatusOK, r.Code) 662 | }) 663 | 664 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 665 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 666 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 11`, name, labels[0], labels[0])) 667 | assert.Equal(t, http.StatusOK, r.Code) 668 | }) 669 | 670 | unregister(p) 671 | } 672 | 673 | func TestInstrumentCustomGauge(t *testing.T) { 674 | var helpText = "help text" 675 | var labels = []string{"label1"} 676 | var name = "custom_gauge" 677 | 678 | r := gin.New() 679 | p := New(Engine(r)) 680 | p.AddCustomGauge(name, helpText, labels) 681 | r.Use(p.Instrument()) 682 | 683 | r.GET("/inc", func(c *gin.Context) { 684 | err := p.IncrementGaugeValue(name, labels) 685 | assert.NoError(t, err, "should not fail with same gauge name") 686 | c.Status(http.StatusOK) 687 | }) 688 | 689 | r.GET("/dec", func(c *gin.Context) { 690 | err := p.DecrementGaugeValue(name, labels) 691 | assert.NoError(t, err, "should not fail with same gauge name") 692 | c.Status(http.StatusOK) 693 | }) 694 | 695 | r.GET("/set", func(c *gin.Context) { 696 | err := p.SetGaugeValue(name, labels, 10) 697 | assert.NoError(t, err, "should not fail with same gauge name") 698 | c.Status(http.StatusOK) 699 | }) 700 | 701 | r.GET("/add", func(c *gin.Context) { 702 | err := p.AddGaugeValue(name, labels, 10) 703 | assert.NoError(t, err, "should not fail with same gauge name") 704 | c.Status(http.StatusOK) 705 | }) 706 | 707 | r.GET("/sub", func(c *gin.Context) { 708 | err := p.SubGaugeValue(name, labels, 10) 709 | assert.NoError(t, err, "should not fail with same gauge name") 710 | c.Status(http.StatusOK) 711 | }) 712 | 713 | g := gofight.New() 714 | 715 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 716 | assert.NotContains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 717 | assert.NotContains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 0`, name, labels[0], labels[0])) 718 | assert.Equal(t, http.StatusOK, r.Code) 719 | }) 720 | 721 | g.GET("/inc").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 722 | assert.Equal(t, http.StatusOK, r.Code) 723 | }) 724 | 725 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 726 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 727 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 1`, name, labels[0], labels[0])) 728 | assert.Equal(t, http.StatusOK, r.Code) 729 | }) 730 | 731 | g.GET("/dec").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 732 | assert.Equal(t, http.StatusOK, r.Code) 733 | }) 734 | 735 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 736 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 737 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 0`, name, labels[0], labels[0])) 738 | assert.Equal(t, http.StatusOK, r.Code) 739 | }) 740 | 741 | g.GET("/set").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 742 | assert.Equal(t, http.StatusOK, r.Code) 743 | }) 744 | 745 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 746 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 747 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 10`, name, labels[0], labels[0])) 748 | assert.Equal(t, http.StatusOK, r.Code) 749 | }) 750 | 751 | g.GET("/add").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 752 | assert.Equal(t, http.StatusOK, r.Code) 753 | }) 754 | 755 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 756 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 757 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 20`, name, labels[0], labels[0])) 758 | assert.Equal(t, http.StatusOK, r.Code) 759 | }) 760 | 761 | g.GET("/sub").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 762 | assert.Equal(t, http.StatusOK, r.Code) 763 | }) 764 | 765 | g.GET(p.MetricsPath).Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 766 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`# HELP gin_gonic_%s %s`, name, helpText)) 767 | assert.Contains(t, r.Body.String(), fmt.Sprintf(`gin_gonic_%s{%s="%s"} 10`, name, labels[0], labels[0])) 768 | assert.Equal(t, http.StatusOK, r.Code) 769 | }) 770 | 771 | unregister(p) 772 | } 773 | 774 | func TestInstrumentCustomMetricsErrors(t *testing.T) { 775 | r := gin.New() 776 | p := New(Engine(r)) 777 | r.Use(p.Instrument()) 778 | 779 | r.GET("/err", func(c *gin.Context) { 780 | err := p.IncrementGaugeValue("notfound", []string{}) 781 | assert.EqualError(t, err, "error finding custom gauge") 782 | c.Status(http.StatusOK) 783 | }) 784 | g := gofight.New() 785 | 786 | g.GET("/err").Run(r, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 787 | assert.Equal(t, http.StatusOK, r.Code) 788 | }) 789 | 790 | unregister(p) 791 | } 792 | 793 | func TestMultipleGinWithDifferentRegistry(t *testing.T) { 794 | // with different registries we don't panic because of multiple metric registration attempt 795 | r1 := gin.New() 796 | p1 := New(Engine(r1), Registry(prometheus.NewRegistry())) 797 | r1.Use(p1.Instrument()) 798 | 799 | r2 := gin.New() 800 | p2 := New(Engine(r2), Registry(prometheus.NewRegistry())) 801 | r2.Use(p2.Instrument()) 802 | } 803 | 804 | func TestCustomGaugeCorrectRegistry(t *testing.T) { 805 | reg := prometheus.NewRegistry() 806 | p := New(Registry(reg)) 807 | 808 | p.AddCustomGauge("some_gauge", "", nil) 809 | // increment the gauge value so it is reported by Gather 810 | err := p.IncrementGaugeValue("some_gauge", nil) 811 | assert.Nil(t, err) 812 | 813 | fams, err := reg.Gather() 814 | assert.Nil(t, err) 815 | assert.Len(t, fams, 3) 816 | 817 | assert.Condition(t, func() (success bool) { 818 | for _, fam := range fams { 819 | if fam.GetName() == fmt.Sprintf("%s_%s_some_gauge", p.Namespace, p.Subsystem) { 820 | return true 821 | } 822 | } 823 | return false 824 | }) 825 | } 826 | 827 | func TestCustomCounterCorrectRegistry(t *testing.T) { 828 | reg := prometheus.NewRegistry() 829 | p := New(Registry(reg)) 830 | 831 | p.AddCustomCounter("some_counter", "", nil) 832 | // increment the counter value so it is reported by Gather 833 | err := p.IncrementCounterValue("some_counter", nil) 834 | assert.Nil(t, err) 835 | 836 | fams, err := reg.Gather() 837 | assert.Nil(t, err) 838 | assert.Len(t, fams, 3) 839 | 840 | assert.Condition(t, func() (success bool) { 841 | for _, fam := range fams { 842 | if fam.GetName() == fmt.Sprintf("%s_%s_some_counter", p.Namespace, p.Subsystem) { 843 | return true 844 | } 845 | } 846 | return false 847 | }) 848 | } 849 | --------------------------------------------------------------------------------