├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── delta ├── delta.go └── delta_test.go ├── docs └── kubernetes │ ├── README.md │ ├── deployment.yaml │ └── service.yaml ├── go.mod ├── go.sum ├── http.go ├── http_test.go ├── main.go ├── metrics └── metrics.go ├── release.sh ├── telemetry.go ├── udp.go └── udp_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | docs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | *.deb 3 | *.swp 4 | /bin 5 | /lib/*.a 6 | /lib/*.o 7 | /pkg 8 | /vendor 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.11" 5 | - "1.12" 6 | 7 | before_install: 8 | - GO111MODULE=on go mod vendor 9 | 10 | script: go test ./... 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine as build-env 2 | 3 | RUN apk add git 4 | RUN mkdir /pushprom 5 | WORKDIR /pushprom 6 | COPY go.mod . 7 | COPY go.sum . 8 | 9 | # Get dependencies - will also be cached if we won't change mod/sum 10 | RUN go mod download 11 | # COPY the source code as the last step 12 | COPY . . 13 | 14 | # Build the binary 15 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags "-s -w" -o /go/bin/pushprom . 16 | 17 | # <- Second step to build minimal image 18 | FROM scratch 19 | COPY --from=build-env /go/bin/pushprom /go/bin/pushprom 20 | ENTRYPOINT ["/go/bin/pushprom"] 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, MessageBird 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY = pushprom 2 | PROJECT = pushprom 3 | 4 | VERSION = $(shell git rev-list HEAD | wc -l |tr -d ' ') 5 | HASH = $(shell git rev-parse --short HEAD) 6 | 7 | GO = go 8 | 9 | all: 10 | @echo "make container # Create a Docker container for $(PROJECT)" 11 | @echo 12 | @echo "make test # Run the test suite" 13 | 14 | 15 | release_linux: 16 | go mod download 17 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o "bin/pushprom-$(VERSION).linux-amd64/pushprom" github.com/messagebird/pushprom 18 | mv bin/pushprom-$(VERSION).linux-amd64/pushprom bin/ 19 | rm -rf bin/pushprom-$(VERSION).linux-amd64/ 20 | file bin/pushprom 21 | 22 | native: 23 | go clean 24 | go mod download 25 | CGO_ENABLED=0 go build -ldflags "-s -w" -o "bin/pushprom-native" github.com/messagebird/pushprom 26 | file bin/pushprom-native 27 | 28 | container: 29 | echo "* Creating $(PROJECT) Docker container" 30 | DOCKER_BUILDKIT=1 docker build -t $(PROJECT):$(VERSION) . 31 | docker tag $(PROJECT):$(VERSION) $(PROJECT):latest 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Pushprom 3 | 4 | [![Build Status](https://travis-ci.org/messagebird/pushprom.svg?branch=master)](https://travis-ci.org/messagebird/pushprom) 5 | 6 | Pushprom is a proxy (HTTP/UDP) to the [Prometheus](https://prometheus.io/) Go client. 7 | 8 | Prometheus doesn't offer a PHP client and PHP clients are hard to implement because they would need to keep track of state and PHP setups generally don't encourage that. That's why we built Pushprom. 9 | 10 | ## Dependencies 11 | 12 | - [make](https://www.gnu.org/software/make/) 13 | - [golang](https://go.dev/) 14 | - [docker](https://www.docker.com/) 15 | - [github-release](https://cli.github.com/manual/gh_release_create) 16 | 17 | ## Installing 18 | 19 | Execute the following command: 20 | 21 | ```bash 22 | go get -u github.com/messagebird/pushprom 23 | ``` 24 | 25 | To build amd64/linux binary: 26 | ```bash 27 | make release_linux 28 | ``` 29 | 30 | To build os & platform dependent native binary: 31 | ```bash 32 | make native 33 | ``` 34 | 35 | To build using Docker container: 36 | 37 | ```bash 38 | make container 39 | ``` 40 | 41 | ## Release 42 | 43 | ```bash 44 | VERSION=1.0.9 ./release.sh 45 | ``` 46 | 47 | ## Usage 48 | 49 | Running Pushprom is as easy as executing `pushprom` on the command line. 50 | 51 | ``` 52 | $ pushprom 53 | 2016/08/25 10:43:32 http.go:36: exposing metrics on http://0.0.0.0:9091/metrics 54 | 2016/08/25 10:43:32 udp.go:10: listening for stats UDP on port :9090 55 | 2016/08/25 10:43:32 http.go:39: listening for stats on http://0.0.0.0:9091 56 | ``` 57 | 58 | Use the `-h` flag to get help information. 59 | 60 | ``` 61 | $ pushprom -h 62 | Usage of bin/pushprom: 63 | -http-listen-address string 64 | The address to listen on for http stat and telemetry requests. (default ":9091") 65 | -udp-listen-address string 66 | The address to listen on for udp stats requests. (default ":9090") 67 | ``` 68 | 69 | Pushprom accepts HTTP and UDP requests. The payloads are in JSON. Here is a full example: 70 | 71 | ```json 72 | { 73 | "type": "gauge", 74 | "name": "trees", 75 | "help": "the amount of trees in the forest.", 76 | "method": "add", 77 | "value": 3002, 78 | "labels": { 79 | "species": "araucaria angustifolia", 80 | "job": "tree-counter-bot" 81 | } 82 | } 83 | ``` 84 | 85 | When Pushprom receives this payload (from now on called Delta) it tries to register the metric with type **Gauge** named **trees** and then apply the operation **add** with value **3002** on it. 86 | 87 | ## Protocol support 88 | 89 | You can use HTTP requests and UDP packages to push deltas to Pushprom. 90 | 91 | ### HTTP 92 | 93 | When using HTTP you should do a `POST /`. 94 | 95 | Example: 96 | 97 | ```bash 98 | curl -H "Content-type: application/json" -X POST -d '{"type": "counter", "name": "gophers", "help": "little burrowing rodents", "method": "inc"}' http://127.0.0.1:9091/ 99 | ``` 100 | 101 | ### UDP 102 | 103 | *You move fast and break things.* 104 | 105 | Example: 106 | 107 | ```bash 108 | echo "{\"type\": \"counter\", \"name\": \"gophers\", \"help\": \"little burrowing rodents\", \"method\": \"inc\"}" | nc -u -w1 127.0.0.1 9090 109 | ``` 110 | 111 | ## Caveats 112 | 113 | In the Prometheus Go client you can not register a metric with the same **name** and different **help** or **labels**. For example: you register a metric with name `gophers` and with help `little rodents` and a little later you think "but they are also burrowing animals!". When you change the help string and push the same metric it won't work: you need to reboot Pushprom. 114 | 115 | ## Clients 116 | 117 | We currently offer two flavors of PHP clients for Pushprom: 118 | * [PHP](https://github.com/messagebird/pushprom-php-client) 119 | * [Yii 2](https://github.com/messagebird/pushprom-yii2-client) 120 | 121 | ## Alternatives 122 | 123 | ### Pushgateway 124 | 125 | [Pushgateway](https://github.com/prometheus/pushgateway) is a metrics cache for Prometheus. It's explicitly not an aggregator, which is the most distinct difference with Pushprom. 126 | 127 | # Tests 128 | 129 | ```bash 130 | go test ./... 131 | ``` 132 | 133 | ## License 134 | 135 | Pushprom is licensed under [The BSD 2-Clause License](http://opensource.org/licenses/BSD-2-Clause). Copyright (c) 2022, MessageBird 136 | -------------------------------------------------------------------------------- /delta/delta.go: -------------------------------------------------------------------------------- 1 | package delta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | // MetricType is a string that uniquelly identifies a Prometheus metric 12 | type MetricType string 13 | 14 | const ( 15 | // COUNTER is a Prometheus metric type that only goes up 16 | COUNTER MetricType = "counter" 17 | // GAUGE is a Prometheus metric type that goes up or down 18 | GAUGE MetricType = "gauge" 19 | // HISTOGRAM is a Prometheus metric type 20 | HISTOGRAM MetricType = "histogram" 21 | // SUMMARY is a Prometheus metric type 22 | SUMMARY MetricType = "summary" 23 | ) 24 | 25 | // Delta defines the pushprom message format. It represent a change on a Prometheus metric. It implements a simplistic rpc. 26 | type Delta struct { 27 | Type MetricType `json:"type"` 28 | Name string `json:"name"` 29 | Help string `json:"help"` 30 | Method string `json:"method"` 31 | Value float64 `json:"value"` 32 | Labels prometheus.Labels `json:"labels"` 33 | Buckets []float64 `json:"buckets"` 34 | } 35 | 36 | // NewDelta creates a new Delta from the json contents of the io.Reader 37 | func NewDelta(reader io.Reader) (*Delta, error) { 38 | dec := json.NewDecoder(reader) 39 | delta := Delta{} 40 | if err := dec.Decode(&delta); err != nil { 41 | return nil, err 42 | } 43 | return &delta, nil 44 | } 45 | 46 | // LabelNames returns the names(keys) of all labels 47 | func (delta Delta) LabelNames() []string { 48 | names := []string{} 49 | for k := range delta.Labels { 50 | names = append(names, k) 51 | } 52 | return names 53 | } 54 | 55 | func (delta Delta) applyOnCounter() error { 56 | var metric prometheus.Counter 57 | opts := prometheus.CounterOpts{ 58 | Name: delta.Name, 59 | Help: delta.Help, 60 | } 61 | if len(delta.Labels) > 0 { 62 | vec := prometheus.NewCounterVec(opts, delta.LabelNames()) 63 | err := prometheus.Register(vec) 64 | if err != nil { 65 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 66 | vec = are.ExistingCollector.(*prometheus.CounterVec) 67 | } else { 68 | return err 69 | } 70 | } 71 | 72 | metric = vec.With(delta.Labels) 73 | } else { 74 | metric = prometheus.NewCounter(opts) 75 | err := prometheus.Register(metric) 76 | if err != nil { 77 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 78 | metric = are.ExistingCollector.(prometheus.Counter) 79 | } else { 80 | return err 81 | } 82 | } 83 | } 84 | 85 | switch delta.Method { 86 | case "inc": 87 | metric.Inc() 88 | case "add": 89 | metric.Add(delta.Value) 90 | default: 91 | return fmt.Errorf("method %s is not implemented yet", delta.Method) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (delta Delta) applyOnGauge() error { 98 | var metric prometheus.Gauge 99 | opts := prometheus.GaugeOpts{ 100 | Name: delta.Name, 101 | Help: delta.Help, 102 | } 103 | if len(delta.Labels) > 0 { 104 | vec := prometheus.NewGaugeVec(opts, delta.LabelNames()) 105 | err := prometheus.Register(vec) 106 | if err != nil { 107 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 108 | vec = are.ExistingCollector.(*prometheus.GaugeVec) 109 | } else { 110 | return err 111 | } 112 | } 113 | 114 | metric = vec.With(delta.Labels) 115 | } else { 116 | metric = prometheus.NewGauge(opts) 117 | err := prometheus.Register(metric) 118 | if err != nil { 119 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 120 | metric = are.ExistingCollector.(prometheus.Gauge) 121 | } else { 122 | return err 123 | } 124 | } 125 | } 126 | 127 | switch delta.Method { 128 | case "set": 129 | metric.Set(delta.Value) 130 | case "inc": 131 | metric.Inc() 132 | case "dec": 133 | metric.Dec() 134 | case "add": 135 | metric.Add(delta.Value) 136 | case "sub": 137 | metric.Sub(delta.Value) 138 | default: 139 | return fmt.Errorf("method %s is not implemented yet", delta.Method) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (delta Delta) applyOnHistogram() error { 146 | var metric prometheus.Histogram 147 | opts := prometheus.HistogramOpts{ 148 | Name: delta.Name, 149 | Help: delta.Help, 150 | Buckets: delta.Buckets, 151 | } 152 | 153 | if len(delta.Labels) > 0 { 154 | vec := prometheus.NewHistogramVec(opts, delta.LabelNames()) 155 | err := prometheus.Register(vec) 156 | if err != nil { 157 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 158 | vec = are.ExistingCollector.(*prometheus.HistogramVec) 159 | } else { 160 | return err 161 | } 162 | } 163 | 164 | metric = vec.With(delta.Labels).(prometheus.Histogram) 165 | } else { 166 | metric = prometheus.NewHistogram(opts) 167 | err := prometheus.Register(metric) 168 | if err != nil { 169 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 170 | metric = are.ExistingCollector.(prometheus.Histogram) 171 | } else { 172 | return err 173 | } 174 | } 175 | } 176 | 177 | if delta.Method == "observe" { 178 | metric.Observe(delta.Value) 179 | } else { 180 | return fmt.Errorf("method %s is not implemented yet", delta.Method) 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func (delta Delta) applyOnSummary() error { 187 | var metric prometheus.Summary 188 | opts := prometheus.SummaryOpts{ 189 | Name: delta.Name, 190 | Help: delta.Help, 191 | } 192 | if len(delta.Labels) > 0 { 193 | vec := prometheus.NewSummaryVec(opts, delta.LabelNames()) 194 | err := prometheus.Register(vec) 195 | if err != nil { 196 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 197 | vec = are.ExistingCollector.(*prometheus.SummaryVec) 198 | } else { 199 | return err 200 | } 201 | } 202 | 203 | metric = vec.With(delta.Labels).(prometheus.Summary) 204 | } else { 205 | metric = prometheus.NewSummary(opts) 206 | err := prometheus.Register(metric) 207 | if err != nil { 208 | if are, ok := err.(prometheus.AlreadyRegisteredError); ok { 209 | metric = are.ExistingCollector.(prometheus.Summary) 210 | } else { 211 | return err 212 | } 213 | } 214 | } 215 | 216 | if delta.Method == "observe" { 217 | metric.Observe(delta.Value) 218 | } else { 219 | return fmt.Errorf("method %s is not implemented yet", delta.Method) 220 | } 221 | 222 | return nil 223 | } 224 | 225 | // Apply applies the delta (calls delta.method(delta.value)) on the correspondent (registered) Prometheus metric 226 | func (delta Delta) Apply() error { 227 | switch delta.Type { 228 | case COUNTER: 229 | return delta.applyOnCounter() 230 | case GAUGE: 231 | return delta.applyOnGauge() 232 | case HISTOGRAM: 233 | return delta.applyOnHistogram() 234 | case SUMMARY: 235 | return delta.applyOnSummary() 236 | } 237 | return fmt.Errorf("metric type %s is not implemented yet", delta.Type) 238 | } 239 | -------------------------------------------------------------------------------- /delta/delta_test.go: -------------------------------------------------------------------------------- 1 | package delta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/messagebird/pushprom/metrics" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestNewDelta(t *testing.T) { 15 | expected := &Delta{ 16 | Type: GAUGE, 17 | Method: "add", 18 | Name: "parachutiste", 19 | Help: "des oufs qui chutent du ciel", 20 | Value: 7.77, 21 | Labels: map[string]string{ 22 | "kind": "walker", 23 | "color": "blue", 24 | }, 25 | } 26 | 27 | out, err := json.Marshal(expected) 28 | assert.Nil(t, err) 29 | 30 | buf := bytes.NewBuffer(out) 31 | 32 | result, err := NewDelta(buf) 33 | assert.Nil(t, err) 34 | 35 | assert.Equal(t, expected, result) 36 | } 37 | 38 | func TestApplyVector(t *testing.T) { 39 | 40 | delta := &Delta{ 41 | Type: GAUGE, 42 | Method: "add", 43 | Name: "monster", 44 | Help: "evil imaginary enemies", 45 | Value: 67.92, 46 | Labels: map[string]string{ 47 | "nose_size_meters": "7", 48 | }, 49 | } 50 | 51 | // set an initial value 52 | initialValue := 1072.3 53 | gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 54 | Name: delta.Name, 55 | Help: delta.Help, 56 | }, delta.LabelNames()) 57 | 58 | err := prometheus.Register(gaugeVec) 59 | assert.Nil(t, err) 60 | 61 | gauge := gaugeVec.With(delta.Labels) 62 | gauge.Set(initialValue) 63 | 64 | // apply the delta 65 | err = delta.Apply() 66 | assert.Nil(t, err) 67 | 68 | ms, err := metrics.Fetch() 69 | assert.Nil(t, err) 70 | result, err := metrics.Read(ms, delta.Name, delta.Labels) 71 | if assert.Nil(t, err) { 72 | // check if the delta value was added to the metric 73 | expected := initialValue + delta.Value 74 | assert.Equal(t, expected, result) 75 | } 76 | } 77 | 78 | func TestApplyOne(t *testing.T) { 79 | delta := &Delta{ 80 | Type: GAUGE, 81 | Method: "add", 82 | Name: "rappers", 83 | Help: "gangsta musicians", 84 | Value: 27.292, 85 | } 86 | // set an initial value 87 | initialValue := 101.4 88 | gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 89 | Name: delta.Name, 90 | Help: delta.Help, 91 | }) 92 | err := prometheus.Register(gauge) 93 | assert.Nil(t, err) 94 | 95 | gauge.Set(initialValue) 96 | 97 | // apply the delta 98 | err = delta.Apply() 99 | assert.Nil(t, err) 100 | 101 | ms, err := metrics.Fetch() 102 | assert.Nil(t, err) 103 | result, err := metrics.Read(ms, delta.Name, delta.Labels) 104 | if assert.Nil(t, err) { 105 | // check if the delta value was added to the metric 106 | expected := initialValue + delta.Value 107 | assert.Equal(t, expected, result) 108 | } 109 | } 110 | 111 | func TestApplyWithWrongLabels(t *testing.T) { 112 | // create a metric with one set of labels then try to update it with another. it should break. 113 | delta := &Delta{ 114 | Type: GAUGE, 115 | Method: "add", 116 | Name: "dancers", 117 | Help: "people that jump pretty", 118 | Value: 1.22, 119 | Labels: map[string]string{ 120 | "nose_size_meters": "187", 121 | }, 122 | } 123 | err := delta.Apply() 124 | assert.Nil(t, err) 125 | 126 | delta.Labels["jumpiness"] = "9" 127 | 128 | err = delta.Apply() 129 | assert.NotNil(t, err) 130 | 131 | delta.Labels = map[string]string{} 132 | err = delta.Apply() 133 | assert.NotNil(t, err) 134 | } 135 | 136 | func TestMulti(t *testing.T) { 137 | 138 | tests := []struct { 139 | delta Delta 140 | f func(a, b float64) float64 141 | firstValue float64 142 | }{ 143 | { 144 | Delta{ 145 | Type: COUNTER, 146 | Method: "inc", 147 | }, 148 | func(before, value float64) float64 { 149 | return before + 1 150 | }, 151 | 0, 152 | }, 153 | { 154 | Delta{ 155 | Type: COUNTER, 156 | Method: "add", 157 | }, 158 | func(before, value float64) float64 { 159 | return before + value 160 | }, 161 | 0, 162 | }, 163 | { 164 | Delta{ 165 | Type: GAUGE, 166 | Method: "add", 167 | }, 168 | func(before, value float64) float64 { 169 | return before + value 170 | }, 171 | 0, 172 | }, 173 | { 174 | Delta{ 175 | Type: GAUGE, 176 | Method: "sub", 177 | }, 178 | func(before, value float64) float64 { 179 | return before - value 180 | }, 181 | 0, 182 | }, 183 | { 184 | Delta{ 185 | Type: GAUGE, 186 | Method: "inc", 187 | }, 188 | func(before, value float64) float64 { 189 | return before + 1 190 | }, 191 | 0, 192 | }, 193 | { 194 | Delta{ 195 | Type: GAUGE, 196 | Method: "dec", 197 | }, 198 | func(before, value float64) float64 { 199 | return before - 1 200 | }, 201 | 0, 202 | }, 203 | { 204 | Delta{ 205 | Type: GAUGE, 206 | Method: "set", 207 | }, 208 | func(before, value float64) float64 { 209 | return value 210 | }, 211 | 0, 212 | }, 213 | { 214 | Delta{ 215 | Type: HISTOGRAM, 216 | Method: "observe", 217 | }, 218 | func(before, value float64) float64 { 219 | // here we just test the _sum 220 | return before + value 221 | }, 222 | 0, 223 | }, 224 | { 225 | Delta{ 226 | Type: SUMMARY, 227 | Method: "observe", 228 | }, 229 | func(before, value float64) float64 { 230 | // here we just test the _sum 231 | return before + value 232 | }, 233 | 0, 234 | }, 235 | } 236 | 237 | // dup all tests so we test deltas with labels and without 238 | for i, test := range tests { 239 | test.delta.Labels = map[string]string{ 240 | fmt.Sprintf("key_%d", i): fmt.Sprintf("val_%d", i), 241 | } 242 | tests = append(tests, test) 243 | } 244 | 245 | // apply all deltas 246 | for i := 0; i < len(tests); i++ { 247 | test := &tests[i] 248 | // stub data 249 | test.delta.Value = 67.92 + float64(i) 250 | test.delta.Name = fmt.Sprintf("test_%d", i) 251 | test.delta.Help = fmt.Sprintf("help_%d", i) 252 | // apply Delta 253 | err := test.delta.Apply() 254 | assert.Nil(t, err) 255 | } 256 | 257 | initialValue := 0.0 // all metrics start with value zero 258 | // get metrics and compare results 259 | ms, err := metrics.Fetch() 260 | assert.Nil(t, err) 261 | 262 | for i := 0; i < len(tests); i++ { 263 | test := &tests[i] 264 | // read result and compare 265 | metricName := test.delta.Name 266 | if test.delta.Type == HISTOGRAM || test.delta.Type == SUMMARY { 267 | metricName += "_sum" 268 | } 269 | result, err := metrics.Read(ms, metricName, test.delta.Labels) 270 | if assert.Nil(t, err) { 271 | test.firstValue = result 272 | // check if the delta value was added to the metric 273 | expected := test.f(initialValue, test.delta.Value) 274 | assert.Equal(t, expected, result) 275 | } 276 | } 277 | 278 | // apply all deltas again 279 | for i := 0; i < len(tests); i++ { 280 | test := &tests[i] 281 | err := test.delta.Apply() 282 | assert.Nil(t, err) 283 | } 284 | 285 | // get metrics and compare results 286 | ms, err = metrics.Fetch() 287 | assert.Nil(t, err) 288 | for i := 0; i < len(tests); i++ { 289 | test := &tests[i] 290 | // read result and compare 291 | metricName := test.delta.Name 292 | if test.delta.Type == HISTOGRAM || test.delta.Type == SUMMARY { 293 | metricName += "_sum" 294 | } 295 | result, err := metrics.Read(ms, metricName, test.delta.Labels) 296 | if assert.Nil(t, err) { 297 | // check if the delta value was added to the metric 298 | expected := test.f(test.firstValue, test.delta.Value) 299 | assert.Equal(t, expected, result) 300 | } 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /docs/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Deploying to Kubernetes 2 | 3 | To deploy `pushprom` to Kubernetes you need two things: 4 | 5 | * a [NodePort Service](http://kubernetes.io/docs/user-guide/services/#type-nodeport) 6 | * a [Deployment](http://kubernetes.io/docs/user-guide/deployments/) 7 | 8 | The Service will expose UDP port 9090 and TCP port 9091 for internal usage, while also generating NodePorts for both (see # for usage). The Deployment will ensure a single [Pod](http://kubernetes.io/docs/user-guide/pods/) with `pushprom` is always running. Note that, since `pushprom` is not a distributed service, you can only have one replica of `pushprom` running in a Pod. 9 | 10 | ## Deploying `pushprom` 11 | 12 | ### Creating a namespace 13 | Before you get started, ensure that the Kubernetes namespace you want to run on exists, you can do that by running: 14 | ``` 15 | kubectl get namespace 16 | ``` 17 | If it does not exist, create it with the following command: 18 | ``` 19 | kubectl create namespace 20 | ``` 21 | 22 | ### Creating Service and the Deployment 23 | Create two files, one called [service.yaml](service.yaml) and one called [deployment.yaml](deployment.yaml). These files will contain the specification for the Service and the Deployment respectively. 24 | 25 | To deploy to them to your Kubernetes cluster you run the following commands: 26 | ``` 27 | kubectl apply -f deployment.yaml 28 | ``` 29 | ``` 30 | kubectl apply -f service.yaml 31 | ``` 32 | 33 | To verify that both the Deployment and the Service generation (or update) worked, run: 34 | ``` 35 | kubectl get po,svc --namespace= 36 | ``` 37 | 38 | ## Making `pushprom` available to your service 39 | By creating a Service you've now made `pushprom` available inside your Kubernetes cluster. You can talk to it by using the following address: `pushprom.`. 40 | 41 | ## Making `pushprom` available to Prometheus 42 | In order for [Prometheus](https://prometheus.io/) to reach your `pushprom` installation, you need to expose the Service to the outside world. First, let's find out what NodePorts were created by running: 43 | ``` 44 | kubectl describe svc pushprom --namespace= 45 | ``` 46 | Using the NodePorts that are listed there, create a rule in your load balancer (or similar ingress point) to point to these ports. 47 | -------------------------------------------------------------------------------- /docs/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: pushprom 6 | namespace: 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | labels: 12 | app: pushprom 13 | spec: 14 | containers: 15 | - name: pushprom 16 | image: messagebird/pushprom 17 | ports: 18 | - containerPort: 9090 19 | protocol: UDP 20 | - containerPort: 9091 21 | protocol: TCP -------------------------------------------------------------------------------- /docs/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | namespace: 6 | name: pushprom 7 | labels: 8 | app: pushprom 9 | spec: 10 | type: NodePort 11 | ports: 12 | - port: 9090 13 | protocol: UDP 14 | name: pushprom-udp-9090 15 | - port: 9091 16 | protocol: TCP 17 | name: pushprom-tcp-9091 18 | selector: 19 | app: pushprom -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/messagebird/pushprom 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/prometheus/client_golang v1.12.1 7 | github.com/stretchr/testify v1.4.0 8 | ) 9 | 10 | require ( 11 | github.com/beorn7/perks v1.0.1 // indirect 12 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/golang/protobuf v1.5.2 // indirect 15 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/prometheus/client_model v0.2.0 // indirect 18 | github.com/prometheus/common v0.32.1 // indirect 19 | github.com/prometheus/procfs v0.7.3 // indirect 20 | golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect 21 | google.golang.org/protobuf v1.27.1 // indirect 22 | gopkg.in/yaml.v2 v2.4.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 37 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 38 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 39 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 40 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 41 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 42 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 43 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 44 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 47 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 48 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 49 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 50 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 51 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 52 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 53 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 54 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 55 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 57 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 61 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 62 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 63 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 64 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 65 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 66 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 67 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 68 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 69 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 70 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 71 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 72 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 73 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 74 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 75 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 76 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 77 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 78 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 79 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 80 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 81 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 82 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 83 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 84 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 85 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 86 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 88 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 89 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 90 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 91 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 92 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 93 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 94 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 95 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 96 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 97 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 98 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 99 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 100 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 101 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 102 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 103 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 104 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 105 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 106 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 107 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 108 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 110 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 111 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 112 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 113 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 115 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 116 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 117 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 118 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 119 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 120 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 121 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 122 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 123 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 124 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 125 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 126 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 127 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 128 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 129 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 130 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 131 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 132 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 133 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 134 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 135 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 136 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 137 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 138 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 139 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 140 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 141 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 142 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 143 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 144 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 145 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 146 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 147 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 148 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 149 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 150 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 151 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 152 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 153 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 154 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 155 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 156 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 157 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 158 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 159 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 160 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 161 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 162 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 163 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 164 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 165 | github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= 166 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 167 | github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= 168 | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 169 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 170 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 171 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 172 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 173 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 174 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 175 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 176 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 177 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 178 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 179 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 180 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 181 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 182 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 183 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 184 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 185 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 186 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 187 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 188 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 189 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 190 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 191 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 192 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 193 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 194 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 195 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 196 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 197 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 198 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 199 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 200 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 201 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 202 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 203 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 204 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 205 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 206 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 207 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 208 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 209 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 210 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 211 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 212 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 213 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 214 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 215 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 216 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 217 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 218 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 219 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 220 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 221 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 222 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 223 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 224 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 225 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 226 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 227 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 228 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 229 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 230 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 231 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 232 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 233 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 234 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 235 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 236 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 237 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 238 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 239 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 240 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 241 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 242 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 243 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 244 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 246 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 247 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 248 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 249 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 250 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 251 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 252 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 253 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 254 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 255 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 256 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 257 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 258 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 259 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 261 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 262 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 263 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 264 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 265 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 266 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 267 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 268 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 269 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 270 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 271 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 272 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 273 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 274 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 275 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 280 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 281 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 282 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 283 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 284 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 286 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 287 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 304 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 305 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 306 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 307 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 308 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 309 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 310 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 311 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 312 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 314 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 315 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 316 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 317 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 318 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= 319 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 320 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 321 | golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= 322 | golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 323 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 324 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 325 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 326 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 327 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 328 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 329 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 330 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 331 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 332 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 333 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 334 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 335 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 336 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 337 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 338 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 339 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 340 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 341 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 342 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 343 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 344 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 345 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 346 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 347 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 348 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 349 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 350 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 351 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 352 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 353 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 354 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 355 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 356 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 357 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 358 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 359 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 360 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 361 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 362 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 363 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 364 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 365 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 366 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 367 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 368 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 369 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 370 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 371 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 372 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 373 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 374 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 375 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 376 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 377 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 378 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 379 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 380 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 381 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 382 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 383 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 384 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 385 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 386 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 387 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 388 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 389 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 390 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 391 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 392 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 393 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 394 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 395 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 396 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 397 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 398 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 399 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 400 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 401 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 402 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 403 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 404 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 405 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 406 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 407 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 408 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 409 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 410 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 411 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 412 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 413 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 414 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 415 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 416 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 417 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 418 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 419 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 420 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 421 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 422 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 423 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 424 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 425 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 426 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 427 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 428 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 429 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 430 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 431 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 432 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 433 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 434 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 435 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 436 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 437 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 438 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 439 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 440 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 441 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 442 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 443 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 444 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 445 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 446 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 447 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 448 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 449 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 450 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 451 | google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= 452 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 453 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 454 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 455 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 456 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 457 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 458 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 459 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 460 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 461 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 462 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 463 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 464 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 465 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 466 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 467 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 468 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 469 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 470 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 471 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 472 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 473 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 474 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 475 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 476 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 477 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 478 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 479 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/messagebird/pushprom/delta" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | type httpHandler struct { 19 | } 20 | 21 | func (httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 22 | if r.Body != nil { 23 | defer r.Body.Close() 24 | } 25 | 26 | if r.Method == "POST" { 27 | httpRequestCount.Inc() 28 | 29 | newDelta, err := delta.NewDelta(r.Body) 30 | if err != nil { 31 | w.WriteHeader(http.StatusBadRequest) 32 | fmt.Fprintln(w, err) 33 | return 34 | } 35 | 36 | if err = newDelta.Apply(); err != nil { 37 | w.WriteHeader(http.StatusBadRequest) 38 | fmt.Fprintln(w, err) 39 | } else { 40 | fmt.Fprintf(w, "ok") 41 | } 42 | } else { 43 | w.WriteHeader(http.StatusMethodNotAllowed) 44 | fmt.Fprintf(w, "method not allowed") 45 | } 46 | } 47 | 48 | func listenHTTP(wg *sync.WaitGroup, ctx context.Context, stderrLogger *log.Logger, stdoutLogger *log.Logger) { 49 | stdoutLogger.Println("exposing metrics on http://" + *httpListenAddress + "/metrics") 50 | 51 | mux := http.NewServeMux() 52 | mux.Handle("/metrics", promhttp.Handler()) 53 | 54 | mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 55 | fmt.Fprintf(w, "ok") 56 | }) 57 | 58 | // Instrument the handlers with all the metrics, injecting the "handler" 59 | // label by currying. 60 | postHandler := promhttp.InstrumentHandlerDuration(httpRequestDuration.MustCurryWith(prometheus.Labels{"handler": "post"}), 61 | promhttp.InstrumentHandlerCounter(httpRequestsTotal, httpHandler{}), 62 | ) 63 | 64 | mux.HandleFunc("/", postHandler) 65 | 66 | server := http.Server{ 67 | // see https://ieftimov.com/posts/make-resilient-golang-net-http-servers-using-timeouts-deadlines-context-cancellation/ 68 | WriteTimeout: 10 * time.Second, 69 | ReadHeaderTimeout: 10 * time.Second, 70 | Addr: *httpListenAddress, 71 | Handler: mux, 72 | } 73 | 74 | go func(wg *sync.WaitGroup) { 75 | <-ctx.Done() 76 | 77 | stdoutLogger.Println("shutting down http listener on " + *httpListenAddress) 78 | 79 | servCtx, cancel := context.WithTimeout(context.Background(), 4*time.Second) 80 | err := server.Shutdown(servCtx) 81 | if err != nil { 82 | stderrLogger.Fatalf("http listener failed to shutdown gracefully: %v", err) 83 | } 84 | cancel() 85 | defer wg.Done() 86 | stdoutLogger.Println("http listener is now offline") 87 | }(wg) 88 | 89 | stdoutLogger.Println("listening for stats HTTP on http://" + *httpListenAddress) 90 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 91 | stderrLogger.Fatalf("Failed to ListenAndServe: %v", err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/messagebird/pushprom/delta" 13 | "github.com/messagebird/pushprom/metrics" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestHTTP(t *testing.T) { 18 | 19 | delta := &delta.Delta{ 20 | Type: delta.GAUGE, 21 | Method: "set", 22 | Name: "tree_size", 23 | Help: "the size in meters of the tree", 24 | Value: 8.2, 25 | } 26 | 27 | out, _ := json.Marshal(delta) 28 | buf := bytes.NewBuffer(out) 29 | 30 | ts := httptest.NewServer(http.Handler(httpHandler{ 31 | 32 | })) 33 | defer ts.Close() 34 | 35 | res, err := http.Post(ts.URL, "application/json", buf) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | _, err = ioutil.ReadAll(res.Body) 40 | res.Body.Close() 41 | assert.Nil(t, err) 42 | 43 | ms, err := metrics.Fetch() 44 | assert.Nil(t, err) 45 | result, err := metrics.Read(ms, delta.Name, delta.Labels) 46 | if assert.Nil(t, err) { 47 | assert.Equal(t, delta.Value, result) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | stderrLogger "log" 9 | stdoutLogger "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | var ( 19 | udpListenAddress = flag.String("udp-listen-address", "0.0.0.0:9090", "The address to listen on for udp stats requests.") 20 | httpListenAddress = flag.String("http-listen-address", "0.0.0.0:9091", "The address to listen on for http stat and telemetry requests.") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | stderrLogger.SetOutput(os.Stderr) 27 | stdoutLogger.SetOutput(os.Stdout) 28 | 29 | errrorLogger := stderrLogger.Default() 30 | infoLogger := stdoutLogger.Default() 31 | 32 | infoLogger.Print("welcome to messagebird/pushprom") 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | 36 | var err error 37 | 38 | *udpListenAddress, err = ListenAddress(*udpListenAddress) 39 | if err != nil { 40 | errrorLogger.Fatalf(err.Error()) 41 | } 42 | 43 | *httpListenAddress, err = ListenAddress(*httpListenAddress) 44 | if err != nil { 45 | errrorLogger.Fatalf(err.Error()) 46 | } 47 | 48 | infoLogger.Print("starting listeners") 49 | 50 | var wg sync.WaitGroup 51 | 52 | go listenUDP(&wg, ctx, errrorLogger, infoLogger) 53 | go listenHTTP(&wg, ctx, errrorLogger, infoLogger) 54 | wg.Add(2) 55 | 56 | handleSIGTERM(&wg, cancel, infoLogger) 57 | } 58 | 59 | func handleSIGTERM(wg *sync.WaitGroup, cancel func(), infoLogger *log.Logger) { 60 | sigc := make(chan os.Signal, 1) 61 | signal.Notify(sigc, syscall.SIGTERM, os.Interrupt) 62 | <-sigc 63 | infoLogger.Println("received termination/interrupt signal, will terminate") 64 | cancel() 65 | defer close(sigc) 66 | 67 | infoLogger.Println("waiting for all servers to gracefully terminate") 68 | wg.Wait() 69 | time.Sleep(1 * time.Second) 70 | infoLogger.Println("all goroutines gracefully finished") 71 | } 72 | 73 | // ListenAddress Format a correct listen address 74 | func ListenAddress(s string) (string, error) { 75 | host, port, err := net.SplitHostPort(s) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return fmt.Sprintf("%s:%s", host, port), nil 81 | } 82 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // TODO replace all usage of this by https://godoc.org/github.com/prometheus/common/expfmt#TextParser.TextToMetricFamilies 2 | package metrics 3 | 4 | import ( 5 | "bufio" 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/prometheus/client_golang/prometheus" 14 | ) 15 | 16 | func Fetch() (map[string]string, error) { 17 | result := make(map[string]string) 18 | ts := httptest.NewServer(promhttp.Handler()) 19 | defer ts.Close() 20 | 21 | res, err := http.Get(ts.URL) 22 | if err != nil { 23 | return result, err 24 | } 25 | reader := bufio.NewReader(res.Body) 26 | err = nil 27 | for err == nil { 28 | var line string 29 | line, err = reader.ReadString('\n') 30 | line = strings.Trim(line, "\n") 31 | parts := strings.Split(line, " ") 32 | if (parts[0] != "#") && (len(parts) > 1) { 33 | result[parts[0]] = parts[1] 34 | } 35 | } 36 | if err == io.EOF { 37 | err = nil 38 | } 39 | return result, err 40 | } 41 | 42 | func Read(metrics map[string]string, name string, labels prometheus.Labels) (float64, error) { 43 | // http_request_duration_microseconds{handler="alerts",quantile="0.5"} 44 | fullName := name 45 | if len(labels) > 0 { 46 | pairs := []string{} 47 | // TODO SORT GOOD 48 | for k, v := range labels { 49 | pairs = append(pairs, k+"=\""+v+"\"") 50 | } 51 | fullName += "{" + strings.Join(pairs, ",") + "}" 52 | } 53 | 54 | stringValue := metrics[fullName] 55 | return strconv.ParseFloat(stringValue, 64) 56 | } 57 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # a hack to generate releases like other prometheus projects 3 | # use like this: 4 | # VERSION=1.0.1 ./release.sh 5 | set -e 6 | 7 | # github user and repo 8 | USER=messagebird 9 | REPO=pushprom 10 | BIN_PACKAGE=github.com/${USER}/${REPO} 11 | BIN_DIR=bin 12 | 13 | if [ -z "$VERSION" ]; then 14 | >&2 echo "missing VERSION=X.X.X" 15 | exit 1 16 | fi 17 | 18 | function build_for { 19 | BIN_NAME="bin/${REPO}-${VERSION}.$1-$2" 20 | GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -ldflags "-s -w" -o ${BIN_NAME} ${BIN_PACKAGE} 21 | shasum -a 256 ${BIN_NAME} > ${BIN_NAME}.sha256 22 | } 23 | 24 | rm -rf ${BIN_DIR} 25 | mkdir -p ${BIN_DIR} 26 | 27 | build_for linux amd64 28 | build_for darwin amd64 29 | 30 | DOCKER_BUILDKIT=1 docker build -t ${USER}/${REPO}:${VERSION} . 31 | 32 | git tag -a $VERSION -m "version $VERSION" 33 | 34 | github-release release \ 35 | --user $USER \ 36 | --repo $REPO \ 37 | --tag $VERSION \ 38 | --name $VERSION \ 39 | --description "version $VERSION" 40 | 41 | for release_file in $(ls ${BIN_DIR}); do 42 | github-release upload \ 43 | --user $USER \ 44 | --repo $REPO \ 45 | --tag $VERSION \ 46 | --name "${release_file}" \ 47 | --file "${BIN_DIR}/${release_file}" 48 | done 49 | 50 | docker push ${USER}/${REPO}:${VERSION} 51 | docker tag ${USER}/${REPO}:${VERSION} ${USER}/${REPO}:latest 52 | docker push ${USER}/${REPO}:latest 53 | -------------------------------------------------------------------------------- /telemetry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | udpPacketCount = prometheus.NewCounter( 7 | prometheus.CounterOpts{ 8 | Name: "pushprom_udp_packets_total", 9 | Help: "The number of packets received.", 10 | }, 11 | ) 12 | 13 | httpRequestCount = prometheus.NewCounter( 14 | prometheus.CounterOpts{ 15 | Name: "pushprom_http_requests_total", 16 | Help: "The number of http requests received.", 17 | }, 18 | ) 19 | 20 | httpRequestsTotal = prometheus.NewCounterVec( 21 | prometheus.CounterOpts{ 22 | Name: "requests_total", 23 | Help: "A counter for requests to the wrapped handler.", 24 | }, 25 | []string{"code", "method"}, 26 | ) 27 | 28 | httpRequestDuration = prometheus.NewHistogramVec( 29 | prometheus.HistogramOpts{ 30 | Name: "request_duration_seconds", 31 | Help: "A histogram of latencies for requests.", 32 | }, 33 | []string{"handler", "method"}, 34 | ) 35 | ) 36 | 37 | func init() { 38 | prometheus.MustRegister(udpPacketCount) 39 | prometheus.MustRegister(httpRequestCount) 40 | prometheus.MustRegister(httpRequestsTotal) 41 | prometheus.MustRegister(httpRequestDuration) 42 | } 43 | -------------------------------------------------------------------------------- /udp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "log" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/messagebird/pushprom/delta" 13 | ) 14 | 15 | func listenUDP(wg *sync.WaitGroup, ctx context.Context, stderrLogger *log.Logger, stdoutLogger *log.Logger) { 16 | stdoutLogger.Println("listening for stats UDP on port " + *udpListenAddress) 17 | serverAddr, err := net.ResolveUDPAddr("udp", *udpListenAddress) 18 | if err != nil { 19 | stderrLogger.Print(err) 20 | } 21 | 22 | serverConn, err := net.ListenUDP("udp", serverAddr) 23 | if err != nil { 24 | stderrLogger.Print(err) 25 | } 26 | 27 | defer func(serverConn *net.UDPConn, wg *sync.WaitGroup) { 28 | stdoutLogger.Println("closing incoming UDP port " + *udpListenAddress) 29 | err := serverConn.Close() 30 | if err != nil { 31 | stderrLogger.Print(err) 32 | } 33 | defer wg.Done() 34 | stdoutLogger.Println("udp listener is now offline") 35 | }(serverConn, wg) 36 | 37 | buf := make([]byte, 8192) 38 | 39 | for { 40 | // handle cancelled context. 41 | select { 42 | case <-ctx.Done(): 43 | stdoutLogger.Println("shutting down udp listener on " + *udpListenAddress) 44 | return 45 | default: 46 | } 47 | 48 | err = serverConn.SetReadDeadline(time.Now().Add(2 * time.Second)) 49 | if err != nil { 50 | stderrLogger.Print("failed setting read UDP deadline: ", err) 51 | } 52 | 53 | n, _, err := serverConn.ReadFromUDP(buf) 54 | if err != nil { 55 | if !strings.Contains(err.Error(), "i/o timeout") { 56 | stderrLogger.Print("error reading from UDP: ", err) 57 | } 58 | continue 59 | } 60 | udpPacketCount.Inc() 61 | 62 | // fmt.Printf("new udp package: %s\n", string(buf[0:n])) 63 | 64 | newDelta, err := delta.NewDelta(bytes.NewBuffer(buf[0:n])) 65 | if err != nil { 66 | stderrLogger.Print("Error creating delta: ", err) 67 | continue 68 | } 69 | 70 | err = newDelta.Apply() 71 | if err != nil { 72 | stderrLogger.Print("Error applying delta: ", err) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /udp_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net" 7 | "os" 8 | "log" 9 | "testing" 10 | "time" 11 | 12 | "github.com/messagebird/pushprom/delta" 13 | "github.com/messagebird/pushprom/metrics" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | var conn *net.UDPConn 19 | 20 | func TestMain(m *testing.M) { 21 | *udpListenAddress = ":3007" 22 | 23 | go listenUDP(context.Background()) 24 | 25 | // wait for it to "boot" 26 | time.Sleep(time.Millisecond * 1000) 27 | 28 | serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1"+*udpListenAddress) 29 | if err != nil { 30 | log.Fatal("Could not resolve server UPD address") 31 | } 32 | 33 | localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") 34 | if err != nil { 35 | log.Fatal("Could not resolve local UDP address") 36 | } 37 | 38 | conn, err = net.DialUDP("udp", localAddr, serverAddr) 39 | if err != nil { 40 | log.Fatal("Could not establish UDP connection") 41 | } 42 | 43 | defer conn.Close() 44 | 45 | os.Exit(m.Run()) 46 | } 47 | 48 | func TestUDP(t *testing.T) { 49 | delta := &delta.Delta{ 50 | Type: delta.GAUGE, 51 | Method: "set", 52 | Name: "tree_width", 53 | Help: "the width in meters of the tree", 54 | Value: 2.2, 55 | } 56 | 57 | buf, _ := json.Marshal(delta) 58 | 59 | _, err := conn.Write(buf) 60 | assert.Nil(t, err) 61 | 62 | time.Sleep(time.Millisecond * 500) 63 | 64 | ms, err := metrics.Fetch() 65 | assert.Nil(t, err) 66 | result, err := metrics.Read(ms, delta.Name, delta.Labels) 67 | if assert.Nil(t, err) { 68 | assert.Equal(t, delta.Value, result) 69 | } 70 | } 71 | 72 | func TestIncorrectJson(t *testing.T) { 73 | // First, let's write the correct value 74 | oldDelta := &delta.Delta{ 75 | Type: delta.GAUGE, 76 | Method: "set", 77 | Name: "tree_width", 78 | Help: "the width in meters of the tree", 79 | Value: 2.2, 80 | } 81 | buf, _ := json.Marshal(oldDelta) 82 | 83 | _, err := conn.Write(buf) 84 | assert.Nil(t, err) 85 | 86 | oldMetrics, err := metrics.Fetch() 87 | assert.Nil(t, err) 88 | oldResult, err := metrics.Read(oldMetrics, oldDelta.Name, oldDelta.Labels) 89 | if assert.Nil(t, err) { 90 | assert.Equal(t, oldDelta.Value, oldResult) 91 | } 92 | 93 | // Now, let's write the incorrect value 94 | buf = []byte("hello") 95 | 96 | _, err = conn.Write(buf) 97 | assert.Nil(t, err) 98 | 99 | // Last, let's write a new value 100 | newDelta := &delta.Delta{ 101 | Type: delta.GAUGE, 102 | Method: "set", 103 | Name: "tree_width", 104 | Help: "the width in meters of the tree", 105 | Value: 2.5, 106 | } 107 | buf, _ = json.Marshal(newDelta) 108 | 109 | _, err = conn.Write(buf) 110 | assert.Nil(t, err) 111 | 112 | newMetrics, err := metrics.Fetch() 113 | assert.Nil(t, err) 114 | newResult, err := metrics.Read(newMetrics, newDelta.Name, newDelta.Labels) 115 | if assert.Nil(t, err) { 116 | assert.Equal(t, newDelta.Value, newResult) 117 | } 118 | } 119 | --------------------------------------------------------------------------------