├── OSSMETADATA ├── version.go ├── wrappers ├── hnyecho │ ├── README.md │ ├── doc.go │ ├── echo.go │ └── echo_test.go ├── hnygoji │ ├── README.md │ ├── doc.go │ ├── hnygoji_test.go │ ├── goji_test.go │ └── goji.go ├── hnysql │ ├── README.md │ ├── doc.go │ └── sql_test.go ├── hnysqlx │ ├── README.md │ ├── doc.go │ └── sqlx_test.go ├── hnygorilla │ ├── README.md │ ├── hnygorilla_test.go │ ├── doc.go │ ├── gorilla_test.go │ └── gorilla.go ├── hnynethttp │ ├── README.md │ ├── doc.go │ ├── nethttp_example_test.go │ └── nethttp_test.go ├── hnyhttprouter │ ├── README.md │ ├── doc.go │ ├── hnyhttprouter_example_router_test.go │ ├── httprouter.go │ └── httprouter_test.go ├── hnygingonic │ ├── doc.go │ ├── hnygingonic_example_router_test.go │ ├── gingonic.go │ └── gingonic_test.go ├── hnypop │ ├── doc.go │ └── pop.go ├── common │ ├── common_prego1.11.go │ ├── common_go1.11.go │ └── common_test.go ├── hnygrpc │ ├── doc.go │ ├── grpc_test.go │ └── grpc.go └── config │ └── config.go ├── examples ├── grpc │ ├── proto │ │ ├── generate.go │ │ ├── hello.proto │ │ ├── hello_grpc.pb.go │ │ └── hello.pb.go │ └── main.go ├── nethttp │ ├── README.md │ └── main.go ├── nethttpfunc │ ├── README.md │ └── main.go ├── echo_ex │ ├── README.md │ └── main.go ├── http_and_sql │ ├── README.md │ └── main.go ├── httprouter │ ├── README.md │ └── main.go ├── goji │ ├── README.md │ └── main.go ├── gorilla │ ├── README.md │ └── main.go ├── README.md └── pop │ └── main.go ├── CONTRIBUTING.md ├── SUPPORT.md ├── .github ├── CODEOWNERS ├── workflows │ ├── apply-labels.yml │ ├── add-to-project-v2.yml │ ├── stale.yml │ └── validate-pr-title.yml ├── ISSUE_TEMPLATE │ ├── question-discussion.md │ ├── security-vulnerability-report.md │ ├── feature_request.md │ └── bug_report.md ├── release.yml ├── PULL_REQUEST_TEMPLATE.md └── dependabot.yml ├── CODE_OF_CONDUCT.md ├── .editorconfig ├── .gitignore ├── CONTRIBUTORS ├── RELEASING.md ├── client ├── client_test.go └── client.go ├── NOTICE ├── timer ├── timer_test.go └── timer.go ├── SECURITY.md ├── .circleci └── config.yml ├── README.md ├── trace ├── context_test.go ├── context.go └── doc.go ├── sample ├── deterministic_sampler.go └── deterministic_sampler_test.go ├── propagation ├── propagation.go ├── honeycomb.go ├── amazon.go ├── w3c.go ├── trace.go └── tracestate.go ├── doc.go ├── go.mod └── beeline_test.go /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=archived 2 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package beeline 2 | 3 | const version = "1.19.0" 4 | -------------------------------------------------------------------------------- /wrappers/hnyecho/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnyecho) 2 | -------------------------------------------------------------------------------- /wrappers/hnygoji/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnygoji) 2 | -------------------------------------------------------------------------------- /wrappers/hnysql/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnysql) 2 | -------------------------------------------------------------------------------- /wrappers/hnysqlx/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnysqlx) 2 | -------------------------------------------------------------------------------- /wrappers/hnygorilla/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnygorilla) 2 | -------------------------------------------------------------------------------- /wrappers/hnynethttp/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnynethttp) 2 | -------------------------------------------------------------------------------- /wrappers/hnyhttprouter/README.md: -------------------------------------------------------------------------------- 1 | Documentation available via [godoc](https://godoc.org/github.com/honeycombio/beeline-go/wrappers/hnyhttprouter) 2 | -------------------------------------------------------------------------------- /examples/grpc/proto/generate.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | //go:generate protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative hello.proto 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Please see our [general guide for OSS lifecycle and practices.](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # How to Get Help 2 | 3 | This project uses GitHub issues to track bugs, feature requests, and questions about using the project. Please search for existing issues before filing a new one. 4 | -------------------------------------------------------------------------------- /.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/pipeline-team 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the Honeycomb User Community Code of Conduct to clarify expected behavior in our community. 4 | 5 | https://www.honeycomb.io/honeycomb-user-community-code-of-conduct/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{go},go.mod,go.sum] 4 | indent_style = tab 5 | indent_size = 8 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | indent_size = 4 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /wrappers/hnygingonic/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnygingonic has Middleware to use with the gin-gonic muxer. 2 | // 3 | // Summary 4 | // 5 | // hnygingonic has Middleware for use in the gin.Use function call wrapping all 6 | // requests that come into the gin muxer 7 | // 8 | package hnygingonic 9 | -------------------------------------------------------------------------------- /examples/grpc/proto/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/honeycombio/examples/grpc/proto"; 4 | 5 | service HelloService { 6 | rpc SayHello(HelloRequest) returns (HelloResponse) {} 7 | } 8 | 9 | message HelloRequest { 10 | string name = 1; 11 | } 12 | 13 | message HelloResponse { 14 | string greeting = 1; 15 | } 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Thanks to the following people for their contributions to the Honeycomb Go Beeline: 2 | 3 | Ben Hartshorne 4 | Carlos Galdino 5 | Chris Post 6 | Chris Toshok 7 | Christine Yen 8 | Ian Wilkes 9 | Jamie Tsao 10 | Kale Blankenship 11 | Luke Mallon 12 | Michael Kania 13 | Mike Atkins 14 | Nathan LeClaire 15 | Nick Gauthier 16 | perangel 17 | Rachel Fong 18 | Sean Hagen 19 | Travis Redman 20 | -------------------------------------------------------------------------------- /wrappers/hnypop/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnypop wraps the gobuffalo/pop ORM. 2 | // 3 | // Summary 4 | // 5 | // hnypop provides a minimal implementation of the pop Store interface. There 6 | // are a few flaws - when starting a pop Transaction, you'll get a Honeycomb 7 | // event for the start of the transaction but none of the incremental 8 | // statements. 9 | // 10 | // Most other operations should come through ok. 11 | // 12 | package hnypop 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question/Discussion 3 | about: General question about how things work or a discussion 4 | title: '' 5 | labels: 'type: discussion' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project-v2.yml: -------------------------------------------------------------------------------- 1 | name: Add to project 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request_target: 6 | types: [opened] 7 | jobs: 8 | add-to-project: 9 | runs-on: ubuntu-latest 10 | name: Add issues and PRs to project 11 | steps: 12 | - uses: actions/add-to-project@main 13 | with: 14 | project-url: https://github.com/orgs/honeycombio/projects/27 15 | github-token: ${{ secrets.GHPROJECTS_TOKEN }} 16 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | - Update the version in `version.go` to the new version 4 | - Update `CHANGELOG.md` with changes since last release 5 | - Commit changes, push, and open a release preparation pull request for review 6 | - Once the pull request is merged, fetch the updated `main` branch 7 | - Apply a tag for the new version on the merged commit: vX.Y.Z, for example v1.1.2 8 | - Push the new version tag up to the project repository to kick off build and artifact publishing to GitHub -------------------------------------------------------------------------------- /wrappers/common/common_prego1.11.go: -------------------------------------------------------------------------------- 1 | // +build !go1.11 2 | 3 | package common 4 | 5 | import ( 6 | "database/sql" 7 | 8 | "github.com/honeycombio/beeline-go/trace" 9 | libhoney "github.com/honeycombio/libhoney-go" 10 | ) 11 | 12 | func addDBStatsToEvent(ev *libhoney.Event, stats sql.DBStats) { 13 | ev.AddField("db.open_conns", stats.OpenConnections) 14 | } 15 | 16 | func addDBStatsToSpan(span *trace.Span, stats sql.DBStats) { 17 | span.AddField("db.open_conns", stats.OpenConnections) 18 | } 19 | -------------------------------------------------------------------------------- /examples/nethttp/README.md: -------------------------------------------------------------------------------- 1 | # Example using HTTP 2 | 3 | This example illustrates wrapping a basic net/http mux. It wraps the globalmux 4 | to easily instrument all requests coming into the application. 5 | 6 | This example is runnable with `go run main.go` - it will start listening on port 7 | 8080. 8 | 9 | Once it's running, in another window, issue a request to the `/hello/` endpoint: 10 | `curl localhost:8080/hello/` and you should see several events appear on STDOUT 11 | in the pane running the example. 12 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestClientWrappersWorkWithoutInit(t *testing.T) { 9 | // None of these should cause panics 10 | Close() 11 | Flush() 12 | AddField("foo", "bar") 13 | b := NewBuilder() 14 | e := b.NewEvent() 15 | e.AddField("beep", "boop") 16 | e.Send() 17 | // we should get a closed channel back that doesn't panic or block forever 18 | resp := TxResponses() 19 | for r := range resp { 20 | fmt.Println(r.Body) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wrappers/hnygorilla/hnygorilla_test.go: -------------------------------------------------------------------------------- 1 | package hnygorilla 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | func ExampleMiddleware() { 10 | // assume you have handlers named root and hello 11 | var root func(w http.ResponseWriter, r *http.Request) 12 | var hello func(w http.ResponseWriter, r *http.Request) 13 | 14 | r := mux.NewRouter() 15 | r.Use(Middleware) 16 | // Routes consist of a path and a handler function. 17 | r.HandleFunc("/", root) 18 | r.HandleFunc("/hello/{person}", hello) 19 | } 20 | -------------------------------------------------------------------------------- /wrappers/hnyhttprouter/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnyhttprouter has Middleware to use with the httprouter muxer. 2 | // 3 | // Summary 4 | // 5 | // hnyhttprouter has Middleware to wrap individual handlers, and is best used in 6 | // conjunction with the nethttp WrapHandler function. Using these two together 7 | // will get you an event for every request that comes through your application 8 | // while also decorating the most interesting paths (the handlers that you wrap) 9 | // with additional fields from the httprouter patterns. 10 | // 11 | package hnyhttprouter 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security-vulnerability-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security vulnerability report 3 | about: Let us know if you discover a security vulnerability 4 | title: '' 5 | labels: 'type: security' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | **Versions** 16 | 17 | - Go: 18 | - Beeline: 19 | 20 | **Description** 21 | 22 | (Please include any relevant CVE advisory links) 23 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - no-changelog 7 | categories: 8 | - title: 💥 Breaking Changes 💥 9 | labels: 10 | - "version: bump major" 11 | - breaking-change 12 | - title: 💡 Enhancements 13 | labels: 14 | - "type: enhancement" 15 | - title: 🐛 Fixes 16 | labels: 17 | - "type: bug" 18 | - title: 🛠 Maintenance 19 | labels: 20 | - "type: maintenance" 21 | - "type: dependencies" 22 | - "type: documentation" 23 | - title: 🤷 Other Changes 24 | labels: 25 | - "*" 26 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present Honeycomb, Hound Technology, Inc. All Rights Reserved. 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 | -------------------------------------------------------------------------------- /examples/nethttpfunc/README.md: -------------------------------------------------------------------------------- 1 | # Example using HTTP and wrapping a HandlerFunc 2 | 3 | This example illustrates wrapping a single HnadlerFunc. It is the simplest form 4 | of wrapping an HTTP handler. Though useful for wrapping individual handler 5 | functions, it quickly gets cumbersome when wrapping too many. Wrapping a handler 6 | or a muxer is much more efficient. 7 | 8 | This example is runnable with `go run main.go` - it will start listening on port 9 | 8080. 10 | 11 | Once it's running, in another window, issue a request to the `/hello` endpoint: 12 | `curl localhost:8080/hello` and you should an event appear on STDOUT in the pane 13 | running the example. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'type: enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | 18 | 19 | **Describe the solution you'd like** 20 | 21 | 22 | **Describe alternatives you've considered** 23 | 24 | 25 | **Additional context** 26 | -------------------------------------------------------------------------------- /wrappers/hnyecho/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnyecho has middleware to use with the Echo router. 2 | // 3 | // Summary 4 | // 5 | // hnyecho provides Honeycomb instrumentation for the Echo router via middleware. 6 | // It is recommended to put this middleware first in the chain via Echo.Use(). 7 | // A Honeycomb event will be generated for every request that comes through your 8 | // Echo router, with basic http fields added. In addition, route related fields will 9 | // be added for that request route. 10 | // 11 | // For a complete example showing this wrapper in use, please see the examples in 12 | // https://github.com/honeycombio/beeline-go/tree/main/examples 13 | // 14 | package hnyecho 15 | -------------------------------------------------------------------------------- /wrappers/hnygoji/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnygoji has Middleware to use with the Goji muxer. 2 | // 3 | // Summary 4 | // 5 | // hnygoji has Middleware to wrap individual handlers, and is best used in 6 | // conjunction with the nethttp WrapHandler function. Using these two together 7 | // will get you an event for every request that comes through your application 8 | // while also decorating the most interesting paths (the hantdlers that you 9 | // wrap) with additional fields from the Goji patterns. 10 | // 11 | // For a complete example showing this wrapper in use, please see the examples in 12 | // https://github.com/honeycombio/beeline-go/tree/main/examples 13 | // 14 | package hnygoji 15 | -------------------------------------------------------------------------------- /wrappers/hnygorilla/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnygorilla has Middleware to use with the Gorilla muxer. 2 | // 3 | // Summary 4 | // 5 | // hnygorilla has Middleware to wrap individual handlers, and is best used in 6 | // conjunction with the nethttp WrapHandler function. Using these two together 7 | // will get you an event for every request that comes through your application 8 | // while also decorating the most interesting paths (the handlers that you wrap) 9 | // with additional fields from the Gorilla patterns. 10 | // 11 | // For a complete example showing this wrapper in use, please see the examples in 12 | // https://github.com/honeycombio/beeline-go/tree/main/examples 13 | // 14 | package hnygorilla 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ## Which problem is this PR solving? 14 | 15 | - 16 | 17 | ## Short description of the changes 18 | 19 | - 20 | 21 | -------------------------------------------------------------------------------- /wrappers/hnynethttp/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package hnynethttp provides Honeycomb wrappers for net/http Handlers. 3 | 4 | Summary 5 | 6 | hnynethttp provides wrappers for the `net/http` types: Handler and HandlerFunc 7 | 8 | For best results, use WrapHandler to wrap the mux passed to http.ListenAndServe 9 | - this will get you an event for every HTTP request handled by the server. 10 | 11 | Wrapping individual Handlers or HandleFuncs will generate events only for the 12 | endpoints that are wrapped; 404s, for example, will not generate events. 13 | 14 | For a complete example showing this wrapper in use, please see the examples in 15 | https://github.com/honeycombio/beeline-go/tree/main/examples 16 | 17 | */ 18 | package hnynethttp 19 | -------------------------------------------------------------------------------- /wrappers/hnygoji/hnygoji_test.go: -------------------------------------------------------------------------------- 1 | package hnygoji 2 | 3 | import ( 4 | "net/http" 5 | 6 | "goji.io/v3" 7 | "goji.io/v3/pat" 8 | ) 9 | 10 | func ExampleMiddleware() { 11 | // assume you have handlers for hello and bye 12 | var hello func(w http.ResponseWriter, r *http.Request) 13 | var bye func(w http.ResponseWriter, r *http.Request) 14 | 15 | // this example uses a submux just to illustrate the middleware's use 16 | root := goji.NewMux() 17 | greetings := goji.SubMux() 18 | root.Handle(pat.New("/greet/*"), greetings) 19 | greetings.HandleFunc(pat.Get("/hello/:name"), hello) 20 | greetings.HandleFunc(pat.Get("/bye/:name"), bye) 21 | 22 | // decorate calls that hit the greetings submux with extra fields 23 | greetings.Use(Middleware) 24 | } 25 | -------------------------------------------------------------------------------- /examples/echo_ex/README.md: -------------------------------------------------------------------------------- 1 | # Example using Echo middleware 2 | 3 | This example shows off using the hnyecho middleware. Adding the middleware to the 4 | Echo router using `Echo.Use()` (preferrably as first in the chain) will generate one 5 | Honeycomb event per request. Fields for basic http properties are added as well as 6 | route related fields (e.g. matched route, path params) 7 | 8 | This example is runnable with `go run main.go` - it will start listening on port 9 | 8080. 10 | 11 | Once it's running, in another window, issue a request to the `/hello` endpoint 12 | with a user's name as the variable: `curl localhost:8080/hello/ben`, and you should 13 | see an event appear on STDOUT in the pane running the example. The event printed will 14 | include a field for the `name` path param. 15 | -------------------------------------------------------------------------------- /wrappers/hnynethttp/nethttp_example_test.go: -------------------------------------------------------------------------------- 1 | package hnynethttp 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func ExampleWrapHandler() { 8 | // assume you have a handler named hello 9 | var hello func(w http.ResponseWriter, r *http.Request) 10 | 11 | globalmux := http.NewServeMux() 12 | // add a bunch of routes to the muxer 13 | globalmux.HandleFunc("/hello/", hello) 14 | 15 | // wrap the globalmux with the honeycomb middleware to send one event per 16 | // request 17 | http.ListenAndServe(":8080", WrapHandler(globalmux)) 18 | } 19 | 20 | func ExampleWrapHandlerFunc() { 21 | // assume you have a handler function named helloServer 22 | var helloServer func(w http.ResponseWriter, r *http.Request) 23 | 24 | http.HandleFunc("/hello", WrapHandlerFunc(helloServer)) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know if something is not working as expected 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | **Versions** 19 | 20 | - Go: 21 | - Beeline: 22 | 23 | 24 | **Steps to reproduce** 25 | 26 | 1. 27 | 28 | **Additional context** 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | labels: 13 | - "type: dependencies" 14 | reviewers: 15 | - "honeycombio/pipeline-team" 16 | groups: 17 | minor-patch: 18 | update-types: 19 | - "minor" 20 | - "patch" 21 | commit-message: 22 | prefix: "maint" 23 | include: "scope" 24 | -------------------------------------------------------------------------------- /examples/http_and_sql/README.md: -------------------------------------------------------------------------------- 1 | # Example using both HTTP and SQL 2 | 3 | This example illustrates what happens when you use multiple wrappers - 4 | specifically one from the HTTP layer accepting incoming requests and one 5 | wrapping DB access. Not only do you get one event for the incoming request and 6 | an event for each DB call, but (because the context is passed all the way down) 7 | the events are connected using a Trace ID and the outer HTTP event gets a few 8 | extra fields indicating how much total time was spent in the DB *for that 9 | request*. 10 | 11 | This example is runnable with `go run main.go` - it will start listening on port 12 | 8080. 13 | 14 | Once it's running, in another window, issue a request to the `/hello/` endpoint: 15 | `curl localhost:8080/hello/` and you should see several events appear on STDOUT 16 | in the pane running the example. 17 | -------------------------------------------------------------------------------- /examples/httprouter/README.md: -------------------------------------------------------------------------------- 1 | # Example using httprouter middleware 2 | 3 | This example shows off using a combination of packages. Using the 4 | `hnynethttp.WrapHandler` around the main httprouter router gets you one basic 5 | event for every request that comes through, regardless of what handler it hits. 6 | Adding the middleware around each handler gets additional fields that are custom 7 | to a matched route. 8 | 9 | This example is runnable with `go run main.go` - it will start listening on port 10 | 8080. 11 | 12 | Once it's running, in another window, issue a request to the `/hello` endpoint 13 | with a user's name as the variable: `curl localhost:8080/hello/ben` and you 14 | should an event appear on STDOUT in the pane running the example. The event 15 | printed will include the pattern matched (`/hello/:name`) as well as the 16 | contents of the `name` variable. 17 | -------------------------------------------------------------------------------- /examples/goji/README.md: -------------------------------------------------------------------------------- 1 | # Example using Goji middleware 2 | 3 | This example shows off using a combination of packages. Using the 4 | `hnynethttp.WrapHandler` around the main goji muxer gets you one basic event for 5 | every request that comes through, regardless of what handler it hits. Adding the 6 | middleware to goji with the `Use` function call gets additional fields that are 7 | custom to a matched goji pattern. 8 | 9 | This example is runnable with `go run main.go` - it will start listening on port 10 | 8080. 11 | 12 | Once it's running, in another window, issue a request to the `/hello` endpoint 13 | with a user's name as the variable: `curl localhost:8080/hello/ben` and you 14 | should an event appear on STDOUT in the pane running the example. The event 15 | printed will include the pattern matched (`/hello/:name`) as well as the 16 | contents of the `name` variable. 17 | -------------------------------------------------------------------------------- /examples/gorilla/README.md: -------------------------------------------------------------------------------- 1 | # Example using Gorilla middleware 2 | 3 | This example shows off using a combination of packages. Using the 4 | `hnynethttp.WrapHandler` around the main gorilla muxer gets you one basic event for 5 | every request that comes through, regardless of what handler it hits. Adding the 6 | middleware to gorilla with the `Use` function call gets additional fields that are 7 | custom to a matched gorilla route. 8 | 9 | This example is runnable with `go run main.go` - it will start listening on port 10 | 8080. 11 | 12 | Once it's running, in another window, issue a request to the `/hello` endpoint 13 | with a user's name as the variable: `curl localhost:8080/hello/ben` and you 14 | should an event appear on STDOUT in the pane running the example. The event 15 | printed will include the pattern matched (`/hello/{name}`) as well as the 16 | contents of the `name` variable. 17 | -------------------------------------------------------------------------------- /timer/timer_test.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Example of combining a timer with a defer to make it easy to put all your 9 | // timing code at the top of a function. 10 | func Example() { 11 | defer func(t Timer) { 12 | dur := t.Finish() 13 | fmt.Printf("log my duration as %g\n", dur) 14 | }(Start()) 15 | } 16 | 17 | // Example_block when timing non-funciton based locks, separate the start and finish 18 | func Example_block() { 19 | // do some work 20 | t := Start() 21 | // do some more work 22 | dur := t.Finish() 23 | fmt.Printf("log my duration as %g\n", dur) 24 | } 25 | 26 | // Example_othertime for when starting from Now isn't quite right 27 | func Example_otherTime() { 28 | actualStart := time.Unix(1525150486, 0) 29 | t := New(actualStart) 30 | // do some work 31 | dur := t.Finish() 32 | fmt.Printf("log my duration as %g\n", dur) 33 | } 34 | -------------------------------------------------------------------------------- /wrappers/common/common_go1.11.go: -------------------------------------------------------------------------------- 1 | //+build go1.11 2 | 3 | package common 4 | 5 | import ( 6 | "database/sql" 7 | 8 | "github.com/honeycombio/beeline-go/trace" 9 | libhoney "github.com/honeycombio/libhoney-go" 10 | ) 11 | 12 | func addDBStatsToEvent(ev *libhoney.Event, stats sql.DBStats) { 13 | ev.AddField("db.open_conns", stats.OpenConnections) 14 | ev.AddField("db.conns_in_use", stats.InUse) 15 | ev.AddField("db.conns_idle", stats.Idle) 16 | ev.AddField("db.wait_count", stats.WaitCount) 17 | ev.AddField("db.wait_duration", stats.WaitDuration) 18 | } 19 | 20 | func addDBStatsToSpan(span *trace.Span, stats sql.DBStats) { 21 | span.AddField("db.open_conns", stats.OpenConnections) 22 | span.AddField("db.conns_in_use", stats.InUse) 23 | span.AddField("db.conns_idle", stats.Idle) 24 | span.AddField("db.wait_count", stats.WaitCount) 25 | span.AddField("db.wait_duration", stats.WaitDuration) 26 | } 27 | -------------------------------------------------------------------------------- /wrappers/hnyhttprouter/hnyhttprouter_example_router_test.go: -------------------------------------------------------------------------------- 1 | package hnyhttprouter 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 8 | "github.com/julienschmidt/httprouter" 9 | ) 10 | 11 | func ExampleMiddleware() { 12 | // assume you have handlers named hello and index 13 | var hello func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) 14 | var index func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) 15 | 16 | router := httprouter.New() 17 | 18 | // call regular httprouter Handles with wrappers to extract parameters 19 | router.GET("/hello/:name", Middleware(hello)) 20 | // though the wrapper also works on routes that don't have parameters 21 | router.GET("/", Middleware(index)) 22 | 23 | // wrap the main router to set everything up for instrumenting 24 | log.Fatal(http.ListenAndServe(":8080", hnynethttp.WrapHandler(router))) 25 | } 26 | -------------------------------------------------------------------------------- /wrappers/hnygrpc/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnygrpc provides wrappers and other utilities for autoinstrumenting 2 | // gRPC services. 3 | // 4 | // Usage 5 | // 6 | // The Honeycomb beeline takes advantage of gRPC interceptors to instrument 7 | // RPCs. The wrapped interceptor can take a `config.GRPCIncomingConfig` object 8 | // which can optionally provide a custom trace parser hook, allowing for easy 9 | // interoperability between W3C, B3, Honeycomb and other trace header formats. 10 | // 11 | // serverOpts := []grpc.ServerOption{ 12 | // grpc.UnaryInterceptor(hnygrpc.UnaryServerInterceptorWithConfig(cfg)), 13 | // } 14 | // server := grpc.NewServer(serverOpts...) 15 | // 16 | // Requests received by the server will now generate Honeycomb events, with 17 | // metadata related to the request included as fields. 18 | // 19 | // Please note that only unary RPCs are supported at this time. Support for 20 | // streaming RPCs may be added later. 21 | package hnygrpc 22 | -------------------------------------------------------------------------------- /wrappers/hnysql/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnysql wraps `database.sql` to emit one Honeycomb event per DB call. 2 | // 3 | // After opening a DB connection, replace the *sql.DB object with a *hnysql.DB 4 | // object. The *hnysql.DB struct implements all the same functions as the normal 5 | // *sql.DB struct, and emits an event to Honeycomb with details about the SQL 6 | // event made. 7 | // 8 | // Additionally, if hnysql is used in conjunction with one of the Honeycomb HTTP 9 | // wrappers *and* you use the context-aware version of the DB calls, the trace 10 | // ID picked up in the HTTP event will appear in the SQL event to allow easy 11 | // identification of what HTTP call triggers which SQL calls. 12 | // 13 | // It is strongly suggested that you use the context-aware version of all calls 14 | // whenever possible; doing so not only lets you cancel your database calls, but 15 | // dramatically increases the value of the SQL isntrumentation by letting you 16 | // tie it back to individual HTTP requests. 17 | package hnysql 18 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # beeline-go examples 2 | 3 | Each of these examples is meant to show the Honeycomb [Beeline for Go](https://docs.honeycomb.io/getting-data-in/beelines/go-beeline/) in action in that particular framework's vocabulary. The examples show simple example use of the Beeline to set up the outermost HTTP wrapper and capture some useful per-request context. 4 | 5 | Two of these examples go a little bit further and are intended to show off more full-fledged use of the Beeline: 6 | 7 | - `nethttp` contains an example of "everything you can do with the Beeline," including: 8 | - beginning a trace 9 | - creating extra spans within a trace 10 | - hooking into the Beeline's sampler function (in order to intelligently sample the events emitted) 11 | - using scrubber functions as callbacks to obscure sensitive metadata 12 | - instrumenting outbound requests (aka, what you want to use when sending things to other instrumented apps) 13 | - `http_and_sql` contains an example of using both `nethttp` and `sql` wrappers together in a single application. 14 | -------------------------------------------------------------------------------- /timer/timer.go: -------------------------------------------------------------------------------- 1 | // Package timer is a small convenience package for timing blocks of code. 2 | package timer 3 | 4 | import "time" 5 | 6 | // Timer is the thing that you pass around to time blocks of code. Represented 7 | // as an interface so that other implementations can do fancy things with the 8 | // values in addition to just returning them, such as submit them to 9 | // instrumentation frameworks. 10 | type Timer interface { 11 | // Finish calculates the time since the timer was started and returns its 12 | // representation in milliseconds 13 | Finish() float64 14 | } 15 | 16 | // timer gives you an object to pass around for timing your code 17 | type timer struct { 18 | start time.Time 19 | } 20 | 21 | // New creates a new timer with an arbitrary starting time 22 | func New(t time.Time) Timer { 23 | return &timer{ 24 | start: t, 25 | } 26 | } 27 | 28 | // Start creates a new timer using `time.Now()` as the starting time 29 | func Start() Timer { 30 | return &timer{ 31 | start: time.Now(), 32 | } 33 | } 34 | 35 | // Finish closes off a started timer. It returns the duration timed in 36 | // milliseconds. Will return zero for timers that were never started. 37 | func (t timer) Finish() float64 { 38 | if t.start.IsZero() { 39 | return 0 40 | } 41 | return float64(time.Since(t.start)) / float64(time.Millisecond) 42 | } 43 | -------------------------------------------------------------------------------- /wrappers/hnyhttprouter/httprouter.go: -------------------------------------------------------------------------------- 1 | package hnyhttprouter 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "runtime" 7 | 8 | "github.com/honeycombio/beeline-go/wrappers/common" 9 | "github.com/julienschmidt/httprouter" 10 | ) 11 | 12 | // Middleware wraps httprouter handlers. Since it wraps handlers with explicit 13 | // parameters, it can add those values to the event it generates. 14 | func Middleware(handle httprouter.Handle) httprouter.Handle { 15 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 16 | // get a new context with our trace from the request, and add common fields 17 | ctx, span := common.StartSpanOrTraceFromHTTP(r) 18 | defer span.Send() 19 | // push the context with our trace and span on to the request 20 | r = r.WithContext(ctx) 21 | 22 | // replace the writer with our wrapper to catch the status code 23 | wrappedWriter := common.NewResponseWriter(w) 24 | 25 | // pull out any variables in the URL, add the thing we're matching, etc. 26 | for _, param := range ps { 27 | span.AddField("handler.vars."+param.Key, param.Value) 28 | } 29 | name := runtime.FuncForPC(reflect.ValueOf(handle).Pointer()).Name() 30 | span.AddField("handler.name", name) 31 | span.AddField("name", name) 32 | 33 | handle(wrappedWriter.Wrapped, r, ps) 34 | 35 | if wrappedWriter.Status == 0 { 36 | wrappedWriter.Status = 200 37 | } 38 | span.AddField("response.status_code", wrappedWriter.Status) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Package client is used to store the state of the libhoney client 2 | // that sends all beeline events, and provides wrappers of libhoney API 3 | // functions that are safe to use even if the client is not initialized. 4 | package client 5 | 6 | import ( 7 | libhoney "github.com/honeycombio/libhoney-go" 8 | "github.com/honeycombio/libhoney-go/transmission" 9 | ) 10 | 11 | var client = &libhoney.Client{} 12 | 13 | // Set the active libhoney client used by the beeline 14 | func Set(c *libhoney.Client) { 15 | client = c 16 | } 17 | 18 | // Get returns the libhoney client used by the beeline 19 | func Get() *libhoney.Client { 20 | return client 21 | } 22 | 23 | // Close the libhoney client 24 | func Close() { 25 | if client != nil { 26 | client.Close() 27 | } 28 | } 29 | 30 | // Flush all pending events in the libhoney client 31 | func Flush() { 32 | if client != nil { 33 | client.Flush() 34 | } 35 | } 36 | 37 | // AddField adds the given field at the client level 38 | func AddField(name string, val interface{}) { 39 | if client != nil { 40 | client.AddField(name, val) 41 | } 42 | } 43 | 44 | func NewBuilder() *libhoney.Builder { 45 | if client != nil { 46 | return client.NewBuilder() 47 | } 48 | return &libhoney.Builder{} 49 | } 50 | 51 | func TxResponses() chan transmission.Response { 52 | if client != nil { 53 | client.TxResponses() 54 | } 55 | 56 | c := make(chan transmission.Response) 57 | close(c) 58 | return c 59 | } 60 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This security policy applies to public projects under the [honeycombio organization][gh-organization] on GitHub. 4 | For security reports involving the services provided at `(ui|ui-eu|api|api-eu).honeycomb.io`, refer to the [Honeycomb Bug Bounty Program][bugbounty] for scope, expectations, and reporting procedures. 5 | 6 | ## Security/Bugfix Versions 7 | 8 | Security and bug fixes are generally provided only for the last minor version. 9 | Fixes are released either as part of the next minor version or as an on-demand patch version. 10 | 11 | Security fixes are given priority and might be enough to cause a new version to be released. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | We encourage responsible disclosure of security vulnerabilities. 16 | If you find something suspicious, we encourage and appreciate your report! 17 | 18 | ### Ways to report 19 | 20 | In order for the vulnerability reports to reach maintainers as soon as possible, the preferred way is to use the "Report a vulnerability" button under the "Security" tab of the associated GitHub project. 21 | This creates a private communication channel between the reporter and the maintainers. 22 | 23 | If you are absolutely unable to or have strong reasons not to use GitHub's vulnerability reporting workflow, please reach out to the Honeycomb security team at [security@honeycomb.io](mailto:security@honeycomb.io). 24 | 25 | [gh-organization]: https://github.com/honeycombio 26 | [bugbounty]: https://www.honeycomb.io/bugbountyprogram 27 | -------------------------------------------------------------------------------- /wrappers/hnysqlx/doc.go: -------------------------------------------------------------------------------- 1 | // Package hnysqlx wraps `jmoiron/sqlx` to emit one Honeycomb event per DB call. 2 | // 3 | // After opening a DB connection, replace the *sqlx.DB object with a *hnysqlx.DB 4 | // object. The *hnysqlx.DB struct implements all the same functions as the 5 | // normal *sqlx.DB struct, and emits an event to Honeycomb with details about 6 | // the SQL event made. 7 | // 8 | // If you're using transactions, named statements, and so on, there will be a 9 | // similar swap of `*sqlx` to `*hnysqlx` for each of the additional types you're 10 | // using. 11 | // 12 | // Additionally, if hnysqlx is used in conjunction with one of the Honeycomb 13 | // HTTP wrappers *and* you're using the context-aware versions of the SQL calls, 14 | // the trace ID picked up in the HTTP event will appear in the SQL event. This 15 | // will ensure you can track any SQL call back to the HTTP event that triggered 16 | // it. 17 | // 18 | // It is strongly suggested that you use the context-aware version of all calls 19 | // whenever possible; doing so not only lets you cancel your database calls, but 20 | // dramatically increases the value of the SQL isntrumentation by letting you 21 | // tie it back to individual HTTP requests. 22 | // 23 | // If you need to differentiate multiple DB connections, there is a 24 | // *libhoney.Builder associated with the *hnysqlx.DB (as well as with 25 | // transactions and statements). Adding fields to this builder will add those 26 | // fields to all events generated from that DB connection. 27 | // 28 | package hnysqlx 29 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | go: circleci/go@1.7.1 5 | 6 | jobs: 7 | test: 8 | parameters: 9 | go-version: 10 | type: string 11 | default: "1.21" 12 | executor: 13 | name: go/default 14 | tag: << parameters.go-version >> 15 | steps: 16 | - checkout 17 | - go/load-cache 18 | - go/test: 19 | race: true 20 | verbose: true 21 | covermode: atomic 22 | - go/save-cache 23 | publish_github: 24 | docker: 25 | - image: cibuilds/github:0.13.0 26 | steps: 27 | - run: 28 | name: "GHR Draft" 29 | command: ghr -draft -n ${CIRCLE_TAG} -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} 30 | 31 | workflows: 32 | weekly: 33 | triggers: 34 | - schedule: 35 | cron: "0 0 * * 0" 36 | filters: 37 | branches: 38 | only: 39 | - main 40 | jobs: 41 | - test: &test 42 | matrix: 43 | parameters: 44 | go-version: 45 | - "1.21" 46 | - "1.22" 47 | - "1.23" 48 | - "1.24" 49 | build: 50 | jobs: 51 | - test: 52 | <<: *test 53 | filters: 54 | tags: 55 | only: /.*/ 56 | - publish_github: 57 | context: Honeycomb Secrets for Public Repos 58 | filters: 59 | tags: 60 | only: /^v.*/ 61 | branches: 62 | ignore: /.*/ 63 | requires: 64 | - test 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Honeycomb Beeline for Go 2 | 3 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/beeline-go?color=pink)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | [![CircleCI](https://circleci.com/gh/honeycombio/beeline-go.svg?style=shield)](https://circleci.com/gh/honeycombio/beeline-go) 5 | [![GoDoc](https://godoc.org/github.com/honeycombio/beeline-go?status.svg)](https://godoc.org/github.com/honeycombio/beeline-go) 6 | 7 | ⚠️**STATUS**: This project is now archived. See [this issue](https://github.com/honeycombio/beeline-go/issues/449) for more details. 8 | 9 | ⚠️**Note**: Beelines are Honeycomb's legacy instrumentation libraries. We embrace OpenTelemetry as the effective way to instrument applications. For any new observability efforts, we recommend [instrumenting with OpenTelemetry](https://docs.honeycomb.io/send-data/go/opentelemetry-sdk/). 10 | 11 | This package makes it easy to instrument your Go app to send useful events to [Honeycomb](https://www.honeycomb.io), a service for debugging your software in production. 12 | - [Usage and Examples](https://docs.honeycomb.io/getting-data-in/beelines/go-beeline/) 13 | - [API Reference](https://godoc.org/github.com/honeycombio/beeline-go) 14 | - For each [wrapper](wrappers/), please see the [godoc](https://godoc.org/github.com/honeycombio/beeline-go#pkg-subdirectories) 15 | 16 | ## Dependencies 17 | 18 | Golang 1.19+ 19 | 20 | ## Contributions 21 | 22 | Features, bug fixes and other changes to `beeline-go` are gladly accepted. Please 23 | open issues or a pull request with your change. Remember to add your name to the 24 | CONTRIBUTORS file! 25 | 26 | All contributions will be released under the Apache License 2.0. 27 | -------------------------------------------------------------------------------- /wrappers/hnygoji/goji_test.go: -------------------------------------------------------------------------------- 1 | package hnygoji 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | beeline "github.com/honeycombio/beeline-go" 9 | libhoney "github.com/honeycombio/libhoney-go" 10 | "github.com/honeycombio/libhoney-go/transmission" 11 | "github.com/stretchr/testify/assert" 12 | "goji.io/v3" 13 | "goji.io/v3/pat" 14 | ) 15 | 16 | func TestGojiMiddleware(t *testing.T) { 17 | // set up libhoney to catch events instead of send them 18 | mo := &transmission.MockSender{} 19 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 20 | APIKey: "placeholder", 21 | Dataset: "placeholder", 22 | APIHost: "placeholder", 23 | Transmission: mo}) 24 | assert.Equal(t, nil, err) 25 | beeline.Init(beeline.Config{Client: client}) 26 | // build a sample request to generate an event 27 | r, _ := http.NewRequest("GET", "/hello/pooh", nil) 28 | w := httptest.NewRecorder() 29 | 30 | // build the goji mux router with Middleware 31 | router := goji.NewMux() 32 | router.HandleFunc(pat.Get("/hello/:name"), func(_ http.ResponseWriter, _ *http.Request) {}) 33 | router.Use(Middleware) 34 | // handle the request 35 | router.ServeHTTP(w, r) 36 | 37 | // verify the MockOutput caught the well formed event 38 | evs := mo.Events() 39 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 40 | fields := evs[0].Data 41 | status, ok := fields["response.status_code"] 42 | assert.True(t, ok, "status field must exist on middleware generated event") 43 | assert.Equal(t, 200, status, "successfully served request should have status 200") 44 | name, ok := fields["goji.pat.name"] 45 | assert.True(t, ok, "goji.pat.name field must exist on middleware generated event") 46 | assert.Equal(t, "pooh", name, "successfully served request should have name var populated") 47 | 48 | } 49 | -------------------------------------------------------------------------------- /trace/context_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTraceFromContext(t *testing.T) { 11 | ctx, tr := NewTrace(context.Background(), nil) 12 | trInCtx := GetTraceFromContext(ctx) 13 | assert.Equal(t, tr, trInCtx, "trace from context should be the trace we got from making a new trace") 14 | emptyTrace := &Trace{} 15 | ctx = PutTraceInContext(ctx, emptyTrace) 16 | trInCtx = GetTraceFromContext(ctx) 17 | assert.Equal(t, emptyTrace, trInCtx, "trace in context should be trace we put in the context") 18 | } 19 | 20 | func TestSpanFromContext(t *testing.T) { 21 | ctx, tr := NewTrace(context.Background(), nil) 22 | rs := tr.GetRootSpan() 23 | spanInCtx := GetSpanFromContext(ctx) 24 | assert.Equal(t, rs, spanInCtx, "span from context should be the root span we got from making a new trace") 25 | emptySpan := &Span{} 26 | ctx = PutSpanInContext(ctx, emptySpan) 27 | spanInCtx = GetSpanFromContext(ctx) 28 | assert.Equal(t, emptySpan, spanInCtx, "span in context should be span we put in the context") 29 | } 30 | 31 | func TestCopyContext(t *testing.T) { 32 | ctx, tr := NewTrace(context.Background(), nil) 33 | rs := tr.GetRootSpan() 34 | 35 | newCtx, err := CopyContext(context.Background(), ctx) 36 | assert.NoError(t, err, "should not return error when trace and span are present") 37 | 38 | trInCtx := GetTraceFromContext(newCtx) 39 | spanInCtx := GetSpanFromContext(newCtx) 40 | 41 | assert.Equal(t, trInCtx, tr, "expected to find the same trace in the new context after copy") 42 | assert.Equal(t, spanInCtx, rs, "expected to find the same span in the new context after copy") 43 | } 44 | 45 | func TestCopyContextError(t *testing.T) { 46 | newCtx, err := CopyContext(context.Background(), context.Background()) 47 | assert.NotNil(t, newCtx, "should return valid context even in errored state") 48 | assert.Equal(t, err, ErrTraceNotFoundInContext, "should error when no trace is present in the context") 49 | 50 | } 51 | -------------------------------------------------------------------------------- /examples/nethttpfunc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/honeycombio/beeline-go" 9 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 10 | ) 11 | 12 | // Simple server example demonstrating how to use `hnynethttp.WrapHandlerFunc(...)`. 13 | // Try `curl localhost:8080/hello` to create an event. 14 | func main() { 15 | 16 | // Initialize beeline. The only required field is WriteKey. 17 | beeline.Init(beeline.Config{ 18 | WriteKey: "abcabc123123", 19 | Dataset: "http-vanilla", 20 | // for demonstration, send the event to STDOUT instead of Honeycomb. 21 | // Remove the STDOUT setting when filling in a real write key. 22 | // NOTE: This should *only* be set to true in development environments. 23 | // Setting to true is Production environments can cause problems. 24 | STDOUT: true, 25 | }) 26 | 27 | // here only the /hello handler is wrapped, no other endpoints. 28 | http.HandleFunc("/hello", hnynethttp.WrapHandlerFunc(helloServer)) 29 | log.Fatal(http.ListenAndServe("localhost:8080", nil)) 30 | 31 | } 32 | 33 | // hello world, the web server 34 | func helloServer(w http.ResponseWriter, req *http.Request) { 35 | beeline.AddField(req.Context(), "custom", "Wheee") 36 | io.WriteString(w, "hello, world!\n") 37 | } 38 | 39 | // Example event created: 40 | // $ go run main.go | jq 41 | // $ curl localhost:8080/hello 42 | // { 43 | // "data": { 44 | // "app.custom": "Wheee", 45 | // "duration_ms": 0.352607, 46 | // "handler_func_name": "main.HelloServer", 47 | // "meta.localhostname": "cobbler.local", 48 | // "meta.type": "http request", 49 | // "request.content_length": 0, 50 | // "request.header.user_agent": "curl/7.54.0", 51 | // "request.host": "localhost:8080", 52 | // "request.method": "GET", 53 | // "request.path": "/hello", 54 | // "request.proto": "HTTP/1.1", 55 | // "request.remote_addr": "[::1]:64794", 56 | // "response.status_code": 200 57 | // "trace.trace_id": "e18a5d0f-9116-4756-b4bb-4d5e4db1477a", 58 | // }, 59 | // "time": "2018-04-06T09:48:36.289114189-07:00" 60 | // } 61 | -------------------------------------------------------------------------------- /sample/deterministic_sampler.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | import ( 4 | "crypto/sha1" 5 | "errors" 6 | "math" 7 | ) 8 | 9 | var ( 10 | ErrInvalidSampleRate = errors.New("sample rate must be >= 1") 11 | ) 12 | 13 | // If you want a globally active sampler, make a new one and set it here. It 14 | // will then be usable globally. If you'd rather not have global state, ignore 15 | // it - this won't get set automatically. 16 | var GlobalSampler *DeterministicSampler 17 | 18 | // DeterministicSampler allows for distributed sampling based on a common field 19 | // such as a request or trace ID. It accepts a sample rate N and will 20 | // deterministically sample 1/N events based on the target field. Hence, two or 21 | // more programs can decide whether or not to sample related events without 22 | // communication. 23 | type DeterministicSampler struct { 24 | sampleRate int 25 | upperBound uint32 26 | } 27 | 28 | func NewDeterministicSampler(sampleRate uint) (*DeterministicSampler, error) { 29 | if sampleRate < 1 { 30 | return nil, ErrInvalidSampleRate 31 | } 32 | 33 | // Get the actual upper bound - the largest possible value divided by 34 | // the sample rate. In the case where the sample rate is 1, this should 35 | // sample every value. 36 | upperBound := math.MaxUint32 / uint32(sampleRate) 37 | return &DeterministicSampler{ 38 | sampleRate: int(sampleRate), 39 | upperBound: upperBound, 40 | }, nil 41 | } 42 | 43 | // bytesToUint32 takes a slice of 4 bytes representing a big endian 32 bit 44 | // unsigned value and returns the equivalent uint32. 45 | func bytesToUint32be(b []byte) uint32 { 46 | return uint32(b[3]) | (uint32(b[2]) << 8) | (uint32(b[1]) << 16) | (uint32(b[0]) << 24) 47 | } 48 | 49 | // Sample returns true when you should *keep* this sample. False when it should 50 | // be dropped. 51 | func (ds *DeterministicSampler) Sample(determinant string) bool { 52 | if ds.sampleRate == 1 { 53 | return true 54 | } 55 | sum := sha1.Sum([]byte(determinant)) 56 | v := bytesToUint32be(sum[:4]) 57 | return v <= ds.upperBound 58 | } 59 | 60 | // GetSampleRate is an accessor to find out how this sampler was initialized 61 | func (ds *DeterministicSampler) GetSampleRate() int { 62 | return ds.sampleRate 63 | } 64 | -------------------------------------------------------------------------------- /wrappers/hnygingonic/hnygingonic_example_router_test.go: -------------------------------------------------------------------------------- 1 | package hnygingonic 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/honeycombio/beeline-go" 10 | ) 11 | 12 | func ExampleMiddleware() { 13 | // Setup a new gin Router, not using the Default here so that we can put the 14 | // Beeline middleware in before the middleware provided by Gin 15 | router := gin.New() 16 | router.Use( 17 | // Add the beeline middleware to the chain 18 | Middleware(nil), 19 | // Doing something like the following would have the Middleware grab specifc 20 | // GET query params that you deal with in your gin application. 21 | //Middleware(map[string]struct{}{ 22 | //"parts": {}, 23 | //"limit": {}, 24 | //"offset": {}, 25 | //}) 26 | // The Logger and Recovery middleware which are setup in the Default gin router 27 | gin.Logger(), 28 | gin.Recovery(), 29 | // Our example middleware that does extra work 30 | exampleMiddleware(), 31 | ) 32 | 33 | // Setup the routes we want to use 34 | router.GET("/", home) 35 | router.GET("/alive", alive) 36 | router.GET("/ready", ready) 37 | 38 | // Start the server 39 | log.Fatal(router.Run("127.0.0.1:8080")) 40 | } 41 | 42 | func home(c *gin.Context) { 43 | hnyctx, span := StartSpan(c, "main.home") 44 | defer span.Send() 45 | span.AddField("Welcome", "Home") 46 | childFunction(hnyctx) 47 | c.Data(http.StatusOK, "text/plain", []byte(`Welcome Home`)) 48 | } 49 | 50 | func alive(c *gin.Context) { 51 | c.Data(http.StatusOK, "text/plain", []byte(`OK`)) 52 | } 53 | 54 | func ready(c *gin.Context) { 55 | _, span := StartSpan(c, "main.ready") 56 | defer span.Send() 57 | // Do some work here 58 | span.AddField("Ready", true) 59 | c.Data(http.StatusOK, "text/plain", []byte(`OK`)) 60 | } 61 | 62 | func exampleMiddleware() gin.HandlerFunc { 63 | return func(c *gin.Context) { 64 | hnyctx, span := StartSpan(c, "main.exampleMiddleware") 65 | defer span.Send() 66 | SetContext(c, hnyctx) 67 | // Do some work 68 | c.Next() 69 | childFunction(hnyctx) 70 | } 71 | } 72 | 73 | func childFunction(ctx context.Context) { 74 | _, span := beeline.StartSpan(ctx, "main.childFunction") 75 | defer span.Send() 76 | // Do some work here 77 | } 78 | -------------------------------------------------------------------------------- /wrappers/hnygoji/goji.go: -------------------------------------------------------------------------------- 1 | package hnygoji 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/honeycombio/beeline-go/wrappers/common" 10 | "goji.io/v3/middleware" 11 | "goji.io/v3/pat" 12 | ) 13 | 14 | // Middleware is specifically to use with goji's router.Use() function for 15 | // inserting middleware 16 | func Middleware(handler http.Handler) http.Handler { 17 | wrappedHandler := func(w http.ResponseWriter, r *http.Request) { 18 | // get a new context with our trace from the request, and add common fields 19 | ctx, span := common.StartSpanOrTraceFromHTTP(r) 20 | defer span.Send() 21 | // push the context with our trace and span on to the request 22 | r = r.WithContext(ctx) 23 | 24 | // replace the writer with our wrapper to catch the status code 25 | wrappedWriter := common.NewResponseWriter(w) 26 | 27 | // get bits about the handler 28 | handler := middleware.Handler(ctx) 29 | if handler == nil { 30 | span.AddField("handler.name", "http.NotFound") 31 | handler = http.NotFoundHandler() 32 | } else { 33 | hType := reflect.TypeOf(handler) 34 | span.AddField("handler.type", hType.String()) 35 | name := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() 36 | span.AddField("handler.name", name) 37 | span.AddField("name", name) 38 | } 39 | // find any matched patterns 40 | pm := middleware.Pattern(ctx) 41 | if pm != nil { 42 | // TODO put a regex on `p.String()` to pull out any `:foo` and then 43 | // use those instead of trying to pull them out of the pattern some 44 | // other way 45 | if p, ok := pm.(*pat.Pattern); ok { 46 | span.AddField("goji.pat", p.String()) 47 | span.AddField("goji.methods", p.HTTPMethods()) 48 | span.AddField("goji.path_prefix", p.PathPrefix()) 49 | patvar := strings.TrimPrefix(p.String(), p.PathPrefix()+":") 50 | span.AddField("goji.pat."+patvar, pat.Param(r, patvar)) 51 | } else { 52 | span.AddField("pat", "NOT pat.Pattern") 53 | 54 | } 55 | } 56 | // TODO get all the parameters and their values 57 | handler.ServeHTTP(wrappedWriter.Wrapped, r) 58 | if wrappedWriter.Status == 0 { 59 | wrappedWriter.Status = 200 60 | } 61 | span.AddField("response.status_code", wrappedWriter.Status) 62 | } 63 | return http.HandlerFunc(wrappedHandler) 64 | } 65 | -------------------------------------------------------------------------------- /propagation/propagation.go: -------------------------------------------------------------------------------- 1 | // Package propagation includes types and functions for marshalling and unmarshalling trace 2 | // context headers between various supported formats and an internal representation. It 3 | // provides support for traces that cross process boundaries with support for interoperability 4 | // between various kinds of trace context header formats. 5 | package propagation 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | var GlobalConfig Config 12 | 13 | type Config struct { 14 | PropagateDataset bool 15 | } 16 | 17 | // getHeaderValue is a helper function that is guaranteed to return a string. Given a key, it 18 | // attempts to find the associated value in the provided header. If none is found, it returns 19 | // an empty string. 20 | func getHeaderValue(headers map[string]string, key string) string { 21 | if value, ok := headers[key]; ok { 22 | return value 23 | } 24 | return "" 25 | } 26 | 27 | // PropagationContext contains information about a trace that can cross process boundaries. 28 | // Typically this information is parsed from an incoming trace context header. 29 | type PropagationContext struct { 30 | TraceID string 31 | ParentID string 32 | Dataset string 33 | TraceContext map[string]interface{} 34 | TraceFlags TraceFlags 35 | TraceState TraceState 36 | } 37 | 38 | // hasTraceID checks that the trace ID is valid. 39 | func (prop PropagationContext) hasTraceID() bool { 40 | return prop.TraceID != "" && prop.TraceID != "00000000000000000000000000000000" 41 | } 42 | 43 | // hasParentID checks that the parent ID is valid. 44 | func (prop PropagationContext) hasParentID() bool { 45 | return prop.ParentID != "" && prop.ParentID != "0000000000000000" 46 | } 47 | 48 | // IsValid checks if the PropagationContext is valid. A valid PropagationContext has a valid 49 | // trace ID and parent ID. 50 | func (prop PropagationContext) IsValid() bool { 51 | return prop.hasTraceID() && prop.hasParentID() 52 | } 53 | 54 | // PropagationError wraps any error encountered while parsing or serializing trace propagation 55 | // contexts. 56 | type PropagationError struct { 57 | message string 58 | wrappedError error 59 | } 60 | 61 | // Error returns a formatted message containing the error. 62 | func (p *PropagationError) Error() string { 63 | if p.wrappedError == nil { 64 | return p.message 65 | } 66 | return fmt.Sprintf(p.message, p.wrappedError) 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: "Validate PR Title" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | id: lint_pr_title 17 | name: "🤖 Check PR title follows conventional commit spec" 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | # Have to specify all types because `maint` and `rel` aren't defaults 22 | types: | 23 | maint 24 | rel 25 | fix 26 | feat 27 | chore 28 | ci 29 | docs 30 | style 31 | refactor 32 | perf 33 | test 34 | ignoreLabels: | 35 | "type: dependencies" 36 | # When the previous steps fails, the workflow would stop. By adding this 37 | # condition you can continue the execution with the populated error message. 38 | - if: always() && (steps.lint_pr_title.outputs.error_message != null) 39 | name: "📝 Add PR comment about using conventional commit spec" 40 | uses: marocchino/sticky-pull-request-comment@v2 41 | with: 42 | header: pr-title-lint-error 43 | message: | 44 | Thank you for contributing to the project! 🎉 45 | 46 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 47 | 48 | Make sure to prepend with `feat:`, `fix:`, or another option in the list below. 49 | 50 | Once you update the title, this workflow will re-run automatically and validate the updated title. 51 | 52 | Details: 53 | 54 | ``` 55 | ${{ steps.lint_pr_title.outputs.error_message }} 56 | ``` 57 | 58 | # Delete a previous comment when the issue has been resolved 59 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 60 | name: "❌ Delete PR comment after title has been updated" 61 | uses: marocchino/sticky-pull-request-comment@v2 62 | with: 63 | header: pr-title-lint-error 64 | delete: true 65 | -------------------------------------------------------------------------------- /wrappers/hnygorilla/gorilla_test.go: -------------------------------------------------------------------------------- 1 | package hnygorilla 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gorilla/mux" 9 | beeline "github.com/honeycombio/beeline-go" 10 | libhoney "github.com/honeycombio/libhoney-go" 11 | "github.com/honeycombio/libhoney-go/transmission" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | type testHandler struct{} 16 | 17 | func (testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | w.WriteHeader(204) 19 | } 20 | 21 | func TestGorillaMiddleware(t *testing.T) { 22 | // set up libhoney to catch events instead of send them 23 | mo := &transmission.MockSender{} 24 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 25 | APIKey: "placeholder", 26 | Dataset: "placeholder", 27 | APIHost: "placeholder", 28 | Transmission: mo}) 29 | assert.Equal(t, nil, err) 30 | beeline.Init(beeline.Config{Client: client}) 31 | 32 | // build the gorilla mux router with Middleware 33 | router := mux.NewRouter() 34 | router.Use(Middleware) 35 | router.HandleFunc("/hello/{name}", func(_ http.ResponseWriter, _ *http.Request) {}) 36 | router.Handle("/yo", testHandler{}) 37 | 38 | t.Run("function handler", func(t *testing.T) { 39 | // build a sample request to generate an event 40 | r, _ := http.NewRequest("GET", "/hello/pooh", nil) 41 | w := httptest.NewRecorder() 42 | // handle the request 43 | router.ServeHTTP(w, r) 44 | 45 | // verify the MockOutput caught the well formed event 46 | evs := mo.Events() 47 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 48 | fields := evs[0].Data 49 | status, ok := fields["response.status_code"] 50 | assert.True(t, ok, "status field must exist on middleware generated event") 51 | assert.Equal(t, 200, status, "successfully served request should have status 200") 52 | name, ok := fields["gorilla.vars.name"] 53 | assert.True(t, ok, "gorilla.vars.name field must exist on middleware generated event") 54 | assert.Equal(t, "pooh", name, "successfully served request should have name var populated") 55 | }) 56 | 57 | t.Run("struct handler should not panic", func(t *testing.T) { 58 | // build a sample request to generate an event 59 | r, _ := http.NewRequest("GET", "/yo", nil) 60 | w := httptest.NewRecorder() 61 | // handle the request 62 | router.ServeHTTP(w, r) 63 | 64 | evs := mo.Events() 65 | assert.Equal(t, 2, len(evs)) 66 | assert.Equal(t, "testHandler", evs[1].Data["name"]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /examples/gorilla/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/honeycombio/beeline-go" 10 | "github.com/honeycombio/beeline-go/wrappers/hnygorilla" 11 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 12 | ) 13 | 14 | func main() { 15 | // Initialize beeline. The only required field is WriteKey. 16 | beeline.Init(beeline.Config{ 17 | WriteKey: "abcabc123123", 18 | Dataset: "http-gorilla", 19 | // for demonstration, send the event to STDOUT intead of Honeycomb. 20 | // Remove the STDOUT setting when filling in a real write key. 21 | // NOTE: This should *only* be set to true in development environments. 22 | // Setting to true is Production environments can cause problems. 23 | STDOUT: true, 24 | }) 25 | // ensure everything gets sent off before we exit 26 | defer beeline.Close() 27 | 28 | r := mux.NewRouter() 29 | r.Use(hnygorilla.Middleware) 30 | // Routes consist of a path and a handler function. 31 | r.HandleFunc("/", YourHandler) 32 | r.HandleFunc("/hello/{person}", HelloHandler) 33 | 34 | // Bind to a port and pass our router in 35 | log.Fatal(http.ListenAndServe("localhost:8080", hnynethttp.WrapHandler(r))) 36 | } 37 | 38 | func YourHandler(w http.ResponseWriter, r *http.Request) { 39 | w.Write([]byte("Gorilla!\n")) 40 | } 41 | 42 | func HelloHandler(w http.ResponseWriter, r *http.Request) { 43 | vars := mux.Vars(r) 44 | person := vars["person"] 45 | beeline.AddField(r.Context(), "inHello", true) 46 | w.Write([]byte(fmt.Sprintf("Gorilla! Gorilla! %s\n", person))) 47 | } 48 | 49 | // generates an event that looks like this: 50 | // 51 | // $ curl localhost:8080/hello/foo 52 | // { 53 | // "data": { 54 | // "duration_ms": 0.092819, 55 | // "gorilla.vars.person": "foo", 56 | // "handler.fnname": "main.HelloHandler", 57 | // "handler.name": "", 58 | // "handler.route": "/hello/{person}", 59 | // "inHello": true, 60 | // "meta.localhostname": "cobbler", 61 | // "meta.type": "http request", 62 | // "request.content_length": 0, 63 | // "request.header.user_agent": "curl/7.54.0", 64 | // "request.host": "localhost:8080", 65 | // "request.method": "GET", 66 | // "request.path": "/hello/foo", 67 | // "request.proto": "HTTP/1.1", 68 | // "request.remote_addr": "[::1]:51830", 69 | // "response.status_code": 200 70 | // "trace.trace_id": "a2ae3280-3b4d-4bb8-828e-b3707e1416f9", 71 | // }, 72 | // "time": "2018-04-06T22:12:53.440369114-07:00" 73 | // } 74 | -------------------------------------------------------------------------------- /examples/httprouter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/honeycombio/beeline-go" 9 | "github.com/honeycombio/beeline-go/wrappers/hnyhttprouter" 10 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 11 | "github.com/julienschmidt/httprouter" 12 | ) 13 | 14 | func main() { 15 | // Initialize beeline. The only required field is WriteKey. 16 | beeline.Init(beeline.Config{ 17 | WriteKey: "abcabc123123", 18 | Dataset: "sql", 19 | // for demonstration, send the event to STDOUT intead of Honeycomb. 20 | // Remove the STDOUT setting when filling in a real write key. 21 | // NOTE: This should *only* be set to true in development environments. 22 | // Setting to true is Production environments can cause problems. 23 | STDOUT: true, 24 | }) 25 | // ensure everything gets sent off before we exit 26 | defer beeline.Close() 27 | 28 | router := httprouter.New() 29 | 30 | // call regular httprouter Handles with wrappers to extract parameters 31 | router.GET("/hello/:name", hnyhttprouter.Middleware(Hello)) 32 | // though the wrapper also works on routes that don't have parameters 33 | router.GET("/", hnyhttprouter.Middleware(Index)) 34 | 35 | // wrap the main router to set everything up for instrumenting 36 | log.Fatal(http.ListenAndServe("localhost:8080", hnynethttp.WrapHandler(router))) 37 | } 38 | 39 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 40 | fmt.Fprint(w, "Welcome!\n") 41 | } 42 | 43 | func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 44 | beeline.AddField(r.Context(), "inHello", true) 45 | fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 46 | } 47 | 48 | // Produces an event like this: 49 | // { 50 | // "data": { 51 | // "app.inHello": true, 52 | // "duration_ms": 0.63284, 53 | // "handler.name": "main.Hello", 54 | // "handler.vars.name": "foo", 55 | // "meta.localhostname": "cobbler", 56 | // "meta.type": "http request", 57 | // "request.content_length": 0, 58 | // "request.header.user_agent": "curl/7.54.0", 59 | // "request.host": "localhost:8080", 60 | // "request.method": "GET", 61 | // "request.path": "/hello/foo", 62 | // "request.proto": "HTTP/1.1", 63 | // "request.remote_addr": "[::1]:52539", 64 | // "response.status_code": 200, 65 | // "trace.span_id": "9eed613e-40f4-4bc8-b6b5-866cae2c51cf", 66 | // "trace.trace_id": "91be396a-41a1-44aa-9f0a-25bf779448cc" 67 | // }, 68 | // "time": "2018-04-06T22:55:05.040951984-07:00" 69 | // } 70 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package beeline aids adding instrumentation to go apps using Honeycomb. 2 | // 3 | // Summary 4 | // 5 | // This package and its subpackages contain bits of code to use to make your 6 | // life easier when instrumenting a Go app to send events to Honeycomb. Most 7 | // applications will use something out of the `wrappers` package and the 8 | // `beeline` package. 9 | // 10 | // The `beeline` package provides the entry point - initialization and the basic 11 | // method to add fields to events. 12 | // 13 | // The `trace` package offers more direct control over the generated events and 14 | // how they connect together to form traces. It can be used if you need more 15 | // functionality (eg asynchronous spans, other field naming standards, trace 16 | // propagation). 17 | // 18 | // The `propagation`, `sample`, and `timer` packages are used internally and not 19 | // very interesting. 20 | // 21 | // The `wrappers` package contains middleware to use with other existing 22 | // packages such as HTTP routers (eg goji, gorilla, or just plain net/http) and 23 | // SQL packages (including sqlx and pop). 24 | // 25 | // Finally the `examples` package contains small example applications that use 26 | // the various wrappers and the beeline. 27 | // 28 | // Regardless of which subpackages are used, there is a small amount of global 29 | // configuration to add to your application's startup process. At the bare 30 | // minimum, you must pass in your team write key and identify a dataset name to 31 | // authorize your code to send events to Honeycomb and tell it where to send 32 | // events. 33 | // 34 | // func main() { 35 | // beeline.Init(beeline.Config{ 36 | // WriteKey: "abcabc123123defdef456456", 37 | // Dataset: "myapp", 38 | // }) 39 | // ... 40 | // 41 | // Once configured, use one of the subpackages to wrap HTTP handlers and SQL db 42 | // objects. 43 | // 44 | // Examples 45 | // 46 | // There are runnable examples at 47 | // https://github.com/honeycombio/beeline-go/tree/main/examples and examples 48 | // of each wrapper in the godoc. 49 | // 50 | // The most complete example is in `nethttp`; it covers 51 | // - beeline initialization 52 | // - using the net/http wrapper 53 | // - creating additional spans for larger chunks of work 54 | // - wrapping an outbound http call 55 | // - modifying spans on the way out to scrub information 56 | // - a custom sampling method 57 | // 58 | // TODO create two comprehensive examples, one showing basic beeline use and the 59 | // other the more exciting things you can do with direct access to the trace and 60 | // span objects. 61 | package beeline 62 | -------------------------------------------------------------------------------- /examples/pop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gobuffalo/pop/v6" 8 | beeline "github.com/honeycombio/beeline-go" 9 | "github.com/honeycombio/beeline-go/wrappers/hnypop" 10 | "github.com/honeycombio/beeline-go/wrappers/hnysqlx" 11 | "github.com/jmoiron/sqlx" 12 | ) 13 | 14 | // this program expects there to be a `donut` database already running on 15 | // localhost via mysql. The database should have one table `flavors` that has an 16 | // id field, a flavor field, and an updated field. It should have a few 17 | // records in there for good measure. 18 | 19 | // the example counts how many rows are in the DB, adds a new one, then counts 20 | // again. 21 | 22 | type flavor struct { 23 | ID int `db:"id"` 24 | Flavor string `db:"flavor"` 25 | Updated int `db:"updated"` 26 | } 27 | 28 | func main() { 29 | // Initialize beeline. The only required field is WriteKey. 30 | beeline.Init(beeline.Config{ 31 | WriteKey: "abcabc123123", 32 | Dataset: "sqlx", 33 | // for demonstration, send the event to STDOUT intead of Honeycomb. 34 | // Remove the STDOUT setting when filling in a real write key. 35 | // NOTE: This should *only* be set to true in development environments. 36 | // Setting to true is Production environments can cause problems. 37 | STDOUT: true, 38 | }) 39 | defer beeline.Close() 40 | 41 | // open up a raw sqlx connection to the database and wrap it 42 | odb, err := sqlx.Open("mysql", "root:@tcp(127.0.0.1)/donut") 43 | db := hnysqlx.WrapDB(odb) 44 | // override the hnysqlx type for DB calls 45 | db.Builder.AddField("meta.type", "pop") 46 | 47 | // make a pop connection, then replace the default pop store with the 48 | // beeline-wrapped store implementation 49 | deets := &pop.ConnectionDetails{ 50 | Dialect: "mysql", 51 | Database: "donut", 52 | Host: "localhost", 53 | User: "root", 54 | } 55 | p, err := pop.NewConnection(deets) 56 | if err != nil { 57 | fmt.Println("err", err) 58 | } 59 | p.Store = &hnypop.DB{ 60 | DB: db, 61 | } 62 | p.Open() 63 | 64 | var before = make([]*flavor, 0) 65 | var after = make([]*flavor, 0) 66 | 67 | p.Select("id", "flavor").All(&before) 68 | fmt.Printf("got back %d rows before adding cherry\n", len(before)) 69 | 70 | newFlavor := &flavor{ 71 | ID: len(before) + 1, 72 | Flavor: "cherry", 73 | } 74 | err = p.Create(newFlavor) 75 | if err != nil { 76 | fmt.Printf("Error creating new flavor: %s\n", err) 77 | os.Exit(1) 78 | } 79 | 80 | p.Select("id", "flavor").All(&after) 81 | fmt.Printf("got back %d rows after adding cherry\n", len(after)) 82 | } 83 | -------------------------------------------------------------------------------- /wrappers/hnypop/pop.go: -------------------------------------------------------------------------------- 1 | package hnypop 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "math/rand" 7 | 8 | "github.com/gobuffalo/pop/v6" 9 | "github.com/honeycombio/beeline-go/wrappers/hnysqlx" 10 | "github.com/jmoiron/sqlx" 11 | ) 12 | 13 | type DB struct { 14 | DB *hnysqlx.DB 15 | tx *pop.Tx 16 | } 17 | 18 | func (m *DB) Select(dest interface{}, query string, args ...interface{}) error { 19 | return m.DB.Select(dest, query, args...) 20 | } 21 | func (m *DB) Get(dest interface{}, query string, args ...interface{}) error { 22 | return m.DB.Get(dest, query, args...) 23 | } 24 | func (m *DB) NamedExec(query string, arg interface{}) (sql.Result, error) { 25 | return m.DB.NamedExec(query, arg) 26 | } 27 | func (m *DB) Exec(query string, args ...interface{}) (sql.Result, error) { 28 | return m.DB.Exec(query, args...) 29 | } 30 | func (m *DB) PrepareNamed(query string) (*sqlx.NamedStmt, error) { 31 | stmt, err := m.DB.PrepareNamed(query) 32 | return stmt.GetWrappedNamedStmt(), err 33 | } 34 | func (m *DB) Transaction() (*pop.Tx, error) { 35 | t := &pop.Tx{ 36 | ID: rand.Int(), 37 | } 38 | tx, err := m.DB.Beginx() 39 | t.Tx = tx.GetWrappedTx() 40 | m.tx = t 41 | return t, err 42 | } 43 | func (m *DB) Rollback() error { 44 | return m.tx.Rollback() 45 | } 46 | func (m *DB) Commit() error { 47 | return m.tx.Commit() 48 | } 49 | func (m *DB) Close() error { 50 | return m.Close() 51 | } 52 | func (m *DB) SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { 53 | return m.DB.SelectContext(ctx, dest, query, args...) 54 | } 55 | func (m *DB) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { 56 | return m.DB.GetContext(ctx, dest, query, args...) 57 | } 58 | func (m *DB) NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) { 59 | return m.DB.NamedExecContext(ctx, query, arg) 60 | } 61 | func (m *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { 62 | return m.DB.ExecContext(ctx, query, args...) 63 | } 64 | func (m *DB) PrepareNamedContext(ctx context.Context, query string) (*sqlx.NamedStmt, error) { 65 | p, err := m.DB.PrepareNamedContext(ctx, query) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return p.GetWrappedNamedStmt(), err 70 | } 71 | func (m *DB) TransactionContext(ctx context.Context) (*pop.Tx, error) { 72 | return m.tx.TransactionContext(ctx) 73 | } 74 | 75 | func (m *DB) TransactionContextOptions(ctx context.Context, options *sql.TxOptions) (*pop.Tx, error) { 76 | return m.tx.TransactionContextOptions(ctx, options) 77 | } 78 | -------------------------------------------------------------------------------- /wrappers/hnygorilla/gorilla.go: -------------------------------------------------------------------------------- 1 | package hnygorilla 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "runtime" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/honeycombio/beeline-go/wrappers/common" 10 | ) 11 | 12 | // Middleware is a gorilla middleware to add Honeycomb instrumentation to the 13 | // gorilla muxer. 14 | func Middleware(handler http.Handler) http.Handler { 15 | wrappedHandler := func(w http.ResponseWriter, r *http.Request) { 16 | // get a new context with our trace from the request, and add common fields 17 | ctx, span := common.StartSpanOrTraceFromHTTP(r) 18 | defer span.Send() 19 | // push the context with our trace and span on to the request 20 | r = r.WithContext(ctx) 21 | 22 | // replace the writer with our wrapper to catch the status code 23 | wrappedWriter := common.NewResponseWriter(w) 24 | // pull out any variables in the URL, add the thing we're matching, etc. 25 | vars := mux.Vars(r) 26 | for k, v := range vars { 27 | span.AddField("gorilla.vars."+k, v) 28 | } 29 | route := mux.CurrentRoute(r) 30 | if route != nil { 31 | chosenHandler := route.GetHandler() 32 | reflectHandler := reflect.ValueOf(chosenHandler) 33 | if reflectHandler.Kind() == reflect.Func { 34 | funcName := runtime.FuncForPC(reflectHandler.Pointer()).Name() 35 | span.AddField("handler.fnname", funcName) 36 | if funcName != "" { 37 | span.AddField("name", funcName) 38 | } 39 | } 40 | typeOfHandler := reflect.TypeOf(chosenHandler) 41 | if typeOfHandler.Kind() == reflect.Struct { 42 | structName := typeOfHandler.Name() 43 | if structName != "" { 44 | span.AddField("name", structName) 45 | } 46 | } 47 | name := route.GetName() 48 | if name != "" { 49 | span.AddField("handler.name", name) 50 | // stomp name because user-supplied names are better than function names 51 | span.AddField("name", name) 52 | } 53 | if path, err := route.GetPathTemplate(); err == nil { 54 | span.AddField("handler.route", path) 55 | } 56 | } 57 | handler.ServeHTTP(wrappedWriter.Wrapped, r) 58 | if wrappedWriter.Status == 0 { 59 | wrappedWriter.Status = 200 60 | } 61 | if cl := wrappedWriter.Wrapped.Header().Get("Content-Length"); cl != "" { 62 | span.AddField("response.content_length", cl) 63 | } 64 | if ct := wrappedWriter.Wrapped.Header().Get("Content-Type"); ct != "" { 65 | span.AddField("response.content_type", ct) 66 | } 67 | if ce := wrappedWriter.Wrapped.Header().Get("Content-Encoding"); ce != "" { 68 | span.AddField("response.content_encoding", ce) 69 | } 70 | span.AddField("response.status_code", wrappedWriter.Status) 71 | } 72 | return http.HandlerFunc(wrappedHandler) 73 | } 74 | -------------------------------------------------------------------------------- /examples/grpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/honeycombio/beeline-go" 10 | "github.com/honeycombio/beeline-go/trace" 11 | "github.com/honeycombio/beeline-go/wrappers/hnygrpc" 12 | "google.golang.org/grpc" 13 | 14 | hpb "github.com/honeycombio/beeline-go/examples/grpc/proto" 15 | ) 16 | 17 | type helloServer struct { 18 | hpb.UnimplementedHelloServiceServer 19 | } 20 | 21 | func (hs *helloServer) SayHello(ctx context.Context, req *hpb.HelloRequest) (*hpb.HelloResponse, error) { 22 | return &hpb.HelloResponse{Greeting: fmt.Sprintf("Hello, %s!", req.GetName())}, nil 23 | } 24 | 25 | func main() { 26 | // Initialize beeline. The only required field is WriteKey. 27 | beeline.Init(beeline.Config{ 28 | WriteKey: "example", 29 | Dataset: "beeline-example", 30 | ServiceName: "sample app", 31 | // For demonstration, send the event to STDOUT instead of Honeycomb. 32 | // Remove the STDOUT setting when filling in a real write key. 33 | // NOTE: This should *only* be set to true in development environments. 34 | // Setting to true in Production environments can cause problems. 35 | STDOUT: true, 36 | }) 37 | defer beeline.Close() 38 | 39 | // Set up a gRPC server. 40 | ln, err := net.Listen("tcp", ":50051") 41 | if err != nil { 42 | log.Fatalf("failed to listen: %v", err) 43 | } 44 | serverOpts := []grpc.ServerOption{ 45 | grpc.UnaryInterceptor(hnygrpc.UnaryServerInterceptor()), 46 | } 47 | s := grpc.NewServer(serverOpts...) 48 | hpb.RegisterHelloServiceServer(s, &helloServer{}) 49 | go s.Serve(ln) 50 | 51 | // Set up a gRPC client. 52 | conn, err := grpc.Dial( 53 | "localhost:50051", 54 | grpc.WithInsecure(), 55 | grpc.WithBlock(), 56 | grpc.WithUnaryInterceptor(hnygrpc.UnaryClientInterceptor()), 57 | ) 58 | if err != nil { 59 | log.Fatalf("did not connect: %v", err) 60 | } 61 | defer conn.Close() 62 | c := hpb.NewHelloServiceClient(conn) 63 | 64 | // Set up a root trace, so we can see it get propagated across the gRPC boundary. 65 | ctx := context.Background() 66 | ctx, tr := trace.NewTrace(ctx, nil) 67 | defer tr.Send() 68 | 69 | // Finally, make a call. 70 | req := &hpb.HelloRequest{Name: "Foo"} 71 | fmt.Printf("Making request: %v\n", req) 72 | res, err := c.SayHello(ctx, req) 73 | if err != nil { 74 | fmt.Printf("gRPC failed: %s\n", err) 75 | } 76 | fmt.Printf("Got response: %v\n", res) 77 | 78 | // Should result in three events being emitted: 79 | // 1) The span autogenerated by the UnaryServerInterceptor inside the gRPC server; 80 | // 2) The span autogenerated by the UnaryClientInterceptor inside the gRPC client; and 81 | // 3) The root span created on line 89. 82 | } 83 | -------------------------------------------------------------------------------- /wrappers/hnygingonic/gingonic.go: -------------------------------------------------------------------------------- 1 | package hnygingonic 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/honeycombio/beeline-go" 8 | "github.com/honeycombio/beeline-go/trace" 9 | "github.com/honeycombio/beeline-go/wrappers/common" 10 | ) 11 | 12 | const ginContextKey = "beeline-middleware-context" 13 | 14 | // Middleware wraps httprouter handlers. Since it wraps handlers with explicit 15 | // parameters, it can add those values to the event it generates. 16 | func Middleware(queryParams map[string]struct{}) gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | // get a new context with our trace from the request, and add common fields 19 | ctx, span := common.StartSpanOrTraceFromHTTP(c.Request) 20 | defer span.Send() 21 | // Add the span context to the gin context as we need to be able to pass 22 | // this context around our gin application 23 | c.Set(ginContextKey, ctx) 24 | // push the context with our trace and span on to the request 25 | c.Request = c.Request.WithContext(ctx) 26 | 27 | // pull out any variables in the URL, add the thing we're matching, etc. 28 | for _, param := range c.Params { 29 | span.AddField("handler.vars."+param.Key, param.Value) 30 | } 31 | 32 | // pull out any GET query params 33 | if queryParams != nil { 34 | for key, value := range c.Request.URL.Query() { 35 | if _, ok := queryParams[key]; ok { 36 | if len(value) > 1 { 37 | span.AddField("handler.query."+key, value) 38 | } else if len(value) == 1 { 39 | span.AddField("handler.query."+key, value[0]) 40 | } else { 41 | span.AddField("handler.query."+key, nil) 42 | } 43 | } 44 | } 45 | } 46 | 47 | name := c.HandlerName() 48 | span.AddField("handler.name", name) 49 | span.AddField("name", name) 50 | // Run the next function in the Middleware chain 51 | c.Next() 52 | span.AddField("response.status_code", c.Writer.Status()) 53 | } 54 | } 55 | 56 | // StartSpan is a helper function to start a new span in a gin-gonic context 57 | // This is required because the gin-gonic handler function expects to receive 58 | // *gin.Context rather than context.Context 59 | func StartSpan(c *gin.Context, name string) (context.Context, *trace.Span) { 60 | beelineContext, exists := c.Get(ginContextKey) 61 | var ctx context.Context 62 | 63 | if exists { 64 | ctx, _ = beelineContext.(context.Context) 65 | } 66 | 67 | return beeline.StartSpan(ctx, name) 68 | } 69 | 70 | // SetContext should be used to replace the context.Context in the gin.Context 71 | // in the case of having multiple custom middleware in the codebase 72 | func SetContext(c *gin.Context, newMiddleWareContext context.Context) { 73 | c.Set(ginContextKey, newMiddleWareContext) 74 | } 75 | -------------------------------------------------------------------------------- /wrappers/hnyecho/echo.go: -------------------------------------------------------------------------------- 1 | package hnyecho 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/honeycombio/beeline-go/wrappers/common" 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | // EchoWrapper provides Honeycomb instrumentation for the Echo router via middleware 11 | type ( 12 | EchoWrapper struct { 13 | handlerNames map[string]string 14 | once sync.Once 15 | } 16 | ) 17 | 18 | // New returns a new EchoWrapper struct 19 | func New() *EchoWrapper { 20 | return &EchoWrapper{} 21 | } 22 | 23 | // Middleware returns an echo.MiddlewareFunc to be used with Echo.Use() 24 | func (e *EchoWrapper) Middleware() echo.MiddlewareFunc { 25 | return func(next echo.HandlerFunc) echo.HandlerFunc { 26 | return func(c echo.Context) error { 27 | r := c.Request() 28 | // get a new context with our trace from the request 29 | ctx, span := common.StartSpanOrTraceFromHTTP(r) 30 | defer span.Send() 31 | // push the context with our trace and span on to the request 32 | c.SetRequest(r.WithContext(ctx)) 33 | 34 | // get name of handler 35 | handlerName := e.handlerName(c) 36 | if handlerName == "" { 37 | handlerName = "handler" 38 | } 39 | span.AddField("handler.name", handlerName) 40 | span.AddField("name", handlerName) 41 | 42 | // add route related fields 43 | span.AddField("route", c.Path()) 44 | span.AddField("route.handler", handlerName) 45 | for _, name := range c.ParamNames() { 46 | // add field for each path param 47 | span.AddField("route.params."+name, c.Param(name)) 48 | } 49 | 50 | // invoke next middleware in chain 51 | err := next(c) 52 | if err != nil { 53 | span.AddField("echo.error", err.Error()) 54 | // invokes the registered HTTP error handler 55 | c.Error(err) 56 | } 57 | 58 | // add fields for http response code and size 59 | span.AddField("response.status_code", c.Response().Status) 60 | span.AddField("response.size", c.Response().Size) 61 | 62 | return nil 63 | } 64 | } 65 | } 66 | 67 | // Unfortunately the name of c.Handler() is an anonymous function 68 | // (https://github.com/labstack/echo/blob/master/echo.go#L487-L494). 69 | // This function will return the correct handler name by building a 70 | // map of request paths to actual handler names (only during the first 71 | // request thus providing quick lookup for every request thereafter). 72 | func (e *EchoWrapper) handlerName(c echo.Context) string { 73 | // only perform once 74 | e.once.Do(func() { 75 | // build map of request paths to handler names 76 | routes := c.Echo().Routes() 77 | e.handlerNames = make(map[string]string, len(routes)) 78 | for _, r := range c.Echo().Routes() { 79 | e.handlerNames[r.Method+r.Path] = r.Name 80 | } 81 | }) 82 | 83 | // lookup handler name for this request 84 | return e.handlerNames[c.Request().Method+c.Path()] 85 | } 86 | -------------------------------------------------------------------------------- /wrappers/hnygingonic/gingonic_test.go: -------------------------------------------------------------------------------- 1 | package hnygingonic 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | beeline "github.com/honeycombio/beeline-go" 10 | libhoney "github.com/honeycombio/libhoney-go" 11 | "github.com/honeycombio/libhoney-go/transmission" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestHTTPRouterMiddleware(t *testing.T) { 16 | // set up libhoney to catch events instead of send them 17 | mo := &transmission.MockSender{} 18 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 19 | APIKey: "placeholder", 20 | Dataset: "placeholder", 21 | APIHost: "placeholder", 22 | Transmission: mo}) 23 | assert.Equal(t, nil, err) 24 | beeline.Init(beeline.Config{Client: client}) 25 | // build a sample request to generate an event 26 | r, _ := http.NewRequest("GET", "/hello/pooh", nil) 27 | w := httptest.NewRecorder() 28 | 29 | // build the httprouter mux router with Middleware 30 | router := gin.New() 31 | router.Use(Middleware(nil)) 32 | router.GET("/hello/:name", func(_ *gin.Context) {}) 33 | // handle the request 34 | router.ServeHTTP(w, r) 35 | 36 | // verify the MockOutput caught the well formed event 37 | evs := mo.Events() 38 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 39 | fields := evs[0].Data 40 | status, ok := fields["response.status_code"] 41 | assert.True(t, ok, "'status_code' field must exist on middleware generated event") 42 | assert.Equal(t, 200, status, "successfully served request should have status 200") 43 | name, ok := fields["handler.vars.name"] 44 | assert.True(t, ok, "handler.vars.name field must exist on middleware generated event") 45 | assert.Equal(t, "pooh", name, "successfully served request should have name var populated") 46 | } 47 | 48 | func TestHTTPRouterMiddlewareReturnsStatusCode(t *testing.T) { 49 | // set up libhoney to catch events instead of send them 50 | mo := &transmission.MockSender{} 51 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 52 | APIKey: "placeholder", 53 | Dataset: "placeholder", 54 | APIHost: "placeholder", 55 | Transmission: mo}) 56 | assert.Equal(t, nil, err) 57 | beeline.Init(beeline.Config{Client: client}) 58 | 59 | r, _ := http.NewRequest("GET", "/does_not_exist", nil) 60 | w := httptest.NewRecorder() 61 | 62 | router := gin.New() 63 | router.Use(Middleware(nil)) 64 | handler := func(c *gin.Context) { 65 | c.AbortWithStatus(http.StatusNotFound) 66 | } 67 | router.GET("/does_not_exist", handler) 68 | router.ServeHTTP(w, r) 69 | 70 | evs := mo.Events() 71 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 72 | fields := evs[0].Data 73 | status, ok := fields["response.status_code"] 74 | assert.True(t, ok, "'status_code' field must exist on middleware generated event") 75 | assert.Equal(t, http.StatusNotFound, status) 76 | } 77 | -------------------------------------------------------------------------------- /wrappers/hnyhttprouter/httprouter_test.go: -------------------------------------------------------------------------------- 1 | package hnyhttprouter 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | beeline "github.com/honeycombio/beeline-go" 9 | libhoney "github.com/honeycombio/libhoney-go" 10 | "github.com/honeycombio/libhoney-go/transmission" 11 | "github.com/julienschmidt/httprouter" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestHTTPRouterMiddleware(t *testing.T) { 16 | // set up libhoney to catch events instead of send them 17 | mo := &transmission.MockSender{} 18 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 19 | APIKey: "placeholder", 20 | Dataset: "placeholder", 21 | APIHost: "placeholder", 22 | Transmission: mo}) 23 | assert.Equal(t, nil, err) 24 | beeline.Init(beeline.Config{Client: client}) 25 | // build a sample request to generate an event 26 | r, _ := http.NewRequest("GET", "/hello/pooh", nil) 27 | w := httptest.NewRecorder() 28 | 29 | // build the httprouter mux router with Middleware 30 | router := httprouter.New() 31 | router.GET("/hello/:name", Middleware(func(_ http.ResponseWriter, _ *http.Request, _ httprouter.Params) {})) 32 | // handle the request 33 | router.ServeHTTP(w, r) 34 | 35 | // verify the MockOutput caught the well formed event 36 | evs := mo.Events() 37 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 38 | fields := evs[0].Data 39 | status, ok := fields["response.status_code"] 40 | assert.True(t, ok, "'status_code' field must exist on middleware generated event") 41 | assert.Equal(t, 200, status, "successfully served request should have status 200") 42 | name, ok := fields["handler.vars.name"] 43 | assert.True(t, ok, "handler.vars.name field must exist on middleware generated event") 44 | assert.Equal(t, "pooh", name, "successfully served request should have name var populated") 45 | } 46 | 47 | func TestHTTPRouterMiddlewareReturnsStatusCode(t *testing.T) { 48 | // set up libhoney to catch events instead of send them 49 | mo := &transmission.MockSender{} 50 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 51 | APIKey: "placeholder", 52 | Dataset: "placeholder", 53 | APIHost: "placeholder", 54 | Transmission: mo}) 55 | assert.Equal(t, nil, err) 56 | beeline.Init(beeline.Config{Client: client}) 57 | 58 | r, _ := http.NewRequest("GET", "/does_not_exist", nil) 59 | w := httptest.NewRecorder() 60 | 61 | router := httprouter.New() 62 | handler := func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { 63 | w.WriteHeader(404) 64 | } 65 | router.GET("/does_not_exist", Middleware(handler)) 66 | router.ServeHTTP(w, r) 67 | 68 | evs := mo.Events() 69 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 70 | fields := evs[0].Data 71 | status, ok := fields["response.status_code"] 72 | assert.True(t, ok, "'status_code' field must exist on middleware generated event") 73 | assert.Equal(t, http.StatusNotFound, status) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /examples/echo_ex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/honeycombio/beeline-go" 8 | "github.com/honeycombio/beeline-go/wrappers/hnyecho" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func main() { 13 | // Initialize beeline. The only required field is WriteKey. 14 | beeline.Init(beeline.Config{ 15 | WriteKey: "abcabc123123", 16 | Dataset: "http-echo", 17 | // for demonstration, send the event to STDOUT instead of Honeycomb. 18 | // Remove the STDOUT setting when filling in a real write key. 19 | // NOTE: This should *only* be set to true in development environments. 20 | // Setting to true is Production environments can cause problems. 21 | STDOUT: true, 22 | }) 23 | // ensure everything gets sent off before we exit 24 | defer beeline.Close() 25 | 26 | // set up Echo router with hnyecho middleware to provide honeycomb instrumentation 27 | router := echo.New() 28 | router.Use(hnyecho.New().Middleware()) 29 | 30 | // set up routes for hello and bye 31 | router.GET("/hello/:name", hello) 32 | router.GET("/bye/:name", bye) 33 | 34 | // start the Echo router (make sure nothing else is running on 8080) 35 | router.Start(":8080") 36 | } 37 | 38 | func hello(c echo.Context) error { 39 | c.Request().Context() 40 | beeline.AddField(c.Request().Context(), "custom", "in hello") 41 | name := c.Param("name") // path param is added to event 42 | 43 | return c.String(http.StatusOK, fmt.Sprintf("Hello, %s!\n", name)) 44 | } 45 | 46 | func bye(c echo.Context) error { 47 | c.Request().Context() 48 | beeline.AddField(c.Request().Context(), "custom", "in bye") 49 | name := c.Param("name") // path param is added to event 50 | 51 | return c.String(http.StatusOK, fmt.Sprintf("Goodbye, %s!\n", name)) 52 | } 53 | 54 | // 55 | // a curl to localhost:8080/hello/ben gets you an event that looks like this: 56 | // 57 | // { 58 | // "data": { 59 | // "app.custom": "in hello", 60 | // "duration_ms": 0.031066, 61 | // "handler.name": "main.hello", 62 | // "meta.beeline_version": "0.3.6", 63 | // "meta.local_hostname": "jamietsao", 64 | // "meta.span_type": "root", 65 | // "meta.type": "http_request", 66 | // "name": "main.hello", 67 | // "request.content_length": 0, 68 | // "request.header.user_agent": "curl/7.54.0", 69 | // "request.host": "localhost:8080", 70 | // "request.http_version": "HTTP/1.1", 71 | // "request.method": "GET", 72 | // "request.path": "/hello/ben", 73 | // "request.remote_addr": "[::1]:56807", 74 | // "request.url": "/hello/ben", 75 | // "response.size": 12, 76 | // "response.status_code": 200, 77 | // "route": "/hello/:name", 78 | // "route.handler": "main.hello", 79 | // "route.params.name": "ben", 80 | // "trace.span_id": "9a20ecc7-de00-4417-bfb8-9a46616e30bc", 81 | // "trace.trace_id": "c5f54e2e-3e42-4338-a3a9-5edb95012d0a" 82 | // }, 83 | // "time": "2019-03-25T18:24:21.780222-07:00", 84 | // "dataset": "http-echo" 85 | // } 86 | -------------------------------------------------------------------------------- /examples/goji/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/honeycombio/beeline-go" 8 | "github.com/honeycombio/beeline-go/wrappers/hnygoji" 9 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 10 | 11 | "goji.io/v3" 12 | "goji.io/v3/pat" 13 | ) 14 | 15 | func main() { 16 | // Initialize beeline. The only required field is WriteKey. 17 | beeline.Init(beeline.Config{ 18 | WriteKey: "abcabc123123", 19 | Dataset: "http-goji", 20 | // for demonstration, send the event to STDOUT instead of Honeycomb. 21 | // Remove the STDOUT setting when filling in a real write key. 22 | // NOTE: This should *only* be set to true in development environments. 23 | // Setting to true is Production environments can cause problems. 24 | STDOUT: true, 25 | }) 26 | // ensure everything gets sent off before we exit 27 | defer beeline.Close() 28 | 29 | // this example uses a submux just to illustrate the middleware's use 30 | root := goji.NewMux() 31 | root.HandleFunc(pat.Get("/hello/:name"), hello) 32 | root.HandleFunc(pat.Get("/bye/:name"), bye) 33 | 34 | // decorate calls that hit the greetings submux with extra fields 35 | // this call adds things like the goji pattern to the event 36 | root.Use(hnygoji.Middleware) 37 | 38 | // wrap the main root handler to get an event out of every request. This 39 | // gets all the default fields like remote address and status code 40 | http.ListenAndServe("localhost:8080", hnynethttp.WrapHandler(root)) 41 | } 42 | 43 | func hello(w http.ResponseWriter, r *http.Request) { 44 | beeline.AddField(r.Context(), "custom", "in hello") 45 | name := pat.Param(r, "name") // pat is automatically added to the event 46 | fmt.Fprintf(w, "Hello, %s!\n", name) 47 | } 48 | 49 | func bye(w http.ResponseWriter, r *http.Request) { 50 | beeline.AddField(r.Context(), "custom", "in bye") 51 | name := pat.Param(r, "name") // pat is automatically added to the event 52 | fmt.Fprintf(w, "goodbye, %s!", name) 53 | } 54 | 55 | // 56 | // a curl to localhost:8080/hello/ben gets you an event that looks like this: 57 | // 58 | // { 59 | // "data": { 60 | // "app.custom": "in hello", 61 | // "duration_ms": 0.632589, 62 | // "goji.methods": { 63 | // "GET": {}, 64 | // "HEAD": {} 65 | // }, 66 | // "goji.pat": "/hello/:name", 67 | // "goji.pat.name": "ben", 68 | // "goji.path_prefix": "/hello/", 69 | // "handler.name": "main.hello", 70 | // "handler.type": "http.HandlerFunc", 71 | // "meta.beeline_version": "0.1.0", 72 | // "meta.local_hostname": "cobbler", 73 | // "meta.type": "http", 74 | // "name": "main.hello", 75 | // "request.content_length": 0, 76 | // "request.header.user_agent": "curl/7.54.0", 77 | // "request.host": "localhost:8080", 78 | // "request.http_version": "HTTP/1.1", 79 | // "request.method": "GET", 80 | // "request.path": "/hello/ben", 81 | // "request.remote_addr": "127.0.0.1:55532", 82 | // "response.status_code": 200, 83 | // "trace.span_id": "8be4e6bc-143d-41e5-9cac-b021444f8998", 84 | // "trace.trace_id": "70761b4d-078a-4fac-a731-bbe9ef3bf542" 85 | // }, 86 | // "time": "2018-05-15T23:41:39.121095627-07:00" 87 | // } 88 | -------------------------------------------------------------------------------- /trace/context.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "runtime/pprof" 7 | ) 8 | 9 | const ( 10 | honeySpanContextKey = "honeycombSpanContextKey" 11 | honeyTraceContextKey = "honeycombTraceContextKey" 12 | profileIDLabelName = "span_id" 13 | ) 14 | 15 | var ( 16 | ErrTraceNotFoundInContext = errors.New("beeline trace not found in source context") 17 | ) 18 | 19 | // GetTraceFromContext retrieves a trace from the passed in context or returns 20 | // nil if no trace exists. 21 | func GetTraceFromContext(ctx context.Context) *Trace { 22 | if ctx != nil { 23 | if val := ctx.Value(honeyTraceContextKey); val != nil { 24 | if trace, ok := val.(*Trace); ok { 25 | return trace 26 | } 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | // PutTraceInContext takes an existing context and a trace and pushes the trace 33 | // into the context. It will replace any traces that already exist in the 34 | // context. Traces put in context are retrieved using GetTraceFromContext. 35 | func PutTraceInContext(ctx context.Context, trace *Trace) context.Context { 36 | // Copy the old context object to preserve its pprof labels to restore later. 37 | oldCtx := ctx 38 | ctx = context.WithValue(ctx, honeyTraceContextKey, trace) 39 | if GlobalConfig.PprofTagging && trace != nil && trace.GetRootSpan() != nil { 40 | // This returns a pointer type, so it's safe to directly manipulate fields. 41 | rootSpan := trace.GetRootSpan() 42 | rootSpan.oldCtx = &oldCtx 43 | ctx = pprof.WithLabels(ctx, pprof.Labels(profileIDLabelName, rootSpan.GetSpanID())) 44 | pprof.SetGoroutineLabels(ctx) 45 | } 46 | return ctx 47 | } 48 | 49 | // GetSpanFromContext identifies the currently active span via the span context 50 | // key. It returns that span, and access to the trace is available via the span 51 | // or from the context directly. It will return nil if there is no span 52 | // available. 53 | func GetSpanFromContext(ctx context.Context) *Span { 54 | if ctx != nil { 55 | if val := ctx.Value(honeySpanContextKey); val != nil { 56 | if span, ok := val.(*Span); ok { 57 | return span 58 | } 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | // PutSpanInContext takes an existing context and a span and pushes the span 65 | // into the context. It will replace any spans that already exist in the 66 | // context. Spans put in context are retrieved using GetSpanFromContext. 67 | func PutSpanInContext(ctx context.Context, span *Span) context.Context { 68 | return context.WithValue(ctx, honeySpanContextKey, span) 69 | } 70 | 71 | // CopyContext takes a context that has a beeline trace and one that doesn't. It 72 | // copies all the bits necessary to continue the trace from one to the other. 73 | // This is useful if you need to break context to launch a goroutine that 74 | // shouldn't be cancelled by the parent's cancellation context. It returns the 75 | // newly populated context. If it can't find a trace in the source context, it 76 | // returns the unchanged dest context with an error. 77 | func CopyContext(dest context.Context, src context.Context) (context.Context, error) { 78 | trace := GetTraceFromContext(src) 79 | span := GetSpanFromContext(src) 80 | if trace == nil || span == nil { 81 | return dest, ErrTraceNotFoundInContext 82 | } 83 | dest = PutTraceInContext(dest, trace) 84 | dest = PutSpanInContext(dest, span) 85 | return dest, nil 86 | } 87 | -------------------------------------------------------------------------------- /wrappers/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/honeycombio/beeline-go/propagation" 8 | "google.golang.org/grpc/metadata" 9 | ) 10 | 11 | // HTTPTraceParserHook is a function that will be invoked on all incoming HTTP requests 12 | // when it is passed as a parameter to an http.Handler wrapper function such as the 13 | // one provided in the hnynethttp package. It can be used to create a PropagationContext 14 | // object using trace context propagation headers in the provided http.Request. It is 15 | // expected that this hook will use one of the unmarshal functions exported in the 16 | // propagation package for a number of supported formats (e.g. Honeycomb, AWS, 17 | // W3C Trace Context, etc). 18 | type HTTPTraceParserHook func(*http.Request) *propagation.PropagationContext 19 | 20 | // HTTPTracePropagationHook is a function that will be invoked on all outgoing HTTP requests 21 | // when it is passed as a parameter to a RoundTripper wrapper function such as the one 22 | // provided in the hnynethttp package. It can be used to create a map of header names 23 | // to header values that will be injected in the outgoing request. The information in 24 | // the provided http.Request can be used to make decisions about what headers to include 25 | // in the outgoing request, for example based on the hostname of the target of the request. 26 | // The information in the provided PropagationContext should be used to create the serialized 27 | // header values. It is expected that this hook will use one of the marshal functions exported 28 | // in the propagation package for a number of supported formats (e.g. Honeycomb, AWS, 29 | // W3C Trace Context, etc). 30 | type HTTPTracePropagationHook func(*http.Request, *propagation.PropagationContext) map[string]string 31 | 32 | // HTTPIncomingConfig stores configuration options relevant to HTTP requests that are handled by 33 | // a wrapper. 34 | type HTTPIncomingConfig struct { 35 | HTTPParserHook HTTPTraceParserHook 36 | } 37 | 38 | // HTTPOutgoingConfig stores configuration options relevant to HTTP requests being sent by an 39 | // instrumented application. 40 | type HTTPOutgoingConfig struct { 41 | HTTPPropagationHook HTTPTracePropagationHook 42 | } 43 | 44 | // GRPCTraceParserHook is a function that will be invoked on all incoming gRPC requests 45 | // when it is passed as a parameter to an interceptor wrapper function such as the one 46 | // provided in the hnygrpc package. It can be used to create a PropagationContext object 47 | // using trace context propagation headers in the provided context. It is functionally 48 | // identical to its HTTP counterpart, HTTPTraceParserHook. 49 | type GRPCTraceParserHook func(context.Context) *propagation.PropagationContext 50 | 51 | // GRPCTracePropagationHook is a function that will be invoked on all outgoing gRPC requests 52 | // when it is passed as a parameter to a client interceptor wrapper function such as the one 53 | // provided in the hnygrpc package. It can be used to create a gRPC metadata object 54 | // that will be injected into the outgoing request. It is functionally identical 55 | // to its HTTP counterpart, HTTPTracePropagationHook. 56 | type GRPCTracePropagationHook func(*propagation.PropagationContext) metadata.MD 57 | 58 | // GRPCIncomingConfig stores configuration options relevant to gRPC requests that are 59 | // handled by a wrapped gRPC interceptor provided in the hnygrpc package. 60 | type GRPCIncomingConfig struct { 61 | GRPCParserHook GRPCTraceParserHook 62 | } 63 | 64 | // GRPCOutgoingConfig stores configuration options relevant to gRPC requests being sent 65 | // by an instrumented application. 66 | type GRPCOutgoingConfig struct { 67 | GRPCPropagationHook GRPCTracePropagationHook 68 | } 69 | -------------------------------------------------------------------------------- /wrappers/hnynethttp/nethttp_test.go: -------------------------------------------------------------------------------- 1 | package hnynethttp 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | beeline "github.com/honeycombio/beeline-go" 9 | libhoney "github.com/honeycombio/libhoney-go" 10 | "github.com/honeycombio/libhoney-go/transmission" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestWrapHandlerFunc(t *testing.T) { 15 | // set up libhoney to catch events instead of send them 16 | mo := &transmission.MockSender{} 17 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 18 | APIKey: "placeholder", 19 | Dataset: "placeholder", 20 | APIHost: "placeholder", 21 | Transmission: mo}) 22 | assert.Equal(t, nil, err) 23 | beeline.Init(beeline.Config{Client: client}) 24 | // build a sample request to generate an event 25 | r, _ := http.NewRequest("GET", "/hello", nil) 26 | w := httptest.NewRecorder() 27 | 28 | // build the wrapped handhler on the default mux 29 | http.HandleFunc("/hello", WrapHandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) 30 | http.HandleFunc("/fail", WrapHandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusTeapot) })) 31 | 32 | // handle successful request 33 | http.DefaultServeMux.ServeHTTP(w, r) 34 | 35 | // set up + handle failed request 36 | r, _ = http.NewRequest("GET", "/fail", nil) 37 | w = httptest.NewRecorder() 38 | http.DefaultServeMux.ServeHTTP(w, r) 39 | 40 | // verify the MockOutput caught the well formed event 41 | evs := mo.Events() 42 | assert.Equal(t, 2, len(evs), "one event is created with one request through the wrapped handler function") 43 | successfulFields := evs[0].Data 44 | status, ok := successfulFields["response.status_code"] 45 | assert.True(t, ok, "status field must exist on middleware generated event") 46 | assert.Equal(t, 200, status, "successfully served request should have status 200") 47 | 48 | failedFields := evs[1].Data 49 | status, ok = failedFields["response.status_code"] 50 | assert.True(t, ok, "status field must exist on middleware generated event") 51 | assert.Equal(t, http.StatusTeapot, status, "served /fail request should have status 418") 52 | } 53 | 54 | func TestWrapHandler(t *testing.T) { 55 | // set up libhoney to catch events instead of send them 56 | mo := &transmission.MockSender{} 57 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 58 | APIKey: "placeholder", 59 | Dataset: "placeholder", 60 | APIHost: "placeholder", 61 | Transmission: mo}) 62 | assert.Equal(t, nil, err) 63 | beeline.Init(beeline.Config{Client: client}) 64 | // build a sample request to generate an event 65 | r, _ := http.NewRequest("GET", "/hello", nil) 66 | w := httptest.NewRecorder() 67 | 68 | // build the wrapped handler 69 | globalmux := http.NewServeMux() 70 | globalmux.HandleFunc("/hello", func(_ http.ResponseWriter, _ *http.Request) {}) 71 | globalmux.HandleFunc("/fail", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusTeapot) }) 72 | // handle the request 73 | WrapHandler(globalmux).ServeHTTP(w, r) 74 | 75 | // set up + handle failed request 76 | r, _ = http.NewRequest("GET", "/fail", nil) 77 | w = httptest.NewRecorder() 78 | http.DefaultServeMux.ServeHTTP(w, r) 79 | 80 | // verify the MockOutput caught the well formed event 81 | evs := mo.Events() 82 | assert.Equal(t, 2, len(evs), "one event is created with one request through the Middleware") 83 | fields := evs[0].Data 84 | status, ok := fields["response.status_code"] 85 | assert.True(t, ok, "status field must exist on middleware generated event") 86 | assert.Equal(t, 200, status, "successfully served request should have status 200") 87 | 88 | failedFields := evs[1].Data 89 | status, ok = failedFields["response.status_code"] 90 | assert.True(t, ok, "status field must exist on middleware generated event") 91 | assert.Equal(t, http.StatusTeapot, status, "served /fail request should have status 418") 92 | } 93 | -------------------------------------------------------------------------------- /examples/http_and_sql/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | _ "github.com/go-sql-driver/mysql" 11 | 12 | "github.com/honeycombio/beeline-go" 13 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 14 | "github.com/honeycombio/beeline-go/wrappers/hnysql" 15 | ) 16 | 17 | func main() { 18 | // Initialize beeline. The only required field is WriteKey. 19 | beeline.Init(beeline.Config{ 20 | WriteKey: "abcabc123123", 21 | Dataset: "http+sql", 22 | // for demonstration, send the event to STDOUT instead of Honeycomb. 23 | // Remove the STDOUT setting when filling in a real write key. 24 | // NOTE: This should *only* be set to true in development environments. 25 | // Setting to true is Production environments can cause problems. 26 | STDOUT: true, 27 | }) 28 | 29 | // open a regular sqlx connection and wrap it 30 | odb, err := sql.Open("mysql", "root:@tcp(127.0.0.1)/donut") 31 | if err != nil { 32 | fmt.Printf("connection err: %s\n", err) 33 | return 34 | } 35 | db := hnysql.WrapDB(odb) 36 | 37 | // hand it to the app for use in the handler 38 | a := &app{} 39 | a.db = db 40 | 41 | globalmux := http.NewServeMux() 42 | globalmux.HandleFunc("/hello/", a.hello) 43 | 44 | // wrap the globalmux with the honeycomb middleware to send one event per 45 | // request 46 | log.Fatal(http.ListenAndServe("localhost:8080", hnynethttp.WrapHandler(globalmux))) 47 | } 48 | 49 | type app struct { 50 | db *hnysql.DB 51 | } 52 | 53 | func (a *app) hello(w http.ResponseWriter, r *http.Request) { 54 | ctx := r.Context() 55 | 56 | // get all flavors from the DB 57 | rows, err := a.db.QueryContext(ctx, "SELECT flavor FROM flavors GROUP BY flavor") 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | defer rows.Close() 62 | 63 | // collect them 64 | flavors := []string{} 65 | for rows.Next() { 66 | var flavor string 67 | if err := rows.Scan(&flavor); err != nil { 68 | log.Fatal(err) 69 | } 70 | flavors = append(flavors, flavor) 71 | } 72 | // add some custom fields to the Honeycomb event 73 | beeline.AddField(ctx, "flavors_count", len(flavors)) 74 | beeline.AddField(ctx, "flavors", flavors) 75 | 76 | // send our response to the caller 77 | io.WriteString(w, 78 | fmt.Sprintf("Hello world! Here are our %d flavors:\n", len(flavors))) 79 | for _, flavor := range flavors { 80 | io.WriteString(w, flavor+"\n") 81 | } 82 | } 83 | 84 | // A call to the hello endpoint produces two events, one for the HTTP request 85 | // and one for the SQL call. They look like this: 86 | // 87 | // { 88 | // "data": { 89 | // "duration_ms": 2.735045, 90 | // "flavors": ["chocolate","mint","rose","vanilla"], 91 | // "flavors_count": 4, 92 | // "meta.localhostname": "cobbler", 93 | // "meta.type": "http request", 94 | // "mux.handler.name": "main.(*app).(main.hello)-fm", 95 | // "mux.handler.pattern": "/hello/", 96 | // "mux.handler.type": "http.HandlerFunc", 97 | // "request.content_length": 0, 98 | // "request.header.user_agent": "curl/7.54.0", 99 | // "request.host": "localhost:8080", 100 | // "request.method": "GET", 101 | // "request.path": "/hello/foo", 102 | // "request.proto": "HTTP/1.1", 103 | // "request.remote_addr": "[::1]:52317", 104 | // "response.status_code": 200 105 | // "trace.trace_id": "a0eca504-a652-46da-b968-07dd076e2d0c", 106 | // }, 107 | // "time": "2018-04-06T22:42:18.449138413-07:00" 108 | // } 109 | // { 110 | // "data": { 111 | // "sql.call": "QueryContext", 112 | // "duration_ms": 1.75518, 113 | // "meta.localhostname": "cobbler", 114 | // "meta.type": "sql", 115 | // "sql.open_conns": 0, 116 | // "sql.query": "SELECT flavor FROM flavors GROUP BY flavor" 117 | // "trace.trace_id": "a0eca504-a652-46da-b968-07dd076e2d0c", 118 | // }, 119 | // "time": "2018-04-06T22:42:18.449620729-07:00" 120 | // } 121 | -------------------------------------------------------------------------------- /examples/grpc/proto/hello_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package proto 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // HelloServiceClient is the client API for HelloService service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type HelloServiceClient interface { 21 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) 22 | } 23 | 24 | type helloServiceClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient { 29 | return &helloServiceClient{cc} 30 | } 31 | 32 | func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { 33 | out := new(HelloResponse) 34 | err := c.cc.Invoke(ctx, "/HelloService/SayHello", in, out, opts...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return out, nil 39 | } 40 | 41 | // HelloServiceServer is the server API for HelloService service. 42 | // All implementations must embed UnimplementedHelloServiceServer 43 | // for forward compatibility 44 | type HelloServiceServer interface { 45 | SayHello(context.Context, *HelloRequest) (*HelloResponse, error) 46 | mustEmbedUnimplementedHelloServiceServer() 47 | } 48 | 49 | // UnimplementedHelloServiceServer must be embedded to have forward compatible implementations. 50 | type UnimplementedHelloServiceServer struct { 51 | } 52 | 53 | func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) { 54 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 55 | } 56 | func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {} 57 | 58 | // UnsafeHelloServiceServer may be embedded to opt out of forward compatibility for this service. 59 | // Use of this interface is not recommended, as added methods to HelloServiceServer will 60 | // result in compilation errors. 61 | type UnsafeHelloServiceServer interface { 62 | mustEmbedUnimplementedHelloServiceServer() 63 | } 64 | 65 | func RegisterHelloServiceServer(s grpc.ServiceRegistrar, srv HelloServiceServer) { 66 | s.RegisterService(&HelloService_ServiceDesc, srv) 67 | } 68 | 69 | func _HelloService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 70 | in := new(HelloRequest) 71 | if err := dec(in); err != nil { 72 | return nil, err 73 | } 74 | if interceptor == nil { 75 | return srv.(HelloServiceServer).SayHello(ctx, in) 76 | } 77 | info := &grpc.UnaryServerInfo{ 78 | Server: srv, 79 | FullMethod: "/HelloService/SayHello", 80 | } 81 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 82 | return srv.(HelloServiceServer).SayHello(ctx, req.(*HelloRequest)) 83 | } 84 | return interceptor(ctx, in, info, handler) 85 | } 86 | 87 | // HelloService_ServiceDesc is the grpc.ServiceDesc for HelloService service. 88 | // It's only intended for direct use with grpc.RegisterService, 89 | // and not to be introspected or modified (even as a copy) 90 | var HelloService_ServiceDesc = grpc.ServiceDesc{ 91 | ServiceName: "HelloService", 92 | HandlerType: (*HelloServiceServer)(nil), 93 | Methods: []grpc.MethodDesc{ 94 | { 95 | MethodName: "SayHello", 96 | Handler: _HelloService_SayHello_Handler, 97 | }, 98 | }, 99 | Streams: []grpc.StreamDesc{}, 100 | Metadata: "hello.proto", 101 | } 102 | -------------------------------------------------------------------------------- /examples/nethttp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "math" 11 | "math/rand" 12 | "net/http" 13 | "strings" 14 | "time" 15 | 16 | _ "github.com/go-sql-driver/mysql" 17 | 18 | "github.com/honeycombio/beeline-go" 19 | "github.com/honeycombio/beeline-go/wrappers/hnynethttp" 20 | ) 21 | 22 | func main() { 23 | // Initialize beeline. The only required field is WriteKey. 24 | beeline.Init(beeline.Config{ 25 | WriteKey: "abcabc123123abcabc", 26 | Dataset: "beeline-example", 27 | ServiceName: "sample app", 28 | // SamplerHook: sampler, 29 | // PresendHook: presend, 30 | // for demonstration, send the event to STDOUT instead of Honeycomb. 31 | // Remove the STDOUT setting when filling in a real write key. 32 | // NOTE: This should *only* be set to true in development environments. 33 | // Setting to true is Production environments can cause problems. 34 | STDOUT: true, 35 | }) 36 | 37 | globalmux := http.NewServeMux() 38 | globalmux.HandleFunc("/hello/", hello) 39 | 40 | // wrap the globalmux with the honeycomb middleware to send one event per 41 | // request 42 | log.Fatal(http.ListenAndServe("localhost:8080", hnynethttp.WrapHandler(globalmux))) 43 | } 44 | 45 | func hello(w http.ResponseWriter, r *http.Request) { 46 | beeline.AddField(r.Context(), "email", "one@two.com") 47 | bigJob(r.Context()) 48 | outboundCall(r.Context()) 49 | // send our response to the caller 50 | io.WriteString(w, fmt.Sprintf("Hello world!\n")) 51 | } 52 | 53 | // bigJob is going to take a long time and do lots of interesting work. It 54 | // should get its own span. 55 | func bigJob(ctx context.Context) { 56 | ctx, span := beeline.StartSpan(ctx, "bigJob") 57 | defer span.Send() 58 | beeline.AddField(ctx, "m1", 5.67) 59 | beeline.AddField(ctx, "m2", 8.90) 60 | // bigJob will take ~300ms 61 | sleepTime := math.Abs(200.0 + (rand.NormFloat64()*50 + 100)) 62 | time.Sleep(time.Duration(sleepTime) * time.Millisecond) 63 | // this job also discovered something that's relevant to the whole trace 64 | beeline.AddFieldToTrace(ctx, "vip_user", true) 65 | } 66 | 67 | // outboundCall demonstrates wrapping an outbound HTTP client 68 | func outboundCall(ctx context.Context) { 69 | // let's make an outbound HTTP call 70 | client := &http.Client{ 71 | Transport: hnynethttp.WrapRoundTripper(http.DefaultTransport), 72 | Timeout: time.Second * 5, 73 | } 74 | req, _ := http.NewRequest(http.MethodGet, "http://scooterlabs.com/echo.json", strings.NewReader("")) 75 | req = req.WithContext(ctx) 76 | resp, err := client.Do(req) 77 | if err == nil { 78 | defer resp.Body.Close() 79 | bod, _ := ioutil.ReadAll(resp.Body) 80 | // data, _ := base64.StdEncoding.DecodeString(string(bod)) 81 | beeline.AddField(ctx, "resp.body", string(bod)) 82 | } 83 | } 84 | 85 | func presend(fields map[string]interface{}) { 86 | // If the email address field exists, add a field representing the 87 | // domain of the user's email address and hash the original email 88 | if email, ok := fields["app.email"]; ok { 89 | if emailStr, ok := email.(string); ok { 90 | splitEmail := strings.SplitN(emailStr, "@", 2) 91 | if len(splitEmail) == 2 { 92 | domain := splitEmail[1] 93 | fields["doamin"] = domain 94 | } 95 | // then hash the email so it is obscured 96 | hashedEmail := sha256.Sum256([]byte(fmt.Sprintf("%v", emailStr))) 97 | fields["app.email"] = fmt.Sprintf("%x", hashedEmail) 98 | } 99 | } 100 | } 101 | 102 | func sampler(fields map[string]interface{}) (bool, int) { 103 | // example sampler that samples at 1/3 when the "m1" field is present and 104 | // 1/2 when it is absent 105 | var sampleRate = 2 106 | if _, ok := fields["app.m1"]; ok { 107 | sampleRate = 3 108 | } 109 | if rand.Intn(sampleRate) == 0 { 110 | // keep the event! 111 | return true, sampleRate 112 | } 113 | // sample rate here doesn't matter because the event is going to get 114 | // dropped 115 | return false, 0 116 | } 117 | -------------------------------------------------------------------------------- /wrappers/hnysql/sql_test.go: -------------------------------------------------------------------------------- 1 | package hnysql_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "testing" 9 | 10 | "github.com/DATA-DOG/go-sqlmock" 11 | _ "github.com/go-sql-driver/mysql" 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/honeycombio/beeline-go" 15 | "github.com/honeycombio/beeline-go/wrappers/hnysql" 16 | ) 17 | 18 | func Example() { 19 | // Initialize beeline. The only required field is WriteKey. 20 | beeline.Init(beeline.Config{ 21 | WriteKey: "abcabc123123", 22 | Dataset: "sql", 23 | // for demonstration, send the event to STDOUT intead of Honeycomb. 24 | // Remove the STDOUT setting when filling in a real write key. 25 | // NOTE: This should *only* be set to true in development environments. 26 | // Setting to true is Production environments can cause problems. 27 | STDOUT: true, 28 | }) 29 | // and make sure we close to force flushing all pending events before shutdown 30 | defer beeline.Close() 31 | 32 | // open a regular sql.DB connection 33 | odb, err := sql.Open("mysql", "root:@tcp(127.0.0.1)/donut") 34 | if err != nil { 35 | fmt.Printf("connection err: %s\n", err) 36 | return 37 | } 38 | 39 | // replace it with a wrapped hnysql.DB 40 | db := hnysql.WrapDB(odb) 41 | // and start up a trace to capture all the calls 42 | ctx, span := beeline.StartSpan(context.Background(), "start") 43 | defer span.Send() 44 | 45 | // from here on, all SQL calls will emit events. 46 | 47 | db.ExecContext(ctx, "insert into flavors (flavor) values ('rose')") 48 | fv := "rose" 49 | rows, err := db.QueryContext(ctx, "SELECT id FROM flavors WHERE flavor=?", fv) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | defer rows.Close() 54 | for rows.Next() { 55 | var id int 56 | if err := rows.Scan(&id); err != nil { 57 | log.Fatal(err) 58 | } 59 | fmt.Printf("%d is %s\n", id, fv) 60 | } 61 | if err := rows.Err(); err != nil { 62 | log.Fatal(err) 63 | } 64 | } 65 | 66 | func TestSQLMiddleware(t *testing.T) { 67 | beeline.Init(beeline.Config{ 68 | WriteKey: "abcabc123123", 69 | Dataset: "sql", 70 | // for demonstration, send the event to STDOUT intead of Honeycomb. 71 | // Remove the STDOUT setting when filling in a real write key. 72 | // NOTE: This should *only* be set to true in development environments. 73 | // Setting to true is Production environments can cause problems. 74 | STDOUT: true, 75 | }) 76 | // and make sure we close to force flushing all pending events before shutdown 77 | defer beeline.Close() 78 | 79 | // Open a mock sql connection. 80 | odb, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) 81 | if err != nil { 82 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 83 | } 84 | defer odb.Close() 85 | 86 | mock.ExpectExec("insert into flavors.+").WillReturnResult(sqlmock.NewResult(0, 0)) 87 | mock.ExpectQuery("SELECT id FROM flavors.+").WillReturnRows(sqlmock.NewRows([]string{"1"})) 88 | mock.ExpectPing() 89 | 90 | // replace it with a wrapped hnysql.DB 91 | db := hnysql.WrapDB(odb) 92 | // and start up a trace to capture all the calls 93 | ctx, span := beeline.StartSpan(context.Background(), "start") 94 | defer span.Send() 95 | 96 | // from here on, all SQL calls will emit events. 97 | 98 | _, err = db.ExecContext(ctx, "insert into flavors (flavor) values ('rose')") 99 | assert.Nil(t, err) 100 | fv := "rose" 101 | rows, err := db.QueryContext(ctx, "SELECT id FROM flavors WHERE flavor=?", fv) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | defer rows.Close() 106 | for rows.Next() { 107 | var id int 108 | if err := rows.Scan(&id); err != nil { 109 | log.Fatal(err) 110 | } 111 | fmt.Printf("%d is %s\n", id, fv) 112 | } 113 | if err := rows.Err(); err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | if err := db.PingContext(ctx); err != nil { 118 | log.Fatal(err) 119 | } 120 | 121 | if err := mock.ExpectationsWereMet(); err != nil { 122 | t.Errorf("there were unfulfilled expectations: %s", err) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /wrappers/hnyecho/echo_test.go: -------------------------------------------------------------------------------- 1 | package hnyecho 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/honeycombio/beeline-go" 10 | "github.com/honeycombio/libhoney-go" 11 | "github.com/honeycombio/libhoney-go/transmission" 12 | "github.com/labstack/echo/v4" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | var ( 17 | errWoops = errors.New("woops") 18 | ) 19 | 20 | func TestEchoMiddleware(t *testing.T) { 21 | evCatcher := beelineSetup(t) 22 | 23 | // build a sample request to generate an event 24 | r, _ := http.NewRequest("GET", "/hello/pooh", nil) 25 | w := httptest.NewRecorder() 26 | 27 | // set up the Echo router with the EchoWrapper middleware 28 | router := echo.New() 29 | router.Use(New().Middleware()) 30 | router.GET("/hello/:name", helloHandler) 31 | // handle the request 32 | router.ServeHTTP(w, r) 33 | 34 | // verify the MockOutput caught the well formed event 35 | evs := evCatcher.Events() 36 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 37 | fields := evs[0].Data 38 | // status code 39 | status, ok := fields["response.status_code"] 40 | assert.True(t, ok, "response.status_code field must exist on middleware generated event") 41 | assert.Equal(t, 200, status, "successfully served request should have status 200") 42 | // response size 43 | size, ok := fields["response.size"] 44 | assert.True(t, ok, "response.size field must exist on middleware generated event") 45 | assert.Equal(t, int64(2), size, "successfully served request should have a response size of 2") 46 | // handler fields 47 | handlerNameFields := []string{"handler.name", "name", "route.handler"} 48 | for _, field := range handlerNameFields { 49 | handler, ok := fields[field] 50 | assert.True(t, ok, "handler.name field must exist on middleware generated event") 51 | assert.Equal(t, "github.com/honeycombio/beeline-go/wrappers/hnyecho.helloHandler", handler, "successfully served request should have correct matched handler") 52 | } 53 | 54 | // route fields 55 | route, ok := fields["route"] 56 | assert.True(t, ok, "route field must exist on middleware generated event") 57 | assert.Equal(t, "/hello/:name", route, "successfully served request should have matched route") 58 | name, ok := fields["route.params.name"] 59 | assert.True(t, ok, "route.params.name field must exist on middleware generated event") 60 | assert.Equal(t, "pooh", name, "successfully served request should have path param 'name' populated") 61 | } 62 | 63 | func TestEchoMiddlewareErrors(t *testing.T) { 64 | evCatcher := beelineSetup(t) 65 | 66 | // build a sample request to generate an event 67 | r, _ := http.NewRequest("GET", "/error", nil) 68 | w := httptest.NewRecorder() 69 | 70 | // set up the Echo router with the EchoWrapper middleware 71 | router := echo.New() 72 | router.Use(New().Middleware()) 73 | router.GET("/error", errorHandler) 74 | // handle the request 75 | router.ServeHTTP(w, r) 76 | 77 | // verify the MockOutput caught the well formed event 78 | evs := evCatcher.Events() 79 | assert.Equal(t, 1, len(evs), "one event is created with one request through the Middleware") 80 | fields := evs[0].Data 81 | // status code 82 | status, ok := fields["response.status_code"] 83 | assert.True(t, ok, "response.status_code field must exist on middleware generated event") 84 | assert.Equal(t, 500, status, "successfully served request should have status 500") 85 | 86 | // response error 87 | echoErr, ok := fields["echo.error"] 88 | assert.True(t, ok, "echo.error field must exist on middleware generated event") 89 | assert.Equal(t, errWoops.Error(), echoErr) 90 | 91 | } 92 | 93 | func beelineSetup(t *testing.T) *transmission.MockSender { 94 | // set up libhoney to catch events instead of send them 95 | evCatcher := &transmission.MockSender{} 96 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 97 | APIKey: "abcd", 98 | Dataset: "efgh", 99 | APIHost: "ijkl", 100 | Transmission: evCatcher, 101 | }) 102 | assert.Equal(t, nil, err) 103 | beeline.Init(beeline.Config{Client: client}) 104 | 105 | return evCatcher 106 | } 107 | 108 | func helloHandler(c echo.Context) error { 109 | return c.String(http.StatusOK, "ok") 110 | } 111 | 112 | func errorHandler(c echo.Context) error { 113 | return errWoops 114 | } 115 | -------------------------------------------------------------------------------- /propagation/honeycomb.go: -------------------------------------------------------------------------------- 1 | package propagation 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | // assumes a header of the form: 12 | 13 | // VERSION;PAYLOAD 14 | 15 | // VERSION=1 16 | // ========= 17 | // PAYLOAD is a list of comma-separated params (k=v pairs), with no spaces. recognized 18 | // keys + value types: 19 | // 20 | // trace_id=${traceId} - traceId is an opaque ascii string which shall not include ',' 21 | // parent_id=${spanId} - spanId is an opaque ascii string which shall not include ',' 22 | // dataset=${datasetId} - datasetId is the slug for the honeycomb dataset to which downstream spans should be sent; shall not include ',' 23 | // context=${contextBlob} - contextBlob is a base64 encoded json object. 24 | // 25 | // ex: X-Honeycomb-Trace: 1;trace_id=weofijwoeifj,parent_id=owefjoweifj,context=eyJoZWxsbyI6IndvcmxkIn0= 26 | 27 | const ( 28 | TracePropagationGRPCHeader = "x-honeycomb-trace" // difference in case matters here 29 | TracePropagationHTTPHeader = "X-Honeycomb-Trace" 30 | TracePropagationVersion = 1 31 | ) 32 | 33 | // MarshalHoneycombTraceContext uses the information in prop to create a trace context header 34 | // in the Honeycomb trace header format. It returns the serialized form of the trace context, 35 | // ready to be inserted into the headers of an outbound HTTP request. 36 | // 37 | // If prop is nil, the returned value will be an empty string. 38 | func MarshalHoneycombTraceContext(prop *PropagationContext) string { 39 | if prop == nil { 40 | return "" 41 | } 42 | tcJSON, err := json.Marshal(prop.TraceContext) 43 | if err != nil { 44 | // if we couldn't marshal the trace level fields, leave it blank 45 | tcJSON = []byte("") 46 | } 47 | 48 | tcB64 := base64.StdEncoding.EncodeToString(tcJSON) 49 | 50 | var datasetClause string 51 | if GlobalConfig.PropagateDataset && prop.Dataset != "" { 52 | datasetClause = fmt.Sprintf("dataset=%s,", url.QueryEscape(prop.Dataset)) 53 | } 54 | 55 | return fmt.Sprintf( 56 | "%d;trace_id=%s,parent_id=%s,%scontext=%s", 57 | TracePropagationVersion, 58 | prop.TraceID, 59 | prop.ParentID, 60 | datasetClause, 61 | tcB64, 62 | ) 63 | } 64 | 65 | // UnmarshalHoneycombTraceContext parses the information provided in header and creates a 66 | // PropagationContext instance. 67 | // 68 | // If the header cannot be used to construct a PropagationContext with a trace id and parent id, 69 | // an error will be returned. 70 | func UnmarshalHoneycombTraceContext(header string) (*PropagationContext, error) { 71 | // pull the version out of the header 72 | getVer := strings.SplitN(header, ";", 2) 73 | if getVer[0] == "1" { 74 | return unmarshalHoneycombTraceContextV1(getVer[1]) 75 | } 76 | return nil, &PropagationError{fmt.Sprintf("unrecognized version for trace header %s", getVer[0]), nil} 77 | } 78 | 79 | // unmarshalHoneycombTraceContextV1 takes the trace header, stripped of the 80 | // version string, and returns the component parts. If the header includes a 81 | // parent id but not a trace id, or if the header contains an unparseable 82 | // string in the trace context, an error will be returned. 83 | func unmarshalHoneycombTraceContextV1(header string) (*PropagationContext, error) { 84 | clauses := strings.Split(header, ",") 85 | var prop = &PropagationContext{} 86 | var tcB64 string 87 | for _, clause := range clauses { 88 | keyval := strings.SplitN(clause, "=", 2) 89 | switch keyval[0] { 90 | case "trace_id": 91 | prop.TraceID = keyval[1] 92 | case "parent_id": 93 | prop.ParentID = keyval[1] 94 | case "dataset": 95 | if GlobalConfig.PropagateDataset { 96 | prop.Dataset, _ = url.QueryUnescape(keyval[1]) 97 | } 98 | case "context": 99 | tcB64 = keyval[1] 100 | } 101 | } 102 | if prop.TraceID == "" && prop.ParentID != "" { 103 | return nil, &PropagationError{"parent_id without trace_id", nil} 104 | } 105 | if tcB64 != "" { 106 | data, err := base64.StdEncoding.DecodeString(tcB64) 107 | if err != nil { 108 | return nil, &PropagationError{"unable to decode base64 trace context", err} 109 | } 110 | prop.TraceContext = make(map[string]interface{}) 111 | err = json.Unmarshal(data, &prop.TraceContext) 112 | if err != nil { 113 | return nil, &PropagationError{"unable to unmarshal trace context", err} 114 | } 115 | } 116 | return prop, nil 117 | } 118 | -------------------------------------------------------------------------------- /propagation/amazon.go: -------------------------------------------------------------------------------- 1 | package propagation 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // MarshalAmazonTraceContext uses the information in prop to create a trace context header 9 | // in the Amazon AWS trace header format. It returns the serialized form of the trace 10 | // context, ready to be inserted into the headers of an outbound HTTP request. 11 | // 12 | // If prop is nil, the returned value will be an empty string. 13 | func MarshalAmazonTraceContext(prop *PropagationContext) string { 14 | if prop == nil { 15 | return "" 16 | } 17 | 18 | // From https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html: 19 | // "If the X-Amzn-Trace-Id header is present and has a Self field, the load balancer updates 20 | // the value of the Self field." 21 | h := fmt.Sprintf("Root=%s;Parent=%s", prop.TraceID, prop.ParentID) 22 | 23 | if len(prop.TraceContext) != 0 { 24 | elems := make([]string, len(prop.TraceContext)) 25 | i := 0 26 | for k, v := range prop.TraceContext { 27 | elems[i] = fmt.Sprintf("%s=%v", k, v) 28 | i++ 29 | } 30 | traceContext := ";" + strings.Join(elems, ";") 31 | h = h + traceContext 32 | } 33 | 34 | return h 35 | } 36 | 37 | // UnmarshalAmazonTraceContext parses the information provided in the headers and creates 38 | // a PropagationContext instance. The provided headers is expected to contain an X-Amzn-Trace-Id 39 | // key which will contain the value of the Amazon header. 40 | // 41 | // According to the documentation for load balancer request tracing: 42 | // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html 43 | // An application can add arbitrary fields for its own purposes. The load balancer preserves these fields 44 | // but does not use them. In our implementation, we stick these fields in the TraceContext field of the 45 | // PropagationContext. We only support strings, so if the header contains foo=32,baz=true, both 32 and true 46 | // will be put into the map as strings. Note that this differs from the Honeycomb header, where trace context 47 | // fields are stored as a base64 encoded JSON object and unmarshaled into ints, bools, etc. 48 | // 49 | // If the header cannot be used to construct a valid PropagationContext, an error will be returned. 50 | func UnmarshalAmazonTraceContext(header string) (*PropagationContext, error) { 51 | segments := strings.Split(header, ";") 52 | // From https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html 53 | // If the X-Amzn-Trace-Id header is not present on an incoming request, the load balancer generates a header 54 | // with a Root field and forwards the request. If the X-Amzn-Trace-Id header is present and has a Root field, 55 | // the load balancer inserts a Self field, and forwards the request. If an application adds a header with a 56 | // Root field and a custom field, the load balancer preserves both fields, inserts a Self field, and forwards 57 | // the request. If the X-Amzn-Trace-Id header is present and has a Self field, the load balancer updates the 58 | // value of the self field. 59 | // 60 | // Using the documentation above (that applies to amazon load balancers) we look for self as the parent id 61 | // and root as the trace id. 62 | prop := &PropagationContext{} 63 | prop.TraceContext = make(map[string]interface{}) 64 | var parent string 65 | for _, segment := range segments { 66 | keyval := strings.SplitN(segment, "=", 2) 67 | if len(keyval) < 2 { 68 | continue 69 | } 70 | switch strings.ToLower(keyval[0]) { 71 | case "self": 72 | prop.ParentID = keyval[1] 73 | case "root": 74 | prop.TraceID = keyval[1] 75 | case "parent": 76 | parent = keyval[1] 77 | default: 78 | prop.TraceContext[keyval[0]] = keyval[1] 79 | } 80 | } 81 | 82 | // Our primary use case is to support load balancers, so favor self over parent. 83 | // If, however, a parent is present and self is not, use it. 84 | if prop.ParentID == "" && parent != "" { 85 | prop.ParentID = parent 86 | } 87 | 88 | // If no header is provided to an ALB or ELB, it will generate a header 89 | // with a Root field and forwards the request. In this case it should be 90 | // used as both the parent id and the trace id. 91 | if prop.TraceID != "" && prop.ParentID == "" { 92 | prop.ParentID = prop.TraceID 93 | } 94 | 95 | if !prop.IsValid() { 96 | return nil, &PropagationError{fmt.Sprintf("unable to parse header into propagationcontext: %s", header), nil} 97 | } 98 | 99 | return prop, nil 100 | } 101 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honeycombio/beeline-go 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/DATA-DOG/go-sqlmock v1.5.2 9 | github.com/felixge/httpsnoop v1.0.4 10 | github.com/gin-gonic/gin v1.10.0 11 | github.com/go-sql-driver/mysql v1.9.1 12 | github.com/gobuffalo/pop/v6 v6.1.1 13 | github.com/google/uuid v1.6.0 14 | github.com/gorilla/mux v1.8.1 15 | github.com/honeycombio/libhoney-go v1.25.0 16 | github.com/jmoiron/sqlx v1.4.0 17 | github.com/julienschmidt/httprouter v1.3.0 18 | github.com/labstack/echo/v4 v4.13.3 19 | github.com/stretchr/testify v1.10.0 20 | goji.io/v3 v3.0.0 21 | google.golang.org/grpc v1.71.0 22 | google.golang.org/protobuf v1.36.4 23 | ) 24 | 25 | require ( 26 | filippo.io/edwards25519 v1.1.0 // indirect 27 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 28 | github.com/aymerick/douceur v0.2.0 // indirect 29 | github.com/bytedance/sonic v1.11.6 // indirect 30 | github.com/bytedance/sonic/loader v0.1.1 // indirect 31 | github.com/cloudwego/base64x v0.1.4 // indirect 32 | github.com/cloudwego/iasm v0.2.0 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 35 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect 36 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 // indirect 37 | github.com/fatih/color v1.13.0 // indirect 38 | github.com/fatih/structs v1.1.0 // indirect 39 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 40 | github.com/gin-contrib/sse v0.1.0 // indirect 41 | github.com/go-playground/locales v0.14.1 // indirect 42 | github.com/go-playground/universal-translator v0.18.1 // indirect 43 | github.com/go-playground/validator/v10 v10.20.0 // indirect 44 | github.com/gobuffalo/envy v1.10.2 // indirect 45 | github.com/gobuffalo/fizz v1.14.4 // indirect 46 | github.com/gobuffalo/flect v1.0.0 // indirect 47 | github.com/gobuffalo/github_flavored_markdown v1.1.3 // indirect 48 | github.com/gobuffalo/helpers v0.6.7 // indirect 49 | github.com/gobuffalo/nulls v0.4.2 // indirect 50 | github.com/gobuffalo/plush/v4 v4.1.18 // indirect 51 | github.com/gobuffalo/tags/v3 v3.1.4 // indirect 52 | github.com/gobuffalo/validate/v3 v3.3.3 // indirect 53 | github.com/goccy/go-json v0.10.2 // indirect 54 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 55 | github.com/gorilla/css v1.0.0 // indirect 56 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 57 | github.com/jackc/pgconn v1.14.3 // indirect 58 | github.com/jackc/pgio v1.0.0 // indirect 59 | github.com/jackc/pgpassfile v1.0.0 // indirect 60 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 61 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 62 | github.com/jackc/pgtype v1.14.0 // indirect 63 | github.com/jackc/pgx/v4 v4.18.2 // indirect 64 | github.com/joho/godotenv v1.4.0 // indirect 65 | github.com/json-iterator/go v1.1.12 // indirect 66 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 67 | github.com/klauspost/compress v1.17.11 // indirect 68 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 69 | github.com/labstack/gommon v0.4.2 // indirect 70 | github.com/leodido/go-urn v1.4.0 // indirect 71 | github.com/luna-duclos/instrumentedsql v1.1.3 // indirect 72 | github.com/mattn/go-colorable v0.1.13 // indirect 73 | github.com/mattn/go-isatty v0.0.20 // indirect 74 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 75 | github.com/microcosm-cc/bluemonday v1.0.20 // indirect 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 77 | github.com/modern-go/reflect2 v1.0.2 // indirect 78 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 79 | github.com/pmezard/go-difflib v1.0.0 // indirect 80 | github.com/rogpeppe/go-internal v1.9.0 // indirect 81 | github.com/sergi/go-diff v1.2.0 // indirect 82 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect 83 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect 84 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 85 | github.com/ugorji/go/codec v1.2.12 // indirect 86 | github.com/valyala/bytebufferpool v1.0.0 // indirect 87 | github.com/valyala/fasttemplate v1.2.2 // indirect 88 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 89 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 90 | golang.org/x/arch v0.8.0 // indirect 91 | golang.org/x/crypto v0.35.0 // indirect 92 | golang.org/x/net v0.36.0 // indirect 93 | golang.org/x/sync v0.11.0 // indirect 94 | golang.org/x/sys v0.30.0 // indirect 95 | golang.org/x/text v0.22.0 // indirect 96 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 97 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect 98 | gopkg.in/yaml.v2 v2.4.0 // indirect 99 | gopkg.in/yaml.v3 v3.0.1 // indirect 100 | ) 101 | -------------------------------------------------------------------------------- /beeline_test.go: -------------------------------------------------------------------------------- 1 | package beeline 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/honeycombio/libhoney-go/transmission" 9 | 10 | libhoney "github.com/honeycombio/libhoney-go" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // TestNestedSpans tests that if you open and close several spans in the same 15 | // function that fields added after the inner spans have closed are correctly 16 | // added to the outer spans. If you don't keep the context from sending the 17 | // spans or somehow break re-inserting the parent span into the context after 18 | // sending a child span, this test will fail. 19 | func TestNestedSpans(t *testing.T) { 20 | mo := setupLibhoney(t) 21 | ctxroot, spanroot := StartSpan(context.Background(), "start") 22 | AddField(ctxroot, "start_col", 1) 23 | ctxmid, spanmid := StartSpan(ctxroot, "middle") 24 | AddField(ctxmid, "mid_col", 1) 25 | ctxleaf, spanleaf := StartSpan(ctxmid, "leaf") 26 | AddField(ctxleaf, "leaf_col", 1) 27 | spanleaf.Send() // sending leaf span 28 | AddField(ctxmid, "after_mid_col", 1) // adding to middle span 29 | spanmid.Send() // sending middle span 30 | AddField(ctxroot, "end_start_col", 1) // adding to start span 31 | spanroot.Send() // sending start span 32 | 33 | events := mo.Events() 34 | assert.Equal(t, 3, len(events), "should have sent 3 events") 35 | var foundStart, foundMiddle bool 36 | for _, ev := range events { 37 | fields := ev.Data 38 | if fields["app.start_col"] == 1 { 39 | foundStart = true 40 | assert.Equal(t, fields["app.end_start_col"], 1, "ending start field should be in start span") 41 | } 42 | if fields["app.mid_col"] == 1 { 43 | foundMiddle = true 44 | assert.Equal(t, fields["app.after_mid_col"], 1, "after middle field should be in middle span") 45 | } 46 | } 47 | assert.True(t, foundStart, "didn't find the start span") 48 | assert.True(t, foundMiddle, "didn't find the middle span") 49 | } 50 | 51 | // TestBasicSpanAttributes verifies that creating and sending a span gives it 52 | // all the basic required attributes: duration, trace, span, and parentIDs, and 53 | // name. 54 | func TestBasicSpanAttributes(t *testing.T) { 55 | mo := setupLibhoney(t) 56 | ctx, span := StartSpan(context.Background(), "start") 57 | AddField(ctx, "start_col", 1) 58 | ctxLeaf, spanLeaf := StartSpan(ctx, "leaf") 59 | AddField(ctxLeaf, "leaf_col", 1) 60 | spanLeaf.Send() 61 | span.Send() 62 | 63 | events := mo.Events() 64 | assert.Equal(t, 2, len(events), "should have sent 2 events") 65 | 66 | var foundRoot bool 67 | for _, ev := range events { 68 | fields := ev.Data 69 | name, ok := fields["name"] 70 | assert.True(t, ok, "failed to find name") 71 | _, ok = fields["duration_ms"] 72 | assert.True(t, ok, "failed to find duration_ms") 73 | _, ok = fields["trace.trace_id"] 74 | assert.True(t, ok, fmt.Sprintf("failed to find trace ID for span %s", name)) 75 | _, ok = fields["trace.span_id"] 76 | assert.True(t, ok, fmt.Sprintf("failed to find span ID for span %s", name)) 77 | 78 | spanType, ok := fields["meta.span_type"] 79 | if ok { 80 | spanTypeStr, ok := spanType.(string) 81 | assert.True(t, ok, "span field meta.span_type should be string") 82 | if spanTypeStr == "root" { 83 | foundRoot = true 84 | } 85 | } else { 86 | // non-root spans should have a parent ID 87 | _, ok = fields["trace.parent_id"] 88 | assert.True(t, ok, fmt.Sprintf("failed to find parent ID for span %s", name)) 89 | } 90 | // root span will be missing parent ID 91 | } 92 | assert.True(t, foundRoot, "root span missing") 93 | } 94 | 95 | func BenchmarkCreateSpan(b *testing.B) { 96 | setupLibhoney(b) 97 | 98 | ctx, _ := StartSpan(context.Background(), "parent") 99 | for n := 0; n < b.N; n++ { 100 | StartSpan(ctx, "child") 101 | } 102 | } 103 | 104 | func BenchmarkBeelineAddField(b *testing.B) { 105 | setupLibhoney(b) 106 | 107 | ctx, _ := StartSpan(context.Background(), "parent") 108 | 109 | b.Run("AddField/no-prefix", func(b *testing.B) { 110 | for n := 0; n < b.N; n++ { 111 | AddField(ctx, "foo", 1) 112 | } 113 | }) 114 | b.Run("AddField/half-prefixed", func(b *testing.B) { 115 | for n := 0; n < b.N; n++ { 116 | if n%2 == 0 { 117 | AddField(ctx, "app.foo", 1) 118 | } else { 119 | AddField(ctx, "foo", 1) 120 | } 121 | } 122 | }) 123 | b.Run("AddField/all-prefixed", func(b *testing.B) { 124 | for n := 0; n < b.N; n++ { 125 | AddField(ctx, "app.foo", 1) 126 | } 127 | }) 128 | } 129 | 130 | func setupLibhoney(t testing.TB) *transmission.MockSender { 131 | mo := &transmission.MockSender{} 132 | client, err := libhoney.NewClient( 133 | libhoney.ClientConfig{ 134 | APIKey: "placeholder", 135 | Dataset: "placeholder", 136 | APIHost: "placeholder", 137 | Transmission: mo, 138 | }, 139 | ) 140 | assert.Equal(t, nil, err) 141 | 142 | Init(Config{Client: client}) 143 | 144 | return mo 145 | } 146 | -------------------------------------------------------------------------------- /propagation/w3c.go: -------------------------------------------------------------------------------- 1 | package propagation 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | ) 10 | 11 | const ( 12 | supportedVersion = 0 13 | maxVersion = 254 14 | TraceparentHeader = "traceparent" 15 | tracestateHeader = "tracestate" 16 | ) 17 | 18 | var traceCtxRegExp = regexp.MustCompile("^(?P[0-9a-f]{2})-(?P[a-f0-9]{32})-(?P[a-f0-9]{16})-(?P[a-f0-9]{2})(?:-.*)?$") 19 | 20 | // MarshalHoneycombTraceContext uses the information in prop to create trace context headers 21 | // that conform to the W3C Trace Context specification. The header values are set in headers, 22 | // which is an HTTPSupplier, an interface to which http.Header is an implementation. The headers 23 | // are also returned as a map[string]string. 24 | // 25 | // Context is passed into this function and returned so that we can maintain the value of the 26 | // tracestate header. This is required in order to use the Propagator interface exported by the 27 | // OpenTelemetry Go SDK and avoid writing our own W3C Trace Context parser and serializer. 28 | // 29 | // If prop is empty or nil, the return value will be an empty map. 30 | func MarshalW3CTraceContext(ctx context.Context, prop *PropagationContext) (context.Context, map[string]string) { 31 | headerMap := make(map[string]string) 32 | 33 | traceID, err := traceIDFromHex(prop.TraceID) 34 | if err != nil { 35 | return ctx, headerMap 36 | } 37 | spanID, err := spanIDFromHex(prop.ParentID) 38 | if err != nil { 39 | return ctx, headerMap 40 | } 41 | 42 | headerMap[tracestateHeader] = prop.TraceState.String() 43 | 44 | // Clear all flags other than the trace-context supported sampling bit. 45 | flags := prop.TraceFlags & FlagsSampled 46 | 47 | h := fmt.Sprintf("%.2x-%s-%s-%s", 48 | supportedVersion, 49 | traceID, 50 | spanID, 51 | flags) 52 | 53 | headerMap[TraceparentHeader] = h 54 | return ctx, headerMap 55 | } 56 | 57 | // UnmarshalW3CTraceContext parses the information provided in the appropriate headers 58 | // and creates a PropagationContext instance. Headers are passed in via an HTTPSupplier, 59 | // which is an interface that defines Get and Set methods, http.Header is an implementation. 60 | // 61 | // Context is passed into this function and returned so that we can maintain the value of the 62 | // tracestate header. This is required in order to use the Propagator interface exported by the 63 | // OpenTelemetry Go SDK and avoid writing our own W3C Trace Context parser and serializer. 64 | // 65 | // If the headers contain neither a trace id or parent id, an error will be returned. 66 | func UnmarshalW3CTraceContext(ctx context.Context, headers map[string]string) (context.Context, *PropagationContext, error) { 67 | prop := &PropagationContext{} 68 | 69 | h := getHeaderValue(headers, TraceparentHeader) 70 | if h == "" { 71 | return ctx, prop, errors.New("cannot unmarshal empty header") 72 | } 73 | 74 | matches := traceCtxRegExp.FindStringSubmatch(h) 75 | 76 | if len(matches) == 0 { 77 | return ctx, prop, errors.New("invalid header format") 78 | } 79 | 80 | if len(matches) < 5 { // four subgroups plus the overall match 81 | return ctx, prop, errors.New("invalid header") 82 | } 83 | 84 | if len(matches[1]) != 2 { 85 | return ctx, prop, errors.New("invalid header. could not parse") 86 | } 87 | ver, err := hex.DecodeString(matches[1]) 88 | if err != nil { 89 | return ctx, prop, errors.New("could not decode version") 90 | } 91 | version := int(ver[0]) 92 | if version > maxVersion { 93 | return ctx, prop, errors.New("unsupported version") 94 | } 95 | 96 | if version == 0 && len(matches) != 5 { // four subgroups plus the overall match 97 | return ctx, prop, errors.New("incorrect number of subgroups in header") 98 | } 99 | 100 | if len(matches[2]) != 32 { 101 | return ctx, prop, errors.New("invalid trace id format") 102 | } 103 | 104 | traceID, err := traceIDFromHex(matches[2][:32]) 105 | if err != nil { 106 | return ctx, prop, errors.New("unable to parse trace id") 107 | } 108 | 109 | prop.TraceID = traceID.String() 110 | 111 | if len(matches[3]) != 16 { 112 | return ctx, prop, errors.New("invalid span id format") 113 | } 114 | 115 | spanID, err := spanIDFromHex(matches[3]) 116 | if err != nil { 117 | return ctx, prop, errors.New("unable to parse span id") 118 | } 119 | 120 | prop.ParentID = spanID.String() 121 | 122 | if len(matches[4]) != 2 { 123 | return ctx, prop, errors.New("invalid traceflags format") 124 | } 125 | opts, err := hex.DecodeString(matches[4]) 126 | if err != nil || len(opts) < 1 || (version == 0 && opts[0] > 2) { 127 | return ctx, prop, errors.New("unable to parse traceflags") 128 | } 129 | // Clear all flags other than the trace-context supported sampling bit. 130 | prop.TraceFlags = TraceFlags(opts[0]) & FlagsSampled 131 | 132 | // Ignore the error returned here. Failure to parse tracestate MUST NOT 133 | // affect the parsing of traceparent according to the W3C tracecontext 134 | // specification. 135 | prop.TraceState, _ = ParseTraceState(getHeaderValue(headers, tracestateHeader)) 136 | 137 | return ctx, prop, nil 138 | } 139 | -------------------------------------------------------------------------------- /propagation/trace.go: -------------------------------------------------------------------------------- 1 | package propagation 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | ) 8 | 9 | // this file contains definitions of types used in propagation between OpenTelemetry and 10 | // beeline instrumented applications. Many of the types have been lifted from the 11 | // opentelemetry-go sdk found here: 12 | // https://github.com/open-telemetry/opentelemetry-go/blob/c912b179fd595cc7c6eec0b22e6c1371e31241d7/trace/trace.go 13 | 14 | const ( 15 | // FlagsSampled is a bitmask with the sampled bit set. A SpanContext 16 | // with the sampling bit set means the span is sampled. 17 | FlagsSampled = TraceFlags(0x01) 18 | 19 | errInvalidHexID errorConst = "trace-id and span-id can only contain [0-9a-f] characters, all lowercase" 20 | 21 | errInvalidTraceIDLength errorConst = "hex encoded trace-id must have length equals to 32" 22 | errNilTraceID errorConst = "trace-id can't be all zero" 23 | 24 | errInvalidSpanIDLength errorConst = "hex encoded span-id must have length equals to 16" 25 | errNilSpanID errorConst = "span-id can't be all zero" 26 | ) 27 | 28 | type errorConst string 29 | 30 | func (e errorConst) Error() string { 31 | return string(e) 32 | } 33 | 34 | // traceID is a unique identity of a trace. 35 | type traceID [16]byte 36 | 37 | var nilTraceID traceID 38 | var _ json.Marshaler = nilTraceID 39 | 40 | // IsValid checks whether the trace traceID is valid. A valid trace ID does 41 | // not consist of zeros only. 42 | func (t traceID) IsValid() bool { 43 | return !bytes.Equal(t[:], nilTraceID[:]) 44 | } 45 | 46 | // MarshalJSON implements a custom marshal function to encode traceID 47 | // as a hex string. 48 | func (t traceID) MarshalJSON() ([]byte, error) { 49 | return json.Marshal(t.String()) 50 | } 51 | 52 | // String returns the hex string representation form of a traceID 53 | func (t traceID) String() string { 54 | return hex.EncodeToString(t[:]) 55 | } 56 | 57 | // spanID is a unique identity of a span in a trace. 58 | type spanID [8]byte 59 | 60 | var nilSpanID spanID 61 | var _ json.Marshaler = nilSpanID 62 | 63 | // IsValid checks whether the spanID is valid. A valid spanID does not consist 64 | // of zeros only. 65 | func (s spanID) IsValid() bool { 66 | return !bytes.Equal(s[:], nilSpanID[:]) 67 | } 68 | 69 | // MarshalJSON implements a custom marshal function to encode spanID 70 | // as a hex string. 71 | func (s spanID) MarshalJSON() ([]byte, error) { 72 | return json.Marshal(s.String()) 73 | } 74 | 75 | // String returns the hex string representation form of a SsanID 76 | func (s spanID) String() string { 77 | return hex.EncodeToString(s[:]) 78 | } 79 | 80 | // TraceIDFromHex returns a traceID from a hex string if it is compliant with 81 | // the W3C trace-context specification. See more at 82 | // https://www.w3.org/TR/trace-context/#trace-id 83 | // nolint:revive // revive complains about stutter of `trace.TraceIDFromHex`. 84 | func traceIDFromHex(h string) (traceID, error) { 85 | t := traceID{} 86 | if len(h) != 32 { 87 | return t, errInvalidTraceIDLength 88 | } 89 | 90 | if err := decodeHex(h, t[:]); err != nil { 91 | return t, err 92 | } 93 | 94 | if !t.IsValid() { 95 | return t, errNilTraceID 96 | } 97 | return t, nil 98 | } 99 | 100 | // SpanIDFromHex returns a spanID from a hex string if it is compliant 101 | // with the w3c trace-context specification. 102 | // See more at https://www.w3.org/TR/trace-context/#parent-id 103 | func spanIDFromHex(h string) (spanID, error) { 104 | s := spanID{} 105 | if len(h) != 16 { 106 | return s, errInvalidSpanIDLength 107 | } 108 | 109 | if err := decodeHex(h, s[:]); err != nil { 110 | return s, err 111 | } 112 | 113 | if !s.IsValid() { 114 | return s, errNilSpanID 115 | } 116 | return s, nil 117 | } 118 | 119 | func decodeHex(h string, b []byte) error { 120 | for _, r := range h { 121 | switch { 122 | case 'a' <= r && r <= 'f': 123 | continue 124 | case '0' <= r && r <= '9': 125 | continue 126 | default: 127 | return errInvalidHexID 128 | } 129 | } 130 | 131 | decoded, err := hex.DecodeString(h) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | copy(b, decoded) 137 | return nil 138 | } 139 | 140 | // TraceFlags contains flags that can be set on a SpanContext 141 | type TraceFlags byte //nolint:revive // revive complains about stutter of `trace.TraceFlags`. 142 | 143 | // IsSampled returns if the sampling bit is set in the TraceFlags. 144 | func (tf TraceFlags) IsSampled() bool { 145 | return tf&FlagsSampled == FlagsSampled 146 | } 147 | 148 | // WithSampled sets the sampling bit in a new copy of the TraceFlags. 149 | func (tf TraceFlags) WithSampled(sampled bool) TraceFlags { 150 | if sampled { 151 | return tf | FlagsSampled 152 | } 153 | 154 | return tf &^ FlagsSampled 155 | } 156 | 157 | // MarshalJSON implements a custom marshal function to encode TraceFlags 158 | // as a hex string. 159 | func (tf TraceFlags) MarshalJSON() ([]byte, error) { 160 | return json.Marshal(tf.String()) 161 | } 162 | 163 | // String returns the hex string representation form of TraceFlags 164 | func (tf TraceFlags) String() string { 165 | return hex.EncodeToString([]byte{byte(tf)}[:]) 166 | } 167 | -------------------------------------------------------------------------------- /sample/deterministic_sampler_test.go: -------------------------------------------------------------------------------- 1 | package sample 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | const requestIDBytes = `abcdef0123456789` 9 | 10 | func init() { 11 | rand.Seed(1) 12 | } 13 | 14 | func randomRequestID() string { 15 | // create request ID roughly resembling something you would get from 16 | // AWS ALB, e.g., 17 | // 18 | // 1-5ababc0a-4df707925c1681932ea22a20 19 | // 20 | // The AWS docs say the middle bit is "time in seconds since epoch", 21 | // (implying base 10) but the above represents an actual Root= ID from 22 | // an ALB access log, so... yeah. 23 | reqID := "1-" 24 | for i := 0; i < 8; i++ { 25 | reqID += string(requestIDBytes[rand.Intn(len(requestIDBytes))]) 26 | } 27 | reqID += "-" 28 | for i := 0; i < 24; i++ { 29 | reqID += string(requestIDBytes[rand.Intn(len(requestIDBytes))]) 30 | } 31 | return reqID 32 | } 33 | 34 | func assertEqual(t *testing.T, a interface{}, b interface{}) { 35 | if a != b { 36 | t.Fatalf("%v != %v", a, b) 37 | } 38 | } 39 | 40 | func TestDeterministicSamplerDatapoints(t *testing.T) { 41 | s, _ := NewDeterministicSampler(17) 42 | a := s.Sample("hello") 43 | assertEqual(t, a, false) 44 | a = s.Sample("hello") 45 | assertEqual(t, a, false) 46 | a = s.Sample("world") 47 | assertEqual(t, a, false) 48 | a = s.Sample("this5") 49 | assertEqual(t, a, true) 50 | } 51 | 52 | func TestDeterministicSampler(t *testing.T) { 53 | const ( 54 | nRequestIDs = 200000 55 | acceptableMarginOfError = 0.05 56 | ) 57 | 58 | testSampleRates := []uint{1, 2, 10, 50} 59 | 60 | // distribution for sampling should be good 61 | for _, sampleRate := range testSampleRates { 62 | ds, err := NewDeterministicSampler(sampleRate) 63 | if err != nil { 64 | t.Fatalf("error creating deterministic sampler: %s", err) 65 | } 66 | 67 | nSampled := 0 68 | 69 | for i := 0; i < nRequestIDs; i++ { 70 | sampled := ds.Sample(randomRequestID()) 71 | if sampled { 72 | nSampled++ 73 | } 74 | } 75 | 76 | expectedNSampled := (nRequestIDs * (1 / float64(sampleRate))) 77 | 78 | // Sampling should be balanced across all request IDs 79 | // regardless of sample rate. If we cross this threshold, flunk 80 | // the test. 81 | unacceptableLowBound := int(expectedNSampled - (expectedNSampled * acceptableMarginOfError)) 82 | unacceptableHighBound := int(expectedNSampled + (expectedNSampled * acceptableMarginOfError)) 83 | if nSampled < unacceptableLowBound { 84 | t.Fatalf("Sampled (%d) less than acceptable lower bound (%d). Sample rate: %d", nSampled, unacceptableLowBound, sampleRate) 85 | } 86 | if nSampled > unacceptableHighBound { 87 | t.Fatalf("Sampled (%d) more than acceptable upper bound (%d). Sample rate: %d", nSampled, unacceptableHighBound, sampleRate) 88 | } 89 | } 90 | 91 | s1, _ := NewDeterministicSampler(2) 92 | s2, _ := NewDeterministicSampler(2) 93 | sampleString := "#hashbrowns" 94 | firstAnswer := s1.Sample(sampleString) 95 | 96 | // sampler should not give different answers for subsequent runs 97 | for i := 0; i < 25; i++ { 98 | s1Answer := s1.Sample(sampleString) 99 | s2Answer := s2.Sample(sampleString) 100 | if s1Answer != firstAnswer || s2Answer != firstAnswer { 101 | t.Fatalf("deterministic samplers were not deterministic:\n\titeration: %d\n\ts1Answer was %t\n\ts2Answer was %t\n\tfirstAnswer was %t", i, s1Answer, s2Answer, firstAnswer) 102 | } 103 | } 104 | } 105 | 106 | // Tests the deterministic sampler against a specific set of determiniants for specific results, 107 | // which should be consistent across beelines 108 | func TestDeterministicBeelineInterop(t *testing.T) { 109 | ds, err := NewDeterministicSampler(2) 110 | if err != nil { 111 | t.Fatalf("error creating deterministic sampler: %s", err) 112 | } 113 | 114 | ids := []string{ 115 | "4YeYygWjTZ41zOBKUoYUaSVxPGm78rdU", 116 | "iow4KAFBl9u6lF4EYIcsFz60rXGvu7ph", 117 | "EgQMHtruEfqaqQqRs5nwaDXsegFGmB5n", 118 | "UnVVepVdyGIiwkHwofyva349tVu8QSDn", 119 | "rWuxi2uZmBEprBBpxLLFcKtXHA8bQkvJ", 120 | "8PV5LN1IGm5T0ZVIaakb218NvTEABNZz", 121 | "EMSmscnxwfrkKd1s3hOJ9bL4zqT1uud5", 122 | "YiLx0WGJrQAge2cVoAcCscDDVidbH4uE", 123 | "IjD0JHdQdDTwKusrbuiRO4NlFzbPotvg", 124 | "ADwiQogJGOS4X8dfIcidcfdT9fY2WpHC", 125 | "DyGaS7rfQsMX0E6TD9yORqx7kJgUYvNR", 126 | "MjOCkn11liCYZspTAhdULMEfWJGMHvpK", 127 | "wtGa41YcFMR5CBNr79lTfRAFi6Vhr6UF", 128 | "3AsMjnpTBawWv2AAPDxLjdxx4QYl9XXb", 129 | "sa2uMVNPiZLK52zzxlakCUXLaRNXddBz", 130 | "NYH9lkdbvXsiUFKwJtjSkQ1RzpHwWloK", 131 | "8AwzQeY5cudY8YUhwxm3UEP7Oos61RTY", 132 | "ADKWL3p5gloRYO3ptarTCbWUHo5JZi3j", 133 | "UAnMARj5x7hkh9kwBiNRfs5aYDsbHKpw", 134 | "Aes1rgTLMNnlCkb9s6bH7iT5CbZTdxUw", 135 | "eh1LYTOfgISrZ54B7JbldEpvqVur57tv", 136 | "u5A1wEYax1kD9HBeIjwyNAoubDreCsZ6", 137 | "mv70SFwpAOHRZt4dmuw5n2lAsM1lOrcx", 138 | "i4nIu0VZMuh5hLrUm9w2kqNxcfYY7Y3a", 139 | "UqfewK2qFZqfJ619RKkRiZeYtO21ngX1", 140 | } 141 | expected := []bool{ 142 | false, 143 | true, 144 | true, 145 | true, 146 | true, 147 | false, 148 | true, 149 | true, 150 | false, 151 | false, 152 | true, 153 | false, 154 | true, 155 | false, 156 | false, 157 | false, 158 | false, 159 | false, 160 | true, 161 | true, 162 | false, 163 | false, 164 | true, 165 | true, 166 | false, 167 | } 168 | 169 | for i := range ids { 170 | keep := ds.Sample(ids[i]) 171 | if keep != expected[i] { 172 | t.Errorf("got unexpected deterministic sampler decision for %s: %t, expected %t", ids[i], keep, expected[i]) 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /wrappers/hnysqlx/sqlx_test.go: -------------------------------------------------------------------------------- 1 | package hnysqlx_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "testing" 8 | 9 | "github.com/DATA-DOG/go-sqlmock" 10 | _ "github.com/go-sql-driver/mysql" 11 | "github.com/jmoiron/sqlx" 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/honeycombio/beeline-go" 15 | "github.com/honeycombio/beeline-go/wrappers/hnysqlx" 16 | ) 17 | 18 | func Example() { 19 | // Initialize beeline. The only required field is WriteKey. 20 | beeline.Init(beeline.Config{ 21 | WriteKey: "abcabc123123", 22 | Dataset: "sqlx", 23 | // for demonstration, send the event to STDOUT intead of Honeycomb. 24 | // Remove the STDOUT setting when filling in a real write key. 25 | // NOTE: This should *only* be set to true in development environments. 26 | // Setting to true is Production environments can cause problems. 27 | STDOUT: true, 28 | }) 29 | // and make sure we close to force flushing all pending events before shutdown 30 | defer beeline.Close() 31 | 32 | // open a regular sqlx connection 33 | odb, err := sqlx.Open("mysql", "root:@tcp(127.0.0.1)/donut") 34 | if err != nil { 35 | fmt.Println("connection err") 36 | } 37 | 38 | // replace it with a wrapped hnysqlx.DB 39 | db := hnysqlx.WrapDB(odb) 40 | // and start up a trace for these statements to join 41 | ctx, span := beeline.StartSpan(context.Background(), "start") 42 | defer span.Send() 43 | 44 | db.MustExecContext(ctx, "insert into flavors (flavor) values ('rose')") 45 | fv := "rose" 46 | rows, err := db.QueryxContext(ctx, "SELECT id FROM flavors WHERE flavor=?", fv) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | defer rows.Close() 51 | for rows.Next() { 52 | var id int 53 | if err := rows.Scan(&id); err != nil { 54 | log.Fatal(err) 55 | } 56 | fmt.Printf("%d is %s\n", id, fv) 57 | } 58 | if err := rows.Err(); err != nil { 59 | log.Fatal(err) 60 | } 61 | } 62 | 63 | func TestDBBindNamed(t *testing.T) { 64 | // Initialize beeline. The only required field is WriteKey. 65 | beeline.Init(beeline.Config{ 66 | WriteKey: "abcabc123123", 67 | Dataset: "sqlx", 68 | // for demonstration, send the event to STDOUT intead of Honeycomb. 69 | // Remove the STDOUT setting when filling in a real write key. 70 | // NOTE: This should *only* be set to true in development environments. 71 | // Setting to true is Production environments can cause problems. 72 | STDOUT: true, 73 | }) 74 | // and make sure we close to force flushing all pending events before shutdown 75 | defer beeline.Close() 76 | 77 | // open a regular sqlx connection 78 | odb, err := sqlx.Open("mysql", "root:@tcp(127.0.0.1)/donut") 79 | if err != nil { 80 | fmt.Println("connection err") 81 | } 82 | 83 | // replace it with a wrapped hnysqlx.DB 84 | db := hnysqlx.WrapDB(odb) 85 | // and start up a trace for these statements to join 86 | _, span := beeline.StartSpan(context.Background(), "start") 87 | defer span.Send() 88 | 89 | originalQ := `select :named` 90 | originalArgs := struct { 91 | Named string `db:"named"` 92 | }{"namedValue"} 93 | 94 | q, args, err := db.BindNamed(originalQ, originalArgs) 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | 99 | expectedQ, expectedArgs, err := db.GetWrappedDB().BindNamed(originalQ, originalArgs) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | if q != expectedQ { 105 | t.Errorf("expected query: %s, got: %s", expectedQ, q) 106 | } 107 | 108 | var argsOK bool 109 | if len(expectedArgs) == len(args) { 110 | argsOK = true 111 | for i, arg := range args { 112 | if arg != expectedArgs[i] { 113 | argsOK = false 114 | break 115 | } 116 | } 117 | } 118 | if !argsOK { 119 | t.Errorf("expected args: %v, got: %v", expectedArgs, args) 120 | } 121 | } 122 | 123 | func TestSQLXMiddleware(t *testing.T) { 124 | beeline.Init(beeline.Config{ 125 | WriteKey: "abcabc123123", 126 | Dataset: "sql", 127 | // for demonstration, send the event to STDOUT intead of Honeycomb. 128 | // Remove the STDOUT setting when filling in a real write key. 129 | // NOTE: This should *only* be set to true in development environments. 130 | // Setting to true is Production environments can cause problems. 131 | STDOUT: true, 132 | }) 133 | // and make sure we close to force flushing all pending events before shutdown 134 | defer beeline.Close() 135 | 136 | // Open a mock sql connection. 137 | odb, mock, err := sqlmock.New() 138 | if err != nil { 139 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 140 | } 141 | defer odb.Close() 142 | sqlxodb := sqlx.NewDb(odb, "sqlmock") 143 | 144 | mock.ExpectExec("insert into flavors.+").WillReturnResult(sqlmock.NewResult(0, 0)) 145 | mock.ExpectQuery("SELECT id FROM flavors.+").WillReturnRows(sqlmock.NewRows([]string{"1"})) 146 | 147 | // replace it with a wrapped hnysql.DB 148 | db := hnysqlx.WrapDB(sqlxodb) 149 | // and start up a trace to capture all the calls 150 | ctx, span := beeline.StartSpan(context.Background(), "start") 151 | defer span.Send() 152 | 153 | // from here on, all SQL calls will emit events. 154 | 155 | _, err = db.ExecContext(ctx, "insert into flavors (flavor) values ('rose')") 156 | assert.Nil(t, err) 157 | fv := "rose" 158 | rows, err := db.QueryContext(ctx, "SELECT id FROM flavors WHERE flavor=?", fv) 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | defer rows.Close() 163 | for rows.Next() { 164 | var id int 165 | if err := rows.Scan(&id); err != nil { 166 | log.Fatal(err) 167 | } 168 | fmt.Printf("%d is %s\n", id, fv) 169 | } 170 | if err := rows.Err(); err != nil { 171 | log.Fatal(err) 172 | } 173 | 174 | if err := mock.ExpectationsWereMet(); err != nil { 175 | t.Errorf("there were unfulfilled expectations: %s", err) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /wrappers/hnygrpc/grpc_test.go: -------------------------------------------------------------------------------- 1 | package hnygrpc 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | beeline "github.com/honeycombio/beeline-go" 8 | "github.com/honeycombio/beeline-go/propagation" 9 | "github.com/honeycombio/beeline-go/trace" 10 | "github.com/honeycombio/beeline-go/wrappers/config" 11 | libhoney "github.com/honeycombio/libhoney-go" 12 | "github.com/honeycombio/libhoney-go/transmission" 13 | "github.com/stretchr/testify/assert" 14 | 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/codes" 17 | "google.golang.org/grpc/metadata" 18 | ) 19 | 20 | func TestStartSpanOrTrace(t *testing.T) { 21 | info := &grpc.UnaryServerInfo{ 22 | FullMethod: "test.method", 23 | } 24 | // no current span, no parser hook, expect a new trace 25 | ctx := context.Background() 26 | ctx, span := startSpanOrTraceFromUnaryGRPC(ctx, info, nil) 27 | assert.Equal(t, 0, len(span.GetChildren()), "Span should not have children") 28 | assert.Equal(t, "", span.GetParentID(), "Span should not have parent") 29 | 30 | // now let's create a child span 31 | ctx = trace.PutSpanInContext(ctx, span) 32 | ctx, spanTwo := startSpanOrTraceFromUnaryGRPC(ctx, info, nil) 33 | assert.Equal(t, 1, len(span.GetChildren()), "Should have one child span") 34 | assert.Equal(t, span, spanTwo.GetParent(), "Span should have been created as child") 35 | 36 | // metadata, no parser hook 37 | ctx = context.Background() 38 | ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{ 39 | "content-type": "application/grpc", 40 | })) 41 | ctx, spanThree := startSpanOrTraceFromUnaryGRPC(ctx, info, nil) 42 | assert.Equal(t, 0, len(spanThree.GetChildren()), "span should not have children") 43 | assert.Equal(t, "", span.GetParentID(), "Span should not have parent") 44 | 45 | // metadata, no parser hook, x-honeycomb-trace header 46 | ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{ 47 | "x-honeycomb-trace": "1;trace_id=4bf92f3577b34da6a3ce929d0e0e473,parent_id=00f067aa0ba902b7,context=", 48 | })) 49 | ctx, spanFour := startSpanOrTraceFromUnaryGRPC(ctx, info, nil) 50 | assert.Equal(t, 0, len(spanFour.GetChildren()), "span should not have children") 51 | assert.Equal(t, "00f067aa0ba902b7", spanFour.GetParentID(), "Expected parent_id from header") 52 | assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e473", spanFour.GetTrace().GetTraceID(), "Expected trace id from header") 53 | 54 | // metadata, parserhook 55 | ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{ 56 | "content-type": "application/grpc", 57 | })) 58 | parserHook := func(ctx context.Context) *propagation.PropagationContext { 59 | return &propagation.PropagationContext{ 60 | TraceID: "fffffffffffffffffffffffffffffff", 61 | ParentID: "aaaaaaaaaaaaaaaa", 62 | } 63 | } 64 | ctx, spanFive := startSpanOrTraceFromUnaryGRPC(ctx, info, parserHook) 65 | assert.Equal(t, 0, len(spanFive.GetChildren()), "span should not have children") 66 | assert.Equal(t, "aaaaaaaaaaaaaaaa", spanFive.GetParentID(), "Expected parent id from propagation context") 67 | assert.Equal(t, "fffffffffffffffffffffffffffffff", spanFive.GetTrace().GetTraceID(), "Expected trace id from propagation context") 68 | } 69 | 70 | func TestUnaryInterceptor(t *testing.T) { 71 | mo := &transmission.MockSender{} 72 | client, err := libhoney.NewClient(libhoney.ClientConfig{ 73 | APIKey: "placeholder", 74 | Dataset: "placeholder", 75 | APIHost: "placeholder", 76 | Transmission: mo}) 77 | assert.Equal(t, nil, err) 78 | beeline.Init(beeline.Config{Client: client}) 79 | 80 | md := metadata.New(map[string]string{ 81 | "content-type": "application/grpc", 82 | ":authority": "api.honeycomb.io:443", 83 | "user-agent": "testing-is-fun", 84 | "X-Forwarded-For": "10.11.12.13", // headers are Kabob-Title-Case from clients 85 | "X-Forwarded-Proto": "https", 86 | }) 87 | ctx := metadata.NewIncomingContext(context.Background(), md) 88 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 89 | return req, nil 90 | } 91 | info := &grpc.UnaryServerInfo{ 92 | FullMethod: "test.method", 93 | } 94 | interceptor := UnaryServerInterceptorWithConfig(config.GRPCIncomingConfig{}) 95 | var dummy interface{} 96 | resp, err := interceptor(ctx, dummy, info, handler) 97 | assert.NoError(t, err, "Unexpected error calling interceptor") 98 | assert.Equal(t, resp, dummy) 99 | 100 | evs := mo.Events() 101 | assert.Equal(t, 1, len(evs), "1 event is created") 102 | successfulFields := evs[0].Data 103 | 104 | contentType, ok := successfulFields["request.content_type"] 105 | assert.True(t, ok, "content-type field must exist on middleware generated event") 106 | assert.Equal(t, "application/grpc", contentType, "content-type should be set") 107 | 108 | authority, ok := successfulFields["request.header.authority"] 109 | assert.True(t, ok, "authority field must exist on middleware generated event") 110 | assert.Equal(t, "api.honeycomb.io:443", authority, "authority should be set") 111 | 112 | userAgent, ok := successfulFields["request.header.user_agent"] 113 | assert.True(t, ok, "user-agent expected to exist on middleware generated event") 114 | assert.Equal(t, "testing-is-fun", userAgent, "user-agent should be set") 115 | 116 | xForwardedFor, ok := successfulFields["request.header.x_forwarded_for"] 117 | assert.True(t, ok, "x_forwarded_for expected to exist on middleware generated event") 118 | assert.Equal(t, "10.11.12.13", xForwardedFor, "x_forwarded_for should be set") 119 | 120 | xForwardedProto, ok := successfulFields["request.header.x_forwarded_proto"] 121 | assert.True(t, ok, "x_forwarded_proto expected to exist on middleware generated event") 122 | assert.Equal(t, "https", xForwardedProto, "x_forwarded_proto should be set") 123 | 124 | method, ok := successfulFields["handler.method"] 125 | assert.True(t, ok, "method name should be set") 126 | assert.Equal(t, "test.method", method, "method name should be set") 127 | 128 | status, ok := successfulFields["response.grpc_status_code"] 129 | assert.True(t, ok, "Status code must exist on middleware generated event") 130 | assert.Equal(t, codes.OK, status, "status must exist") 131 | 132 | statusMsg, ok := successfulFields["response.grpc_status_message"] 133 | assert.True(t, ok, "Status message must exist on middleware generated event") 134 | assert.Equal(t, codes.OK.String(), statusMsg, "human-readable status must exist") 135 | } 136 | -------------------------------------------------------------------------------- /examples/grpc/proto/hello.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.6.1 5 | // source: hello.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type HelloRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 29 | } 30 | 31 | func (x *HelloRequest) Reset() { 32 | *x = HelloRequest{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_hello_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *HelloRequest) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*HelloRequest) ProtoMessage() {} 45 | 46 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 47 | mi := &file_hello_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 59 | func (*HelloRequest) Descriptor() ([]byte, []int) { 60 | return file_hello_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *HelloRequest) GetName() string { 64 | if x != nil { 65 | return x.Name 66 | } 67 | return "" 68 | } 69 | 70 | type HelloResponse struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` 76 | } 77 | 78 | func (x *HelloResponse) Reset() { 79 | *x = HelloResponse{} 80 | if protoimpl.UnsafeEnabled { 81 | mi := &file_hello_proto_msgTypes[1] 82 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 83 | ms.StoreMessageInfo(mi) 84 | } 85 | } 86 | 87 | func (x *HelloResponse) String() string { 88 | return protoimpl.X.MessageStringOf(x) 89 | } 90 | 91 | func (*HelloResponse) ProtoMessage() {} 92 | 93 | func (x *HelloResponse) ProtoReflect() protoreflect.Message { 94 | mi := &file_hello_proto_msgTypes[1] 95 | if protoimpl.UnsafeEnabled && x != nil { 96 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 97 | if ms.LoadMessageInfo() == nil { 98 | ms.StoreMessageInfo(mi) 99 | } 100 | return ms 101 | } 102 | return mi.MessageOf(x) 103 | } 104 | 105 | // Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead. 106 | func (*HelloResponse) Descriptor() ([]byte, []int) { 107 | return file_hello_proto_rawDescGZIP(), []int{1} 108 | } 109 | 110 | func (x *HelloResponse) GetGreeting() string { 111 | if x != nil { 112 | return x.Greeting 113 | } 114 | return "" 115 | } 116 | 117 | var File_hello_proto protoreflect.FileDescriptor 118 | 119 | var file_hello_proto_rawDesc = []byte{ 120 | 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 121 | 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 122 | 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 123 | 0x65, 0x22, 0x2b, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 124 | 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x01, 125 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x32, 0x3b, 126 | 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2b, 127 | 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x0d, 0x2e, 0x48, 0x65, 0x6c, 128 | 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 129 | 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 130 | 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x6f, 0x6e, 0x65, 0x79, 0x63, 131 | 0x6f, 0x6d, 0x62, 0x69, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x67, 132 | 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 133 | 0x33, 134 | } 135 | 136 | var ( 137 | file_hello_proto_rawDescOnce sync.Once 138 | file_hello_proto_rawDescData = file_hello_proto_rawDesc 139 | ) 140 | 141 | func file_hello_proto_rawDescGZIP() []byte { 142 | file_hello_proto_rawDescOnce.Do(func() { 143 | file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData) 144 | }) 145 | return file_hello_proto_rawDescData 146 | } 147 | 148 | var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 149 | var file_hello_proto_goTypes = []interface{}{ 150 | (*HelloRequest)(nil), // 0: HelloRequest 151 | (*HelloResponse)(nil), // 1: HelloResponse 152 | } 153 | var file_hello_proto_depIdxs = []int32{ 154 | 0, // 0: HelloService.SayHello:input_type -> HelloRequest 155 | 1, // 1: HelloService.SayHello:output_type -> HelloResponse 156 | 1, // [1:2] is the sub-list for method output_type 157 | 0, // [0:1] is the sub-list for method input_type 158 | 0, // [0:0] is the sub-list for extension type_name 159 | 0, // [0:0] is the sub-list for extension extendee 160 | 0, // [0:0] is the sub-list for field type_name 161 | } 162 | 163 | func init() { file_hello_proto_init() } 164 | func file_hello_proto_init() { 165 | if File_hello_proto != nil { 166 | return 167 | } 168 | if !protoimpl.UnsafeEnabled { 169 | file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 170 | switch v := v.(*HelloRequest); i { 171 | case 0: 172 | return &v.state 173 | case 1: 174 | return &v.sizeCache 175 | case 2: 176 | return &v.unknownFields 177 | default: 178 | return nil 179 | } 180 | } 181 | file_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 182 | switch v := v.(*HelloResponse); i { 183 | case 0: 184 | return &v.state 185 | case 1: 186 | return &v.sizeCache 187 | case 2: 188 | return &v.unknownFields 189 | default: 190 | return nil 191 | } 192 | } 193 | } 194 | type x struct{} 195 | out := protoimpl.TypeBuilder{ 196 | File: protoimpl.DescBuilder{ 197 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 198 | RawDescriptor: file_hello_proto_rawDesc, 199 | NumEnums: 0, 200 | NumMessages: 2, 201 | NumExtensions: 0, 202 | NumServices: 1, 203 | }, 204 | GoTypes: file_hello_proto_goTypes, 205 | DependencyIndexes: file_hello_proto_depIdxs, 206 | MessageInfos: file_hello_proto_msgTypes, 207 | }.Build() 208 | File_hello_proto = out.File 209 | file_hello_proto_rawDesc = nil 210 | file_hello_proto_goTypes = nil 211 | file_hello_proto_depIdxs = nil 212 | } 213 | -------------------------------------------------------------------------------- /propagation/tracestate.go: -------------------------------------------------------------------------------- 1 | package propagation 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // tracestate contains types and methods for constructing and parsing trace state 11 | // which is included in W3C trace context headers. Most of this code has been lifted 12 | // almost verbatim from: 13 | // https://github.com/open-telemetry/opentelemetry-go/blob/c912b179fd595cc7c6eec0b22e6c1371e31241d7/trace/tracestate.go 14 | 15 | var ( 16 | maxListMembers = 32 17 | 18 | listDelimiter = "," 19 | 20 | // based on the W3C Trace Context specification, see 21 | // https://www.w3.org/TR/trace-context-1/#tracestate-header 22 | noTenantKeyFormat = `[a-z][_0-9a-z\-\*\/]{0,255}` 23 | withTenantKeyFormat = `[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}` 24 | valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]` 25 | 26 | keyRe = regexp.MustCompile(`^((` + noTenantKeyFormat + `)|(` + withTenantKeyFormat + `))$`) 27 | valueRe = regexp.MustCompile(`^(` + valueFormat + `)$`) 28 | memberRe = regexp.MustCompile(`^\s*((` + noTenantKeyFormat + `)|(` + withTenantKeyFormat + `))=(` + valueFormat + `)\s*$`) 29 | 30 | errInvalidKey errorConst = "invalid tracestate key" 31 | errInvalidValue errorConst = "invalid tracestate value" 32 | errInvalidMember errorConst = "invalid tracestate list-member" 33 | errMemberNumber errorConst = "too many list-members in tracestate" 34 | errDuplicate errorConst = "duplicate list-member in tracestate" 35 | ) 36 | 37 | type member struct { 38 | Key string 39 | Value string 40 | } 41 | 42 | func newMember(key, value string) (member, error) { 43 | if !keyRe.MatchString(key) { 44 | return member{}, fmt.Errorf("%w: %s", errInvalidKey, key) 45 | } 46 | if !valueRe.MatchString(value) { 47 | return member{}, fmt.Errorf("%w: %s", errInvalidValue, value) 48 | } 49 | return member{Key: key, Value: value}, nil 50 | } 51 | 52 | func parseMemeber(m string) (member, error) { 53 | matches := memberRe.FindStringSubmatch(m) 54 | if len(matches) != 5 { 55 | return member{}, fmt.Errorf("%w: %s", errInvalidMember, m) 56 | } 57 | 58 | return member{ 59 | Key: matches[1], 60 | Value: matches[4], 61 | }, nil 62 | 63 | } 64 | 65 | // String encodes member into a string compliant with the W3C Trace Context 66 | // specification. 67 | func (m member) String() string { 68 | return fmt.Sprintf("%s=%s", m.Key, m.Value) 69 | } 70 | 71 | // TraceState provides additional vendor-specific trace identification 72 | // information across different distributed tracing systems. It represents an 73 | // immutable list consisting of key/value pairs, each pair is referred to as a 74 | // list-member. 75 | // 76 | // TraceState conforms to the W3C Trace Context specification 77 | // (https://www.w3.org/TR/trace-context-1). All operations that create or copy 78 | // a TraceState do so by validating all input and will only produce TraceState 79 | // that conform to the specification. Specifically, this means that all 80 | // list-member's key/value pairs are valid, no duplicate list-members exist, 81 | // and the maximum number of list-members (32) is not exceeded. 82 | type TraceState struct { //nolint:revive // revive complains about stutter of `trace.TraceState` 83 | // list is the members in order. 84 | list []member 85 | } 86 | 87 | var _ json.Marshaler = TraceState{} 88 | 89 | // ParseTraceState attempts to decode a TraceState from the passed 90 | // string. It returns an error if the input is invalid according to the W3C 91 | // Trace Context specification. 92 | func ParseTraceState(tracestate string) (TraceState, error) { 93 | if tracestate == "" { 94 | return TraceState{}, nil 95 | } 96 | 97 | wrapErr := func(err error) error { 98 | return fmt.Errorf("failed to parse tracestate: %w", err) 99 | } 100 | 101 | var members []member 102 | found := make(map[string]struct{}) 103 | for _, memberStr := range strings.Split(tracestate, listDelimiter) { 104 | if len(memberStr) == 0 { 105 | continue 106 | } 107 | 108 | m, err := parseMemeber(memberStr) 109 | if err != nil { 110 | return TraceState{}, wrapErr(err) 111 | } 112 | 113 | if _, ok := found[m.Key]; ok { 114 | return TraceState{}, wrapErr(errDuplicate) 115 | } 116 | found[m.Key] = struct{}{} 117 | 118 | members = append(members, m) 119 | if n := len(members); n > maxListMembers { 120 | return TraceState{}, wrapErr(errMemberNumber) 121 | } 122 | } 123 | 124 | return TraceState{list: members}, nil 125 | } 126 | 127 | // MarshalJSON marshals the TraceState into JSON. 128 | func (ts TraceState) MarshalJSON() ([]byte, error) { 129 | return json.Marshal(ts.String()) 130 | } 131 | 132 | // String encodes the TraceState into a string compliant with the W3C 133 | // Trace Context specification. The returned string will be invalid if the 134 | // TraceState contains any invalid members. 135 | func (ts TraceState) String() string { 136 | members := make([]string, len(ts.list)) 137 | for i, m := range ts.list { 138 | members[i] = m.String() 139 | } 140 | return strings.Join(members, listDelimiter) 141 | } 142 | 143 | // Get returns the value paired with key from the corresponding TraceState 144 | // list-member if it exists, otherwise an empty string is returned. 145 | func (ts TraceState) Get(key string) string { 146 | for _, member := range ts.list { 147 | if member.Key == key { 148 | return member.Value 149 | } 150 | } 151 | 152 | return "" 153 | } 154 | 155 | // Insert adds a new list-member defined by the key/value pair to the 156 | // TraceState. If a list-member already exists for the given key, that 157 | // list-member's value is updated. The new or updated list-member is always 158 | // moved to the beginning of the TraceState as specified by the W3C Trace 159 | // Context specification. 160 | // 161 | // If key or value are invalid according to the W3C Trace Context 162 | // specification an error is returned with the original TraceState. 163 | // 164 | // If adding a new list-member means the TraceState would have more members 165 | // than is allowed an error is returned instead with the original TraceState. 166 | func (ts TraceState) Insert(key, value string) (TraceState, error) { 167 | m, err := newMember(key, value) 168 | if err != nil { 169 | return ts, err 170 | } 171 | 172 | cTS := ts.Delete(key) 173 | if cTS.Len()+1 > maxListMembers { 174 | // TODO (MrAlias): When the second version of the Trace Context 175 | // specification is published this needs to not return an error. 176 | // Instead it should drop the "right-most" member and insert the new 177 | // member at the front. 178 | // 179 | // https://github.com/w3c/trace-context/pull/448 180 | return ts, fmt.Errorf("failed to insert: %w", errMemberNumber) 181 | } 182 | 183 | cTS.list = append(cTS.list, member{}) 184 | copy(cTS.list[1:], cTS.list) 185 | cTS.list[0] = m 186 | 187 | return cTS, nil 188 | } 189 | 190 | // Delete returns a copy of the TraceState with the list-member identified by 191 | // key removed. 192 | func (ts TraceState) Delete(key string) TraceState { 193 | members := make([]member, ts.Len()) 194 | copy(members, ts.list) 195 | for i, member := range ts.list { 196 | if member.Key == key { 197 | members = append(members[:i], members[i+1:]...) 198 | // TraceState should contain no duplicate members. 199 | break 200 | } 201 | } 202 | return TraceState{list: members} 203 | } 204 | 205 | // Len returns the number of list-members in the TraceState. 206 | func (ts TraceState) Len() int { 207 | return len(ts.list) 208 | } 209 | -------------------------------------------------------------------------------- /trace/doc.go: -------------------------------------------------------------------------------- 1 | // Package trace gives access to direct control over the span and trace objects 2 | // used by the beeline. 3 | // 4 | // Summary 5 | // 6 | // Elementary use of the Honeycomb beeline has few needs - use a wrapper to 7 | // cover most of the basic info about a given event and then augment that event 8 | // with a few fields using `beeline.AddField`. Occasionally add an additional 9 | // span to the trace that is automatically created and add fields to that. 10 | // However, as the beeline starts being used in more sophisticated applications, 11 | // or new wrappers are being written, more direct control is needed to manipulate 12 | // the spans and traces generated by the beeline. Using the `trace` package 13 | // enables this more sophisticated use. 14 | // 15 | // The types of use that are enabled by using the trace package: 16 | // 17 | // 1) creating asynchronous spans; spans that outlive the main trace. These spans 18 | // are especially useful for goroutines that manage some background task after 19 | // the main user-facing chore has been completed. Examples of this are sending 20 | // an email or persisting accepted data. 21 | // 22 | // 2) creating traces with upstream parents when you are a downstream service. 23 | // The existing HTTP wrappers do this for you, but if your trace is getting 24 | // propagated via kafka or SQS or some other mechanism you may need to do this 25 | // yourself 26 | // 27 | // 3) adding fields that use a different naming scheme. Fields added via the 28 | // beeline are all namespaced under `app.`, which is convenient when you're 29 | // adding a few. When you have more complicated code to manage, it can be 30 | // useful to use your own naming scheme. Adding fields directly to the span or 31 | // trace objects allows you to specify the full field name with no prefix. 32 | // 33 | // Lifecycle 34 | // 35 | // A trace is made up of spans. A span represents a single unit of work (what 36 | // makes up a unit of work is up to the application). Spans come in two flavors, 37 | // synchronous (the default) and asynchronous. Synchronous spans finish before 38 | // their parents, async spans don't. Which you should use depends on the 39 | // structure of code - if the outer function will block until the inner function 40 | // returns, a sync span is appropriate. If the inner function is called in a 41 | // goroutine and expected to outlive the outer function, an async span fits 42 | // better. 43 | // 44 | // Spans should be created as children from an existing span. If there is no 45 | // current span, first create a new trace then get its root span and use that to 46 | // create subsequent spans. The beeline `StartSpan()` takes care of all of this 47 | // for you, but if you're using the trace package directly you need to manage 48 | // that bookkeeping. 49 | // 50 | // When should you create a new span? There are no strict rules, but there are a 51 | // few heuristics. If there is a process that will repeat in a loop (a batch or 52 | // something) and each run through the loop is important, make a span. If the 53 | // code being instrumented has enough attributes that are relevant to it 54 | // directly to warrant its own bag of data, make a new span. If all you're 55 | // interested in is a simple timer, it's often cleaner to add that timer to the 56 | // current existing span. 57 | // 58 | // Spans must have `Send()` called in order to be sent to Honeycomb. Every span 59 | // that is created should have a corresponding `Send()` call. When `Send()` is 60 | // called a few things happen. First, there is some trace-level accounting that 61 | // is done (eg adding trace level fields, determining position in the trace, 62 | // finishing the running timer, etc.). When that finishes the presend and 63 | // sampler hooks are called. Finally, the span is dispatched to Honeycomb. 64 | // 65 | // Any span that calls out to another service can serialize the current state of 66 | // the trace into a string suitable for including as an HTTP header (or other 67 | // similar method for encoding as part of a message). That serialized form can 68 | // be fed into the downstream service that will use it to start a new trace 69 | // using the same trace ID. When you look at one of these traces in Honeycomb, 70 | // you will see any spans created by the downstream service appear as children 71 | // of the span that serialized its state. The serialized state includes the 72 | // trace ID and the ID of the span that serialized state, as well as an encoded 73 | // form of all trace level fields. 74 | // 75 | // Putting all this together, this is a visualization of a request that spawns 76 | // two goroutines, each of which must return before the root span can return. 77 | // Each of those also has a synchronous span as a child. One of those also kicks 78 | // off an async span to save some state and it does not block returning the 79 | // result to the original caller. 80 | // 81 | // |----------- root span -----------| 82 | // \---- sync child ----| 83 | // \----| 84 | // \------ async child ---------| 85 | // \--- sync child -------| 86 | // \-------------| 87 | // 88 | // Sampling 89 | // 90 | // The default sampling applied by the beeline samples entire traces. For 91 | // example, if you set a sample rate to 10, then one out of 10 traces will be 92 | // sent, and all spans in that trace will be sent (or none at all). If you take 93 | // advantage of the SamplerHook, it is up to you and your implementation to 94 | // decide whether to sample entire traces or individual spans. If traces are 95 | // incomplete (i.e. some spans are kept and others dropped), the Honeycomb UI 96 | // will show missing traces where there are children of dropped spans. Any 97 | // dropped spans that have no children will be entirely absent from the UI. 98 | // 99 | // Use 100 | // 101 | // While easiest to use the `beeline` package and existing wrappers to do most 102 | // of the legwork for you, here is the general flow of interaction with traces. 103 | // 104 | // - start of the request 105 | // 106 | // When a request starts or program execution begins, create a trace with 107 | // `NewTrace`. If the program is downstream of something that is also traced, 108 | // capture the serialized trace headers and pass them in to the trace creation 109 | // to connect the two. The `NewTrace` function puts the trace and root span in 110 | // the context for you. 111 | // 112 | // - during work 113 | // 114 | // As your program flows the most common pattern will be to start a span at the 115 | // beginning of a function and then immediately defer sending that span. 116 | // 117 | // func myFunc(ctx context.Context) { 118 | // ctx, span := beeline.StartSpan(ctx) // use the beeline if you can 119 | // // parentSpan := trace.GetSpanFromContext(ctx).CreateChild() // or do it manually 120 | // // not shown here - if you do it manually, check for nil to avoid panic 121 | // defer span.Send() 122 | // ... 123 | // span.AddField("app.fancy_feast_flavor", "paté") 124 | // ... 125 | // } 126 | // 127 | // - wrapping up 128 | // 129 | // When each span finishes and gets sent, it also sends any synchronous 130 | // children. Any synchronous spans that were unsent when their parent finished 131 | // will get sent by the parent and will have an additional field 132 | // (`meta.sent_by_parent`) added to indicate that they were unsent. Sending 133 | // unsent spans is likely indicative of either an opportunity to use an async 134 | // span or a bug in the program where a span accidentally does not get sent. 135 | package trace 136 | -------------------------------------------------------------------------------- /wrappers/hnygrpc/grpc.go: -------------------------------------------------------------------------------- 1 | package hnygrpc 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "reflect" 7 | "runtime" 8 | 9 | "github.com/honeycombio/beeline-go/propagation" 10 | "github.com/honeycombio/beeline-go/timer" 11 | "github.com/honeycombio/beeline-go/trace" 12 | "github.com/honeycombio/beeline-go/wrappers/config" 13 | "github.com/honeycombio/libhoney-go" 14 | 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/metadata" 17 | "google.golang.org/grpc/peer" 18 | "google.golang.org/grpc/status" 19 | ) 20 | 21 | // This is a map of GRPC request header names whose values will be retrieved 22 | // and added to handler spans as fields with the corresponding name. 23 | // 24 | // Header names must be lowercase as the metadata.MD API will have normalized 25 | // incoming headers to lower. 26 | // 27 | // The field names should turn dashes (-) into underscores (_) to follow 28 | // precident in HTTP request headers and the patterns established and in 29 | // naming patterns in OTel attributes for requests. 30 | var headersToFields = map[string]string{ 31 | "content-type": "request.content_type", 32 | ":authority": "request.header.authority", 33 | "user-agent": "request.header.user_agent", 34 | "x-forwarded-for": "request.header.x_forwarded_for", 35 | "x-forwarded-proto": "request.header.x_forwarded_proto", 36 | } 37 | 38 | // getMetadataStringValue is a simpler helper method that checks the provided 39 | // metadata for a value associated with the provided key. If the value exists, 40 | // it is returned. If the value does not exist, an empty string is returned. 41 | func getMetadataStringValue(md metadata.MD, key string) string { 42 | if val, ok := md[key]; ok { 43 | if len(val) > 0 { 44 | return val[0] 45 | } 46 | return "" 47 | } 48 | return "" 49 | } 50 | 51 | // startSpanOrTraceFromUnaryGRPC checks to see if a trace already exists in the 52 | // provided context before creating either a root span or a child span of the 53 | // existing active span. The function understands trace parser hooks, so if one 54 | // is provided, it'll use it to parse the incoming request for trace context. 55 | func startSpanOrTraceFromUnaryGRPC( 56 | ctx context.Context, 57 | info *grpc.UnaryServerInfo, 58 | parserHook config.GRPCTraceParserHook, 59 | ) (context.Context, *trace.Span) { 60 | span := trace.GetSpanFromContext(ctx) 61 | if span == nil { 62 | // no active span, create a new trace 63 | var tr *trace.Trace 64 | md, ok := metadata.FromIncomingContext(ctx) 65 | if ok { 66 | if parserHook == nil { 67 | beelineHeader := getMetadataStringValue(md, propagation.TracePropagationGRPCHeader) 68 | prop, _ := propagation.UnmarshalHoneycombTraceContext(beelineHeader) 69 | ctx, tr = trace.NewTrace(ctx, prop) 70 | } else { 71 | prop := parserHook(ctx) 72 | ctx, tr = trace.NewTraceFromPropagationContext(ctx, prop) 73 | } 74 | } else { 75 | ctx, tr = trace.NewTrace(ctx, nil) 76 | } 77 | span = tr.GetRootSpan() 78 | } else { 79 | // create new span as child of active span. 80 | ctx, span = span.CreateChild(ctx) 81 | } 82 | return ctx, span 83 | } 84 | 85 | // addFields just adds available information about a gRPC request to the provided span. 86 | func addFields(ctx context.Context, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, span *trace.Span) { 87 | handlerName := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() 88 | 89 | span.AddField("name", handlerName) 90 | span.AddField("meta.type", "grpc_request") 91 | span.AddField("handler.name", handlerName) 92 | span.AddField("handler.method", info.FullMethod) 93 | 94 | pr, ok := peer.FromContext(ctx) 95 | if ok { 96 | // if we have an address, put it on the span 97 | if pr.Addr != net.Addr(nil) { 98 | span.AddField("request.remote_addr", pr.Addr.String()) 99 | } 100 | } 101 | 102 | md, ok := metadata.FromIncomingContext(ctx) 103 | if ok { 104 | for headerName, fieldName := range headersToFields { 105 | if val, ok := md[headerName]; ok { 106 | span.AddField(fieldName, val[0]) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // UnaryServerInterceptorWithConfig will create a Honeycomb event per invocation of the 113 | // returned interceptor. If passed a config.GRPCIncomingConfig with a GRPCParserHook, 114 | // the hook will be called when creating the event, allowing it to specify how trace context 115 | // information should be included in the span (e.g. it may have come from a remote parent in 116 | // a specific format). 117 | // 118 | // Events created from GRPC interceptors will contain information from the gRPC metadata, if 119 | // it exists, as well as information about the handler used and method being called. 120 | func UnaryServerInterceptorWithConfig(cfg config.GRPCIncomingConfig) grpc.UnaryServerInterceptor { 121 | return func( 122 | ctx context.Context, 123 | req interface{}, 124 | info *grpc.UnaryServerInfo, 125 | handler grpc.UnaryHandler, 126 | ) (interface{}, error) { 127 | ctx, span := startSpanOrTraceFromUnaryGRPC(ctx, info, cfg.GRPCParserHook) 128 | defer span.Send() 129 | 130 | addFields(ctx, info, handler, span) 131 | resp, err := handler(ctx, req) 132 | if err != nil { 133 | span.AddTraceField("handler_error", err.Error()) 134 | } 135 | code := status.Code(err) 136 | span.AddField("response.grpc_status_code", code) 137 | span.AddField("response.grpc_status_message", code.String()) 138 | return resp, err 139 | } 140 | } 141 | 142 | // UnaryServerInterceptor is identical to UnaryServerInterceptorWithConfig called 143 | // with an empty config.GRPCIncomingConfig. 144 | func UnaryServerInterceptor() grpc.UnaryServerInterceptor { 145 | return UnaryServerInterceptorWithConfig(config.GRPCIncomingConfig{}) 146 | } 147 | 148 | // UnaryClientInterceptorWithConfig will create a Honeycomb span per invocation 149 | // of the returned interceptor. It will also serialize the trace propagation 150 | // context into the gRPC metadata so it can be deserialized by the server. If 151 | // passed a config.GRPCOutgoingConfig with a GRPCTracePropagationHook, the hook 152 | // will be called when populating the gRPC metadata, allowing it to specify how 153 | // trace context information should be included in the metadata (e.g. if the 154 | // remote server expects it to come in a specific format). 155 | func UnaryClientInterceptorWithConfig(cfg config.GRPCOutgoingConfig) grpc.UnaryClientInterceptor { 156 | return func( 157 | ctx context.Context, 158 | method string, 159 | req interface{}, 160 | reply interface{}, 161 | cc *grpc.ClientConn, 162 | invoker grpc.UnaryInvoker, 163 | opts ...grpc.CallOption, 164 | ) error { 165 | span := trace.GetSpanFromContext(ctx) 166 | 167 | // If there's no active trace or span, just send an event. 168 | if span == nil { 169 | tm := timer.Start() 170 | ev := libhoney.NewEvent() 171 | defer ev.Send() 172 | 173 | ev.AddField("name", method) 174 | ev.AddField("meta.type", "grpc_client") 175 | ev.AddField("request.target", cc.Target()) 176 | 177 | err := invoker(ctx, method, req, reply, cc, opts...) 178 | if err != nil { 179 | ev.AddField("error", err.Error()) 180 | } 181 | dur := tm.Finish() 182 | ev.AddField("duration_ms", dur) 183 | return err 184 | } 185 | 186 | ctx, span = span.CreateChild(ctx) 187 | defer span.Send() 188 | 189 | span.AddField("name", method) 190 | span.AddField("meta.type", "grpc_client") 191 | span.AddField("request.target", cc.Target()) 192 | 193 | md, ok := metadata.FromOutgoingContext(ctx) 194 | if !ok { 195 | md = metadata.New(nil) 196 | } else { 197 | // Modifying the result of FromOutgoingContext may race, so copy instead. 198 | md = md.Copy() 199 | } 200 | 201 | if cfg.GRPCPropagationHook == nil { 202 | md.Set(propagation.TracePropagationGRPCHeader, span.SerializeHeaders()) 203 | } else { 204 | // If a propagationHook exists, call it to get a metadata to append. 205 | md = metadata.Join(md, cfg.GRPCPropagationHook(span.PropagationContext())) 206 | } 207 | 208 | ctx = metadata.NewOutgoingContext(ctx, md) 209 | err := invoker(ctx, method, req, reply, cc, opts...) 210 | if err != nil { 211 | span.AddField("error", err.Error()) 212 | } 213 | return err 214 | } 215 | } 216 | 217 | // UnaryClientInterceptor is identical to UnaryClientInterceptorWithConfig called 218 | // with an empty config.GRPCOutgoingConfig. 219 | func UnaryClientInterceptor() grpc.UnaryClientInterceptor { 220 | return UnaryClientInterceptorWithConfig(config.GRPCOutgoingConfig{}) 221 | } 222 | -------------------------------------------------------------------------------- /wrappers/common/common_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/honeycombio/beeline-go/propagation" 13 | "github.com/honeycombio/beeline-go/trace" 14 | libhoney "github.com/honeycombio/libhoney-go" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestHostHeader(t *testing.T) { 19 | req := httptest.NewRequest("GET", "/", nil) 20 | req.Host = "beecom.com" 21 | props := GetRequestProps(req) 22 | assert.Equal(t, "beecom.com", props["request.host"]) 23 | } 24 | 25 | func TestNoHostHeader(t *testing.T) { 26 | // if there is no host header, httptest defaults to using `example.com` 27 | req := httptest.NewRequest("GET", "/", nil) 28 | props := GetRequestProps(req) 29 | assert.Equal(t, "example.com", props["request.host"]) 30 | } 31 | 32 | func TestURLHostHeader(t *testing.T) { 33 | req := httptest.NewRequest("GET", "https://doorcom.com/", nil) 34 | props := GetRequestProps(req) 35 | assert.Equal(t, "doorcom.com", props["request.host"]) 36 | } 37 | 38 | func TestClientReqHostAfterRedirect(t *testing.T) { 39 | // This test constructs a request the same way the http client constructs 40 | // one when generating a new request to follow a redirect: 41 | // https://github.com/golang/go/blob/9baddd3f21230c55f0ad2a10f5f20579dcf0a0bb/src/net/http/client.go#L644-L662 42 | // 43 | // When the redirect Location header contains an absolute URL, the new 44 | // request will have an empty Host field. This ensures we capture a useful 45 | // request.host property based on the URL itself. 46 | u, err := url.Parse("http://example.com/") 47 | assert.NoError(t, err) 48 | req := &http.Request{ 49 | Method: "GET", 50 | URL: u, 51 | Header: make(http.Header), 52 | } 53 | assert.Equal(t, "", req.Host) 54 | props := GetRequestProps(req) 55 | assert.Equal(t, "example.com", props["request.host"]) 56 | } 57 | 58 | func TestUserAgentHeader(t *testing.T) { 59 | userAgent := "Lynx" 60 | req := httptest.NewRequest("GET", "https://unused.com/", nil) 61 | req.Header.Set("User-Agent", userAgent) 62 | props := GetRequestProps(req) 63 | assert.Equal(t, userAgent, props["request.header.user_agent"]) 64 | } 65 | 66 | func TestXForwardedForHeader(t *testing.T) { 67 | xForwardedFor := "1.2.3.4" 68 | req := httptest.NewRequest("GET", "https://unused.com/", nil) 69 | req.Header.Set("X-Forwarded-For", xForwardedFor) 70 | props := GetRequestProps(req) 71 | assert.Equal(t, xForwardedFor, props["request.header.x_forwarded_for"]) 72 | } 73 | 74 | func TestXForwardedProtoHeader(t *testing.T) { 75 | xForwardedProto := "https" 76 | req := httptest.NewRequest("GET", "https://unused.com/", nil) 77 | req.Header.Set("X-Forwarded-Proto", xForwardedProto) 78 | props := GetRequestProps(req) 79 | assert.Equal(t, xForwardedProto, props["request.header.x_forwarded_proto"]) 80 | } 81 | 82 | // objForDBCalls gives us an object off which to hang the fake database call 83 | // since the database naming thing looks for actual database calls by name 84 | // it needs a function named as one of the functions in the `dbNames` list 85 | // somewhere in the call stack in order to work. This is essentially a mock 86 | // db call for it to find. 87 | type objForDBCalls struct{} 88 | 89 | func (objForDBCalls) ExecContext(bld *libhoney.Builder, query string) *libhoney.Event { 90 | return sharedDBEvent(bld, query) 91 | } 92 | 93 | // TestSharedDBEvent verifies that the name field is set to something 94 | func TestSharedDBEvent(t *testing.T) { 95 | bld := libhoney.NewBuilder() 96 | query := "this is sql really promise" 97 | var ev *libhoney.Event 98 | 99 | // first test uses sharedDBEvent from outside a blessed db path, and it 100 | // shouldn't really work well but it shouldn't crash 101 | func() { ev = sharedDBEvent(bld, query) }() 102 | assert.Equal(t, "db", ev.Fields()["name"], "being called with a non-database call returns default 'db'") 103 | assert.Equal(t, "", ev.Fields()["db.call"], "being called with a non-database call does not set db.call") 104 | assert.Equal(t, "TestSharedDBEvent", ev.Fields()["db.caller"], "caller should still be TestSharedDBEvent even if no DB call was found") 105 | 106 | // now we test it as though it really is coming from a DB package with a 107 | // real DB call like ExecContext. This best models how the instrumentation 108 | // will be set in a real world use. 109 | o := objForDBCalls{} 110 | ev = o.ExecContext(bld, query) 111 | assert.Equal(t, "ExecContext", ev.Fields()["name"], "being called with a db-specific call returns that string") 112 | assert.Equal(t, "ExecContext", ev.Fields()["db.call"], "being called with a db-specific call returns that string") 113 | assert.Equal(t, "TestSharedDBEvent", ev.Fields()["db.caller"], "caller should be this test function") 114 | } 115 | func TestResponseWriter(t *testing.T) { 116 | rr := httptest.NewRecorder() 117 | wr := NewResponseWriter(rr) 118 | wr.Wrapped.WriteHeader(222) 119 | assert.Equal(t, 222, wr.Status) 120 | wr.Wrapped.WriteHeader(333) 121 | assert.Equal(t, 222, wr.Status) 122 | } 123 | 124 | func TestResponseWriterTypeAssertions(t *testing.T) { 125 | // testResponseWriter implements common http.ResponseWriter optional interfaces 126 | type testResponseWriter struct { 127 | http.ResponseWriter 128 | http.Hijacker 129 | http.Flusher 130 | http.CloseNotifier 131 | http.Pusher 132 | io.ReaderFrom 133 | } 134 | 135 | wr := NewResponseWriter(testResponseWriter{}) 136 | 137 | if _, ok := interface{}(wr).(http.ResponseWriter); ok { 138 | t.Errorf("ResponseWriter improperly implements http.ResponseWriter") 139 | } 140 | 141 | if _, ok := wr.Wrapped.(http.Flusher); !ok { 142 | t.Errorf("ResponseWriter does not implement http.Flusher") 143 | } 144 | if _, ok := wr.Wrapped.(http.CloseNotifier); !ok { 145 | t.Errorf("ResponseWriter does not implement http.CloseNotifier") 146 | } 147 | if _, ok := wr.Wrapped.(http.Hijacker); !ok { 148 | t.Errorf("ResponseWriter does not implement http.Hijacker") 149 | } 150 | if _, ok := wr.Wrapped.(http.Pusher); !ok { 151 | t.Errorf("ResponseWriter does not implement http.Pusher") 152 | } 153 | if _, ok := wr.Wrapped.(io.ReaderFrom); !ok { 154 | t.Errorf("ResponseWriter does not implement io.ReaderFrom") 155 | } 156 | } 157 | 158 | func TestBuildDBEvent(t *testing.T) { 159 | b := libhoney.NewBuilder() 160 | _, sender := BuildDBEvent(b, sql.DBStats{}, "") 161 | sender(nil) 162 | } 163 | 164 | func TestBuildDBSpan(t *testing.T) { 165 | b := libhoney.NewBuilder() 166 | ctx := context.Background() 167 | ctx, _, sender := BuildDBSpan(ctx, b, sql.DBStats{}, "") 168 | sender(nil) 169 | } 170 | 171 | func TestStartSpanOrTraceFromHTTP(t *testing.T) { 172 | t.Run("when no propagation headers present, starts a new trace", func(t *testing.T) { 173 | u, _ := url.Parse("https://test.com") 174 | req := &http.Request{ 175 | Method: "GET", 176 | URL: u, 177 | Header: make(http.Header), 178 | } 179 | ctx, _ := StartSpanOrTraceFromHTTP(req) 180 | traceFromContext := trace.GetTraceFromContext(ctx) 181 | assert.Equal(t, "", traceFromContext.GetParentID()) 182 | }) 183 | t.Run("when honeycomb propagation header present, uses honeycomb", func(t *testing.T) { 184 | u, _ := url.Parse("https://test.com") 185 | header := make(http.Header) 186 | header.Set(propagation.TracePropagationHTTPHeader, "1;trace_id=abcdef,parent_id=12345") 187 | req := &http.Request{ 188 | Method: "GET", 189 | URL: u, 190 | Header: header, 191 | } 192 | ctx, _ := StartSpanOrTraceFromHTTP(req) 193 | traceFromContext := trace.GetTraceFromContext(ctx) 194 | assert.Equal(t, "12345", traceFromContext.GetParentID()) 195 | assert.Equal(t, "abcdef", traceFromContext.GetTraceID()) 196 | }) 197 | t.Run("when w3c propagation header present, uses w3c", func(t *testing.T) { 198 | u, _ := url.Parse("https://test.com") 199 | header := make(http.Header) 200 | header.Set(propagation.TraceparentHeader, "00-7f042f75651d9782dcff93a45fa99be0-c998e73e5420f609-01") 201 | req := &http.Request{ 202 | Method: "GET", 203 | URL: u, 204 | Header: header, 205 | } 206 | ctx, _ := StartSpanOrTraceFromHTTP(req) 207 | traceFromContext := trace.GetTraceFromContext(ctx) 208 | assert.Equal(t, "c998e73e5420f609", traceFromContext.GetParentID()) 209 | assert.Equal(t, "7f042f75651d9782dcff93a45fa99be0", traceFromContext.GetTraceID()) 210 | }) 211 | t.Run("when both honeycomb and w3c propagation headers present, uses honeycomb", func(t *testing.T) { 212 | u, _ := url.Parse("https://test.com") 213 | header := make(http.Header) 214 | header.Set(propagation.TracePropagationHTTPHeader, "1;trace_id=abcdef,parent_id=12345") 215 | header.Set(propagation.TraceparentHeader, "00-7f042f75651d9782dcff93a45fa99be0-c998e73e5420f609-01") 216 | req := &http.Request{ 217 | Method: "GET", 218 | URL: u, 219 | Header: header, 220 | } 221 | ctx, _ := StartSpanOrTraceFromHTTP(req) 222 | traceFromContext := trace.GetTraceFromContext(ctx) 223 | assert.Equal(t, "12345", traceFromContext.GetParentID()) 224 | assert.Equal(t, "abcdef", traceFromContext.GetTraceID()) 225 | }) 226 | } 227 | --------------------------------------------------------------------------------