diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 5b1ed94..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,45 +0,0 @@ -version: 2.1 - -jobs: - test: - docker: - - image: cimg/go:1.18 - steps: - - checkout - - restore_cache: - keys: - - v1-go-mod-{{ checksum "go.sum" }} - - run: make test - - store_test_results: - path: ./unit-tests.xml - - save_cache: - key: v1-go-mod-{{ checksum "go.sum" }} - paths: - - /home/circleci/go/pkg/mod - - publish_github: - docker: - - image: cibuilds/github:0.13.0 - steps: - - run: - name: "Publish Release on GitHub" - command: | - echo "Creating GitHub release for tag ${CIRCLE_TAG}" - ghr -draft -n ${CIRCLE_TAG} -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} - -workflows: - build: - jobs: - - test: - filters: - tags: - only: /.*/ - - publish_github: - context: Honeycomb Secrets for Public Repos - requires: - - test - filters: - tags: - only: /^v[0-9].*/ - branches: - ignore: /.*/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b69494a..b4a6d1c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # This file controls who is tagged for review for any given pull request. # For anything not explicitly taken by someone else: -* @honeycombio/collection-team +* @jayanth-tatina-groww @souravde diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 242bc4f..44ff155 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,7 +12,7 @@ updates: labels: - "type: dependencies" reviewers: - - "honeycombio/collection-team" + - "honeycombio/pipeline" commit-message: prefix: "maint" include: "scope" diff --git a/.github/release.yml b/.github/release.yml index 3d9ee33..a3ad245 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -18,6 +18,8 @@ changelog: - title: 🛠 Maintenance labels: - "type: maintenance" + - "type: dependencies" + - "type: documentation" - title: 🤷 Other Changes labels: - "*" \ No newline at end of file diff --git a/.github/workflows/add-to-project-v2.yml b/.github/workflows/add-to-project-v2.yml deleted file mode 100644 index af6f108..0000000 --- a/.github/workflows/add-to-project-v2.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Add to project -on: - issues: - types: [opened] - pull_request_target: - types: [opened] -jobs: - add-to-project: - runs-on: ubuntu-latest - name: Add issues and PRs to project - steps: - - uses: actions/add-to-project@main - with: - project-url: https://github.com/orgs/honeycombio/projects/14 - github-token: ${{ secrets.GHPROJECTS_TOKEN }} diff --git a/.gitignore b/.gitignore index 4c9c144..5354adb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ unit-tests.xml + +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 478e85d..ae40fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Husky Changelog +## 0.23.1 2023-12-08 + +- fix: bug where we could error after writing a status code (#224) | [Tyler Helmuth](https://github.com/TylerHelmuth) +- maint: add deps and docs to maintenance in release (#223) | [Jamie Danielson](https://github.com/JamieDanielson) +- maint: add extra detail to release doc (#222) | [Jamie Danielson](https://github.com/JamieDanielson) + +## 0.23.0 2023-12-08 + +- feat: Add public functions for handling OTLP HTTP responses (#219) | [Tyler Helmuth](https://github.com/TylerHelmuth) +- maint: update codeowners (#220) | [Tyler Helmuth](https://github.com/TylerHelmuth) +- maint(deps): bump google.golang.org/grpc from 1.58.3 to 1.59.0 (#218) +- maint(deps): bump github.com/klauspost/compress from 1.17.2 to 1.17.4 (#217) +- maint(deps): bump google.golang.org/grpc from 1.58.2 to 1.58.3 (#215) +- maint(deps): bump golang.org/x/net from 0.12.0 to 0.17.0 (#214) +- maint: bump github.com/klauspost/compress from 1.16.7 to 1.17.2 (#213) | [Tyler Helmuth](https://github.com/TylerHelmuth) +- maint(deps): bump google.golang.org/grpc from 1.56.1 to 1.58.2 (#210) +- maint(deps): bump github.com/klauspost/compress from 1.16.5 to 1.16.7 (#204) +- maint(deps): bump google.golang.org/grpc from 1.55.0 to 1.56.1 (#202) +- maint(deps): bump google.golang.org/protobuf from 1.30.0 to 1.31.0 (#203) +- maint(deps): bump google.golang.org/grpc from 1.54.0 to 1.55.0 (#200) +- maint(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#199) + + ## 0.22.4 2023-05-16 fix: Send the values not the Values in exception details (#197) | [Kent Quirk](https://github.com/kentquirk) diff --git a/RELEASING.md b/RELEASING.md index 235ace3..cae477e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,9 +1,12 @@ # Release Process -1. Update [Version.go](version.go) with new version -2. Add release entry to [changelog](./CHANGELOG.md). Consider using a command like so: - * `git log --pretty='%C(green)%d%Creset- %s | [%an](https://github.com/)'` -3. Open a PR with the above, and merge that into main -4. Create new tag on merged commit with the new version (e.g. `v1.4.1`) -5. Push the tag upstream (this will kick off the release pipeline in CI) -6. Copy change log entry for newest version into draft GitHub release created as part of CI publish steps +- Update [`version.go`](version.go) with new version +- Add release entry to [changelog](./CHANGELOG.md). Consider using a command like so: + - `git log --pretty='%C(green)%d%Creset- %s | [%an](https://github.com/)'` +- Commit changes, push, and open a release preparation pull request for review. +- Once the pull request is merged, fetch the updated `main` branch. +- Apply a tag for the new version on the merged commit (e.g. `git tag -a v2.3.1 -m "v2.3.1"`) +- Push the tag upstream (this will kick off the release pipeline in CI) e.g. `git push origin v2.3.1` +- Ensure that there is a draft GitHub release created as part of CI publish steps +- Click "generate release notes" in github for full changelog notes and any new contributors +- Publish the github draft release diff --git a/go.mod b/go.mod index 7ba32d3..2a10fbd 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/honeycombio/husky -go 1.18 +go 1.21 require ( github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.16.7 + github.com/klauspost/compress v1.17.4 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/proto/otlp v0.19.0 - google.golang.org/grpc v1.56.1 + google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 ) @@ -19,10 +19,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0728981..bfc1ca2 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,14 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= @@ -15,9 +17,10 @@ github.com/honeycombio/opentelemetry-proto-go/otlp v0.19.0-compat h1:fMpIzVAl5C2 github.com/honeycombio/opentelemetry-proto-go/otlp v0.19.0-compat/go.mod h1:mC2aK20Z/exugKpqCgcpwEadiS0im8K6mZsD4Is/hCY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= @@ -30,22 +33,27 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/otlp/common.go b/otlp/common.go index 4c15230..cbe0e3b 100644 --- a/otlp/common.go +++ b/otlp/common.go @@ -4,6 +4,9 @@ import ( "bytes" "compress/gzip" "context" + "encoding/base64" + "encoding/hex" + "fmt" "io" "math" "net/http" @@ -13,8 +16,12 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/klauspost/compress/zstd" + collectorlogs "go.opentelemetry.io/proto/otlp/collector/logs/v1" + collectormetrics "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + collectortrace "go.opentelemetry.io/proto/otlp/collector/trace/v1" common "go.opentelemetry.io/proto/otlp/common/v1" resource "go.opentelemetry.io/proto/otlp/resource/v1" + spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" @@ -190,6 +197,61 @@ func GetRequestInfoFromHttpHeaders(header http.Header) RequestInfo { } } +// WriteOtlpHttpFailureResponse is a quick way to write an otlp response for an error. +// It calls WriteOtlpHttpResponse, using the error's HttpStatusCode and building a Status +// using the error's string. +func WriteOtlpHttpFailureResponse(w http.ResponseWriter, r *http.Request, err OTLPError) error { + return WriteOtlpHttpResponse(w, r, err.HTTPStatusCode, &spb.Status{Message: err.Error()}) +} + +// WriteOtlpHttpTraceSuccessResponse is a quick way to write an otlp success response for a trace request. +// It calls WriteOtlpHttpResponse, using the 200 status code and an empty ExportTraceServiceResponse +func WriteOtlpHttpTraceSuccessResponse(w http.ResponseWriter, r *http.Request) error { + return WriteOtlpHttpResponse(w, r, http.StatusOK, &collectortrace.ExportTraceServiceResponse{}) +} + +// WriteOtlpHttpMetricSuccessResponse is a quick way to write an otlp success response for a metric request. +// It calls WriteOtlpHttpResponse, using the 200 status code and an empty ExportMetricsServiceResponse +func WriteOtlpHttpMetricSuccessResponse(w http.ResponseWriter, r *http.Request) error { + return WriteOtlpHttpResponse(w, r, http.StatusOK, &collectormetrics.ExportMetricsServiceResponse{}) +} + +// WriteOtlpHttpLogSuccessResponse is a quick way to write an otlp success response for a trace request. +// It calls WriteOtlpHttpResponse, using the 200 status code and an empty ExportLogsServiceResponse +func WriteOtlpHttpLogSuccessResponse(w http.ResponseWriter, r *http.Request) error { + return WriteOtlpHttpResponse(w, r, http.StatusOK, &collectorlogs.ExportLogsServiceResponse{}) +} + +// WriteOtlpHttpResponse writes a compliant OTLP HTTP response to the given http.ResponseWriter +// based on the provided `contentType`. If an error occurs while marshalling to either json or proto it is returned +// before the http.ResponseWriter is updated. If an error occurs while writing to the http.ResponseWriter it is ignored. +func WriteOtlpHttpResponse(w http.ResponseWriter, r *http.Request, statusCode int, m proto.Message) error { + if r == nil { + return fmt.Errorf("nil Request") + } + + contentType := r.Header.Get("Content-Type") + var body []byte + var err error + switch contentType { + case "application/json": + body, err = protojson.Marshal(m) + case "application/x-protobuf", "application/protobuf": + body, err = proto.Marshal(m) + default: + return ErrInvalidContentType + } + if err != nil { + return err + } + + // At this point we're committed + w.Header().Set("Content-Type", contentType) + w.WriteHeader(statusCode) + _, _ = w.Write(body) + return nil +} + func getValueFromMetadata(md metadata.MD, key string) string { if vals := md.Get(key); len(vals) > 0 { return vals[0] @@ -421,3 +483,62 @@ func parseOtlpRequestBody(body io.ReadCloser, contentType string, contentEncodin return nil } + +// 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. +// There are many ways to achieve this, but careful benchmarking and testing +// showed the below as the most performant, avoiding memory allocations +// and the use of flexible but expensive library functions. As this is hot code, +// it seemed worthwhile to do it this way. +func BytesToTraceID(traceID []byte) string { + var encoded []byte + switch len(traceID) { + case traceIDLongLength: // 16 bytes, trim leading 8 bytes if all 0's + if shouldTrimTraceId(traceID) { + encoded = make([]byte, 16) + traceID = traceID[traceIDShortLength:] + } else { + encoded = make([]byte, 32) + } + hex.Encode(encoded, traceID) + case traceIDShortLength: // 8 bytes + encoded = make([]byte, 16) + hex.Encode(encoded, traceID) + case traceIDb64Length: // 24 bytes + // The spec says that traceID and spanID should be encoded as hex, but + // the protobuf system is interpreting them as b64, so we need to + // reverse them back to b64 which gives us the original hex. + encoded = make([]byte, base64.StdEncoding.EncodedLen(len(traceID))) + base64.StdEncoding.Encode(encoded, traceID) + default: + encoded = make([]byte, len(traceID)*2) + hex.Encode(encoded, traceID) + } + return string(encoded) +} + +func BytesToSpanID(spanID []byte) string { + var encoded []byte + switch len(spanID) { + case spanIDb64Length: // 12 bytes + // The spec says that traceID and spanID should be encoded as hex, but + // the protobuf system is interpreting them as b64, so we need to + // reverse them back to b64 which gives us the original hex. + encoded = make([]byte, base64.StdEncoding.EncodedLen(len(spanID))) + base64.StdEncoding.Encode(encoded, spanID) + default: + encoded = make([]byte, len(spanID)*2) + hex.Encode(encoded, spanID) + } + return string(encoded) +} + +func shouldTrimTraceId(traceID []byte) bool { + for i := 0; i < 8; i++ { + if traceID[i] != 0 { + return false + } + } + return true +} diff --git a/otlp/common_test.go b/otlp/common_test.go index 3f5938c..d591d60 100644 --- a/otlp/common_test.go +++ b/otlp/common_test.go @@ -2,14 +2,24 @@ package otlp import ( "context" + "encoding/base64" + "encoding/hex" + "io" "net/http" + "net/http/httptest" "reflect" "strings" "testing" "github.com/stretchr/testify/assert" + collectorlogs "go.opentelemetry.io/proto/otlp/collector/logs/v1" + collectormetrics "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + collectortrace "go.opentelemetry.io/proto/otlp/collector/trace/v1" common "go.opentelemetry.io/proto/otlp/common/v1" + spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) func TestParseGrpcMetadataIntoRequestInfo(t *testing.T) { @@ -420,3 +430,326 @@ func Test_limitedWriter(t *testing.T) { }) } } + +func Test_WriteOtlpHttpFailureResponse(t *testing.T) { + tests := []struct { + contentType string + err OTLPError + expectedError error + }{ + { + contentType: "application/x-protobuf", + err: OTLPError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "test", + }, + }, + { + contentType: "application/protobuf", + err: OTLPError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "test", + }, + }, + { + contentType: "application/json", + err: OTLPError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "test", + }, + }, + { + contentType: "nonsense", + err: OTLPError{ + HTTPStatusCode: http.StatusBadRequest, + Message: "test", + }, + expectedError: ErrInvalidContentType, + }, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r.Header.Set("Content-Type", tt.contentType) + + err := WriteOtlpHttpFailureResponse(w, r, tt.err) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + + assert.Equal(t, tt.contentType, w.Header().Get("Content-Type")) + assert.Equal(t, tt.err.HTTPStatusCode, w.Code) + + data, err := io.ReadAll(w.Body) + assert.NoError(t, err) + var result spb.Status + if tt.contentType == "application/json" { + err = protojson.Unmarshal(data, &result) + assert.NoError(t, err) + } else { + err = proto.Unmarshal(data, &result) + assert.NoError(t, err) + } + assert.Equal(t, tt.err.Message, result.Message) + } + }) + } +} + +func Test_BytesToTraceID(t *testing.T) { + tests := []struct { + name string + traceID string + b64 bool + want string + }{ + { + name: "64-bit traceID", + traceID: "cbe4decd12429177", + want: "cbe4decd12429177", + }, + { + name: "128-bit zero-padded traceID", + traceID: "0000000000000000cbe4decd12429177", + want: "cbe4decd12429177", + }, + { + name: "128-bit non-zero-padded traceID", + traceID: "f23b42eac289a0fdcde48fcbe3ab1a32", + want: "f23b42eac289a0fdcde48fcbe3ab1a32", + }, + { + name: "Non-hex traceID", + traceID: "foobar1", + want: "666f6f62617231", + }, + { + name: "Longer non-hex traceID", + traceID: "foobarbaz", + want: "666f6f62617262617a", + }, + { + name: "traceID munged by browser", + traceID: "6e994e8673e93a51200c137330aeddad", + b64: true, + want: "6e994e8673e93a51200c137330aeddad", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var traceID []byte + var err error + if tt.b64 { + traceID, err = base64.StdEncoding.DecodeString(tt.traceID) + } else { + traceID, err = hex.DecodeString(tt.traceID) + } + if err != nil { + traceID = []byte(tt.traceID) + } + got := BytesToTraceID(traceID) + if got != tt.want { + t.Errorf("got: %#v\n\twant: %#v", got, tt.want) + } + }) + } +} + +func Test_WriteOtlpHttpTraceSuccessResponse(t *testing.T) { + tests := []struct { + contentType string + expectedError error + }{ + { + contentType: "application/x-protobuf", + }, + { + contentType: "application/protobuf", + }, + { + contentType: "application/json", + }, + { + contentType: "nonsense", + expectedError: ErrInvalidContentType, + }, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r.Header.Set("Content-Type", tt.contentType) + + err := WriteOtlpHttpTraceSuccessResponse(w, r) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + + assert.Equal(t, tt.contentType, w.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, w.Code) + + data, err := io.ReadAll(w.Body) + assert.NoError(t, err) + var result collectortrace.ExportTraceServiceResponse + if tt.contentType == "application/json" { + err = protojson.Unmarshal(data, &result) + assert.NoError(t, err) + } else { + err = proto.Unmarshal(data, &result) + assert.NoError(t, err) + } + assert.Nil(t, result.GetPartialSuccess()) + } + }) + } +} + +func Test_WriteOtlpHttpMetricSuccessResponse(t *testing.T) { + tests := []struct { + contentType string + expectedError error + }{ + { + contentType: "application/x-protobuf", + }, + { + contentType: "application/protobuf", + }, + { + contentType: "application/json", + }, + { + contentType: "nonsense", + expectedError: ErrInvalidContentType, + }, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r.Header.Set("Content-Type", tt.contentType) + + err := WriteOtlpHttpMetricSuccessResponse(w, r) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + + assert.Equal(t, tt.contentType, w.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, w.Code) + + data, err := io.ReadAll(w.Body) + assert.NoError(t, err) + var result collectormetrics.ExportMetricsServiceResponse + if tt.contentType == "application/json" { + err = protojson.Unmarshal(data, &result) + assert.NoError(t, err) + } else { + err = proto.Unmarshal(data, &result) + assert.NoError(t, err) + } + assert.Nil(t, result.GetPartialSuccess()) + } + }) + } +} + +func Test_WriteOtlpHttpLogSuccessResponse(t *testing.T) { + tests := []struct { + contentType string + expectedError error + }{ + { + contentType: "application/x-protobuf", + }, + { + contentType: "application/protobuf", + }, + { + contentType: "application/json", + }, + { + contentType: "nonsense", + expectedError: ErrInvalidContentType, + }, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r.Header.Set("Content-Type", tt.contentType) + + err := WriteOtlpHttpLogSuccessResponse(w, r) + if tt.expectedError != nil { + assert.Equal(t, tt.expectedError, err) + } else { + assert.NoError(t, err) + + assert.Equal(t, tt.contentType, w.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, w.Code) + + data, err := io.ReadAll(w.Body) + assert.NoError(t, err) + var result collectorlogs.ExportLogsServiceResponse + if tt.contentType == "application/json" { + err = protojson.Unmarshal(data, &result) + assert.NoError(t, err) + } else { + err = proto.Unmarshal(data, &result) + assert.NoError(t, err) + } + assert.Nil(t, result.GetPartialSuccess()) + } + }) + } +} + +func Test_BytesToSpanID(t *testing.T) { + tests := []struct { + name string + spanID string + b64 bool + want string + }{ + { + name: "spanID", + spanID: "890452a577ef2e0f", + want: "890452a577ef2e0f", + }, + { + name: "spanID munged by browser (converted in this test)", + spanID: "890452a577ef2e0f", + b64: true, + want: "890452a577ef2e0f", + }, + { + name: "spanID munged by browser (from a bad trace)", + spanID: "e77ddbeb7f7adf77fbd396b9", + b64: false, + want: "533b639633f705a5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var spanID []byte + var err error + if tt.b64 { + spanID, err = base64.StdEncoding.DecodeString(tt.spanID) + } else { + spanID, err = hex.DecodeString(tt.spanID) + } + if err != nil { + spanID = []byte(tt.spanID) + } + got := BytesToSpanID(spanID) + if got != tt.want { + t.Errorf("got: %#v\n\twant: %#v", got, tt.want) + } + }) + } +} diff --git a/otlp/logs.go b/otlp/logs.go index 0d79a52..6734bb9 100644 --- a/otlp/logs.go +++ b/otlp/logs.go @@ -1,7 +1,6 @@ package otlp import ( - "encoding/hex" "io" "time" @@ -51,7 +50,7 @@ func TranslateLogsRequest(request *collectorLogs.ExportLogsServiceRequest, ri Re attrs["meta.annotation_type"] = "span_event" } if len(log.SpanId) > 0 { - attrs["trace.parent_id"] = hex.EncodeToString(log.SpanId) + attrs["trace.parent_id"] = BytesToSpanID(log.SpanId) } if log.SeverityText != "" { attrs["severity_text"] = log.SeverityText diff --git a/otlp/traces.go b/otlp/traces.go index 7218473..2c64048 100644 --- a/otlp/traces.go +++ b/otlp/traces.go @@ -1,7 +1,6 @@ package otlp import ( - "encoding/base64" "encoding/hex" "io" "math" @@ -51,7 +50,7 @@ func TranslateTraceRequest(request *collectorTrace.ExportTraceServiceRequest, ri for _, span := range scopeSpan.GetSpans() { traceID := BytesToTraceID(span.TraceId) - spanID := bytesToSpanID(span.SpanId) + spanID := BytesToSpanID(span.SpanId) spanKind := getSpanKind(span.Kind) statusCode, isError := getSpanStatusCode(span.Status) @@ -69,7 +68,7 @@ func TranslateTraceRequest(request *collectorTrace.ExportTraceServiceRequest, ri "meta.signal_type": "trace", } if span.ParentSpanId != nil { - eventAttrs["trace.parent_id"] = bytesToSpanID(span.ParentSpanId) + eventAttrs["trace.parent_id"] = BytesToSpanID(span.ParentSpanId) } if isError { eventAttrs["error"] = true @@ -223,65 +222,6 @@ func getSpanKind(kind trace.Span_SpanKind) string { } } -// 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. -// There are many ways to achieve this, but careful benchmarking and testing -// showed the below as the most performant, avoiding memory allocations -// and the use of flexible but expensive library functions. As this is hot code, -// it seemed worthwhile to do it this way. -func BytesToTraceID(traceID []byte) string { - var encoded []byte - switch len(traceID) { - case traceIDLongLength: // 16 bytes, trim leading 8 bytes if all 0's - if shouldTrimTraceId(traceID) { - encoded = make([]byte, 16) - traceID = traceID[traceIDShortLength:] - } else { - encoded = make([]byte, 32) - } - hex.Encode(encoded, traceID) - case traceIDShortLength: // 8 bytes - encoded = make([]byte, 16) - hex.Encode(encoded, traceID) - case traceIDb64Length: // 24 bytes - // The spec says that traceID and spanID should be encoded as hex, but - // the protobuf system is interpreting them as b64, so we need to - // reverse them back to b64 and then reencode as hex. - encoded = make([]byte, base64.StdEncoding.EncodedLen(len(traceID))) - base64.StdEncoding.Encode(encoded, traceID) - default: - encoded = make([]byte, len(traceID)*2) - hex.Encode(encoded, traceID) - } - return string(encoded) -} - -func bytesToSpanID(spanID []byte) string { - var encoded []byte - switch len(spanID) { - case spanIDb64Length: // 12 bytes - // The spec says that traceID and spanID should be encoded as hex, but - // the protobuf system is interpreting them as b64, so we need to - // reverse them back to b64 and then reencode as hex. - encoded = make([]byte, base64.StdEncoding.EncodedLen(len(spanID))) - base64.StdEncoding.Encode(encoded, spanID) - default: - encoded = make([]byte, len(spanID)*2) - hex.Encode(encoded, spanID) - } - return string(encoded) -} - -func shouldTrimTraceId(traceID []byte) bool { - for i := 0; i < 8; i++ { - if traceID[i] != 0 { - return false - } - } - return true -} - // getSpanStatusCode returns the integer value of the span's status code and // a bool for whether to consider the status an error. // diff --git a/otlp/traces_test.go b/otlp/traces_test.go index 6ed61f7..b86a9a1 100644 --- a/otlp/traces_test.go +++ b/otlp/traces_test.go @@ -2,7 +2,6 @@ package otlp import ( "bytes" - "encoding/base64" "encoding/hex" "io" "math" @@ -1229,103 +1228,3 @@ func TestKnownInstrumentationPrefixesReturnTrue(t *testing.T) { }) } } - -func Test_BytesToTraceID(t *testing.T) { - tests := []struct { - name string - traceID string - b64 bool - want string - }{ - { - name: "64-bit traceID", - traceID: "cbe4decd12429177", - want: "cbe4decd12429177", - }, - { - name: "128-bit zero-padded traceID", - traceID: "0000000000000000cbe4decd12429177", - want: "cbe4decd12429177", - }, - { - name: "128-bit non-zero-padded traceID", - traceID: "f23b42eac289a0fdcde48fcbe3ab1a32", - want: "f23b42eac289a0fdcde48fcbe3ab1a32", - }, - { - name: "Non-hex traceID", - traceID: "foobar1", - want: "666f6f62617231", - }, - { - name: "Longer non-hex traceID", - traceID: "foobarbaz", - want: "666f6f62617262617a", - }, - { - name: "traceID munged by browser", - traceID: "6e994e8673e93a51200c137330aeddad", - b64: true, - want: "6e994e8673e93a51200c137330aeddad", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var traceID []byte - var err error - if tt.b64 { - traceID, err = base64.StdEncoding.DecodeString(tt.traceID) - } else { - traceID, err = hex.DecodeString(tt.traceID) - } - if err != nil { - traceID = []byte(tt.traceID) - } - got := BytesToTraceID(traceID) - if got != tt.want { - t.Errorf("got: %#v\n\twant: %#v", got, tt.want) - } - }) - } -} - -func Test_BytesToSpanID(t *testing.T) { - tests := []struct { - name string - spanID string - b64 bool - want string - }{ - { - name: "spanID", - spanID: "890452a577ef2e0f", - want: "890452a577ef2e0f", - }, - { - name: "spanID munged by browser", - spanID: "890452a577ef2e0f", - b64: true, - want: "890452a577ef2e0f", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var spanID []byte - var err error - if tt.b64 { - spanID, err = base64.StdEncoding.DecodeString(tt.spanID) - } else { - spanID, err = hex.DecodeString(tt.spanID) - } - if err != nil { - spanID = []byte(tt.spanID) - } - got := bytesToSpanID(spanID) - if got != tt.want { - t.Errorf("got: %#v\n\twant: %#v", got, tt.want) - } - }) - } -} diff --git a/version.go b/version.go index f677bb4..2a77c6d 100644 --- a/version.go +++ b/version.go @@ -1,5 +1,5 @@ package husky var ( - Version string = "1.0.1" + Version string = "1.0.2" )