├── .gitignore ├── LICENSE ├── README.md ├── examples ├── Makefile └── main.go ├── go.mod ├── go.sum ├── metrics.go └── screenshots └── grafana.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Zerodha Technology Pvt. Ltd. (India) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastglue-metrics 2 | 3 | ![grafana-screenshot](screenshots/grafana.png) 4 | 5 | ## Overview [![Go Reference](https://pkg.go.dev/badge/github.com/zerodha/fastglue-metrics.svg)](https://pkg.go.dev/github.com/zerodha/fastglue-metrics) [![Zerodha Tech](https://zerodha.tech/static/images/github-badge.svg)](https://zerodha.tech) 6 | 7 | This package provides an abstraction to collect HTTP metrics from any Golang application using the package [fastglue](https://github.com/zerodha/fastglue). It uses [before-after](https://github.com/zerodha/fastglue/tree/master/examples/before-after) middlewares to collect metrics about the request such as: 8 | 9 | - Count of HTTP requests. 10 | - Latency of each request. 11 | - Response size. 12 | 13 | The metrics collection is inspired from [RED](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/) principles of application monitoring. The components of this monitoring philosophy are: 14 | 15 | - **Rate** (the number of requests per second) 16 | - **Errors** (the number of those requests that are failing) 17 | - **Duration** (the amount of time those requests take) 18 | 19 | All the metrics are grouped by the following labels: 20 | 21 | - `status`: (HTTP Status Code) 22 | - `path`: (Request URI) 23 | - `method`: (HTTP Method) 24 | 25 | ## Usage 26 | 27 | `go get github.com/zerodha/fastglue-metrics` 28 | 29 | To start collecting metrics, simply initialise the metric exporter with: 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | fastgluemetrics "github.com/zerodha/fastglue-metrics" 36 | ) 37 | 38 | // Initialize fastglue. 39 | g := fastglue.NewGlue() 40 | // Initialise fastglue-metrics exporter. 41 | exporter := fastgluemetrics.NewMetrics(g, fastgluemetrics.Opts{ 42 | ExposeGoMetrics: true, 43 | NormalizeHTTPStatus: false, 44 | ServiceName: "dummy", 45 | MatchedRoutePathParam: g.MatchedRoutePathParam, 46 | }) 47 | // Expose the registered metrics at `/metrics` path. 48 | g.GET("/metrics", exporter.HandleMetrics) 49 | ``` 50 | 51 | ### Additional Options 52 | 53 | You can configure options to configure the behaviour of exporter using `fastgluemetrics.Opts`. 54 | To see a fully working example, you can check [examples/main](examples/main.go). 55 | 56 | ### Exporting Custom App Metrics 57 | 58 | In case your app needs to export custom app related metrics, you can modify the 59 | following example. 60 | 61 | ```go 62 | // StatsManager is a struct that will hold your custom stats. 63 | type StatsManager struct { 64 | Stats map[string]int64 65 | ServiceName string 66 | sync.RWMutex 67 | } 68 | 69 | // NewStats returns an instance of StatsManager. 70 | func NewStats(serviceName string) *StatsManager { 71 | if serviceName == "" { 72 | serviceName = "myapp" 73 | } 74 | 75 | return &StatsManager{ 76 | Stats: map[string]int64{}, 77 | ServiceName: serviceName, 78 | } 79 | } 80 | 81 | // PromFormatter writes the value in prometheus format with the service name. 82 | func (s *StatsManager) PromFormatter(b *bytes.Buffer, key string, val int64) { 83 | fmt.Fprintf(b, `%s{service="%s"} %d`, key, s.ServiceName, val) 84 | fmt.Fprintln(b) 85 | } 86 | 87 | // HandleMetrics returns a handler which exports stats. 88 | func (app *App) HandleMetrics(g *fastglue.Fastglue) fastglue.FastRequestHandler { 89 | // Initialize the fastglue exporter 90 | exporter := fastgluemetrics.NewMetrics(g, fastgluemetrics.Opts{ 91 | ExposeGoMetrics: true, 92 | NormalizeHTTPStatus: true, 93 | ServiceName: "veto", 94 | MatchedRoutePathParam: g.MatchedRoutePathParam, 95 | }) 96 | 97 | return func(r *fastglue.Request) error { 98 | app.Stats.RLock() 99 | defer app.Stats.RUnlock() 100 | 101 | buf := new(bytes.Buffer) 102 | 103 | // Write the metrics to the buffer 104 | exporter.Metrics.WritePrometheus(buf) 105 | metrics.WriteProcessMetrics(buf) 106 | 107 | for _, k := range sortedKeys(app.Stats.Stats) { 108 | // Format and write to the buffer 109 | app.Stats.PromFormatter(buf, fmt.Sprintf("count_%s", k), app.Stats.Stats[k]) 110 | } 111 | 112 | return r.SendBytes(200, "text/plain; version=0.0.4", buf.Bytes()) 113 | } 114 | } 115 | ``` 116 | 117 | ## Configuration 118 | 119 | `metrics.Options` takes in additional configurtion to customise the behaviour of exposition. 120 | 121 | - **ServiceName**: Unique identifier for the service name. 122 | 123 | - **NormalizeHTTPStatus**: If multiple status codes like `400`,`404`,`413` are present, setting this to `true` will make them group under their parent category i.e. `4xx`. 124 | 125 | - **ExposeGoMetrics**: Setting this to `true` would expose various `go_*` and `process_*` metrics. 126 | 127 | - **MatchedRoutePathParam**: If the value is set, the `path` variable in metric label will be the one used while registering the handler. If the value is unset, the original request path is used. 128 | 129 | The value is exposed by `fastglue` as `Fastglue.MatchedRoutePathParam`. 130 | 131 | ## Notes 132 | 133 | ### Label Cardinality 134 | 135 | If your application has dynamic endpoints, which make use of the [`Named Params` ](https://github.com/buaazp/fasthttprouter#named-parameters), you **must** set this value in order to keep label cardinality in check. If this value is not set, then a new metric will be created for each dynamic value of the named parameter, thus impacting the performance of downstream monitoring systems. 136 | 137 | For example, for a route `/orders/:userid/fetch`, you don't want a million timeseries labels to be created for each user.`fasthttprouter` would set the value of matched path in `ctx.UserValue` with a **key**. This setting is the value of that key, which is exposed in `fastglue` package with the variable name: `MatchedRoutePathParam`. 138 | 139 | ### Metrics Collection Library 140 | 141 | This package uses [VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) which is an extremely lightweight alternative to the official Prometheus [client library](https://github.com/prometheus/client_golang). The official library pulls a lot of external dependencies and has complex features which are not really needed for this use case. 142 | 143 | Besides being performant, `VM/metrics` has several improvements and optimisations on how a `Histogram` metric is constructed. For more details, you can read [this](https://medium.com/@valyala/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) blog post. 144 | 145 | ## LICENSE 146 | 147 | See [LICENSE](./LICENSE). 148 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build all run clean 2 | 3 | BIN:= fastglue-example.bin 4 | 5 | build: 6 | go build -o ${BIN} 7 | 8 | run: 9 | ./${BIN} 10 | 11 | clean: 12 | go clean 13 | - rm -f ${BIN} 14 | 15 | all: clean build run 16 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/valyala/fasthttp" 9 | "github.com/zerodha/fastglue" 10 | fastgluemetrics "github.com/zerodha/fastglue-metrics" 11 | ) 12 | 13 | var ( 14 | fakeResponse = make([]byte, 1024*1000*1) 15 | ) 16 | 17 | func main() { 18 | // Initialize fastglue. 19 | g := fastglue.NewGlue() 20 | // Initialise fastglue-metrics exporter. 21 | exporter := fastgluemetrics.NewMetrics(g, fastgluemetrics.Opts{ 22 | ExposeGoMetrics: true, 23 | NormalizeHTTPStatus: true, 24 | ServiceName: "dummy", 25 | MatchedRoutePathParam: g.MatchedRoutePathParam, 26 | }) 27 | // Register handlers. 28 | g.GET("/", func(r *fastglue.Request) error { 29 | return r.SendEnvelope("Welcome to dummy-app metrics. Visit /metrics.") 30 | }) 31 | g.GET("/fake", func(r *fastglue.Request) error { 32 | r.RequestCtx.Write(fakeResponse) 33 | return nil 34 | }) 35 | g.GET("/slow/{user}/ping", func(r *fastglue.Request) error { 36 | sleep := 0.5 + rand.Float64()*1.75 37 | time.Sleep(time.Duration(sleep) * 1000 * time.Millisecond) 38 | return r.SendEnvelope("Sleeping slow respo") 39 | }) 40 | g.GET("/bad/{user}", func(r *fastglue.Request) error { 41 | status := [9]int{300, 400, 413, 500, 417, 404, 402, 503, 502} 42 | return r.SendErrorEnvelope(status[rand.Intn(9)], "oops", nil, "") 43 | }) 44 | // Expose the registered metrics at `/metrics` path. 45 | g.GET("/metrics", exporter.HandleMetrics) 46 | // HTTP server. 47 | s := &fasthttp.Server{ 48 | Name: "metrics", 49 | ReadTimeout: time.Millisecond * 3000, 50 | WriteTimeout: time.Millisecond * 6000, 51 | MaxKeepaliveDuration: time.Millisecond * 5000, 52 | MaxRequestBodySize: 50000, 53 | ReadBufferSize: 50000, 54 | } 55 | log.Println("starting server on :6090") 56 | if err := g.ListenAndServe("0.0.0.0:6090", "", s); err != nil { 57 | log.Println("error starting server:", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerodha/fastglue-metrics 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/VictoriaMetrics/metrics v1.35.1 9 | github.com/valyala/fasthttp v1.58.0 10 | github.com/zerodha/fastglue v1.8.1 11 | ) 12 | 13 | require ( 14 | github.com/andybalholm/brotli v1.1.1 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/fasthttp/router v1.5.4 // indirect 17 | github.com/klauspost/compress v1.17.11 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect 20 | github.com/stretchr/testify v1.10.0 // indirect 21 | github.com/valyala/bytebufferpool v1.0.0 // indirect 22 | github.com/valyala/fastrand v1.1.0 // indirect 23 | github.com/valyala/histogram v1.2.0 // indirect 24 | golang.org/x/sys v0.29.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VictoriaMetrics/metrics v1.23.1 h1:/j8DzeJBxSpL2qSIdqnRFLvQQhbJyJbbEi22yMm7oL0= 2 | github.com/VictoriaMetrics/metrics v1.23.1/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= 3 | github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU= 4 | github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= 5 | github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 6 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 7 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 8 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 9 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y= 14 | github.com/fasthttp/router v1.4.16 h1:faWJ9OtaHvAtodreyQLps58M80YFNzphMJtOJzeESXs= 15 | github.com/fasthttp/router v1.4.16/go.mod h1:NFNlTCilbRVkeLc+E5JDkcxUdkpiJGKDL8Zy7Ey2JTI= 16 | github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8= 17 | github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc= 18 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 19 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 21 | github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 22 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 23 | github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 24 | github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= 25 | github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= 26 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 27 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= 31 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= 32 | github.com/savsgio/gotils v0.0.0-20230203094617-bcbc01813b4f h1:LKLq2MuL/uCGWS7BGo7yOeE/sj5wKxy2aQs69D8imsc= 33 | github.com/savsgio/gotils v0.0.0-20230203094617-bcbc01813b4f/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 34 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= 35 | github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 38 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 39 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 42 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 43 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 44 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 45 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 46 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 47 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 48 | github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= 49 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 50 | github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= 51 | github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= 52 | github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= 53 | github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= 54 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 55 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 56 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 57 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 58 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 59 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 60 | github.com/zerodha/fastglue v1.7.1 h1:YbKiSSEYzDmVDM29KCeXMHuh+48TcEHsgYCIGjoUcbU= 61 | github.com/zerodha/fastglue v1.7.1/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= 62 | github.com/zerodha/fastglue v1.8.1 h1:kzv7s6FZrHq1d8Trn9q6E51YbtGOnA1BrQ+iChklcMU= 63 | github.com/zerodha/fastglue v1.8.1/go.mod h1:OvW8RwNF6jOUgy+0itG0BzBvHPOzY0/8IOxGhEjh6dU= 64 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 65 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 67 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 68 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 69 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 70 | golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 71 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 79 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 80 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 81 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 90 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package fastgluemetrics 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/VictoriaMetrics/metrics" 10 | "github.com/zerodha/fastglue" 11 | ) 12 | 13 | const ( 14 | // Key to store the current time in `ctx.UserValue`. 15 | latencyKey = "latency_probe" 16 | ) 17 | 18 | // Opts represents configuration properties for metrics exposition. 19 | type Opts struct { 20 | // If multiple status codes like `400`,`404`,`413` are present, 21 | // setting this to `true` will make them group under their parent category i.e. `4xx`. 22 | NormalizeHTTPStatus bool 23 | // Setting this to `true` would expose various `go_*` and `process_*` metrics. 24 | ExposeGoMetrics bool 25 | // If the value is set, the `path` variable in metric label will be the one used while registering the handler. 26 | // If unset, the original request path is used. 27 | MatchedRoutePathParam string 28 | // Unique identifier for the service name. 29 | ServiceName string 30 | } 31 | 32 | // FastGlueMetrics represents the metrics instance. 33 | type FastGlueMetrics struct { 34 | Opts *Opts 35 | Metrics *metrics.Set 36 | } 37 | 38 | // NewMetrics initializes a new FastGlueMetrics instance with sane defaults. 39 | func NewMetrics(g *fastglue.Fastglue, opts Opts) *FastGlueMetrics { 40 | return initMetrics(g, opts, metrics.NewSet()) 41 | } 42 | 43 | func NewMetricsWithCustom(g *fastglue.Fastglue, opts Opts, m *metrics.Set) *FastGlueMetrics { 44 | return initMetrics(g, opts, m) 45 | } 46 | 47 | func initMetrics(g *fastglue.Fastglue, opts Opts, metrics *metrics.Set) *FastGlueMetrics { 48 | m := &FastGlueMetrics{ 49 | Opts: &Opts{ 50 | ServiceName: "default", 51 | NormalizeHTTPStatus: true, 52 | ExposeGoMetrics: false, 53 | MatchedRoutePathParam: g.MatchedRoutePathParam, 54 | }, 55 | Metrics: metrics, 56 | } 57 | if opts != (Opts{}) { 58 | m.Opts = &opts 59 | } 60 | 61 | // Register middlewares. 62 | g.Before(m.before) 63 | g.After(m.after) 64 | 65 | return m 66 | } 67 | 68 | // HandleMetrics returns the metric data response. 69 | func (m *FastGlueMetrics) HandleMetrics(r *fastglue.Request) error { 70 | buf := new(bytes.Buffer) 71 | m.Metrics.WritePrometheus(buf) 72 | 73 | if m.Opts.ExposeGoMetrics { 74 | metrics.WriteProcessMetrics(buf) 75 | } 76 | 77 | return r.SendBytes(http.StatusOK, "text/plain; version=0.0.4", buf.Bytes()) 78 | } 79 | 80 | func (m *FastGlueMetrics) before(r *fastglue.Request) *fastglue.Request { 81 | r.RequestCtx.SetUserValue(latencyKey, time.Now()) 82 | return r 83 | } 84 | 85 | func (m *FastGlueMetrics) after(r *fastglue.Request) *fastglue.Request { 86 | var ( 87 | path string 88 | status = strconv.Itoa(r.RequestCtx.Response.StatusCode()) 89 | start = r.RequestCtx.UserValue(latencyKey).(time.Time) 90 | method = string(r.RequestCtx.Method()) 91 | size = float64(len(r.RequestCtx.Response.Body())) 92 | ) 93 | // MatchedRoutePathParam stores the actual path before string interpolation by the router. 94 | // This is useful if you want to prevent high cardinality in labels. 95 | // For example, for a path `/orders/:userid/get` the number of metric series would be directly proportional 96 | // to all the unique `userid` hitting that endpoint. In order to prevent such high label cardinality, the raw 97 | // path string which is set to register the handler, is used for the metric label `path`. 98 | if m.Opts.MatchedRoutePathParam != "" { 99 | path = r.RequestCtx.UserValue(m.Opts.MatchedRoutePathParam).(string) 100 | } else { 101 | path = string(r.RequestCtx.URI().Path()) 102 | } 103 | 104 | // NormalizeHTTPStatus groups arbitrary status codes by their cateogry. 105 | // For example 400,417,413 will be grouped as 4xx. 106 | if m.Opts.NormalizeHTTPStatus { 107 | status = string(status[0]) + "xx" 108 | } 109 | 110 | // Write the metrics. 111 | m.Metrics.GetOrCreateCounter(`requests_total{service="` + m.Opts.ServiceName + 112 | `", status="` + status + `", method="` + method + `", path="` + path + `"}`).Inc() 113 | 114 | m.Metrics.GetOrCreateHistogram(`request_duration_seconds{service="` + m.Opts.ServiceName + 115 | `", status="` + status + `", method="` + method + `", path="` + path + `"}`).UpdateDuration(start) 116 | 117 | m.Metrics.GetOrCreateHistogram(`response_size_bytes{service="` + m.Opts.ServiceName + 118 | `", status="` + status + `", method="` + method + `", path="` + path + `"}`).Update(size) 119 | 120 | return r 121 | } 122 | -------------------------------------------------------------------------------- /screenshots/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerodha/fastglue-metrics/2d343ec8e8a8907703a1ab30c8983c0197c104a2/screenshots/grafana.png --------------------------------------------------------------------------------