├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── Dockerfile ├── cluster.go ├── config.go ├── config.json.example ├── entity.go ├── eval.go ├── expectedShards.go ├── go.mod ├── go.sum ├── implementation.md ├── log.go ├── main.go ├── prometheus.go ├── relay.go ├── relay.md ├── renovate.json ├── server.go └── util.go /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | permissions: 7 | contents: write 8 | packages: write 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | name: Build Docker image 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Login into GitHub Container Registry 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.repository_owner }} 22 | password: ${{ secrets.GH_PAT }} 23 | 24 | - name: Cache Docker layers 25 | uses: actions/cache@v3 26 | with: 27 | path: /tmp/.buildx/cache 28 | key: ${{ runner.os }}-buildx-cache-${{ github.sha }} 29 | restore-keys: | 30 | ${{ runner.os }}-buildx-cache- 31 | 32 | - name: Set up Docker Buildx 33 | id: buildx 34 | uses: docker/setup-buildx-action@v1 35 | 36 | - name: Build and push images 37 | uses: docker/build-push-action@v2 38 | with: 39 | context: . 40 | file: ./Dockerfile 41 | platforms: linux/amd64 42 | push: true 43 | cache-from: type=local,src=/tmp/.buildx/cache 44 | cache-to: type=local,dest=/tmp/.buildx/cache 45 | tags: | 46 | ghcr.io/mikabot/cluster-operator:latest 47 | 48 | deploy: 49 | name: Deploy 50 | needs: 51 | - build 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Setup Kubernetes workflow 55 | run: | 56 | mkdir ~/.kube 57 | echo "${{ secrets.KUBECONFIG }}" >> ~/.kube/config 58 | 59 | - name: Set images 60 | run: | 61 | kubectl set image deployment/beta-operator beta-operator=ghcr.io/mikabot/cluster-operator:latest 62 | kubectl set image deployment/premium-operator premium-operator=ghcr.io/mikabot/cluster-operator:latest 63 | kubectl set image deployment/main-operator main-operator=ghcr.io/mikabot/cluster-operator:latest 64 | 65 | - name: Rollout status 66 | run: | 67 | kubectl rollout status deployment/beta-operator 68 | kubectl rollout status deployment/premium-operator 69 | kubectl rollout status deployment/main-operator 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .env 3 | config.json 4 | *.exe -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine AS builder 2 | RUN apk add git 3 | WORKDIR /app 4 | COPY . . 5 | RUN go get 6 | RUN go build 7 | 8 | FROM alpine:3.14 9 | WORKDIR /app 10 | COPY --from=builder /app/cluster-operator /app/ 11 | CMD ["./cluster-operator"] -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gorilla/websocket" 6 | "github.com/sirupsen/logrus" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type ClusterShardData struct { 12 | ID int `json:"id"` 13 | Block ClusterBlock `json:"block"` 14 | } 15 | 16 | type ClusterBlock struct { 17 | Shards []int `json:"shards"` 18 | Total int `json:"total"` 19 | } 20 | 21 | type ClusterState int 22 | 23 | const ( 24 | ClusterWaiting ClusterState = iota 25 | ClusterConnecting 26 | ClusterReady 27 | ) 28 | 29 | type Cluster struct { 30 | ID int `json:"id"` 31 | Client *websocket.Conn `json:"-"` 32 | PingRecv bool `json:"ping_recv"` 33 | Block ClusterBlock `json:"block"` 34 | State ClusterState `json:"state"` 35 | pingTicker *time.Ticker 36 | mutex *sync.Mutex 37 | statsChan chan map[string]interface{} 38 | } 39 | 40 | type EntityRequest struct { 41 | ID string `json:"id,omitempty"` 42 | Type string `json:"type"` 43 | Args map[string]interface{} `json:"args,omitempty"` 44 | } 45 | 46 | type EntityResponse struct { 47 | ID string `json:"id,omitempty"` 48 | Error string `json:"error,omitempty"` 49 | Data interface{} `json:"data,omitempty"` 50 | } 51 | 52 | type BroadcastEvalRequest struct { 53 | ID string `json:"id"` 54 | Code string `json:"code"` 55 | Timeout int `json:"timeout"` 56 | } 57 | 58 | type BroadcastEvalResponse struct { 59 | ID string `json:"id"` 60 | Results []EvalRes `json:"results"` 61 | } 62 | 63 | type EvalRes struct { 64 | ID string `json:"id,omitempty"` 65 | Res string `json:"res,omitempty"` 66 | Error string `json:"error,omitempty"` 67 | } 68 | 69 | type ApiResponse struct { 70 | Error bool `json:"error,omitempty"` 71 | Message string `json:"message,omitempty"` 72 | Data interface{} `json:"data,omitempty"` 73 | } 74 | 75 | func (c *Cluster) Terminate() { 76 | c.TerminateWithReason(0, "", "disconnected") 77 | } 78 | 79 | func (c *Cluster) TerminateWithReason(code int, reason, logReason string) { 80 | logrus.Infof("Terminating cluster %d", c.ID) 81 | if c.pingTicker != nil { 82 | c.pingTicker.Stop() 83 | } 84 | if code > 0 && len(reason) > 0 { 85 | _ = c.Client.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(code, reason)) 86 | } 87 | _ = c.Client.Close() 88 | if c.ID >= 0 && c.ID < len(Server.Clients) { 89 | Log.PostLog(c, ColorDisconnecting, logReason) 90 | } 91 | c.State = ClusterWaiting 92 | c.pingTicker = nil 93 | } 94 | 95 | func (c *Cluster) FirstShardID() int { 96 | return c.Block.Shards[0] 97 | } 98 | 99 | func (c *Cluster) LastShardID() int { 100 | return c.Block.Shards[len(c.Block.Shards)-1] + 1 101 | } 102 | 103 | func (c *Cluster) HandleMessage(msg *Packet) { 104 | switch msg.Type { 105 | case Handshaking: 106 | c.State = ClusterConnecting 107 | lock.Lock() 108 | c.StartHealthCheck() 109 | logrus.Infof("Giving cluster %d shards %d to %d", c.ID, c.FirstShardID(), c.LastShardID()) 110 | Log.PostLog(c, ColorConnecting, "connecting") 111 | c.Write(ShardData, ClusterShardData{ID: c.ID, Block: c.Block}) 112 | lock.Unlock() 113 | break 114 | case StatsAck: 115 | bytes, err := json.Marshal(msg.Body) 116 | if err != nil { 117 | break 118 | } 119 | stats := make(map[string]interface{}) 120 | err = json.Unmarshal(bytes, &stats) 121 | if err != nil { 122 | break 123 | } 124 | c.statsChan <- stats 125 | break 126 | case PingAck: 127 | c.PingRecv = true 128 | break 129 | case Ready: 130 | c.State = ClusterReady 131 | Log.PostLog(c, ColorReady, "ready") 132 | break 133 | case BroadcastEval: 134 | { 135 | bytes, err := json.Marshal(msg.Body) 136 | if err != nil { 137 | break 138 | } 139 | req := &BroadcastEvalRequest{} 140 | err = json.Unmarshal(bytes, &req) 141 | if err != nil { 142 | break 143 | } 144 | Server.PutEvalChan(req.ID) 145 | results := make([]EvalRes, 0, len(Server.Clients)) 146 | for _, cluster := range Server.Clients { 147 | if cluster.State == ClusterWaiting || cluster.State == ClusterConnecting { 148 | results = append(results, EvalRes{ 149 | Error: "Cluster is not ready!", 150 | }) 151 | continue 152 | } 153 | cluster.Write(Eval, BroadcastEvalRequest{ 154 | ID: req.ID, 155 | Code: req.Code, 156 | Timeout: -1, 157 | }) 158 | select { 159 | case resp := <-Server.GetEvalChan(req.ID): 160 | { 161 | results = append(results, EvalRes{ 162 | ID: "", 163 | Res: resp.Res, 164 | Error: resp.Error, 165 | }) 166 | break 167 | } 168 | case <-time.After(time.Duration(req.Timeout) * time.Millisecond): 169 | { 170 | results = append(results, EvalRes{ 171 | Error: "Response timed out", 172 | }) 173 | break 174 | } 175 | } 176 | } 177 | Server.DeleteEvalChan(req.ID) 178 | c.Write(BroadcastEvalAck, BroadcastEvalResponse{ 179 | ID: req.ID, 180 | Results: results, 181 | }) 182 | } 183 | case Eval: 184 | bytes, err := json.Marshal(msg.Body) 185 | if err != nil { 186 | break 187 | } 188 | res := EvalRes{} 189 | err = json.Unmarshal(bytes, &res) 190 | if err != nil { 191 | break 192 | } 193 | if c := Server.GetEvalChan(res.ID); c != nil { 194 | c <- res 195 | } 196 | case EntityAck: 197 | bytes, err := json.Marshal(msg.Body) 198 | if err != nil { 199 | break 200 | } 201 | res := EntityResponse{} 202 | err = json.Unmarshal(bytes, &res) 203 | if err != nil { 204 | break 205 | } 206 | if c := Server.GetEntityChan(res.ID); c != nil { 207 | c <- res 208 | } 209 | } 210 | } 211 | 212 | func (c *Cluster) Write(t int, data interface{}) { 213 | c.mutex.Lock() 214 | msg, _ := json.Marshal(Packet{ 215 | Type: t, 216 | Body: data, 217 | }) 218 | _ = c.Client.WriteMessage(websocket.TextMessage, msg) 219 | c.mutex.Unlock() 220 | } 221 | 222 | func (c *Cluster) RequestStats() map[string]interface{} { 223 | if c.Client != nil { 224 | c.Write(Stats, nil) 225 | select { 226 | case stats := <-c.statsChan: 227 | return stats 228 | case <-time.After(5 * time.Second): 229 | return nil 230 | } 231 | } 232 | return nil 233 | } 234 | 235 | func (c *Cluster) StartHealthCheck() { 236 | c.pingTicker = time.NewTicker(5 * time.Second) 237 | go func() { 238 | for { 239 | if c.pingTicker == nil { 240 | return 241 | } 242 | select { 243 | case <-c.pingTicker.C: 244 | { 245 | if c.State == ClusterReady { 246 | if !c.PingRecv { 247 | logrus.Warnf("Cluster %d has not responded to the last ping, terminating connection...", c.ID) 248 | c.TerminateWithReason(4001, "No ping received", "unhealthy") 249 | } 250 | c.PingRecv = false 251 | c.Write(Ping, nil) 252 | } 253 | } 254 | } 255 | } 256 | }() 257 | } 258 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/sirupsen/logrus" 6 | "os" 7 | ) 8 | 9 | var Config OperatorConfig 10 | 11 | type Metric struct { 12 | // The name of the metric (REQUIRED) 13 | Name string `json:"name"` 14 | // The metric type (REQUIRED) 15 | Type string `json:"type"` 16 | // What this metric is for (REQUIRED) 17 | Description string `json:"description"` 18 | // Optional labels 19 | Labels []string `json:"labels"` 20 | } 21 | 22 | type OperatorConfig struct { 23 | // The ip to bind to (optional, default 127.0.0.1) 24 | Ip string `json:"ip"` 25 | //The port to listen on (optional, default 3010) 26 | Port int `json:"port"` 27 | // The current environment used for logging purposes (REQUIRED) 28 | Env string `json:"env"` 29 | // The count of clusters that will be expected to connect in order to maintain maximum quorum. (REQUIRED) 30 | Clusters int `json:"clusters"` 31 | // The total amount of shards (REQUIRED) 32 | Shards int `json:"shards"` 33 | // The authentication token to use (REQUIRED) 34 | Auth string `json:"auth"` 35 | // A discord webhook to use for posting cluster related logs 36 | Webhook string `json:"webhook"` 37 | // A prefix to use for metrics, which a metric would resolve to `prod_servers`. 38 | MetricsPrefix string `json:"metricsPrefix"` 39 | // An array of metrics that prometheus will scrape 40 | Metrics []Metric `json:"metrics"` 41 | // If metrics between clusters should be merged together, when this is false, the first clusters metrics will be used 42 | MergeMetrics bool `json:"mergeMetrics"` 43 | // If the cluster operator should log cluster events to the webhook (as defined above) 44 | LogEvents bool `json:"logEvents"` 45 | // If prometheus will export default metrics (false by default). 46 | ExportDefaultMetrics bool `json:"exportDefaultMetrics"` 47 | } 48 | 49 | func init() { 50 | cwd, _ := os.Getwd() 51 | file, err := os.ReadFile(cwd + "/config.json") 52 | if err != nil { 53 | logrus.Fatal("Failed to load config file!") 54 | return 55 | } 56 | err = json.Unmarshal(file, &Config) 57 | if err != nil { 58 | logrus.Fatal("Failed to decode the config file, this could be that the file is corrupt, or malformed!") 59 | return 60 | } 61 | if Config.Ip == "" { 62 | Config.Ip = "127.0.0.1" 63 | } 64 | if Config.Port == 0 { 65 | Config.Port = 3010 66 | } 67 | if Config.Env == "" && Config.LogEvents { 68 | logrus.Fatal("An env name should be set when you're using logs!") 69 | } 70 | if Config.Clusters < 1 { 71 | logrus.Fatal("Cluster count should be greater than 0!") 72 | } 73 | if Config.Shards < 1 { 74 | logrus.Fatal("Shard count should be greater than 0!") 75 | } 76 | if len(Config.Metrics) > 0 && Config.MetricsPrefix == "" { 77 | logrus.Warn("You have set multiple metrics, but no metrics prefix; ignore this warning if you know what you're doing! However, a metric with the name 'ping' can be overwritten by any other cluster operators that run on your server, that prometheus scrapes data from!") 78 | } 79 | if len(Config.Metrics) > 0 { 80 | for i, metric := range Config.Metrics { 81 | if metric.Name == "" { 82 | logrus.Fatalf("metrics[%d].name is a required field!", i) 83 | } 84 | if metric.Description == "" { 85 | logrus.Fatalf("metrics[%d].description is a required field!", i) 86 | } 87 | if metric.Type == "" { 88 | logrus.Fatalf("metrics[%d].type is a required field!", i) 89 | } 90 | if !(metric.Type == "gauge" || metric.Type == "counter") { 91 | logrus.Fatalf("metrics[%d].type should be gauge or collector, received: %s!", i, metric.Type) 92 | } 93 | } 94 | } 95 | logrus.Info("Found and loaded config.json!") 96 | } 97 | 98 | func MetricPrefix(key string) string { 99 | return Config.MetricsPrefix + key 100 | } 101 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "env": "Prod", // env name to use for logs (required) 3 | "clusters": 1, // cluster count 4 | "shards": 1, // shard count 5 | "auth": "", // WS/HTTP authentication 6 | "webhook": "", // Where to log cluster related events 7 | "metricsPrefix": "mika_alpha_", 8 | "metrics": [ // this array is optional 9 | { 10 | "key": "servers", // required 11 | "type": "counter", // required 12 | "description": "Server counter!" // required by prometheus 13 | } 14 | ... 15 | ], 16 | "mergeMetrics": true, // if this is false, metrics will be from the FIRST cluster only 17 | "logEvents": false, // if events should be logged 18 | "exportDefaultMetrics": false // if default metrics should be exported 19 | } -------------------------------------------------------------------------------- /entity.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/sirupsen/logrus" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type EntityHandler struct{} 12 | 13 | func (_ *EntityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 14 | if r.Method != "POST" { 15 | writeJson(w, 405, ApiResponse{Error: true, Message: "Method not allowed"}) 16 | return 17 | } 18 | if r.Header.Get("Authorization") == "" { 19 | writeJson(w, 403, ApiResponse{Error: true, Message: "Unauthorized"}) 20 | return 21 | } 22 | if r.Header.Get("Authorization") != Config.Auth { 23 | writeJson(w, 403, ApiResponse{Error: true, Message: "Forbidden"}) 24 | return 25 | } 26 | if strings.Index(r.Header.Get("Content-Type"), "application/json") == -1 { 27 | writeJson(w, 400, ApiResponse{Error: true, Message: "Content-Type either not found, or not application/json!"}) 28 | return 29 | } 30 | body := &EntityRequest{} 31 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 32 | logrus.Errorf("Failed to decode JSON body for entity request: %s", err.Error()) 33 | writeJson(w, 500, ApiResponse{Error: true, Message: "Unable to decode JSON body!"}) 34 | return 35 | } 36 | if body.ID == "" { 37 | writeJson(w, 400, ApiResponse{Error: true, Message: "ID not specified!"}) 38 | return 39 | } 40 | if body.Type == "" { 41 | writeJson(w, 400, ApiResponse{Error: true, Message: "type not specified!"}) 42 | return 43 | } 44 | Server.PutEntityChan(body.ID) 45 | results := make([]EntityResponse, 0, len(Server.Clients)) 46 | for _, cluster := range Server.Clients { 47 | if cluster.State == ClusterWaiting || cluster.State == ClusterConnecting { 48 | results = append(results, EntityResponse{Error: "cluster unhealthy"}) 49 | continue 50 | } 51 | cluster.Write(Entity, body) 52 | select { 53 | case resp := <-Server.GetEntityChan(body.ID): 54 | { 55 | results = append(results, EntityResponse{ 56 | ID: "", 57 | Error: resp.Error, 58 | Data: resp.Data, 59 | }) 60 | break 61 | } 62 | case <-time.After(5 * time.Second): 63 | { 64 | results = append(results, EntityResponse{Error: "timed out"}) 65 | break 66 | } 67 | } 68 | } 69 | Server.DeleteEntityChan(body.ID) 70 | writeJson(w, 200, ApiResponse{Data: results}) 71 | } 72 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/sirupsen/logrus" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type EvalHandler struct{} 12 | 13 | func writeJson(w http.ResponseWriter, status int, body interface{}) { 14 | w.Header().Set("Content-Type", "application/json") 15 | w.WriteHeader(status) 16 | _ = json.NewEncoder(w).Encode(body) 17 | } 18 | 19 | func (_ *EvalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | if r.Method != "POST" { 21 | writeJson(w, 405, ApiResponse{Error: true, Message: "Method not allowed"}) 22 | return 23 | } 24 | if r.Header.Get("Authorization") == "" { 25 | writeJson(w, 403, ApiResponse{Error: true, Message: "Unauthorized"}) 26 | return 27 | } 28 | if r.Header.Get("Authorization") != Config.Auth { 29 | writeJson(w, 403, ApiResponse{Error: true, Message: "Forbidden"}) 30 | return 31 | } 32 | if strings.Index(r.Header.Get("Content-Type"), "application/json") == -1 { 33 | writeJson(w, 400, ApiResponse{Error: true, Message: "Content-Type either not found, or not application/json!"}) 34 | return 35 | } 36 | body := &BroadcastEvalRequest{} 37 | if err := json.NewDecoder(r.Body).Decode(&body); err != nil { 38 | logrus.Errorf("Failed to decode JSON body for eval request: %s", err.Error()) 39 | writeJson(w, 500, ApiResponse{Error: true, Message: "Unable to decode JSON body!"}) 40 | return 41 | } 42 | if body.ID == "" { 43 | writeJson(w, 400, ApiResponse{Error: true, Message: "Eval ID not specified!"}) 44 | return 45 | } 46 | if body.Code == "" { 47 | writeJson(w, 400, ApiResponse{Error: true, Message: "Code not specified!"}) 48 | return 49 | } 50 | if body.Timeout == 0 { 51 | writeJson(w, 400, ApiResponse{Error: true, Message: "Timeout not specified!"}) 52 | return 53 | } 54 | Server.PutEvalChan(body.ID) 55 | results := make([]EvalRes, 0, len(Server.Clients)) 56 | for _, cluster := range Server.Clients { 57 | if cluster.State == ClusterConnecting { 58 | results = append(results, EvalRes{Error: "Cluster is connecting!"}) 59 | continue 60 | } 61 | if cluster.State == ClusterWaiting { 62 | results = append(results, EvalRes{Error: "Cluster is not ready!"}) 63 | continue 64 | } 65 | cluster.Write(Eval, BroadcastEvalRequest{ 66 | ID: body.ID, 67 | Code: body.Code, 68 | Timeout: -1, 69 | }) 70 | select { 71 | case resp := <-Server.GetEvalChan(body.ID): 72 | { 73 | results = append(results, EvalRes{ 74 | ID: "", 75 | Res: resp.Res, 76 | Error: resp.Error, 77 | }) 78 | break 79 | } 80 | case <-time.After(time.Duration(body.Timeout) * time.Millisecond): 81 | { 82 | results = append(results, EvalRes{Error: "Response timed out"}) 83 | break 84 | } 85 | } 86 | } 87 | writeJson(w, 200, ApiResponse{Data: results}) 88 | } 89 | -------------------------------------------------------------------------------- /expectedShards.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ExpectedShardHandler struct{} 8 | 9 | func (_ *ExpectedShardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 10 | if r.Method != "GET" { 11 | writeJson(w, 405, ApiResponse{Error: true, Message: "Method not allowed"}) 12 | return 13 | } 14 | if r.Header.Get("Authorization") == "" { 15 | writeJson(w, 403, ApiResponse{Error: true, Message: "Unauthorized"}) 16 | return 17 | } 18 | if r.Header.Get("Authorization") != Config.Auth { 19 | writeJson(w, 403, ApiResponse{Error: true, Message: "Forbidden"}) 20 | return 21 | } 22 | writeJson(w, 200, ApiResponse{Data: Config.Shards}) 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cluster-operator 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 7 | github.com/prometheus/client_golang v1.11.0 8 | github.com/sirupsen/logrus v1.8.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 5 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 6 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 7 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 8 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 14 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 15 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 16 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 17 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 18 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 19 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 20 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 21 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 22 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 26 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 28 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 29 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 30 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 31 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 32 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 33 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 34 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 35 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 36 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 37 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 38 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 39 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 40 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 41 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 42 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 44 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 46 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 47 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 48 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 49 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 50 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 51 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 52 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 54 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 55 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 56 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 57 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 58 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 59 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 60 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 61 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 62 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 63 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 64 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 65 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 66 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 67 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 68 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 69 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 70 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 71 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 72 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 73 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 74 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 75 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 80 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 81 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 82 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 83 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 84 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 85 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 86 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 87 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 88 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 89 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 90 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 95 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 98 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 99 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 100 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 101 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 102 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 103 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 104 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 105 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 106 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 107 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 108 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 109 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 110 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 111 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 112 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 113 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 114 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 115 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 116 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 117 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 118 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 119 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 120 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 121 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 122 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 123 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 124 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 125 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 126 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 127 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 128 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 129 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 130 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 131 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 132 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 133 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 134 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 135 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 136 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 137 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 138 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 139 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 140 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 141 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 142 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 143 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 144 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 145 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 146 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 147 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 148 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 149 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 150 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 151 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 152 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 153 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 154 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 155 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 156 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 157 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 158 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 159 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 160 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 161 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 162 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 163 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 164 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 165 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 166 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 167 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 168 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 169 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 170 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 171 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 172 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 173 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 174 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 175 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 176 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 177 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 178 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 179 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 180 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 181 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 182 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 183 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 184 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 185 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 186 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 187 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 188 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 189 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 190 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 191 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 192 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 193 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 194 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 195 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 196 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 197 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 198 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 199 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 200 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 201 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 202 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 203 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 204 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 205 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 206 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 207 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 208 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 209 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 210 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 211 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 212 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 213 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 214 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 215 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 216 | github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg= 217 | github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= 218 | github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= 219 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 220 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 221 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 222 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 223 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 224 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 225 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 226 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 227 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 228 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 229 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 230 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 231 | github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y= 232 | github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= 233 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 234 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 235 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 236 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 237 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 238 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 239 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 240 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 241 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 242 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 243 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 244 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 245 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 246 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 247 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 248 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 249 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 250 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 251 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 252 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 253 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 254 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 255 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 256 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 257 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 258 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 259 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 260 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 261 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 262 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 263 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 264 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 265 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 266 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 267 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 268 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 269 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 270 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 271 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 272 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 273 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 274 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 275 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 276 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 277 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 278 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 279 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 280 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 281 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 282 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 283 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 284 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 285 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 286 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 287 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 288 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 289 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 290 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 291 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 292 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 293 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 294 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 295 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 296 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 297 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 298 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 299 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 300 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 301 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 302 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 303 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 304 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 305 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 306 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 307 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 309 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 311 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 312 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 313 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 314 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 315 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 316 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 317 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 318 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 319 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 320 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 327 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 328 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 330 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 331 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 332 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 333 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 334 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 335 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 336 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= 349 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= 351 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 352 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 353 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 354 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 355 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 356 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 357 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 358 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 359 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 360 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 361 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 362 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 363 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 364 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 365 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 366 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 367 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 368 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 369 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 370 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 371 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 372 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 373 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 374 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 375 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 376 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 377 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 378 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 379 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 380 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 381 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 382 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 383 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 384 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 385 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 386 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 387 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 388 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 389 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 390 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 391 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 392 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 393 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 394 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 395 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 396 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 397 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 398 | google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= 399 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 400 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 401 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 402 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 403 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 404 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 405 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 406 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 407 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 408 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 409 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 410 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 411 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 412 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 413 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 414 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 416 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 417 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 418 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 419 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 420 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 421 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 422 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 423 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 424 | -------------------------------------------------------------------------------- /implementation.md: -------------------------------------------------------------------------------- 1 | # Implementation 2 | 3 | Here is a guide of how you can learn how to implement all the JSON spec. 4 | 5 | # Packet structure 6 | 7 | Packets to and from the server have the following structure 8 | 9 | | Field | Type | Description | 10 | |-------|------|------| 11 | | type | number | The packet type, 0 to 11 12 | | body | any | The body of the packet 13 | 14 | An example packet is provided below. 15 | ```json 16 | { 17 | "type": 1, 18 | "body": { 19 | "id": 0, 20 | "block": { 21 | "shards": [0], 22 | "total": 1 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | # Handshaking 29 | 30 | Handshaking is a broad term here, because for one it's used to define the connection to a WebSocket, and for this operator it's defined to alert the operator this cluster is connecting. 31 | 32 | You should send a handshaking packet as soon as the WebSocket opens: 33 | ```json 34 | { 35 | "type": 0 36 | } 37 | ``` 38 | 39 | # Receiving shard data 40 | 41 | When you have successfully sent a type 1 payload to the operator, you will now receive an event containing cluster information (such as the ID, and the shards you'll be handling). 42 | 43 | It will look like so: 44 | ```json 45 | { 46 | "type": 1, 47 | "body": { 48 | "id": 0, 49 | "block": { 50 | "shards": [0], 51 | "total": 1 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | When you have this data, and when your client turns ready, send a ready event: 58 | ```json 59 | { "type": 9 } 60 | ``` 61 | 62 | # Pings 63 | Now we move onto the topic of health checking, the default time the operator checks if your cluster is alive is 5 seconds (this cannot be changed). 64 | If you fail to acknowledge a ping, your cluster will be terminated; furthermore, forcing you to reconnect entirely. 65 | 66 | You should receive a packet like so: 67 | ```json 68 | { "type": 2 } 69 | ``` 70 | 71 | Then, you need to respond like so: 72 | ```json 73 | { "type": 3 } 74 | ``` 75 | 76 | # Eval 77 | Sometimes you might want to evaluate something on all clusters (generally, this should be restricted to the developers of your bot.) 78 | 79 | The field "body.id" should be a randomly generated cryptographic string, to avoid eval ID conflicts. 80 | An example eval payload is below: 81 | ```json 82 | { 83 | "type": 5, 84 | "body": { 85 | "id": "1", 86 | "code": "1 + 1" 87 | } 88 | } 89 | ``` 90 | 91 | On the cluster side, when you get this payload, you'll need to respond to it. 92 | 93 | An optional field `body.error` (string) can be specified to indicate an evaluation error (it is recommended NOT to include `body.res`, but it's up to you.) 94 | 95 | ### Example 96 | ```json 97 | { 98 | "type": 4, 99 | "body": { 100 | "id": "1", 101 | "res": "2" 102 | } 103 | } 104 | ``` 105 | 106 | When all clusters have hopefully responded, you will now get a packet with type 6. 107 | NOTE: Should any error occur with eval, instead of the `res` field, your client should send the `error` field instead. 108 | 109 | ```json 110 | { 111 | "type": 6, 112 | "body": { 113 | "results": [ 114 | { 115 | "res": "2" 116 | } 117 | ] 118 | } 119 | } 120 | ``` 121 | 122 | The second way to evaluate is using 123 | 124 | `POST /eval` 125 | 126 | | Header | Value | 127 | |-------|-------| 128 | | Authorization | The token you have in the operators config. | 129 | 130 | An example is provided below: 131 | ```json 132 | { 133 | "id": "1", 134 | "code": "1 + 1", 135 | "timeout": 5000 136 | } 137 | ``` 138 | 139 | You should then get a response similar to the following: 140 | ```json 141 | { 142 | "data": [ 143 | { 144 | "res": "2" 145 | } 146 | ] 147 | } 148 | ``` 149 | 150 | # Metrics 151 | When prometheus is configured properly, the cluster operator will send a type 7 packet, to collect statistics. 152 | 153 | **IMPORTANT NOTE:** There is a special label called `cluster`, when set it will use the cluster ID as the label value. 154 | 155 | 156 | Assuming, your operator metrics config was 157 | ```json 158 | { 159 | "metrics": [ 160 | {"type": "gauge", "name": "messages", "description": "Messages seen"}, 161 | {"type": "gauge", "name": "commands", "description": "Command usage", "labels": ["name"]} 162 | ] 163 | } 164 | ``` 165 | 166 | You should respond like so: 167 | ```json 168 | { 169 | "type": 8, 170 | "body": { 171 | "messages": 42, 172 | "commands": { 173 | "help": 42 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | # Entities 180 | Instead of evaluating data you want, you should use entities, it will be more secure than evaluating the data you want. 181 | **especially if you rely on user input for those entities.** 182 | 183 | To get started send a request like so 184 | 185 | `POST /entity` 186 | 187 | | Header | Value | 188 | |-------|-------| 189 | | Authorization | The token you have in the operators config. | 190 | 191 | Again, the `id` field should be a randomly generated ID and an optional property `args` can also be specified, this is an object. 192 | 193 | The body should be like so. 194 | 195 | ```json 196 | { 197 | "id": "1", 198 | "type": "hello" 199 | } 200 | ``` 201 | 202 | Now, your client will get a payload similar to (NOTE: If you provided `args` that field will be present.): 203 | ```json 204 | { 205 | "type": 10, 206 | "body": { 207 | "id": "1", 208 | "type": "hello" 209 | } 210 | } 211 | ``` 212 | 213 | Your client should respond like so. 214 | 215 | **NOTE: THE ID MUST MATCH THE ONE IN THE REQUEST, OR THE REQUEST WILL TIMEOUT** 216 | ```json 217 | { 218 | "type": 11, 219 | "body": { 220 | "id": "1", 221 | "data": "world" 222 | } 223 | } 224 | ``` 225 | 226 | You should now get an HTTP response like so, entries are ordered by cluster ID: 227 | ```json 228 | { 229 | "data": [ 230 | { 231 | "res": "world" 232 | }, 233 | { 234 | "error": "cluster unhealthy" 235 | } 236 | ] 237 | } 238 | ``` -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/sirupsen/logrus" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | const ( 13 | ColorConnecting = 0x0E6CF7 14 | ColorReady = 0x00DB62 15 | ColorDisconnecting = 0xFF4444 16 | ) 17 | 18 | type Logger struct { 19 | webhookUrl string 20 | client *http.Client 21 | } 22 | 23 | var ( 24 | Log *Logger 25 | ) 26 | 27 | type Embed struct { 28 | Color int `json:"color"` 29 | Description string `json:"description"` 30 | } 31 | 32 | type WebhookBody struct { 33 | Embeds []Embed `json:"embeds"` 34 | } 35 | 36 | func NewLogger() { 37 | if Log != nil { 38 | panic("Tried to initialise another logger instance.") 39 | } 40 | Log = &Logger{ 41 | webhookUrl: Config.Webhook, 42 | client: &http.Client{ 43 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 44 | return nil 45 | }, 46 | }, 47 | } 48 | logrus.Info("Created a logger instance!") 49 | } 50 | 51 | func (log *Logger) makeRequest(embed Embed) (*http.Request, error) { 52 | marshaled, err := json.Marshal(WebhookBody{ 53 | Embeds: []Embed{embed}, 54 | }) 55 | if err != nil { 56 | return nil, err 57 | } 58 | req, err := http.NewRequest("POST", log.webhookUrl, bytes.NewBuffer(marshaled)) 59 | if err != nil { 60 | return nil, err 61 | } 62 | req.Header.Add("Content-Type", "application/json") 63 | req.Header.Add("User-Agent", fmt.Sprintf("DiscordBot (%s, 1.0.0)", "https://mikabot.gg")) 64 | return req, nil 65 | } 66 | 67 | func (log *Logger) currentDate() string { 68 | now := time.Now() 69 | return fmt.Sprintf( 70 | "%s %02d %02d %02d:%02d:%02d", 71 | now.Month().String(), 72 | now.Day(), 73 | now.Year(), 74 | now.Hour(), 75 | now.Minute(), 76 | now.Second(), 77 | ) 78 | } 79 | 80 | func (log *Logger) description(c *Cluster, event string) string { 81 | return fmt.Sprintf( 82 | "`[%s]` | Cluster `%d` %s | Shards `%d` - `%d` | %s", 83 | log.currentDate(), 84 | c.ID, 85 | event, 86 | c.FirstShardID(), 87 | c.LastShardID(), 88 | Config.Env, 89 | ) 90 | } 91 | 92 | func (log *Logger) operator(event string) string { 93 | return fmt.Sprintf( 94 | "`[%s]` | %s | %s", 95 | log.currentDate(), 96 | event, 97 | Config.Env, 98 | ) 99 | } 100 | 101 | func (log *Logger) PostLog(c *Cluster, color int, event string) { 102 | if !Config.LogEvents { 103 | return 104 | } 105 | s := time.Now() 106 | req, err := log.makeRequest(Embed{ 107 | Color: color, 108 | Description: log.description(c, event), 109 | }) 110 | if err != nil { 111 | logrus.Errorf("PostLog failed to make a request: %s", err.Error()) 112 | return 113 | } 114 | res, err := log.client.Do(req) 115 | if err != nil { 116 | logrus.Errorf("PostLog failed to get a response: %s", err.Error()) 117 | return 118 | } 119 | logrus.Debugf("Got status %s from Discord in %s, when firing event %s!", res.Status, time.Now().Sub(s).String(), event) 120 | } 121 | 122 | func (log *Logger) PostOperatorLog(color int, event string) { 123 | if !Config.LogEvents { 124 | return 125 | } 126 | s := time.Now() 127 | req, err := log.makeRequest(Embed{ 128 | Color: color, 129 | Description: log.operator(event), 130 | }) 131 | if err != nil { 132 | logrus.Errorf("PostOperatorLog failed to make a request: %s", err.Error()) 133 | return 134 | } 135 | res, err := log.client.Do(req) 136 | if err != nil { 137 | logrus.Errorf("PostOperatorLog failed to get a response: %s", err.Error()) 138 | return 139 | } 140 | logrus.Debugf("Got status %s from Discord in %s, when posting operator log!", res.Status, time.Now().Sub(s).String()) 141 | } 142 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/websocket" 6 | "github.com/sirupsen/logrus" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | ) 12 | 13 | func init() { 14 | logrus.SetFormatter(&logrus.TextFormatter{ 15 | DisableColors: true, 16 | }) 17 | logrus.SetLevel(logrus.DebugLevel) 18 | } 19 | 20 | func main() { 21 | NewLogger() 22 | Server = &WSServer{ 23 | Clients: []*Cluster{}, 24 | Upgrader: websocket.Upgrader{}, 25 | ChanMutex: &sync.RWMutex{}, 26 | Channels: make(map[string]chan EvalRes), 27 | EntityMutex: &sync.RWMutex{}, 28 | EntityChannels: make(map[string]chan EntityResponse), 29 | } 30 | CreateClusters(Config.Shards, Config.Clusters) 31 | Log.PostOperatorLog(ColorReady, fmt.Sprintf( 32 | "Operator is online and will be handling %d shards with %d clusters, that's about %d shard(s) per cluster!", 33 | Config.Shards, 34 | Config.Clusters, 35 | Config.Shards/Config.Clusters, 36 | )) 37 | go Server.Listen() 38 | c := make(chan os.Signal, 1) 39 | signal.Notify(c, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGABRT) 40 | s := <-c 41 | Log.PostOperatorLog(ColorDisconnecting, fmt.Sprintf("Operator is going offline, no further clusters can connect (exit code: %s)!", s)) 42 | } 43 | -------------------------------------------------------------------------------- /prometheus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | ) 11 | 12 | type Collector struct { 13 | metric string 14 | original prometheus.Collector 15 | } 16 | 17 | type MetricsHandler struct { 18 | metrics []Collector 19 | clusterCount prometheus.Gauge 20 | shardCount prometheus.Gauge 21 | } 22 | 23 | var ( 24 | registry = prometheus.NewRegistry() 25 | prometheusHandler = promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 26 | ) 27 | 28 | func findMetricByName(name string) *Metric { 29 | for _, metric := range Config.Metrics { 30 | if metric.Name == name { 31 | return &metric 32 | } 33 | } 34 | return nil 35 | } 36 | 37 | // Setup registers all metrics that the user has defined in their config.json 38 | // This also exposes 2 default metrics (unless disabled), cluster_count and shard_count 39 | func (h *MetricsHandler) Setup() { 40 | h.clusterCount = prometheus.NewGauge(prometheus.GaugeOpts{ 41 | Name: MetricPrefix("cluster_count"), 42 | Help: "Total clusters!", 43 | }) 44 | h.shardCount = prometheus.NewGauge(prometheus.GaugeOpts{ 45 | Name: MetricPrefix("shard_count"), 46 | Help: "Total shards!", 47 | }) 48 | registry.MustRegister(h.clusterCount, h.shardCount) 49 | for _, metric := range Config.Metrics { 50 | var collector prometheus.Collector 51 | if metric.Type == "gauge" && len(metric.Labels) > 0 { 52 | collector = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 53 | Name: MetricPrefix(metric.Name), 54 | Help: metric.Description, 55 | }, metric.Labels) 56 | } else if metric.Type == "gauge" && len(metric.Labels) < 1 { 57 | collector = prometheus.NewGauge(prometheus.GaugeOpts{ 58 | Name: MetricPrefix(metric.Name), 59 | Help: metric.Description, 60 | }) 61 | } else if metric.Type == "counter" && len(metric.Labels) > 0 { 62 | collector = prometheus.NewCounterVec(prometheus.CounterOpts{ 63 | Name: MetricPrefix(metric.Name), 64 | Help: metric.Description, 65 | }, metric.Labels) 66 | } else if metric.Type == "counter" && len(metric.Labels) < 1 { 67 | collector = prometheus.NewCounter(prometheus.CounterOpts{ 68 | Name: MetricPrefix(metric.Name), 69 | Help: metric.Description, 70 | }) 71 | } 72 | h.metrics = append(h.metrics, Collector{ 73 | metric: metric.Name, 74 | original: collector, 75 | }) 76 | if err := registry.Register(collector); err == nil { 77 | logrus.Infof("Picked up and registered metric %s as type %s", metric.Name, metric.Type) 78 | } else { 79 | logrus.Errorf("Failed to register metric %s: %s", metric.Name, err.Error()) 80 | } 81 | } 82 | } 83 | 84 | // This function will merge all cluster metrics into a single map of objects. 85 | // Just a reminder that, anything as a nested object will be treated as a LABELED metric, and expects it's children stats to also be maps with a number as it's value. 86 | // Cluster is a special label, representing the current cluster. 87 | func (h *MetricsHandler) mergeMetrics(metrics []map[string]interface{}) map[string]interface{} { 88 | merged := make(map[string]interface{}) 89 | for i, metric := range metrics { 90 | for key, child := range metric { 91 | m := findMetricByName(key) 92 | if m == nil { 93 | logrus.Warnf("Cluster %d has an unknown metric field %s!", i, key) 94 | continue 95 | } 96 | if f, ok := child.(float64); ok { 97 | if len(m.Labels) > 0 && m.Labels[0] == "cluster" { 98 | if _, ok := merged[key]; ok { 99 | merged[key].(map[string]interface{})[strconv.Itoa(i)] = f 100 | } else { 101 | merged[key] = map[string]interface{}{strconv.Itoa(i): f} 102 | } 103 | } else { 104 | if v, ok := merged[key]; ok { 105 | merged[key] = v.(float64) + f 106 | } else { 107 | merged[key] = f 108 | } 109 | } 110 | } 111 | if m, ok := child.(map[string]interface{}); ok { 112 | for k, v := range m { 113 | if f, ok := v.(float64); ok { 114 | if mergedMap, ok := merged[key].(map[string]interface{}); ok { 115 | if v1, ok := mergedMap[k]; ok { 116 | mergedMap[k] = v1.(float64) + f 117 | } else { 118 | mergedMap[k] = f 119 | } 120 | merged[key] = mergedMap 121 | } else { 122 | merged[key] = map[string]interface{}{ 123 | k: f, 124 | } 125 | } 126 | } else { 127 | logrus.Errorf("Expected key %s to be an integer, got %s instead!", k, reflect.TypeOf(v).Name()) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | return merged 134 | } 135 | 136 | func (h *MetricsHandler) findCollector(name string) *Collector { 137 | for _, c := range h.metrics { 138 | if c.metric == name { 139 | return &c 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | func (h *MetricsHandler) setCollector(labels prometheus.Labels, val float64, c *Collector) { 146 | if v, ok := c.original.(prometheus.Gauge); ok { 147 | v.Set(val) 148 | } else if v, ok := c.original.(*prometheus.GaugeVec); ok { 149 | v.With(labels).Set(val) 150 | } else if v, ok := c.original.(prometheus.Counter); ok { 151 | v.Add(val) 152 | } else if v, ok := c.original.(*prometheus.CounterVec); ok { 153 | v.With(labels).Add(val) 154 | } 155 | } 156 | 157 | func (h *MetricsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 158 | if GetHealthyClusters() < 1 { 159 | w.WriteHeader(500) 160 | return 161 | } 162 | clusterMetrics := make([]map[string]interface{}, 0, len(Server.Clients)) 163 | for _, cluster := range Server.Clients { 164 | stats := cluster.RequestStats() 165 | if stats == nil { 166 | continue 167 | } 168 | clusterMetrics = append(clusterMetrics, stats) 169 | } 170 | if Config.ExportDefaultMetrics { 171 | h.clusterCount.Set(float64(Config.Clusters)) 172 | h.shardCount.Set(float64(Config.Shards)) 173 | } 174 | metrics := make(map[string]interface{}) 175 | if !Config.MergeMetrics { 176 | metrics = clusterMetrics[0] 177 | } else { 178 | metrics = h.mergeMetrics(clusterMetrics) 179 | } 180 | for key, child := range metrics { 181 | c := h.findCollector(key) 182 | if c == nil { 183 | continue 184 | } 185 | m := findMetricByName(c.metric) 186 | if m == nil { 187 | continue 188 | } 189 | if f, ok := child.(float64); ok { 190 | h.setCollector(nil, f, c) 191 | } 192 | if data, ok := child.(map[string]interface{}); ok { 193 | for key, v := range data { 194 | f := v.(float64) 195 | labels := prometheus.Labels{} 196 | for _, label := range m.Labels { 197 | labels[label] = key 198 | } 199 | h.setCollector(labels, f, c) 200 | } 201 | } 202 | } 203 | prometheusHandler.ServeHTTP(w, req) 204 | } 205 | -------------------------------------------------------------------------------- /relay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | type RelayHandler struct { 10 | mutex *sync.RWMutex 11 | clients map[string]*websocket.Conn 12 | } 13 | 14 | func (rh *RelayHandler) putClient(id string, con *websocket.Conn) { 15 | rh.mutex.Lock() 16 | rh.clients[id] = con 17 | rh.mutex.Unlock() 18 | } 19 | 20 | func (rh *RelayHandler) deleteClient(id string) { 21 | rh.mutex.Lock() 22 | delete(rh.clients, id) 23 | rh.mutex.Unlock() 24 | } 25 | 26 | func (rh *RelayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 | client, err := Server.Upgrader.Upgrade(w, r, nil) 28 | if err != nil { 29 | return 30 | } 31 | if r.Header.Get("authorization") == "" || r.Header.Get("authorization") != Config.Auth { 32 | _ = client.Close() 33 | return 34 | } 35 | id := RandomID() 36 | if id == "" { 37 | return 38 | } 39 | rh.putClient(id, client) 40 | go func() { 41 | for { 42 | packet := Packet{} 43 | err := client.ReadJSON(&packet) 44 | if err != nil { 45 | rh.deleteClient(id) 46 | return 47 | } 48 | // Type 0 is dispatch 49 | // Type 1 is receive 50 | if packet.Type == 0 { 51 | rh.mutex.RLock() 52 | for i, c := range rh.clients { 53 | if i == id { 54 | continue 55 | } 56 | _ = c.WriteJSON(Packet{ 57 | Type: 1, 58 | Body: packet.Body, 59 | }) 60 | } 61 | rh.mutex.RUnlock() 62 | } 63 | } 64 | }() 65 | } 66 | -------------------------------------------------------------------------------- /relay.md: -------------------------------------------------------------------------------- 1 | # Relay Server 2 | 3 | The relay server allows your clusters to communicate messages to one another (i.e. custom events) 4 | 5 | # Relay dispatch 6 | ```json 7 | { 8 | "type": 0, 9 | "body": "test entity event" 10 | } 11 | ``` 12 | 13 | # Relay response 14 | ```json 15 | { 16 | "type": 1, 17 | "body": "test entity event" 18 | } 19 | ``` 20 | 21 | ```javascript 22 | // This example assumes you're using port 3010, change it if need be. 23 | // The code to connect to this server and publish a message is below. 24 | // NOTE: You need to properly authenticate or else your connection will be closed. 25 | const WebSocket = require("ws"); 26 | // a will be the publisher 27 | const a = new WebSocket("ws://localhost:3010/relay", { 28 | headers: { 29 | Authorization: "token" 30 | } 31 | }); 32 | 33 | // b will act as a subscriber 34 | const b = new WebSocket("ws://localhost:3010/relay", { 35 | headers: { 36 | Authorization: "token" 37 | } 38 | }); 39 | 40 | a.on("open", () => { 41 | console.log("Dispatching message..."); 42 | a.send(JSON.stringify({ 43 | type: 0, // type should be 0 for sending, the relay server will send 1 for received events 44 | body: "Hello, world!" 45 | })); 46 | }); 47 | 48 | b.on("message", m => { 49 | const { type } = JSON.parse(m); 50 | if (type === 1) { 51 | console.log(`Received message: ${m}`); 52 | } 53 | // Received message: {"type":1,"body":"Hello, world!"} 54 | }); 55 | ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/websocket" 6 | "github.com/sirupsen/logrus" 7 | "net/http" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | Server *WSServer 13 | lock = &sync.Mutex{} 14 | ) 15 | 16 | const ( 17 | Handshaking = iota // client -> server 18 | ShardData // server -> client 19 | Ping // server -> client 20 | PingAck // client -> server 21 | Eval // client -> server 22 | BroadcastEval // client -> server 23 | BroadcastEvalAck // server -> client 24 | Stats // server -> client 25 | StatsAck // client -> server 26 | Ready // client -> server 27 | Entity 28 | EntityAck 29 | ) 30 | 31 | type WSServer struct { 32 | Clients []*Cluster 33 | Upgrader websocket.Upgrader 34 | ChanMutex *sync.RWMutex 35 | Channels map[string]chan EvalRes 36 | EntityMutex *sync.RWMutex 37 | EntityChannels map[string]chan EntityResponse 38 | } 39 | 40 | type SocketHandler struct{} 41 | 42 | type Packet struct { 43 | Type int `json:"type"` 44 | Body interface{} `json:"body,omitempty"` 45 | } 46 | 47 | func (w *WSServer) PutEvalChan(id string) { 48 | w.ChanMutex.Lock() 49 | w.Channels[id] = make(chan EvalRes) 50 | w.ChanMutex.Unlock() 51 | } 52 | 53 | func (w *WSServer) GetEvalChan(id string) chan EvalRes { 54 | w.ChanMutex.RLock() 55 | val, ok := w.Channels[id] 56 | if ok { 57 | w.ChanMutex.RUnlock() 58 | return val 59 | } 60 | w.ChanMutex.RUnlock() 61 | return nil 62 | } 63 | 64 | func (w *WSServer) DeleteEvalChan(id string) { 65 | w.ChanMutex.Lock() 66 | delete(w.Channels, id) 67 | w.ChanMutex.Unlock() 68 | } 69 | 70 | func (w *WSServer) PutEntityChan(id string) { 71 | w.EntityMutex.Lock() 72 | w.EntityChannels[id] = make(chan EntityResponse) 73 | w.EntityMutex.Unlock() 74 | } 75 | 76 | func (w *WSServer) GetEntityChan(id string) chan EntityResponse { 77 | w.EntityMutex.RLock() 78 | val, ok := w.EntityChannels[id] 79 | if ok { 80 | w.EntityMutex.RUnlock() 81 | return val 82 | } 83 | w.EntityMutex.RUnlock() 84 | return nil 85 | } 86 | 87 | func (w *WSServer) DeleteEntityChan(id string) { 88 | w.EntityMutex.Lock() 89 | delete(w.EntityChannels, id) 90 | w.EntityMutex.Unlock() 91 | } 92 | 93 | func (*SocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 94 | client, err := Server.Upgrader.Upgrade(w, r, nil) 95 | if err != nil { 96 | return 97 | } 98 | if r.Header.Get("authorization") == "" || r.Header.Get("authorization") != Config.Auth { 99 | _ = client.Close() 100 | return 101 | } 102 | id := NextClusterID() 103 | if id == -1 { 104 | _ = client.Close() 105 | return 106 | } 107 | c := Server.Clients[id] 108 | c.Client = client 109 | c.State = ClusterConnecting 110 | go func() { 111 | for { 112 | var packet *Packet 113 | err := client.ReadJSON(&packet) 114 | if err != nil { 115 | c.Terminate() 116 | break 117 | } 118 | go c.HandleMessage(packet) 119 | } 120 | }() 121 | } 122 | 123 | func (w *WSServer) Listen() { 124 | h := &MetricsHandler{} 125 | h.Setup() 126 | http.Handle("/ws", &SocketHandler{}) 127 | http.Handle("/metrics", h) 128 | http.Handle("/eval", &EvalHandler{}) 129 | http.Handle("/shardCount", &ExpectedShardHandler{}) 130 | http.Handle("/entity", &EntityHandler{}) 131 | http.Handle("/relay", &RelayHandler{ 132 | mutex: &sync.RWMutex{}, 133 | clients: make(map[string]*websocket.Conn), 134 | }) 135 | logrus.Infof("Starting to listen on %s:%d", Config.Ip, Config.Port) 136 | if err := http.ListenAndServe(fmt.Sprintf("%s:%d", Config.Ip, Config.Port), nil); err != nil { 137 | logrus.Fatalf("HTTP Listen error: %v", err) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | func CreateClusters(shards, clusters int) { 10 | shardIds := make([]int, 0, shards) 11 | for i := 0; i < shards; i++ { 12 | shardIds = append(shardIds, i) 13 | } 14 | avgShardsPerCluster := shards / clusters 15 | for i := 0; i < len(shardIds); i += avgShardsPerCluster { 16 | Server.Clients = append(Server.Clients, &Cluster{ 17 | ID: len(Server.Clients), 18 | Client: nil, 19 | PingRecv: false, 20 | Block: ClusterBlock{Shards: shardIds[i : i+avgShardsPerCluster], Total: Config.Shards}, 21 | State: ClusterWaiting, 22 | pingTicker: nil, 23 | mutex: &sync.Mutex{}, 24 | statsChan: make(chan map[string]interface{}), 25 | }) 26 | } 27 | } 28 | 29 | func RandomID() string { 30 | bytes := make([]byte, 4) 31 | _, err := rand.Read(bytes) 32 | if err != nil { 33 | return "" 34 | } 35 | return fmt.Sprintf("%x", bytes) 36 | } 37 | 38 | func NextClusterID() int { 39 | for index, cluster := range Server.Clients { 40 | if cluster.State == ClusterWaiting { 41 | return index 42 | } 43 | } 44 | return -1 45 | } 46 | 47 | func GetHealthyClusters() int { 48 | healthy := 0 49 | for _, cluster := range Server.Clients { 50 | if cluster.State == ClusterReady { 51 | healthy++ 52 | } 53 | } 54 | return healthy 55 | } 56 | --------------------------------------------------------------------------------