├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── workflows │ ├── add-to-project.yml │ ├── apply-labels.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── OSSMETADATA ├── README.md ├── VERSION ├── example ├── README.md ├── client │ └── client.go └── server │ ├── handler.go │ └── server.go ├── go.mod ├── go.sum └── honeycomb ├── honeycomb.go ├── honeycomb_test.go ├── translator.go └── translator_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | go: 5 | parameters: 6 | goversion: 7 | type: string 8 | default: "15" 9 | docker: 10 | - image: circleci/golang:1.<< parameters.goversion >> 11 | environment: 12 | GO111MODULE: "on" 13 | working_directory: /go/src/github.com/honeycombio/opentelemetry-exporter-go 14 | 15 | jobs: 16 | test_opentelemetry: 17 | parameters: 18 | goversion: 19 | type: string 20 | default: "14" 21 | executor: 22 | name: go 23 | goversion: "<< parameters.goversion >>" 24 | steps: 25 | - checkout 26 | - run: go get -v -t -d ./... 27 | - run: go test -race -v ./... 28 | 29 | workflows: 30 | build: 31 | jobs: 32 | - test_opentelemetry: 33 | goversion: "14" 34 | - test_opentelemetry: 35 | goversion: "15" 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | 4 | # For anything not explicitly taken by someone else: 5 | * @honeycombio/telemetry-team 6 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Apply project management flow 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request_target: 6 | types: [opened] 7 | jobs: 8 | project-management: 9 | runs-on: ubuntu-latest 10 | name: Apply project management flow 11 | steps: 12 | - uses: honeycombio/oss-management-actions/projects@v1 13 | with: 14 | ghprojects-token: ${{ secrets.GHPROJECTS_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/apply-labels.yml: -------------------------------------------------------------------------------- 1 | name: Apply project labels 2 | on: [issues, pull_request_target, label] 3 | jobs: 4 | apply-labels: 5 | runs-on: ubuntu-latest 6 | name: Apply common project labels 7 | steps: 8 | - uses: honeycombio/oss-management-actions/labels@v1 9 | with: 10 | github-token: ${{ secrets.GITHUB_TOKEN }} 11 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | name: 'Close stale issues and PRs' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | start-date: '2021-09-01T00:00:00Z' 18 | stale-issue-message: 'Marking this issue as stale because it has been open 14 days with no activity. Please add a comment if this is still an ongoing issue; otherwise this issue will be automatically closed in 7 days.' 19 | stale-pr-message: 'Marking this PR as stale because it has been open 30 days with no activity. Please add a comment if this PR is still relevant; otherwise this PR will be automatically closed in 7 days.' 20 | close-issue-message: 'Closing this issue due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 21 | close-pr-message: 'Closing this PR due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 22 | days-before-issue-stale: 14 23 | days-before-pr-stale: 30 24 | days-before-issue-close: 7 25 | days-before-pr-close: 7 26 | any-of-labels: 'status: info needed,status: revision needed' 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## Unreleased 10 | 11 | ## v0.15.0 12 | 13 | * Updated OpenTelemetry SDK version to v0.15.0 14 | 15 | ## v0.14.0 16 | 17 | * Fix Version and update changelog 18 | 19 | ## v0.13.0 20 | 21 | * Updated OpenTelemetry SDK version to v0.13.0 (#95) 22 | 23 | ## v0.12.0 24 | 25 | * Updated OpenTelemetry SDK version to v0.12.0 (#94) 26 | 27 | ## v0.11.0 28 | 29 | * Updated OpenTelemetry SDK version to v0.11.0 (#90) 30 | 31 | ## v0.10.0 32 | 33 | ### Changed 34 | 35 | * Updated OpenTelemetry SDK version to v0.10.0 (#87) 36 | 37 | ## v0.9.0 38 | 39 | ### Added 40 | 41 | * `withHoneycombSender` exporter option for specifying a `libhoney` `transmission.Sender` (#84) 42 | 43 | ### Changed 44 | 45 | * The exporter now creates an isolated `libhoney.Client` instead of using the package-level api. This reduces interactions between multiple libhoney-based instrumentations if they're run in the same process. (#84) 46 | * Updated OpenTelemetry SDK version to v0.9.0 (#85) 47 | 48 | ### Removed 49 | 50 | * `withHoneycombOutput` exporter option. `libhoney`'s `Output` interface is deprecated (in favor of `transmission.Sender` above) and will be removed at some point. (#84) 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Hound Technology Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=archived 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opentelemetry-exporter-go 2 | 3 | # The Honeycomb OpenTelemetry Exporter for Go 4 | 5 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/opentelemetry-exporter-go)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 6 | [![CircleCI](https://circleci.com/gh/honeycombio/opentelemetry-exporter-go.svg?style=svg)](https://circleci.com/gh/honeycombio/opentelemetry-exporter-go) 7 | 8 | **STATUS: this project has been archived.** See https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md 9 | 10 | ## Default Exporter 11 | 12 | The Exporter can be initialized using `sdktrace.WithSyncer`: 13 | 14 | ```golang 15 | exporter, _ := honeycomb.NewExporter( 16 | honeycomb.Config{ 17 | APIKey: , 18 | }, 19 | honeycomb.TargetingDataset(), 20 | honeycomb.WithServiceName("example-server"), 21 | honeycomb.WithDebugEnabled()) // optional to output diagnostic logs to STDOUT 22 | 23 | defer exporter.Shutdown(context.TODO()) 24 | sdktrace.NewProvider( 25 | sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), 26 | sdktrace.WithSyncer(exporter), 27 | ) 28 | ``` 29 | 30 | ## Sampling 31 | 32 | Read more about [sampling with Honeycomb in our docs](https://docs.honeycomb.io/working-with-your-data/tracing/sampling/). 33 | 34 | ## Example 35 | 36 | You can find an example Honeycomb app in [/example](./example). 37 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v0.15.0 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example Apps 2 | 3 | Pass your Honeycomb API key, found on your Honeycomb Team Settings page and your dataset name as flags when running this example. 4 | 5 | ## To run the client/server example: 6 | 7 | In `example/server` run `go install && server -apikey= -dataset=opentelemetry` 8 | 9 | In `example/client` run `go install && client -apikey= -dataset=opentelemetry` 10 | 11 | [Sign up for free](https://ui.honeycomb.io/signup) if you haven’t already! 12 | -------------------------------------------------------------------------------- /example/client/client.go: -------------------------------------------------------------------------------- 1 | // COPIED FROM OPENTELEMETRY CONTRIB HTTP EXAMPLE 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" 15 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 16 | "go.opentelemetry.io/otel" 17 | "go.opentelemetry.io/otel/baggage" 18 | "go.opentelemetry.io/otel/label" 19 | "go.opentelemetry.io/otel/propagation" 20 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 21 | "go.opentelemetry.io/otel/semconv" 22 | "go.opentelemetry.io/otel/trace" 23 | 24 | "github.com/honeycombio/opentelemetry-exporter-go/honeycomb" 25 | ) 26 | 27 | func initTracer(exporter *honeycomb.Exporter) func(context.Context) error { 28 | bsp := sdktrace.NewBatchSpanProcessor(exporter) 29 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(bsp)) 30 | tp.ApplyConfig( 31 | // The default takes parent sampler hints into account, which we don't need here. 32 | sdktrace.Config{ 33 | // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. 34 | // In a production application, use sdktrace.ProbabilitySampler with a desired 35 | // probability. 36 | DefaultSampler: sdktrace.AlwaysSample(), 37 | }) 38 | otel.SetTracerProvider(tp) 39 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( 40 | propagation.TraceContext{}, 41 | propagation.Baggage{})) 42 | return bsp.Shutdown 43 | } 44 | 45 | func main() { 46 | apikey := flag.String("apikey", "", "Your Honeycomb API Key") 47 | dataset := flag.String("dataset", "opentelemetry", "Your Honeycomb dataset") 48 | flag.Parse() 49 | 50 | exporter, err := honeycomb.NewExporter( 51 | honeycomb.Config{ 52 | APIKey: *apikey, 53 | }, 54 | honeycomb.TargetingDataset(*dataset), 55 | honeycomb.WithServiceName("opentelemetry-client"), 56 | honeycomb.WithDebugEnabled()) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | defer exporter.Shutdown(context.Background()) 61 | defer initTracer(exporter)(context.Background()) 62 | tr := otel.Tracer("honeycomb/example/client") 63 | 64 | url := flag.String("server", "http://localhost:7777/hello", "server URL") 65 | flag.Parse() 66 | 67 | ctx := baggage.ContextWithValues(context.Background(), 68 | label.String("username", "donuts")) 69 | 70 | client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} 71 | 72 | ctx, span := tr.Start(ctx, "say hello", 73 | trace.WithAttributes(semconv.PeerServiceKey.String("ExampleService"))) 74 | defer span.End() 75 | 76 | req, err := http.NewRequestWithContext(ctx, "GET", *url, nil) 77 | if err != nil { 78 | panic(err) 79 | } 80 | _, req = otelhttptrace.W3C(ctx, req) 81 | 82 | fmt.Println("Sending request...") 83 | res, err := client.Do(req) 84 | if err != nil { 85 | fmt.Fprintf(os.Stderr, "HTTP request failed: %v\n", err) 86 | } 87 | defer res.Body.Close() 88 | body, err := ioutil.ReadAll(res.Body) 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "Failed to read HTTP response body: %v\n", err) 91 | } 92 | 93 | fmt.Printf("Response received (HTTP status code %d): %s\n\n\n", res.StatusCode, body) 94 | } 95 | -------------------------------------------------------------------------------- /example/server/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "go.opentelemetry.io/otel/baggage" 8 | "go.opentelemetry.io/otel/label" 9 | "go.opentelemetry.io/otel/trace" 10 | ) 11 | 12 | func speakPlainTextTo(w http.ResponseWriter) { 13 | w.Header().Add("Content-Type", "text/plain") 14 | } 15 | 16 | func makeHandler() http.Handler { 17 | userNameKey := label.Key("username") 18 | var mux http.ServeMux 19 | mux.Handle("/hello", 20 | http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 21 | ctx := req.Context() 22 | span := trace.SpanFromContext(ctx) 23 | span.SetAttributes(label.String("ex.com/another", "yes")) 24 | 25 | eventAttrs := make([]label.KeyValue, 1, 2) 26 | eventAttrs[0] = label.Int("request-handled", 100) 27 | userNameVal := baggage.Value(ctx, label.Key("username")) 28 | if userNameVal.Type() != label.INVALID { 29 | attr := label.KeyValue{ 30 | Key: userNameKey, 31 | Value: userNameVal, 32 | } 33 | span.SetAttributes(attr) 34 | eventAttrs = append(eventAttrs, attr) 35 | } 36 | span.AddEvent("handling this...", trace.WithAttributes(eventAttrs...)) 37 | 38 | speakPlainTextTo(w) 39 | _, err := io.WriteString(w, "Hello, world!\n") 40 | span.RecordError(err) 41 | })) 42 | return &mux 43 | } 44 | -------------------------------------------------------------------------------- /example/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, OpenTelemetry Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "log" 21 | "net" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "sync" 26 | "syscall" 27 | 28 | "github.com/honeycombio/opentelemetry-exporter-go/honeycomb" 29 | 30 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 31 | "go.opentelemetry.io/otel" 32 | "go.opentelemetry.io/otel/propagation" 33 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 34 | ) 35 | 36 | func initTracer(exporter *honeycomb.Exporter) func(context.Context) error { 37 | bsp := sdktrace.NewBatchSpanProcessor(exporter) 38 | tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(bsp)) 39 | tp.ApplyConfig( 40 | // The default takes parent sampler hints into account, which we don't need here. 41 | sdktrace.Config{ 42 | // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. 43 | // In a production application, use sdktrace.ProbabilitySampler with a desired 44 | // probability. 45 | DefaultSampler: sdktrace.AlwaysSample(), 46 | }) 47 | otel.SetTracerProvider(tp) 48 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( 49 | propagation.TraceContext{}, 50 | propagation.Baggage{})) 51 | return bsp.Shutdown 52 | } 53 | 54 | func joinIPAddressAndPort(address net.IP, port string) string { 55 | var host string 56 | var empty net.IP 57 | if !address.Equal(empty) { 58 | host = address.String() 59 | } 60 | return net.JoinHostPort(host, port) 61 | } 62 | 63 | func runHTTPServer(address net.IP, port string, handler http.Handler, stop <-chan struct{}) error { 64 | server := &http.Server{ 65 | Addr: joinIPAddressAndPort(address, port), 66 | Handler: handler, 67 | } 68 | var wg sync.WaitGroup 69 | wg.Add(1) 70 | go func() { 71 | defer wg.Done() 72 | <-stop 73 | // Don't bother imposing a timeout here. 74 | server.Shutdown(context.Background()) 75 | }() 76 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 77 | return err 78 | } 79 | wg.Wait() 80 | return nil 81 | } 82 | 83 | func handleSignals(term func()) { 84 | c := make(chan os.Signal, 1) 85 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 86 | <-c 87 | signal.Stop(c) 88 | term() 89 | } 90 | 91 | func main() { 92 | apikey := flag.String("apikey", "", "Your Honeycomb API Key") 93 | dataset := flag.String("dataset", "opentelemetry", "Your Honeycomb dataset") 94 | serverAddress := flag.String("server-address", "", "IP address on which to serve HTTP requests") 95 | serverPort := flag.String("server-port", "7777", "Port on which to serve HTTP requests") 96 | flag.Parse() 97 | 98 | var serverIPAddress net.IP 99 | if len(*serverAddress) > 0 { 100 | if serverIPAddress = net.ParseIP(*serverAddress); serverIPAddress == nil { 101 | log.Fatalf("server address %q is not a valid IP address", *serverAddress) 102 | } 103 | } 104 | 105 | ctx := context.Background() 106 | 107 | exporter, err := honeycomb.NewExporter( 108 | honeycomb.Config{ 109 | APIKey: *apikey, 110 | }, 111 | honeycomb.TargetingDataset(*dataset), 112 | honeycomb.WithServiceName("opentelemetry-server"), 113 | honeycomb.WithDebugEnabled()) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | defer exporter.Shutdown(context.Background()) 118 | defer initTracer(exporter)(context.Background()) 119 | 120 | ctx, cancel := context.WithCancel(ctx) 121 | defer cancel() 122 | 123 | go handleSignals(cancel) 124 | stop := ctx.Done() 125 | 126 | handler := otelhttp.NewHandler(makeHandler(), "serve-http", 127 | otelhttp.WithPublicEndpoint(), 128 | otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents)) 129 | if err := runHTTPServer(serverIPAddress, *serverPort, handler, stop); err != nil { 130 | log.Fatalf("HTTP server failed: %v", err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honeycombio/opentelemetry-exporter-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/census-instrumentation/opencensus-proto v0.2.1 7 | github.com/golang/protobuf v1.4.2 8 | github.com/google/go-cmp v0.5.4 9 | github.com/honeycombio/libhoney-go v1.12.4 10 | github.com/klauspost/compress v1.10.10 // indirect 11 | github.com/stretchr/testify v1.6.1 12 | github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect 13 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.16.0 14 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.16.0 15 | go.opentelemetry.io/otel v0.16.0 16 | go.opentelemetry.io/otel/sdk v0.16.0 17 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect 18 | google.golang.org/appengine v1.6.6 // indirect 19 | google.golang.org/protobuf v1.25.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/DataDog/zstd v1.4.4 h1:+IawcoXhCBylN7ccwdwf8LOH2jKq7NavGpEPanrlTzE= 6 | github.com/DataDog/zstd v1.4.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 7 | github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= 9 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 10 | github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 17 | github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= 20 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= 21 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= 22 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= 23 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= 24 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= 25 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= 26 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= 27 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= 28 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= 29 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= 30 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= 31 | github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= 32 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 33 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 38 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 40 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 42 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= 44 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 45 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 46 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 47 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 48 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 49 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 50 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 51 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 52 | github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= 53 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 54 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 55 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 56 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 57 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 58 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 59 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 60 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 61 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 62 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 63 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 64 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 65 | github.com/honeycombio/libhoney-go v1.12.4 h1:rWAoxhpvu2briq85wZc04osHgKtueCLAk/3igqTX3+Q= 66 | github.com/honeycombio/libhoney-go v1.12.4/go.mod h1:tp2qtK0xMZyG/ZfykkebQESKFS78xpyPr2wEswZ1j6U= 67 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 68 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 69 | github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= 70 | github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 71 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 72 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 73 | github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= 74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 75 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 77 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 78 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 79 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 80 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 81 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 82 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 83 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 84 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 85 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 86 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 87 | github.com/vmihailenco/msgpack/v4 v4.3.11 h1:Q47CePddpNGNhk4GCnAx9DDtASi2rasatE0cd26cZoE= 88 | github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 89 | github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= 90 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 91 | github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= 92 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 93 | go.opentelemetry.io/contrib v0.16.0 h1:cScR/U3bjTjxsBv939wh4miANY/akdP644rsg9msrIA= 94 | go.opentelemetry.io/contrib v0.16.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= 95 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.16.0 h1:4j5Pg5JoT9FGQx2zXbfKq+JabtbUpuB+bUmhPZB5hUM= 96 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.16.0/go.mod h1:P5mQmVnXk429V+RQnY79cRDxP7KeFqEirKBXwQkvfPA= 97 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.16.0 h1:hPbUH5fugPACtUdBWGL5glNqzowwHvnOdnwvQOATWgM= 98 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.16.0/go.mod h1:dNF4PMGeouMEPAWDwgEjsGFlod9hAU8oj0TU0w2J19g= 99 | go.opentelemetry.io/otel v0.16.0 h1:uIWEbdeb4vpKPGITLsRVUS44L5oDbDUCZxn8lkxhmgw= 100 | go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= 101 | go.opentelemetry.io/otel/sdk v0.16.0 h1:5o+fkNsOfH5Mix1bHUApNBqeDcAYczHDa7Ix+R73K2U= 102 | go.opentelemetry.io/otel/sdk v0.16.0/go.mod h1:Jb0B4wrxerxtBeapvstmAZvJGQmvah4dHgKSngDpiCo= 103 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 104 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 105 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= 106 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 107 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 108 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 109 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= 110 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 111 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 112 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 113 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 114 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 115 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 116 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 117 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 118 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 119 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 120 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 121 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 122 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 123 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 124 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 125 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 126 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 129 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 134 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 136 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 137 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 138 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 139 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 140 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 141 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 142 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= 143 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 144 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 145 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= 147 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 148 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 149 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 150 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 151 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 152 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 153 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= 154 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 155 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 156 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 157 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 158 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 159 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 160 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 161 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 162 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 163 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 164 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 165 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 166 | google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= 167 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 168 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 169 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 170 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 171 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 172 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= 173 | gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= 174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 175 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 176 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 177 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 179 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 180 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 181 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 183 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= 184 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 185 | -------------------------------------------------------------------------------- /honeycomb/honeycomb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, Honeycomb, Hound Technology, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package honeycomb contains a trace exporter for Honeycomb 16 | package honeycomb 17 | 18 | import ( 19 | "context" 20 | "encoding/binary" 21 | "encoding/hex" 22 | "errors" 23 | "fmt" 24 | "log" 25 | "time" 26 | 27 | libhoney "github.com/honeycombio/libhoney-go" 28 | "github.com/honeycombio/libhoney-go/transmission" 29 | 30 | "go.opentelemetry.io/otel/codes" 31 | "go.opentelemetry.io/otel/label" 32 | "go.opentelemetry.io/otel/sdk/export/trace" 33 | ) 34 | 35 | const ( 36 | defaultDataset = "opentelemetry" 37 | ) 38 | 39 | // Config defines the basic configuration for the Honeycomb exporter. 40 | type Config struct { 41 | // APIKey is your Honeycomb authentication token, available from 42 | // https://ui.honeycomb.io/account. This API key must have permission to 43 | // send events. 44 | // 45 | // Don't have a Honeycomb account? Sign up at https://ui.honeycomb.io/signup. 46 | APIKey string 47 | } 48 | 49 | type exporterConfig struct { 50 | dataset string 51 | serviceName string 52 | staticFields map[string]interface{} 53 | dynamicFields map[string]func() interface{} 54 | apiURL string 55 | userAgentAddendum string 56 | sender transmission.Sender 57 | onError func(error) 58 | debug bool 59 | } 60 | 61 | const ( 62 | expectedStaticFieldCount = 8 63 | expectedDynamicFieldCount = 4 64 | ) 65 | 66 | // ExporterOption is an optional change to the configuration used by the 67 | // NewExporter function. 68 | type ExporterOption func(*exporterConfig) error 69 | 70 | func validateField(name string) error { 71 | if len(name) == 0 { 72 | return errors.New("field name must not be empty") 73 | } 74 | return nil 75 | } 76 | 77 | // TargetingDataset specifies the name of the Honeycomb dataset to which the 78 | // exporter will send events. 79 | // 80 | // If not specified, the default dataset name is "opentelemetry." 81 | func TargetingDataset(name string) ExporterOption { 82 | return func(c *exporterConfig) error { 83 | if len(name) == 0 { 84 | return errors.New("dataset name must not be empty") 85 | } 86 | c.dataset = name 87 | return nil 88 | } 89 | } 90 | 91 | // WithServiceName specifies an identifier for your application for use in 92 | // events sent by the exporter. While optional, specifying this name is 93 | // extremely valuable when you instrument multiple services. 94 | // 95 | // If set it will be added to all events as the field "service_name." 96 | func WithServiceName(name string) ExporterOption { 97 | return func(c *exporterConfig) error { 98 | if len(name) > 0 { 99 | c.serviceName = name 100 | } 101 | return nil 102 | } 103 | } 104 | 105 | // WithField adds a field with the given name and value to the exporter. Any 106 | // events published by this exporter will include this field. 107 | // 108 | // This function replaces any field registered previously with the same name. 109 | func WithField(name string, value interface{}) ExporterOption { 110 | return func(c *exporterConfig) error { 111 | if err := validateField(name); err != nil { 112 | return err 113 | } 114 | if c.staticFields == nil { 115 | c.staticFields = make(map[string]interface{}, expectedStaticFieldCount) 116 | } 117 | c.staticFields[name] = value 118 | if c.dynamicFields != nil { 119 | delete(c.dynamicFields, name) 120 | } 121 | return nil 122 | } 123 | } 124 | 125 | // WithFields adds a set of fields to the exporter. Any events published by this 126 | // exporter will include fields pairing each name in the given map with its 127 | // corresponding value. 128 | // 129 | // This function replaces any field registered previously with the same name. 130 | func WithFields(m map[string]interface{}) ExporterOption { 131 | return func(c *exporterConfig) error { 132 | count := len(m) 133 | if count == 0 { 134 | return nil 135 | } 136 | if c.staticFields == nil { 137 | if count < expectedStaticFieldCount { 138 | count = expectedStaticFieldCount 139 | } 140 | c.staticFields = make(map[string]interface{}, count) 141 | } 142 | for name, value := range m { 143 | if err := validateField(name); err != nil { 144 | return err 145 | } 146 | c.staticFields[name] = value 147 | } 148 | if c.dynamicFields != nil { 149 | for name := range m { 150 | delete(c.dynamicFields, name) 151 | } 152 | } 153 | return nil 154 | } 155 | } 156 | 157 | func validateDynamicField(name string, f func() interface{}) error { 158 | if len(name) == 0 { 159 | return errors.New("dynamic field name must not be empty") 160 | } 161 | if f == nil { 162 | return fmt.Errorf("dynamic field %q must have a non-nil function", name) 163 | } 164 | return nil 165 | } 166 | 167 | // WithDynamicField adds a dynamic field with the given name to the 168 | // exporter. Any events published by this exporter will include a field with the 169 | // given name and a value supplied by invoking the corresponding function. 170 | // 171 | // This function replaces any field registered previously with the same name. 172 | func WithDynamicField(name string, f func() interface{}) ExporterOption { 173 | return func(c *exporterConfig) error { 174 | if err := validateDynamicField(name, f); err != nil { 175 | return err 176 | } 177 | if c.dynamicFields == nil { 178 | c.dynamicFields = make(map[string]func() interface{}, expectedDynamicFieldCount) 179 | } 180 | c.dynamicFields[name] = f 181 | if c.staticFields != nil { 182 | delete(c.staticFields, name) 183 | } 184 | return nil 185 | } 186 | } 187 | 188 | // WithDynamicFields adds a set of dynamic fields to the exporter. Any events 189 | // published by this exporter will include fields pairing each name in the given 190 | // map with a value supplied by invoking the corresponding function. 191 | // 192 | // This function replaces any field registered previously with the same name. 193 | func WithDynamicFields(m map[string]func() interface{}) ExporterOption { 194 | return func(c *exporterConfig) error { 195 | count := len(m) 196 | if count == 0 { 197 | return nil 198 | } 199 | if c.dynamicFields == nil { 200 | if count < expectedDynamicFieldCount { 201 | count = expectedDynamicFieldCount 202 | } 203 | c.dynamicFields = make(map[string]func() interface{}, count) 204 | } 205 | for name, f := range m { 206 | if err := validateDynamicField(name, f); err != nil { 207 | return err 208 | } 209 | c.dynamicFields[name] = f 210 | } 211 | if c.staticFields != nil { 212 | for name := range m { 213 | delete(c.staticFields, name) 214 | } 215 | } 216 | return nil 217 | } 218 | } 219 | 220 | // WithAPIURL specifies the URL for the Honeycomb API server to which to send 221 | // events. 222 | // 223 | // If not specified, the default URL is https://api.honeycomb.io/. 224 | func WithAPIURL(url string) ExporterOption { 225 | return func(c *exporterConfig) error { 226 | // NB: libhoney.VerifyAPIKey parses this URL to make sure it's valid. 227 | if len(url) == 0 { 228 | return errors.New("API URL name must not be empty") 229 | } 230 | c.apiURL = url 231 | return nil 232 | } 233 | } 234 | 235 | // WithUserAgentAddendum specifies additional HTTP user agent-related detail to 236 | // include in HTTP requests issued to send events to the Honeycomb API 237 | // server. This value is appended to the user agent value from the libhoney 238 | // library. 239 | // 240 | // If not specified, the default value is "Honeycomb-OpenTelemetry-exporter." 241 | func WithUserAgentAddendum(a string) ExporterOption { 242 | return func(c *exporterConfig) error { 243 | if len(a) == 0 { 244 | return errors.New("user agent addendum must not be empty") 245 | } 246 | c.userAgentAddendum = a 247 | return nil 248 | } 249 | } 250 | 251 | // CallingOnError specifies a hook function to be called when an error occurs 252 | // sending events to Honeycomb. 253 | // 254 | // If not specified, the default hook logs the errors. Specifying a nil value 255 | // suppresses this default logging behavior. 256 | func CallingOnError(f func(error)) ExporterOption { 257 | return func(c *exporterConfig) error { 258 | if f == nil { 259 | f = func(error) {} 260 | } 261 | c.onError = f 262 | return nil 263 | } 264 | } 265 | 266 | // WithDebug causes the exporter to emit verbose logging to STDOUT 267 | // if provided with a true argument, otherwise it has no effect. 268 | // 269 | // If you're having trouble getting the exporter to work, try enabling this 270 | // logging in a development environment to help diagnose the problem. 271 | func WithDebug(d bool) ExporterOption { 272 | return func(c *exporterConfig) error { 273 | c.debug = d 274 | return nil 275 | } 276 | } 277 | 278 | // WithDebugEnabled causes the exporter to emit verbose logging to STDOUT. 279 | // 280 | // If you're having trouble getting the exporter to work, try enabling this 281 | // logging in a development environment to help diagnose the problem. 282 | func WithDebugEnabled() ExporterOption { 283 | return WithDebug(true) 284 | } 285 | 286 | // withHoneycombSender sets the event sender on the Honeycomb transmission subsystem. 287 | func withHoneycombSender(s transmission.Sender) ExporterOption { 288 | return func(c *exporterConfig) error { 289 | c.sender = s 290 | return nil 291 | } 292 | } 293 | 294 | // Exporter is an implementation of trace.Exporter that uploads a span to Honeycomb. 295 | type Exporter struct { 296 | client *libhoney.Client 297 | 298 | // serviceName identifies your application. If set it will be added to all 299 | // events as `service_name`. 300 | // 301 | // While optional, setting this field is extremely valuable when you 302 | // instrument multiple services. 303 | serviceName string 304 | // onError is the hook to be called when there is an error occurred when 305 | // uploading the span data. If no custom hook is set, errors are logged. 306 | onError func(err error) 307 | } 308 | 309 | var _ trace.SpanExporter = (*Exporter)(nil) 310 | 311 | // spanEvent represents an event attached to a specific span. 312 | type spanEvent struct { 313 | Name string `json:"name"` 314 | TraceID string `json:"trace.trace_id"` 315 | ParentID string `json:"trace.parent_id,omitempty"` 316 | ParentName string `json:"trace.parent_name,omitempty"` 317 | AnnotationType string `json:"meta.annotation_type"` 318 | } 319 | 320 | type spanRefType int64 321 | 322 | const ( 323 | spanRefTypeChildOf spanRefType = 0 324 | spanRefTypeFollowsFrom spanRefType = 1 325 | ) 326 | 327 | const ( 328 | traceIDShortLength = 8 329 | traceIDLongLength = 16 330 | ) 331 | 332 | func transcribeAttributesTo(ev *libhoney.Event, attrs []label.KeyValue) { 333 | for _, kv := range attrs { 334 | ev.AddField(string(kv.Key), kv.Value.AsInterface()) 335 | } 336 | } 337 | 338 | // span is the format of trace events that Honeycomb accepts. 339 | type span struct { 340 | TraceID string `json:"trace.trace_id"` 341 | Name string `json:"name"` 342 | ID string `json:"trace.span_id"` 343 | ParentID string `json:"trace.parent_id,omitempty"` 344 | DurationMilli float64 `json:"duration_ms"` 345 | Status string `json:"response.status_code,omitempty"` 346 | Error bool `json:"error,omitempty"` 347 | HasRemoteParent bool `json:"has_remote_parent"` 348 | } 349 | 350 | // getHoneycombTraceID returns a trace ID suitable for use in honeycomb. Before 351 | // encoding the bytes as a hex string, we want to handle cases where we are 352 | // given 128-bit IDs with zero padding, e.g. 0000000000000000f798a1e7f33c8af6. 353 | // To do this, we borrow a strategy from Jaeger [1] wherein we split the byte 354 | // sequence into two parts. The leftmost part could contain all zeros. We use 355 | // that to determine whether to return a 64-bit hex encoded string or a 128-bit 356 | // one. 357 | // 358 | // [1]: https://github.com/jaegertracing/jaeger/blob/cd19b64413eca0f06b61d92fe29bebce1321d0b0/model/ids.go#L81 359 | func getHoneycombTraceID(traceID []byte) string { 360 | // binary.BigEndian.Uint64() does a bounds check on traceID which will 361 | // cause a panic if traceID is fewer than 8 bytes. In this case, we don't 362 | // need to check for zero padding on the high part anyway, so just return a 363 | // hex string. 364 | if len(traceID) < traceIDShortLength { 365 | return fmt.Sprintf("%x", traceID) 366 | } 367 | var low uint64 368 | if len(traceID) == traceIDLongLength { 369 | low = binary.BigEndian.Uint64(traceID[traceIDShortLength:]) 370 | if high := binary.BigEndian.Uint64(traceID[:traceIDShortLength]); high != 0 { 371 | return fmt.Sprintf("%016x%016x", high, low) 372 | } 373 | } else { 374 | low = binary.BigEndian.Uint64(traceID) 375 | } 376 | 377 | return fmt.Sprintf("%016x", low) 378 | } 379 | 380 | func honeycombSpan(s *trace.SpanSnapshot) *span { 381 | sc := s.SpanContext 382 | 383 | hcSpan := &span{ 384 | TraceID: getHoneycombTraceID(sc.TraceID[:]), 385 | ID: sc.SpanID.String(), 386 | Name: s.Name, 387 | HasRemoteParent: s.HasRemoteParent, 388 | } 389 | parentID := hex.EncodeToString(s.ParentSpanID[:]) 390 | var initializedParentID [8]byte 391 | if s.ParentSpanID != sc.SpanID && s.ParentSpanID != initializedParentID { 392 | hcSpan.ParentID = parentID 393 | } 394 | 395 | if s, e := s.StartTime, s.EndTime; !s.IsZero() && !e.IsZero() { 396 | hcSpan.DurationMilli = float64(e.Sub(s)) / float64(time.Millisecond) 397 | } 398 | 399 | if s.StatusCode == codes.Error { 400 | hcSpan.Error = true 401 | } 402 | return hcSpan 403 | } 404 | 405 | // NewExporter returns an implementation of trace.Exporter that uploads spans to Honeycomb. 406 | func NewExporter(config Config, opts ...ExporterOption) (*Exporter, error) { 407 | // Developer note: bump this with each release 408 | // TODO: Stamp this via a variable set at link time with a value derived 409 | // from the current VCS tag. 410 | const versionStr = "0.15.0" 411 | 412 | if len(config.APIKey) == 0 { 413 | return nil, errors.New("API key must not be empty") 414 | } 415 | 416 | econf := exporterConfig{} 417 | for _, o := range opts { 418 | if err := o(&econf); err != nil { 419 | return nil, err 420 | } 421 | } 422 | if len(econf.dataset) == 0 { 423 | econf.dataset = defaultDataset 424 | } 425 | 426 | libhoneyConfig := libhoney.ClientConfig{ 427 | APIKey: config.APIKey, 428 | Dataset: econf.dataset, 429 | } 430 | if len(econf.apiURL) != 0 { 431 | libhoneyConfig.APIHost = econf.apiURL 432 | } 433 | userAgent := econf.userAgentAddendum 434 | if len(userAgent) == 0 { 435 | userAgent = "Honeycomb-OpenTelemetry-exporter" 436 | } 437 | libhoney.UserAgentAddition = userAgent + "/" + versionStr 438 | if econf.sender != nil { 439 | libhoneyConfig.Transmission = econf.sender 440 | } 441 | if econf.debug { 442 | libhoneyConfig.Logger = &libhoney.DefaultLogger{} 443 | } 444 | 445 | client, err := libhoney.NewClient(libhoneyConfig) 446 | if err != nil { 447 | return nil, err 448 | } 449 | 450 | for name, value := range econf.staticFields { 451 | client.AddField(name, value) 452 | } 453 | for name, f := range econf.dynamicFields { 454 | client.AddDynamicField(name, f) 455 | } 456 | 457 | onError := econf.onError 458 | if onError == nil { 459 | onError = func(err error) { 460 | log.Printf("Error when sending spans to Honeycomb: %v", err) 461 | } 462 | } 463 | 464 | return &Exporter{ 465 | client: client, 466 | serviceName: econf.serviceName, 467 | onError: onError, 468 | }, nil 469 | } 470 | 471 | // RunErrorLogger consumes from the response queue, calling the onError callback 472 | // when errors are encountered. 473 | // 474 | // This method will block until the passed context.Context is canceled, or until 475 | // exporter.Close is called. 476 | func (e *Exporter) RunErrorLogger(ctx context.Context) { 477 | responses := e.client.TxResponses() 478 | for { 479 | select { 480 | case r, ok := <-responses: 481 | if !ok { 482 | return 483 | } 484 | if r.Err != nil { 485 | e.onError(r.Err) 486 | } 487 | case <-ctx.Done(): 488 | return 489 | } 490 | } 491 | } 492 | 493 | // ExportSpans exports a sequence of OpenTelemetry spans to Honeycomb. 494 | func (e *Exporter) ExportSpans(ctx context.Context, sds []*trace.SpanSnapshot) error { 495 | for _, span := range sds { 496 | e.exportSpan(ctx, span) 497 | } 498 | return nil 499 | } 500 | 501 | func (e *Exporter) exportSpan(ctx context.Context, data *trace.SpanSnapshot) { 502 | ev := e.client.NewEvent() 503 | 504 | applyResourceAttributes := func(ev *libhoney.Event) { 505 | if data.Resource != nil { 506 | transcribeAttributesTo(ev, data.Resource.Attributes()) 507 | } 508 | if len(e.serviceName) != 0 { 509 | ev.AddField("service_name", e.serviceName) 510 | } 511 | } 512 | transcribeLayeredAttributesTo := func(ev *libhoney.Event, attrs []label.KeyValue) { 513 | // Treat resource-defined attributes as underlays, with any same-keyed message event 514 | // attributes taking precedence. Apply them first. 515 | applyResourceAttributes(ev) 516 | transcribeAttributesTo(ev, attrs) 517 | } 518 | 519 | // Treat resource-defined attributes as underlays, with any same-keyed span attributes taking 520 | // precedence. Apply them first. 521 | applyResourceAttributes(ev) 522 | ev.Timestamp = data.StartTime 523 | ev.Add(honeycombSpan(data)) 524 | 525 | // We send these message events as zero-duration spans. 526 | for _, a := range data.MessageEvents { 527 | spanEv := e.client.NewEvent() 528 | transcribeLayeredAttributesTo(spanEv, a.Attributes) 529 | spanEv.Timestamp = a.Time 530 | 531 | spanEv.Add(spanEvent{ 532 | Name: a.Name, 533 | TraceID: getHoneycombTraceID(data.SpanContext.TraceID[:]), 534 | ParentID: data.SpanContext.SpanID.String(), 535 | ParentName: data.Name, 536 | AnnotationType: "span_event", 537 | }) 538 | if err := spanEv.Send(); err != nil { 539 | e.onError(err) 540 | } 541 | } 542 | 543 | // link represents a link to a trace and span that lives elsewhere. 544 | // 545 | // TraceID and ParentID are used to identify the span with which the trace is associated. 546 | // We are modeling Links for now as child spans rather than properties of the event. 547 | type link struct { 548 | TraceID string `json:"trace.trace_id"` 549 | ParentID string `json:"trace.parent_id,omitempty"` 550 | LinkTraceID string `json:"trace.link.trace_id"` 551 | LinkSpanID string `json:"trace.link.span_id"` 552 | AnnotationType string `json:"meta.annotation_type"` 553 | RefType spanRefType `json:"ref_type,omitempty"` 554 | } 555 | 556 | for _, spanLink := range data.Links { 557 | linkEv := e.client.NewEvent() 558 | transcribeLayeredAttributesTo(linkEv, spanLink.Attributes) 559 | 560 | linkEv.Add(link{ 561 | TraceID: getHoneycombTraceID(data.SpanContext.TraceID[:]), 562 | ParentID: data.SpanContext.SpanID.String(), 563 | LinkTraceID: getHoneycombTraceID(spanLink.TraceID[:]), 564 | LinkSpanID: spanLink.SpanID.String(), 565 | AnnotationType: "link", 566 | // TODO(akvanhar): properly set the reference type when specs are defined 567 | // see https://github.com/open-telemetry/opentelemetry-specification/issues/65 568 | RefType: spanRefTypeChildOf, 569 | }) 570 | if err := linkEv.Send(); err != nil { 571 | e.onError(err) 572 | } 573 | } 574 | 575 | for _, kv := range data.Attributes { 576 | ev.AddField(string(kv.Key), kv.Value.AsInterface()) 577 | } 578 | 579 | ev.AddField("status.code", int32(data.StatusCode)) 580 | ev.AddField("status.message", data.StatusMessage) 581 | 582 | if err := ev.SendPresampled(); err != nil { 583 | e.onError(err) 584 | } 585 | } 586 | 587 | // Shutdown waits for all in-flight messages to be sent. You should 588 | // call Shutdoown() before app termination. 589 | func (e *Exporter) Shutdown(ctx context.Context) error { 590 | e.client.Close() 591 | return nil 592 | } 593 | -------------------------------------------------------------------------------- /honeycomb/honeycomb_test.go: -------------------------------------------------------------------------------- 1 | package honeycomb 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/honeycombio/libhoney-go/transmission" 11 | "github.com/stretchr/testify/assert" 12 | 13 | "go.opentelemetry.io/otel" 14 | "go.opentelemetry.io/otel/codes" 15 | "go.opentelemetry.io/otel/label" 16 | exporttrace "go.opentelemetry.io/otel/sdk/export/trace" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 19 | apitrace "go.opentelemetry.io/otel/trace" 20 | ) 21 | 22 | func TestGetHoneycombTraceID(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | traceID string 26 | want string 27 | }{ 28 | { 29 | name: "64-bit traceID", 30 | traceID: "cbe4decd12429177", 31 | want: "cbe4decd12429177", 32 | }, 33 | { 34 | name: "128-bit zero-padded traceID", 35 | traceID: "0000000000000000cbe4decd12429177", 36 | want: "cbe4decd12429177", 37 | }, 38 | { 39 | name: "128-bit non-zero-padded traceID", 40 | traceID: "f23b42eac289a0fdcde48fcbe3ab1a32", 41 | want: "f23b42eac289a0fdcde48fcbe3ab1a32", 42 | }, 43 | { 44 | name: "Non-hex traceID", 45 | traceID: "foobar1", 46 | want: "666f6f62617231", 47 | }, 48 | { 49 | name: "Longer non-hex traceID", 50 | traceID: "foobarbaz", 51 | want: "666f6f6261726261", 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | traceID, err := hex.DecodeString(tt.traceID) 58 | if err != nil { 59 | traceID = []byte(tt.traceID) 60 | } 61 | got := getHoneycombTraceID(traceID) 62 | if !reflect.DeepEqual(got, tt.want) { 63 | t.Errorf("getHoneycombTraceID:\n\tgot: %#v\n\twant: %#v", got, tt.want) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestExport(t *testing.T) { 70 | now := time.Now().Round(time.Microsecond) 71 | traceID, _ := apitrace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f10") 72 | spanID, _ := apitrace.SpanIDFromHex("0102030405060708") 73 | 74 | expectedTraceID := "0102030405060708090a0b0c0d0e0f10" 75 | expectedSpanID := "0102030405060708" 76 | 77 | tests := []struct { 78 | name string 79 | data *exporttrace.SpanSnapshot 80 | want *span 81 | }{ 82 | { 83 | name: "no parent", 84 | data: &exporttrace.SpanSnapshot{ 85 | SpanContext: apitrace.SpanContext{ 86 | TraceID: traceID, 87 | SpanID: spanID, 88 | }, 89 | Name: "/foo", 90 | StartTime: now, 91 | EndTime: now, 92 | StatusCode: codes.Ok, 93 | }, 94 | want: &span{ 95 | TraceID: expectedTraceID, 96 | ID: expectedSpanID, 97 | Name: "/foo", 98 | DurationMilli: 0, 99 | Error: false, 100 | }, 101 | }, 102 | { 103 | name: "1 day duration", 104 | data: &exporttrace.SpanSnapshot{ 105 | SpanContext: apitrace.SpanContext{ 106 | TraceID: traceID, 107 | SpanID: spanID, 108 | }, 109 | Name: "/bar", 110 | StartTime: now, 111 | EndTime: now.Add(24 * time.Hour), 112 | StatusCode: codes.Ok, 113 | }, 114 | want: &span{ 115 | TraceID: expectedTraceID, 116 | ID: expectedSpanID, 117 | Name: "/bar", 118 | DurationMilli: 86400000, 119 | Error: false, 120 | }, 121 | }, 122 | { 123 | name: "status code Unset", 124 | data: &exporttrace.SpanSnapshot{ 125 | SpanContext: apitrace.SpanContext{ 126 | TraceID: traceID, 127 | SpanID: spanID, 128 | }, 129 | Name: "/baz", 130 | StartTime: now, 131 | EndTime: now, 132 | StatusCode: codes.Unset, 133 | }, 134 | want: &span{ 135 | TraceID: expectedTraceID, 136 | ID: expectedSpanID, 137 | Name: "/baz", 138 | DurationMilli: 0, 139 | Error: false, 140 | }, 141 | }, 142 | { 143 | name: "status code Error", 144 | data: &exporttrace.SpanSnapshot{ 145 | SpanContext: apitrace.SpanContext{ 146 | TraceID: traceID, 147 | SpanID: spanID, 148 | }, 149 | Name: "/bazError", 150 | StartTime: now, 151 | EndTime: now, 152 | StatusCode: codes.Error, 153 | }, 154 | want: &span{ 155 | TraceID: expectedTraceID, 156 | ID: expectedSpanID, 157 | Name: "/bazError", 158 | DurationMilli: 0, 159 | Error: true, 160 | }, 161 | }, 162 | { 163 | name: "status code Ok", 164 | data: &exporttrace.SpanSnapshot{ 165 | SpanContext: apitrace.SpanContext{ 166 | TraceID: traceID, 167 | SpanID: spanID, 168 | }, 169 | Name: "/baz", 170 | StartTime: now, 171 | EndTime: now, 172 | StatusCode: codes.Ok, 173 | }, 174 | want: &span{ 175 | TraceID: expectedTraceID, 176 | ID: expectedSpanID, 177 | Name: "/baz", 178 | DurationMilli: 0, 179 | Error: false, 180 | }, 181 | }, 182 | } 183 | for _, tt := range tests { 184 | t.Run(tt.name, func(t *testing.T) { 185 | got := honeycombSpan(tt.data) 186 | if !reflect.DeepEqual(got, tt.want) { 187 | t.Errorf("honeycombSpan:\n\tgot %#v\n\twant %#v", got, tt.want) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func makeTestExporter(mockHoneycomb *transmission.MockSender, opts ...ExporterOption) (*Exporter, error) { 194 | return NewExporter( 195 | Config{ 196 | APIKey: "overridden", 197 | }, 198 | append(opts, 199 | TargetingDataset("test"), 200 | WithServiceName("opentelemetry-test"), 201 | withHoneycombSender(mockHoneycomb))..., 202 | ) 203 | } 204 | 205 | func setUpTestProvider(exporter exporttrace.SpanExporter, opts ...sdktrace.TracerProviderOption) (apitrace.Tracer, error) { 206 | tp := sdktrace.NewTracerProvider( 207 | append([]sdktrace.TracerProviderOption{ 208 | sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), 209 | sdktrace.WithSyncer(exporter), 210 | }, opts...)..., 211 | ) 212 | otel.SetTracerProvider(tp) 213 | 214 | return otel.Tracer("honeycomb/test"), nil 215 | } 216 | 217 | func setUpTestExporter(mockHoneycomb *transmission.MockSender, opts ...ExporterOption) (apitrace.Tracer, error) { 218 | exporter, err := makeTestExporter(mockHoneycomb, opts...) 219 | if err != nil { 220 | return nil, err 221 | } 222 | return setUpTestProvider(exporter) 223 | } 224 | 225 | func TestHoneycombOutput(t *testing.T) { 226 | mockHoneycomb := &transmission.MockSender{} 227 | assert := assert.New(t) 228 | tr, err := setUpTestExporter(mockHoneycomb) 229 | assert.Nil(err) 230 | 231 | _, span := tr.Start(context.TODO(), "myTestSpan") 232 | var nilString string 233 | span.SetAttributes( 234 | label.String("ex.com/string", "yes"), 235 | label.Bool("ex.com/bool", true), 236 | label.Int64("ex.com/int64", 42), 237 | label.Float64("ex.com/float64", 3.14), 238 | label.String("ex.com/nil", nilString), 239 | ) 240 | time.Sleep(time.Duration(0.5 * float64(time.Millisecond))) 241 | 242 | span.End() 243 | 244 | assert.Len(mockHoneycomb.Events(), 1) 245 | mainEventFields := mockHoneycomb.Events()[0].Data 246 | traceID := mainEventFields["trace.trace_id"] 247 | spanTraceID := span.SpanContext().TraceID 248 | honeycombTranslated := getHoneycombTraceID(spanTraceID[:]) 249 | 250 | assert.Equal(honeycombTranslated, traceID) 251 | 252 | spanID := mainEventFields["trace.span_id"] 253 | expectedSpanID := span.SpanContext().SpanID.String() 254 | assert.Equal(expectedSpanID, spanID) 255 | 256 | name := mainEventFields["name"] 257 | assert.Equal("myTestSpan", name) 258 | 259 | durationMilli := mainEventFields["duration_ms"] 260 | durationMilliFl, ok := durationMilli.(float64) 261 | assert.True(ok) 262 | assert.Greater(durationMilliFl, 0.0) 263 | assert.Less(durationMilliFl, 5.0) 264 | 265 | serviceName := mainEventFields["service_name"] 266 | assert.Equal("opentelemetry-test", serviceName) 267 | assert.Equal(mockHoneycomb.Events()[0].Dataset, "test") 268 | 269 | attribute := mainEventFields["ex.com/string"] 270 | assert.Equal("yes", attribute) 271 | attribute = mainEventFields["ex.com/bool"] 272 | assert.Equal(true, attribute) 273 | attribute = mainEventFields["ex.com/int64"] 274 | assert.Equal(int64(42), attribute) 275 | attribute = mainEventFields["ex.com/float64"] 276 | assert.Equal(3.14, attribute) 277 | attribute = mainEventFields["ex.com/nil"] 278 | assert.Equal("", attribute) 279 | } 280 | 281 | func TestHoneycombOutputWithMessageEvent(t *testing.T) { 282 | mockHoneycomb := &transmission.MockSender{} 283 | assert := assert.New(t) 284 | tr, err := setUpTestExporter(mockHoneycomb) 285 | assert.Nil(err) 286 | 287 | _, span := tr.Start(context.TODO(), "myTestSpan") 288 | span.AddEvent("handling this...", apitrace.WithAttributes(label.Int("request-handled", 100))) 289 | time.Sleep(time.Duration(0.5 * float64(time.Millisecond))) 290 | 291 | span.End() 292 | 293 | assert.Len(mockHoneycomb.Events(), 2) 294 | 295 | // Check the fields on the main span event. 296 | mainEventFields := mockHoneycomb.Events()[1].Data 297 | traceID := mainEventFields["trace.trace_id"] 298 | spanTraceID := span.SpanContext().TraceID 299 | honeycombTranslatedTraceID := getHoneycombTraceID(spanTraceID[:]) 300 | 301 | assert.Equal(honeycombTranslatedTraceID, traceID) 302 | 303 | spanID := mainEventFields["trace.span_id"] 304 | expectedSpanID := span.SpanContext().SpanID.String() 305 | assert.Equal(expectedSpanID, spanID) 306 | 307 | name := mainEventFields["name"] 308 | assert.Equal("myTestSpan", name) 309 | 310 | durationMilli := mainEventFields["duration_ms"] 311 | durationMilliFl, ok := durationMilli.(float64) 312 | assert.True(ok) 313 | assert.Greater(durationMilliFl, 0.0) 314 | 315 | serviceName := mainEventFields["service_name"] 316 | assert.Equal("opentelemetry-test", serviceName) 317 | assert.Equal(mockHoneycomb.Events()[1].Dataset, "test") 318 | 319 | // Check the fields on the zero-duration Message event. 320 | msgEventFields := mockHoneycomb.Events()[0].Data 321 | msgEventName := msgEventFields["name"] 322 | assert.Equal("handling this...", msgEventName) 323 | 324 | attribute := msgEventFields["request-handled"] 325 | assert.Equal(int64(100), attribute) 326 | 327 | msgEventTraceID := msgEventFields["trace.trace_id"] 328 | assert.Equal(honeycombTranslatedTraceID, msgEventTraceID) 329 | 330 | msgEventParentID := msgEventFields["trace.parent_id"] 331 | assert.Equal(spanID, msgEventParentID) 332 | 333 | msgEventParentName := msgEventFields["trace.parent_name"] 334 | assert.Equal("myTestSpan", msgEventParentName) 335 | 336 | msgEventServiceName := msgEventFields["service_name"] 337 | assert.Equal("opentelemetry-test", msgEventServiceName) 338 | 339 | spanEvent := msgEventFields["meta.annotation_type"] 340 | assert.Equal("span_event", spanEvent) 341 | } 342 | 343 | func TestHoneycombOutputWithLinks(t *testing.T) { 344 | linkTraceID, _ := apitrace.TraceIDFromHex("0102030405060709090a0b0c0d0e0f11") 345 | linkSpanID, _ := apitrace.SpanIDFromHex("0102030405060709") 346 | 347 | mockHoneycomb := &transmission.MockSender{} 348 | assert := assert.New(t) 349 | 350 | exporter, err := makeTestExporter(mockHoneycomb) 351 | assert.Nil(err) 352 | assert.NotNil(exporter) 353 | 354 | tr, err := setUpTestProvider(exporter, 355 | sdktrace.WithResource(resource.NewWithAttributes( 356 | label.Int("zero", 0), 357 | label.Int("one", 99), // NB: Deliberately not 1, to be overwritten later. 358 | ))) 359 | assert.Nil(err) 360 | 361 | _, span := tr.Start(context.TODO(), "myTestSpan", apitrace.WithLinks(apitrace.Link{ 362 | SpanContext: apitrace.SpanContext{ 363 | TraceID: linkTraceID, 364 | SpanID: linkSpanID, 365 | }, 366 | Attributes: []label.KeyValue{ 367 | label.Int("one", 1), 368 | label.Int("two", 2), 369 | }, 370 | })) 371 | 372 | span.End() 373 | 374 | assert.Len(mockHoneycomb.Events(), 2) 375 | 376 | // Check the fields on the main span event. 377 | linkFields := mockHoneycomb.Events()[0].Data 378 | mainEventFields := mockHoneycomb.Events()[1].Data 379 | traceID := linkFields["trace.trace_id"] 380 | spanContextTraceID := span.SpanContext().TraceID 381 | honeycombTranslatedTraceID := getHoneycombTraceID(spanContextTraceID[:]) 382 | 383 | assert.Equal(honeycombTranslatedTraceID, traceID) 384 | 385 | linkParentID := linkFields["trace.parent_id"] 386 | assert.Equal(mainEventFields["trace.span_id"], linkParentID) 387 | 388 | hclinkTraceID := linkFields["trace.link.trace_id"] 389 | assert.Equal(getHoneycombTraceID(linkTraceID[:]), hclinkTraceID) 390 | 391 | hclinkSpanID := linkFields["trace.link.span_id"] 392 | assert.Equal("0102030405060709", hclinkSpanID) 393 | linkAnnotationType := linkFields["meta.annotation_type"] 394 | assert.Equal("link", linkAnnotationType) 395 | 396 | assert.Equal(int64(0), linkFields["zero"]) 397 | assert.Equal(int64(1), linkFields["one"]) 398 | assert.Equal(int64(2), linkFields["two"]) 399 | } 400 | 401 | func TestHoneycombConfigValidation(t *testing.T) { 402 | tests := []struct { 403 | description string 404 | config Config 405 | expectError bool 406 | }{ 407 | { 408 | "empty API key", 409 | Config{}, 410 | true, 411 | }, 412 | { 413 | "populated API key", 414 | Config{ 415 | APIKey: "xyz", 416 | }, 417 | false, 418 | }, 419 | } 420 | for _, test := range tests { 421 | t.Run(test.description, func(t *testing.T) { 422 | assert := assert.New(t) 423 | exporter, err := NewExporter(test.config) 424 | if test.expectError { 425 | assert.Error(err) 426 | assert.Nil(exporter) 427 | } else { 428 | assert.Nil(err) 429 | assert.NotNil(exporter) 430 | } 431 | }) 432 | } 433 | } 434 | 435 | func TestHoneycombStaticFieldValidation(t *testing.T) { 436 | tests := []struct { 437 | description string 438 | fieldName string 439 | expectError bool 440 | }{ 441 | { 442 | "empty name", 443 | "", 444 | true, 445 | }, 446 | { 447 | "nonempty name", 448 | "xyz", 449 | false, 450 | }, 451 | } 452 | config := Config{ 453 | APIKey: "overridden", 454 | } 455 | var fieldValue interface{} = 1 456 | for _, test := range tests { 457 | for _, inMap := range []bool{false, true} { 458 | description := test.description 459 | if inMap { 460 | description += " (in map)" 461 | } 462 | t.Run(description, func(t *testing.T) { 463 | assert := assert.New(t) 464 | var opt ExporterOption 465 | if inMap { 466 | opt = WithFields(map[string]interface{}{ 467 | test.fieldName: fieldValue, 468 | }) 469 | } else { 470 | opt = WithField(test.fieldName, fieldValue) 471 | } 472 | exporter, err := NewExporter(config, opt) 473 | if test.expectError { 474 | assert.Error(err) 475 | assert.Nil(exporter) 476 | } else { 477 | assert.Nil(err) 478 | assert.NotNil(exporter) 479 | } 480 | }) 481 | } 482 | } 483 | } 484 | 485 | func TestHoneycombDynamicFieldValidation(t *testing.T) { 486 | valueFunc := func() interface{} { 487 | return 1 488 | } 489 | tests := []struct { 490 | description string 491 | fieldName string 492 | fieldValue func() interface{} 493 | expectError bool 494 | }{ 495 | { 496 | "empty name", 497 | "", 498 | valueFunc, 499 | true, 500 | }, 501 | { 502 | "nil function", 503 | "xyz", 504 | nil, 505 | true, 506 | }, 507 | { 508 | "nonempty name and function", 509 | "xyz", 510 | valueFunc, 511 | false, 512 | }, 513 | } 514 | config := Config{ 515 | APIKey: "overridden", 516 | } 517 | for _, test := range tests { 518 | for _, inMap := range []bool{false, true} { 519 | description := test.description 520 | if inMap { 521 | description += " (in map)" 522 | } 523 | t.Run(description, func(t *testing.T) { 524 | assert := assert.New(t) 525 | var opt ExporterOption 526 | if inMap { 527 | opt = WithDynamicFields(map[string]func() interface{}{ 528 | test.fieldName: test.fieldValue, 529 | }) 530 | } else { 531 | opt = WithDynamicField(test.fieldName, test.fieldValue) 532 | } 533 | exporter, err := NewExporter(config, opt) 534 | if test.expectError { 535 | assert.Error(err) 536 | assert.Nil(exporter) 537 | } else { 538 | assert.Nil(err) 539 | assert.NotNil(exporter) 540 | } 541 | }) 542 | } 543 | } 544 | } 545 | 546 | func TestHoneycombOutputWithStaticFields(t *testing.T) { 547 | mockHoneycomb := &transmission.MockSender{} 548 | assert := assert.New(t) 549 | 550 | tr, err := setUpTestExporter(mockHoneycomb, 551 | WithField("a", 1), 552 | WithField("b", 2), 553 | WithFields(map[string]interface{}{ 554 | "b": 4, 555 | "c": 5, 556 | }), 557 | WithField("a", 3)) 558 | assert.Nil(err) 559 | 560 | _, span := tr.Start(context.TODO(), "myTestSpan") 561 | span.SetAttributes( 562 | label.String("ex.com/string", "yes"), 563 | ) 564 | 565 | span.End() 566 | 567 | assert.Len(mockHoneycomb.Events(), 1) 568 | mainEventFields := mockHoneycomb.Events()[0].Data 569 | 570 | assert.Equal("yes", mainEventFields["ex.com/string"]) 571 | assert.Equal(3, mainEventFields["a"]) 572 | assert.Equal(4, mainEventFields["b"]) 573 | assert.Equal(5, mainEventFields["c"]) 574 | } 575 | 576 | func TestHoneycombOutputWithDynamicFields(t *testing.T) { 577 | mockHoneycomb := &transmission.MockSender{} 578 | assert := assert.New(t) 579 | 580 | constantly := func(v interface{}) func() interface{} { 581 | return func() interface{} { 582 | return v 583 | } 584 | } 585 | tr, err := setUpTestExporter(mockHoneycomb, 586 | WithDynamicField("a", constantly(1)), 587 | WithDynamicField("b", constantly(2)), 588 | WithDynamicFields(map[string]func() interface{}{ 589 | "b": constantly(4), 590 | "c": constantly(5), 591 | }), 592 | WithDynamicField("a", constantly(3))) 593 | assert.Nil(err) 594 | 595 | _, span := tr.Start(context.TODO(), "myTestSpan") 596 | span.SetAttributes( 597 | label.String("ex.com/string", "yes"), 598 | ) 599 | 600 | span.End() 601 | 602 | assert.Len(mockHoneycomb.Events(), 1) 603 | mainEventFields := mockHoneycomb.Events()[0].Data 604 | 605 | assert.Equal("yes", mainEventFields["ex.com/string"]) 606 | assert.Equal(3, mainEventFields["a"]) 607 | assert.Equal(4, mainEventFields["b"]) 608 | assert.Equal(5, mainEventFields["c"]) 609 | } 610 | 611 | func TestHoneycombOutputWithStaticAndDynamicFields(t *testing.T) { 612 | mockHoneycomb := &transmission.MockSender{} 613 | assert := assert.New(t) 614 | 615 | baseValue := 10 616 | delta := func(delta int) func() interface{} { 617 | return func() interface{} { 618 | return baseValue + delta 619 | } 620 | } 621 | tr, err := setUpTestExporter(mockHoneycomb, 622 | WithDynamicField("a", delta(1)), 623 | WithField("b", 2), 624 | WithDynamicFields(map[string]func() interface{}{ 625 | // Replace a static field. 626 | "b": delta(4), 627 | "c": delta(5), 628 | }), 629 | // Replace a dynamic field. 630 | WithField("a", 3)) 631 | assert.Nil(err) 632 | 633 | _, span := tr.Start(context.TODO(), "myTestSpan") 634 | span.SetAttributes( 635 | label.String("ex.com/string", "yes"), 636 | ) 637 | 638 | span.End() 639 | 640 | assert.Len(mockHoneycomb.Events(), 1) 641 | mainEventFields := mockHoneycomb.Events()[0].Data 642 | 643 | assert.Equal("yes", mainEventFields["ex.com/string"]) 644 | assert.Equal(3, mainEventFields["a"]) 645 | assert.Equal(baseValue+4, mainEventFields["b"]) 646 | assert.Equal(baseValue+5, mainEventFields["c"]) 647 | } 648 | 649 | func TestHoneycombOutputWithResource(t *testing.T) { 650 | mockHoneycomb := &transmission.MockSender{} 651 | assert := assert.New(t) 652 | 653 | const ( 654 | underlay int64 = iota 655 | middle 656 | overlay 657 | ) 658 | 659 | exporter, err := makeTestExporter(mockHoneycomb, 660 | WithField("a", underlay), 661 | WithField("b", underlay)) 662 | assert.Nil(err) 663 | assert.NotNil(exporter) 664 | 665 | tr, err := setUpTestProvider(exporter, 666 | sdktrace.WithResource(resource.NewWithAttributes( 667 | label.Int64("a", middle), 668 | label.Int64("c", middle), 669 | ))) 670 | assert.Nil(err) 671 | 672 | _, span := tr.Start(context.TODO(), "myTestSpan") 673 | assert.Nil(err) 674 | span.SetAttributes( 675 | label.Int64("a", overlay), 676 | label.Int64("d", overlay), 677 | ) 678 | span.AddEvent("something", apitrace.WithAttributes(label.Int64("c", overlay))) 679 | time.Sleep(time.Duration(0.5 * float64(time.Millisecond))) 680 | 681 | span.End() 682 | 683 | assert.Len(mockHoneycomb.Events(), 2) 684 | 685 | mainEventFields := mockHoneycomb.Events()[1].Data 686 | assert.Equal(int64(overlay), mainEventFields["a"]) 687 | assert.Equal(int64(underlay), mainEventFields["b"]) 688 | assert.Equal(int64(middle), mainEventFields["c"]) 689 | assert.Equal(int64(overlay), mainEventFields["d"]) 690 | 691 | messageEventFields := mockHoneycomb.Events()[0].Data 692 | assert.Equal(int64(middle), messageEventFields["a"]) 693 | assert.Equal(int64(underlay), mainEventFields["b"]) 694 | assert.Equal(int64(middle), mainEventFields["c"]) 695 | } 696 | -------------------------------------------------------------------------------- /honeycomb/translator.go: -------------------------------------------------------------------------------- 1 | package honeycomb 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" 8 | "github.com/golang/protobuf/ptypes/timestamp" 9 | 10 | "go.opentelemetry.io/otel/codes" 11 | "go.opentelemetry.io/otel/label" 12 | "go.opentelemetry.io/otel/sdk/export/trace" 13 | "go.opentelemetry.io/otel/sdk/resource" 14 | apitrace "go.opentelemetry.io/otel/trace" 15 | ) 16 | 17 | // timestampToTime creates a Go time.Time value from a Google protobuf Timestamp. 18 | func timestampToTime(ts *timestamp.Timestamp) (t time.Time) { 19 | if ts == nil { 20 | return 21 | } 22 | return time.Unix(ts.Seconds, int64(ts.Nanos)) 23 | } 24 | 25 | // Get SpanKind from an OC Span_SpanKind 26 | func oTelSpanKind(kind tracepb.Span_SpanKind) apitrace.SpanKind { 27 | // note that tracepb.SpanKindInternal, tracepb.SpanKindProducer and tracepb.SpanKindConsumer 28 | // have no equivalent OC proto type. 29 | switch kind { 30 | case tracepb.Span_SPAN_KIND_UNSPECIFIED: 31 | return apitrace.SpanKindUnspecified 32 | case tracepb.Span_SERVER: 33 | return apitrace.SpanKindServer 34 | case tracepb.Span_CLIENT: 35 | return apitrace.SpanKindClient 36 | default: 37 | return apitrace.SpanKindUnspecified 38 | } 39 | } 40 | 41 | // Creates an OpenTelemetry SpanContext from information in an OC Span. 42 | // Note that the OC Span has no equivalent to TraceFlags field in the 43 | // OpenTelemetry SpanContext type. 44 | func spanContext(traceID []byte, spanID []byte) apitrace.SpanContext { 45 | ctx := apitrace.SpanContext{} 46 | if traceID != nil { 47 | copy(ctx.TraceID[:], traceID[:]) 48 | } 49 | if spanID != nil { 50 | copy(ctx.SpanID[:], spanID[:]) 51 | } 52 | return ctx 53 | } 54 | 55 | func spanResource(span *tracepb.Span) *resource.Resource { 56 | if span.Resource == nil { 57 | return nil 58 | } 59 | attrs := make([]label.KeyValue, len(span.Resource.Labels)) 60 | i := 0 61 | for k, v := range span.Resource.Labels { 62 | attrs[i] = label.String(k, v) 63 | i++ 64 | } 65 | return resource.NewWithAttributes(attrs...) 66 | } 67 | 68 | // Create []kv.KeyValue attributes from an OC *Span_Attributes 69 | func createOTelAttributes(attributes *tracepb.Span_Attributes) []label.KeyValue { 70 | if attributes == nil || attributes.AttributeMap == nil { 71 | return nil 72 | } 73 | 74 | oTelAttrs := make([]label.KeyValue, len(attributes.AttributeMap)) 75 | 76 | i := 0 77 | for key, attributeValue := range attributes.AttributeMap { 78 | keyValue := label.KeyValue{ 79 | Key: label.Key(key), 80 | } 81 | switch val := attributeValue.Value.(type) { 82 | case *tracepb.AttributeValue_StringValue: 83 | keyValue.Value = label.StringValue(attributeValueAsString(attributeValue)) 84 | case *tracepb.AttributeValue_BoolValue: 85 | keyValue.Value = label.BoolValue(val.BoolValue) 86 | case *tracepb.AttributeValue_IntValue: 87 | keyValue.Value = label.Int64Value(val.IntValue) 88 | case *tracepb.AttributeValue_DoubleValue: 89 | keyValue.Value = label.Float64Value(val.DoubleValue) 90 | } 91 | oTelAttrs[i] = keyValue 92 | i++ 93 | } 94 | 95 | return oTelAttrs 96 | } 97 | 98 | // Create Span Links (including their attributes) from an OC Span 99 | func createSpanLinks(spanLinks *tracepb.Span_Links) []apitrace.Link { 100 | if spanLinks == nil { 101 | return nil 102 | } 103 | 104 | links := make([]apitrace.Link, len(spanLinks.Link)) 105 | 106 | for i, link := range spanLinks.Link { 107 | traceLink := apitrace.Link{ 108 | SpanContext: spanContext(link.GetTraceId(), link.GetSpanId()), 109 | Attributes: createOTelAttributes(link.Attributes), 110 | } 111 | links[i] = traceLink 112 | } 113 | 114 | return links 115 | } 116 | 117 | func createMessageEvents(spanEvents *tracepb.Span_TimeEvents) []trace.Event { 118 | if spanEvents == nil { 119 | return nil 120 | } 121 | 122 | annotations := 0 123 | for _, event := range spanEvents.TimeEvent { 124 | if annotation := event.GetAnnotation(); annotation != nil { 125 | annotations++ 126 | } 127 | } 128 | 129 | events := make([]trace.Event, annotations) 130 | 131 | for i, event := range spanEvents.TimeEvent { 132 | if annotation := event.GetAnnotation(); annotation != nil { 133 | events[i] = trace.Event{ 134 | Time: timestampToTime(event.GetTime()), 135 | Name: annotation.GetDescription().GetValue(), 136 | Attributes: createOTelAttributes(annotation.GetAttributes()), 137 | } 138 | } 139 | } 140 | 141 | return events 142 | } 143 | 144 | func attributeValueAsString(val *tracepb.AttributeValue) string { 145 | if wrapper := val.GetStringValue(); wrapper != nil { 146 | return wrapper.GetValue() 147 | } 148 | 149 | return "" 150 | } 151 | 152 | func getDroppedLinkCount(links *tracepb.Span_Links) int { 153 | if links != nil { 154 | return int(links.DroppedLinksCount) 155 | } 156 | 157 | return 0 158 | } 159 | 160 | func getChildSpanCount(span *tracepb.Span) int { 161 | if count := span.GetChildSpanCount(); count != nil { 162 | return int(count.GetValue()) 163 | } 164 | 165 | return 0 166 | } 167 | 168 | func getSpanName(span *tracepb.Span) string { 169 | if name := span.GetName(); name != nil { 170 | return name.GetValue() 171 | } 172 | 173 | return "" 174 | } 175 | 176 | func getHasRemoteParent(span *tracepb.Span) bool { 177 | if sameProcess := span.GetSameProcessAsParentSpan(); sameProcess != nil { 178 | return !sameProcess.Value 179 | } 180 | 181 | return false 182 | } 183 | 184 | func getStatusCode(span *tracepb.Span) codes.Code { 185 | if span.Status != nil { 186 | return codes.Code(span.Status.Code) 187 | } 188 | 189 | return codes.Ok 190 | } 191 | 192 | func getStatusMessage(span *tracepb.Span) string { 193 | switch { 194 | case span.Status == nil: 195 | return codes.Ok.String() 196 | case span.Status.Message != "": 197 | return span.Status.Message 198 | default: 199 | return codes.Code(span.Status.Code).String() 200 | } 201 | } 202 | 203 | // OCProtoSpanToOTelSpanSnapshot converts an OC Span to an OTel SpanSnapshot 204 | func OCProtoSpanToOTelSpanSnapshot(span *tracepb.Span) (*trace.SpanSnapshot, error) { 205 | if span == nil { 206 | return nil, errors.New("expected a non-nil span") 207 | } 208 | 209 | spanData := &trace.SpanSnapshot{ 210 | SpanContext: spanContext(span.GetTraceId(), span.GetSpanId()), 211 | } 212 | 213 | copy(spanData.ParentSpanID[:], span.GetParentSpanId()[:]) 214 | spanData.Name = getSpanName(span) 215 | spanData.SpanKind = oTelSpanKind(span.GetKind()) 216 | spanData.Links = createSpanLinks(span.GetLinks()) 217 | spanData.Attributes = createOTelAttributes(span.GetAttributes()) 218 | spanData.MessageEvents = createMessageEvents(span.GetTimeEvents()) 219 | spanData.StartTime = timestampToTime(span.GetStartTime()) 220 | spanData.EndTime = timestampToTime(span.GetEndTime()) 221 | spanData.StatusCode = getStatusCode(span) 222 | spanData.StatusMessage = getStatusMessage(span) 223 | spanData.HasRemoteParent = getHasRemoteParent(span) 224 | spanData.DroppedLinkCount = getDroppedLinkCount(span.GetLinks()) 225 | spanData.ChildSpanCount = getChildSpanCount(span) 226 | spanData.Resource = spanResource(span) 227 | 228 | return spanData, nil 229 | } 230 | -------------------------------------------------------------------------------- /honeycomb/translator_test.go: -------------------------------------------------------------------------------- 1 | package honeycomb 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "go.opentelemetry.io/otel/label" 9 | 10 | resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" 11 | tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" 12 | "github.com/golang/protobuf/ptypes" 13 | "github.com/golang/protobuf/ptypes/wrappers" 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/google/go-cmp/cmp/cmpopts" 16 | 17 | "go.opentelemetry.io/otel/codes" 18 | expTrace "go.opentelemetry.io/otel/sdk/export/trace" 19 | "go.opentelemetry.io/otel/sdk/resource" 20 | "go.opentelemetry.io/otel/trace" 21 | apitrace "go.opentelemetry.io/otel/trace" 22 | ) 23 | 24 | func TestOCProtoSpanToOTelSpanSnapshot(t *testing.T) { 25 | start := time.Now() 26 | end := start.Add(10 * time.Millisecond) 27 | annotationTime := start.Add(3 * time.Millisecond) 28 | 29 | startTimestamp, err := ptypes.TimestampProto(start) 30 | if err != nil { 31 | t.Fatalf("failed to convert time to timestamp: %v", err) 32 | } 33 | endTimestamp, err := ptypes.TimestampProto(end) 34 | if err != nil { 35 | t.Fatalf("failed to convert time to timestamp: %v", err) 36 | } 37 | annotationTimestamp, err := ptypes.TimestampProto(annotationTime) 38 | if err != nil { 39 | t.Fatalf("failed to convert time to timestamp: %v", err) 40 | } 41 | 42 | span := tracepb.Span{ 43 | TraceId: []byte{0x02}, 44 | SpanId: []byte{0x03}, 45 | ParentSpanId: []byte{0x01}, 46 | Name: &tracepb.TruncatableString{Value: "trace-name"}, 47 | Kind: tracepb.Span_CLIENT, 48 | StartTime: startTimestamp, 49 | EndTime: endTimestamp, 50 | Attributes: &tracepb.Span_Attributes{ 51 | AttributeMap: map[string]*tracepb.AttributeValue{ 52 | "some-string": { 53 | Value: &tracepb.AttributeValue_StringValue{ 54 | StringValue: &tracepb.TruncatableString{Value: "some-value"}, 55 | }, 56 | }, 57 | "some-double": { 58 | Value: &tracepb.AttributeValue_DoubleValue{DoubleValue: math.Pi}, 59 | }, 60 | "some-int": { 61 | Value: &tracepb.AttributeValue_IntValue{IntValue: 42}, 62 | }, 63 | "some-boolean": { 64 | Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}, 65 | }, 66 | }, 67 | }, 68 | Links: &tracepb.Span_Links{ 69 | Link: []*tracepb.Span_Link{ 70 | { 71 | TraceId: []byte{0x04}, 72 | SpanId: []byte{0x05}, 73 | Attributes: &tracepb.Span_Attributes{ 74 | AttributeMap: map[string]*tracepb.AttributeValue{ 75 | "e": { 76 | Value: &tracepb.AttributeValue_DoubleValue{DoubleValue: math.E}, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | DroppedLinksCount: 2, 83 | }, 84 | TimeEvents: &tracepb.Span_TimeEvents{ 85 | TimeEvent: []*tracepb.Span_TimeEvent{ 86 | { 87 | Time: annotationTimestamp, 88 | Value: &tracepb.Span_TimeEvent_Annotation_{ 89 | Annotation: &tracepb.Span_TimeEvent_Annotation{ 90 | Description: &tracepb.TruncatableString{Value: "test-event"}, 91 | Attributes: &tracepb.Span_Attributes{ 92 | AttributeMap: map[string]*tracepb.AttributeValue{ 93 | "annotation-attr": { 94 | Value: &tracepb.AttributeValue_StringValue{ 95 | StringValue: &tracepb.TruncatableString{Value: "annotation-val"}, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | Status: &tracepb.Status{Code: int32(codes.Unset), Message: "status message"}, 106 | SameProcessAsParentSpan: &wrappers.BoolValue{Value: false}, 107 | ChildSpanCount: &wrappers.UInt32Value{Value: 5}, 108 | Resource: &resourcepb.Resource{ 109 | Type: "host", 110 | Labels: map[string]string{ 111 | "host.name": "xanadu", 112 | }, 113 | }, 114 | } 115 | 116 | want := &expTrace.SpanSnapshot{ 117 | SpanContext: spanContext([]byte{0x02}, []byte{0x03}), 118 | ParentSpanID: apitrace.SpanID{0x01}, 119 | SpanKind: apitrace.SpanKindClient, 120 | Name: "trace-name", 121 | StartTime: time.Unix(start.Unix(), int64(start.Nanosecond())), 122 | EndTime: time.Unix(end.Unix(), int64(end.Nanosecond())), 123 | Attributes: []label.KeyValue{ 124 | label.String("some-string", "some-value"), 125 | label.Float64("some-double", math.Pi), 126 | label.Int("some-int", 42), 127 | label.Bool("some-boolean", true), 128 | }, 129 | Links: []apitrace.Link{ 130 | { 131 | SpanContext: spanContext([]byte{0x04}, []byte{0x05}), 132 | Attributes: []label.KeyValue{ 133 | label.Float64("e", math.E), 134 | }, 135 | }, 136 | }, 137 | MessageEvents: []expTrace.Event{ 138 | { 139 | Name: "test-event", 140 | Time: time.Unix(annotationTime.Unix(), int64(annotationTime.Nanosecond())), 141 | Attributes: []label.KeyValue{ 142 | label.String("annotation-attr", "annotation-val"), 143 | }, 144 | }, 145 | }, 146 | StatusCode: codes.Unset, 147 | StatusMessage: "status message", 148 | HasRemoteParent: true, 149 | DroppedLinkCount: 2, 150 | ChildSpanCount: 5, 151 | Resource: resource.NewWithAttributes(label.String("host.name", "xanadu")), 152 | } 153 | 154 | got, err := OCProtoSpanToOTelSpanSnapshot(&span) 155 | if err != nil { 156 | t.Fatalf("failed to convert proto span to otel span data: %v", err) 157 | } 158 | 159 | if diff := cmp.Diff(want, got, cmp.AllowUnexported(label.Value{}), cmp.AllowUnexported(trace.TraceState{}), cmpopts.SortSlices(keyValueLess)); diff != "" { 160 | t.Errorf("otel span: (-want +got):\n%s", diff) 161 | } 162 | } 163 | 164 | func keyValueLess(lhs, rhs label.KeyValue) bool { 165 | return lhs.Key < rhs.Key 166 | } 167 | --------------------------------------------------------------------------------