Repository: knative/pkg
Pull request #1773 (2020-10-05 19:41:52)

Use zapdriver config as base zap config
Changes the following fields in the default logging configuration:
"msg" => "message"
"level" => "severity"
"ts" => "timestamp"

To retain the old names for these fields, users should specify the old field names in the logging configmap by setting
```
zap-logger-config: |
      {
        "encoderConfig": {                                                                                                                                                                                         
          "timeKey": "ts",
          "levelKey": "level",
          "messageKey": "msg",
        }
      }
```

Handling of the zap-logger-config entry has been changed to allow for partially specified config records such as this one. Unspecified fields will fallback to the default config values.

Also adds the fields "logging.googleapis.com/labels" and "logging.googleapis.com/sourceLocation" to every log message. Example of a pretty-printed log message with the new defaults:

```
{
  "severity": "INFO",
  "timestamp": "2020-10-29T23:46:13.962986283Z",
  "logger": "controller",
  "caller": "leaderelection/context.go:138",
  "message": "controller.knative.dev-serving-pkg-reconciler-service.reconciler.00-of-01 will run in leader-elected mode with id \"controller-8476b45df-bknpc_371cbe10-2659-441f-9739-a8a32e49403e\"",
  "commit": "2d707b8",
  "knative.dev/pod": "controller-8476b45df-bknpc",
  "logging.googleapis.com/labels": {},
  "logging.googleapis.com/sourceLocation": {
    "file": "knative.dev/pkg@v0.0.0-20201029122234-6d905b3f84a6/leaderelection/context.go",
    "line": "138",
    "function": "knative.dev/pkg/leaderelection.(*standardBuilder).buildElector"
  }
}
```

Closes #1681

