├── .gitignore ├── LICENSE ├── README.md ├── example └── example.go └── middleware.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | vendor 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Zachary Sais 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-gin-prometheus 2 | [![](https://godoc.org/github.com/zsais/go-gin-prometheus?status.svg)](https://godoc.org/github.com/zsais/go-gin-prometheus) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | 4 | Gin Web Framework Prometheus metrics exporter 5 | 6 | ## Installation 7 | 8 | `$ go get github.com/zsais/go-gin-prometheus` 9 | 10 | ## Usage 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "github.com/gin-gonic/gin" 17 | "github.com/zsais/go-gin-prometheus" 18 | ) 19 | 20 | func main() { 21 | r := gin.New() 22 | 23 | p := ginprometheus.NewPrometheus("gin") 24 | p.Use(r) 25 | 26 | r.GET("/", func(c *gin.Context) { 27 | c.JSON(200, "Hello world!") 28 | }) 29 | 30 | r.Run(":29090") 31 | } 32 | ``` 33 | 34 | See the [example.go file](https://github.com/zsais/go-gin-prometheus/blob/master/example/example.go) 35 | 36 | ## Preserving a low cardinality for the request counter 37 | 38 | The request counter (`requests_total`) has a `url` label which, 39 | although desirable, can become problematic in cases where your 40 | application uses templated routes expecting a great number of 41 | variations, as Prometheus explicitly recommends against metrics having 42 | high cardinality dimensions: 43 | 44 | https://prometheus.io/docs/practices/naming/#labels 45 | 46 | If you have for instance a `/customer/:name` templated route and you 47 | don't want to generate a time series for every possible customer name, 48 | you could supply this mapping function to the middleware: 49 | 50 | ```go 51 | package main 52 | 53 | import ( 54 | "github.com/gin-gonic/gin" 55 | "github.com/zsais/go-gin-prometheus" 56 | ) 57 | 58 | func main() { 59 | r := gin.New() 60 | 61 | p := ginprometheus.NewPrometheus("gin") 62 | 63 | p.ReqCntURLLabelMappingFn = func(c *gin.Context) string { 64 | url := c.Request.URL.Path 65 | for _, p := range c.Params { 66 | if p.Key == "name" { 67 | url = strings.Replace(url, p.Value, ":name", 1) 68 | break 69 | } 70 | } 71 | return url 72 | } 73 | 74 | p.Use(r) 75 | 76 | r.GET("/", func(c *gin.Context) { 77 | c.JSON(200, "Hello world!") 78 | }) 79 | 80 | r.Run(":29090") 81 | } 82 | ``` 83 | 84 | which would map `/customer/alice` and `/customer/bob` to their 85 | template `/customer/:name`, and thus preserve a low cardinality for 86 | our metrics. 87 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mcuadros/go-gin-prometheus" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func main() { 10 | r := gin.New() 11 | 12 | /* // Optional custom metrics list 13 | customMetrics := []*ginprometheus.Metric{ 14 | &ginprometheus.Metric{ 15 | ID: "1234", // optional string 16 | Name: "test_metric", // required string 17 | Description: "Counter test metric", // required string 18 | Type: "counter", // required string 19 | }, 20 | &ginprometheus.Metric{ 21 | ID: "1235", // Identifier 22 | Name: "test_metric_2", // Metric Name 23 | Description: "Summary test metric", // Help Description 24 | Type: "summary", // type associated with prometheus collector 25 | }, 26 | // Type Options: 27 | // counter, counter_vec, gauge, gauge_vec, 28 | // histogram, histogram_vec, summary, summary_vec 29 | } 30 | p := ginprometheus.NewPrometheus("gin", customMetrics) 31 | */ 32 | 33 | p := ginprometheus.NewPrometheus("gin") 34 | 35 | p.Use(r) 36 | r.GET("/", func(c *gin.Context) { 37 | c.JSON(200, "Hello world!") 38 | }) 39 | 40 | r.Run(":29090") 41 | } 42 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package ginprometheus 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var defaultMetricPath = "/metrics" 18 | 19 | // Standard default metrics 20 | // counter, counter_vec, gauge, gauge_vec, 21 | // histogram, histogram_vec, summary, summary_vec 22 | var reqCnt = &Metric{ 23 | ID: "reqCnt", 24 | Name: "requests_total", 25 | Description: "How many HTTP requests processed, partitioned by status code and HTTP method.", 26 | Type: "counter_vec", 27 | Args: []string{"code", "method", "handler", "host", "url"}} 28 | 29 | var reqDur = &Metric{ 30 | ID: "reqDur", 31 | Name: "request_duration_seconds", 32 | Description: "The HTTP request latencies in seconds.", 33 | Type: "histogram_vec", 34 | Args: []string{"code", "method", "url"}, 35 | } 36 | 37 | var resSz = &Metric{ 38 | ID: "resSz", 39 | Name: "response_size_bytes", 40 | Description: "The HTTP response sizes in bytes.", 41 | Type: "summary"} 42 | 43 | var reqSz = &Metric{ 44 | ID: "reqSz", 45 | Name: "request_size_bytes", 46 | Description: "The HTTP request sizes in bytes.", 47 | Type: "summary"} 48 | 49 | var standardMetrics = []*Metric{ 50 | reqCnt, 51 | reqDur, 52 | resSz, 53 | reqSz, 54 | } 55 | 56 | /* 57 | RequestCounterURLLabelMappingFn is a function which can be supplied to the middleware to control 58 | the cardinality of the request counter's "url" label, which might be required in some contexts. 59 | For instance, if for a "/customer/:name" route you don't want to generate a time series for every 60 | possible customer name, you could use this function: 61 | 62 | func(c *gin.Context) string { 63 | url := c.Request.URL.Path 64 | for _, p := range c.Params { 65 | if p.Key == "name" { 66 | url = strings.Replace(url, p.Value, ":name", 1) 67 | break 68 | } 69 | } 70 | return url 71 | } 72 | 73 | which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". 74 | */ 75 | type RequestCounterURLLabelMappingFn func(c *gin.Context) string 76 | 77 | // Metric is a definition for the name, description, type, ID, and 78 | // prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric 79 | type Metric struct { 80 | MetricCollector prometheus.Collector 81 | ID string 82 | Name string 83 | Description string 84 | Type string 85 | Args []string 86 | } 87 | 88 | // Prometheus contains the metrics gathered by the instance and its path 89 | type Prometheus struct { 90 | reqCnt *prometheus.CounterVec 91 | reqDur *prometheus.HistogramVec 92 | reqSz, resSz prometheus.Summary 93 | router *gin.Engine 94 | listenAddress string 95 | Ppg PrometheusPushGateway 96 | 97 | MetricsList []*Metric 98 | MetricsPath string 99 | 100 | ReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn 101 | 102 | // gin.Context string to use as a prometheus URL label 103 | URLLabelFromContext string 104 | } 105 | 106 | // PrometheusPushGateway contains the configuration for pushing to a Prometheus pushgateway (optional) 107 | type PrometheusPushGateway struct { 108 | 109 | // Push interval in seconds 110 | PushIntervalSeconds time.Duration 111 | 112 | // Push Gateway URL in format http://domain:port 113 | // where JOBNAME can be any string of your choice 114 | PushGatewayURL string 115 | 116 | // Local metrics URL where metrics are fetched from, this could be ommited in the future 117 | // if implemented using prometheus common/expfmt instead 118 | MetricsURL string 119 | 120 | // pushgateway job name, defaults to "gin" 121 | Job string 122 | } 123 | 124 | // NewPrometheus generates a new set of metrics with a certain subsystem name 125 | func NewPrometheus(subsystem string, customMetricsList ...[]*Metric) *Prometheus { 126 | 127 | var metricsList []*Metric 128 | 129 | if len(customMetricsList) > 1 { 130 | panic("Too many args. NewPrometheus( string, ).") 131 | } else if len(customMetricsList) == 1 { 132 | metricsList = customMetricsList[0] 133 | } 134 | 135 | for _, metric := range standardMetrics { 136 | metricsList = append(metricsList, metric) 137 | } 138 | 139 | p := &Prometheus{ 140 | MetricsList: metricsList, 141 | MetricsPath: defaultMetricPath, 142 | ReqCntURLLabelMappingFn: func(c *gin.Context) string { 143 | return c.Request.URL.Path // i.e. by default do nothing, i.e. return URL as is 144 | }, 145 | } 146 | 147 | p.registerMetrics(subsystem) 148 | 149 | return p 150 | } 151 | 152 | // SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL 153 | // every pushIntervalSeconds. Metrics are fetched from metricsURL 154 | func (p *Prometheus) SetPushGateway(pushGatewayURL, metricsURL string, pushIntervalSeconds time.Duration) { 155 | p.Ppg.PushGatewayURL = pushGatewayURL 156 | p.Ppg.MetricsURL = metricsURL 157 | p.Ppg.PushIntervalSeconds = pushIntervalSeconds 158 | p.startPushTicker() 159 | } 160 | 161 | // SetPushGatewayJob job name, defaults to "gin" 162 | func (p *Prometheus) SetPushGatewayJob(j string) { 163 | p.Ppg.Job = j 164 | } 165 | 166 | // SetListenAddress for exposing metrics on address. If not set, it will be exposed at the 167 | // same address of the gin engine that is being used 168 | func (p *Prometheus) SetListenAddress(address string) { 169 | p.listenAddress = address 170 | if p.listenAddress != "" { 171 | p.router = gin.Default() 172 | } 173 | } 174 | 175 | // SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of 176 | // your content's access log). 177 | func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *gin.Engine) { 178 | p.listenAddress = listenAddress 179 | if len(p.listenAddress) > 0 { 180 | p.router = r 181 | } 182 | } 183 | 184 | // SetMetricsPath set metrics paths 185 | func (p *Prometheus) SetMetricsPath(e *gin.Engine) { 186 | 187 | if p.listenAddress != "" { 188 | p.router.GET(p.MetricsPath, prometheusHandler()) 189 | p.runServer() 190 | } else { 191 | e.GET(p.MetricsPath, prometheusHandler()) 192 | } 193 | } 194 | 195 | // SetMetricsPathWithAuth set metrics paths with authentication 196 | func (p *Prometheus) SetMetricsPathWithAuth(e *gin.Engine, accounts gin.Accounts) { 197 | 198 | if p.listenAddress != "" { 199 | p.router.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler()) 200 | p.runServer() 201 | } else { 202 | e.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler()) 203 | } 204 | 205 | } 206 | 207 | func (p *Prometheus) runServer() { 208 | if p.listenAddress != "" { 209 | go p.router.Run(p.listenAddress) 210 | } 211 | } 212 | 213 | func (p *Prometheus) getMetrics() []byte { 214 | response, _ := http.Get(p.Ppg.MetricsURL) 215 | 216 | defer response.Body.Close() 217 | body, _ := ioutil.ReadAll(response.Body) 218 | 219 | return body 220 | } 221 | 222 | func (p *Prometheus) getPushGatewayURL() string { 223 | h, _ := os.Hostname() 224 | if p.Ppg.Job == "" { 225 | p.Ppg.Job = "gin" 226 | } 227 | return p.Ppg.PushGatewayURL + "/metrics/job/" + p.Ppg.Job + "/instance/" + h 228 | } 229 | 230 | func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) { 231 | req, err := http.NewRequest("POST", p.getPushGatewayURL(), bytes.NewBuffer(metrics)) 232 | client := &http.Client{} 233 | if _, err = client.Do(req); err != nil { 234 | log.WithError(err).Errorln("Error sending to push gateway") 235 | } 236 | } 237 | 238 | func (p *Prometheus) startPushTicker() { 239 | ticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds) 240 | go func() { 241 | for range ticker.C { 242 | p.sendMetricsToPushGateway(p.getMetrics()) 243 | } 244 | }() 245 | } 246 | 247 | // NewMetric associates prometheus.Collector based on Metric.Type 248 | func NewMetric(m *Metric, subsystem string) prometheus.Collector { 249 | var metric prometheus.Collector 250 | switch m.Type { 251 | case "counter_vec": 252 | metric = prometheus.NewCounterVec( 253 | prometheus.CounterOpts{ 254 | Subsystem: subsystem, 255 | Name: m.Name, 256 | Help: m.Description, 257 | }, 258 | m.Args, 259 | ) 260 | case "counter": 261 | metric = prometheus.NewCounter( 262 | prometheus.CounterOpts{ 263 | Subsystem: subsystem, 264 | Name: m.Name, 265 | Help: m.Description, 266 | }, 267 | ) 268 | case "gauge_vec": 269 | metric = prometheus.NewGaugeVec( 270 | prometheus.GaugeOpts{ 271 | Subsystem: subsystem, 272 | Name: m.Name, 273 | Help: m.Description, 274 | }, 275 | m.Args, 276 | ) 277 | case "gauge": 278 | metric = prometheus.NewGauge( 279 | prometheus.GaugeOpts{ 280 | Subsystem: subsystem, 281 | Name: m.Name, 282 | Help: m.Description, 283 | }, 284 | ) 285 | case "histogram_vec": 286 | metric = prometheus.NewHistogramVec( 287 | prometheus.HistogramOpts{ 288 | Subsystem: subsystem, 289 | Name: m.Name, 290 | Help: m.Description, 291 | }, 292 | m.Args, 293 | ) 294 | case "histogram": 295 | metric = prometheus.NewHistogram( 296 | prometheus.HistogramOpts{ 297 | Subsystem: subsystem, 298 | Name: m.Name, 299 | Help: m.Description, 300 | }, 301 | ) 302 | case "summary_vec": 303 | metric = prometheus.NewSummaryVec( 304 | prometheus.SummaryOpts{ 305 | Subsystem: subsystem, 306 | Name: m.Name, 307 | Help: m.Description, 308 | }, 309 | m.Args, 310 | ) 311 | case "summary": 312 | metric = prometheus.NewSummary( 313 | prometheus.SummaryOpts{ 314 | Subsystem: subsystem, 315 | Name: m.Name, 316 | Help: m.Description, 317 | }, 318 | ) 319 | } 320 | return metric 321 | } 322 | 323 | func (p *Prometheus) registerMetrics(subsystem string) { 324 | 325 | for _, metricDef := range p.MetricsList { 326 | metric := NewMetric(metricDef, subsystem) 327 | if err := prometheus.Register(metric); err != nil { 328 | log.WithError(err).Errorf("%s could not be registered in Prometheus", metricDef.Name) 329 | } 330 | switch metricDef { 331 | case reqCnt: 332 | p.reqCnt = metric.(*prometheus.CounterVec) 333 | case reqDur: 334 | p.reqDur = metric.(*prometheus.HistogramVec) 335 | case resSz: 336 | p.resSz = metric.(prometheus.Summary) 337 | case reqSz: 338 | p.reqSz = metric.(prometheus.Summary) 339 | } 340 | metricDef.MetricCollector = metric 341 | } 342 | } 343 | 344 | // Use adds the middleware to a gin engine. 345 | func (p *Prometheus) Use(e *gin.Engine) { 346 | e.Use(p.HandlerFunc()) 347 | p.SetMetricsPath(e) 348 | } 349 | 350 | // UseWithAuth adds the middleware to a gin engine with BasicAuth. 351 | func (p *Prometheus) UseWithAuth(e *gin.Engine, accounts gin.Accounts) { 352 | e.Use(p.HandlerFunc()) 353 | p.SetMetricsPathWithAuth(e, accounts) 354 | } 355 | 356 | // HandlerFunc defines handler function for middleware 357 | func (p *Prometheus) HandlerFunc() gin.HandlerFunc { 358 | return func(c *gin.Context) { 359 | if c.Request.URL.Path == p.MetricsPath { 360 | c.Next() 361 | return 362 | } 363 | 364 | start := time.Now() 365 | reqSz := computeApproximateRequestSize(c.Request) 366 | 367 | c.Next() 368 | 369 | status := strconv.Itoa(c.Writer.Status()) 370 | elapsed := float64(time.Since(start)) / float64(time.Second) 371 | resSz := float64(c.Writer.Size()) 372 | 373 | url := p.ReqCntURLLabelMappingFn(c) 374 | // jlambert Oct 2018 - sidecar specific mod 375 | if len(p.URLLabelFromContext) > 0 { 376 | u, found := c.Get(p.URLLabelFromContext) 377 | if !found { 378 | u = "unknown" 379 | } 380 | url = u.(string) 381 | } 382 | p.reqDur.WithLabelValues(status, c.Request.Method, url).Observe(elapsed) 383 | p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc() 384 | p.reqSz.Observe(float64(reqSz)) 385 | p.resSz.Observe(resSz) 386 | } 387 | } 388 | 389 | func prometheusHandler() gin.HandlerFunc { 390 | h := promhttp.Handler() 391 | return func(c *gin.Context) { 392 | h.ServeHTTP(c.Writer, c.Request) 393 | } 394 | } 395 | 396 | // From https://github.com/DanielHeckrath/gin-prometheus/blob/master/gin_prometheus.go 397 | func computeApproximateRequestSize(r *http.Request) int { 398 | s := 0 399 | if r.URL != nil { 400 | s = len(r.URL.Path) 401 | } 402 | 403 | s += len(r.Method) 404 | s += len(r.Proto) 405 | for name, values := range r.Header { 406 | s += len(name) 407 | for _, value := range values { 408 | s += len(value) 409 | } 410 | } 411 | s += len(r.Host) 412 | 413 | // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 414 | 415 | if r.ContentLength != -1 { 416 | s += int(r.ContentLength) 417 | } 418 | return s 419 | } 420 | --------------------------------------------------------------------------------