├── LICENSE ├── README.md ├── _examples └── basic │ └── main.go ├── config.go ├── go.mod ├── go.sum ├── http.go ├── ip_utils.go └── telemetry.go /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | telemetry 2 | ========= 3 | 4 | Go package to simplify recording metrics in a Prometheus compatible format. 5 | Its based on github.com/uber-go/tally/v4/prometheus. 6 | 7 | Checkout the [example](_examples/basic/main.go) for more details. 8 | -------------------------------------------------------------------------------- /_examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/go-chi/chi/v5" 15 | "github.com/go-chi/chi/v5/middleware" 16 | "github.com/go-chi/telemetry" 17 | ) 18 | 19 | var ( 20 | AppMetrics = &MyAppMetrics{telemetry.NewScope("app")} 21 | ) 22 | 23 | type MyAppMetrics struct { 24 | *telemetry.Scope 25 | } 26 | 27 | func (m *MyAppMetrics) RecordMyAppHit() { 28 | m.RecordHit("my_app_hit") 29 | } 30 | 31 | func (m *MyAppMetrics) RecordAppGauge(value float64) { 32 | m.RecordGauge("my_app_gauge", value) 33 | } 34 | 35 | func main() { 36 | r := chi.NewRouter() 37 | 38 | r.Use(middleware.Logger) 39 | 40 | // telemetry.Collector middleware mounts /metrics endpoint 41 | // with prometheus metrics collector. 42 | r.Use(telemetry.Collector(telemetry.Config{ 43 | AllowAny: true, 44 | }, []string{"/api"})) // path prefix filters records generic http request metrics 45 | 46 | r.Route("/api", func(r chi.Router) { 47 | r.Get("/hello", func(w http.ResponseWriter, r *http.Request) { 48 | w.Write([]byte("Hello World!")) 49 | }) 50 | r.Get("/hit", func(w http.ResponseWriter, r *http.Request) { 51 | // record a hit 52 | AppMetrics.RecordMyAppHit() 53 | w.Write([]byte("Hit recorded!")) 54 | }) 55 | r.Get("/gauge/{value}", func(w http.ResponseWriter, r *http.Request) { 56 | value := chi.URLParam(r, "value") 57 | floatValue, err := strconv.ParseFloat(value, 64) 58 | if err != nil { 59 | w.Write([]byte("Invalid value")) 60 | return 61 | } 62 | // record a gauge 63 | AppMetrics.RecordAppGauge(floatValue) 64 | w.Write([]byte("Gauge recorded!")) 65 | }) 66 | r.Get("/compute", func(w http.ResponseWriter, r *http.Request) { 67 | span := AppMetrics.RecordSpan("compute") 68 | defer span.Stop() 69 | 70 | // do random work for random tie,, 71 | time.Sleep(time.Duration(rand.Intn(5)) * time.Second) 72 | 73 | w.Write([]byte("Span recorded!")) 74 | }) 75 | }) 76 | 77 | sig := make(chan os.Signal, 1) 78 | signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 79 | 80 | srvAddr := "localhost:3000" 81 | srv := &http.Server{ 82 | Addr: srvAddr, 83 | Handler: r, 84 | } 85 | 86 | go func() { 87 | <-sig 88 | 89 | slog.Info("server is shutting down") 90 | 91 | err := srv.Shutdown(context.Background()) 92 | if err != nil { 93 | panic(err) 94 | } 95 | }() 96 | 97 | slog.Info("server is running on", "address", srvAddr) 98 | 99 | err := srv.ListenAndServe() 100 | if err != nil && err != http.ErrServerClosed { 101 | panic(err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | // Config represents settings for the telemetry tool. 4 | type Config struct { 5 | // If any of these values are not provided, measurements won't be exposed. 6 | Username string `toml:"username"` 7 | Password string `toml:"password"` 8 | 9 | // Allow any traffic. Ie. if username/password are not specified, but AllowAny 10 | // is true, then the metrics endpoint will be available. 11 | AllowAny bool `toml:"allow_any"` 12 | 13 | // Allow internal private subnet traffic 14 | AllowInternal bool `toml:"allow_internal"` 15 | } 16 | 17 | func (a Config) Creds() map[string]string { 18 | creds := map[string]string{} 19 | if a.Username != "" && a.Password != "" { 20 | creds[a.Username] = a.Password 21 | } 22 | return creds 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/telemetry 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.12 7 | github.com/uber-go/tally/v4 v4.1.16 8 | ) 9 | 10 | require ( 11 | github.com/beorn7/perks v1.0.1 // indirect 12 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 13 | github.com/golang/mock v1.6.0 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | github.com/prometheus/client_golang v1.19.0 // indirect 16 | github.com/prometheus/client_model v0.6.1 // indirect 17 | github.com/prometheus/common v0.52.3 // indirect 18 | github.com/prometheus/procfs v0.13.0 // indirect 19 | github.com/twmb/murmur3 v1.1.8 // indirect 20 | go.uber.org/atomic v1.11.0 // indirect 21 | golang.org/x/sys v0.19.0 // indirect 22 | google.golang.org/protobuf v1.33.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/cactus/go-statsd-client/v5 v5.0.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= 12 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 14 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 19 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 20 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 21 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 22 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 23 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 24 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 25 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 26 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 27 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 28 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 29 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 30 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 34 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 35 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 36 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 37 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 38 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 39 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 40 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 46 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 48 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 49 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 50 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 51 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 52 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 53 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 54 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 55 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 56 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 57 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 59 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 60 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 64 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 65 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 66 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 67 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 69 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 70 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 74 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 75 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 76 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 77 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 78 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 79 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 80 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 81 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 82 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 83 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 84 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 85 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 86 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 87 | github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= 88 | github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= 89 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 90 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 91 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 92 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 93 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= 94 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= 95 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 96 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 97 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 101 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 102 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 103 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 104 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 105 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 106 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 107 | github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 108 | github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= 109 | github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 110 | github.com/uber-go/tally/v4 v4.1.16 h1:by2hveWRh/cUReButk6ns1sHK/hiKry7BuOV6iY16XI= 111 | github.com/uber-go/tally/v4 v4.1.16/go.mod h1:RW5DgqsyEPs0lA4b0YNf4zKj7DveKHd73hnO6zVlyW0= 112 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 113 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 114 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 115 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 116 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 117 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 118 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 121 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 122 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 123 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 124 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 125 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 127 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 128 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 129 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 130 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 131 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 132 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 153 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 154 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 155 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 156 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 157 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 158 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 159 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 160 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 161 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 166 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 167 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 168 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 169 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 170 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 171 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 172 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 173 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 174 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 175 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 180 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 181 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 185 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 186 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 187 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 188 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 189 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "time" 9 | "unicode/utf8" 10 | 11 | "github.com/go-chi/chi/v5" 12 | "github.com/go-chi/chi/v5/middleware" 13 | ) 14 | 15 | // Collector creates a handler that exposes a /metrics endpoint. Passing 16 | // an array of strings to pathPrefixFilters will help reduce the noise on the service 17 | // from random Internet traffic; that is, only the path prefixes will be measured. 18 | func Collector(cfg Config, optPathPrefixFilters ...[]string) func(next http.Handler) http.Handler { 19 | if (!cfg.AllowAny && !cfg.AllowInternal) && (cfg.Username == "" || cfg.Password == "") { 20 | return func(next http.Handler) http.Handler { 21 | return next 22 | } 23 | } 24 | 25 | httpMetrics := NewScope("http") 26 | 27 | pathPrefixFilters := []string{} 28 | if len(optPathPrefixFilters) > 0 { 29 | pathPrefixFilters = append(pathPrefixFilters, optPathPrefixFilters[0]...) 30 | } 31 | 32 | authHandler := middleware.BasicAuth( 33 | "metrics", 34 | map[string]string{cfg.Username: cfg.Password}, 35 | ) 36 | 37 | metricsReporterHandler := chi.Chain( 38 | func(next http.Handler) http.Handler { 39 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | 41 | // Maybe allow internal traffic 42 | if cfg.AllowInternal { 43 | ipAddress := net.ParseIP(getIPAddress(r)) 44 | if isPrivateSubnet(ipAddress) { 45 | next.ServeHTTP(w, r) 46 | return 47 | } 48 | } 49 | 50 | // Maybw allow basic auth traffic 51 | if cfg.Username != "" && cfg.Password != "" { 52 | authHandler(next).ServeHTTP(w, r) 53 | return 54 | } 55 | 56 | // Maybe allow any 57 | if cfg.AllowAny { 58 | next.ServeHTTP(w, r) 59 | return 60 | } 61 | 62 | w.WriteHeader(http.StatusNotFound) 63 | }) 64 | }, 65 | func(http.Handler) http.Handler { 66 | // reporter is the global prometheus reporter 67 | return reporter.HTTPHandler() 68 | }, 69 | ) 70 | 71 | return func(next http.Handler) http.Handler { 72 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | // serve prometheus metrics reporter page 74 | if r.Method == "GET" && strings.EqualFold(r.URL.Path, "/metrics") { 75 | metricsReporterHandler.Handler(next).ServeHTTP(w, r) 76 | return 77 | } 78 | 79 | // track request mtrics 80 | // 81 | // first, check path filters as we skip collecting metrics for these paths 82 | if len(pathPrefixFilters) > 0 { 83 | found := false 84 | for _, p := range pathPrefixFilters { 85 | if strings.HasPrefix(r.URL.Path, p) { 86 | found = true 87 | break 88 | } 89 | } 90 | if !found && r.URL.Path != "/" { 91 | // skip measurement of the http request 92 | next.ServeHTTP(w, r) 93 | return 94 | } 95 | } 96 | 97 | // measure request 98 | ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) 99 | defer httpResponseSample(httpMetrics, time.Now().UTC(), r, ww) 100 | 101 | next.ServeHTTP(ww, r) 102 | }) 103 | } 104 | } 105 | 106 | func httpResponseSample(httpMetrics *Scope, start time.Time, r *http.Request, ww middleware.WrapResponseWriter) { 107 | status := ww.Status() 108 | if status == 0 { // TODO: see why we have status = 0 under test conditions (this came up during benchmarks for some of the requests) 109 | status = http.StatusOK 110 | } 111 | 112 | // prometheus errors if string is not uft8 encoded 113 | if !utf8.ValidString(r.URL.Path) { 114 | return 115 | } 116 | 117 | metricsKey := fmt.Sprintf("%d %s %s", status, r.Method, r.URL.Path) 118 | 119 | requestMetrics, _ := httpMetrics.GetTaggedScope(metricsKey) 120 | if requestMetrics == nil { 121 | requestMetrics = httpMetrics.SetTaggedScope(metricsKey, map[string]string{ 122 | "endpoint": fmt.Sprintf("%s %s", r.Method, r.URL.Path), 123 | "status": fmt.Sprintf("%d", status), 124 | }) 125 | } 126 | 127 | requestMetrics.RecordDuration("request", start, time.Now().UTC()) 128 | requestMetrics.RecordHit("requests") 129 | } 130 | -------------------------------------------------------------------------------- /ip_utils.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // getIPAddress gets the real ip address from headers 11 | // if not found it returns r.RemoteAddr 12 | func getIPAddress(r *http.Request) string { 13 | ipHeaders := []string{ 14 | "True-Client-IP", 15 | "X-Forwarded-For", 16 | "X-Real-Ip", 17 | } 18 | for _, h := range ipHeaders { 19 | addresses := strings.Split(r.Header.Get(h), ",") 20 | // march from right to left 21 | for i := len(addresses) - 1; i >= 0; i-- { 22 | // header can contain spaces too, strip those out. 23 | ip := strings.TrimSpace(addresses[i]) 24 | if ip == "" { 25 | continue 26 | } 27 | return ip 28 | } 29 | } 30 | return ipRemoteAddr(r.RemoteAddr) 31 | } 32 | 33 | // isPrivateSubnet - check to see if this ip is in a private subnet 34 | func isPrivateSubnet(ipAddress net.IP) bool { 35 | if ipCheck := ipAddress.To4(); ipCheck != nil { 36 | // iterate over all our ranges 37 | for _, r := range privateRanges { 38 | // check if this ip is in a private range 39 | if inRange(r, ipAddress) { 40 | return true 41 | } 42 | } 43 | } 44 | // TODO: implement ipv6 ranges 45 | return false 46 | } 47 | 48 | func ipRemoteAddr(remoteAddr string) string { 49 | ip, _, err := net.SplitHostPort(remoteAddr) 50 | if err != nil { 51 | return "" 52 | } 53 | return ip 54 | } 55 | 56 | // ipRange - a structure that holds the start and end of a range of ip addresses 57 | type ipRange struct { 58 | start net.IP 59 | end net.IP 60 | } 61 | 62 | // inRange - check to see if a given ip address is within a range given 63 | func inRange(r ipRange, ipAddress net.IP) bool { 64 | // strcmp type byte comparison 65 | if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 { 66 | return true 67 | } 68 | return false 69 | } 70 | 71 | // refer https://datatracker.ietf.org/doc/html/rfc1918#section-3 72 | var privateRanges = []ipRange{ 73 | { 74 | start: net.ParseIP("10.0.0.0"), 75 | end: net.ParseIP("10.255.255.255"), 76 | }, 77 | { 78 | start: net.ParseIP("100.64.0.0"), 79 | end: net.ParseIP("100.127.255.255"), 80 | }, 81 | { 82 | start: net.ParseIP("172.16.0.0"), 83 | end: net.ParseIP("172.31.255.255"), 84 | }, 85 | { 86 | start: net.ParseIP("192.0.0.0"), 87 | end: net.ParseIP("192.0.0.255"), 88 | }, 89 | { 90 | start: net.ParseIP("192.168.0.0"), 91 | end: net.ParseIP("192.168.255.255"), 92 | }, 93 | { 94 | start: net.ParseIP("198.18.0.0"), 95 | end: net.ParseIP("198.19.255.255"), 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/uber-go/tally/v4" 11 | "github.com/uber-go/tally/v4/prometheus" 12 | ) 13 | 14 | // reporter is the global prometheus reporter, which will also register 15 | // the metrics with the prometheus registry. The Collector middleware will 16 | // use this reporter to expose the metrics on the /metrics endpoint. 17 | var reporter = prometheus.NewReporter(prometheus.Options{}) 18 | 19 | var defaultBucketsForIntegerValues = tally.ValueBuckets{ 20 | 1, 21 | 2, 22 | 5, 23 | 7, 24 | 9, 25 | 10, 26 | 50, 27 | 100, 28 | } 29 | 30 | var defaultBucketFactorsForDurations = []float64{ 31 | 0.001, 32 | 0.005, 33 | 0.01, 34 | 0.05, 35 | 0.1, 36 | 0.25, 37 | 0.5, 38 | 0.75, 39 | 0.9, 40 | 0.95, 41 | 0.99, 42 | 1, 43 | 2.5, 44 | 5, 45 | 10, 46 | 25, 47 | 50, 48 | 100, 49 | } 50 | 51 | // Scope represents the measurements scope for an application. 52 | type Scope struct { 53 | scope tally.Scope 54 | closer io.Closer 55 | cache scopeCache 56 | } 57 | 58 | // NewScope creates a scope for an application. Receives a scope 59 | // argument (a single-word) that is used as a prefix for all measurements. 60 | // Optionally you can pass a map of tags to be used for all measurements in 61 | // the scope. 62 | func NewScope(scope string, optTags ...map[string]string) *Scope { 63 | var tags map[string]string = nil 64 | if len(optTags) > 0 { 65 | tags = optTags[0] 66 | } 67 | 68 | s, closer := newRootScope(tally.ScopeOptions{ 69 | Prefix: scope, 70 | Tags: tags, 71 | }, 1*time.Second) 72 | 73 | return &Scope{ 74 | scope: s, 75 | closer: closer, 76 | cache: newScopeCache(), 77 | } 78 | } 79 | 80 | // WithTaggedMap returns a new scope with the given tags. However, if this is a 81 | // hot-path (and telemetry usually is), we recommend using GetTaggedScope 82 | // and SetTaggedScope to avoid the overhead of creating a new scope. 83 | func (n *Scope) WithTaggedMap(tags map[string]string) *Scope { 84 | return &Scope{ 85 | scope: n.scope.Tagged(tags), 86 | closer: n.closer, 87 | cache: newScopeCache(), 88 | } 89 | } 90 | 91 | // Tagged returns a tagged scope for the tag name and value pair. It's very 92 | // efficient as it will return a cached scope if it exists for the tagged 93 | // scope, or it will create a new scope and cache it based on tag name + value. 94 | // If either tagName or tagValue is empty, it will return the current scope. 95 | func (n *Scope) Tagged(tagName, tagValue string) *Scope { 96 | if tagName == "" || tagValue == "" { 97 | return n 98 | } 99 | metricsKey := fmt.Sprintf("%s:%s", tagName, tagValue) 100 | scope, _ := n.GetTaggedScope(metricsKey) 101 | if scope == nil { 102 | scope = n.SetTaggedScope(metricsKey, map[string]string{tagName: tagValue}) 103 | } 104 | return scope 105 | } 106 | 107 | // GetTaggedScope returns a new scope with the given tags based on the 108 | // supplied key identifier. 109 | func (n *Scope) GetTaggedScope(key string) (*Scope, bool) { 110 | var scope *Scope 111 | var ok bool 112 | n.cache.taggedMu.RLock() 113 | scope, ok = n.cache.tagged[key] 114 | n.cache.taggedMu.RUnlock() 115 | return scope, ok 116 | } 117 | 118 | // SetTaggedScope returns a new scope with the given tags based on the 119 | // supplied key identifier. 120 | func (n *Scope) SetTaggedScope(key string, tags map[string]string) *Scope { 121 | s := &Scope{ 122 | scope: n.scope.Tagged(tags), 123 | closer: n.closer, 124 | cache: newScopeCache(), 125 | } 126 | n.cache.taggedMu.Lock() 127 | n.cache.tagged[key] = s 128 | n.cache.taggedMu.Unlock() 129 | return s 130 | } 131 | 132 | // Close closes the scope and stops reporting. 133 | func (n *Scope) Close() error { 134 | return n.closer.Close() 135 | } 136 | 137 | // RecordHit increases a hit counter. This is ideal for counting HTTP requests 138 | // or other events that are incremented by one each time. 139 | // 140 | // RecordHit adds the "_total" suffix to the name of the measurement. 141 | // 142 | // Measurement type: Counter 143 | func (n *Scope) RecordHit(measurement string) { 144 | n.RecordIncrementValue(measurement, 1) 145 | } 146 | 147 | // RecordIncrementValue increases a counter. 148 | // 149 | // RecordIncrementValue adds the "_total" suffix to the name of the measurement. 150 | // 151 | // Measurement type: Counter 152 | func (n *Scope) RecordIncrementValue(measurement string, value int64) { 153 | var scope tally.Counter 154 | var ok bool 155 | n.cache.counterMu.RLock() 156 | scope, ok = n.cache.counter[measurement] 157 | n.cache.counterMu.RUnlock() 158 | if !ok { 159 | n.cache.counterMu.Lock() 160 | scope = n.scope.Counter(SnakeCasef(measurement + "_total")) 161 | n.cache.counter[measurement] = scope 162 | n.cache.counterMu.Unlock() 163 | } 164 | scope.Inc(value) 165 | } 166 | 167 | // RecordGauge sets the value of a measurement that can go up or down over 168 | // time. 169 | // 170 | // RecordGauge measures a prometheus raw type and no suffix is added to the 171 | // measurement. 172 | // 173 | // Measurement type: Gauge 174 | func (n *Scope) RecordGauge(measurement string, value float64) { 175 | var scope tally.Gauge 176 | var ok bool 177 | n.cache.gaugeMu.RLock() 178 | scope, ok = n.cache.gauge[measurement] 179 | n.cache.gaugeMu.RUnlock() 180 | if !ok { 181 | n.cache.gaugeMu.Lock() 182 | scope = n.scope.Gauge(SnakeCasef(measurement)) 183 | n.cache.gauge[measurement] = scope 184 | n.cache.gaugeMu.Unlock() 185 | } 186 | scope.Update(value) 187 | } 188 | 189 | // RecordSize records a numeric unit-less value that can go up or down. Use it 190 | // when it's more important to know the last value of said size. This is useful 191 | // to measure things like the size of a queue. 192 | // 193 | // RecordSize adds the "_size" prefix to the name of the measurement. 194 | // 195 | // Measurement type: Gauge 196 | func (n *Scope) RecordSize(measurement string, value float64) { 197 | n.RecordGauge(measurement+"_size", value) 198 | } 199 | 200 | // RecordIntegerValue records a numeric unit-less value that can go up or down. 201 | // Use it when is important to see how the value evolved over time. 202 | // 203 | // RecordIntegerValue uses an histogram configured with buckets that priorize 204 | // values closer to zero. 205 | // 206 | // Measurement type: Histogram 207 | func (n *Scope) RecordIntegerValue(measurement string, value int) { 208 | n.RecordValueWithBuckets(measurement, float64(value), defaultBucketsForIntegerValues) 209 | } 210 | 211 | // RecordValue records a numeric unit-less value that can go up or down. Use it 212 | // when is important to see how the value evolved over time. 213 | // 214 | // RecordValue measures a prometheus raw type and no suffix is added to the measurement. 215 | // 216 | // Measurement type: Histogram 217 | func (n *Scope) RecordValue(measurement string, value float64) { 218 | n.RecordValueWithBuckets(measurement, value, nil) 219 | } 220 | 221 | // RecordValueWithBuckets records a numeric unit-less value that can go up or 222 | // down. Use it when is important to see how the value evolved over time. 223 | // 224 | // RecordValueWithBuckets adds the "_value" suffix to the name of the measurement. 225 | // 226 | // Measurement type: Histogram 227 | func (n *Scope) RecordValueWithBuckets(measurement string, value float64, buckets []float64) { 228 | var scope tally.Histogram 229 | var ok bool 230 | n.cache.histogramMu.RLock() 231 | scope, ok = n.cache.histogram[measurement] 232 | n.cache.histogramMu.RUnlock() 233 | if !ok { 234 | n.cache.histogramMu.Lock() 235 | scope = n.scope.Histogram(SnakeCasef(measurement+"_value"), tally.ValueBuckets(buckets)) 236 | n.cache.histogram[measurement] = scope 237 | n.cache.histogramMu.Unlock() 238 | } 239 | scope.RecordValue(value) 240 | } 241 | 242 | // RecordDuration records an elapsed time. Use it when is important to see how 243 | // values. Use it when is important to see how a value evolved over time, for 244 | // instance request durations, the time it takes for a task to finish, etc. 245 | // 246 | // RecordDuration adds the "_duration_seconds" prefix to the name of the 247 | // measurement. 248 | // 249 | // Measurement type: Histogram 250 | func (n *Scope) RecordDuration(measurement string, start time.Time, stop time.Time) { 251 | n.RecordDurationWithResolution(measurement, start, stop, 0) 252 | } 253 | 254 | // RecordDurationWithResolution records the elapsed duration between two time 255 | // values. Use it when is important to see how a value evolved over time, for 256 | // instance request durations, the time it takes for a task to finish, etc. 257 | // 258 | // The resolution parameter can be any value, this value will be taken as base 259 | // to build buckets. 260 | // 261 | // RecordDurationWithResolution adds the "_duration_seconds" prefix to the name 262 | // of the measurement. 263 | // 264 | // Measurement type: Histogram 265 | func (n *Scope) RecordDurationWithResolution(measurement string, timeA time.Time, timeB time.Time, resolution time.Duration) { 266 | if resolution <= 0 { 267 | resolution = time.Second 268 | } 269 | 270 | var buckets tally.Buckets 271 | var ok bool 272 | resKey := int64(resolution) 273 | 274 | n.cache.bucketsMu.RLock() 275 | buckets, ok = n.cache.buckets[resKey] 276 | n.cache.bucketsMu.RUnlock() 277 | 278 | if !ok { 279 | unit := float64(resolution) 280 | durations := make([]time.Duration, len(defaultBucketFactorsForDurations)) 281 | for i := range durations { 282 | durations[i] = time.Duration(int64(unit * defaultBucketFactorsForDurations[i])) 283 | } 284 | calculatedBuckets := tally.DurationBuckets(durations) 285 | 286 | n.cache.bucketsMu.Lock() 287 | buckets = calculatedBuckets 288 | n.cache.buckets[resKey] = buckets 289 | n.cache.bucketsMu.Unlock() 290 | } 291 | 292 | var scope tally.Histogram 293 | metricsKey := fmt.Sprintf("%s:%d", measurement, resKey) 294 | 295 | n.cache.histogramMu.RLock() 296 | scope, ok = n.cache.histogram[metricsKey] 297 | n.cache.histogramMu.RUnlock() 298 | if !ok { 299 | n.cache.histogramMu.Lock() 300 | scope = n.scope.Histogram(SnakeCasef(measurement+"_duration_seconds"), buckets) 301 | n.cache.histogram[metricsKey] = scope 302 | n.cache.histogramMu.Unlock() 303 | } 304 | 305 | elapsed := timeB.Sub(timeA) 306 | if elapsed < 0 { 307 | elapsed = elapsed * -1 308 | } 309 | scope.RecordDuration(elapsed) 310 | } 311 | 312 | // RecordSpan starts a stopwatch for a span. 313 | // 314 | // RecordSpan adds the "_span" suffix to the name of the measurement. 315 | // 316 | // Measurement type: Timer 317 | func (n *Scope) RecordSpan(measurement string) tally.Stopwatch { 318 | var scope tally.Timer 319 | var ok bool 320 | n.cache.timerMu.RLock() 321 | scope, ok = n.cache.timer[measurement] 322 | n.cache.timerMu.RUnlock() 323 | if !ok { 324 | n.cache.timerMu.Lock() 325 | scope = n.scope.Timer(SnakeCasef(measurement + "_span")) 326 | n.cache.timer[measurement] = scope 327 | n.cache.timerMu.Unlock() 328 | } 329 | return scope.Start() 330 | } 331 | 332 | func SnakeCasef(format string, args ...any) string { 333 | s := strings.ToLower(fmt.Sprintf(format, args...)) 334 | if strings.Contains(s, "-") { 335 | s = strings.ReplaceAll(s, "-", "_") 336 | } 337 | if strings.Contains(s, " ") { 338 | s = strings.ReplaceAll(s, " ", "_") 339 | } 340 | if strings.Contains(s, ".") { 341 | s = strings.ReplaceAll(s, ".", "_") 342 | } 343 | return s 344 | } 345 | 346 | func newRootScope(opts tally.ScopeOptions, interval time.Duration) (tally.Scope, io.Closer) { 347 | opts.CachedReporter = reporter 348 | opts.Separator = prometheus.DefaultSeparator 349 | opts.SanitizeOptions = nil // noop sanitizer 350 | opts.OmitCardinalityMetrics = true 351 | return tally.NewRootScope(opts, interval) 352 | } 353 | 354 | type scopeCache struct { 355 | tagged map[string]*Scope // map key is a unique identifier for the group of tags 356 | taggedMu sync.RWMutex 357 | counter map[string]tally.Counter 358 | counterMu sync.RWMutex 359 | gauge map[string]tally.Gauge 360 | gaugeMu sync.RWMutex 361 | timer map[string]tally.Timer 362 | timerMu sync.RWMutex 363 | histogram map[string]tally.Histogram 364 | histogramMu sync.RWMutex 365 | buckets map[int64]tally.Buckets 366 | bucketsMu sync.RWMutex 367 | } 368 | 369 | func newScopeCache() scopeCache { 370 | return scopeCache{ 371 | tagged: make(map[string]*Scope), 372 | counter: make(map[string]tally.Counter), 373 | gauge: make(map[string]tally.Gauge), 374 | timer: make(map[string]tally.Timer), 375 | histogram: make(map[string]tally.Histogram), 376 | buckets: make(map[int64]tally.Buckets), 377 | } 378 | } 379 | --------------------------------------------------------------------------------