knative-prow-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/1773#" title="Author self-approved">ian-mi</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **dprotaso**
You can assign the PR to them by writing `/assign @dprotaso` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/knative/pkg/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["dprotaso"]} -->
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/1773#pullrequestreview-502370102" title="Approved">evankanderson</a>*, *<a href="https://github.com/knative/pkg/pull/1773#" title="Author self-approved">ian-mi</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative/pkg/blob/master/OWNERS)~~ [evankanderson]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
vagababov: If the default config yields the same output, then it should be alright in reads to backcompat as well, I think.
ian-mi: The default has been changed to use the new configuration. Any JSON config present in the loggin configmap will be unmarshalled on-top of the new default configuration rather than into an empty config. As a consequence a sparse configJSON can be specified and unset fields will default to those in the default zapdriver config.
ian-mi: Note that I haven't wrapped the zap Core with the custom zapdriver Core, but it would be easy to do so. The zapdriver core only seems to be necessary for two things: automatic insertion of SourceLocation on every log call and automatic handling of labels.
grantr: :+1: to this change. Ideally we'd also give instructions in release notes for users to revert to the previous behavior, or at least loudly proclaim that the default log format has changed.
knative-prow-robot: @ian-mi: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
codecov[bot]: # [Codecov](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=h1) Report
> Merging [#1773](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=desc) into [master](https://codecov.io/gh/knative/pkg/commit/6d905b3f84a6d524a5b3fbb5eb0dd4b39affdc5a?el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/knative/pkg/pull/1773/graphs/tree.svg?width=650&height=150&src=pr&token=rlqpDnfxbO)](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1773      +/-   ##
==========================================
- Coverage   69.35%   69.31%   -0.04%     
==========================================
  Files         223      223              
  Lines        9492     9490       -2     
==========================================
- Hits         6583     6578       -5     
- Misses       2576     2578       +2     
- Partials      333      334       +1     
```


| [Impacted Files](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [logging/config.go](https://codecov.io/gh/knative/pkg/pull/1773/diff?src=pr&el=tree#diff-bG9nZ2luZy9jb25maWcuZ28=) | `84.04% <100.00%> (-3.46%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=footer). Last update [6d905b3...91b407e](https://codecov.io/gh/knative/pkg/pull/1773?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

knative-prow-robot: @ian-mi: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-build-tests | 91b407e99c36c738cb82820b23b5b7ef7e370607 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-build-tests/1321888770034765824) | `/test pull-knative-pkg-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1773). [Your PR dashboard](https://gubernator.knative.dev/pr/ian-mi).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @ian-mi: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
markusthoemmes: Is this still something we're pursuing? Needs a rebase!
knative-prow-robot: @ian-mi: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-build-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-build-tests/1331649294150668288) | `/test pull-knative-pkg-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1773). [Your PR dashboard](https://gubernator.knative.dev/pr/ian-mi).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @ian-mi: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-build-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-build-tests/1331649294150668288) | `/test pull-knative-pkg-build-tests`
pull-knative-pkg-unit-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-unit-tests/1331649294184222720) | `/test pull-knative-pkg-unit-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1773). [Your PR dashboard](https://gubernator.knative.dev/pr/ian-mi).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @ian-mi: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-build-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-build-tests/1331649294150668288) | `/test pull-knative-pkg-build-tests`
pull-knative-pkg-unit-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-unit-tests/1331649294184222720) | `/test pull-knative-pkg-unit-tests`
pull-knative-pkg-integration-tests | cd82b1faba418ef9e1175060e1f2a901ff43a230 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_pkg/1773/pull-knative-pkg-integration-tests/1331649294205194240) | `/test pull-knative-pkg-integration-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1773). [Your PR dashboard](https://gubernator.knative.dev/pr/ian-mi).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
ian-mi: Thanks for the ping! I've rebased this PR, but it is still blocked on https://github.com/knative/serving/pull/9989 which is blocked on a failing test.

Patch:
diff --git a/.github/workflows/knative-releasability.yaml b/.github/workflows/knative-releasability.yaml
index f13cc479a..ca0820a6e 100644
--- a/.github/workflows/knative-releasability.yaml
+++ b/.github/workflows/knative-releasability.yaml
@@ -142,7 +142,7 @@ jobs:
           echo 'SLACK_TITLE=Releasability for ${{ github.repository }} @ ${{ env.RELEASE }} is NO-GO!' >> $GITHUB_ENV
 
       - name: Post status to Slack
-        if: steps.exists.outputs.release-branch == 'false'
+        if: ${{ secrets.SLACK_WEBHOOK != '' && steps.exists.outputs.release-branch == 'false' }}
         uses: rtCamp/action-slack-notify@v2.1.0
         env:
           SLACK_ICON: http://github.com/knative.png?size=48
diff --git a/.github/workflows/knative-style.yaml b/.github/workflows/knative-style.yaml
index 56360215f..5d3d3163e 100644
--- a/.github/workflows/knative-style.yaml
+++ b/.github/workflows/knative-style.yaml
@@ -100,7 +100,7 @@ jobs:
 
       - name: Install Tools
         env:
-          WOKE_VERSION: v0.1.15
+          WOKE_VERSION: v0.4.1
         run: |
           TEMP_PATH="$(mktemp -d)"
           cd $TEMP_PATH
diff --git a/go.mod b/go.mod
index 229ef3537..9320177ff 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	contrib.go.opencensus.io/exporter/zipkin v0.1.2
 	github.com/aws/aws-sdk-go v1.31.12 // indirect
 	github.com/blang/semver/v4 v4.0.0
+	github.com/blendle/zapdriver v1.3.1
 	github.com/census-instrumentation/opencensus-proto v0.3.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/dgryski/go-gk v0.0.0-20200319235926-a69029f61654 // indirect
@@ -74,6 +75,6 @@ require (
 	k8s.io/klog v1.0.0
 	k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29 // indirect
 	k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 // indirect
-	knative.dev/hack v0.0.0-20201120192952-353db687ec5b
+	knative.dev/hack v0.0.0-20201201234937-fddbf732e450
 	sigs.k8s.io/structured-merge-diff/v3 v3.0.1-0.20200706213357-43c19bbb7fba // indirect
 )
diff --git a/go.sum b/go.sum
index cf77665d0..581f0595d 100644
--- a/go.sum
+++ b/go.sum
@@ -109,6 +109,8 @@ github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17
 github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
+github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
 github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE=
 github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
 github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
@@ -682,6 +684,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -1120,8 +1123,8 @@ k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29/go.mod h1:F+5wygcW0wmRTnM
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 h1:v8ud2Up6QK1lNOKFgiIVrZdMg7MpmSnvtrOieolJKoE=
 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-knative.dev/hack v0.0.0-20201120192952-353db687ec5b h1:Lc+AKgwhAZUD98mN++qTHeeaP6FRmS8fcwc/rXkP8G0=
-knative.dev/hack v0.0.0-20201120192952-353db687ec5b/go.mod h1:PHt8x8yX5Z9pPquBEfIj0X66f8iWkWfR0S/sarACJrI=
+knative.dev/hack v0.0.0-20201201234937-fddbf732e450 h1:IyitWF7OzfunCgE4b9ZsJAeIRoQ6JOyqDrz/19X4iS4=
+knative.dev/hack v0.0.0-20201201234937-fddbf732e450/go.mod h1:PHt8x8yX5Z9pPquBEfIj0X66f8iWkWfR0S/sarACJrI=
 pgregory.net/rapid v0.3.3 h1:jCjBsY4ln4Atz78QoBWxUEvAHaFyNDQg9+WU62aCn1U=
 pgregory.net/rapid v0.3.3/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/logging/config.go b/logging/config.go
index e1480033d..65b50395a 100644
--- a/logging/config.go
+++ b/logging/config.go
@@ -23,6 +23,7 @@ import (
 	"os"
 	"strings"
 
+	"github.com/blendle/zapdriver"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 	corev1 "k8s.io/api/core/v1"
@@ -54,14 +55,14 @@ func NewLogger(configJSON string, levelOverride string, opts ...zap.Option) (*za
 		return enrichLoggerWithCommitID(logger), atomicLevel
 	}
 
-	loggingCfg := zap.NewProductionConfig()
+	loggingCfg := zapdriver.NewProductionConfig()
 	if levelOverride != "" {
 		if level, err := levelFromString(levelOverride); err == nil {
 			loggingCfg.Level = zap.NewAtomicLevelAt(*level)
 		}
 	}
 
-	logger, err2 := loggingCfg.Build(opts...)
+	logger, err2 := loggingCfg.Build(append(opts, zapdriver.WrapCore())...)
 	if err2 != nil {
 		panic(err2)
 	}
@@ -105,7 +106,7 @@ func newLoggerFromConfig(configJSON string, levelOverride string, opts []zap.Opt
 		}
 	}
 
-	logger, err := loggingCfg.Build(opts...)
+	logger, err := loggingCfg.Build(append(opts, zapdriver.WrapCore())...)
 	if err != nil {
 		return nil, zap.AtomicLevel{}, err
 	}
@@ -116,15 +117,13 @@ func newLoggerFromConfig(configJSON string, levelOverride string, opts []zap.Opt
 }
 
 func zapConfigFromJSON(configJSON string) (*zap.Config, error) {
-	if configJSON == "" {
-		return nil, errEmptyLoggerConfig
-	}
-
-	loggingCfg := &zap.Config{}
-	if err := json.Unmarshal([]byte(configJSON), loggingCfg); err != nil {
-		return nil, err
+	loggingCfg := zapdriver.NewProductionConfig()
+	if configJSON != "" {
+		if err := json.Unmarshal([]byte(configJSON), &loggingCfg); err != nil {
+			return nil, err
+		}
 	}
-	return loggingCfg, nil
+	return &loggingCfg, nil
 }
 
 // Config contains the configuration defined in the logging ConfigMap.
@@ -134,31 +133,9 @@ type Config struct {
 	LoggingLevel  map[string]zapcore.Level
 }
 
-const defaultZLC = `{
-  "level": "info",
-  "development": false,
-  "outputPaths": ["stdout"],
-  "errorOutputPaths": ["stderr"],
-  "encoding": "json",
-  "encoderConfig": {
-    "timeKey": "ts",
-    "levelKey": "level",
-    "nameKey": "logger",
-    "callerKey": "caller",
-    "messageKey": "msg",
-    "stacktraceKey": "stacktrace",
-    "lineEnding": "",
-    "levelEncoder": "",
-    "timeEncoder": "iso8601",
-    "durationEncoder": "",
-    "callerEncoder": ""
-  }
-}`
-
 func defaultConfig() *Config {
 	return &Config{
-		LoggingConfig: defaultZLC,
-		LoggingLevel:  make(map[string]zapcore.Level),
+		LoggingLevel: make(map[string]zapcore.Level),
 	}
 }
 
diff --git a/network/handlers/error.go b/network/handlers/error.go
deleted file mode 100644
index a6bd3bd03..000000000
--- a/network/handlers/error.go
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-Copyright 2020 The Knative Authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package handlers
-
-import "knative.dev/pkg/network"
-
-// Error sets up a handler suitable for use with the ErrorHandler field on
-// httputil's reverse proxy, which logs /proc/net/sockstat data.
-var Error = network.ErrorHandler
diff --git a/third_party/VENDOR-LICENSE/github.com/blendle/zapdriver/LICENSE b/third_party/VENDOR-LICENSE/github.com/blendle/zapdriver/LICENSE
new file mode 100644
index 000000000..7db456775
--- /dev/null
+++ b/third_party/VENDOR-LICENSE/github.com/blendle/zapdriver/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) Blendle
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/vendor/github.com/blendle/zapdriver/.gitignore b/vendor/github.com/blendle/zapdriver/.gitignore
new file mode 100644
index 000000000..48b8bf907
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/.gitignore
@@ -0,0 +1,1 @@
+vendor/
diff --git a/vendor/github.com/blendle/zapdriver/LICENSE b/vendor/github.com/blendle/zapdriver/LICENSE
new file mode 100644
index 000000000..7db456775
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) Blendle
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/vendor/github.com/blendle/zapdriver/README.md b/vendor/github.com/blendle/zapdriver/README.md
new file mode 100644
index 000000000..8044c5523
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/README.md
@@ -0,0 +1,347 @@
+# :zap: Zapdriver
+
+Blazing fast, [Zap][zap]-based [Stackdriver][stackdriver] logging.
+
+[zap]: https://github.com/uber-go/zap
+[stackdriver]: https://cloud.google.com/stackdriver/
+
+## Usage
+
+This package provides three building blocks to support the full array of
+structured logging capabilities of Stackdriver:
+
+* [Special purpose logging fields](#special-purpose-logging-fields)
+* [Pre-configured Stackdriver-optimized encoder](#pre-configured-stackdriver-optimized-encoder)
+* [Custom Stackdriver Zap core](#custom-stackdriver-zap-core)
+* [Using Error Reporting](#using-error-reporting)
+
+The above components can be used separately, but to start, you can create a new
+Zap logger with all of the above included:
+
+```golang
+logger, err := zapdriver.NewProduction() // with sampling
+logger, err := zapdriver.NewDevelopment() // with `development` set to `true`
+```
+
+The above functions give back a pointer to a `zap.Logger` object, so you can use
+[Zap][zap] like you've always done, except that it now logs in the proper
+[Stackdriver][stackdriver] format.
+
+You can also create a configuration struct, and build your logger from there:
+
+```golang
+config := zapdriver.NewProductionConfig()
+config := zapdriver.NewDevelopmentConfig()
+```
+
+Or, get the Zapdriver encoder, and build your own configuration struct from
+that:
+
+```golang
+encoder := zapdriver.NewProductionEncoderConfig()
+encoder := zapdriver.NewDevelopmentEncoderConfig()
+```
+
+Read on to learn more about the available Stackdriver-specific log fields, and
+how to use the above-mentioned components.
+
+### Special purpose logging fields
+
+You can use the following fields to add extra information to your log entries.
+These fields are parsed by Stackdriver to make it easier to query your logs or
+to use the log details in the Stackdriver monitoring interface.
+
+* [`HTTP`](#http)
+* [`Label`](#label)
+* [`SourceLocation`](#sourcelocation)
+* [`Operation`](#operation)
+* [`TraceContext`](#tracecontext)
+
+#### HTTP
+
+You can log HTTP request/response cycles using the following field:
+
+```golang
+HTTP(req *HTTPPayload) zap.Field
+```
+
+You can either manually build the request payload:
+
+```golang
+req := &HTTPPayload{
+  RequestMethod: "GET",
+  RequestURL: "/",
+  Status: 200,
+}
+```
+
+Or, you can auto generate the struct, based on the available request and
+response objects:
+
+```golang
+NewHTTP(req *http.Request, res *http.Response) *HTTPPayload
+```
+
+You are free to pass in `nil` for either the request or response object, if one
+of them is unavailable to you at the point of logging. Any field depending on
+one or the other will be omitted if `nil` is passed in.
+
+Note that there are some fields that are not populated by either the request or
+response object, and need to be set manually:
+
+* `ServerIP string`
+* `Latency string`
+* `CacheLookup bool`
+* `CacheHit bool`
+* `CacheValidatedWithOriginServer bool`
+* `CacheFillBytes string`
+
+If you have no need for those fields, the quickest way to get started is like
+so:
+
+```golang
+logger.Info("Request Received.", zapdriver.HTTP(zapdriver.NewHTTP(req, res)))
+```
+
+#### Label
+
+You can add a "label" to your payload as follows:
+
+```golang
+Label(key, value string) zap.Field
+```
+
+Note that underwater, this sets the key to `labels.<key>`. You need to be using
+the `zapdriver.Core` core for this to be converted to the proper format for
+Stackdriver to recognize the labels.
+
+See "Custom Stackdriver Zap core" for more details.
+
+If you have a reason not to use the provided Core, you can still wrap labels in
+the right `labels` namespace by using the available function:
+
+```golang
+Labels(fields ...zap.Field) zap.Field
+```
+
+Like so:
+
+```golang
+logger.Info(
+  "Did something.",
+  zapdriver.Labels(
+    zapdriver.Label("hello", "world"),
+    zapdriver.Label("hi", "universe"),
+  ),
+)
+```
+
+Again, wrapping the `Label` calls in `Labels` is not required if you use the
+supplied Zap Core.
+
+#### SourceLocation
+
+You can add a source code location to your log lines to be picked up by
+Stackdriver.
+
+Note that you can set this manually, or use `zapdriver.Core` to automatically
+add this. If you set it manually, _and_ use `zapdriver.Core`, the manual call
+stack will be preserved over the automated one.
+
+```golang
+SourceLocation(pc uintptr, file string, line int, ok bool) zap.Field
+```
+
+Note that the function signature equals that of the return values of
+`runtime.Caller()`. This allows you to catch the stack frame at one location,
+while logging it at a different location, like so:
+
+```golang
+pc, file, line, ok := runtime.Caller(0)
+
+// do other stuff...
+
+logger.Error("Something happened!", zapdriver.SourceLocation(pc, file, line, ok))
+```
+
+If you use `zapdriver.Core`, the above use-case is the only use-case where you
+would want to manually set the source location. In all other situations, you can
+simply omit this field, and it will be added automatically, using the stack
+frame at the location where the log line is triggered.
+
+If you don't use `zapdriver.Core`, and still want to add the source location at
+the frame of the triggered log line, you'd do it like this:
+
+```golang
+logger.Error("Something happened!", zapdriver.SourceLocation(runtime.Caller(0)))
+```
+
+#### Operation
+
+The `Operation` log field allows you to group log lines into a single
+"operation" performed by the application:
+
+```golang
+Operation(id, producer string, first, last bool) zap.Field
+```
+
+For a pair of logs that belong to the same operation, you should use the same
+`id` between them. The `producer` is an arbitrary identifier that should be
+globally unique amongst all the logs of all your applications (meaning it should
+probably be the unique name of the current application). You should set `first`
+to true for the first log in the operation, and `last` to true for the final log
+of the operation.
+
+```golang
+logger.Info("Started.", zapdriver.Operation("3g4d3g", "my-app", true, false))
+logger.Debug("Progressing.", zapdriver.Operation("3g4d3g", "my-app", false, false))
+logger.Info("Done.", zapdriver.Operation("3g4d3g", "my-app", false, true))
+```
+
+Instead of defining the "start" and "end" booleans, you can also use these three
+convenience functions:
+
+```golang
+OperationStart(id, producer string) zap.Field
+OperationCont(id, producer string) zap.Field
+OperationEnd(id, producer string) zap.Field
+```
+
+#### TraceContext
+
+You can add trace context information to your log lines to be picked up by
+Stackdriver.
+
+```golang
+TraceContext(trace string, spanId string, sampled bool, projectName string) []zap.Field
+```
+
+Like so:
+
+```golang
+logger.Error("Something happened!", zapdriver.TraceContext("105445aa7843bc8bf206b120001000", "0", true, "my-project-name")...)
+```
+
+### Pre-configured Stackdriver-optimized encoder
+
+The Stackdriver encoder maps all Zap log levels to the appropriate
+[Stackdriver-supported levels][levels]:
+
+> DEBUG     (100) Debug or trace information.
+>
+> INFO      (200) Routine information, such as ongoing status or performance.
+>
+> WARNING   (400) Warning events might cause problems.
+>
+> ERROR     (500) Error events are likely to cause problems.
+>
+> CRITICAL  (600) Critical events cause more severe problems or outages.
+>
+> ALERT     (700) A person must take an action immediately.
+>
+> EMERGENCY (800) One or more systems are unusable.
+
+[levels]: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
+
+It also sets some of the default keys to use [the right names][names], such as
+`timestamp`, `severity`, and `message`.
+
+[names]: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
+
+You can use this encoder if you want to build your Zap logger configuration
+manually:
+
+```golang
+zapdriver.NewProductionEncoderConfig()
+```
+
+For parity-sake, there's also `zapdriver.NewDevelopmentEncoderConfig()`, but it
+returns the exact same encoder right now.
+
+### Custom Stackdriver Zap core
+
+A custom Zap core is included in this package to support some special use-cases.
+
+First of all, if you use `zapdriver.NewProduction()` (or `NewDevelopment`) , you
+already have this core enabled, so everything _just works_ ™.
+
+There are two use-cases which require this core:
+
+1. If you use `zapdriver.Label("hello", "world")`, it will initially end up in
+   your log with the key `labels.hello` and value `world`. Now if you have two
+   labels, you could also have `labels.hi` with value `universe`. This works as-
+   is, but for this to be correctly parsed by Stackdriver as true "labels", you
+   need to use the Zapdriver core, so that both of these fields get rewritten,
+   to use the namespace `labels`, and use the keys `hello` and `hi` within that
+   namespace. This is done automatically.
+
+2. If you don't want to use `zapdriver.SourceLocation()` on every log call, you
+   can use this core for the source location to be automatically added to
+   each log entry.
+
+When building a logger, you can inject the Zapdriver core as follows:
+
+```golang
+config := &zap.Config{}
+logger, err := config.Build(zapdriver.WrapCore())
+```
+
+### Using Error Reporting
+
+To report errors using StackDriver's Error Reporting tool, a log line needs to follow a separate log format described in the [Error Reporting][errorreporting] documentation.
+
+[errorreporting]: https://cloud.google.com/error-reporting/docs/formatting-error-messages
+
+The simplest way to do this is by using `NewProductionWithCore`:
+
+```golang
+logger, err := zapdriver.NewProductionWithCore(zapdriver.WrapCore(
+  zapdriver.ReportAllErrors(true),
+  zapdriver.ServiceName("my service"),
+))
+```
+
+For parity-sake, there's also `zapdriver.NewDevelopmentWithCore()`
+
+If you are building a custom logger, you can use `WrapCore()` to configure the driver core:
+
+```golang
+config := &zap.Config{}
+logger, err := config.Build(zapdriver.WrapCore(
+  zapdriver.ReportAllErrors(true),
+  zapdriver.ServiceName("my service"),
+))
+```
+
+Configuring this way, every error log entry will be reported to Stackdriver's Error Reporting tool.
+
+#### Reporting errors manually
+
+If you do not want every error to be reported, you can attach `ErrorReport()` to log call manually:
+
+```golang
+logger.Error("An error to be reported!", zapdriver.ErrorReport(runtime.Caller(0)))
+// Or get Caller details
+pc, file, line, ok := runtime.Caller(0)
+// do other stuff... and log elsewhere
+logger.Error("Another error to be reported!", zapdriver.ErrorReport(pc, file, line, ok))
+```
+
+Please keep in mind that ErrorReport needs a ServiceContext attached to the log
+entry. If you did not configure this using `WrapCore`, error reports will
+get attached using service name as `unknown`. To prevent this from happeneing,
+either configure your core or attach service context before (or when) using
+the logger:
+
+```golang
+logger.Error(
+  "An error to be reported!",
+  zapdriver.ErrorReport(runtime.Caller(0)),
+  zapdriver.ServiceContext("my service"),
+)
+
+// Or permanently attach it to your logger
+logger = logger.With(zapdriver.ServiceContext("my service"))
+// and then use it
+logger.Error("An error to be reported!", zapdriver.ErrorReport(runtime.Caller(0)))
+```
diff --git a/vendor/github.com/blendle/zapdriver/config.go b/vendor/github.com/blendle/zapdriver/config.go
new file mode 100644
index 000000000..fe901ce3b
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/config.go
@@ -0,0 +1,55 @@
+package zapdriver
+
+import (
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// NewProductionEncoderConfig returns an opinionated EncoderConfig for
+// production environments.
+func NewProductionEncoderConfig() zapcore.EncoderConfig {
+	return encoderConfig
+}
+
+// NewDevelopmentEncoderConfig returns an opinionated EncoderConfig for
+// development environments.
+func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
+	return encoderConfig
+}
+
+// NewProductionConfig is a reasonable production logging configuration.
+// Logging is enabled at InfoLevel and above.
+//
+// It uses a JSON encoder, writes to standard error, and enables sampling.
+// Stacktraces are automatically included on logs of ErrorLevel and above.
+func NewProductionConfig() zap.Config {
+	return zap.Config{
+		Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
+		Development: false,
+		Sampling: &zap.SamplingConfig{
+			Initial:    100,
+			Thereafter: 100,
+		},
+		Encoding:         "json",
+		EncoderConfig:    NewProductionEncoderConfig(),
+		OutputPaths:      []string{"stderr"},
+		ErrorOutputPaths: []string{"stderr"},
+	}
+}
+
+// NewDevelopmentConfig is a reasonable development logging configuration.
+// Logging is enabled at DebugLevel and above.
+//
+// It enables development mode (which makes DPanicLevel logs panic), uses a
+// console encoder, writes to standard error, and disables sampling.
+// Stacktraces are automatically included on logs of WarnLevel and above.
+func NewDevelopmentConfig() zap.Config {
+	return zap.Config{
+		Level:            zap.NewAtomicLevelAt(zap.DebugLevel),
+		Development:      true,
+		Encoding:         "json",
+		EncoderConfig:    NewDevelopmentEncoderConfig(),
+		OutputPaths:      []string{"stderr"},
+		ErrorOutputPaths: []string{"stderr"},
+	}
+}
diff --git a/vendor/github.com/blendle/zapdriver/core.go b/vendor/github.com/blendle/zapdriver/core.go
new file mode 100644
index 000000000..bcb7660f7
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/core.go
@@ -0,0 +1,243 @@
+package zapdriver
+
+import (
+	"strings"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// driverConfig is used to configure core.
+type driverConfig struct {
+	// Report all logs with level error or above to stackdriver using
+	// `ErrorReport()` when set to true
+	ReportAllErrors bool
+
+	// ServiceName is added as `ServiceContext()` to all logs when set
+	ServiceName string
+}
+
+// Core is a zapdriver specific core wrapped around the default zap core. It
+// allows to merge all defined labels
+type core struct {
+	zapcore.Core
+
+	// permLabels is a collection of labels that have been added to the logger
+	// through the use of `With()`. These labels should never be cleared after
+	// logging a single entry, unlike `tempLabel`.
+	permLabels *labels
+
+	// tempLabels keeps a record of all the labels that need to be applied to the
+	// current log entry. Zap serializes log fields at different parts of the
+	// stack, one such location is when calling `core.With` and the other one is
+	// when calling `core.Write`. This makes it impossible to (for example) take
+	// all `labels.xxx` fields, and wrap them in the `labels` namespace in one go.
+	//
+	// Instead, we have to filter out these labels at both locations, and then add
+	// them back in the proper format right before we call `Write` on the original
+	// Zap core.
+	tempLabels *labels
+
+	// Configuration for the zapdriver core
+	config driverConfig
+}
+
+// zapdriver core option to report all logs with level error or above to stackdriver
+// using `ErrorReport()` when set to true
+func ReportAllErrors(report bool) func(*core) {
+	return func(c *core) {
+		c.config.ReportAllErrors = report
+	}
+}
+
+// zapdriver core option to add `ServiceContext()` to all logs with `name` as
+// service name
+func ServiceName(name string) func(*core) {
+	return func(c *core) {
+		c.config.ServiceName = name
+	}
+}
+
+// WrapCore returns a `zap.Option` that wraps the default core with the
+// zapdriver one.
+func WrapCore(options ...func(*core)) zap.Option {
+	return zap.WrapCore(func(c zapcore.Core) zapcore.Core {
+		newcore := &core{
+			Core:       c,
+			permLabels: newLabels(),
+			tempLabels: newLabels(),
+		}
+		for _, option := range options {
+			option(newcore)
+		}
+		return newcore
+	})
+}
+
+// With adds structured context to the Core.
+func (c *core) With(fields []zap.Field) zapcore.Core {
+	var lbls *labels
+	lbls, fields = c.extractLabels(fields)
+
+	lbls.mutex.RLock()
+	c.permLabels.mutex.Lock()
+	for k, v := range lbls.store {
+		c.permLabels.store[k] = v
+	}
+	c.permLabels.mutex.Unlock()
+	lbls.mutex.RUnlock()
+
+	return &core{
+		Core:       c.Core.With(fields),
+		permLabels: c.permLabels,
+		tempLabels: newLabels(),
+		config:     c.config,
+	}
+}
+
+// Check determines whether the supplied Entry should be logged (using the
+// embedded LevelEnabler and possibly some extra logic). If the entry
+// should be logged, the Core adds itself to the CheckedEntry and returns
+// the result.
+//
+// Callers must use Check before calling Write.
+func (c *core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
+	if c.Enabled(ent.Level) {
+		return ce.AddCore(ent, c)
+	}
+
+	return ce
+}
+
+func (c *core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
+	var lbls *labels
+	lbls, fields = c.extractLabels(fields)
+
+	lbls.mutex.RLock()
+	c.tempLabels.mutex.Lock()
+	for k, v := range lbls.store {
+		c.tempLabels.store[k] = v
+	}
+	c.tempLabels.mutex.Unlock()
+	lbls.mutex.RUnlock()
+
+	fields = append(fields, labelsField(c.allLabels()))
+	fields = c.withSourceLocation(ent, fields)
+	if c.config.ServiceName != "" {
+		fields = c.withServiceContext(c.config.ServiceName, fields)
+	}
+	if c.config.ReportAllErrors && zapcore.ErrorLevel.Enabled(ent.Level) {
+		fields = c.withErrorReport(ent, fields)
+		if c.config.ServiceName == "" {
+			// A service name was not set but error report needs it
+			// So attempt to add a generic service name
+			fields = c.withServiceContext("unknown", fields)
+		}
+	}
+
+	c.tempLabels.reset()
+
+	return c.Core.Write(ent, fields)
+}
+
+// Sync flushes buffered logs (if any).
+func (c *core) Sync() error {
+	return c.Core.Sync()
+}
+
+func (c *core) allLabels() *labels {
+	lbls := newLabels()
+
+	lbls.mutex.Lock()
+	c.permLabels.mutex.RLock()
+	for k, v := range c.permLabels.store {
+		lbls.store[k] = v
+	}
+	c.permLabels.mutex.RUnlock()
+
+	c.tempLabels.mutex.RLock()
+	for k, v := range c.tempLabels.store {
+		lbls.store[k] = v
+	}
+	c.tempLabels.mutex.RUnlock()
+	lbls.mutex.Unlock()
+
+	return lbls
+}
+
+func (c *core) extractLabels(fields []zapcore.Field) (*labels, []zapcore.Field) {
+	lbls := newLabels()
+	out := []zapcore.Field{}
+
+	lbls.mutex.Lock()
+	for i := range fields {
+		if !isLabelField(fields[i]) {
+			out = append(out, fields[i])
+			continue
+		}
+
+		lbls.store[strings.Replace(fields[i].Key, "labels.", "", 1)] = fields[i].String
+	}
+	lbls.mutex.Unlock()
+
+	return lbls, out
+}
+
+func (c *core) withLabels(fields []zapcore.Field) []zapcore.Field {
+	lbls := newLabels()
+	out := []zapcore.Field{}
+
+	lbls.mutex.Lock()
+	for i := range fields {
+		if isLabelField(fields[i]) {
+			lbls.store[strings.Replace(fields[i].Key, "labels.", "", 1)] = fields[i].String
+			continue
+		}
+
+		out = append(out, fields[i])
+	}
+	lbls.mutex.Unlock()
+
+	return append(out, labelsField(lbls))
+}
+
+func (c *core) withSourceLocation(ent zapcore.Entry, fields []zapcore.Field) []zapcore.Field {
+	// If the source location was manually set, don't overwrite it
+	for i := range fields {
+		if fields[i].Key == sourceKey {
+			return fields
+		}
+	}
+
+	if !ent.Caller.Defined {
+		return fields
+	}
+
+	return append(fields, SourceLocation(ent.Caller.PC, ent.Caller.File, ent.Caller.Line, true))
+}
+
+func (c *core) withServiceContext(name string, fields []zapcore.Field) []zapcore.Field {
+	// If the service context was manually set, don't overwrite it
+	for i := range fields {
+		if fields[i].Key == serviceContextKey {
+			return fields
+		}
+	}
+
+	return append(fields, ServiceContext(name))
+}
+
+func (c *core) withErrorReport(ent zapcore.Entry, fields []zapcore.Field) []zapcore.Field {
+	// If the error report was manually set, don't overwrite it
+	for i := range fields {
+		if fields[i].Key == contextKey {
+			return fields
+		}
+	}
+
+	if !ent.Caller.Defined {
+		return fields
+	}
+
+	return append(fields, ErrorReport(ent.Caller.PC, ent.Caller.File, ent.Caller.Line, true))
+}
diff --git a/vendor/github.com/blendle/zapdriver/encoder.go b/vendor/github.com/blendle/zapdriver/encoder.go
new file mode 100644
index 000000000..eec571fb8
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/encoder.go
@@ -0,0 +1,59 @@
+package zapdriver
+
+import (
+	"time"
+
+	"go.uber.org/zap/zapcore"
+)
+
+// logLevelSeverity maps the Zap log levels to the correct level names as
+// defined by Stackdriver.
+//
+// DEFAULT     (0) The log entry has no assigned severity level.
+// DEBUG     (100) Debug or trace information.
+// INFO      (200) Routine information, such as ongoing status or performance.
+// NOTICE    (300) Normal but significant events, such as start up, shut down, or a configuration change.
+// WARNING   (400) Warning events might cause problems.
+// ERROR     (500) Error events are likely to cause problems.
+// CRITICAL  (600) Critical events cause more severe problems or outages.
+// ALERT     (700) A person must take an action immediately.
+// EMERGENCY (800) One or more systems are unusable.
+//
+// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
+var logLevelSeverity = map[zapcore.Level]string{
+	zapcore.DebugLevel:  "DEBUG",
+	zapcore.InfoLevel:   "INFO",
+	zapcore.WarnLevel:   "WARNING",
+	zapcore.ErrorLevel:  "ERROR",
+	zapcore.DPanicLevel: "CRITICAL",
+	zapcore.PanicLevel:  "ALERT",
+	zapcore.FatalLevel:  "EMERGENCY",
+}
+
+// encoderConfig is the default encoder configuration, slightly tweaked to use
+// the correct fields for Stackdriver to parse them.
+var encoderConfig = zapcore.EncoderConfig{
+	TimeKey:        "timestamp",
+	LevelKey:       "severity",
+	NameKey:        "logger",
+	CallerKey:      "caller",
+	MessageKey:     "message",
+	StacktraceKey:  "stacktrace",
+	LineEnding:     zapcore.DefaultLineEnding,
+	EncodeLevel:    EncodeLevel,
+	EncodeTime:     RFC3339NanoTimeEncoder,
+	EncodeDuration: zapcore.SecondsDurationEncoder,
+	EncodeCaller:   zapcore.ShortCallerEncoder,
+}
+
+// EncodeLevel maps the internal Zap log level to the appropriate Stackdriver
+// level.
+func EncodeLevel(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
+	enc.AppendString(logLevelSeverity[l])
+}
+
+// RFC3339NanoTimeEncoder serializes a time.Time to an RFC3339Nano-formatted
+// string with nanoseconds precision.
+func RFC3339NanoTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+	enc.AppendString(t.Format(time.RFC3339Nano))
+}
diff --git a/vendor/github.com/blendle/zapdriver/go.mod b/vendor/github.com/blendle/zapdriver/go.mod
new file mode 100644
index 000000000..aecbd4f12
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/go.mod
@@ -0,0 +1,12 @@
+module github.com/blendle/zapdriver
+
+go 1.12
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pkg/errors v0.8.1 // indirect
+	github.com/stretchr/testify v1.3.0
+	go.uber.org/atomic v1.4.0 // indirect
+	go.uber.org/multierr v1.1.0 // indirect
+	go.uber.org/zap v1.10.0
+)
diff --git a/vendor/github.com/blendle/zapdriver/go.sum b/vendor/github.com/blendle/zapdriver/go.sum
new file mode 100644
index 000000000..237b5d2e2
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/go.sum
@@ -0,0 +1,16 @@
+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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
diff --git a/vendor/github.com/blendle/zapdriver/http.go b/vendor/github.com/blendle/zapdriver/http.go
new file mode 100644
index 000000000..441e58590
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/http.go
@@ -0,0 +1,160 @@
+package zapdriver
+
+// "Broker: Request timed out"
+// https://console.cloud.google.com/logs/viewer?project=bnl-blendle&minLogLevel=
+// 0&expandAll=false&timestamp=2018-05-23T22:21:56.142000000Z&customFacets=&limi
+// tCustomFacetWidth=true&dateRangeEnd=2018-05-23T22:21:52.545Z&interval=PT1H&re
+// source=container%2Fcluster_name%2Fblendle-2%2Fnamespace_id%2Fstream-
+// composition-analytic-events-
+// backfill&scrollTimestamp=2018-05-23T05:29:33.000000000Z&logName=projects
+// %2Fbnl-blendle%2Flogs%2Fstream-composition-analytic-events-
+// pipe-1&dateRangeUnbound=backwardInTime
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"strconv"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// HTTP adds the correct Stackdriver "HTTP" field.
+//
+// see: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest
+func HTTP(req *HTTPPayload) zap.Field {
+	return zap.Object("httpRequest", req)
+}
+
+// HTTPPayload is the complete payload that can be interpreted by
+// Stackdriver as a HTTP request.
+type HTTPPayload struct {
+	// The request method. Examples: "GET", "HEAD", "PUT", "POST".
+	RequestMethod string `json:"requestMethod"`
+
+	// The scheme (http, https), the host name, the path and the query portion of
+	// the URL that was requested.
+	//
+	// Example: "http://example.com/some/info?color=red".
+	RequestURL string `json:"requestUrl"`
+
+	// The size of the HTTP request message in bytes, including the request
+	// headers and the request body.
+	RequestSize string `json:"requestSize"`
+
+	// The response code indicating the status of response.
+	//
+	// Examples: 200, 404.
+	Status int `json:"status"`
+
+	// The size of the HTTP response message sent back to the client, in bytes,
+	// including the response headers and the response body.
+	ResponseSize string `json:"responseSize"`
+
+	// The user agent sent by the client.
+	//
+	// Example: "Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; Q312461; .NET CLR 1.0.3705)".
+	UserAgent string `json:"userAgent"`
+
+	// The IP address (IPv4 or IPv6) of the client that issued the HTTP request.
+	//
+	// Examples: "192.168.1.1", "FE80::0202:B3FF:FE1E:8329".
+	RemoteIP string `json:"remoteIp"`
+
+	// The IP address (IPv4 or IPv6) of the origin server that the request was
+	// sent to.
+	ServerIP string `json:"serverIp"`
+
+	// The referrer URL of the request, as defined in HTTP/1.1 Header Field
+	// Definitions.
+	Referer string `json:"referer"`
+
+	// The request processing latency on the server, from the time the request was
+	// received until the response was sent.
+	//
+	// A duration in seconds with up to nine fractional digits, terminated by 's'.
+	//
+	// Example: "3.5s".
+	Latency string `json:"latency"`
+
+	// Whether or not a cache lookup was attempted.
+	CacheLookup bool `json:"cacheLookup"`
+
+	// Whether or not an entity was served from cache (with or without
+	// validation).
+	CacheHit bool `json:"cacheHit"`
+
+	// Whether or not the response was validated with the origin server before
+	// being served from cache. This field is only meaningful if cacheHit is True.
+	CacheValidatedWithOriginServer bool `json:"cacheValidatedWithOriginServer"`
+
+	// The number of HTTP response bytes inserted into cache. Set only when a
+	// cache fill was attempted.
+	CacheFillBytes string `json:"cacheFillBytes"`
+
+	// Protocol used for the request.
+	//
+	// Examples: "HTTP/1.1", "HTTP/2", "websocket"
+	Protocol string `json:"protocol"`
+}
+
+// NewHTTP returns a new HTTPPayload struct, based on the passed
+// in http.Request and http.Response objects.
+func NewHTTP(req *http.Request, res *http.Response) *HTTPPayload {
+	if req == nil {
+		req = &http.Request{}
+	}
+
+	if res == nil {
+		res = &http.Response{}
+	}
+
+	sdreq := &HTTPPayload{
+		RequestMethod: req.Method,
+		Status:        res.StatusCode,
+		UserAgent:     req.UserAgent(),
+		RemoteIP:      req.RemoteAddr,
+		Referer:       req.Referer(),
+		Protocol:      req.Proto,
+	}
+
+	if req.URL != nil {
+		sdreq.RequestURL = req.URL.String()
+	}
+
+	buf := &bytes.Buffer{}
+	if req.Body != nil {
+		n, _ := io.Copy(buf, req.Body) // nolint: gas
+		sdreq.RequestSize = strconv.FormatInt(n, 10)
+	}
+
+	if res.Body != nil {
+		buf.Reset()
+		n, _ := io.Copy(buf, res.Body) // nolint: gas
+		sdreq.ResponseSize = strconv.FormatInt(n, 10)
+	}
+
+	return sdreq
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (req HTTPPayload) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("requestMethod", req.RequestMethod)
+	enc.AddString("requestUrl", req.RequestURL)
+	enc.AddString("requestSize", req.RequestSize)
+	enc.AddInt("status", req.Status)
+	enc.AddString("responseSize", req.ResponseSize)
+	enc.AddString("userAgent", req.UserAgent)
+	enc.AddString("remoteIp", req.RemoteIP)
+	enc.AddString("serverIp", req.ServerIP)
+	enc.AddString("referer", req.Referer)
+	enc.AddString("latency", req.Latency)
+	enc.AddBool("cacheLookup", req.CacheLookup)
+	enc.AddBool("cacheHit", req.CacheHit)
+	enc.AddBool("cacheValidatedWithOriginServer", req.CacheValidatedWithOriginServer)
+	enc.AddString("cacheFillBytes", req.CacheFillBytes)
+	enc.AddString("protocol", req.Protocol)
+
+	return nil
+}
diff --git a/vendor/github.com/blendle/zapdriver/label.go b/vendor/github.com/blendle/zapdriver/label.go
new file mode 100644
index 000000000..723864209
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/label.go
@@ -0,0 +1,77 @@
+package zapdriver
+
+import (
+	"strings"
+	"sync"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const labelsKey = "logging.googleapis.com/labels"
+
+// Label adds an optional label to the payload.
+//
+// Labels are a set of user-defined (key, value) data that provides additional
+// information about the log entry.
+//
+// Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }.
+func Label(key, value string) zap.Field {
+	return zap.String("labels."+key, value)
+}
+
+// Labels takes Zap fields, filters the ones that have their key start with the
+// string `labels.` and their value type set to StringType. It then wraps those
+// key/value pairs in a top-level `labels` namespace.
+func Labels(fields ...zap.Field) zap.Field {
+	lbls := newLabels()
+
+	lbls.mutex.Lock()
+	for i := range fields {
+		if isLabelField(fields[i]) {
+			lbls.store[strings.Replace(fields[i].Key, "labels.", "", 1)] = fields[i].String
+		}
+	}
+	lbls.mutex.Unlock()
+
+	return labelsField(lbls)
+}
+
+func isLabelField(field zap.Field) bool {
+	return strings.HasPrefix(field.Key, "labels.") && field.Type == zapcore.StringType
+}
+
+func labelsField(l *labels) zap.Field {
+	return zap.Object(labelsKey, l)
+}
+
+type labels struct {
+	store map[string]string
+	mutex *sync.RWMutex
+}
+
+func newLabels() *labels {
+	return &labels{store: map[string]string{}, mutex: &sync.RWMutex{}}
+}
+
+func (l *labels) Add(key, value string) {
+	l.mutex.Lock()
+	l.store[key] = value
+	l.mutex.Unlock()
+}
+
+func (l *labels) reset() {
+	l.mutex.Lock()
+	l.store = map[string]string{}
+	l.mutex.Unlock()
+}
+
+func (l labels) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	l.mutex.RLock()
+	for k, v := range l.store {
+		enc.AddString(k, v)
+	}
+	l.mutex.RUnlock()
+
+	return nil
+}
diff --git a/vendor/github.com/blendle/zapdriver/logger.go b/vendor/github.com/blendle/zapdriver/logger.go
new file mode 100644
index 000000000..59c10993e
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/logger.go
@@ -0,0 +1,39 @@
+package zapdriver
+
+import (
+	"go.uber.org/zap"
+)
+
+// NewProduction builds a sensible production Logger that writes InfoLevel and
+// above logs to standard error as JSON.
+//
+// It's a shortcut for NewProductionConfig().Build(...Option).
+func NewProduction(options ...zap.Option) (*zap.Logger, error) {
+	options = append(options, WrapCore())
+
+	return NewProductionConfig().Build(options...)
+}
+
+// NewProductionWithCore is same as NewProduction but accepts a custom configured core
+func NewProductionWithCore(core zap.Option, options ...zap.Option) (*zap.Logger, error) {
+	options = append(options, core)
+
+	return NewProductionConfig().Build(options...)
+}
+
+// NewDevelopment builds a development Logger that writes DebugLevel and above
+// logs to standard error in a human-friendly format.
+//
+// It's a shortcut for NewDevelopmentConfig().Build(...Option).
+func NewDevelopment(options ...zap.Option) (*zap.Logger, error) {
+	options = append(options, WrapCore())
+
+	return NewDevelopmentConfig().Build(options...)
+}
+
+// NewDevelopmentWithCore is same as NewDevelopment but accepts a custom configured core
+func NewDevelopmentWithCore(core zap.Option, options ...zap.Option) (*zap.Logger, error) {
+	options = append(options, core)
+
+	return NewDevelopmentConfig().Build(options...)
+}
diff --git a/vendor/github.com/blendle/zapdriver/operation.go b/vendor/github.com/blendle/zapdriver/operation.go
new file mode 100644
index 000000000..e8b862f79
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/operation.go
@@ -0,0 +1,72 @@
+package zapdriver
+
+import (
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const operationKey = "logging.googleapis.com/operation"
+
+// Operation adds the correct Stackdriver "operation" field.
+//
+// Additional information about a potentially long-running operation with which
+// a log entry is associated.
+//
+// see: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntryOperation
+func Operation(id, producer string, first, last bool) zap.Field {
+	op := &operation{
+		ID:       id,
+		Producer: producer,
+		First:    first,
+		Last:     last,
+	}
+
+	return zap.Object(operationKey, op)
+}
+
+// OperationStart is a convenience function for `Operation`. It should be called
+// for the first operation log.
+func OperationStart(id, producer string) zap.Field {
+	return Operation(id, producer, true, false)
+}
+
+// OperationCont is a convenience function for `Operation`. It should be called
+// for any non-start/end operation log.
+func OperationCont(id, producer string) zap.Field {
+	return Operation(id, producer, false, false)
+}
+
+// OperationEnd is a convenience function for `Operation`. It should be called
+// for the last operation log.
+func OperationEnd(id, producer string) zap.Field {
+	return Operation(id, producer, false, true)
+}
+
+// operation is the complete payload that can be interpreted by Stackdriver as
+// an operation.
+type operation struct {
+	// Optional. An arbitrary operation identifier. Log entries with the same
+	// identifier are assumed to be part of the same operation.
+	ID string `json:"id"`
+
+	// Optional. An arbitrary producer identifier. The combination of id and
+	// producer must be globally unique. Examples for producer:
+	// "MyDivision.MyBigCompany.com", "github.com/MyProject/MyApplication".
+	Producer string `json:"producer"`
+
+	// Optional. Set this to True if this is the first log entry in the operation.
+	First bool `json:"first"`
+
+	// Optional. Set this to True if this is the last log entry in the operation.
+	Last bool `json:"last"`
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (op operation) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("id", op.ID)
+	enc.AddString("producer", op.Producer)
+	enc.AddBool("first", op.First)
+	enc.AddBool("last", op.Last)
+
+	return nil
+}
diff --git a/vendor/github.com/blendle/zapdriver/report.go b/vendor/github.com/blendle/zapdriver/report.go
new file mode 100644
index 000000000..1bb8ac761
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/report.go
@@ -0,0 +1,70 @@
+package zapdriver
+
+import (
+	"runtime"
+	"strconv"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const contextKey = "context"
+
+// ErrorReport adds the correct Stackdriver "context" field for getting the log line
+// reported as error.
+//
+// see: https://cloud.google.com/error-reporting/docs/formatting-error-messages
+func ErrorReport(pc uintptr, file string, line int, ok bool) zap.Field {
+	return zap.Object(contextKey, newReportContext(pc, file, line, ok))
+}
+
+// reportLocation is the source code location information associated with the log entry
+// for the purpose of reporting an error,
+// if any.
+type reportLocation struct {
+	File     string `json:"filePath"`
+	Line     string `json:"lineNumber"`
+	Function string `json:"functionName"`
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (location reportLocation) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("filePath", location.File)
+	enc.AddString("lineNumber", location.Line)
+	enc.AddString("functionName", location.Function)
+
+	return nil
+}
+
+// reportContext is the context information attached to a log for reporting errors
+type reportContext struct {
+	ReportLocation reportLocation `json:"reportLocation"`
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (context reportContext) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddObject("reportLocation", context.ReportLocation)
+
+	return nil
+}
+
+func newReportContext(pc uintptr, file string, line int, ok bool) *reportContext {
+	if !ok {
+		return nil
+	}
+
+	var function string
+	if fn := runtime.FuncForPC(pc); fn != nil {
+		function = fn.Name()
+	}
+
+	context := &reportContext{
+		ReportLocation: reportLocation{
+			File:     file,
+			Line:     strconv.Itoa(line),
+			Function: function,
+		},
+	}
+
+	return context
+}
diff --git a/vendor/github.com/blendle/zapdriver/service.go b/vendor/github.com/blendle/zapdriver/service.go
new file mode 100644
index 000000000..134ff3634
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/service.go
@@ -0,0 +1,36 @@
+package zapdriver
+
+import (
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const serviceContextKey = "serviceContext"
+
+// ServiceContext adds the correct service information adding the log line
+// It is a required field if an error needs to be reported.
+//
+// see: https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext
+// see: https://cloud.google.com/error-reporting/docs/formatting-error-messages
+func ServiceContext(name string) zap.Field {
+	return zap.Object(serviceContextKey, newServiceContext(name))
+}
+
+// serviceContext describes a running service that sends errors.
+// Currently it only describes a service name.
+type serviceContext struct {
+	Name string `json:"service"`
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (service_context serviceContext) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("service", service_context.Name)
+
+	return nil
+}
+
+func newServiceContext(name string) *serviceContext {
+	return &serviceContext{
+		Name: name,
+	}
+}
diff --git a/vendor/github.com/blendle/zapdriver/source.go b/vendor/github.com/blendle/zapdriver/source.go
new file mode 100644
index 000000000..ff5ecf765
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/source.go
@@ -0,0 +1,66 @@
+package zapdriver
+
+import (
+	"runtime"
+	"strconv"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const sourceKey = "logging.googleapis.com/sourceLocation"
+
+// SourceLocation adds the correct Stackdriver "SourceLocation" field.
+//
+// see: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation
+func SourceLocation(pc uintptr, file string, line int, ok bool) zap.Field {
+	return zap.Object(sourceKey, newSource(pc, file, line, ok))
+}
+
+// source is the source code location information associated with the log entry,
+// if any.
+type source struct {
+	// Optional. Source file name. Depending on the runtime environment, this
+	// might be a simple name or a fully-qualified name.
+	File string `json:"file"`
+
+	// Optional. Line within the source file. 1-based; 0 indicates no line number
+	// available.
+	Line string `json:"line"`
+
+	// Optional. Human-readable name of the function or method being invoked, with
+	// optional context such as the class or package name. This information may be
+	// used in contexts such as the logs viewer, where a file and line number are
+	// less meaningful.
+	//
+	// The format should be dir/package.func.
+	Function string `json:"function"`
+}
+
+// MarshalLogObject implements zapcore.ObjectMarshaller interface.
+func (source source) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("file", source.File)
+	enc.AddString("line", source.Line)
+	enc.AddString("function", source.Function)
+
+	return nil
+}
+
+func newSource(pc uintptr, file string, line int, ok bool) *source {
+	if !ok {
+		return nil
+	}
+
+	var function string
+	if fn := runtime.FuncForPC(pc); fn != nil {
+		function = fn.Name()
+	}
+
+	source := &source{
+		File:     file,
+		Line:     strconv.Itoa(line),
+		Function: function,
+	}
+
+	return source
+}
diff --git a/vendor/github.com/blendle/zapdriver/trace.go b/vendor/github.com/blendle/zapdriver/trace.go
new file mode 100644
index 000000000..7e1e2e8ab
--- /dev/null
+++ b/vendor/github.com/blendle/zapdriver/trace.go
@@ -0,0 +1,24 @@
+package zapdriver
+
+import (
+	"fmt"
+
+	"go.uber.org/zap"
+)
+
+const (
+	traceKey        = "logging.googleapis.com/trace"
+	spanKey         = "logging.googleapis.com/spanId"
+	traceSampledKey = "logging.googleapis.com/trace_sampled"
+)
+
+// TraceContext adds the correct Stackdriver "trace", "span", "trace_sampled fields
+//
+// see: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
+func TraceContext(trace string, spanId string, sampled bool, projectName string) []zap.Field {
+	return []zap.Field{
+		zap.String(traceKey, fmt.Sprintf("projects/%s/traces/%s", projectName, trace)),
+		zap.String(spanKey, spanId),
+		zap.Bool(traceSampledKey, sampled),
+	}
+}
diff --git a/vendor/knative.dev/hack/codegen-library.sh b/vendor/knative.dev/hack/codegen-library.sh
new file mode 100644
index 000000000..cb9c282b2
--- /dev/null
+++ b/vendor/knative.dev/hack/codegen-library.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+# Copyright 2020 The Knative Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Setup the env for doing Knative style codegen.
+
+kn_hack_library=${kn_hack_library:-"$(dirname $0)/../vendor/knative.dev/hack/library.sh"}
+
+if [[ -f "$kn_hack_library" ]]; then
+  source $kn_hack_library
+else
+  echo "this file is intended to be imported from a golang project that vendors knative.dev/hack"
+  exit
+fi
+
+export GOPATH=$(go_mod_gopath_hack)
+export MODULE_NAME=$(go_mod_module_name)
+export CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${REPO_ROOT_DIR}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}
+export KNATIVE_CODEGEN_PKG=${KNATIVE_CODEGEN_PKG:-$(cd ${REPO_ROOT_DIR}; ls -d -1 ./vendor/knative.dev/pkg 2>/dev/null || echo ../pkg)}
+
+chmod +x ${CODEGEN_PKG}/generate-groups.sh
+chmod +x ${KNATIVE_CODEGEN_PKG}/hack/generate-knative.sh
diff --git a/vendor/knative.dev/hack/library.sh b/vendor/knative.dev/hack/library.sh
index 9b90b0025..8ed7182d9 100644
--- a/vendor/knative.dev/hack/library.sh
+++ b/vendor/knative.dev/hack/library.sh
@@ -98,6 +98,37 @@ function function_exists() {
   [[ "$(type -t $1)" == "function" ]]
 }
 
+# GitHub Actions aware output grouping.
+function group() {
+  # End the group is there is already a group.
+  if [ -z ${__GROUP_TRACKER+x} ]; then
+    export __GROUP_TRACKER="grouping"
+    trap end_group EXIT
+  else
+    end_group
+  fi
+  # Start a new group.
+  start_group "$@"
+}
+
+# GitHub Actions aware output grouping.
+function start_group() {
+  if [[ -n ${GITHUB_WORKFLOW:-} ]]; then
+    echo "::group::$@"
+    trap end_group EXIT
+  else
+    echo "--- $@"
+  fi
+}
+
+# GitHub Actions aware end of output grouping.
+function end_group() {
+  if [[ -n ${GITHUB_WORKFLOW:-} ]]; then
+    echo "::endgroup::"
+  fi
+}
+
+
 # Waits until the given object doesn't exist.
 # Parameters: $1 - the kind of the object.
 #             $2 - object's name.
@@ -528,7 +559,7 @@ function go_update_deps() {
   done
 
   if [[ $UPGRADE == 1 ]]; then
-    echo "--- Upgrading to ${VERSION}"
+    group "Upgrading to ${VERSION}"
     # From shell parameter expansion:
     # ${parameter:+word}
     # If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.
@@ -548,7 +579,7 @@ function go_update_deps() {
     fi
   fi
 
-  echo "--- Go mod tidy and vendor"
+  group "Go mod tidy and vendor"
 
   # Prune modules.
   local orig_pipefail_opt=$(shopt -p -o pipefail)
@@ -557,7 +588,7 @@ function go_update_deps() {
   go mod vendor 2>&1 |  grep -v "ignoring symlink" || true
   eval "$orig_pipefail_opt"
 
-  echo "--- Removing unwanted vendor files"
+  group "Removing unwanted vendor files"
 
   # Remove unwanted vendor files
   find vendor/ \( -name "OWNERS" \
@@ -568,13 +599,32 @@ function go_update_deps() {
 
   export GOFLAGS=-mod=vendor
 
-  echo "--- Updating licenses"
+  group "Updating licenses"
   update_licenses third_party/VENDOR-LICENSE "./..."
 
-  echo "--- Removing broken symlinks"
+  group "Removing broken symlinks"
   remove_broken_symlinks ./vendor
 }
 
+# Return the go module name of the current module.
+# Intended to be used like:
+#   export MODULE_NAME=$(go_mod_module_name)
+function go_mod_module_name() {
+  go mod graph | cut -d' ' -f 1 | grep -v '@' | head -1
+}
+
+# Return a GOPATH to a temp directory. Works around the out-of-GOPATH issues
+# for k8s client gen mixed with go mod.
+# Intended to be used like:
+#   export GOPATH=$(go_mod_gopath_hack)
+function go_mod_gopath_hack() {
+  local TMP_DIR="$(mktemp -d)"
+  local TMP_REPO_PATH="${TMP_DIR}/src/$(go_mod_module_name)"
+  mkdir -p "$(dirname "${TMP_REPO_PATH}")" && ln -s "${REPO_ROOT_DIR}" "${TMP_REPO_PATH}"
+
+  echo "${TMP_DIR}"
+}
+
 # Run kntest tool, error out and ask users to install it if it's not currently installed.
 # Parameters: $1..$n - parameters passed to the tool.
 function run_kntest() {
diff --git a/vendor/knative.dev/hack/microbenchmarks.sh b/vendor/knative.dev/hack/microbenchmarks.sh
new file mode 100644
index 000000000..ee375842c
--- /dev/null
+++ b/vendor/knative.dev/hack/microbenchmarks.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+
+# Copyright 2019 The Knative Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -count argument when running go test
+readonly BENCH_RUN_COUNT=${BENCH_RUN_COUNT:-5}
+
+# -benchtime argument when running go test
+readonly BENCH_RUN_TIME=${BENCH_RUN_TIME:-5s}
+
+function microbenchmarks_run() {
+  if [ "$1" != "" ]; then
+    OUTPUT_FILE="$1"
+  else
+    OUTPUT_FILE="${ARTIFACTS:-$(mktemp -d)}/bench-result.txt"
+  fi
+
+  # Run all microbenchmarks
+  go clean
+  go test -bench=. -benchtime=$BENCH_RUN_TIME -count=$BENCH_RUN_COUNT -benchmem -run="^$" -v ./...   >> "$OUTPUT_FILE" || exit
+}
+
+# To run microbenchmarks on your machine and compare your revision with upstream/master:
+#
+# git fetch upstream
+# source microbenchmarks.sh
+# microbenchmarks_run_and_compare upstream/master
+#
+# NOTE: Hypothetically we should run these microbenchmarks on a machine running only a kernel and a shell,
+# but this might be sometimes hard to achieve unless you have a spare computer to play with.
+# When running microbenchmarks on your dev machine, make sure you have the bare minimum applications running on your laptop,
+# otherwise you'll get a variance too high.
+# Close Slack, the browser, the IDE, stop Docker, stop Netflix and grab a coffee while waiting for the results.
+# Depending on the benchmark complexity/reproducibility, if the variance is too high, repeat the benchmarks.
+function microbenchmarks_run_and_compare() {
+  if [ "$1" == "" ] || [ $# -gt 1 ]; then
+    echo "Error: Expecting an argument" >&2
+    echo "usage: $(basename $0) revision_to_compare" >&2
+    exit 1
+  fi
+
+  if [ "$2" == "" ]; then
+    OUTPUT_DIR=${ARTIFACTS:-$(mktemp -d)}
+  else
+    OUTPUT_DIR="$2"
+  fi
+
+  mkdir -p "$OUTPUT_DIR"
+
+  # Benchstat is required to compare the bench results
+  GO111MODULE=off go get golang.org/x/perf/cmd/benchstat
+
+  # Revision to use to compare with actual
+  REVISION="$1"
+
+  echo "--- Outputs will be at $OUTPUT_DIR"
+
+  # Run this revision benchmarks
+  microbenchmarks_run "$OUTPUT_DIR/new.txt"
+
+  echo "--- This revision results:"
+  cat "$OUTPUT_DIR/new.txt"
+
+  # Run other revision benchmarks
+  git checkout "$REVISION"
+  microbenchmarks_run "$OUTPUT_DIR/old.txt"
+
+  echo "--- $REVISION results:"
+  cat "$OUTPUT_DIR/old.txt"
+
+  # Print results in console
+  echo "--- Benchstat:"
+  benchstat "$OUTPUT_DIR/old.txt" "$OUTPUT_DIR/new.txt"
+
+  # Generate html results
+  benchstat -html "$OUTPUT_DIR/old.txt" "$OUTPUT_DIR/new.txt" >> "$OUTPUT_DIR/results.html"
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d2b9ad606..8bd4fdea8 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -79,6 +79,9 @@ github.com/beorn7/perks/quantile
 # github.com/blang/semver/v4 v4.0.0
 ## explicit
 github.com/blang/semver/v4
+# github.com/blendle/zapdriver v1.3.1
+## explicit
+github.com/blendle/zapdriver
 # github.com/census-instrumentation/opencensus-proto v0.3.0
 ## explicit
 github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1
@@ -955,7 +958,7 @@ k8s.io/utils/buffer
 k8s.io/utils/integer
 k8s.io/utils/pointer
 k8s.io/utils/trace
-# knative.dev/hack v0.0.0-20201120192952-353db687ec5b
+# knative.dev/hack v0.0.0-20201201234937-fddbf732e450
 ## explicit
 knative.dev/hack
 # sigs.k8s.io/structured-merge-diff/v3 v3.0.1-0.20200706213357-43c19bbb7fba
diff --git a/webhook/psbinding/index.go b/webhook/psbinding/index.go
index 2ad47b10e..1a4dd49cc 100644
--- a/webhook/psbinding/index.go
+++ b/webhook/psbinding/index.go
@@ -7,7 +7,7 @@ You may obtain a copy of the License at
 
     http://www.apache.org/licenses/LICENSE-2.0
 
-Unless requ ired by applicable law or agreed to in writing, software
+Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
@@ -16,7 +16,11 @@ limitations under the License.
 
 package psbinding
 
-import "k8s.io/apimachinery/pkg/labels"
+import (
+	"sync"
+
+	"k8s.io/apimachinery/pkg/labels"
+)
 
 // exactKey is the type for keys that match exactly.
 type exactKey struct {
@@ -28,17 +32,16 @@ type exactKey struct {
 
 // exactMatcher is our reverse index from subjects to the Bindings that apply to
 // them.
-type exactMatcher map[exactKey]Bindable
+type exactMatcher map[exactKey][]Bindable
 
-// Add writes a key into the reverse index.
-func (em exactMatcher) Add(key exactKey, b Bindable) {
-	em[key] = b
+// add associates a Binding with a key in the reverse index.
+func (em exactMatcher) add(key exactKey, b Bindable) {
+	em[key] = append(em[key], b)
 }
 
-// Get fetches the key from the reverse index, if present.
-func (em exactMatcher) Get(key exactKey) (bindable Bindable, present bool) {
-	b, ok := em[key]
-	return b, ok
+// get fetches the key from the reverse index, if present.
+func (em exactMatcher) get(key exactKey) []Bindable {
+	return em[key]
 }
 
 // inexactKey is the type for keys that match inexactly (via selector)
@@ -58,8 +61,8 @@ type pair struct {
 // them.
 type inexactMatcher map[inexactKey][]pair
 
-// Add writes a key into the reverse index.
-func (im inexactMatcher) Add(key inexactKey, selector labels.Selector, b Bindable) {
+// add writes a key into the reverse index.
+func (im inexactMatcher) add(key inexactKey, selector labels.Selector, b Bindable) {
 	pl := im[key]
 	pl = append(pl, pair{
 		selector: selector,
@@ -68,14 +71,78 @@ func (im inexactMatcher) Add(key inexactKey, selector labels.Selector, b Bindabl
 	im[key] = pl
 }
 
-// Get fetches the key from the reverse index, if present.
-func (im inexactMatcher) Get(key inexactKey, ls labels.Set) (bindable Bindable, present bool) {
-	// Iterate over the list of pairs matched for this GK + namespace and return the first
-	// Bindable that matches our selector.
+// get fetches the key from the reverse index, if present.
+func (im inexactMatcher) get(key inexactKey, ls labels.Set) []Bindable {
+	found := []Bindable{}
+	// Iterate over the list of pairs matched for this GK + namespace and collect the
+	// Bindables that matches our selector.
 	for _, p := range im[key] {
 		if p.selector.Matches(ls) {
-			return p.sb, true
+			found = append(found, p.sb)
 		}
 	}
-	return nil, false
+	return found
+}
+
+// index is a collection of Bindables indexed by their subject resources.
+type index struct {
+	// lock protects access to exact and inexact
+	lock    sync.RWMutex
+	exact   exactMatcher
+	inexact inexactMatcher
+}
+
+// indexBuilder allows an index to be built atomically
+type indexBuilder struct {
+	exact   exactMatcher
+	inexact inexactMatcher
+}
+
+// newIndexBuilder constructs a new IndexBuilder.
+func newIndexBuilder() *indexBuilder {
+	return &indexBuilder{
+		exact:   make(exactMatcher),
+		inexact: make(inexactMatcher),
+	}
+}
+
+// associate associates a resource with a given exact key with a given Bindable.
+func (ib *indexBuilder) associate(key exactKey, fb Bindable) *indexBuilder {
+	ib.exact.add(key, fb)
+	return ib
+}
+
+// associateSelection associates resources with the given inexact key and labels matching the given selector with a given Bindable.
+func (ib *indexBuilder) associateSelection(inexactKey inexactKey, selector labels.Selector, fb Bindable) *indexBuilder {
+	ib.inexact.add(inexactKey, selector, fb)
+	return ib
+}
+
+// build sets the given index to the built value.
+func (ib *indexBuilder) build(index *index) {
+	index.setIndex(ib.exact, ib.inexact)
+}
+
+// setIndex replaces the index atomically with the given matchers.
+func (idx *index) setIndex(exact exactMatcher, inexact inexactMatcher) {
+	idx.lock.Lock()
+	defer idx.lock.Unlock()
+	idx.exact = exact
+	idx.inexact = inexact
+}
+
+// lookUp returns the Bindables associated with a resource with the given group, kind, namespace, name, and labels.
+func (idx *index) lookUp(key exactKey, labels labels.Set) []Bindable {
+	idx.lock.RLock()
+	defer idx.lock.RUnlock()
+
+	exactMatches := idx.exact.get(key)
+
+	inexactMatches := idx.inexact.get(inexactKey{
+		Group:     key.Group,
+		Kind:      key.Kind,
+		Namespace: key.Namespace,
+	}, labels)
+
+	return append(exactMatches, inexactMatches...)
 }
diff --git a/webhook/psbinding/psbinding.go b/webhook/psbinding/psbinding.go
index 741859058..5a3a6b258 100644
--- a/webhook/psbinding/psbinding.go
+++ b/webhook/psbinding/psbinding.go
@@ -23,7 +23,6 @@ import (
 	"fmt"
 	"sort"
 	"strings"
-	"sync"
 
 	"github.com/markbates/inflect"
 	"go.uber.org/zap"
@@ -117,10 +116,7 @@ type Reconciler struct {
 
 	selector metav1.LabelSelector
 
-	// lock protects access to exact and inexact
-	lock    sync.RWMutex
-	exact   exactMatcher
-	inexact inexactMatcher
+	index index
 }
 
 var _ controller.Reconciler = (*Reconciler)(nil)
@@ -197,56 +193,47 @@ func (ac *Reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionR
 		return webhook.MakeErrorStatus("unable to decode object: %v", err)
 	}
 
-	// Look up the Bindable for this resource.
-	fb := func() Bindable {
-		ac.lock.RLock()
-		defer ac.lock.RUnlock()
-
-		// Always try to find an exact match first.
-		if sb, ok := ac.exact.Get(exactKey{
-			Group:     request.Kind.Group,
-			Kind:      request.Kind.Kind,
-			Namespace: request.Namespace,
-			Name:      orig.Name,
-		}); ok {
-			return sb
-		}
-
-		// Next look for inexact matches.
-		if sb, ok := ac.inexact.Get(inexactKey{
-			Group:     request.Kind.Group,
-			Kind:      request.Kind.Kind,
-			Namespace: request.Namespace,
-		}, labels.Set(orig.Labels)); ok {
-			return sb
-		}
-		return nil
-	}()
-	if fb == nil {
+	// Look up the Bindables for this resource.
+	fbs := ac.index.lookUp(exactKey{
+		Group:     request.Kind.Group,
+		Kind:      request.Kind.Kind,
+		Namespace: request.Namespace,
+		Name:      orig.Name},
+		labels.Set(orig.Labels))
+	if len(fbs) == 0 {
 		// This doesn't apply!
 		return &admissionv1.AdmissionResponse{Allowed: true}
 	}
 
-	// Callback into the user's code to setup the context with additional
-	// information needed to perform the mutation.
-	if ac.WithContext != nil {
-		var err error
-		ctx, err = ac.WithContext(ctx, fb)
-		if err != nil {
-			return webhook.MakeErrorStatus("unable to setup binding context: %v", err)
+	// Copy the subject state.
+	mutated := orig.DeepCopy()
+
+	// Apply the Bindables to the copy of the subject state. If conflicts occur, for example because multiple Bindables
+	// make incompatible changes, the reconciler will attempt to correct the state later.
+	for _, fb := range fbs {
+		var bindingContext context.Context
+		// Callback into the user's code to setup the context with additional
+		// information needed to perform the mutation.
+		if ac.WithContext != nil {
+			var err error
+			bindingContext, err = ac.WithContext(ctx, fb)
+			if err != nil {
+				return webhook.MakeErrorStatus("unable to setup binding context: %v", err)
+			}
+		} else {
+			bindingContext = ctx
 		}
-	}
 
-	// Mutate a copy according to the deletion state of the Bindable.
-	delta := orig.DeepCopy()
-	if fb.GetDeletionTimestamp() != nil {
-		fb.Undo(ctx, delta)
-	} else {
-		fb.Do(ctx, delta)
+		// Mutate the copy of the subject state according to the deletion state of the Bindable.
+		if fb.GetDeletionTimestamp() != nil {
+			fb.Undo(bindingContext, mutated)
+		} else {
+			fb.Do(bindingContext, mutated)
+		}
 	}
 
 	// Synthesize a patch from the changes and return it in our AdmissionResponse
-	patchBytes, err := duck.CreateBytePatch(orig, delta)
+	patchBytes, err := duck.CreateBytePatch(orig, mutated)
 	if err != nil {
 		return webhook.MakeErrorStatus("unable to create patch with binding: %v", err)
 	}
@@ -270,8 +257,8 @@ func (ac *Reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byt
 	if err != nil {
 		return err
 	}
-	exact := make(exactMatcher, len(fbs))
-	inexact := make(inexactMatcher, len(fbs))
+
+	ib := newIndexBuilder()
 	for _, fb := range fbs {
 		ref := fb.GetSubject()
 		gv, err := schema.ParseGroupVersion(ref.APIVersion)
@@ -290,32 +277,27 @@ func (ac *Reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byt
 		gks[gk] = set
 
 		if ref.Name != "" {
-			exact.Add(exactKey{
+			ib.associate(exactKey{
 				Group:     gk.Group,
 				Kind:      gk.Kind,
 				Namespace: ref.Namespace,
-				Name:      ref.Name,
-			}, fb)
+				Name:      ref.Name},
+				fb)
 		} else {
 			selector, err := metav1.LabelSelectorAsSelector(ref.Selector)
 			if err != nil {
 				return err
 			}
-			inexact.Add(inexactKey{
+			ib.associateSelection(inexactKey{
 				Group:     gk.Group,
 				Kind:      gk.Kind,
-				Namespace: ref.Namespace,
-			}, selector, fb)
+				Namespace: ref.Namespace},
+				selector, fb)
 		}
 	}
 
-	// Update our indices
-	func() {
-		ac.lock.Lock()
-		defer ac.lock.Unlock()
-		ac.exact = exact
-		ac.inexact = inexact
-	}()
+	// Update the index.
+	ib.build(&ac.index)
 
 	// After we've updated our indices, bail out unless we are the leader.
 	// Only the leader should be mutating the webhook.


Test patch:
diff --git a/configmap/informed_watcher_test.go b/configmap/informed_watcher_test.go
index 801825226..fa2057eef 100644
--- a/configmap/informed_watcher_test.go
+++ b/configmap/informed_watcher_test.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"sync"
 	"testing"
+	"time"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/equality"
@@ -27,6 +28,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/selection"
+	"k8s.io/apimachinery/pkg/util/wait"
 	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
 )
 
@@ -52,6 +54,26 @@ func (c *counter) count() int {
 	return len(c.cfg)
 }
 
+func (c *counter) eventuallyEquals(t *testing.T, want int) {
+	got := 0
+
+	err := wait.Poll(
+		// interval
+		100*time.Millisecond,
+
+		// timeout
+		5*time.Second,
+		func() (done bool, err error) {
+			got = c.count()
+			return got == want, nil
+		},
+	)
+
+	if err != nil {
+		t.Errorf("%v.count = %d, want %d", c.name, got, want)
+	}
+}
+
 func TestInformedWatcher(t *testing.T) {
 	fooCM := &corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
@@ -83,12 +105,10 @@ func TestInformedWatcher(t *testing.T) {
 		t.Fatal("cm.Start() =", err)
 	}
 
-	// When Start returns the callbacks should have been called with the
+	// When Start returns the callbacks will eventually be called with the
 	// version of the objects that is available.
-	for _, obj := range []*counter{foo1, foo2, bar} {
-		if got, want := obj.count(), 1; got != want {
-			t.Errorf("%v.count = %d, want %d", obj.name, got, want)
-		}
+	for _, count := range []*counter{foo1, foo2, bar} {
+		count.eventuallyEquals(t, 1)
 	}
 
 	// After a "foo" event, the "foo" watchers should have 2,
diff --git a/logging/config_test.go b/logging/config_test.go
index 1adb04524..506d57bef 100644
--- a/logging/config_test.go
+++ b/logging/config_test.go
@@ -125,7 +125,7 @@ func TestNewConfigNoEntry(t *testing.T) {
 	if err != nil {
 		t.Error("Expected no errors. got:", err)
 	}
-	if got, want := c.LoggingConfig, defaultZLC; got != want {
+	if got, want := c.LoggingConfig, ""; got != want {
 		t.Errorf("LoggingConfig = %v, want %v", got, want)
 	}
 	if got, want := len(c.LoggingLevel), 0; got != want {
@@ -181,10 +181,10 @@ func TestNewLoggerFromConfig(t *testing.T) {
 		expectLvl:  zapcore.ErrorLevel,
 		expectName: componentName,
 	}, {
-		name:       "Has default level and fallback name when config is empty",
+		name:       "Has default level when config is empty",
 		cfg:        makeTestConfig(),
 		expectLvl:  zapcore.InfoLevel,
-		expectName: "fallback-logger." + componentName,
+		expectName: componentName,
 	}}
 
 	for _, tc := range testCases {
diff --git a/test/mako/sidecar.go b/test/mako/sidecar.go
index ad1531f34..ff890f624 100644
--- a/test/mako/sidecar.go
+++ b/test/mako/sidecar.go
@@ -93,7 +93,7 @@ func SetupHelper(ctx context.Context, benchmarkKey *string, benchmarkName *strin
 	// Get the commit of the benchmarks
 	commitID, err := changeset.Get()
 	if err != nil {
-		return nil, err
+		log.Println("Cannot find commit ID")
 	}
 
 	// Setup a deployment informer, so that we can use the lister to track
diff --git a/webhook/psbinding/index_test.go b/webhook/psbinding/index_test.go
index 6c8e3b812..010e31a42 100644
--- a/webhook/psbinding/index_test.go
+++ b/webhook/psbinding/index_test.go
@@ -34,9 +34,6 @@ func TestExact(t *testing.T) {
 			Namespace: "blah",
 			Name:      "asdf",
 		},
-		Spec: TestBindableSpec{
-			Foo: "bar",
-		},
 	}
 
 	gvk := want.GetGroupVersionKind()
@@ -47,19 +44,18 @@ func TestExact(t *testing.T) {
 		Name:      want.GetName(),
 	}
 
-	// Before we Add something, we shouldn't be able to Get anything.
-	if got, ok := em.Get(key); ok {
-		t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+	// Before we add something, we shouldn't be able to get anything.
+	if got := em.get(key); len(got) != 0 {
+		t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
 	}
 
-	// Now Add it.
-	em.Add(key, want)
+	// Now add it.
+	em.add(key, want)
 
-	// After we Add something, we should be able to Get it.
-	if got, ok := em.Get(key); !ok {
-		t.Errorf("Get(%+v) = %v, %v; wanted b, true", key, want, ok)
-	} else if !cmp.Equal(got, want) {
-		t.Error("Get (-want, +got):", cmp.Diff(want, got))
+	// After we add something, we should be able to get it.
+	wanted := []Bindable{want}
+	if got := em.get(key); !cmp.Equal(got, wanted) {
+		t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 	}
 
 	otherKey := exactKey{
@@ -69,9 +65,24 @@ func TestExact(t *testing.T) {
 		Name:      "bar",
 	}
 
-	// After we Add something, we still shouldn't return things for other keys.
-	if got, ok := em.Get(otherKey); ok {
-		t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+	// After we add something, we still shouldn't return things for other keys.
+	if got := em.get(otherKey); len(got) != 0 {
+		t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
+	}
+
+	// Add another Binding with the same coordinates.
+	alsoWant := &TestBindable{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "blah",
+			Name:      "asdf",
+		},
+	}
+	em.add(key, want)
+
+	// We should now get both Bindings.
+	wanted = []Bindable{want, alsoWant}
+	if got := em.get(key); !cmp.Equal(got, wanted) {
+		t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 	}
 }
 
@@ -85,9 +96,6 @@ func TestInexact(t *testing.T) {
 				"baz": "blah",
 			},
 		},
-		Spec: TestBindableSpec{
-			Foo: "bar",
-		},
 	}
 	ls := labels.Set(want.Labels)
 
@@ -100,8 +108,8 @@ func TestInexact(t *testing.T) {
 
 	t.Run("empty matcher doesn't match", func(t *testing.T) {
 		im := make(inexactMatcher, 1)
-		if got, ok := im.Get(key, ls); ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+		if got := im.get(key, ls); len(got) != 0 {
+			t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
 		}
 	})
 
@@ -111,13 +119,12 @@ func TestInexact(t *testing.T) {
 		// Use exactly the labels from the resource.
 		selector := ls.AsSelector()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
 		// With an appropriate selector, we match and get the binding.
-		if got, ok := im.Get(key, ls); !ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted b, true", key, want, ok)
-		} else if !cmp.Equal(got, want) {
-			t.Error("Get (-want, +got):", cmp.Diff(want, got))
+		wanted := []Bindable{want}
+		if got := im.get(key, ls); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 		}
 	})
 
@@ -127,13 +134,12 @@ func TestInexact(t *testing.T) {
 		// Match everything.
 		selector := labels.Everything()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
 		// With an appropriate selector, we match and get the binding.
-		if got, ok := im.Get(key, ls); !ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted b, true", key, want, ok)
-		} else if !cmp.Equal(got, want) {
-			t.Error("Get (-want, +got):", cmp.Diff(want, got))
+		wanted := []Bindable{want}
+		if got := im.get(key, ls); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 		}
 	})
 
@@ -143,10 +149,10 @@ func TestInexact(t *testing.T) {
 		// Match nothing.
 		selector := labels.Nothing()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
-		if got, ok := im.Get(key, ls); ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+		if got := im.get(key, ls); len(got) != 0 {
+			t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
 		}
 	})
 
@@ -158,13 +164,12 @@ func TestInexact(t *testing.T) {
 			"foo": "bar",
 		}).AsSelector()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
 		// With an appropriate selector, we match and get the binding.
-		if got, ok := im.Get(key, ls); !ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted b, true", key, want, ok)
-		} else if !cmp.Equal(got, want) {
-			t.Error("Get (-want, +got):", cmp.Diff(want, got))
+		wanted := []Bindable{want}
+		if got := im.get(key, ls); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 		}
 	})
 
@@ -177,11 +182,11 @@ func TestInexact(t *testing.T) {
 			"not": "found",
 		}).AsSelector()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
 		// We shouldn't match because the second labels shouldn't match.
-		if got, ok := im.Get(key, ls); ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+		if got := im.get(key, ls); len(got) != 0 {
+			t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
 		}
 	})
 
@@ -191,14 +196,184 @@ func TestInexact(t *testing.T) {
 		// Use exactly the labels from the resource.
 		selector := ls.AsSelector()
 
-		im.Add(key, selector, want)
+		im.add(key, selector, want)
 
 		otherKey := key
 		otherKey.Namespace = "another"
 
 		// We shouldn't match because the second labels shouldn't match.
-		if got, ok := im.Get(otherKey, ls); ok {
-			t.Errorf("Get(%+v) = %v, %v; wanted nil, false", key, got, ok)
+		if got := im.get(otherKey, ls); len(got) != 0 {
+			t.Errorf("Get(%+v) = %v; wanted empty list", key, got)
+		}
+	})
+
+	t.Run("multiple Bindings match", func(t *testing.T) {
+		alsoWant := &TestBindable{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "blah",
+				Name:      "asdf",
+				Labels: map[string]string{
+					"foo": "bar",
+					"baz": "blah",
+				},
+			},
+		}
+
+		im := make(inexactMatcher, 1)
+
+		// Use a subset of the resources labels.
+		selector := labels.Set(map[string]string{
+			"foo": "bar",
+		}).AsSelector()
+
+		im.add(key, selector, want)
+		im.add(key, selector, alsoWant)
+
+		// With an appropriate selector, we match and get both bindings.
+		wanted := []Bindable{want, alsoWant}
+		if got := im.get(key, ls); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+}
+
+func TestIndex(t *testing.T) {
+	wantExact1 := &TestBindable{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "a",
+			Name:      "b",
+			Labels: map[string]string{
+				"colour": "red",
+			},
+		},
+	}
+
+	wantExact2 := &TestBindable{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "a",
+			Name:      "b",
+			Labels: map[string]string{
+				"colour": "blue",
+			},
+		},
+	}
+
+	wantInexact1 := &TestBindable{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "a",
+			Name:      "b",
+			Labels: map[string]string{
+				"colour": "red",
+			},
+		},
+	}
+
+	wantInexact2 := &TestBindable{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "a",
+			Name:      "b",
+			Labels: map[string]string{
+				"colour": "blue",
+			},
+		},
+	}
+
+	gvk := wantExact1.GetGroupVersionKind()
+	eKey := exactKey{
+		Group:     gvk.Group,
+		Kind:      gvk.Kind,
+		Namespace: wantExact1.GetNamespace(),
+		Name:      wantExact1.GetName(),
+	}
+	iKey := inexactKey{
+		Group:     gvk.Group,
+		Kind:      gvk.Kind,
+		Namespace: wantExact1.GetNamespace(),
+	}
+	redLabels := labels.Set(wantInexact1.Labels)
+	redSelector := redLabels.AsSelector()
+	blueLabels := labels.Set(wantInexact2.Labels)
+	blueSelector := blueLabels.AsSelector()
+	none := make(labels.Set)
+	allSelector := none.AsSelector()
+
+	t.Run("single exact match is found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associate(eKey, wantExact1).
+			build(&index)
+
+		wanted := []Bindable{wantExact1}
+		if got := index.lookUp(eKey, none); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+
+	t.Run("single inexact match is found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associateSelection(iKey, redSelector, wantInexact1).
+			build(&index)
+
+		wanted := []Bindable{wantInexact1}
+		if got := index.lookUp(eKey, redLabels); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+
+	t.Run("multiple exact matches are found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associate(eKey, wantExact1).
+			associate(eKey, wantExact2).
+			build(&index)
+
+		wanted := []Bindable{wantExact1, wantExact2}
+		if got := index.lookUp(eKey, none); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+
+	t.Run("multiple inexact matches are found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associateSelection(iKey, allSelector, wantInexact1).
+			associateSelection(iKey, allSelector, wantInexact2).
+			build(&index)
+
+		wanted := []Bindable{wantInexact1, wantInexact2}
+		if got := index.lookUp(eKey, redLabels); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+
+	t.Run("multiple exact and inexact matches are found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associate(eKey, wantExact1).
+			associate(eKey, wantExact2).
+			associateSelection(iKey, allSelector, wantInexact1).
+			associateSelection(iKey, allSelector, wantInexact2).
+			build(&index)
+
+		wanted := []Bindable{wantExact1, wantExact2, wantInexact1, wantInexact2}
+		if got := index.lookUp(eKey, none); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
+		}
+	})
+
+	t.Run("the correct exact and inexact matches are found", func(t *testing.T) {
+		var index index
+		newIndexBuilder().
+			associate(eKey, wantExact1).
+			associate(eKey, wantExact2).
+			associateSelection(iKey, redSelector, wantInexact1).
+			associateSelection(iKey, blueSelector, wantInexact2).
+			build(&index)
+
+		wanted := []Bindable{wantExact1, wantExact2, wantInexact1}
+		if got := index.lookUp(eKey, redLabels); !cmp.Equal(got, wanted) {
+			t.Error("Get (-want, +got):", cmp.Diff(wanted, got))
 		}
 	})
 }