Repository: atlassian/gostatsd
Pull request #283 (2020-02-10 06:06:28)

k8s cloud provider
Implementation of #281 

I still need to write the docs for this but I wanted to get it up here for comment ASAP.

Things to know about the k8s provider implementation provided here:
* Sets up a watch of pod objects, which can be either targeted at the pod's current node, or at the entire cluster
* Indexes these pods by IP address so we can look them up at-will
* Retrieves tags that are allowed via lists of regexes for both pod annotations and labels
* For the first cut, we just immediately forward all metrics that don't have a pod IP. This is our solution to the race condition of a pod starting to run and send metrics before the API server has sent out information about the Pod's IP address.
  * Not ideal. Future work could be done to fix this but as things stand today this will require a refactor of the metrics pipeline to allow waiting for some metrics without waiting on the whole batch waiting to be processed.
* Has a bunch of k8s options to allow running locally or in-cluster, and to set the user agent so that you can track API server requests back to gostatsd

Things I think are outside the scope of this first implementation:
* The above mentioned waiting for pod IPs to arrive before forwarding metrics
* Deduplicating labels and annotations that have the same key
* Extra k8s ecosystem goodies like prometheus metrics and k8s events
* Allowing multiple cloud providers to run simultaneously (e.g. AWS and k8s)
* 

ash2k: > The above mentioned waiting for pod IPs to arrive before forwarding metrics
I'm working on the refactoring that will allow us to do this. Once we merge this PR I'll hopefully have the refactoring ready. Will need to rebase and then open a PR.
tiedotguy: The test failures seem to be timing racy, not data racy. I think the main thing `test-race` is doing is introducing slowdown with its overhead.  If I run non-race with `-count 1000`, it triggers, although far less frequently.

I'd suggest giving the whole thing a bit of a soak test with `-count 1000` (and maybe `-race -count 1000`) once you figure out the failure.  The panic in the build failure is masking the underlying issue, which is `assert` vs `require`. The assert should print, but I guess the panic is causing it to not output.
tiedotguy: I've been poking at the build failure (I love a good race condition!), and haven't been able to identify a good fix for it. My approach would be to replace...
```
  for _, p := range testCase.pods {
    fixtures.podsWatch.Add(p)
  }
```

with
```
  for _, p := range testCase.pods {
    fixtures.podsWatch.Add(p)
  }
  for i := 1; i < 100 && len(testCase.pods) < podsInCache(); i++ {
    time.Sleep(10 * time.Millisecond)
  }
  require.Equal(ttt, len(testCase.pods), podsInCache())
```

