├── .gitignore ├── Makefile ├── env ├── rules.yaml ├── grafana │ ├── provisioning │ │ ├── datasources │ │ │ └── all.yaml │ │ └── dashboards │ │ │ └── all.yml │ └── dashboards │ │ ├── r8limiter.json │ │ └── go-metrics_rev1.json └── prometheus │ └── prometheus.yml ├── pkg ├── limiter │ ├── counter.go │ ├── time.go │ ├── rules.go │ ├── limiter.go │ └── limiter_test.go ├── configs │ └── config.go ├── counter │ ├── storage.go │ ├── counter.go │ └── counter_test.go ├── transport │ ├── http │ │ ├── option.go │ │ ├── handler.go │ │ ├── middleware.go │ │ └── http.go │ └── grpc │ │ ├── options.go │ │ └── grpc.go ├── file │ ├── testdata │ │ └── generic.yaml │ ├── rules_test.go │ └── rules.go └── redis │ └── storage.go ├── Dockerfile ├── examples └── rules │ ├── config.yaml │ └── rules_with_sync.yaml ├── .circleci └── config.yml ├── docker-compose.yml ├── LICENSE ├── go.mod ├── cmd ├── client │ └── client.go └── server │ └── server.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run cmd/server/server.go --rules-file=./env/rules.yaml 3 | 4 | docker-build: 5 | docker build -t localhost:32000/r8limiter:0.2 . 6 | 7 | docker-push: 8 | docker push localhost:32000/r8limiter:0.2 9 | 10 | pprof: 11 | go tool pprof -http=localhost:8444 http://localhost:8082/debug/pprof/profile -------------------------------------------------------------------------------- /env/rules.yaml: -------------------------------------------------------------------------------- 1 | domains: 2 | - domain: kong 3 | rules: 4 | - labels: 5 | - key: tenant_id 6 | syncRate: -1 7 | limit: 8 | unit: minute 9 | requests: 100000000 10 | - labels: 11 | - key: ip_address 12 | limit: 13 | unit: day 14 | requests: 576876 -------------------------------------------------------------------------------- /env/grafana/provisioning/datasources/all.yaml: -------------------------------------------------------------------------------- 1 | datasources: 2 | - access: 'prometheus' 3 | editable: true 4 | is_default: true 5 | name: 'FfMetrics' 6 | org_id: 1 7 | type: 'prometheus' 8 | url: 'http://localhost:9090' 9 | version: 1 -------------------------------------------------------------------------------- /pkg/limiter/counter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type CounterService interface { 8 | Increment(ctx context.Context, key string, n uint32, ttl int64, syncRate int) (uint32, error) 9 | IncrementOnStorage(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) 10 | Get(ctx context.Context, key string) (uint32, error) 11 | GetFromStorage(ctx context.Context, key string) (uint32, error) 12 | } 13 | -------------------------------------------------------------------------------- /env/grafana/provisioning/dashboards/all.yml: -------------------------------------------------------------------------------- 1 | 2 | - name: 'default' # name of this dashboard configuration (not dashboard itself) 3 | org_id: 1 # id of the org to hold the dashboard 4 | folder: '' # name of the folder to put the dashboard (http://docs.grafana.org/v5.0/reference/dashboard_folders/) 5 | type: 'file' # type of dashboard description (json files) 6 | options: 7 | folder: '/var/lib/grafana/dashboards' # where dashboards are -------------------------------------------------------------------------------- /pkg/configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | GrpcAddr string 7 | HttpAddr string 8 | DebugAddr string 9 | Datastore string 10 | Redis RedisConfig 11 | RulesFile string 12 | LogLevel string 13 | SyncBatchSize int 14 | ShutdownGracePeriod time.Duration 15 | } 16 | 17 | type RedisConfig struct { 18 | Address string 19 | Database int 20 | Password string 21 | } 22 | -------------------------------------------------------------------------------- /pkg/counter/storage.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import "context" 4 | 5 | type CounterStorage interface { 6 | BatchIncrement(ctx context.Context, incrs []CounterInc) ([]CounterIncResponse, error) 7 | Increment(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) 8 | Get(ctx context.Context, key string) (uint32, error) 9 | } 10 | 11 | type CounterInc struct { 12 | Key string 13 | Inc uint32 14 | TTL int64 15 | } 16 | 17 | type CounterIncResponse struct { 18 | Key string 19 | Curr uint32 20 | Err error 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 as builder 2 | 3 | ENV GO111MODULE=on 4 | 5 | WORKDIR /app 6 | COPY go.mod /app 7 | RUN go get -v all 8 | COPY . /app 9 | RUN CGO_ENABLED=0 GOOS=linux GOPROXY=https://proxy.golang.org go build -o app cmd/server/server.go 10 | 11 | FROM alpine:latest 12 | # mailcap adds mime detection and ca-certificates help with TLS (basic stuff) 13 | RUN apk --no-cache add ca-certificates mailcap && addgroup -S app && adduser -S app -G app 14 | USER app 15 | WORKDIR /app 16 | COPY --from=builder /app/app . 17 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /examples/rules/config.yaml: -------------------------------------------------------------------------------- 1 | domains: 2 | - domain: envoy 3 | rules: 4 | - name: authenticated users 5 | labels: 6 | - key: authenticated 7 | value: "true" 8 | - key: user_id 9 | limit: 10 | unit: hour 11 | requests: 500 12 | 13 | - name: any user 14 | labels: 15 | - key: user_id 16 | limit: 17 | unit: hour 18 | requests: 1000 19 | 20 | - name: portuguese users 21 | labels: 22 | - key: country 23 | value: "PT" 24 | - key: user_id 25 | limit: 26 | unit: minute 27 | requests: 4000 28 | -------------------------------------------------------------------------------- /examples/rules/rules_with_sync.yaml: -------------------------------------------------------------------------------- 1 | domains: 2 | - domain: envoy 3 | rules: 4 | - name: authenticated users 5 | labels: 6 | - key: authenticated 7 | value: "true" 8 | - key: user_id 9 | limit: 10 | unit: hour 11 | requests: 500 12 | syncRate: 1 13 | 14 | - name: any user 15 | labels: 16 | - key: user_id 17 | limit: 18 | unit: hour 19 | requests: 1000 20 | syncRate: 10 21 | 22 | - name: portuguese users 23 | labels: 24 | - key: country 25 | value: "PT" 26 | - key: user_id 27 | limit: 28 | unit: minute 29 | requests: 4000 30 | sync_rate: 0 -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.13 6 | 7 | working_directory: /go/src/github.com/samueltorres/r8limiter 8 | 9 | steps: 10 | - checkout 11 | - run: go get all 12 | - run: go test ./... 13 | 14 | publish: 15 | machine: 16 | docker_layer_caching: false 17 | steps: 18 | - checkout 19 | - run: docker build -t $DOCKER_IMAGE:latest . 20 | - run: docker login -u $DOCKER_USER -p $DOCKER_PASS 21 | - run: docker push $DOCKER_IMAGE:latest 22 | 23 | 24 | workflows: 25 | version: 2 26 | build-and-publish: 27 | jobs: 28 | - build 29 | - publish: 30 | requires: 31 | - build 32 | filters: 33 | branches: 34 | only: master 35 | -------------------------------------------------------------------------------- /pkg/limiter/time.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 5 | ) 6 | 7 | func timeUnitToWindowSize(unit string) int64 { 8 | switch unit { 9 | case "second": 10 | return 1 11 | case "minute": 12 | return 60 13 | case "hour": 14 | return 60 * 60 15 | case "day": 16 | return 60 * 60 * 24 17 | } 18 | 19 | panic("should not get here") 20 | } 21 | 22 | func timeUnitToPb(unit string) pb.RateLimitResponse_RateLimit_Unit { 23 | switch unit { 24 | case "second": 25 | return pb.RateLimitResponse_RateLimit_SECOND 26 | case "minute": 27 | return pb.RateLimitResponse_RateLimit_MINUTE 28 | case "hour": 29 | return pb.RateLimitResponse_RateLimit_HOUR 30 | case "day": 31 | return pb.RateLimitResponse_RateLimit_DAY 32 | default: 33 | return pb.RateLimitResponse_RateLimit_UNKNOWN 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/transport/http/option.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type options struct { 8 | gracePeriod time.Duration 9 | listen string 10 | } 11 | 12 | // Option overrides behavior of Server. 13 | type Option interface { 14 | apply(*options) 15 | } 16 | 17 | type optionFunc func(*options) 18 | 19 | func (f optionFunc) apply(o *options) { 20 | f(o) 21 | } 22 | 23 | // WithGracePeriod sets shutdown grace period for HTTP server. 24 | // Server waits connections to drain for specified amount of time. 25 | func WithGracePeriod(t time.Duration) Option { 26 | return optionFunc(func(o *options) { 27 | o.gracePeriod = t 28 | }) 29 | } 30 | 31 | // WithListen sets address to listen for HTTP server. 32 | // Server accepts incoming TCP connections on given address. 33 | func WithListen(s string) Option { 34 | return optionFunc(func(o *options) { 35 | o.listen = s 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/limiter/rules.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "errors" 5 | 6 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 7 | ) 8 | 9 | var ErrNoMatchedRule = errors.New("no matched rules") 10 | 11 | type RulesService interface { 12 | GetRatelimitRule(domain string, requestDescriptor *rl.RateLimitDescriptor) (*Rule, error) 13 | } 14 | 15 | type RulesConfig struct { 16 | Domains []*DomainRules `yaml:"domains"` 17 | } 18 | 19 | type DomainRules struct { 20 | Domain string `yaml:"domain"` 21 | Rules []*Rule `yaml:"rules"` 22 | } 23 | 24 | type Rule struct { 25 | Name string `yaml:"name"` 26 | Labels []DescriptorLabel `yaml:"labels"` 27 | Limit Limit `yaml:"limit"` 28 | SyncRate int `yaml:"syncRate"` 29 | InnerRank int 30 | } 31 | 32 | type DescriptorLabel struct { 33 | Key string `yaml:"key"` 34 | Value string `yaml:"value"` 35 | } 36 | 37 | type Limit struct { 38 | Requests uint32 `yaml:"requests"` 39 | Unit string `yaml:"unit"` 40 | } 41 | -------------------------------------------------------------------------------- /pkg/transport/http/handler.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 9 | "github.com/samueltorres/r8limiter/pkg/limiter" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type rateLimitingHandler struct { 14 | limiter *limiter.LimiterService 15 | logger *logrus.Logger 16 | } 17 | 18 | func (h *rateLimitingHandler) handleRateLimit(w http.ResponseWriter, r *http.Request) { 19 | w.Header().Set("Content-Type", "application/json") 20 | 21 | var req pb.RateLimitRequest 22 | err := json.NewDecoder(r.Body).Decode(&req) 23 | if err != nil { 24 | h.logger.Info("could not decode request", err) 25 | w.WriteHeader(http.StatusNotFound) 26 | return 27 | } 28 | 29 | res, err := h.limiter.ShouldRateLimit(context.Background(), &req) 30 | if err != nil { 31 | h.logger.Info("could not get rate limit", err) 32 | w.WriteHeader(http.StatusNotFound) 33 | return 34 | } 35 | 36 | w.WriteHeader(http.StatusOK) 37 | if err := json.NewEncoder(w).Encode(res); err != nil { 38 | panic(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | prometheus: 5 | image: prom/prometheus:v2.7.2 6 | container_name: monitoring-prometheus 7 | network_mode: host 8 | ports: 9 | - "9090:9090" 10 | volumes: 11 | - ./env/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 12 | command: 13 | - "--config.file=/etc/prometheus/prometheus.yml" 14 | logging: 15 | driver: none 16 | 17 | grafana: 18 | image: grafana/grafana:6.0.1 19 | container_name: monitoring-grafana 20 | network_mode: host 21 | ports: 22 | - "3000:3000" 23 | volumes: 24 | - ./env/grafana/provisioning:/etc/grafana/provisioning 25 | - ./env/grafana/dashboards:/var/lib/grafana/dashboards 26 | logging: 27 | driver: none 28 | depends_on: 29 | - prometheus 30 | 31 | redis: 32 | image: redis 33 | container_name: cache 34 | ports: 35 | - "6379:6379" 36 | 37 | # r8: 38 | # build: . 39 | # environment: 40 | # - R8_REDIS_ADDRESS=localhost:6379 41 | # - R8_RULES_FILE=/env/rules.yaml 42 | # volumes: 43 | # - ./env:/env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Samuel Torres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/transport/grpc/options.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | ) 7 | 8 | type options struct { 9 | gracePeriod time.Duration 10 | listen string 11 | 12 | tlsConfig *tls.Config 13 | } 14 | 15 | // Option overrides behavior of Server. 16 | type Option interface { 17 | apply(*options) 18 | } 19 | 20 | type optionFunc func(*options) 21 | 22 | func (f optionFunc) apply(o *options) { 23 | f(o) 24 | } 25 | 26 | // WithGracePeriod sets shutdown grace period for gRPC server. 27 | // Server waits connections to drain for specified amount of time. 28 | func WithGracePeriod(t time.Duration) Option { 29 | return optionFunc(func(o *options) { 30 | o.gracePeriod = t 31 | }) 32 | } 33 | 34 | // WithListen sets address to listen for gRPC server. 35 | // Server accepts incoming connections on given address. 36 | func WithListen(s string) Option { 37 | return optionFunc(func(o *options) { 38 | o.listen = s 39 | }) 40 | } 41 | 42 | // WithTLSConfig sets TLS configuration for gRPC server. 43 | func WithTLSConfig(cfg *tls.Config) Option { 44 | return optionFunc(func(o *options) { 45 | o.tlsConfig = cfg 46 | }) 47 | } -------------------------------------------------------------------------------- /pkg/file/testdata/generic.yaml: -------------------------------------------------------------------------------- 1 | domains: 2 | - domain: test 3 | rules: 4 | - name: test1 5 | labels: 6 | - key: t 7 | value: v 8 | limit: 9 | unit: hour 10 | requests: 1 11 | 12 | - domain: api-gateway 13 | rules: 14 | - name: authorized + user_id 15 | labels: 16 | - key: authorized 17 | value: "true" 18 | - key: user_id 19 | limit: 20 | unit: hour 21 | requests: 1 22 | 23 | - name: non-authorized + user_id 24 | labels: 25 | - key: authorized 26 | value: "false" 27 | - key: user_id 28 | limit: 29 | unit: hour 30 | requests: 2 31 | 32 | - name: any-user_id 33 | labels: 34 | - key: user_id 35 | limit: 36 | unit: hour 37 | requests: 3 38 | 39 | - name: authorized 40 | labels: 41 | - key: authorized 42 | value: "true" 43 | - key: tenant 44 | limit: 45 | unit: hour 46 | requests: 4 47 | 48 | - name: tenant 49 | labels: 50 | - key: tenant 51 | limit: 52 | unit: hour 53 | requests: 5 -------------------------------------------------------------------------------- /env/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 4 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Attach these labels to any time series or alerts when communicating with 8 | # external systems (federation, remote storage, Alertmanager). 9 | external_labels: 10 | monitor: 'codelab-monitor' 11 | 12 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 13 | rule_files: 14 | # - "first.rules" 15 | # - "second.rules" 16 | 17 | # A scrape configuration containing exactly one endpoint to scrape: 18 | # Here it's Prometheus itself. 19 | scrape_configs: 20 | # The job name is added as a label `job=` to any timeseries scraped from this config. 21 | - job_name: 'prometheus' 22 | 23 | # Override the global default and scrape targets from this job every 5 seconds. 24 | scrape_interval: 5s 25 | 26 | # metrics_path defaults to '/metrics' 27 | # scheme defaults to 'http'. 28 | 29 | static_configs: 30 | - targets: ['localhost:9090'] 31 | 32 | - job_name: 'r8limiter' 33 | metrics_path: /metrics 34 | scrape_interval: 5s 35 | static_configs: 36 | - targets: ['localhost:8082'] 37 | -------------------------------------------------------------------------------- /pkg/transport/http/middleware.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/urfave/negroni" 10 | ) 11 | 12 | type metricsMiddleware struct { 13 | requestCounter *prometheus.CounterVec 14 | requestLatency *prometheus.HistogramVec 15 | } 16 | 17 | func NewMetricsMiddleware(registerer prometheus.Registerer) *metricsMiddleware { 18 | // metrics 19 | requestCounter := prometheus.NewCounterVec( 20 | prometheus.CounterOpts{ 21 | Name: "http_request_total", 22 | Help: "Total http requests counter", 23 | }, 24 | []string{"handler", "method", "status"}) 25 | 26 | requestLatency := prometheus.NewHistogramVec( 27 | prometheus.HistogramOpts{ 28 | Name: "http_request_duration_seconds", 29 | Help: "Duration of the http requests", 30 | Buckets: prometheus.ExponentialBuckets(0.00005, 2, 12), 31 | }, 32 | []string{"handler", "method", "status"}) 33 | 34 | registerer.MustRegister(requestCounter, requestLatency) 35 | 36 | return &metricsMiddleware{ 37 | requestCounter: requestCounter, 38 | requestLatency: requestLatency, 39 | } 40 | } 41 | 42 | func (m *metricsMiddleware) Handler(handler string, next http.HandlerFunc) http.HandlerFunc { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | start := time.Now() 45 | 46 | ww := negroni.NewResponseWriter(w) 47 | next.ServeHTTP(ww, r) 48 | 49 | status := strconv.Itoa(ww.Status()) 50 | m.requestCounter.WithLabelValues(handler, r.Method, status).Inc() 51 | m.requestLatency.WithLabelValues(handler, r.Method, status).Observe(time.Since(start).Seconds()) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samueltorres/r8limiter 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 7 | github.com/cncf/udpa/go v0.0.0-20191230090109-edbea6a78f6d // indirect 8 | github.com/envoyproxy/go-control-plane v0.9.2 9 | github.com/fsnotify/fsnotify v1.4.7 10 | github.com/go-redis/redis v6.15.7+incompatible 11 | github.com/go-redis/redis/v7 v7.0.0-beta.5 12 | github.com/google/go-cmp v0.4.0 // indirect 13 | github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect 14 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 15 | github.com/julienschmidt/httprouter v1.3.0 16 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 17 | github.com/kr/pretty v0.2.0 // indirect 18 | github.com/oklog/run v1.1.0 19 | github.com/onsi/ginkgo v1.11.0 // indirect 20 | github.com/onsi/gomega v1.8.1 // indirect 21 | github.com/peterbourgon/ff v1.7.0 22 | github.com/pkg/errors v0.9.0 23 | github.com/prometheus/client_golang v1.3.0 24 | github.com/prometheus/common v0.8.0 25 | github.com/segmentio/fasthash v1.0.1 26 | github.com/sirupsen/logrus v1.4.2 27 | github.com/smartystreets/assertions v1.0.1 // indirect 28 | github.com/spf13/afero v1.2.2 // indirect 29 | github.com/spf13/cast v1.3.1 // indirect 30 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | github.com/spf13/viper v1.6.1 33 | github.com/stretchr/testify v1.4.0 34 | github.com/urfave/negroni v1.0.0 35 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect 36 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 // indirect 37 | google.golang.org/genproto v0.0.0-20200113173426-e1de0a7b01eb // indirect 38 | google.golang.org/grpc v1.26.0 39 | gopkg.in/ini.v1 v1.51.1 // indirect 40 | gopkg.in/yaml.v2 v2.2.7 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /pkg/transport/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | _ "net/http/pprof" 7 | 8 | "github.com/julienschmidt/httprouter" 9 | "github.com/pkg/errors" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/promhttp" 12 | "github.com/samueltorres/r8limiter/pkg/limiter" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Server struct { 17 | handler *rateLimitingHandler 18 | srv *http.Server 19 | router *httprouter.Router 20 | opts options 21 | metricsRegistry *prometheus.Registry 22 | logger *logrus.Logger 23 | } 24 | 25 | func New( 26 | limiter *limiter.LimiterService, 27 | logger *logrus.Logger, 28 | metricsRegistry *prometheus.Registry, 29 | opts ...Option) *Server { 30 | 31 | options := options{} 32 | for _, o := range opts { 33 | o.apply(&options) 34 | } 35 | 36 | handler := &rateLimitingHandler{limiter: limiter, logger: logger} 37 | router := httprouter.New() 38 | 39 | server := &Server{ 40 | handler: handler, 41 | router: router, 42 | metricsRegistry: metricsRegistry, 43 | opts: options, 44 | logger: logger, 45 | srv: &http.Server{Addr: options.listen, Handler: router}, 46 | } 47 | 48 | server.registerRoutes() 49 | 50 | return server 51 | } 52 | 53 | func (s *Server) registerRoutes() { 54 | 55 | // middlewares 56 | mm := NewMetricsMiddleware(s.metricsRegistry) 57 | 58 | // rate limiting 59 | s.router.HandlerFunc(http.MethodPost, "/ratelimit", mm.Handler("/rateLimit", s.handler.handleRateLimit)) 60 | 61 | // metrics 62 | s.router.Handler(http.MethodGet, "/metrics", promhttp.HandlerFor(s.metricsRegistry, promhttp.HandlerOpts{})) 63 | 64 | // profiling 65 | s.router.Handler(http.MethodGet, "/debug/pprof/:item", http.DefaultServeMux) 66 | } 67 | 68 | func (s *Server) Start() error { 69 | s.logger.Info("listening for http address ", s.opts.listen) 70 | return errors.Wrap(s.srv.ListenAndServe(), "serve http") 71 | } 72 | 73 | func (s *Server) Stop(err error) { 74 | if err == http.ErrServerClosed { 75 | s.logger.Info("internal server closed unexpectedly") 76 | return 77 | } 78 | 79 | defer s.logger.Info("internal server shutdown", "err", err) 80 | 81 | if s.opts.gracePeriod == 0 { 82 | s.srv.Close() 83 | return 84 | } 85 | 86 | ctx, cancel := context.WithTimeout(context.Background(), s.opts.gracePeriod) 87 | defer cancel() 88 | 89 | if err := s.srv.Shutdown(ctx); err != nil { 90 | s.logger.Error("internal server shut down failed", "err", err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pkg/transport/grpc/grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "net" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/sirupsen/logrus" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials" 13 | 14 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 15 | grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 16 | "github.com/samueltorres/r8limiter/pkg/limiter" 17 | ) 18 | 19 | type Server struct { 20 | srv *grpc.Server 21 | listener net.Listener 22 | opts options 23 | limiterService *limiter.LimiterService 24 | logger *logrus.Logger 25 | } 26 | 27 | func NewServer(limiterService *limiter.LimiterService, logger *logrus.Logger, reg *prometheus.Registry, opts ...Option) *Server { 28 | options := options{} 29 | for _, o := range opts { 30 | o.apply(&options) 31 | } 32 | 33 | met := grpc_prometheus.NewServerMetrics() 34 | met.EnableHandlingTimeHistogram( 35 | grpc_prometheus.WithHistogramBuckets(prometheus.ExponentialBuckets(0.00005, 2, 12)), 36 | ) 37 | reg.MustRegister(met) 38 | 39 | grpcOpts := []grpc.ServerOption{ 40 | grpc.MaxSendMsgSize(math.MaxInt32), 41 | grpc.UnaryInterceptor( 42 | met.UnaryServerInterceptor(), 43 | ), 44 | grpc.StreamInterceptor( 45 | met.StreamServerInterceptor(), 46 | ), 47 | } 48 | 49 | if options.tlsConfig != nil { 50 | grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(options.tlsConfig))) 51 | } 52 | 53 | s := grpc.NewServer(grpcOpts...) 54 | 55 | rateLimtitServer := &Server{ 56 | limiterService: limiterService, 57 | logger: logger, 58 | opts: options, 59 | srv: s, 60 | } 61 | 62 | pb.RegisterRateLimitServiceServer(s, rateLimtitServer) 63 | met.InitializeMetrics(s) 64 | 65 | return rateLimtitServer 66 | } 67 | 68 | func (s *Server) ShouldRateLimit(ctx context.Context, req *pb.RateLimitRequest) (*pb.RateLimitResponse, error) { 69 | return s.limiterService.ShouldRateLimit(ctx, req) 70 | } 71 | 72 | func (s *Server) Start() error { 73 | l, err := net.Listen("tcp", s.opts.listen) 74 | if err != nil { 75 | return errors.Wrapf(err, "listen grpc on address %s", s.opts.listen) 76 | } 77 | s.listener = l 78 | 79 | s.logger.Info("listening for grpc address ", s.opts.listen) 80 | return errors.Wrap(s.srv.Serve(s.listener), "serve gRPC") 81 | } 82 | 83 | func (s *Server) Stop() (err error) { 84 | defer s.logger.Info("internal server shutdown", "err", err) 85 | 86 | if s.opts.gracePeriod == 0 { 87 | s.srv.Stop() 88 | return 89 | } 90 | 91 | ctx, cancel := context.WithTimeout(context.Background(), s.opts.gracePeriod) 92 | defer cancel() 93 | 94 | stopped := make(chan struct{}) 95 | go func() { 96 | s.logger.Info("gracefully stopping internal server") 97 | s.srv.GracefulStop() // Also closes s.listener. 98 | close(stopped) 99 | }() 100 | 101 | select { 102 | case <-ctx.Done(): 103 | s.logger.Info("grace period exceeded enforcing shutdown") 104 | s.srv.Stop() 105 | case <-stopped: 106 | cancel() 107 | } 108 | 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /pkg/redis/storage.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-redis/redis/v7" 8 | "github.com/pkg/errors" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/samueltorres/r8limiter/pkg/counter" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type metrics struct { 15 | incDuration prometheus.Histogram 16 | batchIncDuration prometheus.Histogram 17 | getDuration prometheus.Histogram 18 | incCount prometheus.Counter 19 | } 20 | 21 | func newMetrics(r prometheus.Registerer) *metrics { 22 | var m metrics 23 | 24 | m.incDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ 25 | Name: "redis_inc_duration_seconds", 26 | Help: "Duration of add operation", 27 | }) 28 | 29 | m.batchIncDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ 30 | Name: "redis_batchinc_duration_seconds", 31 | Help: "Duration of add operation", 32 | }) 33 | 34 | m.getDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ 35 | Name: "redis_get_duration_seconds", 36 | Help: "Duration of get operation", 37 | }) 38 | 39 | m.incCount = prometheus.NewCounter(prometheus.CounterOpts{ 40 | Name: "redis_inc_total", 41 | Help: "Number of counter increments in total, including batch", 42 | }) 43 | 44 | r.MustRegister(m.incDuration, m.batchIncDuration, m.getDuration, m.incCount) 45 | return &m 46 | } 47 | 48 | type Storage struct { 49 | client *redis.Client 50 | logger *logrus.Logger 51 | metrics *metrics 52 | } 53 | 54 | func NewStorage(client *redis.Client, logger *logrus.Logger, registerer prometheus.Registerer) *Storage { 55 | metrics := newMetrics(registerer) 56 | 57 | return &Storage{ 58 | client: client, 59 | logger: logger, 60 | metrics: metrics, 61 | } 62 | } 63 | 64 | func (s *Storage) BatchIncrement(ctx context.Context, incrs []counter.CounterInc) ([]counter.CounterIncResponse, error) { 65 | timer := prometheus.NewTimer(s.metrics.batchIncDuration) 66 | defer func() { 67 | timer.ObserveDuration() 68 | }() 69 | 70 | incrCmds := make([]*redis.IntCmd, len(incrs)) 71 | 72 | pipe := s.client.Pipeline() 73 | for i, increment := range incrs { 74 | incrCmds[i] = pipe.IncrBy(increment.Key, int64(increment.Inc)) 75 | pipe.ExpireAt(increment.Key, time.Unix(increment.TTL+2, 0)) 76 | } 77 | 78 | c := make(chan error, 1) 79 | go func() { 80 | _, err := pipe.Exec() 81 | c <- err 82 | }() 83 | 84 | select { 85 | case <-ctx.Done(): 86 | return nil, ctx.Err() 87 | case err := <-c: 88 | if err != nil { 89 | return nil, err 90 | } 91 | } 92 | 93 | incrResponses := make([]counter.CounterIncResponse, len(incrs)) 94 | for i, cmd := range incrCmds { 95 | 96 | incrResponses[i] = counter.CounterIncResponse{ 97 | Key: incrs[i].Key, 98 | } 99 | 100 | res, err := cmd.Result() 101 | if err != nil { 102 | incrResponses[i].Err = errors.Wrap(err, "redis storage pipeline command failure") 103 | } 104 | 105 | incrResponses[i].Curr = uint32(res) 106 | } 107 | 108 | s.metrics.incCount.Add(float64(len(incrs))) 109 | 110 | return incrResponses, nil 111 | } 112 | 113 | func (s *Storage) Increment(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) { 114 | timer := prometheus.NewTimer(s.metrics.incDuration) 115 | defer func() { 116 | timer.ObserveDuration() 117 | }() 118 | 119 | pipe := s.client.Pipeline() 120 | incrCmd := pipe.IncrBy(key, int64(n)) 121 | pipe.ExpireAt(key, time.Unix(ttl+2, 0)) 122 | 123 | _, err := pipe.Exec() 124 | if err != nil { 125 | return 0, errors.Wrap(err, "redis storage pipeline failure") 126 | } 127 | 128 | res, err := incrCmd.Result() 129 | if err != nil { 130 | return 0, errors.Wrap(err, "redis storage pipeline command failure") 131 | } 132 | 133 | s.metrics.incCount.Add(1) 134 | 135 | return uint32(res), nil 136 | } 137 | 138 | func (s *Storage) Get(ctx context.Context, key string) (uint32, error) { 139 | timer := prometheus.NewTimer(s.metrics.getDuration) 140 | defer func() { 141 | timer.ObserveDuration() 142 | }() 143 | 144 | c, err := s.client.Get(key).Int64() 145 | if err != nil { 146 | return 0, errors.Wrap(err, "redis storage get failure "+key) 147 | } 148 | 149 | return uint32(c), nil 150 | } 151 | -------------------------------------------------------------------------------- /cmd/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "math/rand" 12 | "net/http" 13 | "strconv" 14 | "sync" 15 | 16 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 17 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 18 | "google.golang.org/grpc" 19 | ) 20 | 21 | func main() { 22 | var ( 23 | grpcAddr = flag.String("grpc-addr", "localhost:8081", "grpc address") 24 | httpAddr = flag.String("http-addr", "localhost:8082", "http address") 25 | transport = flag.String("transport", "grpc", "type of transport (grpc/http)") 26 | routines = flag.Int("routines", 20, "number of goroutines doing calls concurrently") 27 | ) 28 | flag.Parse() 29 | 30 | cancel := make(chan struct{}) 31 | wg := &sync.WaitGroup{} 32 | 33 | if *transport == "grpc" { 34 | wg.Add(1) 35 | go runGRPC(wg, cancel, *grpcAddr, *routines) 36 | } else if *transport == "http" { 37 | wg.Add(1) 38 | go runHTTP(wg, cancel, *httpAddr, *routines) 39 | } 40 | 41 | var input string 42 | fmt.Scanln(&input) 43 | close(cancel) 44 | wg.Wait() 45 | 46 | log.Printf("finished") 47 | } 48 | 49 | // ---------------- gRPC ----------------- 50 | 51 | func runGRPC(wg *sync.WaitGroup, cancel chan struct{}, addr string, routines int) { 52 | defer wg.Done() 53 | 54 | var conn *grpc.ClientConn 55 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 56 | if err != nil { 57 | log.Fatalf("did not connect: %s", err) 58 | } 59 | defer conn.Close() 60 | client := pb.NewRateLimitServiceClient(conn) 61 | rwg := &sync.WaitGroup{} 62 | 63 | for i := 0; i < routines; i++ { 64 | rwg.Add(1) 65 | go grpcCaller(rwg, client, cancel) 66 | } 67 | rwg.Wait() 68 | } 69 | 70 | func grpcCaller(wg *sync.WaitGroup, client pb.RateLimitServiceClient, cancel chan struct{}) { 71 | for { 72 | select { 73 | case <-cancel: 74 | return 75 | default: 76 | } 77 | 78 | request := newRateLimitRequest() 79 | response, err := client.ShouldRateLimit(context.Background(), request) 80 | if err != nil { 81 | fmt.Printf("response error: %v", err) 82 | } else { 83 | fmt.Println(response) 84 | } 85 | } 86 | } 87 | 88 | // ---------------- HTTP ----------------- 89 | 90 | func runHTTP(wg *sync.WaitGroup, cancel chan struct{}, addr string, routines int) { 91 | defer wg.Done() 92 | 93 | client := &http.Client{} 94 | 95 | rwg := &sync.WaitGroup{} 96 | for i := 0; i < routines; i++ { 97 | rwg.Add(1) 98 | go httpCaller(rwg, client, addr, cancel) 99 | } 100 | rwg.Wait() 101 | } 102 | 103 | func httpCaller(wg *sync.WaitGroup, client *http.Client, addr string, cancel chan struct{}) { 104 | defer wg.Done() 105 | reqURL := "http://" + addr + "/rateLimit" 106 | 107 | for { 108 | select { 109 | case <-cancel: 110 | return 111 | default: 112 | } 113 | 114 | request := newRateLimitRequest() 115 | requestBody, err := json.Marshal(request) 116 | if err != nil { 117 | fmt.Printf("could not marshall http request body error: %v", err) 118 | continue 119 | } 120 | 121 | httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(requestBody)) 122 | httpReq.Header.Set("Content-Type", "application/json") 123 | 124 | resp, err := client.Do(httpReq) 125 | if err != nil { 126 | fmt.Printf("http response error: %v", err) 127 | continue 128 | } 129 | 130 | body, err := ioutil.ReadAll(resp.Body) 131 | resp.Body.Close() 132 | if err != nil { 133 | fmt.Printf("http response error: %v", err) 134 | continue 135 | } 136 | 137 | ratelimitResponse := &pb.RateLimitResponse{} 138 | err = json.Unmarshal(body, ratelimitResponse) 139 | if err != nil { 140 | fmt.Printf("could not unmarshall http response body error: %v", err) 141 | } 142 | 143 | fmt.Println(ratelimitResponse) 144 | } 145 | } 146 | 147 | // ---------------- Request Builder ----------------- 148 | 149 | func newRateLimitRequest() *pb.RateLimitRequest { 150 | tenant := rand.Intn(10000000) 151 | 152 | return &pb.RateLimitRequest{ 153 | Domain: "kong", 154 | HitsAddend: 1, 155 | Descriptors: []*rl.RateLimitDescriptor{ 156 | { 157 | Entries: []*rl.RateLimitDescriptor_Entry{ 158 | { 159 | Key: "tenant_id", 160 | Value: strconv.Itoa(tenant), 161 | }, 162 | }, 163 | }, 164 | }, 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pkg/file/rules_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "testing" 5 | 6 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 7 | "github.com/samueltorres/r8limiter/pkg/limiter" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func BenchmarkGetRatelimitRule(b *testing.B) { 12 | rs, _ := NewRuleService("testdata/generic.yaml") 13 | 14 | descriptor := &rl.RateLimitDescriptor{ 15 | Entries: []*rl.RateLimitDescriptor_Entry{ 16 | { 17 | Key: "authorized", 18 | Value: "true", 19 | }, 20 | { 21 | Key: "tenant", 22 | Value: "10000", 23 | }, 24 | }, 25 | } 26 | 27 | for i := 0; i < b.N; i++ { 28 | rs.GetRatelimitRule("api-gateway", descriptor) 29 | } 30 | } 31 | 32 | func TestGetRatelimitRule(t *testing.T) { 33 | testCases := []struct { 34 | desc string 35 | file string 36 | domain string 37 | descriptor rl.RateLimitDescriptor 38 | want *limiter.Limit 39 | wantErr error 40 | }{ 41 | { 42 | desc: "tc1", 43 | file: "testdata/generic.yaml", 44 | domain: "api-gateway", 45 | descriptor: rl.RateLimitDescriptor{ 46 | Entries: []*rl.RateLimitDescriptor_Entry{ 47 | { 48 | Key: "authorized", 49 | Value: "true", 50 | }, 51 | { 52 | Key: "user_id", 53 | Value: "1234", 54 | }, 55 | }, 56 | }, 57 | want: &limiter.Limit{ 58 | Unit: "hour", 59 | Requests: 1, 60 | }, 61 | wantErr: nil, 62 | }, 63 | { 64 | desc: "tc2", 65 | file: "testdata/generic.yaml", 66 | domain: "api-gateway", 67 | descriptor: rl.RateLimitDescriptor{ 68 | Entries: []*rl.RateLimitDescriptor_Entry{ 69 | { 70 | Key: "authorized", 71 | Value: "false", 72 | }, 73 | { 74 | Key: "user_id", 75 | Value: "1234", 76 | }, 77 | }, 78 | }, 79 | want: &limiter.Limit{ 80 | Unit: "hour", 81 | Requests: 2, 82 | }, 83 | wantErr: nil, 84 | }, 85 | { 86 | desc: "tc3", 87 | file: "testdata/generic.yaml", 88 | domain: "api-gateway", 89 | descriptor: rl.RateLimitDescriptor{ 90 | Entries: []*rl.RateLimitDescriptor_Entry{ 91 | { 92 | Key: "user_id", 93 | Value: "1234", 94 | }, 95 | }, 96 | }, 97 | want: &limiter.Limit{ 98 | Unit: "hour", 99 | Requests: 3, 100 | }, 101 | wantErr: nil, 102 | }, 103 | { 104 | desc: "tc4", 105 | file: "testdata/generic.yaml", 106 | domain: "api-gateway", 107 | descriptor: rl.RateLimitDescriptor{ 108 | Entries: []*rl.RateLimitDescriptor_Entry{ 109 | { 110 | Key: "authorized", 111 | Value: "true", 112 | }, 113 | { 114 | Key: "tenant", 115 | Value: "10000", 116 | }, 117 | }, 118 | }, 119 | want: &limiter.Limit{ 120 | Unit: "hour", 121 | Requests: 4, 122 | }, 123 | wantErr: nil, 124 | }, 125 | { 126 | desc: "tc5", 127 | file: "testdata/generic.yaml", 128 | domain: "api-gateway", 129 | descriptor: rl.RateLimitDescriptor{ 130 | Entries: []*rl.RateLimitDescriptor_Entry{ 131 | { 132 | Key: "authorized", 133 | Value: "false", 134 | }, 135 | { 136 | Key: "tenant", 137 | Value: "10000", 138 | }, 139 | }, 140 | }, 141 | want: &limiter.Limit{ 142 | Unit: "hour", 143 | Requests: 5, 144 | }, 145 | wantErr: nil, 146 | }, 147 | { 148 | desc: "tc6", 149 | file: "testdata/generic.yaml", 150 | domain: "api-gateway", 151 | descriptor: rl.RateLimitDescriptor{ 152 | Entries: []*rl.RateLimitDescriptor_Entry{ 153 | { 154 | Key: "random", 155 | Value: "value", 156 | }, 157 | }, 158 | }, 159 | want: nil, 160 | wantErr: limiter.ErrNoMatchedRule, 161 | }, 162 | { 163 | desc: "tc7", 164 | file: "testdata/generic.yaml", 165 | domain: "api-gateway", 166 | descriptor: rl.RateLimitDescriptor{ 167 | Entries: []*rl.RateLimitDescriptor_Entry{ 168 | { 169 | Key: "authorized", 170 | Value: "true", 171 | }, 172 | }, 173 | }, 174 | want: nil, 175 | wantErr: limiter.ErrNoMatchedRule, 176 | }, 177 | } 178 | for _, tC := range testCases { 179 | t.Run(tC.desc, func(t *testing.T) { 180 | 181 | rs, err := NewRuleService(tC.file) 182 | if err != nil { 183 | assert.FailNow(t, "could not initialize rule service", err) 184 | } 185 | 186 | got, err := rs.GetRatelimitRule(tC.domain, &tC.descriptor) 187 | 188 | if tC.wantErr != err { 189 | t.Errorf("got %v, want %v", err, tC.wantErr) 190 | } 191 | 192 | if tC.want == nil { 193 | if got != nil { 194 | t.Errorf("got %v, want nil", got) 195 | } 196 | return 197 | } 198 | 199 | if tC.want.Requests != got.Limit.Requests { 200 | t.Errorf("got %v, want %v", got.Limit.Requests, tC.want.Requests) 201 | } 202 | 203 | if tC.want.Unit != got.Limit.Unit { 204 | t.Errorf("got %v, want %v", got.Limit.Unit, tC.want.Unit) 205 | } 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | rediscli "github.com/go-redis/redis/v7" 12 | "github.com/oklog/run" 13 | "github.com/peterbourgon/ff" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/common/version" 16 | "github.com/samueltorres/r8limiter/pkg/configs" 17 | "github.com/samueltorres/r8limiter/pkg/counter" 18 | "github.com/samueltorres/r8limiter/pkg/file" 19 | "github.com/samueltorres/r8limiter/pkg/limiter" 20 | "github.com/samueltorres/r8limiter/pkg/redis" 21 | "github.com/samueltorres/r8limiter/pkg/transport/grpc" 22 | "github.com/samueltorres/r8limiter/pkg/transport/http" 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | func main() { 27 | // configs 28 | config := parseConfig() 29 | 30 | //logging 31 | logger := createLogger(config) 32 | 33 | // metrics 34 | metrics := prometheus.NewRegistry() 35 | metrics.MustRegister( 36 | version.NewCollector("r8limiter"), 37 | prometheus.NewGoCollector(), 38 | prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}), 39 | ) 40 | 41 | // rate limiter service 42 | ruleService, err := file.NewRuleService(config.RulesFile) 43 | if err != nil { 44 | logger.Fatalf("error creating rule service: %v", err) 45 | } 46 | 47 | remoteCounterStorage, err := createCounterStorage(config, logger, metrics) 48 | if err != nil { 49 | logger.Fatalf("could not create remote counter storage: %v", err) 50 | } 51 | 52 | counterService := counter.NewCounterService(remoteCounterStorage, logger, metrics, config.SyncBatchSize) 53 | limiterService := limiter.NewLimiterService(ruleService, counterService, logger, metrics) 54 | 55 | cancel := make(chan struct{}) 56 | 57 | var g run.Group 58 | { 59 | g.Add(func() error { 60 | return counterService.RunSync(cancel) 61 | }, func(error) {}) 62 | } 63 | { 64 | limiterGrpcServer := grpc.NewServer( 65 | limiterService, 66 | logger, 67 | metrics, 68 | grpc.WithListen(config.GrpcAddr), 69 | grpc.WithGracePeriod(config.ShutdownGracePeriod)) 70 | 71 | g.Add(func() error { 72 | return limiterGrpcServer.Start() 73 | }, func(error) { 74 | limiterGrpcServer.Stop() 75 | }) 76 | } 77 | { 78 | limiterHTTPServer := http.New( 79 | limiterService, 80 | logger, 81 | metrics, 82 | http.WithListen(config.HttpAddr), 83 | http.WithGracePeriod(config.ShutdownGracePeriod)) 84 | 85 | g.Add(func() error { 86 | return limiterHTTPServer.Start() 87 | }, func(err error) { 88 | limiterHTTPServer.Stop(err) 89 | }) 90 | } 91 | { 92 | g.Add(func() error { 93 | c := make(chan os.Signal, 1) 94 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 95 | sig := <-c 96 | return fmt.Errorf("received signal %s", sig) 97 | 98 | }, func(error) { 99 | close(cancel) 100 | }) 101 | } 102 | 103 | logger.Info("exit", g.Run()) 104 | } 105 | 106 | func parseConfig() configs.Config { 107 | fs := flag.NewFlagSet("r8limiter", flag.ExitOnError) 108 | var ( 109 | grpcAddress = fs.String("grpc-addr", ":8081", "grpc address") 110 | httpAddress = fs.String("http-addr", ":8082", "http address") 111 | debugAddress = fs.String("debug-addr", ":8083", "debug address for metrics and healthcheck") 112 | redisAddress = fs.String("redis-address", "", "redis address") 113 | redisDatabase = fs.Int("redis-database", 0, "redis database") 114 | redisPassword = fs.String("redis-password", "", "redis password") 115 | rulesFile = fs.String("rules-file", "/env/rules.yaml", "rules file") 116 | logLevel = fs.String("log-level", "info", "log level (panic, fatal, error, warn, info, debug, trace)") 117 | syncBatchSize = fs.Int("sync-batch-size", 1000, "number of counters to sync in each batch") 118 | shutdownGracePeriod = fs.Duration("shutdown-grace-period", 30*time.Second, "shutdown grace period for grpc and http servers") 119 | ) 120 | ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("R8")) 121 | 122 | var config configs.Config 123 | { 124 | config.GrpcAddr = *grpcAddress 125 | config.HttpAddr = *httpAddress 126 | config.DebugAddr = *debugAddress 127 | config.Redis.Address = *redisAddress 128 | config.Redis.Database = *redisDatabase 129 | config.Redis.Password = *redisPassword 130 | config.RulesFile = *rulesFile 131 | config.LogLevel = *logLevel 132 | config.SyncBatchSize = *syncBatchSize 133 | config.ShutdownGracePeriod = *shutdownGracePeriod 134 | } 135 | 136 | return config 137 | } 138 | 139 | func createLogger(config configs.Config) *logrus.Logger { 140 | logger := logrus.StandardLogger() 141 | logger.SetFormatter(&logrus.JSONFormatter{}) 142 | 143 | level, err := logrus.ParseLevel(config.LogLevel) 144 | if err != nil { 145 | level = logrus.ErrorLevel 146 | } 147 | 148 | logger.Infof("setting log level to %v", level) 149 | logger.SetLevel(level) 150 | 151 | return logger 152 | } 153 | 154 | func createCounterStorage(config configs.Config, logger *logrus.Logger, metrics prometheus.Registerer) (counter.CounterStorage, error) { 155 | redisClient := rediscli.NewClient(&rediscli.Options{ 156 | Addr: config.Redis.Address, 157 | Password: config.Redis.Password, 158 | DB: config.Redis.Database, 159 | }) 160 | 161 | _, err := redisClient.Ping().Result() 162 | if err != nil { 163 | return nil, fmt.Errorf("could not connect to redis : %w", err) 164 | } 165 | 166 | return redis.NewStorage(redisClient, logger, metrics), nil 167 | } 168 | -------------------------------------------------------------------------------- /pkg/limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 11 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type metrics struct { 17 | okResp prometheus.Counter 18 | limitedResp prometheus.Counter 19 | unkownResp prometheus.Counter 20 | } 21 | 22 | func newMetrics(r prometheus.Registerer) *metrics { 23 | var m metrics 24 | 25 | m.okResp = prometheus.NewCounter(prometheus.CounterOpts{ 26 | Name: "ratelimit_ok_responses", 27 | Help: "Total ok responses", 28 | }) 29 | 30 | m.limitedResp = prometheus.NewCounter(prometheus.CounterOpts{ 31 | Name: "ratelimit_limited_requests", 32 | Help: "Total limited responses", 33 | }) 34 | 35 | m.unkownResp = prometheus.NewCounter(prometheus.CounterOpts{ 36 | Name: "ratelimit_unknown_requests", 37 | Help: "Total unknown responses", 38 | }) 39 | 40 | r.MustRegister(m.okResp, m.limitedResp, m.unkownResp) 41 | return &m 42 | } 43 | 44 | type LimiterService struct { 45 | rulesService RulesService 46 | counterService CounterService 47 | logger *logrus.Logger 48 | metrics *metrics 49 | } 50 | 51 | func NewLimiterService( 52 | rulesService RulesService, 53 | counterService CounterService, 54 | logger *logrus.Logger, 55 | registerer prometheus.Registerer) *LimiterService { 56 | 57 | metrics := newMetrics(registerer) 58 | 59 | return &LimiterService{ 60 | rulesService: rulesService, 61 | counterService: counterService, 62 | logger: logger, 63 | metrics: metrics, 64 | } 65 | } 66 | 67 | func (l *LimiterService) ShouldRateLimit(ctx context.Context, req *pb.RateLimitRequest) (response *pb.RateLimitResponse, err error) { 68 | defer func() { 69 | if err != nil { 70 | return 71 | } 72 | 73 | switch response.OverallCode { 74 | case pb.RateLimitResponse_OVER_LIMIT: 75 | l.metrics.limitedResp.Inc() 76 | case pb.RateLimitResponse_OK: 77 | l.metrics.okResp.Inc() 78 | case pb.RateLimitResponse_UNKNOWN: 79 | l.metrics.unkownResp.Inc() 80 | } 81 | }() 82 | 83 | response = &pb.RateLimitResponse{ 84 | Statuses: make([]*pb.RateLimitResponse_DescriptorStatus, len(req.Descriptors)), 85 | } 86 | 87 | for i, desc := range req.Descriptors { 88 | response.Statuses[i] = &pb.RateLimitResponse_DescriptorStatus{ 89 | CurrentLimit: &pb.RateLimitResponse_RateLimit{}, 90 | } 91 | 92 | rule, err := l.rulesService.GetRatelimitRule(req.Domain, desc) 93 | if err != nil { 94 | if err == ErrNoMatchedRule { 95 | continue 96 | } 97 | } 98 | 99 | windowSize := timeUnitToWindowSize(rule.Limit.Unit) 100 | now := time.Now().Unix() 101 | 102 | // service does not allow 0 hits addend 103 | if req.HitsAddend == 0 { 104 | req.HitsAddend = 1 105 | } 106 | 107 | // get current bucket usage 108 | var currUsage uint32 109 | { 110 | key := generateKey(desc, rule, now/windowSize) 111 | 112 | // current usage must be available 2 buckets later for interpolation 113 | ttl := now + (windowSize * 2) 114 | 115 | if rule.SyncRate == 0 { 116 | currUsage, err = l.counterService.IncrementOnStorage(ctx, key, req.HitsAddend, ttl) 117 | if err != nil { 118 | return nil, err 119 | } 120 | } else { 121 | currUsage, err = l.counterService.Increment(ctx, key, req.HitsAddend, ttl, rule.SyncRate) 122 | if err != nil { 123 | return nil, err 124 | } 125 | } 126 | } 127 | 128 | // get previous bucket usage 129 | var previousUsage uint32 130 | { 131 | key := generateKey(desc, rule, (now/windowSize)-1) 132 | if rule.SyncRate == 0 { 133 | previousUsage, _ = l.counterService.GetFromStorage(ctx, key) 134 | } else { 135 | previousUsage, _ = l.counterService.Get(ctx, key) 136 | } 137 | } 138 | 139 | // calculate rate 140 | weight := float64(windowSize-(now%windowSize)) / float64(windowSize) 141 | rate := currUsage + uint32(float64(previousUsage)*weight) 142 | 143 | response.Statuses[i].CurrentLimit.RequestsPerUnit = rule.Limit.Requests 144 | response.Statuses[i].CurrentLimit.Unit = timeUnitToPb(rule.Limit.Unit) 145 | 146 | if rate > rule.Limit.Requests { 147 | response.OverallCode = pb.RateLimitResponse_OVER_LIMIT 148 | response.Statuses[i].Code = pb.RateLimitResponse_OVER_LIMIT 149 | response.Statuses[i].LimitRemaining = 0 150 | } else { 151 | // if its already over-limit, we shouldnt tag it as ok 152 | if response.OverallCode != pb.RateLimitResponse_OVER_LIMIT { 153 | response.OverallCode = pb.RateLimitResponse_OK 154 | } 155 | 156 | response.Statuses[i].Code = pb.RateLimitResponse_OK 157 | response.Statuses[i].LimitRemaining = rule.Limit.Requests - currUsage 158 | } 159 | } 160 | 161 | return response, nil 162 | } 163 | 164 | func generateKey(desc *rl.RateLimitDescriptor, rule *Rule, timeValue int64) string { 165 | usedLimitDescriptorLabels := make(map[string]bool) 166 | for _, label := range rule.Labels { 167 | usedLimitDescriptorLabels[label.Key] = true 168 | } 169 | 170 | descriptorKeyValues := make([]string, 0, len(desc.Entries)) 171 | for _, entry := range desc.Entries { 172 | if _, exists := usedLimitDescriptorLabels[entry.Key]; exists { 173 | descriptorKeyValues = append(descriptorKeyValues, entry.Key+"."+entry.Value) 174 | } 175 | } 176 | sort.Strings(descriptorKeyValues) 177 | 178 | return strings.Join(descriptorKeyValues, "_") + ":" + rule.Limit.Unit + ":" + strconv.FormatInt(timeValue, 10) 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r8limiter 2 | 3 | r8limiter is an Envoy pluggable RateLimit gRPC service. It can serve multiple purposes, e.g - API Gateways, Database accesses, Service-to-Service communications etc. It offers a totally configurable API, in which you can define the rules by which a request can be limited. 4 | This service uses the sliding window rate limiting algorithm, with asynchronous or synchronous data replication, or even an in-mem rate limiter. 5 | 6 | #### CI Status 7 | [![CircleCI](https://circleci.com/gh/samueltorres/r8limiter.svg?style=svg)](https://circleci.com/gh/samueltorres/r8limiter) 8 | 9 | 10 | # Installation 11 | ``` 12 | go get github.com/samueltorres/r8limiter 13 | ``` 14 | 15 | # Usage 16 | ``` 17 | Usage of r8limiter: 18 | -debug-addr string 19 | debug address for metrics and healthcheck (default ":8083") 20 | -grpc-addr string 21 | grpc address (default ":8081") 22 | -http-addr string 23 | http address (default ":8082") 24 | -log-level string 25 | log level (panic, fatal, error, warn, info, debug, trace) (default "info") 26 | -redis-address string 27 | redis address 28 | -redis-database int 29 | redis database 30 | -redis-password string 31 | redis password 32 | -rules-file string 33 | rules file (default "/env/rules.yaml") 34 | -shutdown-grace-period duration 35 | shutdown grace period for grpc and http servers (default 30s) 36 | -sync-batch-size int 37 | number of counters to sync in each batch (default 1000) 38 | ``` 39 | 40 | # Rules Configuration 41 | Rate limiting rules are being described in yaml format. 42 | 43 | Please look at the following example, in which we are limiting any authenticated user to make 500 requests per hour, all the other users can do 1000 per hour. 44 | 45 | ```yaml 46 | domains: 47 | - domain: envoy 48 | rules: 49 | - name: authenticated users 50 | labels: 51 | - key: authenticated 52 | value: "true" 53 | - key: user_id 54 | limit: 55 | unit: hour 56 | requests: 500 57 | syncRate: 1 58 | 59 | - name: any user 60 | labels: 61 | - key: user_id 62 | limit: 63 | unit: hour 64 | requests: 1000 65 | syncRate: -1 66 | ``` 67 | ### Synchronization 68 | 69 | This implementation offers you the possibility of configuring the synchronization rate for a given rule. 70 | 71 | | sync rate | | 72 | |-----------|-------------------------------------------------------| 73 | | -1 | no synchronization | 74 | | 0 (default) | all rate limit requests go through the remote storage | 75 | | [1-n] | number of seconds between synchronizations | 76 | 77 | Sometimes the rate limiter needs to be strict e.g number of requests per client tier on an enterprise offering. 78 | 79 | ```yaml 80 | domains: 81 | - domain: api-gateway 82 | rules: 83 | - name: starter users 84 | labels: 85 | - key: user_type 86 | value: starter 87 | limit: 88 | unit: minute 89 | requests: 500 90 | syncRate: 0 91 | - name: premium users 92 | labels: 93 | - key: user_type 94 | value: premium 95 | limit: 96 | unit: minute 97 | requests: 20000 98 | syncRate: 0 99 | ``` 100 | 101 | In other use cases we can be very lenient in the number of requests, like protecting a server from DDOS attacks, and the in-mem rate limiter will be sufficient. 102 | 103 | ```yaml 104 | domains: 105 | - domain: api-gateway 106 | rules: 107 | - name: ddos protection 108 | labels: 109 | - key: ip_address 110 | limit: 111 | unit: second 112 | requests: 5000 113 | syncRate: -1 114 | - name: tenant 115 | labels: 116 | - key: tenant 117 | limit: 118 | unit: minute 119 | requests: 50000 120 | syncRate: 3 121 | ``` 122 | 123 | If we want to be a little bit strict, we can have an in-mem rate limiter that synchronizes the counters with the remote storage after n seconds, thereforew we are sure that all instances of the rate limiter have. 124 | 125 | ```yaml 126 | domains: 127 | - domain: api-gateway 128 | rules: 129 | - name: tenant 130 | labels: 131 | - key: tenant 132 | limit: 133 | unit: minute 134 | requests: 50000 135 | syncRate: 3 136 | ``` 137 | 138 | ### Other examples 139 | This configuration schema allows you to configure different rate limits configurations for each kind use case you'll need, e.g - limiting mongodb accesses to 10000 reqs/hour, limiting Portuguese users to 20 reqs/min etc. 140 | 141 | ```yaml 142 | domains: 143 | - domain: api-gateway 144 | rules: 145 | - name: any user 146 | labels: 147 | - key: country 148 | value: PT 149 | - key: user_id 150 | limit: 151 | unit: minute 152 | requests: 20 153 | - domain: mongodb 154 | rules: 155 | - name: any request 156 | labels: 157 | - key: generic 158 | limit: 159 | unit: hour 160 | requests: 10000 161 | ``` 162 | 163 | # Remote Storages 164 | When synchronization is needed, a remote storage must be provided, currently it only supports Redis. 165 | 166 | # Run locally 167 | 168 | Spin up environment using docker-compose 169 | 170 | ```bash 171 | docker-compose up 172 | ``` 173 | 174 | Spin up the r8limiter server 175 | 176 | ```bash 177 | go run cmd/server/server.go --rules-file ./env/rules.yaml 178 | ``` 179 | 180 | Spin up a test client 181 | ```bash 182 | go run cmd/client/client.go 183 | ``` 184 | 185 | # License 186 | This project is licensed under the MIT open source license. 187 | -------------------------------------------------------------------------------- /pkg/file/rules.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "sync" 8 | 9 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 10 | "github.com/fsnotify/fsnotify" 11 | "github.com/pkg/errors" 12 | "github.com/samueltorres/r8limiter/pkg/limiter" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | type RulesService struct { 17 | mux *sync.RWMutex 18 | viper *viper.Viper 19 | rulesConfig *limiter.RulesConfig 20 | rulesIndex map[string][]*limiter.Rule 21 | ruleCount int 22 | } 23 | 24 | func NewRuleService(file string) (*RulesService, error) { 25 | if file == "" { 26 | return nil, errors.Errorf("rules file must be provided") 27 | } 28 | 29 | v := viper.New() 30 | v.SetConfigFile(file) 31 | v.SetConfigType("yaml") 32 | 33 | err := v.ReadInConfig() 34 | if err != nil { 35 | return nil, errors.Wrap(err, "error reading in rule file config") 36 | } 37 | 38 | fc := &RulesService{ 39 | viper: v, 40 | mux: &sync.RWMutex{}, 41 | } 42 | 43 | err = fc.loadRules() 44 | if err != nil { 45 | return nil, errors.Wrap(err, "error loading rules") 46 | } 47 | 48 | v.WatchConfig() 49 | v.OnConfigChange(func(e fsnotify.Event) { 50 | fmt.Println("Config file changed:", e.Name) 51 | err := fc.loadRules() 52 | if err != nil { 53 | fmt.Println(err) 54 | } 55 | }) 56 | 57 | return fc, nil 58 | } 59 | 60 | func (rs *RulesService) GetRatelimitRule(domain string, rateLimitDescriptor *rl.RateLimitDescriptor) (*limiter.Rule, error) { 61 | rs.mux.RLock() 62 | defer rs.mux.RUnlock() 63 | 64 | ruleMatchCount := make(map[*limiter.Rule]int, rs.ruleCount) 65 | 66 | // 1. find possible matches 67 | for _, entry := range rateLimitDescriptor.Entries { 68 | // 1.1 descriptors that contain a key 69 | key := domain + "." + entry.Key 70 | if descriptors, ok := rs.rulesIndex[key]; ok { 71 | for _, desc := range descriptors { 72 | ruleMatchCount[desc]++ 73 | } 74 | } 75 | 76 | // 1.2 descriptors that contain a key & value 77 | key = domain + "." + entry.Key + "." + entry.Value 78 | if descriptors, ok := rs.rulesIndex[key]; ok { 79 | for _, desc := range descriptors { 80 | ruleMatchCount[desc]++ 81 | } 82 | } 83 | } 84 | 85 | if len(ruleMatchCount) == 0 { 86 | return nil, limiter.ErrNoMatchedRule 87 | } 88 | 89 | // 2. filter out matches 90 | type rankedMatch struct { 91 | rule *limiter.Rule 92 | count int 93 | } 94 | 95 | // todo: #performance rankedMatches is escaping to the heap, please review later 96 | rankedMatches := make([]rankedMatch, 0, len(ruleMatchCount)) 97 | requestDescriptorLabels := make(map[string]bool) 98 | for _, label := range rateLimitDescriptor.Entries { 99 | requestDescriptorLabels[label.Key] = true 100 | requestDescriptorLabels[label.Key+"."+label.Value] = true 101 | } 102 | 103 | for rule, count := range ruleMatchCount { 104 | // todo: add support for regex on rules here 105 | // filter out non existing labels 106 | if len(rateLimitDescriptor.Entries) >= len(rule.Labels) { 107 | descriptorEntriesValid := true 108 | for _, label := range rule.Labels { 109 | // if there's a label key not present 110 | if _, exists := requestDescriptorLabels[label.Key]; !exists { 111 | descriptorEntriesValid = false 112 | break 113 | } 114 | 115 | // if label value is specified, it must match descriptor's 116 | if label.Value != "" { 117 | if _, exists := requestDescriptorLabels[label.Key+"."+label.Value]; !exists { 118 | descriptorEntriesValid = false 119 | break 120 | } 121 | } 122 | } 123 | 124 | if descriptorEntriesValid { 125 | rankedMatches = append(rankedMatches, rankedMatch{rule, count}) 126 | } 127 | } 128 | } 129 | 130 | if len(rankedMatches) == 0 { 131 | return nil, limiter.ErrNoMatchedRule 132 | } 133 | 134 | // 2.1 sort matches by count descending 135 | sort.Slice(rankedMatches, func(i, j int) bool { 136 | return rankedMatches[i].count > rankedMatches[j].count 137 | }) 138 | 139 | // 2.2 return descriptor with matches 140 | selectedDescriptor := rankedMatches[0] 141 | maxInnerRank := rankedMatches[0].rule.InnerRank 142 | 143 | // 2.3 check for ties in matches 144 | for j := 1; j < len(rankedMatches); j++ { 145 | // if there's a tie we need to find the one with the biggest rank 146 | if selectedDescriptor.count == rankedMatches[j].count { 147 | if rankedMatches[j].rule.InnerRank > maxInnerRank { 148 | selectedDescriptor = rankedMatches[j] 149 | maxInnerRank = rankedMatches[j].rule.InnerRank 150 | } 151 | } else { 152 | return selectedDescriptor.rule, nil 153 | } 154 | } 155 | 156 | return selectedDescriptor.rule, nil 157 | } 158 | 159 | func (rs *RulesService) loadRules() error { 160 | var rulesConfig limiter.RulesConfig 161 | err := rs.viper.Unmarshal(&rulesConfig) 162 | if err != nil { 163 | return errors.Wrap(err, "error on rule config unmarshal") 164 | } 165 | 166 | err = validateRules(rulesConfig) 167 | if err != nil { 168 | return errors.Wrap(err, "rules file is invalid") 169 | } 170 | 171 | rs.mux.Lock() 172 | rs.mux.Unlock() 173 | rs.rulesConfig = &rulesConfig 174 | rs.rulesIndex, rs.ruleCount = createSearchIndex(&rulesConfig) 175 | 176 | return nil 177 | } 178 | 179 | func createSearchIndex(rc *limiter.RulesConfig) (map[string][]*limiter.Rule, int) { 180 | ruleMap := make(map[string][]*limiter.Rule) 181 | ruleCount := 0 182 | for _, domain := range rc.Domains { 183 | for _, rule := range domain.Rules { 184 | ruleCount++ 185 | 186 | for _, k := range rule.Labels { 187 | var key string 188 | if k.Value == "" { 189 | key = domain.Domain + "." + k.Key 190 | rule.InnerRank = 10 191 | } else { 192 | key = domain.Domain + "." + k.Key + "." + k.Value 193 | rule.InnerRank = 1000 194 | } 195 | 196 | _, exist := ruleMap[key] 197 | if !exist { 198 | ruleMap[key] = []*limiter.Rule{} 199 | } 200 | ruleMap[key] = append(ruleMap[key], rule) 201 | } 202 | } 203 | } 204 | 205 | return ruleMap, ruleCount 206 | } 207 | 208 | func validateRules(rulesConfig limiter.RulesConfig) error { 209 | 210 | // validate that there is at least one domain config 211 | if len(rulesConfig.Domains) == 0 { 212 | return errors.Errorf("there are no rule domain configs") 213 | } 214 | 215 | // validate domain rule configs 216 | domainMap := make(map[string]bool, len(rulesConfig.Domains)) 217 | 218 | for i, d := range rulesConfig.Domains { 219 | // validate domain name 220 | if d.Domain == "" { 221 | return errors.Errorf("invalid domain name (%d)", i) 222 | } 223 | 224 | // validate that there are no duplicated domains 225 | if _, exists := domainMap[d.Domain]; exists { 226 | return errors.Errorf("duplicated domain name (%d)", i) 227 | } 228 | domainMap[d.Domain] = true 229 | 230 | // validate that the domain has at least one rule 231 | if len(d.Rules) == 0 { 232 | return errors.Errorf("domain with no rules (%s)", d.Domain) 233 | } 234 | 235 | labelsMap := make(map[string]bool) 236 | for j, r := range d.Rules { 237 | 238 | // validate that there are no rules with the same labels 239 | labelKeyValues := make([]string, 0, len(r.Labels)) 240 | for _, label := range r.Labels { 241 | labelKeyValues = append(labelKeyValues, label.Key+"."+label.Value) 242 | } 243 | sort.Strings(labelKeyValues) 244 | labelSummary := strings.Join(labelKeyValues, ":") 245 | 246 | if _, exists := labelsMap[labelSummary]; exists { 247 | return errors.Errorf("duplicated rule labels - domain (%s) rule (%d)", d.Domain, j) 248 | } 249 | labelsMap[labelSummary] = true 250 | 251 | // validate rule limit 252 | if !validateLimitUnit(r.Limit.Unit) { 253 | return errors.Errorf("invalid rule limit unit - domain (%s) rule (%d)", d.Domain, j) 254 | } 255 | if r.Limit.Requests < 0 { 256 | return errors.Errorf("invalid rule limit request - domain (%s), rule (%d)", d.Domain, j) 257 | } 258 | } 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func validateLimitUnit(unit string) bool { 265 | switch unit { 266 | case "second": 267 | return true 268 | case "minute": 269 | return true 270 | case "hour": 271 | return true 272 | case "day": 273 | return true 274 | default: 275 | return false 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /pkg/counter/counter.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/segmentio/fasthash/fnv1a" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ErrNonExistingCounter = errors.New("counter does not exist") 16 | var ErrExpiredCounter = errors.New("counter is expired") 17 | 18 | type counter struct { 19 | current uint32 20 | stored uint32 21 | ttl int64 22 | lastSync int64 23 | syncRate int 24 | } 25 | type counterUpdate struct { 26 | shard uint64 27 | key string 28 | } 29 | 30 | type shardUpdates struct { 31 | shard uint64 32 | keys []string 33 | } 34 | 35 | type counterMetrics struct { 36 | counterSyncTotal prometheus.Counter 37 | } 38 | 39 | func newCounterMetrics(r prometheus.Registerer) *counterMetrics { 40 | var m counterMetrics 41 | 42 | m.counterSyncTotal = prometheus.NewCounter(prometheus.CounterOpts{ 43 | Name: "counter_sync_total", 44 | Help: "Total number of counter synchronizations", 45 | }) 46 | 47 | r.MustRegister(m.counterSyncTotal) 48 | return &m 49 | } 50 | 51 | // CounterService serves the purpose of storing request counters 52 | type CounterService struct { 53 | shardCount uint64 54 | shardedCounters []map[string]*counter 55 | shardedMutexes []*sync.RWMutex 56 | 57 | storage CounterStorage 58 | logger *logrus.Logger 59 | 60 | metrics *counterMetrics 61 | 62 | syncChan chan counterUpdate 63 | syncWg *sync.WaitGroup 64 | syncBatchSize int 65 | } 66 | 67 | // NewCounterService creates a new counter service 68 | func NewCounterService( 69 | storage CounterStorage, 70 | logger *logrus.Logger, 71 | registerer prometheus.Registerer, 72 | syncBatchSize int) *CounterService { 73 | metrics := newCounterMetrics(registerer) 74 | 75 | var shards uint64 = 64 76 | 77 | cm := &CounterService{ 78 | syncBatchSize: syncBatchSize, 79 | shardCount: shards, 80 | shardedCounters: make([]map[string]*counter, shards), 81 | shardedMutexes: make([]*sync.RWMutex, shards), 82 | storage: storage, 83 | logger: logger, 84 | syncChan: make(chan counterUpdate), 85 | syncWg: &sync.WaitGroup{}, 86 | metrics: metrics, 87 | } 88 | 89 | // initialize shards 90 | for i := uint64(0); i < shards; i++ { 91 | cm.shardedCounters[i] = make(map[string]*counter) 92 | cm.shardedMutexes[i] = &sync.RWMutex{} 93 | } 94 | 95 | return cm 96 | } 97 | 98 | // Increment creates or increments a given value on a counter, it also allows the counter to have a ttl 99 | // and to have a synchronization rate to a remote storage 100 | func (cs *CounterService) Increment(ctx context.Context, key string, n uint32, ttl int64, syncRate int) (uint32, error) { 101 | shard := fnv1a.HashString64(key) % cs.shardCount 102 | mux := cs.shardedMutexes[shard] 103 | mux.Lock() 104 | defer mux.Unlock() 105 | 106 | c, ok := cs.shardedCounters[shard][key] 107 | if !ok { 108 | cs.shardedCounters[shard][key] = &counter{ 109 | current: n, 110 | stored: 0, 111 | ttl: ttl, 112 | syncRate: syncRate, 113 | } 114 | return n, nil 115 | } 116 | 117 | newValue := atomic.AddUint32(&c.current, n) 118 | if syncRate != c.syncRate { 119 | c.syncRate = syncRate 120 | } 121 | return newValue + c.stored, nil 122 | } 123 | 124 | // IncrementOnStorage creates or increments a given value on a counter directly on the storage, bypassing in-memory storage 125 | func (cs *CounterService) IncrementOnStorage(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) { 126 | count, err := cs.storage.Increment(ctx, key, n, ttl) 127 | if err != nil { 128 | return 0, err 129 | } 130 | 131 | return count, nil 132 | } 133 | 134 | // Get will get the current counter value for a given key 135 | func (cs *CounterService) Get(ctx context.Context, key string) (uint32, error) { 136 | shard := fnv1a.HashString64(key) % cs.shardCount 137 | mux := cs.shardedMutexes[shard] 138 | mux.RLock() 139 | defer mux.RUnlock() 140 | 141 | c, ok := cs.shardedCounters[shard][key] 142 | if !ok { 143 | return 0, ErrNonExistingCounter 144 | } 145 | 146 | return atomic.LoadUint32(&c.current) + atomic.LoadUint32(&c.stored), nil 147 | } 148 | 149 | // GetFromStorage will get the current counter value stored on a remote storage 150 | func (cs *CounterService) GetFromStorage(ctx context.Context, key string) (uint32, error) { 151 | count, err := cs.storage.Get(ctx, key) 152 | if err != nil { 153 | return 0, err 154 | } 155 | 156 | return count, nil 157 | } 158 | 159 | // RunSync will start the syncronization process 160 | func (cs *CounterService) RunSync(cancel chan struct{}) error { 161 | 162 | // spawn shardAnalyzers 163 | analyzerCount := 8 164 | for i := 0; i < analyzerCount; i++ { 165 | cs.syncWg.Add(1) 166 | go cs.shardAnalyzer(cancel, i, analyzerCount) 167 | } 168 | 169 | for i := 0; i < analyzerCount; i++ { 170 | cs.syncWg.Add(1) 171 | go cs.syncLoop(cancel) 172 | } 173 | 174 | <-cancel 175 | cs.syncWg.Wait() 176 | close(cs.syncChan) 177 | 178 | return nil 179 | } 180 | 181 | func (cs *CounterService) shardAnalyzer(cancel chan struct{}, initShard int, analyzerCount int) { 182 | defer cs.syncWg.Done() 183 | for { 184 | select { 185 | case <-cancel: 186 | return 187 | case <-time.After(500 * time.Millisecond): 188 | now := time.Now().Unix() 189 | 190 | for i := initShard; i < int(cs.shardCount); i += analyzerCount { 191 | 192 | countersToDelete := make([]string, 0, len(cs.shardedCounters[i])) 193 | countersToUpdate := make([]string, 0, len(cs.shardedCounters[i])) 194 | 195 | mux := cs.shardedMutexes[i] 196 | mux.RLock() 197 | for k, v := range cs.shardedCounters[i] { 198 | // counter does not need to be synchronized as it only lives in memory 199 | if v.syncRate == -1 { 200 | continue 201 | } 202 | 203 | // if key is expired we should remove it 204 | if v.ttl < now { 205 | countersToDelete = append(countersToDelete, k) 206 | continue 207 | } 208 | 209 | // if counter needs to be synchronized 210 | if now-atomic.LoadInt64(&v.lastSync) > int64(v.syncRate) { 211 | countersToUpdate = append(countersToUpdate, k) 212 | } 213 | } 214 | mux.RUnlock() 215 | 216 | for _, k := range countersToUpdate { 217 | // there's a select here so the code does not get blocked here 218 | // if the syncronizer gets a big amount of lag 219 | select { 220 | case cs.syncChan <- counterUpdate{uint64(i), k}: 221 | case <-cancel: 222 | return 223 | } 224 | } 225 | 226 | // We need to remove the keys outside of the loop, as there can be other threads creating new counters 227 | // at the same time the loop is happening, and adding a rlock in the range statement would make impossible the 228 | // upgrade rlock for a lock. 229 | mux.Lock() 230 | for _, k := range countersToDelete { 231 | delete(cs.shardedCounters[i], k) 232 | } 233 | mux.Unlock() 234 | } 235 | } 236 | } 237 | } 238 | 239 | func (cs *CounterService) syncLoop(cancel chan struct{}) { 240 | batch := make([]CounterInc, 0, cs.syncBatchSize) 241 | ticker := time.NewTicker(300 * time.Millisecond) 242 | defer cs.syncWg.Done() 243 | defer ticker.Stop() 244 | 245 | for { 246 | needsFlush := false 247 | select { 248 | case c := <-cs.syncChan: 249 | mux := cs.shardedMutexes[c.shard] 250 | mux.RLock() 251 | 252 | counter := cs.shardedCounters[c.shard][c.key] 253 | curr := atomic.LoadUint32(&counter.current) 254 | 255 | inc := CounterInc{ 256 | Key: c.key, 257 | Inc: curr, 258 | TTL: counter.ttl, 259 | } 260 | 261 | batch = append(batch, inc) 262 | mux.RUnlock() 263 | 264 | if len(batch) >= cs.syncBatchSize { 265 | needsFlush = true 266 | } 267 | case <-ticker.C: 268 | needsFlush = true 269 | case <-cancel: 270 | return 271 | } 272 | 273 | if needsFlush { 274 | cs.syncCounters(batch) 275 | batch = batch[0:0] 276 | } 277 | } 278 | } 279 | 280 | func (cs *CounterService) syncCounters(incs []CounterInc) { 281 | if len(incs) == 0 { 282 | return 283 | } 284 | 285 | ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond) 286 | defer cancel() 287 | incvalues, err := cs.storage.BatchIncrement(ctx, incs) 288 | 289 | if err != nil { 290 | cs.logger.Error(err) 291 | return 292 | } 293 | 294 | for i, inc := range incvalues { 295 | shard := fnv1a.HashString64(inc.Key) % cs.shardCount 296 | mux := cs.shardedMutexes[shard] 297 | mux.RLock() 298 | 299 | counter, ok := cs.shardedCounters[shard][inc.Key] 300 | if !ok { 301 | mux.RUnlock() 302 | continue 303 | } 304 | 305 | if inc.Err != nil { 306 | cs.logger.Error(err) 307 | mux.RUnlock() 308 | return 309 | } 310 | 311 | // decrement current counter with the value that was 312 | // incremented on storage 313 | atomic.AddUint32(&counter.current, ^uint32(incs[i].Inc-1)) 314 | atomic.SwapUint32(&counter.stored, inc.Curr) 315 | atomic.SwapInt64(&counter.lastSync, time.Now().Unix()) 316 | cs.metrics.counterSyncTotal.Add(1) 317 | 318 | mux.RUnlock() 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /pkg/counter/counter_test.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | func TestCounterService_Increment(t *testing.T) { 17 | testCases := []struct { 18 | desc string 19 | key string 20 | ttl int64 21 | increment uint32 22 | preIncrement bool 23 | want uint32 24 | err error 25 | }{ 26 | { 27 | desc: "Non Existing key, initializes counter with value", 28 | key: "key", 29 | preIncrement: false, 30 | increment: 10, 31 | ttl: time.Now().Unix() + 30, 32 | want: 10, 33 | }, 34 | { 35 | desc: "Existing key, increments counter", 36 | key: "key", 37 | preIncrement: true, 38 | increment: 10, 39 | ttl: time.Now().Unix() + 30, 40 | want: 20, 41 | }, 42 | } 43 | for _, tC := range testCases { 44 | t.Run(tC.desc, func(t *testing.T) { 45 | counterService := NewCounterService(newInMemStorage(), newNullLogger(), prometheus.NewRegistry(), 100) 46 | 47 | if tC.preIncrement { 48 | counterService.Increment(context.Background(), tC.key, tC.increment, tC.ttl, -1) 49 | } 50 | 51 | got, err := counterService.Increment(context.Background(), tC.key, tC.increment, tC.ttl, -1) 52 | 53 | if tC.want != got { 54 | t.Errorf("got %v, want %v", got, tC.want) 55 | } 56 | 57 | if tC.err != err { 58 | t.Errorf("got err %v, want error %v", err, tC.err) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestCounterService_Increment_ConcurrentIncrement(t *testing.T) { 65 | // arrange 66 | key := "key" 67 | ttl := time.Now().Unix() + 30 68 | counterService := NewCounterService(newInMemStorage(), newNullLogger(), prometheus.NewRegistry(), 100) 69 | 70 | // act 71 | wg := sync.WaitGroup{} 72 | for i := 0; i < 20; i++ { 73 | wg.Add(1) 74 | go func() { 75 | defer wg.Done() 76 | for j := 0; j < 20; j++ { 77 | counterService.Increment(context.Background(), key, 1, ttl, -1) 78 | } 79 | }() 80 | } 81 | wg.Wait() 82 | 83 | got, err := counterService.Get(context.Background(), key) 84 | 85 | // assert 86 | var want uint32 = 400 87 | if got != want { 88 | t.Errorf("got %v, want %v", got, want) 89 | } 90 | if err != nil { 91 | t.Errorf("got %v, want no error", err) 92 | } 93 | } 94 | 95 | func TestCounterService_IncrementOnStorage(t *testing.T) { 96 | testCases := []struct { 97 | desc string 98 | key string 99 | increment uint32 100 | ttl int64 101 | storageRes uint32 102 | want uint32 103 | err error 104 | }{ 105 | { 106 | desc: "Increments on counter storage", 107 | key: "key", 108 | increment: 2, 109 | ttl: time.Now().Unix() + 30, 110 | storageRes: 22, 111 | want: 22, 112 | }, 113 | { 114 | desc: "Counter storage fails, returns counter storage error", 115 | key: "key", 116 | increment: 2, 117 | ttl: time.Now().Unix() + 30, 118 | err: errors.New("error occurred incrementing"), 119 | storageRes: 0, 120 | want: 0, 121 | }, 122 | } 123 | for _, tC := range testCases { 124 | t.Run(tC.desc, func(t *testing.T) { 125 | // arrange 126 | storageMock := new(storageMock) 127 | storageMock.On("Increment", mock.Anything, tC.key, tC.increment, tC.ttl).Return(tC.storageRes, tC.err) 128 | counterService := NewCounterService(storageMock, newNullLogger(), prometheus.NewRegistry(), 100) 129 | 130 | // act 131 | got, err := counterService.IncrementOnStorage(context.Background(), tC.key, tC.increment, tC.ttl) 132 | 133 | // assert 134 | if tC.want != got { 135 | t.Errorf("got %v, want %v", got, tC.want) 136 | } 137 | 138 | if err != tC.err { 139 | t.Errorf("got err %v, want err %v", err, tC.err) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func TestCounterService_Get(t *testing.T) { 146 | testCases := []struct { 147 | desc string 148 | key string 149 | ttl int64 150 | increment uint32 151 | preIncrement bool 152 | err error 153 | want uint32 154 | }{ 155 | { 156 | desc: "Non existing counter", 157 | key: "key", 158 | preIncrement: false, 159 | err: ErrNonExistingCounter, 160 | }, 161 | { 162 | desc: "Valid counter", 163 | key: "key", 164 | preIncrement: true, 165 | increment: 1, 166 | ttl: time.Now().Unix() + 60, 167 | want: 1, 168 | err: nil, 169 | }, 170 | } 171 | 172 | for _, tC := range testCases { 173 | t.Run(tC.desc, func(t *testing.T) { 174 | // arrange 175 | counterService := NewCounterService(new(storageMock), newNullLogger(), prometheus.NewRegistry(), 100) 176 | 177 | if tC.preIncrement { 178 | counterService.Increment(context.Background(), tC.key, tC.increment, tC.ttl, -1) 179 | } 180 | 181 | // act 182 | got, err := counterService.Get(context.Background(), tC.key) 183 | 184 | // assert 185 | if tC.want != got { 186 | t.Errorf("got %v, want %v", got, tC.want) 187 | } 188 | 189 | if err != tC.err { 190 | t.Errorf("got err %v, want err %v", err, tC.err) 191 | } 192 | }) 193 | } 194 | } 195 | 196 | func TestCounterService_GetFromStorage(t *testing.T) { 197 | testCases := []struct { 198 | desc string 199 | key string 200 | storageRes uint32 201 | want uint32 202 | err error 203 | }{ 204 | { 205 | desc: "Gets from counter storage", 206 | key: "key", 207 | storageRes: 22, 208 | want: 22, 209 | }, 210 | { 211 | desc: "Counter storage fails, returns counter storage error", 212 | key: "key", 213 | err: errors.New("error occurred obtaining counter"), 214 | storageRes: 0, 215 | want: 0, 216 | }, 217 | } 218 | for _, tC := range testCases { 219 | t.Run(tC.desc, func(t *testing.T) { 220 | // arrange 221 | storageMock := new(storageMock) 222 | storageMock.On("Get", mock.Anything, tC.key).Return(tC.storageRes, tC.err) 223 | counterService := NewCounterService(storageMock, newNullLogger(), prometheus.NewRegistry(), 100) 224 | 225 | // act 226 | got, err := counterService.GetFromStorage(context.Background(), tC.key) 227 | 228 | // assert 229 | if tC.want != got { 230 | t.Errorf("got %v, want %v", got, tC.want) 231 | } 232 | 233 | if err != tC.err { 234 | t.Errorf("got err %v, want err %v", err, tC.err) 235 | } 236 | }) 237 | } 238 | } 239 | 240 | func TestCounterService_RunSync_RemovesOutdatedCounters(t *testing.T) { 241 | // arrange 242 | key := "key" 243 | counterService := NewCounterService(newInMemStorage(), newNullLogger(), prometheus.NewRegistry(), 1) 244 | 245 | // act 246 | cancel := make(chan struct{}) 247 | errch := make(chan error) 248 | go func() { 249 | errch <- counterService.RunSync(cancel) 250 | }() 251 | 252 | counterService.Increment(context.Background(), key, 1, time.Now().Unix()-1, 2) 253 | time.Sleep(1 * time.Second) 254 | close(cancel) 255 | <-errch 256 | 257 | // assert 258 | _, err := counterService.Get(context.Background(), key) 259 | if err != ErrNonExistingCounter { 260 | t.Errorf("got err %v, want error %v", err, ErrNonExistingCounter) 261 | } 262 | } 263 | 264 | func TestCounterService_RunSync_SynchronizesCounters(t *testing.T) { 265 | // arrange 266 | key := "key" 267 | 268 | counterService := NewCounterService(newInMemStorage(), newNullLogger(), prometheus.NewRegistry(), 1) 269 | 270 | // act 271 | cancel := make(chan struct{}) 272 | errch := make(chan error) 273 | go func() { 274 | errch <- counterService.RunSync(cancel) 275 | }() 276 | 277 | counterService.Increment(context.Background(), key, 5, time.Now().Unix()+30, 1) 278 | time.Sleep(1 * time.Second) 279 | counterService.Increment(context.Background(), key, 5, time.Now().Unix()+30, 1) 280 | time.Sleep(2000 * time.Millisecond) 281 | 282 | close(cancel) 283 | <-errch 284 | 285 | // assert 286 | var want uint32 = 10 287 | res, err := counterService.GetFromStorage(context.Background(), key) 288 | if res != want { 289 | t.Errorf("got %v, want %v", res, want) 290 | } 291 | 292 | if err != nil { 293 | t.Errorf("got err %v, want no error", err) 294 | } 295 | } 296 | 297 | func newNullLogger() *logrus.Logger { 298 | logger := logrus.New() 299 | logger.Out = ioutil.Discard 300 | 301 | return logger 302 | } 303 | 304 | type storageMock struct { 305 | mock.Mock 306 | } 307 | 308 | func (s *storageMock) BatchIncrement(ctx context.Context, incrs []CounterInc) ([]CounterIncResponse, error) { 309 | args := s.Called(ctx, incrs) 310 | return args.Get(0).([]CounterIncResponse), args.Error(1) 311 | } 312 | 313 | func (s *storageMock) Increment(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) { 314 | args := s.Called(ctx, key, n, ttl) 315 | return args.Get(0).(uint32), args.Error(1) 316 | } 317 | 318 | func (s *storageMock) Get(ctx context.Context, key string) (uint32, error) { 319 | args := s.Called(ctx, key) 320 | return args.Get(0).(uint32), args.Error(1) 321 | } 322 | 323 | type inmemStorage struct { 324 | mux sync.Mutex 325 | counters map[string]uint32 326 | } 327 | 328 | func newInMemStorage() *inmemStorage { 329 | return &inmemStorage{mux: sync.Mutex{}, counters: make(map[string]uint32)} 330 | } 331 | 332 | func (s *inmemStorage) BatchIncrement(ctx context.Context, incrs []CounterInc) ([]CounterIncResponse, error) { 333 | s.mux.Lock() 334 | s.mux.Unlock() 335 | 336 | resp := make([]CounterIncResponse, len(incrs)) 337 | for i := 0; i < len(incrs); i++ { 338 | s.counters[incrs[i].Key] += incrs[i].Inc 339 | 340 | resp[i] = CounterIncResponse{} 341 | resp[i].Key = incrs[i].Key 342 | resp[i].Curr = s.counters[incrs[i].Key] 343 | } 344 | return resp, nil 345 | } 346 | 347 | func (s *inmemStorage) Increment(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) { 348 | s.mux.Lock() 349 | s.mux.Unlock() 350 | 351 | s.counters[key] += n 352 | return s.counters[key], nil 353 | } 354 | 355 | func (s *inmemStorage) Get(ctx context.Context, key string) (uint32, error) { 356 | s.mux.Lock() 357 | s.mux.Unlock() 358 | 359 | if c, ok := s.counters[key]; ok { 360 | return c, nil 361 | } 362 | 363 | return 0, errors.New("not existing counter") 364 | } 365 | -------------------------------------------------------------------------------- /pkg/limiter/limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "testing" 7 | 8 | rl "github.com/envoyproxy/go-control-plane/envoy/api/v2/ratelimit" 9 | pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | func BenchmarkLimiterService_GenerateKey(b *testing.B) { 16 | descriptor := &rl.RateLimitDescriptor{ 17 | Entries: []*rl.RateLimitDescriptor_Entry{ 18 | { 19 | Key: "authorized", 20 | Value: "true", 21 | }, 22 | { 23 | Key: "tenant", 24 | Value: "10000", 25 | }, 26 | }, 27 | } 28 | 29 | rule := &Rule{ 30 | Labels: []DescriptorLabel{ 31 | { 32 | Key: "authorized", 33 | Value: "true", 34 | }, 35 | }, 36 | Limit: Limit{ 37 | Requests: 20, 38 | Unit: "second", 39 | }, 40 | } 41 | 42 | var time int64 = 20 43 | for i := 0; i < b.N; i++ { 44 | generateKey(descriptor, rule, time) 45 | } 46 | } 47 | 48 | func TestLimiterService_GenerateKey(t *testing.T) { 49 | testCases := []struct { 50 | desc string 51 | descriptor *rl.RateLimitDescriptor 52 | rule *Rule 53 | time int64 54 | want string 55 | }{ 56 | { 57 | desc: "with all parameters", 58 | descriptor: &rl.RateLimitDescriptor{ 59 | Entries: []*rl.RateLimitDescriptor_Entry{ 60 | { 61 | Key: "authorized", 62 | Value: "true", 63 | }, 64 | }, 65 | }, 66 | rule: &Rule{ 67 | Labels: []DescriptorLabel{ 68 | { 69 | Key: "authorized", 70 | }, 71 | }, 72 | Limit: Limit{ 73 | Requests: 20, 74 | Unit: "second", 75 | }, 76 | }, 77 | time: 1, 78 | want: "authorized.true:second:1", 79 | }, 80 | { 81 | desc: "with missing parameters", 82 | descriptor: &rl.RateLimitDescriptor{ 83 | Entries: []*rl.RateLimitDescriptor_Entry{ 84 | { 85 | Key: "authorized", 86 | Value: "true", 87 | }, 88 | { 89 | Key: "tenant", 90 | Value: "10", 91 | }, 92 | }, 93 | }, 94 | rule: &Rule{ 95 | Labels: []DescriptorLabel{ 96 | { 97 | Key: "authorized", 98 | }, 99 | }, 100 | Limit: Limit{ 101 | Requests: 20, 102 | Unit: "second", 103 | }, 104 | }, 105 | time: 1, 106 | want: "authorized.true:second:1", 107 | }, 108 | } 109 | for _, tC := range testCases { 110 | t.Run(tC.desc, func(t *testing.T) { 111 | got := generateKey(tC.descriptor, tC.rule, tC.time) 112 | if tC.want != got { 113 | t.Errorf("got %v, want %v", got, tC.want) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func TestLimiterService_ShouldRateLimit(t *testing.T) { 120 | testCases := []struct { 121 | desc string 122 | 123 | rule *Rule 124 | ruleErr error 125 | 126 | getCounterResponse uint32 127 | getCounterResponseErr error 128 | getCounterFromStorageResponse uint32 129 | getCounterFromStorageErr error 130 | 131 | incrementCounterResponse uint32 132 | incrementCounterResponseErr error 133 | incrementCounterOnStorageResponse uint32 134 | incrementCounterOnStorageErr error 135 | 136 | request *pb.RateLimitRequest 137 | 138 | want *pb.RateLimitResponse 139 | wantErr error 140 | }{ 141 | { 142 | desc: "No Ratelimit rule for descriptor returns unkown response", 143 | ruleErr: ErrNoMatchedRule, 144 | request: &pb.RateLimitRequest{ 145 | Domain: "domain", 146 | Descriptors: []*rl.RateLimitDescriptor{ 147 | { 148 | Entries: []*rl.RateLimitDescriptor_Entry{ 149 | { 150 | Key: "tenant", 151 | Value: "10000", 152 | }, 153 | }, 154 | }, 155 | }, 156 | HitsAddend: 1, 157 | }, 158 | want: &pb.RateLimitResponse{ 159 | OverallCode: pb.RateLimitResponse_UNKNOWN, 160 | Statuses: []*pb.RateLimitResponse_DescriptorStatus{ 161 | { 162 | Code: pb.RateLimitResponse_UNKNOWN, 163 | }, 164 | }, 165 | }, 166 | }, 167 | { 168 | desc: "Ratelimit rule has sync rate 0, increments on storage and returns ok result", 169 | rule: &Rule{ 170 | Name: "rule_name", 171 | Labels: []DescriptorLabel{ 172 | { 173 | Key: "tenant", 174 | }, 175 | }, 176 | SyncRate: 0, 177 | Limit: Limit{ 178 | Requests: 1000, 179 | Unit: "minute", 180 | }, 181 | }, 182 | getCounterFromStorageResponse: 100, 183 | incrementCounterOnStorageResponse: 100, 184 | request: &pb.RateLimitRequest{ 185 | Domain: "domain", 186 | Descriptors: []*rl.RateLimitDescriptor{ 187 | { 188 | Entries: []*rl.RateLimitDescriptor_Entry{ 189 | { 190 | Key: "tenant", 191 | Value: "10000", 192 | }, 193 | }, 194 | }, 195 | }, 196 | HitsAddend: 1, 197 | }, 198 | want: &pb.RateLimitResponse{ 199 | OverallCode: pb.RateLimitResponse_OK, 200 | Statuses: []*pb.RateLimitResponse_DescriptorStatus{ 201 | { 202 | Code: pb.RateLimitResponse_OK, 203 | CurrentLimit: &pb.RateLimitResponse_RateLimit{ 204 | RequestsPerUnit: 1000, 205 | Unit: pb.RateLimitResponse_RateLimit_MINUTE, 206 | }, 207 | LimitRemaining: 900, 208 | }, 209 | }, 210 | }, 211 | }, 212 | { 213 | desc: "Ratelimit rule has sync rate bigger than 0, increments on counter service and returns ok result", 214 | rule: &Rule{ 215 | Name: "rule_name", 216 | Labels: []DescriptorLabel{ 217 | { 218 | Key: "tenant", 219 | }, 220 | }, 221 | SyncRate: 5, 222 | Limit: Limit{ 223 | Requests: 1000, 224 | Unit: "minute", 225 | }, 226 | }, 227 | getCounterResponse: 100, 228 | incrementCounterResponse: 100, 229 | request: &pb.RateLimitRequest{ 230 | Domain: "domain", 231 | Descriptors: []*rl.RateLimitDescriptor{ 232 | { 233 | Entries: []*rl.RateLimitDescriptor_Entry{ 234 | { 235 | Key: "tenant", 236 | Value: "10000", 237 | }, 238 | }, 239 | }, 240 | }, 241 | HitsAddend: 1, 242 | }, 243 | want: &pb.RateLimitResponse{ 244 | OverallCode: pb.RateLimitResponse_OK, 245 | Statuses: []*pb.RateLimitResponse_DescriptorStatus{ 246 | { 247 | Code: pb.RateLimitResponse_OK, 248 | CurrentLimit: &pb.RateLimitResponse_RateLimit{ 249 | RequestsPerUnit: 1000, 250 | Unit: pb.RateLimitResponse_RateLimit_MINUTE, 251 | }, 252 | LimitRemaining: 900, 253 | }, 254 | }, 255 | }, 256 | }, 257 | { 258 | desc: "Current minute is going to be rate limited using predicted request rate algorithm", 259 | rule: &Rule{ 260 | Name: "rule_name", 261 | Labels: []DescriptorLabel{ 262 | { 263 | Key: "tenant", 264 | }, 265 | }, 266 | SyncRate: 5, 267 | Limit: Limit{ 268 | Requests: 1000, 269 | Unit: "minute", 270 | }, 271 | }, 272 | getCounterResponse: 1000, 273 | incrementCounterResponse: 999, 274 | request: &pb.RateLimitRequest{ 275 | Domain: "domain", 276 | Descriptors: []*rl.RateLimitDescriptor{ 277 | { 278 | Entries: []*rl.RateLimitDescriptor_Entry{ 279 | { 280 | Key: "tenant", 281 | Value: "10000", 282 | }, 283 | }, 284 | }, 285 | }, 286 | HitsAddend: 1, 287 | }, 288 | want: &pb.RateLimitResponse{ 289 | OverallCode: pb.RateLimitResponse_OVER_LIMIT, 290 | Statuses: []*pb.RateLimitResponse_DescriptorStatus{ 291 | { 292 | Code: pb.RateLimitResponse_OVER_LIMIT, 293 | CurrentLimit: &pb.RateLimitResponse_RateLimit{ 294 | RequestsPerUnit: 1000, 295 | Unit: pb.RateLimitResponse_RateLimit_MINUTE, 296 | }, 297 | LimitRemaining: 0, 298 | }, 299 | }, 300 | }, 301 | }, 302 | } 303 | for _, tC := range testCases { 304 | t.Run(tC.desc, func(t *testing.T) { 305 | // arrange 306 | rulesServiceMock := new(ruleServiceMock) 307 | rulesServiceMock.On("GetRatelimitRule", tC.request.Domain, tC.request.Descriptors[0]).Return(tC.rule, tC.ruleErr) 308 | 309 | counterServiceMock := new(counterServiceMock) 310 | if tC.rule != nil { 311 | counterServiceMock.On("Increment", mock.Anything, mock.Anything, tC.request.HitsAddend, mock.Anything, tC.rule.SyncRate).Return(tC.incrementCounterResponse, tC.incrementCounterResponseErr) 312 | counterServiceMock.On("IncrementOnStorage", mock.Anything, mock.Anything, tC.request.HitsAddend, mock.Anything).Return(tC.incrementCounterOnStorageResponse, tC.incrementCounterOnStorageErr) 313 | counterServiceMock.On("Get", mock.Anything, mock.Anything).Return(tC.getCounterResponse, tC.getCounterResponseErr) 314 | counterServiceMock.On("GetFromStorage", mock.Anything, mock.Anything).Return(tC.getCounterFromStorageResponse, tC.getCounterFromStorageErr) 315 | } 316 | 317 | limiterService := NewLimiterService(rulesServiceMock, counterServiceMock, newNullLogger(), prometheus.NewRegistry()) 318 | 319 | // act 320 | got, _ := limiterService.ShouldRateLimit(context.Background(), tC.request) 321 | 322 | // assert 323 | if tC.want.OverallCode != got.OverallCode { 324 | t.Errorf("got overall code %v, want %v", got.OverallCode, tC.want.OverallCode) 325 | } 326 | 327 | for i := 0; i < len(tC.want.Statuses); i++ { 328 | if tC.want.Statuses[i].LimitRemaining != got.Statuses[i].LimitRemaining { 329 | t.Errorf("got limit remaining %v on status index %v, want %v", got.Statuses[i].LimitRemaining, i, tC.want.Statuses[i].LimitRemaining) 330 | } 331 | 332 | if tC.want.Statuses[i].CurrentLimit != nil { 333 | if tC.want.Statuses[i].CurrentLimit.RequestsPerUnit != got.Statuses[i].CurrentLimit.RequestsPerUnit { 334 | t.Errorf("got requests per unit %v on status index %v, want %v", got.Statuses[i].CurrentLimit.RequestsPerUnit, i, tC.want.Statuses[i].CurrentLimit.RequestsPerUnit) 335 | } 336 | 337 | if tC.want.Statuses[i].CurrentLimit.Unit != got.Statuses[i].CurrentLimit.Unit { 338 | t.Errorf("got unit %v on status index %v, want %v", got.Statuses[i].CurrentLimit.Unit, i, tC.want.Statuses[i].CurrentLimit.Unit) 339 | } 340 | } 341 | } 342 | }) 343 | } 344 | } 345 | 346 | type ruleServiceMock struct { 347 | mock.Mock 348 | } 349 | 350 | func (r *ruleServiceMock) GetRatelimitRule(domain string, requestDescriptor *rl.RateLimitDescriptor) (*Rule, error) { 351 | args := r.Called(domain, requestDescriptor) 352 | return args.Get(0).(*Rule), args.Error(1) 353 | } 354 | 355 | type counterServiceMock struct { 356 | mock.Mock 357 | } 358 | 359 | func (c *counterServiceMock) Increment(ctx context.Context, key string, n uint32, ttl int64, syncRate int) (uint32, error) { 360 | args := c.Called(ctx, key, n, ttl, syncRate) 361 | return args.Get(0).(uint32), args.Error(1) 362 | } 363 | 364 | func (c *counterServiceMock) IncrementOnStorage(ctx context.Context, key string, n uint32, ttl int64) (uint32, error) { 365 | args := c.Called(ctx, key, n, ttl) 366 | return args.Get(0).(uint32), args.Error(1) 367 | } 368 | 369 | func (c *counterServiceMock) Get(ctx context.Context, key string) (uint32, error) { 370 | args := c.Called(ctx, key) 371 | return args.Get(0).(uint32), args.Error(1) 372 | } 373 | 374 | func (c *counterServiceMock) GetFromStorage(ctx context.Context, key string) (uint32, error) { 375 | args := c.Called(ctx, key) 376 | return args.Get(0).(uint32), args.Error(1) 377 | } 378 | 379 | func newNullLogger() *logrus.Logger { 380 | logger := logrus.New() 381 | logger.Out = ioutil.Discard 382 | 383 | return logger 384 | } 385 | -------------------------------------------------------------------------------- /env/grafana/dashboards/r8limiter.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "links": [], 19 | "panels": [ 20 | { 21 | "collapsed": false, 22 | "gridPos": { 23 | "h": 1, 24 | "w": 24, 25 | "x": 0, 26 | "y": 0 27 | }, 28 | "id": 23, 29 | "panels": [], 30 | "title": "HTTP", 31 | "type": "row" 32 | }, 33 | { 34 | "aliasColors": {}, 35 | "bars": false, 36 | "dashLength": 10, 37 | "dashes": false, 38 | "fill": 1, 39 | "gridPos": { 40 | "h": 10, 41 | "w": 12, 42 | "x": 0, 43 | "y": 1 44 | }, 45 | "id": 21, 46 | "legend": { 47 | "avg": false, 48 | "current": false, 49 | "max": false, 50 | "min": false, 51 | "show": true, 52 | "total": false, 53 | "values": false 54 | }, 55 | "lines": true, 56 | "linewidth": 2, 57 | "links": [], 58 | "nullPointMode": "null", 59 | "paceLength": 10, 60 | "percentage": false, 61 | "pointradius": 2, 62 | "points": false, 63 | "renderer": "flot", 64 | "seriesOverrides": [], 65 | "stack": false, 66 | "steppedLine": false, 67 | "targets": [ 68 | { 69 | "expr": "sum(rate(http_request_total[2m])) by (handler)", 70 | "format": "time_series", 71 | "intervalFactor": 1, 72 | "legendFormat": "{{handler}}", 73 | "refId": "A" 74 | } 75 | ], 76 | "thresholds": [], 77 | "timeFrom": null, 78 | "timeRegions": [], 79 | "timeShift": null, 80 | "title": "HTTP Requests p/ second", 81 | "tooltip": { 82 | "shared": true, 83 | "sort": 0, 84 | "value_type": "individual" 85 | }, 86 | "type": "graph", 87 | "xaxis": { 88 | "buckets": null, 89 | "mode": "time", 90 | "name": null, 91 | "show": true, 92 | "values": [] 93 | }, 94 | "yaxes": [ 95 | { 96 | "format": "reqps", 97 | "label": null, 98 | "logBase": 1, 99 | "max": null, 100 | "min": null, 101 | "show": true 102 | }, 103 | { 104 | "format": "short", 105 | "label": null, 106 | "logBase": 1, 107 | "max": null, 108 | "min": null, 109 | "show": true 110 | } 111 | ], 112 | "yaxis": { 113 | "align": false, 114 | "alignLevel": null 115 | } 116 | }, 117 | { 118 | "aliasColors": {}, 119 | "bars": false, 120 | "dashLength": 10, 121 | "dashes": false, 122 | "fill": 1, 123 | "gridPos": { 124 | "h": 10, 125 | "w": 12, 126 | "x": 12, 127 | "y": 1 128 | }, 129 | "id": 20, 130 | "legend": { 131 | "avg": false, 132 | "current": false, 133 | "max": false, 134 | "min": false, 135 | "show": true, 136 | "total": false, 137 | "values": false 138 | }, 139 | "lines": true, 140 | "linewidth": 1, 141 | "links": [], 142 | "nullPointMode": "null", 143 | "paceLength": 10, 144 | "percentage": false, 145 | "pointradius": 2, 146 | "points": false, 147 | "renderer": "flot", 148 | "seriesOverrides": [], 149 | "stack": false, 150 | "steppedLine": false, 151 | "targets": [ 152 | { 153 | "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[2m])) by (le))", 154 | "format": "time_series", 155 | "intervalFactor": 1, 156 | "legendFormat": "p99", 157 | "refId": "A" 158 | }, 159 | { 160 | "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[2m])) by (le))", 161 | "format": "time_series", 162 | "intervalFactor": 1, 163 | "legendFormat": "p95", 164 | "refId": "B" 165 | }, 166 | { 167 | "expr": "histogram_quantile(0.90, sum(rate(http_request_duration_seconds_bucket[2m])) by (le))", 168 | "format": "time_series", 169 | "intervalFactor": 1, 170 | "legendFormat": "p90", 171 | "refId": "C" 172 | } 173 | ], 174 | "thresholds": [], 175 | "timeFrom": null, 176 | "timeRegions": [], 177 | "timeShift": null, 178 | "title": "HTTP Request latency", 179 | "tooltip": { 180 | "shared": true, 181 | "sort": 0, 182 | "value_type": "individual" 183 | }, 184 | "type": "graph", 185 | "xaxis": { 186 | "buckets": null, 187 | "mode": "time", 188 | "name": null, 189 | "show": true, 190 | "values": [] 191 | }, 192 | "yaxes": [ 193 | { 194 | "format": "s", 195 | "label": null, 196 | "logBase": 1, 197 | "max": null, 198 | "min": null, 199 | "show": true 200 | }, 201 | { 202 | "format": "short", 203 | "label": null, 204 | "logBase": 1, 205 | "max": null, 206 | "min": null, 207 | "show": true 208 | } 209 | ], 210 | "yaxis": { 211 | "align": false, 212 | "alignLevel": null 213 | } 214 | }, 215 | { 216 | "collapsed": false, 217 | "gridPos": { 218 | "h": 1, 219 | "w": 24, 220 | "x": 0, 221 | "y": 11 222 | }, 223 | "id": 18, 224 | "panels": [], 225 | "title": "gRPC", 226 | "type": "row" 227 | }, 228 | { 229 | "aliasColors": {}, 230 | "bars": false, 231 | "dashLength": 10, 232 | "dashes": false, 233 | "fill": 1, 234 | "gridPos": { 235 | "h": 10, 236 | "w": 12, 237 | "x": 0, 238 | "y": 12 239 | }, 240 | "id": 4, 241 | "legend": { 242 | "avg": false, 243 | "current": false, 244 | "max": false, 245 | "min": false, 246 | "show": true, 247 | "total": false, 248 | "values": false 249 | }, 250 | "lines": true, 251 | "linewidth": 2, 252 | "links": [], 253 | "nullPointMode": "null", 254 | "paceLength": 10, 255 | "percentage": false, 256 | "pointradius": 2, 257 | "points": false, 258 | "renderer": "flot", 259 | "seriesOverrides": [], 260 | "stack": false, 261 | "steppedLine": false, 262 | "targets": [ 263 | { 264 | "expr": "rate(grpc_server_msg_sent_total[2m])", 265 | "format": "time_series", 266 | "intervalFactor": 1, 267 | "legendFormat": "{{grpc_method}}", 268 | "refId": "A" 269 | } 270 | ], 271 | "thresholds": [], 272 | "timeFrom": null, 273 | "timeRegions": [], 274 | "timeShift": null, 275 | "title": "gRPC Requests p/ second", 276 | "tooltip": { 277 | "shared": true, 278 | "sort": 0, 279 | "value_type": "individual" 280 | }, 281 | "type": "graph", 282 | "xaxis": { 283 | "buckets": null, 284 | "mode": "time", 285 | "name": null, 286 | "show": true, 287 | "values": [] 288 | }, 289 | "yaxes": [ 290 | { 291 | "format": "reqps", 292 | "label": null, 293 | "logBase": 1, 294 | "max": null, 295 | "min": null, 296 | "show": true 297 | }, 298 | { 299 | "format": "short", 300 | "label": null, 301 | "logBase": 1, 302 | "max": null, 303 | "min": null, 304 | "show": true 305 | } 306 | ], 307 | "yaxis": { 308 | "align": false, 309 | "alignLevel": null 310 | } 311 | }, 312 | { 313 | "aliasColors": {}, 314 | "bars": false, 315 | "dashLength": 10, 316 | "dashes": false, 317 | "fill": 1, 318 | "gridPos": { 319 | "h": 10, 320 | "w": 12, 321 | "x": 12, 322 | "y": 12 323 | }, 324 | "id": 2, 325 | "legend": { 326 | "avg": false, 327 | "current": false, 328 | "max": false, 329 | "min": false, 330 | "show": true, 331 | "total": false, 332 | "values": false 333 | }, 334 | "lines": true, 335 | "linewidth": 2, 336 | "links": [], 337 | "nullPointMode": "null", 338 | "paceLength": 10, 339 | "percentage": false, 340 | "pointradius": 2, 341 | "points": false, 342 | "renderer": "flot", 343 | "seriesOverrides": [], 344 | "stack": false, 345 | "steppedLine": false, 346 | "targets": [ 347 | { 348 | "expr": "histogram_quantile(0.90, rate(grpc_server_handling_seconds_bucket[5m]))", 349 | "format": "time_series", 350 | "intervalFactor": 1, 351 | "legendFormat": "p90", 352 | "refId": "A" 353 | }, 354 | { 355 | "expr": "histogram_quantile(0.95, rate(grpc_server_handling_seconds_bucket[5m]))", 356 | "format": "time_series", 357 | "intervalFactor": 1, 358 | "legendFormat": "p95", 359 | "refId": "B" 360 | }, 361 | { 362 | "expr": "histogram_quantile(0.99, rate(grpc_server_handling_seconds_bucket[5m]))", 363 | "format": "time_series", 364 | "intervalFactor": 1, 365 | "legendFormat": "p99", 366 | "refId": "C" 367 | }, 368 | { 369 | "expr": "histogram_quantile(0.999, rate(grpc_server_handling_seconds_bucket[5m]))", 370 | "format": "time_series", 371 | "intervalFactor": 1, 372 | "legendFormat": "p999", 373 | "refId": "D" 374 | } 375 | ], 376 | "thresholds": [], 377 | "timeFrom": null, 378 | "timeRegions": [], 379 | "timeShift": null, 380 | "title": "gRPC Request latency", 381 | "tooltip": { 382 | "shared": true, 383 | "sort": 0, 384 | "value_type": "individual" 385 | }, 386 | "type": "graph", 387 | "xaxis": { 388 | "buckets": null, 389 | "mode": "time", 390 | "name": null, 391 | "show": true, 392 | "values": [] 393 | }, 394 | "yaxes": [ 395 | { 396 | "format": "s", 397 | "label": null, 398 | "logBase": 1, 399 | "max": null, 400 | "min": null, 401 | "show": true 402 | }, 403 | { 404 | "format": "short", 405 | "label": null, 406 | "logBase": 1, 407 | "max": null, 408 | "min": null, 409 | "show": true 410 | } 411 | ], 412 | "yaxis": { 413 | "align": false, 414 | "alignLevel": null 415 | } 416 | }, 417 | { 418 | "collapsed": false, 419 | "gridPos": { 420 | "h": 1, 421 | "w": 24, 422 | "x": 0, 423 | "y": 22 424 | }, 425 | "id": 12, 426 | "panels": [], 427 | "title": "Redis", 428 | "type": "row" 429 | }, 430 | { 431 | "aliasColors": {}, 432 | "bars": false, 433 | "dashLength": 10, 434 | "dashes": false, 435 | "fill": 1, 436 | "gridPos": { 437 | "h": 8, 438 | "w": 12, 439 | "x": 0, 440 | "y": 23 441 | }, 442 | "id": 10, 443 | "legend": { 444 | "avg": false, 445 | "current": false, 446 | "max": false, 447 | "min": false, 448 | "show": true, 449 | "total": false, 450 | "values": false 451 | }, 452 | "lines": true, 453 | "linewidth": 1, 454 | "links": [], 455 | "nullPointMode": "null", 456 | "paceLength": 10, 457 | "percentage": false, 458 | "pointradius": 2, 459 | "points": false, 460 | "renderer": "flot", 461 | "seriesOverrides": [], 462 | "stack": false, 463 | "steppedLine": false, 464 | "targets": [ 465 | { 466 | "expr": "rate(redis_inc_duration_seconds_count[2m])", 467 | "format": "time_series", 468 | "intervalFactor": 1, 469 | "legendFormat": "inc", 470 | "refId": "A" 471 | }, 472 | { 473 | "expr": "rate(redis_get_duration_seconds_count[2m])", 474 | "format": "time_series", 475 | "intervalFactor": 1, 476 | "legendFormat": "get", 477 | "refId": "B" 478 | }, 479 | { 480 | "expr": "rate(redis_batchinc_duration_seconds_count[2m])", 481 | "format": "time_series", 482 | "intervalFactor": 1, 483 | "legendFormat": "batchinc", 484 | "refId": "C" 485 | } 486 | ], 487 | "thresholds": [], 488 | "timeFrom": null, 489 | "timeRegions": [], 490 | "timeShift": null, 491 | "title": "Redis request count", 492 | "tooltip": { 493 | "shared": true, 494 | "sort": 0, 495 | "value_type": "individual" 496 | }, 497 | "type": "graph", 498 | "xaxis": { 499 | "buckets": null, 500 | "mode": "time", 501 | "name": null, 502 | "show": true, 503 | "values": [] 504 | }, 505 | "yaxes": [ 506 | { 507 | "format": "short", 508 | "label": null, 509 | "logBase": 1, 510 | "max": null, 511 | "min": null, 512 | "show": true 513 | }, 514 | { 515 | "format": "short", 516 | "label": null, 517 | "logBase": 1, 518 | "max": null, 519 | "min": null, 520 | "show": true 521 | } 522 | ], 523 | "yaxis": { 524 | "align": false, 525 | "alignLevel": null 526 | } 527 | }, 528 | { 529 | "aliasColors": {}, 530 | "bars": false, 531 | "dashLength": 10, 532 | "dashes": false, 533 | "fill": 1, 534 | "gridPos": { 535 | "h": 8, 536 | "w": 12, 537 | "x": 12, 538 | "y": 23 539 | }, 540 | "id": 8, 541 | "legend": { 542 | "avg": false, 543 | "current": false, 544 | "max": false, 545 | "min": false, 546 | "show": true, 547 | "total": false, 548 | "values": false 549 | }, 550 | "lines": true, 551 | "linewidth": 1, 552 | "links": [], 553 | "nullPointMode": "null", 554 | "paceLength": 10, 555 | "percentage": false, 556 | "pointradius": 2, 557 | "points": false, 558 | "renderer": "flot", 559 | "seriesOverrides": [], 560 | "stack": false, 561 | "steppedLine": false, 562 | "targets": [ 563 | { 564 | "expr": "histogram_quantile(0.9, sum(rate(redis_inc_duration_seconds_bucket[1m])) by (le))", 565 | "format": "time_series", 566 | "interval": "", 567 | "intervalFactor": 1, 568 | "legendFormat": "inc p90", 569 | "refId": "A" 570 | }, 571 | { 572 | "expr": "histogram_quantile(0.99, sum(rate(redis_inc_duration_seconds_bucket[1m])) by (le))", 573 | "format": "time_series", 574 | "intervalFactor": 1, 575 | "legendFormat": "inc p99", 576 | "refId": "B" 577 | }, 578 | { 579 | "expr": "histogram_quantile(0.9, sum(rate(redis_get_duration_seconds_bucket[1m])) by (le))", 580 | "format": "time_series", 581 | "intervalFactor": 1, 582 | "legendFormat": "get p90", 583 | "refId": "C" 584 | }, 585 | { 586 | "expr": "histogram_quantile(0.99, sum(rate(redis_get_duration_seconds_bucket[1m])) by (le))", 587 | "format": "time_series", 588 | "intervalFactor": 1, 589 | "legendFormat": "get p99", 590 | "refId": "D" 591 | }, 592 | { 593 | "expr": "histogram_quantile(0.9, sum(rate(redis_batchinc_duration_seconds_bucket[1m])) by (le))", 594 | "format": "time_series", 595 | "intervalFactor": 1, 596 | "legendFormat": "batchinc p90", 597 | "refId": "E" 598 | }, 599 | { 600 | "expr": "histogram_quantile(0.99, sum(rate(redis_batchinc_duration_seconds_bucket[1m])) by (le))", 601 | "format": "time_series", 602 | "intervalFactor": 1, 603 | "legendFormat": "batchinc p99", 604 | "refId": "F" 605 | } 606 | ], 607 | "thresholds": [], 608 | "timeFrom": null, 609 | "timeRegions": [], 610 | "timeShift": null, 611 | "title": "Redis latencies", 612 | "tooltip": { 613 | "shared": true, 614 | "sort": 0, 615 | "value_type": "individual" 616 | }, 617 | "type": "graph", 618 | "xaxis": { 619 | "buckets": null, 620 | "mode": "time", 621 | "name": null, 622 | "show": true, 623 | "values": [] 624 | }, 625 | "yaxes": [ 626 | { 627 | "format": "s", 628 | "label": null, 629 | "logBase": 1, 630 | "max": null, 631 | "min": null, 632 | "show": true 633 | }, 634 | { 635 | "format": "short", 636 | "label": null, 637 | "logBase": 1, 638 | "max": null, 639 | "min": null, 640 | "show": true 641 | } 642 | ], 643 | "yaxis": { 644 | "align": false, 645 | "alignLevel": null 646 | } 647 | }, 648 | { 649 | "collapsed": false, 650 | "gridPos": { 651 | "h": 1, 652 | "w": 24, 653 | "x": 0, 654 | "y": 31 655 | }, 656 | "id": 14, 657 | "panels": [], 658 | "title": "Sync", 659 | "type": "row" 660 | }, 661 | { 662 | "aliasColors": {}, 663 | "bars": false, 664 | "dashLength": 10, 665 | "dashes": false, 666 | "fill": 1, 667 | "gridPos": { 668 | "h": 8, 669 | "w": 12, 670 | "x": 0, 671 | "y": 32 672 | }, 673 | "id": 6, 674 | "legend": { 675 | "avg": false, 676 | "current": false, 677 | "max": false, 678 | "min": false, 679 | "show": true, 680 | "total": false, 681 | "values": false 682 | }, 683 | "lines": true, 684 | "linewidth": 1, 685 | "links": [], 686 | "nullPointMode": "null", 687 | "paceLength": 10, 688 | "percentage": false, 689 | "pointradius": 2, 690 | "points": false, 691 | "renderer": "flot", 692 | "seriesOverrides": [], 693 | "stack": false, 694 | "steppedLine": false, 695 | "targets": [ 696 | { 697 | "expr": "rate(counter_sync_total[1m])", 698 | "format": "time_series", 699 | "intervalFactor": 1, 700 | "legendFormat": "", 701 | "refId": "A" 702 | } 703 | ], 704 | "thresholds": [], 705 | "timeFrom": null, 706 | "timeRegions": [], 707 | "timeShift": null, 708 | "title": "Counter Synchronizations p/ second", 709 | "tooltip": { 710 | "shared": true, 711 | "sort": 0, 712 | "value_type": "individual" 713 | }, 714 | "type": "graph", 715 | "xaxis": { 716 | "buckets": null, 717 | "mode": "time", 718 | "name": null, 719 | "show": true, 720 | "values": [] 721 | }, 722 | "yaxes": [ 723 | { 724 | "format": "reqps", 725 | "label": null, 726 | "logBase": 1, 727 | "max": null, 728 | "min": null, 729 | "show": true 730 | }, 731 | { 732 | "format": "short", 733 | "label": null, 734 | "logBase": 1, 735 | "max": null, 736 | "min": null, 737 | "show": true 738 | } 739 | ], 740 | "yaxis": { 741 | "align": false, 742 | "alignLevel": null 743 | } 744 | } 745 | ], 746 | "refresh": false, 747 | "schemaVersion": 18, 748 | "style": "dark", 749 | "tags": [], 750 | "templating": { 751 | "list": [] 752 | }, 753 | "time": { 754 | "from": "now-5m", 755 | "to": "now" 756 | }, 757 | "timepicker": { 758 | "refresh_intervals": [ 759 | "5s", 760 | "10s", 761 | "30s", 762 | "1m", 763 | "5m", 764 | "15m", 765 | "30m", 766 | "1h", 767 | "2h", 768 | "1d" 769 | ], 770 | "time_options": [ 771 | "5m", 772 | "15m", 773 | "1h", 774 | "6h", 775 | "12h", 776 | "24h", 777 | "2d", 778 | "7d", 779 | "30d" 780 | ] 781 | }, 782 | "timezone": "", 783 | "title": "R8limiter metrics", 784 | "uid": "pmpag5xWz", 785 | "version": 1 786 | } -------------------------------------------------------------------------------- /env/grafana/dashboards/go-metrics_rev1.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "description": "Golang Application Runtime metrics", 16 | "editable": true, 17 | "gnetId": 10826, 18 | "graphTooltip": 0, 19 | "iteration": 1574430716962, 20 | "links": [], 21 | "panels": [ 22 | { 23 | "aliasColors": {}, 24 | "bars": false, 25 | "dashLength": 10, 26 | "dashes": false, 27 | "fill": 1, 28 | "fillGradient": 0, 29 | "gridPos": { 30 | "h": 8, 31 | "w": 12, 32 | "x": 0, 33 | "y": 0 34 | }, 35 | "id": 26, 36 | "legend": { 37 | "avg": false, 38 | "current": false, 39 | "max": false, 40 | "min": false, 41 | "show": true, 42 | "total": false, 43 | "values": false 44 | }, 45 | "lines": true, 46 | "linewidth": 1, 47 | "links": [], 48 | "nullPointMode": "null", 49 | "options": { 50 | "dataLinks": [] 51 | }, 52 | "paceLength": 10, 53 | "percentage": false, 54 | "pointradius": 2, 55 | "points": false, 56 | "renderer": "flot", 57 | "seriesOverrides": [], 58 | "spaceLength": 10, 59 | "stack": false, 60 | "steppedLine": false, 61 | "targets": [ 62 | { 63 | "expr": "go_memstats_mspan_inuse_bytes{job=\"r8limiter\"}", 64 | "format": "time_series", 65 | "intervalFactor": 1, 66 | "refId": "A" 67 | }, 68 | { 69 | "expr": "go_memstats_mspan_sys_bytes{job=\"r8limiter\"}", 70 | "format": "time_series", 71 | "intervalFactor": 1, 72 | "refId": "B" 73 | }, 74 | { 75 | "expr": "go_memstats_mcache_inuse_bytes{job=\"r8limiter\"}", 76 | "format": "time_series", 77 | "intervalFactor": 1, 78 | "refId": "C" 79 | }, 80 | { 81 | "expr": "go_memstats_mcache_sys_bytes{job=\"r8limiter\"}", 82 | "format": "time_series", 83 | "intervalFactor": 1, 84 | "refId": "D" 85 | }, 86 | { 87 | "expr": "go_memstats_buck_hash_sys_bytes{job=\"r8limiter\"}", 88 | "format": "time_series", 89 | "intervalFactor": 1, 90 | "refId": "E" 91 | }, 92 | { 93 | "expr": "go_memstats_gc_sys_bytes{job=\"r8limiter\"}", 94 | "format": "time_series", 95 | "intervalFactor": 1, 96 | "refId": "F" 97 | }, 98 | { 99 | "expr": "go_memstats_other_sys_bytes - go_memstats_other_sys_bytes", 100 | "format": "time_series", 101 | "intervalFactor": 1, 102 | "legendFormat": "bytes of memory are used for other runtime allocations {pod={{kubernetes_pod_name}}}", 103 | "refId": "G" 104 | }, 105 | { 106 | "expr": "go_memstats_next_gc_bytes", 107 | "format": "time_series", 108 | "intervalFactor": 1, 109 | "refId": "H" 110 | } 111 | ], 112 | "thresholds": [], 113 | "timeFrom": null, 114 | "timeRegions": [], 115 | "timeShift": null, 116 | "title": "Memory in Off-Heap", 117 | "tooltip": { 118 | "shared": true, 119 | "sort": 0, 120 | "value_type": "individual" 121 | }, 122 | "type": "graph", 123 | "xaxis": { 124 | "buckets": null, 125 | "mode": "time", 126 | "name": null, 127 | "show": true, 128 | "values": [] 129 | }, 130 | "yaxes": [ 131 | { 132 | "format": "decbytes", 133 | "label": null, 134 | "logBase": 1, 135 | "max": null, 136 | "min": null, 137 | "show": true 138 | }, 139 | { 140 | "format": "short", 141 | "label": null, 142 | "logBase": 1, 143 | "max": null, 144 | "min": null, 145 | "show": false 146 | } 147 | ], 148 | "yaxis": { 149 | "align": false, 150 | "alignLevel": null 151 | } 152 | }, 153 | { 154 | "aliasColors": {}, 155 | "bars": false, 156 | "dashLength": 10, 157 | "dashes": false, 158 | "fill": 1, 159 | "fillGradient": 0, 160 | "gridPos": { 161 | "h": 8, 162 | "w": 12, 163 | "x": 12, 164 | "y": 0 165 | }, 166 | "id": 12, 167 | "legend": { 168 | "avg": false, 169 | "current": false, 170 | "max": false, 171 | "min": false, 172 | "show": true, 173 | "total": false, 174 | "values": false 175 | }, 176 | "lines": true, 177 | "linewidth": 1, 178 | "links": [], 179 | "nullPointMode": "null", 180 | "options": { 181 | "dataLinks": [] 182 | }, 183 | "paceLength": 10, 184 | "percentage": false, 185 | "pointradius": 2, 186 | "points": false, 187 | "renderer": "flot", 188 | "seriesOverrides": [], 189 | "spaceLength": 10, 190 | "stack": false, 191 | "steppedLine": false, 192 | "targets": [ 193 | { 194 | "expr": "go_memstats_heap_alloc_bytes{job=\"r8limiter\"}", 195 | "format": "time_series", 196 | "intervalFactor": 1, 197 | "refId": "B" 198 | }, 199 | { 200 | "expr": "go_memstats_heap_sys_bytes{job=\"r8limiter\"}", 201 | "format": "time_series", 202 | "intervalFactor": 1, 203 | "refId": "A" 204 | }, 205 | { 206 | "expr": "go_memstats_heap_idle_bytes{job=\"r8limiter\"}", 207 | "format": "time_series", 208 | "intervalFactor": 1, 209 | "refId": "C" 210 | }, 211 | { 212 | "expr": "go_memstats_heap_inuse_bytes{job=\"r8limiter\"}", 213 | "format": "time_series", 214 | "intervalFactor": 1, 215 | "legendFormat": "", 216 | "refId": "D" 217 | }, 218 | { 219 | "expr": "go_memstats_heap_released_bytes{job=\"r8limiter\"}", 220 | "format": "time_series", 221 | "intervalFactor": 1, 222 | "refId": "E" 223 | } 224 | ], 225 | "thresholds": [], 226 | "timeFrom": null, 227 | "timeRegions": [], 228 | "timeShift": null, 229 | "title": "Memory in Heap", 230 | "tooltip": { 231 | "shared": true, 232 | "sort": 0, 233 | "value_type": "individual" 234 | }, 235 | "type": "graph", 236 | "xaxis": { 237 | "buckets": null, 238 | "mode": "time", 239 | "name": null, 240 | "show": true, 241 | "values": [] 242 | }, 243 | "yaxes": [ 244 | { 245 | "format": "decbytes", 246 | "label": null, 247 | "logBase": 1, 248 | "max": null, 249 | "min": null, 250 | "show": true 251 | }, 252 | { 253 | "format": "short", 254 | "label": null, 255 | "logBase": 1, 256 | "max": null, 257 | "min": null, 258 | "show": true 259 | } 260 | ], 261 | "yaxis": { 262 | "align": false, 263 | "alignLevel": null 264 | } 265 | }, 266 | { 267 | "aliasColors": {}, 268 | "bars": false, 269 | "dashLength": 10, 270 | "dashes": false, 271 | "fill": 1, 272 | "fillGradient": 0, 273 | "gridPos": { 274 | "h": 8, 275 | "w": 12, 276 | "x": 0, 277 | "y": 8 278 | }, 279 | "id": 24, 280 | "legend": { 281 | "avg": false, 282 | "current": false, 283 | "max": false, 284 | "min": false, 285 | "show": true, 286 | "total": false, 287 | "values": false 288 | }, 289 | "lines": true, 290 | "linewidth": 1, 291 | "links": [], 292 | "nullPointMode": "null", 293 | "options": { 294 | "dataLinks": [] 295 | }, 296 | "paceLength": 10, 297 | "percentage": false, 298 | "pointradius": 2, 299 | "points": false, 300 | "renderer": "flot", 301 | "seriesOverrides": [], 302 | "spaceLength": 10, 303 | "stack": false, 304 | "steppedLine": false, 305 | "targets": [ 306 | { 307 | "expr": "go_memstats_stack_inuse_bytes{job=\"r8limiter\"}", 308 | "format": "time_series", 309 | "intervalFactor": 1, 310 | "refId": "A" 311 | }, 312 | { 313 | "expr": "go_memstats_stack_sys_bytes{job=\"r8limiter\"}", 314 | "format": "time_series", 315 | "intervalFactor": 1, 316 | "refId": "B" 317 | } 318 | ], 319 | "thresholds": [], 320 | "timeFrom": null, 321 | "timeRegions": [], 322 | "timeShift": null, 323 | "title": "Memory in Stack", 324 | "tooltip": { 325 | "shared": true, 326 | "sort": 0, 327 | "value_type": "individual" 328 | }, 329 | "type": "graph", 330 | "xaxis": { 331 | "buckets": null, 332 | "mode": "time", 333 | "name": null, 334 | "show": true, 335 | "values": [] 336 | }, 337 | "yaxes": [ 338 | { 339 | "format": "decbytes", 340 | "label": null, 341 | "logBase": 1, 342 | "max": null, 343 | "min": null, 344 | "show": true 345 | }, 346 | { 347 | "format": "short", 348 | "label": null, 349 | "logBase": 1, 350 | "max": null, 351 | "min": null, 352 | "show": true 353 | } 354 | ], 355 | "yaxis": { 356 | "align": false, 357 | "alignLevel": null 358 | } 359 | }, 360 | { 361 | "aliasColors": {}, 362 | "bars": false, 363 | "dashLength": 10, 364 | "dashes": false, 365 | "fill": 1, 366 | "fillGradient": 0, 367 | "gridPos": { 368 | "h": 8, 369 | "w": 12, 370 | "x": 12, 371 | "y": 8 372 | }, 373 | "id": 16, 374 | "legend": { 375 | "avg": false, 376 | "current": false, 377 | "max": false, 378 | "min": false, 379 | "show": true, 380 | "total": false, 381 | "values": false 382 | }, 383 | "lines": true, 384 | "linewidth": 1, 385 | "links": [], 386 | "nullPointMode": "null", 387 | "options": { 388 | "dataLinks": [] 389 | }, 390 | "paceLength": 10, 391 | "percentage": false, 392 | "pointradius": 2, 393 | "points": false, 394 | "renderer": "flot", 395 | "seriesOverrides": [], 396 | "spaceLength": 10, 397 | "stack": false, 398 | "steppedLine": false, 399 | "targets": [ 400 | { 401 | "expr": "go_memstats_sys_bytes{job=\"r8limiter\"}", 402 | "format": "time_series", 403 | "intervalFactor": 1, 404 | "refId": "A" 405 | } 406 | ], 407 | "thresholds": [], 408 | "timeFrom": null, 409 | "timeRegions": [], 410 | "timeShift": null, 411 | "title": "Total Used Memory", 412 | "tooltip": { 413 | "shared": true, 414 | "sort": 0, 415 | "value_type": "individual" 416 | }, 417 | "type": "graph", 418 | "xaxis": { 419 | "buckets": null, 420 | "mode": "time", 421 | "name": null, 422 | "show": true, 423 | "values": [] 424 | }, 425 | "yaxes": [ 426 | { 427 | "format": "decbytes", 428 | "label": null, 429 | "logBase": 1, 430 | "max": null, 431 | "min": null, 432 | "show": true 433 | }, 434 | { 435 | "format": "short", 436 | "label": null, 437 | "logBase": 1, 438 | "max": null, 439 | "min": null, 440 | "show": true 441 | } 442 | ], 443 | "yaxis": { 444 | "align": false, 445 | "alignLevel": null 446 | } 447 | }, 448 | { 449 | "aliasColors": {}, 450 | "bars": false, 451 | "dashLength": 10, 452 | "dashes": false, 453 | "fill": 1, 454 | "fillGradient": 0, 455 | "gridPos": { 456 | "h": 8, 457 | "w": 12, 458 | "x": 0, 459 | "y": 16 460 | }, 461 | "id": 22, 462 | "legend": { 463 | "alignAsTable": false, 464 | "avg": false, 465 | "current": false, 466 | "max": false, 467 | "min": false, 468 | "rightSide": false, 469 | "show": true, 470 | "total": false, 471 | "values": false 472 | }, 473 | "lines": true, 474 | "linewidth": 1, 475 | "links": [], 476 | "nullPointMode": "null", 477 | "options": { 478 | "dataLinks": [] 479 | }, 480 | "paceLength": 10, 481 | "percentage": false, 482 | "pointradius": 2, 483 | "points": false, 484 | "renderer": "flot", 485 | "seriesOverrides": [], 486 | "spaceLength": 10, 487 | "stack": false, 488 | "steppedLine": false, 489 | "targets": [ 490 | { 491 | "expr": "go_memstats_mallocs_total{job=\"r8limiter\"} - go_memstats_frees_total{job=\"r8limiter\"}", 492 | "format": "time_series", 493 | "intervalFactor": 1, 494 | "refId": "A" 495 | } 496 | ], 497 | "thresholds": [], 498 | "timeFrom": null, 499 | "timeRegions": [], 500 | "timeShift": null, 501 | "title": "Number of Live Objects", 502 | "tooltip": { 503 | "shared": true, 504 | "sort": 0, 505 | "value_type": "individual" 506 | }, 507 | "type": "graph", 508 | "xaxis": { 509 | "buckets": null, 510 | "mode": "time", 511 | "name": null, 512 | "show": true, 513 | "values": [] 514 | }, 515 | "yaxes": [ 516 | { 517 | "format": "short", 518 | "label": null, 519 | "logBase": 1, 520 | "max": null, 521 | "min": null, 522 | "show": true 523 | }, 524 | { 525 | "format": "short", 526 | "label": null, 527 | "logBase": 1, 528 | "max": null, 529 | "min": null, 530 | "show": false 531 | } 532 | ], 533 | "yaxis": { 534 | "align": false, 535 | "alignLevel": null 536 | } 537 | }, 538 | { 539 | "aliasColors": {}, 540 | "bars": false, 541 | "dashLength": 10, 542 | "dashes": false, 543 | "description": "shows how many heap objects are allocated. This is a counter value so you can use rate() to objects allocated/s.", 544 | "fill": 1, 545 | "fillGradient": 0, 546 | "gridPos": { 547 | "h": 8, 548 | "w": 12, 549 | "x": 12, 550 | "y": 16 551 | }, 552 | "id": 20, 553 | "legend": { 554 | "avg": false, 555 | "current": false, 556 | "max": false, 557 | "min": false, 558 | "show": true, 559 | "total": false, 560 | "values": false 561 | }, 562 | "lines": true, 563 | "linewidth": 1, 564 | "links": [], 565 | "nullPointMode": "null", 566 | "options": { 567 | "dataLinks": [] 568 | }, 569 | "paceLength": 10, 570 | "percentage": false, 571 | "pointradius": 2, 572 | "points": false, 573 | "renderer": "flot", 574 | "seriesOverrides": [], 575 | "spaceLength": 10, 576 | "stack": false, 577 | "steppedLine": false, 578 | "targets": [ 579 | { 580 | "expr": "rate(go_memstats_mallocs_total{job=\"r8limiter\"}[1m])", 581 | "format": "time_series", 582 | "intervalFactor": 1, 583 | "refId": "A" 584 | } 585 | ], 586 | "thresholds": [], 587 | "timeFrom": null, 588 | "timeRegions": [], 589 | "timeShift": null, 590 | "title": "Rate of Objects Allocated", 591 | "tooltip": { 592 | "shared": true, 593 | "sort": 0, 594 | "value_type": "individual" 595 | }, 596 | "type": "graph", 597 | "xaxis": { 598 | "buckets": null, 599 | "mode": "time", 600 | "name": null, 601 | "show": true, 602 | "values": [] 603 | }, 604 | "yaxes": [ 605 | { 606 | "format": "short", 607 | "label": null, 608 | "logBase": 1, 609 | "max": null, 610 | "min": null, 611 | "show": true 612 | }, 613 | { 614 | "format": "short", 615 | "label": null, 616 | "logBase": 1, 617 | "max": null, 618 | "min": null, 619 | "show": true 620 | } 621 | ], 622 | "yaxis": { 623 | "align": false, 624 | "alignLevel": null 625 | } 626 | }, 627 | { 628 | "aliasColors": {}, 629 | "bars": false, 630 | "dashLength": 10, 631 | "dashes": false, 632 | "description": "go_memstats_lookups_total – counts how many pointer dereferences happened. This is a counter value so you can use rate() to lookups/s.", 633 | "fill": 1, 634 | "fillGradient": 0, 635 | "gridPos": { 636 | "h": 8, 637 | "w": 12, 638 | "x": 0, 639 | "y": 24 640 | }, 641 | "id": 18, 642 | "legend": { 643 | "avg": false, 644 | "current": false, 645 | "max": false, 646 | "min": false, 647 | "show": true, 648 | "total": false, 649 | "values": false 650 | }, 651 | "lines": true, 652 | "linewidth": 1, 653 | "links": [], 654 | "nullPointMode": "null", 655 | "options": { 656 | "dataLinks": [] 657 | }, 658 | "paceLength": 10, 659 | "percentage": false, 660 | "pointradius": 2, 661 | "points": false, 662 | "renderer": "flot", 663 | "seriesOverrides": [], 664 | "spaceLength": 10, 665 | "stack": false, 666 | "steppedLine": false, 667 | "targets": [ 668 | { 669 | "expr": "rate(go_memstats_lookups_total{job=\"r8limiter\"}[1m])", 670 | "format": "time_series", 671 | "intervalFactor": 1, 672 | "refId": "A" 673 | } 674 | ], 675 | "thresholds": [], 676 | "timeFrom": null, 677 | "timeRegions": [], 678 | "timeShift": null, 679 | "title": "Rate of a Pointer Dereferences", 680 | "tooltip": { 681 | "shared": true, 682 | "sort": 0, 683 | "value_type": "individual" 684 | }, 685 | "type": "graph", 686 | "xaxis": { 687 | "buckets": null, 688 | "mode": "time", 689 | "name": null, 690 | "show": true, 691 | "values": [] 692 | }, 693 | "yaxes": [ 694 | { 695 | "format": "ops", 696 | "label": null, 697 | "logBase": 1, 698 | "max": null, 699 | "min": null, 700 | "show": true 701 | }, 702 | { 703 | "format": "short", 704 | "label": null, 705 | "logBase": 1, 706 | "max": null, 707 | "min": null, 708 | "show": true 709 | } 710 | ], 711 | "yaxis": { 712 | "align": false, 713 | "alignLevel": null 714 | } 715 | }, 716 | { 717 | "aliasColors": {}, 718 | "bars": false, 719 | "dashLength": 10, 720 | "dashes": false, 721 | "fill": 1, 722 | "fillGradient": 0, 723 | "gridPos": { 724 | "h": 8, 725 | "w": 12, 726 | "x": 12, 727 | "y": 24 728 | }, 729 | "id": 8, 730 | "legend": { 731 | "avg": false, 732 | "current": false, 733 | "max": false, 734 | "min": false, 735 | "show": true, 736 | "total": false, 737 | "values": false 738 | }, 739 | "lines": true, 740 | "linewidth": 1, 741 | "links": [], 742 | "nullPointMode": "null", 743 | "options": { 744 | "dataLinks": [] 745 | }, 746 | "paceLength": 10, 747 | "percentage": false, 748 | "pointradius": 2, 749 | "points": false, 750 | "renderer": "flot", 751 | "seriesOverrides": [], 752 | "spaceLength": 10, 753 | "stack": false, 754 | "steppedLine": false, 755 | "targets": [ 756 | { 757 | "expr": "go_goroutines{job=\"r8limiter\"}", 758 | "format": "time_series", 759 | "intervalFactor": 1, 760 | "refId": "A" 761 | } 762 | ], 763 | "thresholds": [], 764 | "timeFrom": null, 765 | "timeRegions": [], 766 | "timeShift": null, 767 | "title": "Goroutines", 768 | "tooltip": { 769 | "shared": true, 770 | "sort": 0, 771 | "value_type": "individual" 772 | }, 773 | "type": "graph", 774 | "xaxis": { 775 | "buckets": null, 776 | "mode": "time", 777 | "name": null, 778 | "show": true, 779 | "values": [] 780 | }, 781 | "yaxes": [ 782 | { 783 | "format": "short", 784 | "label": null, 785 | "logBase": 1, 786 | "max": null, 787 | "min": null, 788 | "show": true 789 | }, 790 | { 791 | "format": "short", 792 | "label": null, 793 | "logBase": 1, 794 | "max": null, 795 | "min": null, 796 | "show": true 797 | } 798 | ], 799 | "yaxis": { 800 | "align": false, 801 | "alignLevel": null 802 | } 803 | }, 804 | { 805 | "aliasColors": {}, 806 | "bars": false, 807 | "dashLength": 10, 808 | "dashes": false, 809 | "fill": 1, 810 | "fillGradient": 0, 811 | "gridPos": { 812 | "h": 8, 813 | "w": 12, 814 | "x": 0, 815 | "y": 32 816 | }, 817 | "id": 14, 818 | "legend": { 819 | "avg": false, 820 | "current": false, 821 | "max": false, 822 | "min": false, 823 | "show": true, 824 | "total": false, 825 | "values": false 826 | }, 827 | "lines": true, 828 | "linewidth": 1, 829 | "links": [], 830 | "nullPointMode": "null", 831 | "options": { 832 | "dataLinks": [] 833 | }, 834 | "paceLength": 10, 835 | "percentage": false, 836 | "pointradius": 1, 837 | "points": true, 838 | "renderer": "flot", 839 | "seriesOverrides": [], 840 | "spaceLength": 10, 841 | "stack": false, 842 | "steppedLine": false, 843 | "targets": [ 844 | { 845 | "expr": "rate(go_memstats_alloc_bytes_total{job=\"r8limiter\"}[1m])", 846 | "format": "time_series", 847 | "intervalFactor": 1, 848 | "refId": "A" 849 | } 850 | ], 851 | "thresholds": [], 852 | "timeFrom": null, 853 | "timeRegions": [], 854 | "timeShift": null, 855 | "title": "Rates of Allocation", 856 | "tooltip": { 857 | "shared": true, 858 | "sort": 0, 859 | "value_type": "individual" 860 | }, 861 | "type": "graph", 862 | "xaxis": { 863 | "buckets": null, 864 | "mode": "time", 865 | "name": null, 866 | "show": true, 867 | "values": [] 868 | }, 869 | "yaxes": [ 870 | { 871 | "format": "Bps", 872 | "label": null, 873 | "logBase": 1, 874 | "max": null, 875 | "min": null, 876 | "show": true 877 | }, 878 | { 879 | "format": "short", 880 | "label": null, 881 | "logBase": 1, 882 | "max": null, 883 | "min": null, 884 | "show": false 885 | } 886 | ], 887 | "yaxis": { 888 | "align": false, 889 | "alignLevel": null 890 | } 891 | }, 892 | { 893 | "aliasColors": {}, 894 | "bars": false, 895 | "dashLength": 10, 896 | "dashes": false, 897 | "fill": 1, 898 | "fillGradient": 0, 899 | "gridPos": { 900 | "h": 8, 901 | "w": 12, 902 | "x": 12, 903 | "y": 32 904 | }, 905 | "id": 4, 906 | "legend": { 907 | "alignAsTable": false, 908 | "avg": false, 909 | "current": false, 910 | "max": false, 911 | "min": false, 912 | "show": true, 913 | "total": false, 914 | "values": false 915 | }, 916 | "lines": true, 917 | "linewidth": 1, 918 | "links": [], 919 | "nullPointMode": "null", 920 | "options": { 921 | "dataLinks": [] 922 | }, 923 | "paceLength": 10, 924 | "percentage": false, 925 | "pointradius": 2, 926 | "points": false, 927 | "renderer": "flot", 928 | "seriesOverrides": [], 929 | "spaceLength": 10, 930 | "stack": false, 931 | "steppedLine": false, 932 | "targets": [ 933 | { 934 | "expr": "go_gc_duration_seconds{job=\"r8limiter\"}", 935 | "format": "time_series", 936 | "intervalFactor": 1, 937 | "refId": "A" 938 | } 939 | ], 940 | "thresholds": [], 941 | "timeFrom": null, 942 | "timeRegions": [], 943 | "timeShift": null, 944 | "title": "GC duration quantile", 945 | "tooltip": { 946 | "shared": true, 947 | "sort": 0, 948 | "value_type": "individual" 949 | }, 950 | "type": "graph", 951 | "xaxis": { 952 | "buckets": null, 953 | "mode": "time", 954 | "name": null, 955 | "show": true, 956 | "values": [] 957 | }, 958 | "yaxes": [ 959 | { 960 | "format": "ms", 961 | "label": null, 962 | "logBase": 1, 963 | "max": null, 964 | "min": null, 965 | "show": true 966 | }, 967 | { 968 | "format": "short", 969 | "label": null, 970 | "logBase": 1, 971 | "max": null, 972 | "min": null, 973 | "show": true 974 | } 975 | ], 976 | "yaxis": { 977 | "align": false, 978 | "alignLevel": null 979 | } 980 | } 981 | ], 982 | "refresh": "5s", 983 | "schemaVersion": 18, 984 | "style": "dark", 985 | "tags": [ 986 | "go", 987 | "golang" 988 | ], 989 | "templating": { 990 | "list": [] 991 | }, 992 | "time": { 993 | "from": "now-15m", 994 | "to": "now" 995 | }, 996 | "timepicker": { 997 | "refresh_intervals": [ 998 | "5s", 999 | "10s", 1000 | "30s", 1001 | "1m", 1002 | "5m", 1003 | "15m", 1004 | "30m", 1005 | "1h", 1006 | "2h", 1007 | "1d" 1008 | ], 1009 | "time_options": [ 1010 | "5m", 1011 | "15m", 1012 | "1h", 1013 | "6h", 1014 | "12h", 1015 | "24h", 1016 | "2d", 1017 | "7d", 1018 | "30d" 1019 | ] 1020 | }, 1021 | "timezone": "", 1022 | "title": "Go Metrics", 1023 | "uid": "CgCw8jKZz", 1024 | "version": 1 1025 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 12 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 16 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 19 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 20 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 21 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 22 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 23 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= 24 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 25 | github.com/cncf/udpa/go v0.0.0-20191230090109-edbea6a78f6d h1:F6x9XOn7D+HmM4z8vuG/vvlE53rWPWebGLdIy3Nh+XM= 26 | github.com/cncf/udpa/go v0.0.0-20191230090109-edbea6a78f6d/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 27 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 28 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 29 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 30 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 31 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 32 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 34 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 36 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 37 | github.com/envoyproxy/go-control-plane v0.9.0 h1:67WMNTvGrl7V1dWdKCeTwxDr7nio9clKoTlLhwIPnT4= 38 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 39 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 40 | github.com/envoyproxy/go-control-plane v0.9.2 h1:GJ5MKABRjz+QuET1GHm0KD9HC/mAzb3g2FznLQ0aThc= 41 | github.com/envoyproxy/go-control-plane v0.9.2/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 42 | github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= 43 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 44 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 45 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 46 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 47 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 48 | github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= 49 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 50 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 51 | github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= 52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 53 | github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= 54 | github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 55 | github.com/go-redis/redis/v7 v7.0.0-beta.5 h1:7bdbDkv2nKZm6Tydrvmay3xOvVaxpAT4ZsNTrSDMZUE= 56 | github.com/go-redis/redis/v7 v7.0.0-beta.5/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= 57 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 58 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 59 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 60 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 61 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 62 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 63 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 64 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 67 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 69 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 70 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 71 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 72 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 73 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 76 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 77 | github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU= 78 | github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 79 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 80 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 81 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 82 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 83 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 84 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 85 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 86 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 87 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 88 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 89 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 90 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 91 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 92 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 93 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 94 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 95 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 96 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 97 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 98 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 99 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 100 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 101 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 102 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 103 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 104 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 105 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 106 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 107 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 108 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 109 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 110 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 111 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 112 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 113 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 114 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 115 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 116 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 117 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 118 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 119 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 120 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 121 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 122 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 123 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 124 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 125 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 126 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 127 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 128 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 129 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 130 | github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= 131 | github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 132 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 133 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 134 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= 135 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 136 | github.com/peterbourgon/ff v1.7.0 h1:hknvTgsh90jNBIjPq7xeq32Y9AmSbpXvjrFW4sJwW+A= 137 | github.com/peterbourgon/ff v1.7.0/go.mod h1:/KKxnU5cBj4w21jEMj4Rway/kslRP6XAOHh7CH8AyAM= 138 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 139 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 140 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 141 | github.com/pkg/errors v0.9.0 h1:J8lpUdobwIeCI7OiSxHqEwJUKvJwicL5+3v1oe2Yb4k= 142 | github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 143 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 144 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 145 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 146 | github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= 147 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 148 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 149 | github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= 150 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 151 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 152 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 153 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 154 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 155 | github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE= 156 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 157 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 158 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 159 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 160 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 161 | github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= 162 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 163 | github.com/prometheus/common v0.8.0 h1:bLkjvFe2ZRX1DpcgZcdf7j/+MnusEps5hktST/FHA34= 164 | github.com/prometheus/common v0.8.0/go.mod h1:PC/OgXc+UN7B4ALwvn1yzVZmVwvhXp5JsbBv6wSv6i0= 165 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 166 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= 167 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 168 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 169 | github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= 170 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 171 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 172 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 173 | github.com/segmentio/fasthash v1.0.1 h1:U+9f+rh5LxMOquTrEKNw1Z3JgsBlms9QoReNfUo+fws= 174 | github.com/segmentio/fasthash v1.0.1/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= 175 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 176 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 177 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 178 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 179 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 180 | github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= 181 | github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 182 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 183 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 184 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 185 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 186 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 187 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 188 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 189 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 190 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 191 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 192 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 193 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 194 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 195 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 196 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 197 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 198 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 199 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 200 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 201 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 202 | github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= 203 | github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 204 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 205 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 206 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 207 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 208 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 209 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 210 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 211 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 212 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 213 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 214 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 215 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 216 | github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= 217 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 218 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 219 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 220 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 221 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 222 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 223 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 224 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 225 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 226 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 227 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 228 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 229 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 230 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 233 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 234 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 235 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 236 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 237 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 238 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 239 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 242 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 243 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 244 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 248 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 253 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 254 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 256 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA= 260 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 262 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 263 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 264 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 265 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 266 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 267 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 268 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 270 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 271 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 272 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 273 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 274 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 276 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 277 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 278 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 279 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 280 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 281 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 282 | google.golang.org/genproto v0.0.0-20200113173426-e1de0a7b01eb h1:EsMpWw4S8DM2QYm5idfmmWsv2N57GWi2tx3p96Gpja4= 283 | google.golang.org/genproto v0.0.0-20200113173426-e1de0a7b01eb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 284 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 285 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 286 | google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= 287 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 288 | google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= 289 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 290 | google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= 291 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 292 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 293 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 294 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 295 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 296 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 297 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 298 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 299 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 300 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 301 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 302 | gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= 303 | gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 304 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 305 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 306 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 307 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 308 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 309 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 310 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 311 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 312 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 313 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 315 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 316 | --------------------------------------------------------------------------------