Repository: honeycombio/refinery
Pull request #252 (2021-05-12 16:04:45)

Bump github.com/golang/protobuf from 1.4.3 to 1.5.2
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some time.

Note: if you make any changes to this PR yourself, they will take precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [github.com/golang/protobuf](https://github.com/golang/protobuf) from 1.4.3 to 1.5.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/golang/protobuf/releases">github.com/golang/protobuf's releases</a>.</em></p>
<blockquote>
<h2>v1.5.2</h2>
<p>Notable changes:</p>
<ul>
<li>(<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1306">#1306</a>) all: deprecate the module</li>
<li>(<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1300">#1300</a>) jsonpb: restore previous behavior for handling nulls and JSONPBUnmarshaler</li>
</ul>
<h2>v1.5.1</h2>
<p>Notable changes:</p>
<ul>
<li>(<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1293">#1293</a>) all: depend on <a href="https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.26.0">google.golang.org/protobuf@v1.26.0</a></li>
</ul>
<h2>v1.5.0</h2>
<ul>
<li><a href="https://github.com/golang/protobuf/blob/HEAD/#v1.5-overview">Overview</a></li>
<li><a href="https://github.com/golang/protobuf/blob/HEAD/#v1.5-notable-changes">Notable changes</a></li>
</ul>
<h1>Overview <!-- raw HTML omitted --><!-- raw HTML omitted --></h1>
<p>This marks the <a href="https://pkg.go.dev/github.com/golang/protobuf/ptypes"><code>ptypes</code></a> package as deprecated and upgrades the dependency on <code>google.golang.org/protobuf</code> to a pre-release version of v1.26.0. A subsequent patch release will update the dependency to <a href="https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.26.0">v1.26.0</a> proper.</p>
<h1>Notable changes <!-- raw HTML omitted --><!-- raw HTML omitted --></h1>
<ul>
<li>(<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1217">#1217</a>) ptypes: deprecate the package</li>
<li>(<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1214">#1214</a>) rely on protodesc.ToFileDescriptorProto</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/protobuf/commit/ae97035608a719c7a1c1c41bed0ae0744bdb0c6f"><code>ae97035</code></a> all: deprecate the module (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1306">#1306</a>)</li>
<li><a href="https://github.com/golang/protobuf/commit/af940030a2b77f37337632168f18403433e21e61"><code>af94003</code></a> jsonpb: restore previous behavior for handling nulls and JSONPBUnmarshaler (#...</li>
<li><a href="https://github.com/golang/protobuf/commit/a36a1a1c346d5cb6951b9440755adb530334f910"><code>a36a1a1</code></a> all: depend on google.golang.org/protobuf@v1.26.0 (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1293">#1293</a>)</li>
<li><a href="https://github.com/golang/protobuf/commit/f746d3b8ecd31eac7c04a954f3905d46b09f8114"><code>f746d3b</code></a> all: depend on google.golang.org/protobuf@v1.26.0-rc.1 (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1292">#1292</a>)</li>
<li><a href="https://github.com/golang/protobuf/commit/78b1f09b4310f9ee5c9a07dca51bf007b82c83ea"><code>78b1f09</code></a> Switch from Travis-CI to GitHub actions (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1286">#1286</a>)</li>
<li><a href="https://github.com/golang/protobuf/commit/acacf8158c9a307051c92dc233966e8324facd45"><code>acacf81</code></a> all: rely on protodesc.ToFileDescriptorProto (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1214">#1214</a>)</li>
<li><a href="https://github.com/golang/protobuf/commit/eccd77d6ffe33de3bca6050a06c4e8294783de69"><code>eccd77d</code></a> ptypes: deprecate the package (<a href="https://github-redirect.dependabot.com/golang/protobuf/issues/1217">#1217</a>)</li>
<li>See full diff in <a href="https://github.com/golang/protobuf/compare/v1.4.3...v1.5.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/golang/protobuf&package-manager=go_modules&previous-version=1.4.3&new-version=1.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

vreynolds: @dependabot rebase

Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8dcecb4..bc06334 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,1 +1,1 @@
-* @honeycombio/integrations-team
+* @honeycombio/telemetry-team
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index caf9e4d..2a0f635 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,6 +10,6 @@ updates:
     schedule:
       interval: "weekly"
     labels:
-      - "type: maintenance"
+      - "type: dependencies"
     reviewers:
-      - "honeycombio/integrations-team"
+      - "honeycombio/telemetry-team"
diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml
new file mode 100644
index 0000000..ac02faa
--- /dev/null
+++ b/.github/workflows/add-to-project.yml
@@ -0,0 +1,14 @@
+name: Apply project management flow
+on:
+  issues:
+    types: [opened]
+  pull_request_target:
+    types: [opened]
+jobs:
+  project-management:
+    runs-on: ubuntu-latest
+    name: Apply project management flow
+    steps:
+      - uses: honeycombio/oss-management-actions/projects@v1
+        with:
+          ghprojects-token: ${{ secrets.GHPROJECTS_TOKEN }}
diff --git a/.github/workflows/apply-labels.yml b/.github/workflows/apply-labels.yml
index b6aeae2..7d90af5 100644
--- a/.github/workflows/apply-labels.yml
+++ b/.github/workflows/apply-labels.yml
@@ -1,16 +1,10 @@
 name: Apply project labels
-
-on:
-  - issues
-  - label
-  - pull_request_target
-  - pull_request
-
+on: [issues, pull_request, label]
 jobs:
   apply-labels:
     runs-on: ubuntu-latest
     name: Apply common project labels
     steps:
-      - uses: honeycombio/integrations-labels@v1
+      - uses: honeycombio/oss-management-actions/labels@v1
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9e889d..305965c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,48 @@
 # Refinery Changelog
 
+## 1.3.0
+
+### Added
+
+- Add support to "does-not-contain" operator on RulesBasedSampler [#267](https://github.com/honeycombio/refinery/pull/267) | [@tr-fteixeira](https://github.com/tr-fteixeira)
+
+### Fixes
+
+- Ensure span links and events generate events and get resource attrs [#264](https://github.com/honeycombio/refinery/pull/264) | [@MikeGoldsmith](https://github.com/MikeGoldsmith)
+
+## 1.2.1
+
+### Fixes
+
+- OTLP span events are now supported, they were being dropped on the floor previously (#261) | [@dstrelau](https://github.com/dstrelau)
+
+## 1.2.0
+
+### Added
+
+- Add `UseTLSInsecure` config option to skip TLS verification with Redis (#254) | [@beanieboi](https://github.com/beanieboi)
+- Add `AddHostMetadataToTrace` config option to add Refinery hostname information to spans (#250) | [@jharley](https://github.com/jharley)
+- Additional config validation: verify that sample rate trace field key is specified, if needed (#248) | [@paulosman](https://github.com/paulosman)
+
+### Changed
+
+- Remove redundant peer/api suffix from response error metrics (#247) | [@vreynolds](https://github.com/vreynolds)
+    - `api_response_errors_api`, `api_response_errors_peer`, `peer_response_errors_api`, `peer_response_errors_peer`
+    - replaced by `api_response_errors`, `peer_response_errors`
+- Fix rules sampler to emit correct metric (#236) | [@isnotajoke](https://github.com/isnotajoke)
+    - Previously `dynsampler_num_dropped` was emitted, now `rulessampler_num_dropped` will be emitted
+
+### Maintenance
+
+- Update README content (#239) | [@jjziv](https://github.com/jjziv)
+- Move from garyburd Redigo to supported redigo (#249) | [@verajohne](https://github.com/verajohne)
+- Bump google.golang.org/grpc from 1.32.0 to 1.37.1 (#253)
+- Bump github.com/prometheus/client_golang from 0.9.3 to 0.9.4 (#240)
+- Bump github.com/pkg/errors from 0.8.1 to 0.9.1 (#232)
+- Bump github.com/stretchr/testify from 1.5.1 to 1.7.0 (#231)
+- Bump github.com/jessevdk/go-flags from 1.4.0 to 1.5.0 (#230)
+- Bump github.com/hashicorp/golang-lru from 0.5.1 to 0.5.4 (#229)
+
 ## 1.1.1
 
 ### Fixes
diff --git a/go.mod b/go.mod
index dfaf6f5..9664168 100644
--- a/go.mod
+++ b/go.mod
@@ -12,10 +12,10 @@ require (
 	github.com/go-playground/universal-translator v0.17.0 // indirect
 	github.com/go-playground/validator v9.31.0+incompatible
 	github.com/gogo/protobuf v1.3.1
-	github.com/golang/protobuf v1.4.3
+	github.com/golang/protobuf v1.5.2
 	github.com/gomodule/redigo v1.8.4
 	github.com/gorilla/mux v1.6.3-0.20190108142930-08e7f807d38d
-	github.com/grpc-ecosystem/grpc-gateway v1.12.1
+	github.com/grpc-ecosystem/grpc-gateway v1.16.0
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/honeycombio/dynsampler-go v0.2.1
 	github.com/honeycombio/libhoney-go v1.12.4
@@ -26,7 +26,7 @@ require (
 	github.com/mitchellh/mapstructure v1.3.3 // indirect
 	github.com/pelletier/go-toml v1.8.0 // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/prometheus/client_golang v0.9.3
+	github.com/prometheus/client_golang v0.9.4
 	github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0
 	github.com/sirupsen/logrus v1.2.0
 	github.com/spf13/afero v1.3.2 // indirect
diff --git a/go.sum b/go.sum
index 0850042..923f4a5 100644
--- a/go.sum
+++ b/go.sum
@@ -20,7 +20,7 @@ github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -32,6 +32,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -45,6 +46,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
@@ -93,6 +95,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -102,8 +105,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg=
 github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -112,8 +116,9 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -129,8 +134,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.12.1 h1:zCy2xE9ablevUOrUZc3Dl72Dt+ya2FNAvC2yLYMHzi4=
-github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -218,18 +223,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v0.9.4 h1:Y8E/JaaPbmFSW2V81Ab/d8yZFYQQGbni1b1jPcG9Y6A=
+github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
 github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -287,8 +295,9 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -321,13 +330,14 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -377,8 +387,9 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -398,17 +409,17 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
 google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.37.1 h1:ARnQJNWxGyYJpdf/JXscNlQr/uv607ZPU9Z7ogHi+iI=
 google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -419,8 +430,10 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc=
 gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU=
diff --git a/route/errors.go b/route/errors.go
index 5e347ec..ff697d9 100644
--- a/route/errors.go
+++ b/route/errors.go
@@ -33,6 +33,7 @@ var (
 	ErrUpstreamUnavailable = handlerError{nil, "upstream target unavailable", http.StatusServiceUnavailable, true, true}
 	ErrReqToEvent          = handlerError{nil, "failed to parse event", http.StatusBadRequest, false, true}
 	ErrBatchToEvent        = handlerError{nil, "failed to parse event within batch", http.StatusBadRequest, false, true}
+	ErrInvalidContentType  = handlerError{nil, "invalid content-type - only 'application/protobuf' is supported", http.StatusNotImplemented, false, true}
 )
 
 func (r *Router) handlerReturnWithError(w http.ResponseWriter, he handlerError, err error) {
diff --git a/route/middleware.go b/route/middleware.go
index 8264bc7..7836433 100644
--- a/route/middleware.go
+++ b/route/middleware.go
@@ -45,7 +45,7 @@ func (r *Router) apiKeyChecker(next http.Handler) http.Handler {
 				return
 			}
 		}
-		err = errors.New(fmt.Sprintf("api key %s not found in list of authed keys", apiKey))
+		err = fmt.Errorf("api key %s not found in list of authed keys", apiKey)
 		r.handlerReturnWithError(w, ErrAuthNeeded, err)
 	})
 }
diff --git a/route/otlp_trace.go b/route/otlp_trace.go
new file mode 100644
index 0000000..dca390e
--- /dev/null
+++ b/route/otlp_trace.go
@@ -0,0 +1,404 @@
+package route
+
+import (
+	"bytes"
+	"compress/gzip"
+	"context"
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/honeycombio/refinery/types"
+	"github.com/klauspost/compress/zstd"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/status"
+
+	collectortrace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/collector/trace/v1"
+	common "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/common/v1"
+	trace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/trace/v1"
+)
+
+func (router *Router) postOTLP(w http.ResponseWriter, req *http.Request) {
+	defer req.Body.Close()
+	contentType := req.Header.Get("content-type")
+	if contentType != "application/protobuf" && contentType != "application/x-protobuf" {
+		router.handlerReturnWithError(w, ErrInvalidContentType, errors.New("invalid content-type"))
+		return
+	}
+
+	apiKey, datasetName, err := getAPIKeyDatasetAndTokenFromHttpHeaders(req)
+	if err != nil {
+		router.handlerReturnWithError(w, ErrAuthNeeded, err)
+		return
+	}
+
+	request, cleanup, err := parseOTLPBody(req, router.zstdDecoders)
+	defer cleanup()
+	if err != nil {
+		router.handlerReturnWithError(w, ErrPostBody, err)
+	}
+
+	if err := processTraceRequest(req.Context(), router, request, apiKey, datasetName); err != nil {
+		router.handlerReturnWithError(w, ErrUpstreamFailed, err)
+	}
+}
+
+func (router *Router) Export(ctx context.Context, req *collectortrace.ExportTraceServiceRequest) (*collectortrace.ExportTraceServiceResponse, error) {
+	apiKey, datasetName, err := getAPIKeyDatasetAndTokenFromMetadata(ctx)
+	if err != nil {
+		return nil, status.Error(codes.Unauthenticated, err.Error())
+	}
+
+	if err := processTraceRequest(ctx, router, req, apiKey, datasetName); err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	return &collectortrace.ExportTraceServiceResponse{}, nil
+}
+
+func processTraceRequest(
+	ctx context.Context,
+	router *Router,
+	request *collectortrace.ExportTraceServiceRequest,
+	apiKey string,
+	datasetName string) error {
+
+	var requestID types.RequestIDContextKey
+	debugLog := router.iopLogger.Debug().WithField("request_id", requestID)
+
+	apiHost, err := router.Config.GetHoneycombAPI()
+	if err != nil {
+		router.Logger.Error().Logf("Unable to retrieve APIHost from config while processing OTLP batch")
+		return err
+	}
+
+	for _, resourceSpan := range request.ResourceSpans {
+		resourceAttrs := make(map[string]interface{})
+
+		if resourceSpan.Resource != nil {
+			addAttributesToMap(resourceAttrs, resourceSpan.Resource.Attributes)
+		}
+
+		for _, librarySpan := range resourceSpan.InstrumentationLibrarySpans {
+			library := librarySpan.InstrumentationLibrary
+			if library != nil {
+				if len(library.Name) > 0 {
+					resourceAttrs["library.name"] = library.Name
+				}
+				if len(library.Version) > 0 {
+					resourceAttrs["library.version"] = library.Version
+				}
+			}
+
+			for _, span := range librarySpan.GetSpans() {
+				traceID := bytesToTraceID(span.TraceId)
+				spanID := hex.EncodeToString(span.SpanId)
+				timestamp := time.Unix(0, int64(span.StartTimeUnixNano)).UTC()
+
+				eventAttrs := map[string]interface{}{
+					"trace.trace_id": traceID,
+					"trace.span_id":  spanID,
+					"type":           getSpanKind(span.Kind),
+					"name":           span.Name,
+					"duration_ms":    float64(span.EndTimeUnixNano-span.StartTimeUnixNano) / float64(time.Millisecond),
+					"status_code":    int32(getSpanStatusCode(span.Status)),
+				}
+				if span.ParentSpanId != nil {
+					eventAttrs["trace.parent_id"] = hex.EncodeToString(span.ParentSpanId)
+				}
+				if getSpanStatusCode(span.Status) == trace.Status_STATUS_CODE_ERROR {
+					eventAttrs["error"] = true
+				}
+				if span.Status != nil && len(span.Status.Message) > 0 {
+					eventAttrs["status_message"] = span.Status.Message
+				}
+				if span.Attributes != nil {
+					addAttributesToMap(eventAttrs, span.Attributes)
+				}
+
+				sampleRate, err := getSampleRateFromAttributes(eventAttrs)
+				if err != nil {
+					debugLog.WithField("error", err.Error()).WithField("sampleRate", eventAttrs["sampleRate"]).Logf("error parsing sampleRate")
+				}
+
+				// copy resource attributes to event attributes
+				for k, v := range resourceAttrs {
+					eventAttrs[k] = v
+				}
+
+				events := make([]*types.Event, 0, 1+len(span.Events)+len(span.Links))
+				events = append(events, &types.Event{
+					Context:    ctx,
+					APIHost:    apiHost,
+					APIKey:     apiKey,
+					Dataset:    datasetName,
+					SampleRate: uint(sampleRate),
+					Timestamp:  timestamp,
+					Data:       eventAttrs,
+				})
+
+				for _, sevent := range span.Events {
+					timestamp := time.Unix(0, int64(sevent.TimeUnixNano)).UTC()
+					attrs := map[string]interface{}{
+						"trace.trace_id":       traceID,
+						"trace.parent_id":      spanID,
+						"name":                 sevent.Name,
+						"parent_name":          span.Name,
+						"meta.annotation_type": "span_event",
+					}
+
+					if sevent.Attributes != nil {
+						addAttributesToMap(attrs, sevent.Attributes)
+					}
+					for k, v := range resourceAttrs {
+						attrs[k] = v
+					}
+					sampleRate, err := getSampleRateFromAttributes(attrs)
+					if err != nil {
+						debugLog.
+							WithField("error", err.Error()).
+							WithField("sampleRate", attrs["sampleRate"]).
+							Logf("error parsing sampleRate")
+					}
+					events = append(events, &types.Event{
+						Context:    ctx,
+						APIHost:    apiHost,
+						APIKey:     apiKey,
+						Dataset:    datasetName,
+						SampleRate: uint(sampleRate),
+						Timestamp:  timestamp,
+						Data:       attrs,
+					})
+				}
+
+				for _, slink := range span.Links {
+					attrs := map[string]interface{}{
+						"trace.trace_id":       traceID,
+						"trace.parent_id":      spanID,
+						"trace.link.trace_id":  bytesToTraceID(slink.TraceId),
+						"trace.link.span_id":   hex.EncodeToString(slink.SpanId),
+						"parent_name":          span.Name,
+						"meta.annotation_type": "link",
+					}
+
+					if slink.Attributes != nil {
+						addAttributesToMap(attrs, slink.Attributes)
+					}
+					for k, v := range resourceAttrs {
+						attrs[k] = v
+					}
+					sampleRate, err := getSampleRateFromAttributes(attrs)
+					if err != nil {
+						debugLog.
+							WithField("error", err.Error()).
+							WithField("sampleRate", attrs["sampleRate"]).
+							Logf("error parsing sampleRate")
+					}
+					events = append(events, &types.Event{
+						Context:    ctx,
+						APIHost:    apiHost,
+						APIKey:     apiKey,
+						Dataset:    datasetName,
+						SampleRate: uint(sampleRate),
+						Timestamp:  time.Time{}, //links don't have timestamps, so use empty time
+						Data:       attrs,
+					})
+				}
+
+				for _, evt := range events {
+					err = router.processEvent(evt, requestID)
+					if err != nil {
+						router.Logger.Error().Logf("Error processing event: " + err.Error())
+					}
+				}
+
+			}
+		}
+	}
+
+	return nil
+}
+
+func addAttributesToMap(attrs map[string]interface{}, attributes []*common.KeyValue) {
+	for _, attr := range attributes {
+		if attr.Key == "" {
+			continue
+		}
+		switch attr.Value.Value.(type) {
+		case *common.AnyValue_StringValue:
+			attrs[attr.Key] = attr.Value.GetStringValue()
+		case *common.AnyValue_BoolValue:
+			attrs[attr.Key] = attr.Value.GetBoolValue()
+		case *common.AnyValue_DoubleValue:
+			attrs[attr.Key] = attr.Value.GetDoubleValue()
+		case *common.AnyValue_IntValue:
+			attrs[attr.Key] = attr.Value.GetIntValue()
+		}
+	}
+}
+
+func getSpanKind(kind trace.Span_SpanKind) string {
+	switch kind {
+	case trace.Span_SPAN_KIND_CLIENT:
+		return "client"
+	case trace.Span_SPAN_KIND_SERVER:
+		return "server"
+	case trace.Span_SPAN_KIND_PRODUCER:
+		return "producer"
+	case trace.Span_SPAN_KIND_CONSUMER:
+		return "consumer"
+	case trace.Span_SPAN_KIND_INTERNAL:
+		return "internal"
+	case trace.Span_SPAN_KIND_UNSPECIFIED:
+		fallthrough
+	default:
+		return "unspecified"
+	}
+}
+
+// bytesToTraceID returns an ID suitable for use for spans and traces. Before
+// encoding the bytes as a hex string, we want to handle cases where we are
+// given 128-bit IDs with zero padding, e.g. 0000000000000000f798a1e7f33c8af6.
+// To do this, we borrow a strategy from Jaeger [1] wherein we split the byte
+// sequence into two parts. The leftmost part could contain all zeros. We use
+// that to determine whether to return a 64-bit hex encoded string or a 128-bit
+// one.
+//
+// [1]: https://github.com/jaegertracing/jaeger/blob/cd19b64413eca0f06b61d92fe29bebce1321d0b0/model/ids.go#L81
+func bytesToTraceID(traceID []byte) string {
+	// binary.BigEndian.Uint64() does a bounds check on traceID which will
+	// cause a panic if traceID is fewer than 8 bytes. In this case, we don't
+	// need to check for zero padding on the high part anyway, so just return a
+	// hex string.
+	if len(traceID) < traceIDShortLength {
+		return fmt.Sprintf("%x", traceID)
+	}
+	var low uint64
+	if len(traceID) == traceIDLongLength {
+		low = binary.BigEndian.Uint64(traceID[traceIDShortLength:])
+		if high := binary.BigEndian.Uint64(traceID[:traceIDShortLength]); high != 0 {
+			return fmt.Sprintf("%016x%016x", high, low)
+		}
+	} else {
+		low = binary.BigEndian.Uint64(traceID)
+	}
+
+	return fmt.Sprintf("%016x", low)
+}
+
+// getSpanStatusCode checks the value of both the deprecated code and code fields
+// on the span status and using the rules specified in the backward compatibility
+// notes in the protobuf definitions. See:
+//
+// https://github.com/open-telemetry/opentelemetry-proto/blob/59c488bfb8fb6d0458ad6425758b70259ff4a2bd/opentelemetry/proto/trace/v1/trace.proto#L230
+func getSpanStatusCode(status *trace.Status) trace.Status_StatusCode {
+	if status == nil {
+		return trace.Status_STATUS_CODE_UNSET
+	}
+	if status.Code == trace.Status_STATUS_CODE_UNSET {
+		if status.DeprecatedCode == trace.Status_DEPRECATED_STATUS_CODE_OK {
+			return trace.Status_STATUS_CODE_UNSET
+		}
+		return trace.Status_STATUS_CODE_ERROR
+	}
+	return status.Code
+}
+
+func getAPIKeyDatasetAndTokenFromMetadata(ctx context.Context) (
+	apiKey string,
+	datasetName string,
+	err error) {
+	if md, ok := metadata.FromIncomingContext(ctx); ok {
+		apiKey = getValueFromMetadata(md, "x-honeycomb-team")
+		datasetName = getValueFromMetadata(md, "x-honeycomb-dataset")
+	}
+
+	if err := validateHeaders(apiKey, datasetName); err != nil {
+		return "", "", err
+	}
+	return apiKey, datasetName, nil
+}
+
+func getValueFromMetadata(md metadata.MD, key string) string {
+	if vals := md.Get(key); len(vals) > 0 {
+		return vals[0]
+	}
+	return ""
+}
+
+func getAPIKeyDatasetAndTokenFromHttpHeaders(r *http.Request) (
+	apiKey string,
+	datasetName string,
+	err error) {
+	apiKey = r.Header.Get("x-honeycomb-team")
+	datasetName = r.Header.Get("x-honeycomb-dataset")
+
+	if err := validateHeaders(apiKey, datasetName); err != nil {
+		return "", "", err
+	}
+	return apiKey, datasetName, nil
+}
+
+func validateHeaders(apiKey string, datasetName string) error {
+	if apiKey == "" {
+		return errors.New("missing x-honeycomb-team header")
+	}
+	if datasetName == "" {
+		return errors.New("missing x-honeycomb-team header")
+	}
+	return nil
+}
+
+func parseOTLPBody(r *http.Request, zstdDecoders chan *zstd.Decoder) (request *collectortrace.ExportTraceServiceRequest, cleanup func(), err error) {
+	cleanup = func() { /* empty cleanup */ }
+	bodyBytes, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		return nil, cleanup, err
+	}
+	bodyReader := bytes.NewReader(bodyBytes)
+
+	var reader io.Reader
+	switch r.Header.Get("Content-Encoding") {
+	case "gzip":
+		var err error
+		reader, err = gzip.NewReader(bodyReader)
+		if err != nil {
+			return nil, cleanup, err
+		}
+	case "zstd":
+		zReader := <-zstdDecoders
+		cleanup = func() {
+			zReader.Reset(nil)
+			zstdDecoders <- zReader
+		}
+
+		err = zReader.Reset(bodyReader)
+		if err != nil {
+			return nil, cleanup, err
+		}
+
+		reader = zReader
+	default:
+		reader = bodyReader
+	}
+
+	bytes, err := ioutil.ReadAll(reader)
+	if err != nil {
+		return nil, cleanup, err
+	}
+
+	otlpRequet := &collectortrace.ExportTraceServiceRequest{}
+	err = proto.Unmarshal(bytes, otlpRequet)
+	if err != nil {
+		return nil, cleanup, err
+	}
+
+	return otlpRequet, cleanup, nil
+}
diff --git a/route/route.go b/route/route.go
index 05630cc..6597318 100644
--- a/route/route.go
+++ b/route/route.go
@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"compress/gzip"
 	"context"
-	"encoding/binary"
-	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -15,7 +13,6 @@ import (
 	"net"
 	"net/http"
 	"strconv"
-	"strings"
 	"sync"
 	"time"
 
@@ -39,8 +36,6 @@ import (
 	"github.com/honeycombio/refinery/types"
 
 	collectortrace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/collector/trace/v1"
-	common "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/common/v1"
-	trace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/trace/v1"
 )
 
 const (
@@ -161,6 +156,13 @@ func (r *Router) LnS(incomingOrPeer string) {
 	authedMuxxer.HandleFunc("/events/{datasetName}", r.event).Name("event")
 	authedMuxxer.HandleFunc("/batch/{datasetName}", r.batch).Name("batch")
 
+	// require an auth header for OTLP requests
+	otlpMuxxer := muxxer.PathPrefix("/v1/").Methods("POST").Subrouter()
+	otlpMuxxer.Use(r.apiKeyChecker)
+
+	// handle OTLP trace requests
+	otlpMuxxer.HandleFunc("/traces", r.postOTLP).Name("otlp")
+
 	// pass everything else through unmolested
 	muxxer.PathPrefix("/").HandlerFunc(r.proxy).Name("proxy")
 
@@ -386,118 +388,6 @@ func (r *Router) batch(w http.ResponseWriter, req *http.Request) {
 	w.Write(response)
 }
 
-func (r *Router) Export(ctx context.Context, req *collectortrace.ExportTraceServiceRequest) (*collectortrace.ExportTraceServiceResponse, error) {
-
-	md, ok := metadata.FromIncomingContext(ctx)
-	if !ok {
-		r.Logger.Error().Logf("Unable to retreive metadata from OTLP request.")
-		return &collectortrace.ExportTraceServiceResponse{}, nil
-	}
-
-	// requestID is used to track a requst as it moves between refinery nodes (peers)
-	// the OTLP handler only receives incoming (not peer) requests for now so will be empty here
-	var requestID types.RequestIDContextKey
-	debugLog := r.iopLogger.Debug().WithField("request_id", requestID)
-
-	apiKey, dataset := getAPIKeyAndDatasetFromMetadata(md)
-	if apiKey == "" {
-		r.Logger.Error().Logf("Received OTLP request without Honeycomb APIKey header")
-		return &collectortrace.ExportTraceServiceResponse{}, nil
-	}
-	if dataset == "" {
-		r.Logger.Error().Logf("Received OTLP request without Honeycomb dataset header")
-		return &collectortrace.ExportTraceServiceResponse{}, nil
-	}
-
-	apiHost, err := r.Config.GetHoneycombAPI()
-	if err != nil {
-		r.Logger.Error().Logf("Unable to retrieve APIHost from config while processing OTLP batch")
-		return &collectortrace.ExportTraceServiceResponse{}, nil
-	}
-
-	var grpcRequestEncoding string
-	if val := md.Get("grpc-accept-encoding"); val != nil {
-		grpcRequestEncoding = strings.Join(val, ",")
-	}
-
-	for _, resourceSpan := range req.ResourceSpans {
-		resourceAttrs := make(map[string]interface{})
-
-		if resourceSpan.Resource != nil {
-			addAttributesToMap(resourceAttrs, resourceSpan.Resource.Attributes)
-		}
-
-		for _, librarySpan := range resourceSpan.InstrumentationLibrarySpans {
-			library := librarySpan.InstrumentationLibrary
-			if library != nil {
-				if len(library.Name) > 0 {
-					resourceAttrs["library.name"] = library.Name
-				}
-				if len(library.Version) > 0 {
-					resourceAttrs["library.version"] = library.Version
-				}
-			}
-
-			for _, span := range librarySpan.GetSpans() {
-				traceID := bytesToTraceID(span.TraceId)
-				spanID := hex.EncodeToString(span.SpanId)
-				timestamp := time.Unix(0, int64(span.StartTimeUnixNano)).UTC()
-
-				eventAttrs := map[string]interface{}{
-					"trace.trace_id": traceID,
-					"trace.span_id":  spanID,
-					"type":           getSpanKind(span.Kind),
-					"name":           span.Name,
-					"duration_ms":    float64(span.EndTimeUnixNano-span.StartTimeUnixNano) / float64(time.Millisecond),
-					"status_code":    int32(r.getSpanStatusCode(span.Status)),
-				}
-				if span.ParentSpanId != nil {
-					eventAttrs["trace.parent_id"] = hex.EncodeToString(span.ParentSpanId)
-				}
-				if r.getSpanStatusCode(span.Status) == trace.Status_STATUS_CODE_ERROR {
-					eventAttrs["error"] = true
-				}
-				if span.Status != nil && len(span.Status.Message) > 0 {
-					eventAttrs["status_message"] = span.Status.Message
-				}
-				if span.Attributes != nil {
-					addAttributesToMap(eventAttrs, span.Attributes)
-				}
-				if grpcRequestEncoding != "" {
-					eventAttrs["grpc_request_encoding"] = grpcRequestEncoding
-				}
-
-				sampleRate, err := getSampleRateFromAttributes(eventAttrs)
-				if err != nil {
-					debugLog.WithField("error", err.Error()).WithField("sampleRate", eventAttrs["sampleRate"]).Logf("error parsing sampleRate")
-				}
-
-				// copy resource attributes to event attributes
-				for k, v := range resourceAttrs {
-					eventAttrs[k] = v
-				}
-
-				event := &types.Event{
-					Context:    ctx,
-					APIHost:    apiHost,
-					APIKey:     apiKey,
-					Dataset:    dataset,
-					SampleRate: uint(sampleRate),
-					Timestamp:  timestamp,
-					Data:       eventAttrs,
-				}
-
-				err = r.processEvent(event, requestID)
-				if err != nil {
-					r.Logger.Error().Logf("Error processing event: " + err.Error())
-				}
-			}
-		}
-	}
-
-	return &collectortrace.ExportTraceServiceResponse{}, nil
-}
-
 func (r *Router) processEvent(ev *types.Event, reqID interface{}) error {
 	debugLog := r.iopLogger.Debug().
 		WithField("request_id", reqID).
@@ -734,91 +624,6 @@ func getFirstValueFromMetadata(key string, md metadata.MD) string {
 	return ""
 }
 
-func addAttributesToMap(attrs map[string]interface{}, attributes []*common.KeyValue) {
-	for _, attr := range attributes {
-		if attr.Key == "" {
-			continue
-		}
-		switch attr.Value.Value.(type) {
-		case *common.AnyValue_StringValue:
-			attrs[attr.Key] = attr.Value.GetStringValue()
-		case *common.AnyValue_BoolValue:
-			attrs[attr.Key] = attr.Value.GetBoolValue()
-		case *common.AnyValue_DoubleValue:
-			attrs[attr.Key] = attr.Value.GetDoubleValue()
-		case *common.AnyValue_IntValue:
-			attrs[attr.Key] = attr.Value.GetIntValue()
-		}
-	}
-}
-
-func getSpanKind(kind trace.Span_SpanKind) string {
-	switch kind {
-	case trace.Span_SPAN_KIND_CLIENT:
-		return "client"
-	case trace.Span_SPAN_KIND_SERVER:
-		return "server"
-	case trace.Span_SPAN_KIND_PRODUCER:
-		return "producer"
-	case trace.Span_SPAN_KIND_CONSUMER:
-		return "consumer"
-	case trace.Span_SPAN_KIND_INTERNAL:
-		return "internal"
-	case trace.Span_SPAN_KIND_UNSPECIFIED:
-		fallthrough
-	default:
-		return "unspecified"
-	}
-}
-
-// bytesToTraceID returns an ID suitable for use for spans and traces. Before
-// encoding the bytes as a hex string, we want to handle cases where we are
-// given 128-bit IDs with zero padding, e.g. 0000000000000000f798a1e7f33c8af6.
-// To do this, we borrow a strategy from Jaeger [1] wherein we split the byte
-// sequence into two parts. The leftmost part could contain all zeros. We use
-// that to determine whether to return a 64-bit hex encoded string or a 128-bit
-// one.
-//
-// [1]: https://github.com/jaegertracing/jaeger/blob/cd19b64413eca0f06b61d92fe29bebce1321d0b0/model/ids.go#L81
-func bytesToTraceID(traceID []byte) string {
-	// binary.BigEndian.Uint64() does a bounds check on traceID which will
-	// cause a panic if traceID is fewer than 8 bytes. In this case, we don't
-	// need to check for zero padding on the high part anyway, so just return a
-	// hex string.
-	if len(traceID) < traceIDShortLength {
-		return fmt.Sprintf("%x", traceID)
-	}
-	var low uint64
-	if len(traceID) == traceIDLongLength {
-		low = binary.BigEndian.Uint64(traceID[traceIDShortLength:])
-		if high := binary.BigEndian.Uint64(traceID[:traceIDShortLength]); high != 0 {
-			return fmt.Sprintf("%016x%016x", high, low)
-		}
-	} else {
-		low = binary.BigEndian.Uint64(traceID)
-	}
-
-	return fmt.Sprintf("%016x", low)
-}
-
-// getSpanStatusCode checks the value of both the deprecated code and code fields
-// on the span status and using the rules specified in the backward compatibility
-// notes in the protobuf definitions. See:
-//
-// https://github.com/open-telemetry/opentelemetry-proto/blob/59c488bfb8fb6d0458ad6425758b70259ff4a2bd/opentelemetry/proto/trace/v1/trace.proto#L230
-func (r *Router) getSpanStatusCode(status *trace.Status) trace.Status_StatusCode {
-	if status == nil {
-		return trace.Status_STATUS_CODE_UNSET
-	}
-	if status.Code == trace.Status_STATUS_CODE_UNSET {
-		if status.DeprecatedCode == trace.Status_DEPRECATED_STATUS_CODE_OK {
-			return trace.Status_STATUS_CODE_UNSET
-		}
-		return trace.Status_STATUS_CODE_ERROR
-	}
-	return status.Code
-}
-
 func getSampleRateFromAttributes(attrs map[string]interface{}) (int, error) {
 	var sampleRateKey string
 	if attrs["sampleRate"] != nil {
@@ -851,7 +656,7 @@ func getSampleRateFromAttributes(attrs map[string]interface{}) (int, error) {
 			sampleRate = int(v)
 		}
 	default:
-		err = fmt.Errorf("Unrecognised sampleRate datatype - %T", sampleRate)
+		err = fmt.Errorf("unrecognised sampleRate datatype - %T", sampleRate)
 		sampleRate = defaultSampleRate
 	}
 	// remove sampleRate from event fields
diff --git a/sample/rules.go b/sample/rules.go
index da9d997..4f035c3 100644
--- a/sample/rules.go
+++ b/sample/rules.go
@@ -104,6 +104,14 @@ func (s *RulesBasedSampler) GetSampleRate(trace *types.Trace) (rate uint, keep b
 								match = strings.Contains(a, b)
 							}
 						}
+					case "does-not-contain":
+						switch a := value.(type) {
+						case string:
+							switch b := condition.Value.(type) {
+							case string:
+								match = !strings.Contains(a, b)
+							}
+						}
 					}
 				case false:
 					switch condition.Operator {
diff --git a/transmit/transmit.go b/transmit/transmit.go
index fc6d7d1..fa46711 100644
--- a/transmit/transmit.go
+++ b/transmit/transmit.go
@@ -23,10 +23,9 @@ type Transmission interface {
 }
 
 const (
-	counterEnqueueErrors      = "enqueue_errors"
-	counterResponse20x        = "response_20x"
-	counterResponseErrorsAPI  = "response_errors_api"
-	counterResponseErrorsPeer = "response_errors_peer"
+	counterEnqueueErrors  = "enqueue_errors"
+	counterResponse20x    = "response_20x"
+	counterResponseErrors = "response_errors"
 )
 
 type DefaultTransmission struct {
@@ -72,8 +71,7 @@ func (d *DefaultTransmission) Start() error {
 
 	d.Metrics.Register(d.Name+counterEnqueueErrors, "counter")
 	d.Metrics.Register(d.Name+counterResponse20x, "counter")
-	d.Metrics.Register(d.Name+counterResponseErrorsAPI, "counter")
-	d.Metrics.Register(d.Name+counterResponseErrorsPeer, "counter")
+	d.Metrics.Register(d.Name+counterResponseErrors, "counter")
 
 	processCtx, canceler := context.WithCancel(context.Background())
 	d.responseCanceler = canceler
@@ -147,7 +145,6 @@ func (d *DefaultTransmission) processResponses(
 	ctx context.Context,
 	responses chan transmission.Response,
 ) {
-	honeycombAPI, _ := d.Config.GetHoneycombAPI()
 	for {
 		select {
 		case r := <-responses:
@@ -170,14 +167,7 @@ func (d *DefaultTransmission) processResponses(
 					log = log.WithField("error", r.Err.Error())
 				}
 				log.Logf("error when sending event")
-				if honeycombAPI == apiHost {
-					// if the API host matches the configured honeycomb API,
-					// count it as an API error
-					d.Metrics.Increment(d.Name + counterResponseErrorsAPI)
-				} else {
-					// otherwise, it's probably a peer error
-					d.Metrics.Increment(d.Name + counterResponseErrorsPeer)
-				}
+				d.Metrics.Increment(d.Name + counterResponseErrors)
 			} else {
 				d.Metrics.Increment(d.Name + counterResponse20x)
 			}


Test patch:
diff --git a/route/otlp_trace_test.go b/route/otlp_trace_test.go
new file mode 100644
index 0000000..2354b29
--- /dev/null
+++ b/route/otlp_trace_test.go
@@ -0,0 +1,348 @@
+package route
+
+import (
+	"bytes"
+	"compress/gzip"
+	"context"
+	"encoding/hex"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/honeycombio/refinery/config"
+	collectortrace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/collector/trace/v1"
+	common "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/common/v1"
+	trace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/trace/v1"
+	"github.com/honeycombio/refinery/logger"
+	"github.com/honeycombio/refinery/metrics"
+	"github.com/honeycombio/refinery/transmit"
+	"github.com/klauspost/compress/zstd"
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc/metadata"
+)
+
+func TestOTLPHandler(t *testing.T) {
+	md := metadata.New(map[string]string{"x-honeycomb-team": "meow", "x-honeycomb-dataset": "ds"})
+	ctx := metadata.NewIncomingContext(context.Background(), md)
+
+	mockMetrics := metrics.MockMetrics{}
+	mockMetrics.Start()
+	mockTransmission := &transmit.MockTransmission{}
+	mockTransmission.Start()
+	decoders, err := makeDecoders(1)
+	if err != nil {
+		t.Error(err)
+	}
+	router := &Router{
+		Config:               &config.MockConfig{},
+		Metrics:              &mockMetrics,
+		UpstreamTransmission: mockTransmission,
+		iopLogger: iopLogger{
+			Logger:         &logger.MockLogger{},
+			incomingOrPeer: "incoming",
+		},
+		Logger:       &logger.MockLogger{},
+		zstdDecoders: decoders,
+	}
+
+	conf := &config.MockConfig{
+		GetSendDelayVal:    0,
+		GetTraceTimeoutVal: 60 * time.Second,
+		GetSamplerTypeVal:  &config.DeterministicSamplerConfig{SampleRate: 1},
+		SendTickerVal:      2 * time.Millisecond,
+		GetInMemoryCollectorCacheCapacityVal: config.InMemoryCollectorCacheCapacity{
+			CacheCapacity: 100,
+			MaxAlloc:      100,
+		},
+	}
+
+	t.Run("span with status", func(t *testing.T) {
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: helperOTLPRequestSpansWithStatus(),
+				}},
+			}},
+		}
+		_, err := router.Export(ctx, req)
+		if err != nil {
+			t.Errorf(`Unexpected error: %s`, err)
+		}
+		assert.Equal(t, 2, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+
+	t.Run("span without status", func(t *testing.T) {
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: helperOTLPRequestSpansWithoutStatus(),
+				}},
+			}},
+		}
+		_, err := router.Export(ctx, req)
+		if err != nil {
+			t.Errorf(`Unexpected error: %s`, err)
+		}
+		assert.Equal(t, 2, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+
+	// TODO: (MG) figuure out how we can test JSON created from OTLP requests
+	// Below is example, but requires significant usage of collector, sampler, conf, etc
+	t.Run("creates events for span events", func(t *testing.T) {
+		t.Skip("need additional work to support inspecting outbound JSON")
+
+		traceID := []byte{0, 0, 0, 0, 1}
+		spanID := []byte{1, 0, 0, 0, 0}
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: []*trace.Span{{
+						TraceId: traceID,
+						SpanId:  spanID,
+						Name:    "span_with_event",
+						Events: []*trace.Span_Event{{
+							TimeUnixNano: 12345,
+							Name:         "span_link",
+							Attributes: []*common.KeyValue{{
+								Key: "event_attr_key", Value: &common.AnyValue{Value: &common.AnyValue_StringValue{StringValue: "event_attr_val"}},
+							}},
+						}},
+					}},
+				}},
+			}},
+		}
+		_, err := router.Export(ctx, req)
+		if err != nil {
+			t.Errorf(`Unexpected error: %s`, err)
+		}
+
+		time.Sleep(conf.SendTickerVal * 2)
+
+		mockTransmission.Mux.Lock()
+		assert.Equal(t, 2, len(mockTransmission.Events))
+
+		spanEvent := mockTransmission.Events[0]
+		// assert.Equal(t, time.Unix(0, int64(12345)).UTC(), spanEvent.Timestamp)
+		assert.Equal(t, bytesToTraceID(traceID), spanEvent.Data["trace.trace_id"])
+		assert.Equal(t, hex.EncodeToString(spanID), spanEvent.Data["trace.span_id"])
+		assert.Equal(t, "span_link", spanEvent.Data["span.name"])
+		assert.Equal(t, "span_with_event", spanEvent.Data["parent.name"])
+		assert.Equal(t, "span_event", spanEvent.Data["meta.annotation_type"])
+		assert.Equal(t, "event_attr_key", spanEvent.Data["event_attr_val"])
+		mockTransmission.Mux.Unlock()
+		mockTransmission.Flush()
+	})
+
+	t.Run("creates events for span links", func(t *testing.T) {
+		t.Skip("need additional work to support inspecting outbound JSON")
+
+		traceID := []byte{0, 0, 0, 0, 1}
+		spanID := []byte{1, 0, 0, 0, 0}
+		linkTraceID := []byte{0, 0, 0, 0, 2}
+		linkSpanID := []byte{2, 0, 0, 0, 0}
+
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: []*trace.Span{{
+						Name:    "span_with_link",
+						TraceId: traceID,
+						SpanId:  spanID,
+						Links: []*trace.Span_Link{{
+							TraceId:    traceID,
+							SpanId:     spanID,
+							TraceState: "link_trace_state",
+							Attributes: []*common.KeyValue{{
+								Key: "link_attr_key", Value: &common.AnyValue{Value: &common.AnyValue_StringValue{StringValue: "link_attr_val"}},
+							}},
+						}},
+					}},
+				}},
+			}},
+		}
+		_, err := router.Export(ctx, req)
+		if err != nil {
+			t.Errorf(`Unexpected error: %s`, err)
+		}
+
+		time.Sleep(conf.SendTickerVal * 2)
+		assert.Equal(t, 2, len(mockTransmission.Events))
+
+		spanLink := mockTransmission.Events[1]
+		assert.Equal(t, bytesToTraceID(traceID), spanLink.Data["trace.trace_id"])
+		assert.Equal(t, hex.EncodeToString(spanID), spanLink.Data["trace.span_id"])
+		assert.Equal(t, bytesToTraceID(linkTraceID), spanLink.Data["trace.link.trace_id"])
+		assert.Equal(t, hex.EncodeToString(linkSpanID), spanLink.Data["trace.link.span_id"])
+		assert.Equal(t, "link", spanLink.Data["meta.annotation_type"])
+		assert.Equal(t, "link_attr_val", spanLink.Data["link_attr_key"])
+		mockTransmission.Flush()
+	})
+
+	t.Run("can receive OTLP over HTTP/protobuf", func(t *testing.T) {
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: helperOTLPRequestSpansWithStatus(),
+				}},
+			}},
+		}
+		body, err := proto.Marshal(req)
+		if err != nil {
+			t.Error(err)
+		}
+
+		request, _ := http.NewRequest("POST", "/v1/traces", strings.NewReader(string(body)))
+		request.Header = http.Header{}
+		request.Header.Set("content-type", "application/protobuf")
+		request.Header.Set("x-honeycomb-team", "apikey")
+		request.Header.Set("x-honeycomb-dataset", "dataset")
+
+		w := httptest.NewRecorder()
+		router.postOTLP(w, request)
+		assert.Equal(t, w.Code, http.StatusOK)
+
+		assert.Equal(t, 2, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+
+	t.Run("can receive OTLP over HTTP/protobuf with gzip encoding", func(t *testing.T) {
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: helperOTLPRequestSpansWithStatus(),
+				}},
+			}},
+		}
+		body, err := proto.Marshal(req)
+		if err != nil {
+			t.Error(err)
+		}
+
+		buf := new(bytes.Buffer)
+		writer := gzip.NewWriter(buf)
+		writer.Write(body)
+		writer.Close()
+		if err != nil {
+			t.Error(err)
+		}
+
+		request, _ := http.NewRequest("POST", "/v1/traces", strings.NewReader(buf.String()))
+		request.Header = http.Header{}
+		request.Header.Set("content-type", "application/protobuf")
+		request.Header.Set("content-encoding", "gzip")
+		request.Header.Set("x-honeycomb-team", "apikey")
+		request.Header.Set("x-honeycomb-dataset", "dataset")
+
+		w := httptest.NewRecorder()
+		router.postOTLP(w, request)
+		assert.Equal(t, w.Code, http.StatusOK)
+
+		assert.Equal(t, 2, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+
+	t.Run("can receive OTLP over HTTP/protobuf with zstd encoding", func(t *testing.T) {
+		req := &collectortrace.ExportTraceServiceRequest{
+			ResourceSpans: []*trace.ResourceSpans{{
+				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
+					Spans: helperOTLPRequestSpansWithStatus(),
+				}},
+			}},
+		}
+		body, err := proto.Marshal(req)
+		if err != nil {
+			t.Error(err)
+		}
+
+		buf := new(bytes.Buffer)
+		writer, err := zstd.NewWriter(buf)
+		if err != nil {
+			t.Error(err)
+		}
+		writer.Write(body)
+		writer.Close()
+		if err != nil {
+			t.Error(err)
+		}
+
+		request, _ := http.NewRequest("POST", "/v1/traces", strings.NewReader(buf.String()))
+		request.Header = http.Header{}
+		request.Header.Set("content-type", "application/protobuf")
+		request.Header.Set("content-encoding", "zstd")
+		request.Header.Set("x-honeycomb-team", "apikey")
+		request.Header.Set("x-honeycomb-dataset", "dataset")
+
+		w := httptest.NewRecorder()
+		router.postOTLP(w, request)
+		assert.Equal(t, w.Code, http.StatusOK)
+
+		assert.Equal(t, 2, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+
+	t.Run("rejects OTLP over HTTP/JSON ", func(t *testing.T) {
+		request, _ := http.NewRequest("POST", "/v1/traces", strings.NewReader("{}"))
+		request.Header = http.Header{}
+		request.Header.Set("content-type", "application/json")
+		request.Header.Set("x-honeycomb-team", "apikey")
+		request.Header.Set("x-honeycomb-dataset", "dataset")
+
+		w := httptest.NewRecorder()
+		router.postOTLP(w, request)
+		assert.Equal(t, w.Code, http.StatusNotImplemented)
+		assert.Equal(t, `{"source":"refinery","error":"invalid content-type - only 'application/protobuf' is supported"}`, string(w.Body.String()))
+
+		assert.Equal(t, 0, len(mockTransmission.Events))
+		mockTransmission.Flush()
+	})
+}
+
+func helperOTLPRequestSpansWithoutStatus() []*trace.Span {
+	now := time.Now()
+	return []*trace.Span{
+		{
+			StartTimeUnixNano: uint64(now.UnixNano()),
+			Events: []*trace.Span_Event{
+				{
+					TimeUnixNano: uint64(now.UnixNano()),
+					Attributes: []*common.KeyValue{
+						{
+							Key: "attribute_key",
+							Value: &common.AnyValue{
+								Value: &common.AnyValue_StringValue{StringValue: "attribute_value"},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func helperOTLPRequestSpansWithStatus() []*trace.Span {
+	now := time.Now()
+	return []*trace.Span{
+		{
+			StartTimeUnixNano: uint64(now.UnixNano()),
+			Events: []*trace.Span_Event{
+				{
+					TimeUnixNano: uint64(now.UnixNano()),
+					Attributes: []*common.KeyValue{
+						{
+							Key: "attribute_key",
+							Value: &common.AnyValue{
+								Value: &common.AnyValue_StringValue{StringValue: "attribute_value"},
+							},
+						},
+					},
+				},
+			},
+			Status: &trace.Status{Code: trace.Status_STATUS_CODE_OK},
+		},
+	}
+}
diff --git a/route/route_test.go b/route/route_test.go
index 8cb5beb..2080396 100644
--- a/route/route_test.go
+++ b/route/route_test.go
@@ -1,11 +1,8 @@
-// +build all race
-
 package route
 
 import (
 	"bytes"
 	"compress/gzip"
-	"context"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -19,9 +16,6 @@ import (
 	"github.com/facebookgo/inject"
 	"github.com/honeycombio/refinery/collect"
 	"github.com/honeycombio/refinery/config"
-	collectortrace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/collector/trace/v1"
-	common "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/common/v1"
-	trace "github.com/honeycombio/refinery/internal/opentelemetry-proto-gen/trace/v1"
 	"github.com/honeycombio/refinery/logger"
 	"github.com/honeycombio/refinery/metrics"
 	"github.com/honeycombio/refinery/transmit"
@@ -399,51 +393,6 @@ func TestDebugTrace(t *testing.T) {
 	}
 }
 
-func TestOTLPHandler(t *testing.T) {
-	md := metadata.New(map[string]string{"x-honeycomb-team": "meow", "x-honeycomb-dataset": "ds"})
-	ctx := metadata.NewIncomingContext(context.Background(), md)
-
-	mockMetrics := metrics.MockMetrics{}
-	mockMetrics.Start()
-	router := &Router{
-		Config:               &config.MockConfig{},
-		Metrics:              &mockMetrics,
-		UpstreamTransmission: &transmit.MockTransmission{},
-		iopLogger: iopLogger{
-			Logger:         &logger.MockLogger{},
-			incomingOrPeer: "incoming",
-		},
-	}
-
-	t.Run("span with status", func(t *testing.T) {
-		req := &collectortrace.ExportTraceServiceRequest{
-			ResourceSpans: []*trace.ResourceSpans{{
-				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
-					Spans: helperOTLPRequestSpansWithStatus(),
-				}},
-			}},
-		}
-		_, err := router.Export(ctx, req)
-		if err != nil {
-			t.Errorf(`Unexpected error: %s`, err)
-		}
-	})
-
-	t.Run("span without status", func(t *testing.T) {
-		req := &collectortrace.ExportTraceServiceRequest{
-			ResourceSpans: []*trace.ResourceSpans{{
-				InstrumentationLibrarySpans: []*trace.InstrumentationLibrarySpans{{
-					Spans: helperOTLPRequestSpansWithoutStatus(),
-				}},
-			}},
-		}
-		_, err := router.Export(ctx, req)
-		if err != nil {
-			t.Errorf(`Unexpected error: %s`, err)
-		}
-	})
-}
-
 func TestDependencyInjection(t *testing.T) {
 	var g inject.Graph
 	err := g.Provide(
@@ -466,51 +415,6 @@ func TestDependencyInjection(t *testing.T) {
 	}
 }
 
-func helperOTLPRequestSpansWithoutStatus() []*trace.Span {
-	now := time.Now()
-	return []*trace.Span{
-		{
-			StartTimeUnixNano: uint64(now.UnixNano()),
-			Events: []*trace.Span_Event{
-				{
-					TimeUnixNano: uint64(now.UnixNano()),
-					Attributes: []*common.KeyValue{
-						{
-							Key: "attribute_key",
-							Value: &common.AnyValue{
-								Value: &common.AnyValue_StringValue{StringValue: "attribute_value"},
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-}
-
-func helperOTLPRequestSpansWithStatus() []*trace.Span {
-	now := time.Now()
-	return []*trace.Span{
-		{
-			StartTimeUnixNano: uint64(now.UnixNano()),
-			Events: []*trace.Span_Event{
-				{
-					TimeUnixNano: uint64(now.UnixNano()),
-					Attributes: []*common.KeyValue{
-						{
-							Key: "attribute_key",
-							Value: &common.AnyValue{
-								Value: &common.AnyValue_StringValue{StringValue: "attribute_value"},
-							},
-						},
-					},
-				},
-			},
-			Status: &trace.Status{Code: trace.Status_STATUS_CODE_OK},
-		},
-	}
-}
-
 type TestSharder struct{}
 
 func (s *TestSharder) MyShard() sharder.Shard { return nil }
diff --git a/sample/rules_test.go b/sample/rules_test.go
index 7add1ae..2965c70 100644
--- a/sample/rules_test.go
+++ b/sample/rules_test.go
@@ -439,6 +439,34 @@ func TestRules(t *testing.T) {
 			ExpectedKeep: true,
 			ExpectedRate: 4,
 		},
+		{
+			Rules: &config.RulesBasedSamplerConfig{
+				Rule: []*config.RulesBasedSamplerRule{
+					{
+						Name:       "does not contain test",
+						SampleRate: 4,
+						Condition: []*config.RulesBasedSamplerCondition{
+							{
+								Field:    "first",
+								Operator: "does-not-contain",
+								Value:    "noteyco",
+							},
+						},
+					},
+				},
+			},
+			Spans: []*types.Span{
+				{
+					Event: types.Event{
+						Data: map[string]interface{}{
+							"first": "honeycomb",
+						},
+					},
+				},
+			},
+			ExpectedKeep: true,
+			ExpectedRate: 4,
+		},
 		{
 			Rules: &config.RulesBasedSamplerConfig{
 				Rule: []*config.RulesBasedSamplerRule{