Implementing `podsInCache()` is left as an exercise for the reader, because damned if I can figure out how to get it :(
mwhittington21: > I've been poking at the build failure (I love a good race condition!), and haven't been able to identify a good fix for it. My approach would be to replace...
> 
> ```
>   for _, p := range testCase.pods {
>     fixtures.podsWatch.Add(p)
>   }
> ```
> 
> with
> 
> ```
>   for _, p := range testCase.pods {
>     fixtures.podsWatch.Add(p)
>   }
>   for i := 1; i < 100 && len(testCase.pods) < podsInCache(); i++ {
>     time.Sleep(10 * time.Millisecond)
>   }
>   require.Equal(ttt, len(testCase.pods), podsInCache())
> ```
> 
> Implementing `podsInCache()` is left as an exercise for the reader, because damned if I can figure out how to get it :(

This worked great! Although I'm tired and didn't realise that the logic in the `<` statement in the if was the wrong way around for far too long :(. Using `podsInCache() < len(testCase.pods)` worked fine.
tiedotguy: Nice! I'm not a fan of busy loops, but I'm even less of a fan of a 6 core system doing a `sleep(1 second)` just because the build system has 1/3 of a CPU...

Derp, sorry about the logic mixup!
mwhittington21: @ash2k @tiedotguy

Okay I've addressed all of the proposed changes above that weren't to be moved into other PRs. Let me know if there's anything else you'd like to see.

I'll start working on a docs PR now, branched off this one.
mwhittington21: Okay I've addressed the latest batch of comments and fixed some more bugs / cleaned things up a bit.
mwhittington21: Added docs to the PR.
tiedotguy: Looks good! I assume we're not cutting a new release until @ash2k has his refactoring work in?
mwhittington21: Last round of nits have been addressed!
ash2k: @tiedotguy I think we don't have to wait for my refactoring, this is good and working code 🤞 
tiedotguy: @ash2k if you give it an approve, I'll do a quick update of the changelog with pending changes, and cut a release.
ash2k: @mwhittington21 feel free to merge it! I suggest to squash it - we don't need all the 23 commits :)

Patch:
diff --git a/BACKENDS.md b/BACKENDS.md
index 0d3abee..70ab09b 100644
--- a/BACKENDS.md
+++ b/BACKENDS.md
@@ -1,6 +1,6 @@
 Configuring backends
 --------------------
-Backends must be configured through the usage of a configuration file (toml, yaml and json are supported), pass via
+Backends must be configured through the usage of a configuration file (toml, yaml and json are supported), passed via
 `--config-path`.
 
 Documentation is currently provided for `graphite` and `newrelic` backends.  For `datadog`, `statsdaemon`, `stdout`,
diff --git a/CLOUDPROVIDERS.md b/CLOUDPROVIDERS.md
new file mode 100644
index 0000000..76dcd68
--- /dev/null
+++ b/CLOUDPROVIDERS.md
@@ -0,0 +1,104 @@
+Configuring cloud providers
+--------------------
+Cloud providers are a way to automatically enrich metrics with metadata from a cloud vendor.
+
+Cloud providers must be configured through the usage of a configuration file (toml, yaml and json are supported), passed via
+`--config-path`.
+
+There are currently two supported cloud providers:
+
+* `aws` which retrieves tags from AWS instance tags via AWS API calls.
+* `k8s` which retrieves tags from kubernetes pod labels and annotations.
+
+All configuration is in a stanza named after the backend, and takes simple key value pairs.
+
+**Cloud providers should be disabled on the aggregation server when using http forwarding, as the source IP isn't
+propagated, and that information should be collected on the ingestion server.**
+
+aws
+---
+### TODO
+
+k8s
+---
+#### Overview
+
+The k8s cloud provider looks at the IP addresses of incoming metric datagrams and compares that with a list of pods
+running in the cluster to determine who sent the metric. If no pod is found with that IP then the metric is sent without
+enrichment.
+
+The list of pods is maintained via a watch API operation on the pod resource. This opens a websocket on which any pod
+handled by the API server is sent to. This means we get updates as they happen and don't do any active requests to
+the API server. This cache is fully resynced every `resync-period` but this is just to ensure total correctness.
+
+#### Important details
+
+`ignore-host` must be set to `false` for the k8s cloud provider to work at all! This is because it works based off the
+source IP address of incoming metrics, and these are dropped if `ignore-host=true`.
+
+In kubernetes when a new pod is created it gets a new unique identifier. The k8s cloud provider uses these identifiers
+as the hostname for the pods. This means if the k8s cluster has a high pod churn, then there will be a lot of unique
+values for the hostname tag. This is not ideal, and will waste resources unless you really care about those hostnames.
+To fix this it is highly recommended to drop the hostname from incoming metrics. The
+[examples]((examples/cloudproviders/k8s/K8S.md)) contain the configuration for a filter that will do this.
+
+#### Example with defaults
+
+```$toml
+[k8s]
+kube-api-qps = 5
+kube-api-burst = 1.5
+# Matches any annotation beginning with gostatsd.atlassian.com/
+annotation-tag-regex = '^gostatsd.atlassian.com/(?P<tag>.*)$'
+# Matches nothing - no labels included
+label-tag-regex = ''
+# Set these next two if you're not running inside a kubernetes cluster, or want to use
+# a custom role
+kubeconfig-context = ''
+kubeconfig-path = ''
+# This should be overriden via environment variable - see below for example
+node-name = ''
+resync-period = '5m'
+user-agent = 'gostatsd'
+watch-cluster = true
+```
+
+The configuration settings are as follows:
+- `kube-api-qps`: the number of queries per second gostatsd can make to the kubernetes API server before being rate limited
+- `kube-api-burst`: the number of queries per second gostatsd can burst above the limit when necessary
+- `annotation-tag-regex`: a regex that is compared to every pod's annotations. Any annotations matching the
+regex have their value used as a metric tag value. The key of the metric tag is either the entire annotation key, or a subset
+matching a named capture group called `tag`
+- `label-tag-regex`: like `annotation-tag-regex` but applied to pod labels
+- `kubeconfig-context`: specify a kubeconfig context to use to auth to the API server. Must exist within the kubeconfig
+file specified by `kubeconfig-path`
+- `kubeconfig-path`: path to a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)
+file to use as auth for the cluster. If this is not provided then gostatsd will assume it is running inside a kubernetes
+cluster and use in-cluster authentication.
+- `node-name`: only used if `watch-cluster` is `false`. The name of the node to watch for pods on. The recommended way
+to provide this value is actually via the
+[kubernetes downwards API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/)
+as an environment variable `GSD_K8S_NODE_NAME`. See the kubernetes deployment example for details.
+- `resync-period`: how often to completely refresh every entry in the pod watch cache
+- `user-agent`: the base user agent used when communicating with the kubernetes API server. The version of the binary
+is automatically appended to this string
+- `watch-cluster`: if `true` then can enrich metrics from all pods in the cluster. If `false` will only enrich metrics
+that are running on the node named `node-name`
+
+#### Tag names and values
+
+The most important options are `annotation-tag-regex` and `label-tag-regex`. These are what allow you to specify what
+you care about emitting as tags from your pods.
+
+If the regex matches an annotation/label then it is included as a tag on an emitted metric. If the regex does not match
+then the annotation/label is ignored.
+
+Both regexes allow you to specify a named capture group called `tag`. If this capture group exists then the tag name
+will be set to the contents of this capture group. If the capture group does not exist, or does not match anything,
+then the tag name will be set to the entire annotation/label name.
+
+The value of any included statsd tag is the value of the annotation/label on the pod.
+
+#### Example kubernetes deployments
+
+[See here for example configurations for using the k8s cloud provider in Kubernetes.](examples/cloudproviders/k8s/K8S.md)
diff --git a/README.md b/README.md
index c4c85b4..b7dc2ba 100644
--- a/README.md
+++ b/README.md
@@ -119,13 +119,13 @@ under HTTP.md
 
 Configuring backends
 --------------------
-Refer to [BACKENDS.md] for configuration options for the backends
+Refer to [backends](BACKENDS.md) for configuration options for the backends.
 
 Cloud providers
 --------------
-Cloud providers are a way to automatically enrich metrics with metadata from a cloud vendor.  Currently only AWS is
-supported.  If enabled, the AWS cloudprovider will set the `host` to the instance id, collect all the EC2 tags, and
-the region.
+Cloud providers are a way to automatically enrich metrics with metadata from a cloud vendor.
+
+Refer to [cloud providers](CLOUDPROVIDERS.md) for configuration options for the cloud providers.
 
 They should be disabled on the aggregation server when using http forwarding, as the source IP isn't propagated, and
 that information should be collected on the ingestion server.
diff --git a/cloudprovider.go b/cloudprovider.go
index a7b6c4d..cf97631 100644
--- a/cloudprovider.go
+++ b/cloudprovider.go
@@ -8,7 +8,7 @@ import (
 )
 
 // CloudProviderFactory is a function that returns a CloudProvider.
-type CloudProviderFactory func(v *viper.Viper, logger logrus.FieldLogger) (CloudProvider, error)
+type CloudProviderFactory func(v *viper.Viper, logger logrus.FieldLogger, version string) (CloudProvider, error)
 
 // Instance represents a cloud instance.
 type Instance struct {
@@ -17,6 +17,7 @@ type Instance struct {
 }
 
 // CloudProvider represents a cloud provider.
+// If CloudProvider implements the Runner interface, it's started in a new goroutine at creation.
 type CloudProvider interface {
 	// Name returns the name of the cloud provider.
 	Name() string
diff --git a/cmd/gostatsd/main.go b/cmd/gostatsd/main.go
index 2377c79..48f1dfd 100644
--- a/cmd/gostatsd/main.go
+++ b/cmd/gostatsd/main.go
@@ -10,13 +10,13 @@ import (
 	"os"
 	"os/signal"
 	"strconv"
-	"strings"
 	"syscall"
 	"time"
 
+	"github.com/atlassian/gostatsd/pkg/util"
+
 	"github.com/atlassian/gostatsd"
 	"github.com/atlassian/gostatsd/pkg/backends"
-	"github.com/atlassian/gostatsd/pkg/cloudproviders"
 	"github.com/atlassian/gostatsd/pkg/statsd"
 	"github.com/atlassian/gostatsd/pkg/transport"
 
@@ -39,9 +39,6 @@ const (
 	ParamVersion = "version"
 )
 
-// EnvPrefix is the prefix of the inspected environment variables.
-const EnvPrefix = "GSD" //Go Stats D
-
 func main() {
 	rand.Seed(time.Now().UnixNano())
 	v, version, err := setupConfiguration()
@@ -91,11 +88,14 @@ func constructServer(v *viper.Viper) (*statsd.Server, error) {
 	// HTTP client pool
 	pool := transport.NewTransportPool(logger, v)
 
-	// Cloud provider
-	cloud, err := cloudproviders.Init(v.GetString(statsd.ParamCloudProvider), v, logger)
+	// Cloud handler factory
+	cloud, err := statsd.NewCloudHandlerFactoryFromViper(v, logger, Version)
 	if err != nil {
 		return nil, err
 	}
+	if err := cloud.InitCloudProvider(v); err != nil {
+		return nil, err
+	}
 	// Backends
 	backendNames := v.GetStringSlice(statsd.ParamBackends)
 	backendsList := make([]gostatsd.Backend, len(backendNames))
@@ -114,8 +114,7 @@ func constructServer(v *viper.Viper) (*statsd.Server, error) {
 	// Create server
 	return &statsd.Server{
 		Backends:            backendsList,
-		CloudProvider:       cloud,
-		Limiter:             rate.NewLimiter(rate.Limit(v.GetInt(statsd.ParamMaxCloudRequests)), v.GetInt(statsd.ParamBurstCloudRequests)),
+		CloudHandlerFactory: cloud,
 		InternalTags:        v.GetStringSlice(statsd.ParamInternalTags),
 		InternalNamespace:   v.GetString(statsd.ParamInternalNamespace),
 		DefaultTags:         v.GetStringSlice(statsd.ParamDefaultTags),
@@ -138,12 +137,6 @@ func constructServer(v *viper.Viper) (*statsd.Server, error) {
 		ConnPerReader:       v.GetBool(statsd.ParamConnPerReader),
 		ServerMode:          v.GetString(statsd.ParamServerMode),
 		LogRawMetric:        v.GetBool(statsd.ParamLogRawMetric),
-		CacheOptions: statsd.CacheOptions{
-			CacheRefreshPeriod:        v.GetDuration(statsd.ParamCacheRefreshPeriod),
-			CacheEvictAfterIdlePeriod: v.GetDuration(statsd.ParamCacheEvictAfterIdlePeriod),
-			CacheTTL:                  v.GetDuration(statsd.ParamCacheTTL),
-			CacheNegativeTTL:          v.GetDuration(statsd.ParamCacheNegativeTTL),
-		},
 		HeartbeatTags: gostatsd.Tags{
 			fmt.Sprintf("version:%s", Version),
 			fmt.Sprintf("commit:%s", GitCommit),
@@ -183,10 +176,7 @@ func cancelOnInterrupt(ctx context.Context, f context.CancelFunc) {
 func setupConfiguration() (*viper.Viper, bool, error) {
 	v := viper.New()
 	defer setupLogger(v) // Apply logging configuration in case of early exit
-	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
-	v.SetEnvPrefix(EnvPrefix)
-	v.SetTypeByDefaultValue(true)
-	v.AutomaticEnv()
+	util.InitViper(v, "")
 
 	var version bool
 
diff --git a/examples/cloudproviders/k8s/K8S.md b/examples/cloudproviders/k8s/K8S.md
new file mode 100644
index 0000000..cf9457c
--- /dev/null
+++ b/examples/cloudproviders/k8s/K8S.md
@@ -0,0 +1,29 @@
+Running the k8s cloud provider in Kubernetes
+--------------------------------------------
+
+For large clusters it is highly recommended to use the "one gostatsd per node" approach below, as this will distribute
+CPU and network load across the nodes in your cluster.
+
+#### RBAC Permissions
+
+The k8s cloud provider requires certain cluster permissions to operate. The
+[RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) permissions required are in
+[rbac.yaml](rbac.yaml).
+
+#### One gostatsd for a cluster
+
+[deployment.yaml](deployment.yaml) shows an example configuration for running one gostatsd that enriches metrics for
+an entire cluster. Pods can send metrics to `gostatsd.default.svc.cluster.local:8125` and they will be enriched and
+processed.
+
+#### One gostatsd per node
+
+[daemonset.yaml](daemonset.yaml) shows an example configuration for running a gostatsd on each node in the cluster.
+This gostatsd is only able to enrich metrics that come from pods running on the same node as the gostatsd. We pass the
+node name into gostatsd via the downwards API and the `GSD_K8S_NODE_NAME` variable.
+
+Pods running on the node are responsible for knowing how to send metrics to the node itself. The `daemonset.yaml` also
+contains an example application which sends metrics to it's own node every 10 seconds, using the downward API to find
+the IP address of the node.
+
+**This is the preferred configuration for large kubernetes clusters as it will distribute CPU load across the cluster, and reduce network load.** 
\ No newline at end of file
diff --git a/examples/cloudproviders/k8s/daemonset.yaml b/examples/cloudproviders/k8s/daemonset.yaml
new file mode 100644
index 0000000..963a510
--- /dev/null
+++ b/examples/cloudproviders/k8s/daemonset.yaml
@@ -0,0 +1,116 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: gostatsd-cfg
+  namespace: default
+data:
+  config.toml: |
+    # Example: don't actually do anything with the metrics
+    backends=['stdout']
+    default-tags=[]
+    internal-tags=['in_cluster:true']
+    statser-type='internal'
+    flush-interval='1s'
+    bad-lines-per-minute=1000
+    metrics-addr=':8125'
+    filters='no-hostname'
+
+    [k8s]
+    # Include any annotation beginning with myapp.company.com as a tag, using the part after the / as the tag name
+    annotation-tag-regex = '^myapp\.company\.com/(?P<tag>.*)$'
+    # Include the 'app' label as a tag
+    label-tag-regex = '^app$'
+    # Only watch pods that are on the node that this gostatsd is running on.
+    watch-cluster = false
+    # This MUST be overridden via environment variable GSD_K8S_NODE_NAME. See pod spec for example.
+    node-name = ''
+
+    [filter.no-hostname]
+    # Drop the hostname from metrics because it can be very noisy with pod churn on Kubernetes
+    drop-host=true
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gostatsd
+  namespace: default
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: gostatsd
+  namespace: default
+  labels:
+    app: gostatsd
+spec:
+  selector:
+    matchLabels:
+      app: gostatsd
+  updateStrategy:
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: gostatsd
+      annotations:
+        myapp.company.com/imageVersion: "latest"
+    spec:
+      serviceAccountName: gostatsd
+      # We need the host network so we have an explicitly addressable pod from every pod running on the node.
+      # This will be the node's IP address
+      hostNetwork: true
+      containers:
+      - name: gostatsd
+        imagePullPolicy: IfNotPresent
+        image: atlassianlabs/gostatsd:latest
+        # Make sure nothing else on the node is listening on this port
+        ports:
+        - containerPort: 8125
+        command: ['/bin/gostatsd', '--cloud-provider', 'k8s', '--config-path', '/etc/gostatsd-cfg/config.toml']
+        env:
+        # Set the `node-name` option under the k8s provider config sub options to the name of the node we're running on
+        - name: GSD_K8S_NODE_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+        volumeMounts:
+        - name: config
+          mountPath: /etc/gostatsd-cfg
+      volumes:
+      - name: config
+        configMap:
+          name: gostatsd-cfg
+---
+# This is an example app that would consume the above node-local gostatsd DaemonSet
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: gostatsd-consumer
+  namespace: default
+  labels:
+    app: gostatsd-consumer
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: gostatsd-consumer
+  template:
+    metadata:
+      labels:
+        app: gostatsd-consumer
+      annotations:
+        myapp.company.com/imageVersion: "latest"
+    spec:
+      containers:
+        - name: metric-sender
+          imagePullPolicy: IfNotPresent
+          image: alpine:latest
+          # Send a metric called `metric.namespace` which is a counter with a value of 1, tagged with some dummy tags,
+          # to the local gostatsd DaemonSet. Do this once every 10 seconds.
+          command: ['/bin/sh', '-c', 'while 1; echo "metric.namespace:1|c|#tag1:value1,tag2:value2" |  nc -w 1 -u $MY_NODE_IP 8125; sleep 10; done']
+          env:
+          - name: MY_NODE_IP
+            valueFrom:
+              fieldRef:
+                fieldPath: status.hostIP
diff --git a/examples/cloudproviders/k8s/deployment.yaml b/examples/cloudproviders/k8s/deployment.yaml
new file mode 100644
index 0000000..106edac
--- /dev/null
+++ b/examples/cloudproviders/k8s/deployment.yaml
@@ -0,0 +1,85 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: gostatsd-cfg
+  namespace: default
+data:
+  config.toml: |
+    # Example: don't actually do anything with the metrics
+    backends=['null']
+    default-tags=[]
+    internal-tags=['in_cluster:true']
+    statser-type='internal'
+    flush-interval='1s'
+    bad-lines-per-minute=1000
+    metrics-addr=':8125'
+    filters='no-hostname'
+
+    [k8s]
+    # Include any annotation beginning with myapp.company.com as a tag, using the part after the / as the tag name
+    annotation-tag-regex = '^myapp\.company\.com/(?P<tag>.*)$'
+    # Include the 'app' label as a tag
+    label-tag-regex = '^app$'
+
+    [filter.no-hostname]
+    # Drop the hostname from metrics because it can be very noisy with pod churn on Kubernetes
+    drop-host=true
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gostatsd
+  namespace: default
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: gostatsd
+  namespace: default
+  labels:
+    app: gostatsd
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: gostatsd
+  template:
+    metadata:
+      labels:
+        app: gostatsd
+      annotations:
+        myapp.company.com/imageVersion: "latest"
+    spec:
+      serviceAccountName: gostatsd
+      containers:
+      - name: gostatsd
+        imagePullPolicy: IfNotPresent
+        image: atlassianlabs/gostatsd:latest
+        ports:
+        - containerPort: 8125
+        command: ['/bin/gostatsd', '--cloud-provider', 'k8s', '--config-path', '/etc/gostatsd-cfg/config.toml']
+        volumeMounts:
+        - name: config
+          mountPath: /etc/gostatsd-cfg
+      volumes:
+      - name: config
+        configMap:
+          name: gostatsd-cfg
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: gostatsd
+  name: gostatsd
+  namespace: default
+spec:
+  ports:
+    - name: udp-metrics
+      port: 8125
+      protocol: UDP
+      targetPort: 8125
+  selector:
+    app: gostatsd
+  type: ClusterIP
diff --git a/examples/cloudproviders/k8s/rbac.yaml b/examples/cloudproviders/k8s/rbac.yaml
new file mode 100644
index 0000000..ae3c05c
--- /dev/null
+++ b/examples/cloudproviders/k8s/rbac.yaml
@@ -0,0 +1,25 @@
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: gostatsd
+rules:
+  - apiGroups: [""]
+    resources:
+      - pods
+    verbs:
+      - list
+      - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gostatsd
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: gostatsd
+subjects:
+  - kind: ServiceAccount
+    name: gostatsd
+    namespace: default
diff --git a/go.mod b/go.mod
index ec75b4a..1633923 100644
--- a/go.mod
+++ b/go.mod
@@ -4,40 +4,33 @@ go 1.13
 
 require (
 	github.com/ash2k/stager v0.0.0-20170622123058-6e9c7b0eacd4
-	github.com/aws/aws-sdk-go v1.28.6
+	github.com/aws/aws-sdk-go v1.28.13
 	github.com/cenkalti/backoff v2.2.1+incompatible
 	github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c
 	github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect
 	github.com/githubnemo/CompileDaemon v1.0.0
-	github.com/go-redis/redis v6.15.6+incompatible
-	github.com/golang/protobuf v1.3.2
-	// # Unpin github.com/golangci/golangci-lint from v1.18.0 when gitea 1.10 is released
-	// # https://github.com/go-gitea/gitea/issues/8126
-	// # https://github.com/atlassian/gostatsd/issues/266
-	github.com/golangci/golangci-lint v1.18.0
+	github.com/go-redis/redis v6.15.7+incompatible
+	github.com/golang/protobuf v1.3.3
+	github.com/golangci/golangci-lint v1.23.3
 	github.com/gorilla/mux v1.7.3
 	github.com/howeyc/fsnotify v0.9.0 // indirect
+	github.com/imdario/mergo v0.3.8 // indirect
 	github.com/json-iterator/go v1.1.9
 	github.com/jstemmer/go-junit-report v0.9.1
-	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
 	github.com/libp2p/go-reuseport v0.0.1
 	github.com/magiconair/properties v1.8.1
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.1 // indirect
-	github.com/pelletier/go-toml v1.6.0 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
+	github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd
 	github.com/sirupsen/logrus v1.4.2
-	github.com/spf13/afero v1.2.2 // indirect
-	github.com/spf13/cast v1.3.1 // indirect
-	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.6.2
 	github.com/stephens2424/writerset v1.0.2 // indirect
 	github.com/stretchr/testify v1.4.0
 	github.com/tilinna/clock v1.0.2
-	golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
-	golang.org/x/sys v0.0.0-20200120151820-655fe14d7479 // indirect
+	golang.org/x/net v0.0.0-20200202094626-16171245cfb2
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
-	golang.org/x/tools v0.0.0-20200119215504-eb0d8dd85bcc
-	gopkg.in/ini.v1 v1.51.1 // indirect
+	golang.org/x/tools v0.0.0-20200207224406-61798d64f025
+	k8s.io/api v0.17.3
+	k8s.io/apimachinery v0.17.3
+	k8s.io/client-go v0.17.3
+	k8s.io/utils v0.0.0-20200124190032-861946025e34 // indirect
 )
diff --git a/go.sum b/go.sum
index 41926b9..92e9cc6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,55 +1,85 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM=
-github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
+github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
+github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/ash2k/stager v0.0.0-20170622123058-6e9c7b0eacd4 h1:pG7CUDQmAqAxVv4smDHWTtorVUI5B7aOcFDfgqtZuWA=
 github.com/ash2k/stager v0.0.0-20170622123058-6e9c7b0eacd4/go.mod h1:20N8GhJtHSLeRJvNhy5D1SnEHni4Xlt6p13JQMHYdDY=
-github.com/aws/aws-sdk-go v1.28.6 h1:GUbRiuJnFDA6hQw+jK1oQyONVMO5U1EjRdrfSxfONC4=
-github.com/aws/aws-sdk-go v1.28.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.28.13 h1:JyCQQ86yil3hg7MtWdNH8Pbcgx92qlUV2v22Km63Mf4=
+github.com/aws/aws-sdk-go v1.28.13/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bombsimon/wsl/v2 v2.0.0 h1:+Vjcn+/T5lSrO8Bjzhk4v14Un/2UyCA1E3V5j9nwTkQ=
+github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
 github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c h1:/bXaeEuNG6V0HeyEGw11DYLW5BGsOPlcVRIXbHNUWSo=
 github.com/dvyukov/go-fuzz v0.0.0-20191206100749-a378175e205c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
 github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
-github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU=
-github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
+github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/githubnemo/CompileDaemon v1.0.0 h1:F4nrVyPOxva6gJIw320fwfaFaPl8ZuEk95RZOy9Q4eM=
 github.com/githubnemo/CompileDaemon v1.0.0/go.mod h1:lE3EXX1td33uhlkFLp+ImWY9qBaoRcDeA3neh4m8ic0=
-github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540 h1:djv/qAomOVj8voCHt0M0OYwR/4vfDq1zNKSPKjJCexs=
-github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
+github.com/go-critic/go-critic v0.4.1 h1:4DTQfT1wWwLg/hzxwD9bkdhDQrdJtxe6DUTadPlrIeE=
+github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
 github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
-github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
-github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U=
+github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
 github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
@@ -74,18 +104,26 @@ github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2X
 github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
+github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
 github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
@@ -94,22 +132,18 @@ github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpc
 github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
 github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
 github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
-github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c h1:/7detzz5stiXWPzkTlPTzkBEIIE4WGpppBJYjKqBiPI=
-github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
 github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
 github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
 github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8=
 github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
-github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
-github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
-github.com/golangci/golangci-lint v1.18.0 h1:XmQgfcLofSG/6AsQuQqmLizB+3GggD+o6ObBG9L+VMM=
-github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=
-github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 h1:fUdgm/BdKvwOHxg5AhNbkNRp2mSy8sxTXyBVs/laQHo=
-github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
+github.com/golangci/golangci-lint v1.23.3 h1:wkACDEoy+b0CVqnSK8BbWrVkN2tsVLUA1+SIkGSm4o0=
+github.com/golangci/golangci-lint v1.23.3/go.mod h1:LNexeEyqT5hQH7v47e67JekL0V51lXFUjbPkopxNSK4=
 github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
 github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
-github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 h1:En/tZdwhAn0JNwLuXzP3k2RVtMqMmOEK7Yu/g3tmtJE=
-github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
 github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
 github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
 github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk=
@@ -120,10 +154,25 @@ github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1
 github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
 github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
 github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
@@ -131,30 +180,44 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
 github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY=
 github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a h1:GmsqmapfzSJkm28dhRoHz2tLRbJmqhU86IPgBtN3mmk=
+github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
+github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 h1:jNYPNLe3d8smommaoQlK7LOA5ESyUJJ+Wf79ZtA7Vp4=
+github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@@ -163,60 +226,70 @@ github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM52
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/libp2p/go-reuseport v0.0.1 h1:7PhkfH73VXfPJYKQ6JwS5I/eVcoyYi9IMNGc6FWpFLw=
 github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA=
 github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
-github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE=
+github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
+github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
-github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
+github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd h1:Av0AX0PnAlPZ3AY2rQUobGFaZfE4KHVRdKWIEPvsCWY=
+github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
-github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
-github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
+github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
-github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
+github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
-github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -231,15 +304,16 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
 github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
-github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83 h1:AtnWoOvTioyDXFvu96MWEeE8qj4COSQnJogzLy/u41A=
+github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
+github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
 github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
-github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -251,37 +325,32 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
 github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
 github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
-github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
 github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
 github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
 github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=
 github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -289,116 +358,189 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tilinna/clock v1.0.2 h1:6BO2tyAC9JbPExKH/z9zl44FLu1lImh3nDNKA0kgrkI=
 github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
-github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec h1:AmoEvWAO3nDx1MEcMzPh+GzOOIA5Znpv6++c7bePPY0=
-github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
+github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
+github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tommy-muehle/go-mnd v1.1.1 h1:4D0wuPKjOTiK2garzuPGGvm4zZ/wLYDOH8TJSABC7KU=
+github.com/tommy-muehle/go-mnd v1.1.1/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ultraware/funlen v0.0.1 h1:UeC9tpM4wNWzUJfan8z9sFE4QCzjjzlCZmuJN+aOkH0=
-github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbdo=
+github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
+github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
+github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
+github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
-github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
+github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200120151820-655fe14d7479 h1:LhLiKguPgZL+Tglay4GhVtfF0kb8cvOJ0dHTCBO8YNI=
-golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200119215504-eb0d8dd85bcc h1:ZA7KFRdqWZkBr0/YbHm1h08vDJ5gQdjVG/8L153z5c4=
-golang.org/x/tools v0.0.0-20200119215504-eb0d8dd85bcc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200102140908-9497f49d5709/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207224406-61798d64f025 h1:i84/3szN87uN9jFX/jRqUbszQto2oAsFlqPf6lbR8H4=
+golang.org/x/tools v0.0.0-20200207224406-61798d64f025/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho=
-gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0=
+k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0=
+k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg=
+k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
+k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU=
+k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ=
+k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
+k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
+k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
+k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
+k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20200124190032-861946025e34 h1:HjlUD6M0K3P8nRXmr2B9o4F9dUy9TCj/aEpReeyi6+k=
+k8s.io/utils v0.0.0-20200124190032-861946025e34/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
-mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 h1:duVSyluuJA+u0BnkcLR01smoLrGgDTfWt5c8ODYG8fU=
-mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
+mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f h1:Cq7MalBHYACRd6EesksG1Q8EoIAKOsiZviGKbOLIej4=
+mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
+sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
 sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/pkg/cloudproviders/aws/aws.go b/pkg/cloudproviders/aws/aws.go
index 3d2966e..0f993cb 100644
--- a/pkg/cloudproviders/aws/aws.go
+++ b/pkg/cloudproviders/aws/aws.go
@@ -10,6 +10,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	"github.com/spf13/viper"
+
 	"github.com/atlassian/gostatsd"
 	"github.com/atlassian/gostatsd/pkg/stats"
 	"github.com/atlassian/gostatsd/pkg/util"
@@ -20,7 +22,6 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/sirupsen/logrus"
-	"github.com/spf13/viper"
 	"golang.org/x/net/http2"
 )
 
@@ -204,7 +205,7 @@ func azToRegion(az string) (string, error) {
 }
 
 // NewProviderFromViper returns a new aws provider.
-func NewProviderFromViper(v *viper.Viper, logger logrus.FieldLogger) (gostatsd.CloudProvider, error) {
+func NewProviderFromViper(v *viper.Viper, logger logrus.FieldLogger, _ string) (gostatsd.CloudProvider, error) {
 	a := util.GetSubViper(v, "aws")
 	a.SetDefault("max_retries", 3)
 	a.SetDefault("client_timeout", defaultClientTimeout)
diff --git a/pkg/cloudproviders/cloudproviders.go b/pkg/cloudproviders/cloudproviders.go
index 04a60ec..9e64366 100644
--- a/pkg/cloudproviders/cloudproviders.go
+++ b/pkg/cloudproviders/cloudproviders.go
@@ -3,37 +3,40 @@ package cloudproviders
 import (
 	"fmt"
 
+	"github.com/spf13/viper"
+
 	"github.com/atlassian/gostatsd"
 	"github.com/atlassian/gostatsd/pkg/cloudproviders/aws"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders/k8s"
 
 	"github.com/sirupsen/logrus"
-	"github.com/spf13/viper"
 )
 
 // All registered cloud providers.
 var providers = map[string]gostatsd.CloudProviderFactory{
 	aws.ProviderName: aws.NewProviderFromViper,
+	k8s.ProviderName: k8s.NewProviderFromViper,
 }
 
 // Get creates an instance of the named provider, or nil if
 // the name is not known.  The error return is only used if the named provider
 // was known but failed to initialize.
-func Get(name string, v *viper.Viper, logger logrus.FieldLogger) (gostatsd.CloudProvider, error) {
+func Get(name string, v *viper.Viper, logger logrus.FieldLogger, version string) (gostatsd.CloudProvider, error) {
 	f, found := providers[name]
 	if !found {
 		return nil, nil
 	}
-	return f(v, logger)
+	return f(v, logger, version)
 }
 
 // Init creates an instance of the named cloud provider.
-func Init(name string, v *viper.Viper, logger logrus.FieldLogger) (gostatsd.CloudProvider, error) {
+func Init(name string, v *viper.Viper, logger logrus.FieldLogger, version string) (gostatsd.CloudProvider, error) {
 	if name == "" {
 		logrus.Info("No cloud provider specified")
 		return nil, nil
 	}
 
-	provider, err := Get(name, v, logger.WithField("cloud_provider", name))
+	provider, err := Get(name, v, logger.WithField("cloud_provider", name), version)
 	if err != nil {
 		return nil, fmt.Errorf("could not init cloud provider %q: %v", name, err)
 	}
diff --git a/pkg/cloudproviders/k8s/k8s.go b/pkg/cloudproviders/k8s/k8s.go
new file mode 100644
index 0000000..58b9934
--- /dev/null
+++ b/pkg/cloudproviders/k8s/k8s.go
@@ -0,0 +1,367 @@
+package k8s
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"time"
+
+	"github.com/atlassian/gostatsd"
+	"github.com/atlassian/gostatsd/pkg/util"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
+	core_v1 "k8s.io/api/core/v1"
+	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/clientcmd"
+)
+
+var (
+	// DefaultAnnotationTagRegex is the default ParamAnnotationTagRegex. Everything beginning with a standard
+	// gostatsd string is included by default. The default sets the tag name as the part after the "/", for example
+	// gostatsd.atlassian.com/tag1 would return "tag1" as the tag name after matching.
+	DefaultAnnotationTagRegex = fmt.Sprintf("^%s%s$", regexp.QuoteMeta(AnnotationPrefix), DefaultTagCaptureRegex)
+	// DefaultTagCaptureRegex is a regex subexpression to capture from now until the end of the regex as the tag name.
+	DefaultTagCaptureRegex = fmt.Sprintf("(?P<%s>.*)", TagNameRegexSubexp)
+)
+
+const (
+	// ProviderName is the name of the k8s cloud provider.
+	ProviderName = "k8s"
+	// PodsByIPIndexName is the name of the index function storing pods by IP.
+	PodsByIPIndexName = "PodByIP"
+	// AnnotationPrefix is the annotation prefix that is turned into tags by default.
+	AnnotationPrefix = "gostatsd.atlassian.com/"
+	// TagNameRegexSubexp is the name of the regex subexpression that is used to parse tag names from label/annotation
+	// names.
+	TagNameRegexSubexp = "tag"
+
+	// ParamAPIQPS is the maximum amount of queries per second we allow to the Kubernetes API server. This is
+	// so we don't overwhelm the server with requests under load.
+	ParamAPIQPS = "kube-api-qps"
+	// ParamAPIQPSBurst is the amount of queries per second we can burst above ParamAPIQPS.
+	ParamAPIQPSBurst = "kube-api-burst"
+	// ParamAnnotationTagRegex is a regex to check annotations against. Any pod annotations matching
+	// this pattern will be included as tags on metrics emitted by that pod. The tag name for these annotations will
+	// be the capture group named "tag". "" means ignore all annotations.
+	ParamAnnotationTagRegex = "annotation-tag-regex"
+	// ParamLabelTagRegex is a list of regexes to check labels against. Any pod labels matching this
+	// pattern will be included as tags on metrics emitted by that pod. The tag name for these labels will
+	// be the capture group named "tag". "" means ignore all labels.
+	ParamLabelTagRegex = "label-tag-regex"
+	// KubeconfigContextis the name of the context to use inside a provided ParamKubeconfigPath. If ParamKubeconfigPath
+	// is unset this has no effect.
+	ParamKubeconfigContext = "kubeconfig-context"
+	// ParamKubeconfigPath is the path to the kubeconfig file to use for auth, or "" if using in-cluster auth.
+	ParamKubeconfigPath = "kubeconfig-path"
+	// ParamNodeName is the Kubernetes node name of the node this is running on. This is only used if ParamWatchCluster
+	// is set to false.
+	ParamNodeName = "node-name"
+	// ParamResyncPeriod is the resync period for the pod cache as a Duration.
+	ParamResyncPeriod = "resync-period"
+	// ParamUserAgent is the user agent used when talking to the k8s API.
+	ParamUserAgent = "user-agent"
+	// ParamWatchCluster is true if we should watch pods in the entire cluster, false if we should watch pods on our
+	// own node.
+	ParamWatchCluster = "watch-cluster"
+
+	// DefaultAPIQPS is the default maximum amount of queries per second we allow to the Kubernetes API server.
+	DefaultAPIQPS = 5
+	// DefaultAPIQPSBurst is the default amount of queries per second we can burst above the maximum QPS.
+	DefaultAPIQPSBurst = 10
+	// DefaultKubeconfigContext is the default context to use inside the kubeconfig file. "" means use the current
+	// context without switching.
+	DefaultKubeconfigContext = ""
+	// DefaultKubeconfigPath is the default path to the kubeconfig file. "" means use in-cluster auth.
+	DefaultKubeconfigPath = ""
+	// DefaultLabelTagRegex is the default ParamLabelTagRegex. Every label is ignored by default.
+	DefaultLabelTagRegex = ""
+	// DefaultNodeName is the default node name to watch pods on when ParamWatchCluster is false. Defaults to unset
+	// as this will fail fast and alert users to set this appropriately.
+	DefaultNodeName = ""
+	// DefaultResyncPeriod is the default resync period for the pod cache.
+	DefaultResyncPeriod = 5 * time.Minute
+	// DefaultUserAgent is the default user agent used when talking to the k8s API.
+	DefaultUserAgent = "gostatsd"
+	// DefaultWatchCluster is the default watch mode for pods. By default we watch the entire cluster.
+	DefaultWatchCluster = true
+)
+
+// PodInformerOptions represent options for a pod informer.
+type PodInformerOptions struct {
+	ResyncPeriod time.Duration
+	WatchCluster bool
+	NodeName     string
+}
+
+// Provider represents a k8s provider.
+type Provider struct {
+	logger logrus.FieldLogger
+
+	podsInf         cache.SharedIndexInformer
+	factory         informers.SharedInformerFactory
+	annotationRegex *regexp.Regexp
+	labelRegex      *regexp.Regexp
+}
+
+func (p *Provider) EstimatedTags() int {
+	// There is no real way to estimate this for k8s provider as any pod can have arbitrary labels/annotations
+	return 0
+}
+
+// Instance returns pod details from k8s API server watches.
+// ip -> nil pointer if pod was not found.
+// map is returned even in case of errors because it may contain partial data.
+// An "instance", as far as k8s cloud provider is concerned, is an individual pod running in the cluster
+func (p *Provider) Instance(ctx context.Context, IP ...gostatsd.IP) (map[gostatsd.IP]*gostatsd.Instance, error) {
+	instanceIPs := make(map[gostatsd.IP]*gostatsd.Instance, len(IP))
+	var returnErr error
+
+	// Lookup via the pod cache
+	for _, lookupIP := range IP {
+		if lookupIP == gostatsd.UnknownIP {
+			instanceIPs[lookupIP] = nil
+			continue
+		}
+
+		p.logger.WithField("ip", lookupIP).Debug("Looking up pod ip")
+		objs, err := p.podsInf.GetIndexer().ByIndex(PodsByIPIndexName, string(lookupIP))
+		if err != nil {
+			instanceIPs[lookupIP] = nil
+			returnErr = err
+			continue
+		}
+		if len(objs) < 1 {
+			p.logger.Debug("Could not find IP in cache, continuing")
+			instanceIPs[lookupIP] = nil
+			continue
+		}
+		if len(objs) > 1 {
+			p.logger.WithField("ip", lookupIP).Warn("More than one pod in cache. Using first stored")
+		}
+		pod := objs[0].(*core_v1.Pod)
+
+		// Turn the pod metadata into tags
+		var tags gostatsd.Tags
+		// TODO: deduplicate labels and annotations in their tag format, rather than overwriting
+		for k, v := range pod.ObjectMeta.Labels {
+			tagName := getTagNameFromRegex(p.labelRegex, k)
+			if tagName != "" {
+				tags = append(tags, tagName+":"+v)
+			}
+		}
+		for k, v := range pod.ObjectMeta.Annotations {
+			tagName := getTagNameFromRegex(p.annotationRegex, k)
+			if tagName != "" {
+				tags = append(tags, tagName+":"+v)
+			}
+		}
+		instanceID := pod.Namespace + "/" + pod.Name
+		instanceIPs[lookupIP] = &gostatsd.Instance{
+			ID:   instanceID,
+			Tags: tags,
+		}
+		p.logger.WithFields(logrus.Fields{
+			"instance": instanceID,
+			"ip":       lookupIP,
+			"tags":     tags,
+		}).Debug("Added tags")
+	}
+	return instanceIPs, returnErr
+}
+
+// MaxInstancesBatch returns maximum number of instances that could be requested via the Instance method.
+func (p *Provider) MaxInstancesBatch() int {
+	// This is arbitrary since we have a local cache of information
+	return 64
+}
+
+// Name returns the name of the provider.
+func (p *Provider) Name() string {
+	return ProviderName
+}
+
+// SelfIP returns host's IPv4 address.
+func (p *Provider) SelfIP() (gostatsd.IP, error) {
+	// This IP is only used for start/stop events of gostatsd. To simplify the k8s provider we have
+	// chosen to just avoid finding the IP.
+	return gostatsd.UnknownIP, nil
+}
+
+// NewProviderFromViper returns a new k8s provider.
+func NewProviderFromViper(v *viper.Viper, logger logrus.FieldLogger, version string) (gostatsd.CloudProvider, error) {
+	k := util.GetSubViper(v, "k8s")
+	setViperDefaults(k, version)
+
+	// Set up the k8s client
+	clientset, err := createKubernetesClient(k.GetString(ParamUserAgent), k.GetString(ParamKubeconfigPath),
+		k.GetString(ParamKubeconfigContext), k.GetFloat64(ParamAPIQPS), k.GetFloat64(ParamAPIQPSBurst))
+	if err != nil {
+		return nil, err
+	}
+
+	return NewProvider(
+		logger,
+		clientset,
+		PodInformerOptions{
+			ResyncPeriod: k.GetDuration(ParamResyncPeriod),
+			WatchCluster: k.GetBool(ParamWatchCluster),
+			NodeName:     k.GetString(ParamNodeName),
+		},
+		regexp.MustCompile(k.GetString(ParamAnnotationTagRegex)),
+		regexp.MustCompile(k.GetString(ParamLabelTagRegex)))
+}
+
+// NewProvider returns a new k8s provider.
+func NewProvider(logger logrus.FieldLogger, clientset kubernetes.Interface, podInfOpts PodInformerOptions,
+	annotationRegex, labelRegex *regexp.Regexp) (gostatsd.CloudProvider, error) {
+
+	// This list operation is for debugging purposes and failing fast. If this fails then the cache will likely fail.
+	_, err := clientset.CoreV1().Pods(meta_v1.NamespaceAll).List(meta_v1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	// If we're not watching the entire cluster we need to limit our watch to pods with our node name
+	customWatchOptions := func(*meta_v1.ListOptions) {}
+	if !podInfOpts.WatchCluster {
+		if podInfOpts.NodeName == "" {
+			return nil, fmt.Errorf("watch-cluster set to false, and node name not supplied")
+		}
+		fieldSelector := "spec.nodeName=" + podInfOpts.NodeName
+		logger.WithField("fieldSelector", fieldSelector).Debug("set fieldSelector for informers")
+		customWatchOptions = func(lo *meta_v1.ListOptions) {
+			lo.FieldSelector = fieldSelector
+		}
+	}
+
+	// Create a shared informer factory that watches the correct nodes
+	factory := informers.NewSharedInformerFactoryWithOptions(
+		clientset,
+		podInfOpts.ResyncPeriod,
+		informers.WithTweakListOptions(customWatchOptions))
+
+	// Set up the pod informer which fills an index with the pods we care about
+	podsInf := factory.Core().V1().Pods().Informer()
+	indexers := cache.Indexers{
+		PodsByIPIndexName: podByIpIndexFunc,
+	}
+	err = podsInf.AddIndexers(indexers)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: we should emit prometheus metrics to fit in with the k8s ecosystem
+	// TODO: we should emit events to the k8s API to fit in with the k8s ecosystem
+
+	return &Provider{
+		logger:          logger,
+		podsInf:         podsInf,
+		factory:         factory,
+		annotationRegex: annotationRegex,
+		labelRegex:      labelRegex,
+	}, nil
+}
+
+func (p *Provider) Run(ctx context.Context) {
+	p.logger.Debug("Starting informer cache")
+	p.factory.Start(ctx.Done())
+}
+
+func createKubernetesClient(userAgent, kubeconfigPath, kubeconfigContext string, apiQPS, apiQPSBurst float64) (kubernetes.Interface, error) {
+	var restConfig *rest.Config
+	var err error
+	if kubeconfigPath != "" {
+		configOverrides := &clientcmd.ConfigOverrides{}
+		if kubeconfigContext != "" {
+			configOverrides.CurrentContext = kubeconfigContext
+		}
+
+		restConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+			&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
+			configOverrides).ClientConfig()
+	} else {
+		restConfig, err = rest.InClusterConfig()
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	restConfig.QPS = float32(apiQPS)
+	restConfig.Burst = int(apiQPSBurst)
+	restConfig.UserAgent = userAgent
+
+	clientset, err := kubernetes.NewForConfig(restConfig)
+	if err != nil {
+		return nil, err
+	}
+	return clientset, nil
+}
+
+func podByIpIndexFunc(obj interface{}) ([]string, error) {
+	pod := obj.(*core_v1.Pod)
+	ip := pod.Status.PodIP
+	// Pod must have an IP, be running, and also not have the same IP as the host
+	// If a pod is a HostNetwork pod then there could be multiple with the same IP sending stats, which breaks this
+	// abstraction
+	if ip == "" ||
+		podIsFinishedRunning(pod) ||
+		podIsHostNetwork(pod) {
+		// Do not index irrelevant Pods
+		return nil, nil
+	}
+	return []string{ip}, nil
+}
+
+func podIsHostNetwork(pod *core_v1.Pod) bool {
+	return pod.Spec.HostNetwork || pod.Status.PodIP == pod.Status.HostIP
+}
+
+func podIsFinishedRunning(pod *core_v1.Pod) bool {
+	return pod.Status.Phase == core_v1.PodSucceeded || pod.Status.Phase == core_v1.PodFailed || pod.DeletionTimestamp != nil
+}
+
+func setViperDefaults(v *viper.Viper, version string) {
+	v.SetDefault(ParamAPIQPS, DefaultAPIQPS)
+	v.SetDefault(ParamAPIQPSBurst, DefaultAPIQPSBurst)
+	v.SetDefault(ParamAnnotationTagRegex, DefaultAnnotationTagRegex)
+	v.SetDefault(ParamKubeconfigContext, DefaultKubeconfigContext)
+	v.SetDefault(ParamKubeconfigPath, DefaultKubeconfigPath)
+	v.SetDefault(ParamLabelTagRegex, DefaultLabelTagRegex)
+	// This is intended to be taken in primarily as an environment variable when running inside k8s, as that is the
+	// k8s standard way of providing variable information to pods via the downwards API.
+	// See: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/
+	v.SetDefault(ParamNodeName, DefaultNodeName)
+	v.SetDefault(ParamResyncPeriod, DefaultResyncPeriod)
+	v.SetDefault(ParamUserAgent, DefaultUserAgent+"/"+version)
+	v.SetDefault(ParamWatchCluster, DefaultWatchCluster)
+}
+
+// getTagNameFromRegex gets a tag name from the regex. This is either the entire input string, or a subset matching
+// the "tag" capture group if it exists and matched. Returns "" if the regex matched nothing.
+func getTagNameFromRegex(re *regexp.Regexp, s string) string {
+	// Empty regex means ignore
+	if re.String() == "" {
+		return ""
+	}
+
+	match := re.FindStringSubmatch(s)
+	if match == nil {
+		return ""
+	}
+	// The first subexp name is always "" and is the entire regex, so go in reverse to find the tag name subexp first
+	for i, subexpName := range re.SubexpNames() {
+		if subexpName == TagNameRegexSubexp && match[i] != "" {
+			return match[i] // the matching text of the tag name subexp
+		}
+	}
+	// If the regex matched as a whole then match[0] will be the matching text, otherwise ""
+	if match[0] != "" {
+		return s
+	}
+	return ""
+}
diff --git a/pkg/statsd/handler_cloud.go b/pkg/statsd/handler_cloud.go
index 9420c81..10860b6 100644
--- a/pkg/statsd/handler_cloud.go
+++ b/pkg/statsd/handler_cloud.go
@@ -2,15 +2,19 @@ package statsd
 
 import (
 	"context"
+	"fmt"
 	"sync"
 	"sync/atomic"
 	"time"
 
 	"github.com/atlassian/gostatsd"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders"
 	"github.com/atlassian/gostatsd/pkg/stats"
 
+	"github.com/ash2k/stager"
 	"github.com/ash2k/stager/wait"
 	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
 	"golang.org/x/time/rate"
 )
 
@@ -41,6 +45,81 @@ type CacheOptions struct {
 	CacheNegativeTTL          time.Duration
 }
 
+// CloudHandlerFactory creates a CloudHandler based on some predefined options.
+type CloudHandlerFactory struct {
+	cloudProviderName string
+	version           string
+	cloudProvider     gostatsd.CloudProvider
+	logger            logrus.FieldLogger
+	limiter           *rate.Limiter
+	cacheOptions      CacheOptions
+}
+
+// newCloudHandlerFactory initialises a new cloud handler factory
+func newCloudHandlerFactory(cloudProviderName string, logger logrus.FieldLogger, cacheOptions CacheOptions, limiter *rate.Limiter, version string) *CloudHandlerFactory {
+	return &CloudHandlerFactory{
+		cacheOptions:      cacheOptions,
+		logger:            logger,
+		limiter:           limiter,
+		cloudProvider:     nil,
+		cloudProviderName: cloudProviderName,
+		version:           version,
+	}
+}
+
+// NewCloudHandlerFactoryFromViper initialises a new cloud handler factory with defaults based on the name of the cloud
+// provider requested.
+func NewCloudHandlerFactoryFromViper(v *viper.Viper, logger logrus.FieldLogger, version string) (*CloudHandlerFactory, error) {
+	cloudProviderName := v.GetString(ParamCloudProvider)
+	if cloudProviderName == "" {
+		return newCloudHandlerFactory(cloudProviderName, logger, CacheOptions{}, nil, version), nil
+	}
+
+	// Cloud provider defaults
+	cpDefaultCacheOpts, ok := DefaultCloudProviderCacheValues[cloudProviderName]
+	if !ok {
+		return nil, fmt.Errorf("could not find default cache values for cloud provider '%s'", cloudProviderName)
+	}
+	cpDefaultLimiterOpts, ok := DefaultCloudProviderLimiterValues[cloudProviderName]
+	if !ok {
+		return nil, fmt.Errorf("could not find default cache values for cloud provider '%s'", cloudProviderName)
+	}
+
+	// Set the defaults in Viper based on the cloud provider values before we manipulate things
+	v.SetDefault(ParamCacheRefreshPeriod, cpDefaultCacheOpts.CacheRefreshPeriod)
+	v.SetDefault(ParamCacheEvictAfterIdlePeriod, cpDefaultCacheOpts.CacheEvictAfterIdlePeriod)
+	v.SetDefault(ParamCacheTTL, cpDefaultCacheOpts.CacheTTL)
+	v.SetDefault(ParamCacheNegativeTTL, cpDefaultCacheOpts.CacheNegativeTTL)
+	v.SetDefault(ParamMaxCloudRequests, cpDefaultLimiterOpts.MaxCloudRequests)
+	v.SetDefault(ParamBurstCloudRequests, cpDefaultLimiterOpts.BurstCloudRequests)
+
+	// Set the used values based on the defaults merged with any overrides
+	cacheOptions := CacheOptions{
+		CacheRefreshPeriod:        v.GetDuration(ParamCacheRefreshPeriod),
+		CacheEvictAfterIdlePeriod: v.GetDuration(ParamCacheEvictAfterIdlePeriod),
+		CacheTTL:                  v.GetDuration(ParamCacheTTL),
+		CacheNegativeTTL:          v.GetDuration(ParamCacheNegativeTTL),
+	}
+	limiter := rate.NewLimiter(rate.Limit(v.GetInt(ParamMaxCloudRequests)), v.GetInt(ParamBurstCloudRequests))
+	return newCloudHandlerFactory(cloudProviderName, logger, cacheOptions, limiter, version), nil
+}
+
+// InitCloudProvider initialises the cloud provider given some already parsed defaults.
+// This is mainly split out to enable testing the config parsing separately from the cloud providers.
+func (f *CloudHandlerFactory) InitCloudProvider(v *viper.Viper) error {
+	cloudProvider, err := cloudproviders.Init(f.cloudProviderName, v, f.logger, f.version)
+	if err != nil {
+		return err
+	}
+	f.cloudProvider = cloudProvider
+	return nil
+}
+
+// NewCloudHandler creates a new Cloud Handler based on the options set in the CloudHandlerFactory.
+func (f *CloudHandlerFactory) NewCloudHandler(handler gostatsd.PipelineHandler) *CloudHandler {
+	return NewCloudHandler(f.cloudProvider, handler, f.logger, f.limiter, f.cacheOptions)
+}
+
 // CloudHandler enriches metrics and events with additional information fetched from cloud provider.
 type CloudHandler struct {
 	// statsCacheHit is accessed by any go routine, must use atomic ops
@@ -81,9 +160,9 @@ type CloudHandler struct {
 }
 
 // NewCloudHandler initialises a new cloud handler.
-func NewCloudHandler(cloud gostatsd.CloudProvider, handler gostatsd.PipelineHandler, logger logrus.FieldLogger, limiter *rate.Limiter, cacheOptions *CacheOptions) *CloudHandler {
+func NewCloudHandler(cloud gostatsd.CloudProvider, handler gostatsd.PipelineHandler, logger logrus.FieldLogger, limiter *rate.Limiter, cacheOptions CacheOptions) *CloudHandler {
 	return &CloudHandler{
-		cacheOpts:       *cacheOptions,
+		cacheOpts:       cacheOptions,
 		cloud:           cloud,
 		handler:         handler,
 		limiter:         limiter,
@@ -226,13 +305,21 @@ func (ch *CloudHandler) Run(ctx context.Context) {
 		logger:        ch.logger,
 	}
 
-	var wg wait.Group
-	defer wg.Wait() // Wait for lookupDispatcher to stop
+	// Stager will perform ordered, graceful shutdown. Stage by stage in reverse startup order.
+	stgr := stager.New()
+	defer stgr.Shutdown() // Wait for CloudProvider and lookupDispatcher to stop
+
+	stage := stgr.NextStage()
 
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel() // Tell lookupDispatcher to stop
+	// If the cloud provider implements runnable, it has things to do. It must start before anything that
+	// accesses it
+	if ch.cloud != nil {
+		if r, ok := ch.cloud.(gostatsd.Runner); ok {
+			go r.Run(ctx) // fork
+		}
+	}
 
-	wg.StartWithContext(ctx, ld.run)
+	stage.StartWithContext(ld.run) // Start the lookup mechanism
 
 	refreshTicker := time.NewTicker(ch.cacheOpts.CacheRefreshPeriod)
 	defer refreshTicker.Stop()
diff --git a/pkg/statsd/statsd.go b/pkg/statsd/statsd.go
index 83aa991..f0c9a35 100644
--- a/pkg/statsd/statsd.go
+++ b/pkg/statsd/statsd.go
@@ -15,7 +15,7 @@ import (
 	"github.com/atlassian/gostatsd/pkg/web"
 
 	"github.com/ash2k/stager"
-	reuseport "github.com/libp2p/go-reuseport"
+	"github.com/libp2p/go-reuseport"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	"golang.org/x/time/rate"
@@ -25,8 +25,7 @@ import (
 // the statsd server. These can either be set via command line or directly.
 type Server struct {
 	Backends                  []gostatsd.Backend
-	CloudProvider             gostatsd.CloudProvider
-	Limiter                   *rate.Limiter
+	CloudHandlerFactory       *CloudHandlerFactory
 	InternalTags              gostatsd.Tags
 	InternalNamespace         string
 	DefaultTags               gostatsd.Tags
@@ -53,9 +52,8 @@ type Server struct {
 	ServerMode                string
 	Hostname                  string
 	LogRawMetric              bool
-	CacheOptions
-	Viper         *viper.Viper
-	TransportPool *transport.TransportPool
+	Viper                     *viper.Viper
+	TransportPool             *transport.TransportPool
 }
 
 // Run runs the server until context signals done.
@@ -150,11 +148,11 @@ func (s *Server) RunWithCustomSocket(ctx context.Context, sf SocketFactory) erro
 
 	// Create the cloud handler
 	ip := gostatsd.UnknownIP
-	if s.CloudProvider != nil {
-		cloudHandler := NewCloudHandler(s.CloudProvider, handler, log.StandardLogger(), s.Limiter, &s.CacheOptions)
+	if s.CloudHandlerFactory != nil {
+		cloudHandler := s.CloudHandlerFactory.NewCloudHandler(handler)
 		runnables = append(runnables, cloudHandler.Run)
 		handler = cloudHandler
-		selfIP, err2 := s.CloudProvider.SelfIP()
+		selfIP, err2 := cloudHandler.cloud.SelfIP()
 		if err2 != nil {
 			log.Warnf("Failed to get self ip: %v", err2)
 		} else {
diff --git a/pkg/statsd/statsd_defaults_and_params.go b/pkg/statsd/statsd_defaults_and_params.go
index 4243572..cb61f3a 100644
--- a/pkg/statsd/statsd_defaults_and_params.go
+++ b/pkg/statsd/statsd_defaults_and_params.go
@@ -2,11 +2,12 @@ package statsd
 
 import (
 	"runtime"
-	"time"
-
 	"strings"
+	"time"
 
 	"github.com/atlassian/gostatsd"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders/aws"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders/k8s"
 
 	"github.com/spf13/pflag"
 )
@@ -32,6 +33,43 @@ var DefaultTags = gostatsd.Tags{}
 // DefaultInternalTags is the default list of additional tags on internal metrics
 var DefaultInternalTags = gostatsd.Tags{}
 
+// DefaultCloudProviderCacheValues contains the default cache values for each cloud provider.
+var DefaultCloudProviderCacheValues = map[string]CacheOptions{
+	k8s.ProviderName: {
+		// The refresh period is low for k8s provider since there is no network cost to looking up pods
+		CacheRefreshPeriod:        15 * time.Second,
+		CacheEvictAfterIdlePeriod: DefaultCacheEvictAfterIdlePeriod,
+		// TTLs are low for the k8s provider because there is no network cost to doing a refresh of the data
+		// This means we get fresh data every time the refresh period is up
+		CacheTTL:         1 * time.Millisecond,
+		CacheNegativeTTL: 1 * time.Millisecond,
+	},
+	aws.ProviderName: {
+		CacheRefreshPeriod:        DefaultCacheRefreshPeriod,
+		CacheEvictAfterIdlePeriod: DefaultCacheEvictAfterIdlePeriod,
+		CacheTTL:                  DefaultCacheTTL,
+		CacheNegativeTTL:          DefaultCacheNegativeTTL,
+	},
+}
+
+type LimiterValues struct {
+	MaxCloudRequests   int
+	BurstCloudRequests int
+}
+
+// DefaultCloudProviderLimiterValues contains the default limiter values for each cloud provider.
+var DefaultCloudProviderLimiterValues = map[string]LimiterValues{
+	k8s.ProviderName: {
+		// High limit for k8s since we don't make network requests for each data request
+		MaxCloudRequests:   10000,
+		BurstCloudRequests: 5000,
+	},
+	aws.ProviderName: {
+		MaxCloudRequests:   DefaultMaxCloudRequests,
+		BurstCloudRequests: DefaultBurstCloudRequests,
+	},
+}
+
 const (
 	// StatserInternal is the name used to indicate the use of the internal statser.
 	StatserInternal = "internal"
diff --git a/pkg/util/viper.go b/pkg/util/viper.go
index f82477d..9a9183d 100644
--- a/pkg/util/viper.go
+++ b/pkg/util/viper.go
@@ -1,13 +1,33 @@
 package util
 
 import (
+	"strings"
+
 	"github.com/spf13/viper"
 )
 
+// EnvPrefix is the prefix of the inspected environment variables.
+const EnvPrefix = "GSD" //Go Stats D
+
 func GetSubViper(v *viper.Viper, key string) *viper.Viper {
 	n := v.Sub(key)
 	if n == nil {
 		n = viper.New()
 	}
+	InitViper(n, key)
 	return n
 }
+
+// InitViper sets up env var handling for a viper. This must be run on every created sub viper as these settings
+// are not persisted to nested viper instances.
+func InitViper(v *viper.Viper, subViperName string) {
+	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+	if subViperName != "" {
+		// Sub viper environment variables are accessed via <EnvPrefix>_<subViperName>_<varName>
+		v.SetEnvPrefix(EnvPrefix + "_" + strings.ToUpper(subViperName))
+	} else {
+		v.SetEnvPrefix(EnvPrefix)
+	}
+	v.SetTypeByDefaultValue(true)
+	v.AutomaticEnv()
+}


Test patch:
diff --git a/pkg/cloudproviders/k8s/k8s_test.go b/pkg/cloudproviders/k8s/k8s_test.go
new file mode 100644
index 0000000..b1f2422
--- /dev/null
+++ b/pkg/cloudproviders/k8s/k8s_test.go
@@ -0,0 +1,339 @@
+package k8s
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/atlassian/gostatsd"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	core_v1 "k8s.io/api/core/v1"
+	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/watch"
+	mainFake "k8s.io/client-go/kubernetes/fake"
+	kube_testing "k8s.io/client-go/testing"
+)
+
+const (
+	namespace = "foo"
+	podName1  = "pod1"
+	podName2  = "pod2"
+	nodeName  = "node1"
+
+	ipAddr  = "127.0.0.1"
+	ipAddr2 = "10.0.0.1"
+)
+
+type tagTestValue struct {
+	key         string // the key of the annotation/label
+	expectedKey string // the expected key of the tag
+	value       string // the value of the annotation/label & tag
+}
+
+var (
+	annotationTestValues = []tagTestValue{
+		{AnnotationPrefix + "tag1", "tag1", "value1"},
+		{"product.company.com/tag2", "tag2", "value2"},
+		{"tag3", "tag3", "value3"},
+	}
+	labelTestValues = []tagTestValue{
+		{"app", "app", "testApp"},
+		{"label", "label", "value"},
+	}
+)
+
+func pod() *core_v1.Pod {
+	annotations := map[string]string{}
+	for _, atv := range annotationTestValues {
+		annotations[atv.key] = atv.value
+	}
+	labels := map[string]string{}
+	for _, atv := range labelTestValues {
+		labels[atv.key] = atv.value
+	}
+
+	return &core_v1.Pod{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:        podName1,
+			Namespace:   namespace,
+			Annotations: annotations,
+			Labels:      labels,
+		},
+		Status: core_v1.PodStatus{
+			PodIP: ipAddr,
+		},
+	}
+}
+
+func pod2() *core_v1.Pod {
+	// No labels/annotations for this pod
+	annotations := map[string]string{}
+	labels := map[string]string{}
+
+	return &core_v1.Pod{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:        podName2,
+			Namespace:   namespace,
+			Annotations: annotations,
+			Labels:      labels,
+		},
+		Status: core_v1.PodStatus{
+			PodIP: ipAddr2,
+		},
+	}
+}
+
+type testFixture struct {
+	fakeClient    *mainFake.Clientset
+	cloudProvider gostatsd.CloudProvider
+	podsWatch     *watch.FakeWatcher
+}
+
+func setupTest(t *testing.T, test func(*testing.T, *testFixture), v *viper.Viper, nn string) {
+	fakeClient := mainFake.NewSimpleClientset()
+	podsWatch := watch.NewFake()
+	fakeClient.PrependWatchReactor("pods", kube_testing.DefaultWatchReactor(podsWatch, nil))
+
+	// We have to set these to the defaults manually here as we're sidestepping the Viper creation path
+	// to inject things
+	setViperDefaults(v, "test")
+
+	cloudProvider, err := NewProvider(
+		logrus.StandardLogger(),
+		fakeClient,
+		PodInformerOptions{
+			ResyncPeriod: v.GetDuration(ParamResyncPeriod),
+			WatchCluster: v.GetBool(ParamWatchCluster),
+			NodeName:     nn,
+		},
+		regexp.MustCompile(v.GetString(ParamAnnotationTagRegex)),
+		regexp.MustCompile(v.GetString(ParamLabelTagRegex)),
+	)
+	require.NoError(t, err)
+
+	r, ok := cloudProvider.(gostatsd.Runner)
+	require.True(t, ok)
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	r.Run(ctx) // run the cloud provider
+
+	test(t, &testFixture{
+		fakeClient:    fakeClient,
+		cloudProvider: cloudProvider,
+		podsWatch:     podsWatch,
+	})
+}
+
+type tagTest struct {
+	name                 string
+	viperParams          map[string]interface{}
+	pods                 []*core_v1.Pod
+	expectedNumTags      int
+	expectedTagsContains []tagTestValue
+}
+
+var ipTagTests = []tagTest{
+	{
+		name:                 "WithDefaultTagWhitelists",
+		pods:                 []*core_v1.Pod{pod()},
+		expectedNumTags:      1,
+		expectedTagsContains: []tagTestValue{annotationTestValues[0]},
+	},
+	// The same test as above but with two pods to choose from
+	{
+		name:                 "WhenMultiplePods",
+		pods:                 []*core_v1.Pod{pod(), pod2()},
+		expectedNumTags:      1,
+		expectedTagsContains: []tagTestValue{annotationTestValues[0]},
+	},
+	{
+		name: "WithCustomAnnotationTagWhitelist",
+		viperParams: map[string]interface{}{
+			ParamAnnotationTagRegex: fmt.Sprintf("^%s%s$", regexp.QuoteMeta("product.company.com/"), DefaultTagCaptureRegex),
+		},
+		pods:                 []*core_v1.Pod{pod()},
+		expectedNumTags:      1,
+		expectedTagsContains: []tagTestValue{annotationTestValues[1]},
+	},
+	{
+		name: "WithCustomAnnotationTagWhitelistMultipleRegex",
+		viperParams: map[string]interface{}{
+			ParamAnnotationTagRegex: fmt.Sprintf(
+				"^(%s|%s)%s$",
+				regexp.QuoteMeta("product.company.com/"),
+				regexp.QuoteMeta(AnnotationPrefix),
+				DefaultTagCaptureRegex),
+		},
+		pods:                 []*core_v1.Pod{pod()},
+		expectedNumTags:      2,
+		expectedTagsContains: []tagTestValue{annotationTestValues[0], annotationTestValues[1]},
+	},
+	{
+		name: "WithCustomLabelTagWhitelist",
+		viperParams: map[string]interface{}{
+			ParamLabelTagRegex: "^app",
+		},
+		pods:                 []*core_v1.Pod{pod()},
+		expectedNumTags:      2, // we're still using the default annotation whitelist too
+		expectedTagsContains: []tagTestValue{labelTestValues[0], annotationTestValues[0]},
+	},
+	{
+		name: "WithCustomLabelTagWhitelistMultipleRegex",
+		viperParams: map[string]interface{}{
+			ParamLabelTagRegex: "^(app|label)",
+		},
+		pods:                 []*core_v1.Pod{pod()},
+		expectedNumTags:      3, // we're still using the default annotation whitelist too
+		expectedTagsContains: []tagTestValue{labelTestValues[0], annotationTestValues[0], labelTestValues[1]},
+	},
+}
+
+func waitForFullCache(t *testing.T, fixtures *testFixture, numExpectedPods int) {
+	// Wait for the cache to fill up before moving on
+	cp, ok := fixtures.cloudProvider.(*Provider)
+	require.True(t, ok, "fixture must produce a k8s.Provider")
+	for i := 1; i < 100 && len(cp.podsInf.GetIndexer().List()) < numExpectedPods; i++ {
+		time.Sleep(10 * time.Millisecond)
+	}
+	require.Equal(t, numExpectedPods, len(cp.podsInf.GetIndexer().List()))
+}
+
+func TestIPToTags(t *testing.T) {
+	t.Parallel()
+
+	for _, testCase := range ipTagTests {
+		flags := viper.New()
+		for k, v := range testCase.viperParams {
+			flags.Set(k, v)
+		}
+
+		// Run tests in their own namespace for clarity
+		t.Run(testCase.name, func(tt *testing.T) {
+			setupTest(tt, func(ttt *testing.T, fixtures *testFixture) {
+				for _, p := range testCase.pods {
+					fixtures.podsWatch.Add(p)
+				}
+				waitForFullCache(t, fixtures, len(testCase.pods))
+
+				// Run the test
+				ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+				defer cancel()
+
+				instanceData, err := fixtures.cloudProvider.Instance(ctx, ipAddr)
+				require.NoError(ttt, err)
+
+				instance := instanceData[ipAddr]
+				require.NotNil(ttt, instance)
+				assert.Equal(ttt, instance.ID, fmt.Sprintf("%s/%s", namespace, podName1))
+				assert.Len(ttt, instance.Tags, testCase.expectedNumTags)
+				for _, expectedTag := range testCase.expectedTagsContains {
+					assert.Contains(ttt, instance.Tags, fmt.Sprintf("%s:%s", expectedTag.expectedKey, expectedTag.value))
+				}
+			}, flags, nodeName)
+		})
+	}
+}
+
+func TestNoHostNetworkPodsCached(t *testing.T) {
+	setupTest(t, func(t *testing.T, fixtures *testFixture) {
+		hostNetworkPod := pod()
+		hostNetworkPod.Status.HostIP = ipAddr
+		hostNetworkPod.Spec.HostNetwork = true
+		fixtures.podsWatch.Add(hostNetworkPod)
+		waitForFullCache(t, fixtures, 1)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+		defer cancel()
+
+		instanceData, err := fixtures.cloudProvider.Instance(ctx, ipAddr)
+		require.NoError(t, err)
+
+		instance := instanceData[ipAddr]
+		require.Nil(t, instance)
+	}, viper.New(), nodeName)
+}
+
+func TestWatchNodeOnly(t *testing.T) {
+	v := viper.New()
+	v.Set(ParamWatchCluster, false)
+
+	setupTest(t, func(t *testing.T, fixtures *testFixture) {
+		expectedKey := "node"
+		nodeAnnotationKey := AnnotationPrefix + expectedKey
+		expectedValue := nodeName
+
+		p := pod()
+		p.Spec.NodeName = nodeName
+		p.ObjectMeta.Annotations = map[string]string{nodeAnnotationKey: expectedValue}
+		p.ObjectMeta.Labels = map[string]string{}
+		fixtures.podsWatch.Add(p)
+
+		p = pod2()
+		p.Spec.NodeName = "node2"
+		p.ObjectMeta.Annotations = map[string]string{nodeAnnotationKey: "node2"}
+		p.ObjectMeta.Labels = map[string]string{}
+		fixtures.podsWatch.Add(p)
+
+		waitForFullCache(t, fixtures, 2)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+		defer cancel()
+
+		instanceData, err := fixtures.cloudProvider.Instance(ctx, ipAddr)
+		require.NoError(t, err)
+
+		instance := instanceData[ipAddr2]
+		require.Nil(t, instance, "expected pod with IP '%s' not to be indexed since it is on a different node to us", ipAddr2)
+
+		instance = instanceData[ipAddr]
+		require.NotNil(t, instance, "expected pod with IP '%s' to be indexed since it is on our node", ipAddr)
+		assert.Len(t, instance.Tags, 1)
+		assert.Contains(t, instance.Tags, fmt.Sprintf("%s:%s", expectedKey, expectedValue))
+	}, v, nodeName)
+}
+
+func TestWatchNodeFailsNoNodeName(t *testing.T) {
+	fakeClient := mainFake.NewSimpleClientset()
+	podsWatch := watch.NewFake()
+	fakeClient.PrependWatchReactor("pods", kube_testing.DefaultWatchReactor(podsWatch, nil))
+
+	_, err := NewProvider(
+		logrus.StandardLogger(),
+		fakeClient,
+		PodInformerOptions{
+			ResyncPeriod: DefaultResyncPeriod,
+			WatchCluster: false, // the important bit
+			NodeName:     "",
+		},
+		regexp.MustCompile(DefaultAnnotationTagRegex),
+		regexp.MustCompile(DefaultLabelTagRegex),
+	)
+	require.Error(t, err, "creating k8s provider to watch node with no node name should fail")
+}
+
+func TestGetTagNameFromRegex(t *testing.T) {
+	tests := map[string]struct {
+		str             string
+		re              string
+		expectedTagName string
+	}{
+		"NoMatchNoTagCaptureGroup":   {"donotmatch", "aaa", ""},
+		"NoMatchWithTagCaptureGroup": {"donotmatch", "(?P<tag>aaa)", ""},
+		"MatchNoTagCaptureGroup":     {"matchthis", "match", "matchthis"},
+		"MatchWithTagCaptureGroup":   {"matchthis", "(?P<tag>match)", "match"},
+		"NoMatchWithEmptyRegex":      {"matchthis", "", ""},
+	}
+
+	for name, testCase := range tests {
+		t.Run(name, func(t *testing.T) {
+			tagName := getTagNameFromRegex(regexp.MustCompile(testCase.re), testCase.str)
+			assert.Equal(t, testCase.expectedTagName, tagName)
+		})
+	}
+}
diff --git a/pkg/statsd/handler_cloud_test.go b/pkg/statsd/handler_cloud_test.go
index 343c19c..0a7d1d2 100644
--- a/pkg/statsd/handler_cloud_test.go
+++ b/pkg/statsd/handler_cloud_test.go
@@ -10,9 +10,12 @@ import (
 	"time"
 
 	"github.com/atlassian/gostatsd"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders/aws"
+	"github.com/atlassian/gostatsd/pkg/cloudproviders/k8s"
 
 	"github.com/ash2k/stager/wait"
 	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/time/rate"
 )
@@ -22,7 +25,7 @@ import (
 func BenchmarkCloudHandlerDispatchMetric(b *testing.B) {
 	fp := &fakeProviderIP{}
 	nh := &nopHandler{}
-	ch := NewCloudHandler(fp, nh, logrus.StandardLogger(), rate.NewLimiter(100, 120), &CacheOptions{
+	ch := NewCloudHandler(fp, nh, logrus.StandardLogger(), rate.NewLimiter(100, 120), CacheOptions{
 		CacheRefreshPeriod:        100 * time.Millisecond,
 		CacheEvictAfterIdlePeriod: 700 * time.Millisecond,
 		CacheTTL:                  500 * time.Millisecond,
@@ -56,7 +59,7 @@ func TestTransientInstanceFailure(t *testing.T) {
 
 	counting := &countingHandler{}
 
-	ch := NewCloudHandler(fpt, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), &CacheOptions{
+	ch := NewCloudHandler(fpt, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), CacheOptions{
 		CacheRefreshPeriod:        50 * time.Millisecond,
 		CacheEvictAfterIdlePeriod: 1 * time.Minute,
 		CacheTTL:                  1 * time.Millisecond,
@@ -139,7 +142,7 @@ func testExpireAndRefresh(t *testing.T, expectedIp gostatsd.IP, f func(*CloudHan
 		T+66: refresh loop, entry is expired
 		T+70: sleep completes
 	*/
-	ch := NewCloudHandler(fp, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), &CacheOptions{
+	ch := NewCloudHandler(fp, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), CacheOptions{
 		CacheRefreshPeriod:        11 * time.Millisecond,
 		CacheEvictAfterIdlePeriod: 50 * time.Millisecond,
 		CacheTTL:                  10 * time.Millisecond,
@@ -238,9 +241,51 @@ func TestCloudHandlerFailingProvider(t *testing.T) {
 	doCheck(t, fp, sm1(), se1(), sm2(), se2(), &fp.ips, expectedIps, expectedMetrics, expectedEvents)
 }
 
+func TestConstructCloudHandlerFactoryFromViper(t *testing.T) {
+	t.Parallel()
+	v := viper.New()
+	logger := logrus.StandardLogger()
+
+	// Test no cloud handler
+	factory, err := NewCloudHandlerFactoryFromViper(v, logger, "test")
+	assert.NoError(t, err)
+	assert.Nil(t, factory.cloudProvider)
+
+	// Test unknown cloud handler - unsupported
+	v.Set(ParamCloudProvider, "unknown")
+	_, err = NewCloudHandlerFactoryFromViper(v, logger, "test")
+	assert.Error(t, err)
+
+	// Test known cloud provider defaults
+	cloudProvidersToTest := []string{aws.ProviderName, k8s.ProviderName}
+	for _, cpName := range cloudProvidersToTest {
+		v.Set(ParamCloudProvider, cpName)
+		factory, err = NewCloudHandlerFactoryFromViper(v, logger, "test")
+		assert.NoError(t, err)
+		assert.EqualValues(t, DefaultCloudProviderCacheValues[cpName], factory.cacheOptions)
+		assert.Equal(t, rate.Limit(DefaultCloudProviderLimiterValues[cpName].MaxCloudRequests), factory.limiter.Limit())
+		assert.Equal(t, DefaultCloudProviderLimiterValues[cpName].BurstCloudRequests, factory.limiter.Burst())
+	}
+}
+
+func TestInitCloudHandlerFactory(t *testing.T) {
+	t.Parallel()
+	v := viper.New()
+	logger := logrus.StandardLogger()
+
+	factory, err := NewCloudHandlerFactoryFromViper(v, logger, "test")
+	assert.NoError(t, err)
+	assert.Nil(t, factory.cloudProvider)
+
+	err = factory.InitCloudProvider(v)
+	assert.NoError(t, err)
+
+	// We don't test the path for specific cloud providers as they have logic that isn't easily mockable
+}
+
 func doCheck(t *testing.T, cloud gostatsd.CloudProvider, m1 gostatsd.Metric, e1 gostatsd.Event, m2 gostatsd.Metric, e2 gostatsd.Event, ips *[]gostatsd.IP, expectedIps []gostatsd.IP, expectedM []gostatsd.Metric, expectedE gostatsd.Events) {
 	counting := &countingHandler{}
-	ch := NewCloudHandler(cloud, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), &CacheOptions{
+	ch := NewCloudHandler(cloud, counting, logrus.StandardLogger(), rate.NewLimiter(100, 120), CacheOptions{
 		CacheRefreshPeriod:        DefaultCacheRefreshPeriod,
 		CacheEvictAfterIdlePeriod: DefaultCacheEvictAfterIdlePeriod,
 		CacheTTL:                  DefaultCacheTTL,
diff --git a/pkg/statsd/statsd_test.go b/pkg/statsd/statsd_test.go
index 5f2eff3..cf4452e 100644
--- a/pkg/statsd/statsd_test.go
+++ b/pkg/statsd/statsd_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/atlassian/gostatsd"
 	"github.com/atlassian/gostatsd/pkg/fakesocket"
 
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	"golang.org/x/time/rate"
 )
@@ -22,15 +23,29 @@ func TestStatsdThroughput(t *testing.T) {
 	var memStatsStart, memStatsFinish runtime.MemStats
 	runtime.ReadMemStats(&memStatsStart)
 	backend := &countingBackend{}
-	s := Server{
-		Backends: []gostatsd.Backend{backend},
-		CloudProvider: &fakeProvider{
-			instance: &gostatsd.Instance{
-				ID:   "i-13123123",
-				Tags: gostatsd.Tags{"region:us-west-3", "tag1", "tag2:234"},
-			},
+
+	cloudHandlerFactory := newCloudHandlerFactory(
+		"",
+		logrus.StandardLogger(),
+		CacheOptions{
+			CacheRefreshPeriod:        DefaultCacheRefreshPeriod,
+			CacheEvictAfterIdlePeriod: DefaultCacheEvictAfterIdlePeriod,
+			CacheTTL:                  DefaultCacheTTL,
+			CacheNegativeTTL:          DefaultCacheNegativeTTL,
 		},
-		Limiter:             rate.NewLimiter(DefaultMaxCloudRequests, DefaultBurstCloudRequests),
+		rate.NewLimiter(DefaultMaxCloudRequests, DefaultBurstCloudRequests),
+		"test")
+	// Inject the mock provider
+	cloudHandlerFactory.cloudProvider = &fakeProvider{
+		instance: &gostatsd.Instance{
+			ID:   "i-13123123",
+			Tags: gostatsd.Tags{"region:us-west-3", "tag1", "tag2:234"},
+		},
+	}
+
+	s := Server{
+		Backends:            []gostatsd.Backend{backend},
+		CloudHandlerFactory: cloudHandlerFactory,
 		DefaultTags:         DefaultTags,
 		ExpiryInterval:      DefaultExpiryInterval,
 		FlushInterval:       DefaultFlushInterval,
@@ -44,13 +59,7 @@ func TestStatsdThroughput(t *testing.T) {
 		ReceiveBatchSize:    DefaultReceiveBatchSize,
 		MaxConcurrentEvents: 2,
 		ServerMode:          "standalone",
-		CacheOptions: CacheOptions{
-			CacheRefreshPeriod:        DefaultCacheRefreshPeriod,
-			CacheEvictAfterIdlePeriod: DefaultCacheEvictAfterIdlePeriod,
-			CacheTTL:                  DefaultCacheTTL,
-			CacheNegativeTTL:          DefaultCacheNegativeTTL,
-		},
-		Viper: viper.New(),
+		Viper:               viper.New(),
 	}
 	ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancelFunc()