├── .circleci └── config.yml ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── question-discussion.md │ └── security-vulnerability-report.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── release.yml └── workflows │ ├── add-to-project-v2.yml │ ├── apply-labels.yml │ ├── stale.yml │ └── validate-pr-title.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── DEVELOPMENT.md ├── LICENSE ├── Makefile ├── NOTICE ├── OSSMETADATA ├── README.md ├── RELEASING.md ├── SECURITY.md ├── SUPPORT.md ├── client.go ├── client_test.go ├── doc.go ├── examples ├── json_reader │ ├── example1.json │ ├── example2.json │ └── read_json_log.go ├── webapp │ ├── Dockerfile │ ├── README.md │ ├── db.go │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── honeycomb_helpers.go │ ├── main.go │ ├── templates │ │ ├── base.html │ │ ├── home.html │ │ ├── login.html │ │ └── signup.html │ └── types.go └── wiki-manual-tracing │ ├── README.md │ ├── edit.html │ ├── view.html │ └── wiki.go ├── go.mod ├── go.sum ├── helpers_test.go ├── libhoney.go ├── libhoney_test.go ├── logger.go ├── mockoutput.go ├── transmission ├── discard.go ├── event.go ├── event_test.go ├── helpers_test.go ├── logger.go ├── metrics.go ├── mock.go ├── response.go ├── response_test.go ├── sender.go ├── transmission.go ├── transmission_test.go ├── writer.go └── writer_test.go ├── version └── version.go └── writer.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # enable a job when tag created (tag create is ignored by default) 4 | filters_always: &filters_always 5 | filters: 6 | tags: 7 | only: /.*/ 8 | 9 | # restrict a job to only run when a version tag (vNNNN) is created 10 | filters_publish: &filters_publish 11 | filters: 12 | tags: 13 | only: /^v[0-9].*/ 14 | branches: 15 | ignore: /.*/ 16 | 17 | matrix_goversions: &matrix_goversions 18 | matrix: 19 | parameters: 20 | goversion: ["21", "22", "23"] 21 | 22 | # Default version of Go to use for Go steps 23 | default_goversion: &default_goversion "21" 24 | 25 | executors: 26 | go: 27 | parameters: 28 | goversion: 29 | type: string 30 | default: *default_goversion 31 | working_directory: /home/circleci/go/src/github.com/honeycombio/libhoney-go 32 | docker: 33 | - image: cimg/go:1.<< parameters.goversion >> 34 | environment: 35 | GO111MODULE: "on" 36 | github: 37 | docker: 38 | - image: cibuilds/github:0.13.0 39 | 40 | jobs: 41 | test: 42 | parameters: 43 | goversion: 44 | type: string 45 | default: *default_goversion 46 | executor: 47 | name: go 48 | goversion: "<< parameters.goversion >>" 49 | steps: 50 | - checkout 51 | - run: make test 52 | - store_test_results: 53 | path: ./unit-tests.xml 54 | - run: 55 | name: Build JSON reader example 56 | command: go build examples/json_reader/read_json_log.go 57 | - run: 58 | name: Build Wiki example 59 | command: go build examples/wiki-manual-tracing/wiki.go 60 | 61 | publish_github: 62 | executor: github 63 | steps: 64 | - attach_workspace: 65 | at: ~/ 66 | - run: 67 | name: "create draft release at GitHub" 68 | command: ghr -draft -n ${CIRCLE_TAG} -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} 69 | 70 | workflows: 71 | nightly: 72 | triggers: 73 | - schedule: 74 | cron: "0 0 * * *" 75 | filters: 76 | branches: 77 | only: 78 | - main 79 | jobs: 80 | - test: 81 | <<: *matrix_goversions 82 | 83 | build_libhoney: 84 | jobs: 85 | - test: 86 | <<: *filters_always 87 | <<: *matrix_goversions 88 | - publish_github: 89 | <<: *filters_publish 90 | context: Honeycomb Secrets for Public Repos 91 | filters: 92 | tags: 93 | only: /^v[0-9].*/ 94 | branches: 95 | ignore: /.*/ 96 | requires: 97 | - test 98 | 99 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | - Libhoney: 22 | 23 | **Steps to reproduce** 24 | 25 | 1. 26 | 27 | **Additional context** 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | - Libhoney: 19 | 20 | **Description** 21 | 22 | (Please include any relevant CVE advisory links) 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | commit-message: 17 | prefix: "maint" 18 | include: "scope" 19 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/apply-labels.yml: -------------------------------------------------------------------------------- 1 | name: Apply project labels 2 | on: [issues, pull_request_target, label] 3 | jobs: 4 | apply-labels: 5 | runs-on: ubuntu-latest 6 | name: Apply common project labels 7 | steps: 8 | - uses: honeycombio/oss-management-actions/labels@v1 9 | with: 10 | github-token: ${{ secrets.GITHUB_TOKEN }} 11 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | name: 'Close stale issues and PRs' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | start-date: '2021-09-01T00:00:00Z' 18 | stale-issue-message: 'Marking this issue as stale because it has been open 14 days with no activity. Please add a comment if this is still an ongoing issue; otherwise this issue will be automatically closed in 7 days.' 19 | stale-pr-message: 'Marking this PR as stale because it has been open 30 days with no activity. Please add a comment if this PR is still relevant; otherwise this PR will be automatically closed in 7 days.' 20 | close-issue-message: 'Closing this issue due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 21 | close-pr-message: 'Closing this PR due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 22 | days-before-issue-stale: 14 23 | days-before-pr-stale: 30 24 | days-before-issue-close: 7 25 | days-before-pr-close: 7 26 | any-of-labels: 'status: info needed,status: revision needed' 27 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Example artifacts 2 | examples/wiki-manual-tracing/*.txt 3 | 4 | # Test report 5 | unit-tests.xml 6 | 7 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Folders 13 | _obj 14 | _test 15 | 16 | # Architecture specific extensions/prefixes 17 | *.[568vq] 18 | [568vq].out 19 | 20 | *.cgo1.go 21 | *.cgo2.c 22 | _cgo_defun.c 23 | _cgo_gotypes.go 24 | _cgo_export.* 25 | 26 | _testmain.go 27 | 28 | *.exe 29 | *.test 30 | *.prof 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # libhoney Changelog 2 | 3 | ## 1.25.0 2025-01-09 4 | 5 | Honeycomb's backend can now accept events with sizes up to 1 million bytes. This release bumps libhoney to 6 | conform to the same limit. It also updates some libraries and sets the minimum version of Go to 1.21. 7 | 8 | - maint: Increase max event size, clean up some nits (#261) | @kentquirk 9 | - maint(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#260) | @dependabot 10 | 11 | ## 1.24.0 2024-11-22 12 | 13 | ### Enhancements 14 | 15 | - feat: improve error string for too large events (#258) | @maplebed 16 | 17 | ### Maintenance 18 | 19 | - maint(build): add 1.23 to supported versions (#256) | @lizthegrey 20 | - docs: update vulnerability reporting process (#253) | @robbkidd 21 | - maint(deps): bump github.com/DataDog/zstd from 1.5.5 to 1.5.6 (#254) | @dependabot 22 | - maint(deps): bump github.com/klauspost/compress from 1.17.8 to 1.17.9 (#251) | @dependabot 23 | - build(deps): bump github.com/gorilla/schema from 1.2.0 to 1.4.1 in /examples/webapp (#252) | @dependabot 24 | 25 | ## 1.23.1 2024-06-13 26 | 27 | ### Fixes 28 | 29 | - fix: Build the URL using url.JoinPath instead of path.Join (#249) | @MikeGoldsmith 30 | 31 | ## 1.23.0 2024-06-10 32 | 33 | ### ⚠️ Breaking Changes ⚠️ 34 | 35 | Minimum Go version required is 1.19 36 | 37 | ### Enhancements 38 | 39 | - feat: URL encode dataset (#242) | @MikeGoldsmith 40 | 41 | ### Maintenance 42 | 43 | - maint: add labels to release.yml for auto-generated grouping (#241) | @JamieDanielson 44 | - maint: Update minimum go version to 1.19 | @MikeGoldsmith 45 | - chore: Update dependabot reviewer to pipeline-team | @MikeGoldsmith 46 | - maint(deps): bump github.com/klauspost/compress from 1.16.6 to 1.17.8 | @dependabot 47 | - maint(deps): bump github.com/vmihailenco/msgpack/v5 from 5.3.5 to 5.4.1 (#248) | @dependabot 48 | - maint(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#247) | @dependabot 49 | 50 | ## 1.22.0 2024-03-04 51 | 52 | ### Enhancements 53 | 54 | - feat: introduce IsClassicKey helper function (#239) | @jharley 55 | 56 | ## 1.21.0 2024-02-28 57 | 58 | ### Enhancements 59 | 60 | - feat: support classic-flavored ingest keys (#237) | @jharley 61 | 62 | ### Maintenance 63 | 64 | - maint: update codeowners to pipeline (#233) | @JamieDanielson 65 | - maint: update codeowners to pipeline-team (#234) | @JamieDanielson 66 | 67 | ## 1.20.0 2023-06-29 68 | 69 | ### Enhancements 70 | 71 | - perf: allow pre-sizing the data map (#228) | @lizthegrey 72 | 73 | ### Maintenance 74 | 75 | - maint(deps): bump github.com/klauspost/compress from 1.16.5 to 1.16.6 (#229) | @dependabot 76 | 77 | ## 1.19.0 2023-06-05 78 | 79 | ### ⚠️ Breaking Changes ⚠️ 80 | 81 | Minimum Go version required is 1.17 82 | 83 | ### Maintenance 84 | 85 | - Drop go 14, 15, 16 (#225) | @vreynolds 86 | - Bump github.com/stretchr/testify from 1.8.0 to 1.8.2 (#218) | @dependabot 87 | - Bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#224) | @dependabot 88 | - Bump github.com/klauspost/compress from 1.15.9 to 1.16.5 (#223) | @dependabot 89 | - Bump github.com/DataDog/zstd from 1.5.2 to 1.5.5 (#222) | @dependabot 90 | 91 | ## 1.18.0 2022-10-28 92 | 93 | ### Enhancements 94 | 95 | - Include Go version, GOOS, & GOARCH in user-agent (#207) | @robbkidd 96 | 97 | ### Maintenance 98 | 99 | - Convert stray fmt.Printf into logger.Printf (#203) | @glenjamin 100 | 101 | ## 1.17.1 2022-10-19 102 | 103 | ### Fixed 104 | 105 | - Pre-define field map capacities (#197) | [lizthegrey](https://github.com/lizthegrey) 106 | 107 | ### Maintenance 108 | 109 | - Add release file (#199) | [@vreynolds](https://github.com/vreynolds) 110 | - Add new project workflow (#196) | [@vreynolds](https://github.com/vreynolds) 111 | 112 | ## 1.17.0 2022-09-23 113 | 114 | ### Enhancements 115 | 116 | - feat: adds a configurable event batch send timeout (#190) | [@robbkidd](https://github.com/robbkidd) 117 | 118 | ### Maintenance 119 | 120 | - maint: add go 1.19 to CI (#189) | [@vreynolds](https://github.com/vreynolds) 121 | - docs: add wiki manual tracing example (#188) | [@vreynolds](https://github.com/vreynolds) 122 | - Bump github.com/klauspost/compress from 1.15.7 to 1.15.9 (#192) | [@robbkidd](https://github.com/robbkidd) 123 | 124 | ## 1.16.0 2022-07-13 125 | 126 | There were several v2 releases that were unusable because they were incomplete according to Go's semantic versioning strategy. 127 | Changes that appeared in those unusable v2 releases are consolidated into this minor release. 128 | 129 | ### ⚠️ Breaking Changes ⚠️ 130 | 131 | Minimum Go version required is 1.14 132 | 133 | ### Enhancements 134 | 135 | - Update default dataset name for non-classic API keys (#170) | [@MikeGoldsmith](https://github.com/MikeGoldsmith) 136 | - Add support to retrieve team and environment (#183) | [@MikeGoldsmith](https://github.com/MikeGoldsmith) 137 | 138 | ### Maintenance 139 | 140 | - maint: drop support for go before 1.14 (#164) | [lizthegrey](https://github.com/lizthegrey) 141 | - maint: add go 1.18 to CI (#172) | [@vreynolds](https://github.com/vreynolds) 142 | - Fix race condition in test and other test bugs (#162) | [@kentquirk](https://github.com/kentquirk) 143 | - Update examples (#184) | [@vreynolds](https://github.com/vreynolds) 144 | - Build example app during CI test phase (#179) | [@MikeGoldsmith](https://github.com/MikeGoldsmith) 145 | - Bump github.com/stretchr/testify from 1.6.1 to 1.8.0 (#111, #174, #181) | [dependabot](https://github.com/dependabot) 146 | - Bump github.com/klauspost/compress from 1.13.6 to 1.15.7 (#175, #177, #180, #182) | [dependabot](https://github.com/dependabot) 147 | - Bump github.com/DataDog/zstd from 1.5.0 to 1.5.2 (#178) | [dependabot](https://github.com/dependabot) 148 | 149 | ## 1.15.8 2022-01-05 150 | 151 | ### Fixed 152 | 153 | - Pass bytes.Reader to http.Request, clean up pooledReader (#159) | | [lizthegrey](https://github.com/lizthegrey) 154 | 155 | ## 1.15.7 2022-01-04 156 | 157 | ### Fixed 158 | 159 | - Don't crash on stream aborts, always add content length (#156) | [lizthegrey](https://github.com/lizthegrey) 160 | 161 | ### Maintenance 162 | 163 | - Add re-triage workflow (#155) | [vreynolds](https://github.com/vreynolds) 164 | - Bump github.com/vmihailenco/msgpack/v5 from 5.3.4 to 5.3.5 (#149) 165 | - Bump github.com/DataDog/zstd from 1.4.8 to 1.5.0 (#153) 166 | - Bump github.com/klauspost/compress from 1.13.5 to 1.13.6 (#145) 167 | 168 | ## 1.15.6 2021-11-03 169 | 170 | ### Fixed 171 | 172 | - Ensure valid JSON even when individual events in a batch can't be marshalled (#151) 173 | 174 | ### Maintenance 175 | 176 | - empower apply-labels action to apply labels (#150) 177 | - add min go version to readme (#147) 178 | - update certs in old CI image (#148) 179 | - ci: remove buildevents from nightly (#144) 180 | - ci: secrets management (#142) 181 | 182 | ## 1.15.5 2021-09-27 183 | 184 | ### Fixed 185 | 186 | - fix race condition on Honeycomb.Flush() (#140) | [@bfreis](https://github.com/bfreis) 187 | 188 | ### Maintenance 189 | 190 | - Change maintenance badge to maintained (#138) 191 | - Adds Stalebot (#141) 192 | - Add issue and PR templates (#136) 193 | - Add OSS lifecycle badge (#135) 194 | - Add community health files (#134) 195 | - Bump github.com/klauspost/compress from 1.12.3 to 1.13.5 (#130, #137) 196 | - Bump github.com/vmihailenco/msgpack/v5 from 5.2.0 to 5.3.4 (#133) 197 | 198 | ## 1.15.4 2021-07-21 199 | 200 | ### Maintenance 201 | 202 | - Upgrade msgpack from v4 to v5. (#127) 203 | 204 | ## 1.15.3 2021-06-02 205 | 206 | ### Improvements 207 | 208 | - Add more context to batch response parsing error (#116) 209 | 210 | ### Maintenance 211 | 212 | - Add go 1.15 & 1.16 to the testing matrix (#114, #119) 213 | 214 | ## 1.15.2 2021-01-22 215 | 216 | NOTE: v1.15.1 may cause update warnings due to checksum error, please use v1.15.2 instead. 217 | 218 | ### Maintenance 219 | 220 | - Add Github action to manage project labels (#110) 221 | - Automate the creation of draft releases when project is tagged (#109) 222 | 223 | ## 1.15.1 2021-01-14 224 | 225 | ### Improvements 226 | 227 | - Fix data race on dynFields length in Builder.Clone (#72) 228 | 229 | ### Maintenance 230 | 231 | - Update dependencies 232 | - github.com/klauspost/compress from 1.11.2 to 1.11.4 (#105, #106) 233 | 234 | ## 1.15.0 2020-11-10 235 | 236 | - Mask writekey when printing events (#103) 237 | 238 | ## 1.14.1 2020-9-24 239 | 240 | - Add .editorconfig to help provide consistent IDE styling (#99) 241 | 242 | ## 1.14.0 2020-09-01 243 | 244 | - Documentation - document potential failures if pendingWorkCapacity not specified 245 | - Documentation - use Deprecated tags for deprecated fields 246 | - Log when event batch is rejected with an invalid API key 247 | - Dependency bump (compress) 248 | 249 | ## 1.13.0 2020-08-21 250 | 251 | - This release includes a change by @apechimp that makes Flush thread-safe (#80) 252 | - Update dependencies 253 | - Have a more obvious default statsd prefix (libhoney) 254 | -------------------------------------------------------------------------------- /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/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Libhoney contributors: 2 | 3 | Andy Isaacson 4 | Ben Hartshorne 5 | Brad Erickson 6 | Charity Majors 7 | Chris Toshok 8 | Christine Yen 9 | Christopher Swenson 10 | Conrad Irwin 11 | Eben Freeman 12 | Ian Smith 13 | Ian Wilkes 14 | Nathan LeClaire 15 | Rachel Fong 16 | Travis Redman 17 | 18 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | ## Requirements 4 | 5 | Go: https://go.dev/doc/install 6 | 7 | ## Install Dependencies 8 | 9 | ```shell 10 | go mod download 11 | ``` 12 | 13 | ## Run Tests 14 | 15 | To run all tests: 16 | 17 | ```shell 18 | go test -race -v ./... 19 | ``` 20 | 21 | To run individual tests: 22 | 23 | ```shell 24 | go test -race -v -run TestEmptyHoneycombTransmission ./... 25 | ``` 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | #: run the tests! 3 | test: 4 | ifeq (, $(shell which gotestsum)) 5 | @echo " ***" 6 | @echo "Running with standard go test because gotestsum was not found on PATH. Consider installing gotestsum for friendlier test output!" 7 | @echo " ***" 8 | go test -race -v ./... 9 | else 10 | gotestsum --junitfile unit-tests.xml --format testname -- -race ./... 11 | endif 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=maintained 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libhoney 2 | 3 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/libhoney-go?color=success)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | [![CircleCI](https://circleci.com/gh/honeycombio/libhoney-go.svg?style=shield)](https://circleci.com/gh/honeycombio/libhoney-go) 5 | 6 | Go library for sending events to [Honeycomb](https://honeycomb.io), a service for debugging your software in production. 7 | 8 | - [Usage and Examples](https://docs.honeycomb.io/sdk/go/) 9 | - [API Reference](https://godoc.org/github.com/honeycombio/libhoney-go) 10 | 11 | For tracing support and automatic instrumentation of Gorilla, `httprouter`, `sqlx`, and other common libraries, check out our [Beeline for Go](https://github.com/honeycombio/beeline-go). 12 | 13 | ## Dependencies 14 | 15 | Golang 1.19+ 16 | 17 | ## Contributions 18 | 19 | See [DEVELOPMENT.md](./DEVELOPMENT.md) 20 | 21 | Features, bug fixes and other changes to libhoney are gladly accepted. Please 22 | open issues or a pull request with your change. Remember to add your name to the 23 | CONTRIBUTORS file! 24 | 25 | All contributions will be released under the Apache License 2.0. 26 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Creating a new release 2 | 3 | 1. Update the Version string in `version/version.go`. 4 | 2. Add new release notes to the Changelog. 5 | 3. Open a PR with above changes. 6 | 4. Once the above PR is merged, tag `main` with the new version, e.g. `v0.1.1`. Push the tags. This will kick off a CI workflow, which will publish a draft GitHub release. 7 | 5. Update Release Notes on the new draft GitHub release, and publish that. 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/honeycombio/libhoney-go/transmission" 8 | ) 9 | 10 | // Client represents an object that can create new builders and events and send 11 | // them somewhere. It maintains its own sending queue for events, distinct from 12 | // both the package-level libhoney queue and any other client. Clients should be 13 | // created with NewClient(config). A manually created Client{} will function as 14 | // a nil output and drop everything handed to it (so can be safely used in dev 15 | // and tests). For more complete testing you can create a Client with a 16 | // MockOutput transmission then inspect the events it would have sent. 17 | type Client struct { 18 | transmission transmission.Sender 19 | logger Logger 20 | builder *Builder 21 | 22 | oneTx sync.Once 23 | oneLogger sync.Once 24 | oneBuilder sync.Once 25 | } 26 | 27 | // ClientConfig is a subset of the global libhoney config that focuses on the 28 | // configuration of the client itself. The other config options are specific to 29 | // a given transmission Sender and should be specified there if the defaults 30 | // need to be overridden. 31 | type ClientConfig struct { 32 | // APIKey is the Honeycomb authentication token. If it is specified during 33 | // libhoney initialization, it will be used as the default API key for all 34 | // events. If absent, API key must be explicitly set on a builder or 35 | // event. Find your team's API keys at https://ui.honeycomb.io/account 36 | APIKey string 37 | 38 | // Dataset is the name of the Honeycomb dataset to which to send these events. 39 | // If it is specified during libhoney initialization, it will be used as the 40 | // default dataset for all events. If absent, dataset must be explicitly set 41 | // on a builder or event. 42 | Dataset string 43 | 44 | // SampleRate is the rate at which to sample this event. Default is 1, 45 | // meaning no sampling. If you want to send one event out of every 250 times 46 | // Send() is called, you would specify 250 here. 47 | SampleRate uint 48 | 49 | // APIHost is the hostname for the Honeycomb API server to which to send this 50 | // event. default: https://api.honeycomb.io/ 51 | APIHost string 52 | 53 | // Transmission allows you to override what happens to events after you call 54 | // Send() on them. By default, events are asynchronously sent to the 55 | // Honeycomb API. You can use the MockOutput included in this package in 56 | // unit tests, or use the transmission.WriterSender to write events to 57 | // STDOUT or to a file when developing locally. 58 | Transmission transmission.Sender 59 | 60 | // Logger defaults to nil and the SDK is silent. If you supply a logger here 61 | // (or set it to &DefaultLogger{}), some debugging output will be emitted. 62 | // Intended for human consumption during development to understand what the 63 | // SDK is doing and diagnose trouble emitting events. 64 | Logger Logger 65 | } 66 | 67 | // NewClient creates a Client with defaults correctly set 68 | func NewClient(conf ClientConfig) (*Client, error) { 69 | if conf.SampleRate == 0 { 70 | conf.SampleRate = defaultSampleRate 71 | } 72 | if conf.APIHost == "" { 73 | conf.APIHost = defaultAPIHost 74 | } 75 | if conf.Dataset == "" { 76 | conf.Dataset = defaultDataset 77 | } 78 | 79 | c := &Client{ 80 | logger: conf.Logger, 81 | } 82 | c.ensureLogger() 83 | 84 | if conf.Transmission == nil { 85 | c.transmission = &transmission.Honeycomb{ 86 | MaxBatchSize: DefaultMaxBatchSize, 87 | BatchTimeout: DefaultBatchTimeout, 88 | MaxConcurrentBatches: DefaultMaxConcurrentBatches, 89 | PendingWorkCapacity: DefaultPendingWorkCapacity, 90 | UserAgentAddition: UserAgentAddition, 91 | Logger: c.logger, 92 | Metrics: sd, 93 | } 94 | } else { 95 | c.transmission = conf.Transmission 96 | } 97 | if err := c.transmission.Start(); err != nil { 98 | c.logger.Printf("transmission client failed to start: %s", err.Error()) 99 | return nil, err 100 | } 101 | 102 | c.builder = &Builder{ 103 | WriteKey: conf.APIKey, 104 | Dataset: conf.Dataset, 105 | SampleRate: conf.SampleRate, 106 | APIHost: conf.APIHost, 107 | dynFields: make([]dynamicField, 0, 0), 108 | fieldHolder: fieldHolder{ 109 | data: make(map[string]interface{}), 110 | }, 111 | client: c, 112 | } 113 | 114 | return c, nil 115 | } 116 | 117 | func (c *Client) ensureTransmission() { 118 | c.oneTx.Do(func() { 119 | if c.transmission == nil { 120 | c.transmission = &transmission.DiscardSender{} 121 | c.transmission.Start() 122 | } 123 | }) 124 | } 125 | 126 | func (c *Client) ensureLogger() { 127 | c.oneLogger.Do(func() { 128 | if c.logger == nil { 129 | c.logger = &nullLogger{} 130 | } 131 | }) 132 | } 133 | 134 | func (c *Client) ensureBuilder() { 135 | c.oneBuilder.Do(func() { 136 | if c.builder == nil { 137 | c.builder = &Builder{ 138 | SampleRate: 1, 139 | dynFields: make([]dynamicField, 0, 0), 140 | fieldHolder: fieldHolder{ 141 | data: make(map[string]interface{}), 142 | }, 143 | client: c, 144 | } 145 | } 146 | }) 147 | } 148 | 149 | // Close waits for all in-flight messages to be sent. You should 150 | // call Close() before app termination. 151 | func (c *Client) Close() { 152 | c.ensureLogger() 153 | c.logger.Printf("closing libhoney client") 154 | if c.transmission != nil { 155 | c.transmission.Stop() 156 | } 157 | } 158 | 159 | // Flush closes and reopens the Output interface, ensuring events 160 | // are sent without waiting on the batch to be sent asyncronously. 161 | // Generally, it is more efficient to rely on asyncronous batches than to 162 | // call Flush, but certain scenarios may require Flush if asynchronous sends 163 | // are not guaranteed to run (i.e. running in AWS Lambda) 164 | func (c *Client) Flush() { 165 | c.ensureLogger() 166 | c.logger.Printf("flushing libhoney client") 167 | if c.transmission != nil { 168 | if err := c.transmission.Flush(); err != nil { 169 | c.logger.Printf("unable to flush: %v", err) 170 | } 171 | } 172 | } 173 | 174 | // TxResponses returns the channel from which the caller can read the responses 175 | // to sent events. 176 | func (c *Client) TxResponses() chan transmission.Response { 177 | c.ensureTransmission() 178 | return c.transmission.TxResponses() 179 | } 180 | 181 | // AddDynamicField takes a field name and a function that will generate values 182 | // for that metric. The function is called once every time a NewEvent() is 183 | // created and added as a field (with name as the key) to the newly created 184 | // event. 185 | func (c *Client) AddDynamicField(name string, fn func() interface{}) error { 186 | c.ensureTransmission() 187 | c.ensureBuilder() 188 | return c.builder.AddDynamicField(name, fn) 189 | } 190 | 191 | // AddField adds a Field to the Client's scope. This metric will be inherited by 192 | // all builders and events. 193 | func (c *Client) AddField(name string, val interface{}) { 194 | c.ensureTransmission() 195 | c.ensureBuilder() 196 | c.builder.AddField(name, val) 197 | } 198 | 199 | // Add adds its data to the Client's scope. It adds all fields in a struct or 200 | // all keys in a map as individual Fields. These metrics will be inherited by 201 | // all builders and events. 202 | func (c *Client) Add(data interface{}) error { 203 | c.ensureTransmission() 204 | c.ensureBuilder() 205 | return c.builder.Add(data) 206 | } 207 | 208 | // NewEvent creates a new event prepopulated with any Fields present in the 209 | // Client's scope. 210 | func (c *Client) NewEvent() *Event { 211 | c.ensureTransmission() 212 | c.ensureBuilder() 213 | return c.builder.NewEvent() 214 | } 215 | 216 | // NewBuilder creates a new event builder. The builder inherits any Dynamic or 217 | // Static Fields present in the Client's scope. 218 | func (c *Client) NewBuilder() *Builder { 219 | c.ensureTransmission() 220 | c.ensureBuilder() 221 | return c.builder.Clone() 222 | } 223 | 224 | // sendResponse sends a dropped event response down the response channel 225 | func (c *Client) sendDroppedResponse(e *Event, message string) { 226 | c.ensureTransmission() 227 | r := transmission.Response{ 228 | Err: errors.New(message), 229 | Metadata: e.Metadata, 230 | } 231 | c.transmission.SendResponse(r) 232 | 233 | } 234 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/honeycombio/libhoney-go/transmission" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestClientAdding(t *testing.T) { 14 | b := &Builder{ 15 | dynFields: make([]dynamicField, 0, 0), 16 | fieldHolder: fieldHolder{ 17 | data: make(map[string]interface{}), 18 | }, 19 | } 20 | c := &Client{ 21 | builder: b, 22 | } 23 | 24 | c.AddDynamicField("dynamo", func() interface{} { return nil }) 25 | assert.Equal(t, 1, len(b.dynFields), "after adding dynamic field, builder should have it") 26 | 27 | c.AddField("pine", 34) 28 | assert.Equal(t, 34, b.data["pine"], "after adding field, builder should have it") 29 | 30 | c.Add(map[string]interface{}{"birch": 45}) 31 | assert.Equal(t, 45, b.data["birch"], "after adding complex field, builder should have it") 32 | 33 | ev := c.NewEvent() 34 | assert.Equal(t, 34, ev.data["pine"], "with default content, created events should be prepopulated") 35 | 36 | b2 := c.NewBuilder() 37 | assert.Equal(t, 34, b2.data["pine"], "with default content, cloned builders should be prepopulated") 38 | } 39 | 40 | func TestNewClient(t *testing.T) { 41 | conf := ClientConfig{ 42 | APIKey: "Oliver", 43 | } 44 | c, err := NewClient(conf) 45 | 46 | assert.NoError(t, err, "new client should not error") 47 | assert.Equal(t, "Oliver", c.builder.WriteKey, "initialized client should respect config") 48 | } 49 | 50 | func TestClientIsolated(t *testing.T) { 51 | c1, _ := NewClient(ClientConfig{}) 52 | c2, _ := NewClient(ClientConfig{}) 53 | 54 | AddField("Mary", 83) 55 | c1.AddField("Ursula", 88) 56 | c2.AddField("Philip", 53) 57 | 58 | ed := NewEvent() 59 | assert.Equal(t, 83, ed.data["Mary"], "global libhoney should have global content") 60 | assert.Equal(t, nil, ed.data["Ursula"], "global libhoney should not have client content") 61 | assert.Equal(t, nil, ed.data["Philip"], "global libhoney should not have client content") 62 | 63 | e1 := c1.NewEvent() 64 | assert.Equal(t, 88, e1.data["Ursula"], "client events should have client-scoped date") 65 | assert.Equal(t, nil, e1.data["Philip"], "client events should not have other client's content") 66 | assert.Equal(t, nil, e1.data["Mary"], "client events should not have global content") 67 | 68 | e2 := c2.NewEvent() 69 | assert.Equal(t, 53, e2.data["Philip"], "client events should have client-scoped data") 70 | assert.Equal(t, nil, e2.data["Ursula"], "client events should not have other client's content") 71 | assert.Equal(t, nil, e2.data["Mary"], "client events should not have global content") 72 | } 73 | 74 | func TestClientRaces(t *testing.T) { 75 | t.Parallel() 76 | wg := sync.WaitGroup{} 77 | wg.Add(3) 78 | for i := 0; i < 3; i++ { 79 | go func() { 80 | c, _ := NewClient(ClientConfig{}) 81 | c.AddField("name", "val") 82 | ev := c.NewEvent() 83 | ev.AddField("argel", "bargle") 84 | ev.Send() 85 | c.Close() 86 | wg.Done() 87 | }() 88 | } 89 | wg.Wait() 90 | } 91 | 92 | // dirtySender is a transmisison Sender that reads and writes all the event's 93 | // fields in an attempt to create a data race 94 | type dirtySender struct{} 95 | 96 | func (ds *dirtySender) Start() error { return nil } 97 | func (ds *dirtySender) Stop() error { return nil } 98 | func (ds *dirtySender) Flush() error { return nil } 99 | func (ds *dirtySender) TxResponses() chan transmission.Response { return nil } 100 | func (ds *dirtySender) SendResponse(transmission.Response) bool { return true } 101 | func (ds *dirtySender) Add(ev *transmission.Event) { 102 | // fmt.Printf("transmission address of ev.Data is %v", ev.Data) 103 | oldAPIKey := ev.APIKey 104 | ev.APIKey = "some new value" 105 | oldDataset := ev.Dataset 106 | ev.Dataset = "some different value" 107 | oldSampleRate := ev.SampleRate 108 | ev.SampleRate = 10 109 | oldAPIHost := ev.APIHost 110 | ev.APIHost = "some wacky value" 111 | oldTimestamp := ev.Timestamp 112 | ev.Timestamp = time.Now() 113 | oldMetadata := ev.Metadata 114 | ev.Metadata = "some intertype value" 115 | ev.Data["new value"] = fmt.Sprintf("%s %s %d %s %s %+v", oldAPIKey, 116 | oldDataset, oldSampleRate, oldAPIHost, oldTimestamp.Format(time.RFC3339), oldMetadata) 117 | vals := make([]interface{}, 0) 118 | keys := make([]string, 0) 119 | for key, val := range ev.Data { 120 | time.Sleep(1) 121 | keys = append(keys, key) 122 | vals = append(vals, val) 123 | } 124 | for _, key := range keys { 125 | time.Sleep(1) 126 | ev.Data[key] = "overwrite all keys in the event" 127 | } 128 | } 129 | 130 | func TestAddSendRaces(t *testing.T) { 131 | t.Parallel() 132 | wg := sync.WaitGroup{} 133 | wg.Add(1) 134 | tx := &dirtySender{} 135 | c, _ := NewClient(ClientConfig{ 136 | Transmission: tx, 137 | }) 138 | ev := c.NewEvent() 139 | ev.WriteKey = "oh my, a write" 140 | ev.Dataset = "there is no data in this set" 141 | ev.APIHost = "APIHostess with the mostess" 142 | ev.AddField("preexisting", "condition") 143 | for i := 0; i < 10; i++ { 144 | wg.Add(1) 145 | go func(num int) { 146 | ev.AddField(fmt.Sprintf("argle%d", num), fmt.Sprintf("bargle%d", num)) 147 | wg.Done() 148 | }(i) 149 | } 150 | go func() { 151 | // fmt.Printf("libh address of ev.data is %v", &ev.data) 152 | err := ev.Send() 153 | assert.NoError(t, err, "sending event shouldn't error") 154 | wg.Done() 155 | }() 156 | for i := 0; i < 10; i++ { 157 | wg.Add(1) 158 | go func(num int) { 159 | ev.AddField(fmt.Sprintf("foogle%d", num), fmt.Sprintf("boogle%d", num)) 160 | wg.Done() 161 | }(i) 162 | } 163 | wg.Wait() 164 | // fmt.Printf("after, event data is %v", ev.data) 165 | } 166 | 167 | // Expected to error, but not panic. 168 | func TestDefaultClient(t *testing.T) { 169 | t.Parallel() 170 | 171 | c := Client{} 172 | e := c.NewEvent() 173 | e.AddField("foo", "bar") 174 | err := e.Send() 175 | assert.Error(t, err) 176 | 177 | b := c.NewBuilder() 178 | e = b.NewEvent() 179 | e.AddField("foo", "bar") 180 | err = e.Send() 181 | assert.Error(t, err) 182 | } 183 | 184 | func TestEnsureLoggerRaces(t *testing.T) { 185 | t.Parallel() 186 | c := Client{} 187 | 188 | // Close() ensures the Logger exists. 189 | wg := sync.WaitGroup{} 190 | for i := 0; i < 10; i++ { 191 | wg.Add(2) 192 | go func() { c.Close(); wg.Done() }() 193 | go func() { c.Close(); wg.Done() }() 194 | } 195 | wg.Wait() 196 | } 197 | 198 | func TestEnsureTransmissionRaces(t *testing.T) { 199 | t.Parallel() 200 | c := Client{} 201 | 202 | // TxResponses() ensures the Transmission exists. 203 | wg := sync.WaitGroup{} 204 | for i := 0; i < 10; i++ { 205 | wg.Add(2) 206 | go func() { c.TxResponses(); wg.Done() }() 207 | go func() { c.TxResponses(); wg.Done() }() 208 | } 209 | wg.Wait() 210 | } 211 | 212 | func TestEnsureBuilderRaces(t *testing.T) { 213 | t.Parallel() 214 | c := Client{} 215 | 216 | // AddField() ensures the Builder exists. 217 | wg := sync.WaitGroup{} 218 | for i := 0; i < 10; i++ { 219 | wg.Add(2) 220 | go func() { c.AddField("ready, set", "GO"); wg.Done() }() 221 | go func() { c.AddField("ready, and", "GO"); wg.Done() }() 222 | } 223 | wg.Wait() 224 | } 225 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package libhoney is a client library for sending data to https://honeycomb.io 3 | 4 | Summary 5 | 6 | libhoney aims to make it as easy as possible to create events and send them on 7 | into Honeycomb. 8 | 9 | See https://honeycomb.io/docs for background on this library. 10 | 11 | Look in the examples/ directory for a complete example using libhoney. 12 | */ 13 | package libhoney 14 | -------------------------------------------------------------------------------- /examples/json_reader/example1.json: -------------------------------------------------------------------------------- 1 | {"timestamp":"2016-05-12T19:00:03-08:00","ingredient":"bacon","amount":"5 strips", "step":1, "extra_instructions":"cook first, then set aside"} 2 | {"timestamp":"2016-05-12T19:13:03-08:00","ingredient":"onions","amount":"1 small", "step":2} 3 | {"timestamp":"2016-05-12T19:12:03-08:00","ingredient":"garlic","amount":"2 cloves", "step":3} 4 | {"timestamp":"Monday at 12:30pm", "ingredient":"bell pepper","amount":"1/2", "step":4, "comment":"broken time field; this event will be sent with the current time."} 5 | {"ingredient":"eggs","amount":"3", "step":5, "comment":"missing time field; this event will be sent with the current time."} 6 | -------------------------------------------------------------------------------- /examples/json_reader/example2.json: -------------------------------------------------------------------------------- 1 | {"timestamp":"2016-05-12T20:00:03-08:00","ingredient":"eggplant","amount":"1 large", "step":1} 2 | {"timestamp":"2016-05-12T20:13:03-08:00","ingredient":"green beans","amount":"2 cups", "step":2, "extra_instructions": "remove ends, snap in half."} 3 | {"timestamp":"2016-05-12T20:12:03-08:00","ingredient":"garlic","amount":"1/2 head", "step":3} 4 | {"malformed"-"json" example of what happens when coming across malformed json 5 | {"timestamp":"2016-05-12T20:10:00-08:00","ingredient":"spices","amount":"generous", "step":5} 6 | -------------------------------------------------------------------------------- /examples/json_reader/read_json_log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "runtime" 10 | "sync" 11 | "time" 12 | 13 | libhoney "github.com/honeycombio/libhoney-go" 14 | ) 15 | 16 | // This example reads JSON blobs from a file and sends them as Honeycomb events. 17 | // It will log the success or failure of each sent event to STDOUT. 18 | // The expectation of the file is that it has one JSON blob per line. Each 19 | // JSON blob must have a field named timestamp that includes a time objected 20 | // formatted like RFC3339. We will use that field to set the timestamp for the 21 | // event. 22 | 23 | const ( 24 | version = "0.0.1-example" 25 | honeyFakeAPIKey = "abcabc123123defdef456456" 26 | honeyDataset = "example json blobs" 27 | ) 28 | 29 | var jsonFilePaths = []string{"./example1.json", "./example2.json"} 30 | 31 | func main() { 32 | 33 | // basic initialization 34 | libhConf := libhoney.Config{ 35 | // TODO change to use APIKey 36 | APIKey: honeyFakeAPIKey, 37 | Dataset: honeyDataset, 38 | Logger: &libhoney.DefaultLogger{}, 39 | } 40 | libhoney.Init(libhConf) 41 | 42 | wg := sync.WaitGroup{} 43 | wg.Add(1) 44 | go readResponses(&wg, libhoney.Responses()) 45 | 46 | // We want every event to include the number of currently running goroutines 47 | // and the version number of this app. The goroutines is contrived for this 48 | // example, but is useful in larger apps. Adding the version number to the 49 | // global scope means every event sent will include this field. 50 | libhoney.AddDynamicField("num_goroutines", 51 | func() interface{} { return runtime.NumGoroutine() }) 52 | libhoney.AddField("read_json_log_version", version) 53 | 54 | // go through each json file and parse it. 55 | for _, fileName := range jsonFilePaths { 56 | fh, err := os.Open(fileName) 57 | if err != nil { 58 | fmt.Println("Error opening file:", err) 59 | os.Exit(1) 60 | } 61 | defer fh.Close() 62 | 63 | // Create a new builder to store the information about the file being 64 | // processed. This builder will be passed in to processLine so all events 65 | // created within that function will have the information about the file 66 | // and line being processed. 67 | perFileBulider := libhoney.NewBuilder() 68 | perFileBulider.AddField("json_file_name", fileName) 69 | 70 | scanner := bufio.NewScanner(fh) 71 | i := 1 72 | for scanner.Scan() { 73 | // each time this is added to the builder the field is overwritten 74 | perFileBulider.AddField("json_line_number", i) 75 | i += 1 76 | processLine(scanner.Text(), perFileBulider) 77 | } 78 | } 79 | 80 | libhoney.Close() 81 | wg.Wait() 82 | fmt.Println("All done! Go check Honeycomb https://ui.honeycomb.io/ to see your data.") 83 | } 84 | 85 | func readResponses(wg *sync.WaitGroup, responses chan libhoney.Response) { 86 | // responses will be closed when libhoney is closed 87 | for r := range responses { 88 | if r.StatusCode >= 200 && r.StatusCode < 300 { 89 | id := r.Metadata.(string) 90 | fmt.Printf("Successfully sent event %s to Honeycomb\n", id) 91 | } else { 92 | fmt.Printf("Error sending event to Honeycomb! Code %d, err %v and response body %s\n", 93 | r.StatusCode, r.Err, r.Body) 94 | } 95 | } 96 | wg.Done() 97 | } 98 | 99 | func processLine(line string, builder *libhoney.Builder) { 100 | 101 | // Create the event that this line will fill. because we're creating it from 102 | // the Builder, it will already have a field containing the name and line 103 | // number of the JSON file we're parsing. 104 | ev := builder.NewEvent() 105 | ev.Metadata = fmt.Sprintf("id %d", rand.Intn(20)) 106 | defer ev.Send() 107 | defer fmt.Printf("Sending event %s\n", ev.Metadata) 108 | 109 | // unmarshal the JSON blob 110 | data := make(map[string]interface{}) 111 | err := json.Unmarshal([]byte(line), &data) 112 | if err != nil { 113 | ev.AddField("error", err.Error()) 114 | return 115 | } 116 | 117 | // Override the event timestamp if the JSON blob has a valid time. If time 118 | // is missing or it doesn't parse correctly, the event will be sent with the 119 | // default time (Now()) 120 | if timeVal, ok := data["timestamp"]; ok { 121 | ts, err := time.Parse(time.RFC3339Nano, timeVal.(string)) 122 | if err == nil { 123 | // we got a valid timestamp. Override the event's timestamp and remove the 124 | // field from data so we don't get it reported twice 125 | ev.Timestamp = ts 126 | delete(data, "timestamp") 127 | } else { 128 | ev.AddField("timestamp problem", fmt.Sprintf("problem parsing:%s", err.Error())) 129 | } 130 | } else { 131 | ev.AddField("timestamp problem", "missing timestamp") 132 | } 133 | 134 | // Add all the fields in the JSON blob to the event 135 | ev.Add(data) 136 | 137 | // Sending is handled by the defer. 138 | } 139 | -------------------------------------------------------------------------------- /examples/webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . ./ 6 | 7 | RUN go mod download 8 | RUN go build -o ./shoutr 9 | 10 | RUN apk add --update --no-cache ca-certificates 11 | ENTRYPOINT ["./shoutr"] 12 | -------------------------------------------------------------------------------- /examples/webapp/README.md: -------------------------------------------------------------------------------- 1 | ## golang-webapp 2 | 3 | Shoutr is an example Golang web application. You can register for accounts, sign 4 | in and shout your opinions on the Internet. It has two tiers: A Golang web app 5 | and a MySQL database. 6 | 7 | ## Install 8 | 9 | Clone the repository. 10 | 11 | Create database in MySQL. 12 | 13 | ``` 14 | $ mysql -uroot -e 'create database shoutr;' 15 | ``` 16 | 17 | ## Run app 18 | 19 | ``` 20 | $ export HONEYCOMB_API_KEY= 21 | $ go run . 22 | ``` 23 | 24 | ## (Alternatively) Run in Docker 25 | 26 | The whole webapp can be run in Docker (Compose). 27 | 28 | Set your [Honeycomb API key](https://ui.honeycomb.io/account) to 29 | `HONEYCOMB_API_KEY`, or edit the `docker-compose.yml`. The `shoutr` database in 30 | MySQL will be created automatically. 31 | 32 | ``` 33 | $ export HONEYCOMB_API_KEY= 34 | ``` 35 | 36 | Then: 37 | 38 | ``` 39 | $ docker-compose build && docker-compose up 40 | ``` 41 | 42 | ## Event Fields 43 | 44 | | **Name** | **Description** | **Example Value** | 45 | | --- | --- | --- | 46 | | `flash.value` | Contents of the rendered flash message | `Your shout is too long!` | 47 | | `request.content_length`| Length of the content (in bytes) of the sent HTTP request | `952` | 48 | | `request.host` | Host the request was sent to | `localhost` | 49 | | `request.method` | HTTP method | `POST` | 50 | | `request.path` | Path of the request | `/shout` | 51 | | `request.proto` | HTTP protocol version | `HTTP/1.1` | 52 | | `request.remote_addr` | The IP and port that answered the request | `172.18.0.1:40484` | 53 | | `request.user_agent`| User agent | `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36` | 54 | | `response.status_code` | Status code written back to user | `200` | 55 | | `runtime.memory_inuse` | Amount of memory in use (in bytes) by the whole process | `4,971,776` | 56 | | `runtime.num_goroutines` | Number of goroutines in the process | `7` | 57 | | `shout.content` | Content of the user's comment | `Hello world!` | 58 | | `shout.content_length` | Length (in characters) of the user's comment | `80` | 59 | | `system.hostname` | System hostname | `1ba87a98788c` | 60 | | `timers.total_time_ms` | The total amount of time the request took to serve | `180` | 61 | | `timers.mysql_insert_user_ms` | The time the `INSERT INTO users` query took | `50` | 62 | | `user.id`| User ID | `2` | 63 | -------------------------------------------------------------------------------- /examples/webapp/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/jmoiron/sqlx" 10 | ) 11 | 12 | var ( 13 | maxConnectRetries = 10 14 | db *sqlx.DB 15 | ) 16 | 17 | func init() { 18 | var err error 19 | dbUser := "root" 20 | dbPass := "" 21 | dbName := "shoutr" 22 | dbHost := os.Getenv("DB_HOST") 23 | if dbHost == "" { 24 | dbHost = "localhost" 25 | } 26 | 27 | for i := 0; i < maxConnectRetries; i++ { 28 | db, err = sqlx.Connect("mysql", fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", dbUser, dbPass, dbHost, dbName)) 29 | if err != nil { 30 | log.Print("Error connecting to database: ", err) 31 | } else { 32 | break 33 | } 34 | if i == maxConnectRetries-1 { 35 | panic("Couldn't connect to DB") 36 | } 37 | time.Sleep(1 * time.Second) 38 | } 39 | 40 | log.Print("Bootstrapping database...") 41 | 42 | _, err = db.Exec(` 43 | CREATE TABLE IF NOT EXISTS users ( 44 | id INT NOT NULL AUTO_INCREMENT, 45 | first_name VARCHAR(64) NOT NULL, 46 | last_name VARCHAR(64) NOT NULL, 47 | username VARCHAR(64) NOT NULL, 48 | email VARCHAR(64), 49 | PRIMARY KEY (id), 50 | UNIQUE KEY (username) 51 | );`) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | _, err = db.Exec(` 57 | CREATE TABLE IF NOT EXISTS shouts ( 58 | id INT NOT NULL AUTO_INCREMENT, 59 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 60 | user_id INT, 61 | content VARCHAR(140) NOT NULL, 62 | PRIMARY KEY (id) 63 | ); 64 | `) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/webapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - "8888:8888" 8 | networks: 9 | - main 10 | depends_on: 11 | - db 12 | environment: 13 | DB_HOST: db 14 | HONEYCOMB_API_KEY: 15 | 16 | db: 17 | image: mysql 18 | networks: 19 | - main 20 | volumes: 21 | - example-golang-webapp:/var/lib/mysql 22 | environment: 23 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" # value is arbitrary 24 | MYSQL_DATABASE: "shoutr" # create DB shoutr automatically 25 | ENV: "dev" 26 | 27 | volumes: 28 | example-golang-webapp: 29 | 30 | networks: 31 | main: 32 | driver: bridge 33 | -------------------------------------------------------------------------------- /examples/webapp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honeycombio/libhoney-go/examples/webapp 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible 7 | github.com/go-sql-driver/mysql v1.6.0 8 | github.com/gorilla/mux v1.8.0 9 | github.com/gorilla/schema v1.4.1 10 | github.com/gorilla/sessions v1.2.1 11 | github.com/honeycombio/libhoney-go v1.15.8 12 | github.com/jmoiron/sqlx v1.3.5 13 | ) 14 | 15 | require ( 16 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 17 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 18 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect 19 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 // indirect 20 | github.com/gorilla/securecookie v1.1.1 // indirect 21 | github.com/klauspost/compress v1.13.6 // indirect 22 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 23 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 24 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /examples/webapp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= 2 | github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 3 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 4 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= 8 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= 9 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= 10 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= 11 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= 12 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= 13 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= 14 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= 15 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= 16 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= 17 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= 18 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= 19 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= 20 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= 21 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 22 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 23 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 24 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 25 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= 26 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 27 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 28 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 29 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 30 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 31 | github.com/honeycombio/libhoney-go v1.15.8 h1:TECEltZ48K6J4NG1JVYqmi0vCJNnHYooFor83fgKesA= 32 | github.com/honeycombio/libhoney-go v1.15.8/go.mod h1:+tnL2etFnJmVx30yqmoUkVyQjp7uRJw0a2QGu48lSyY= 33 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 34 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 35 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 36 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 37 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 38 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 39 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 40 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 45 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 47 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 48 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 49 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 50 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= 51 | gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 54 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /examples/webapp/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | "path/filepath" 8 | "reflect" 9 | "time" 10 | 11 | validation "github.com/go-ozzo/ozzo-validation" 12 | "github.com/go-ozzo/ozzo-validation/is" 13 | "github.com/gorilla/schema" 14 | "github.com/gorilla/sessions" 15 | libhoney "github.com/honeycombio/libhoney-go" 16 | ) 17 | 18 | var ( 19 | decoder = schema.NewDecoder() 20 | sessionName = "default" 21 | sessionStore = sessions.NewCookieStore([]byte("best-secret-in-the-world")) 22 | ) 23 | 24 | const ( 25 | templatesDir = "templates" 26 | // 140 is the proper amount of characters for a microblog. Any other 27 | // value is heresy. 28 | maxShoutLength = 140 29 | ) 30 | 31 | func hnyEventFromRequest(r *http.Request) *libhoney.Event { 32 | ev, ok := r.Context().Value(hnyContextKey).(*libhoney.Event) 33 | if !ok { 34 | // We control the way this is being put on context anyway. 35 | panic("Couldn't get libhoney event from request context") 36 | } 37 | 38 | // Every libhoney event gets annotated automatically with user ID if a 39 | // user is logged in! 40 | session, _ := sessionStore.Get(r, sessionName) 41 | userID, ok := session.Values["user_id"] 42 | if ok { 43 | ev.AddField("user.id", userID) 44 | } 45 | 46 | return ev 47 | } 48 | 49 | func addFinalErr(err *error, ev *libhoney.Event) { 50 | if *err != nil { 51 | ev.AddField("error", (*err).Error()) 52 | } 53 | } 54 | 55 | func signupHandlerGet(w http.ResponseWriter, r *http.Request) { 56 | var err error 57 | ev := hnyEventFromRequest(r) 58 | defer addFinalErr(&err, ev) 59 | 60 | tmpl := template.Must(template. 61 | ParseFiles( 62 | filepath.Join(templatesDir, "base.html"), 63 | filepath.Join(templatesDir, "signup.html"), 64 | )) 65 | tmplData := struct { 66 | ErrorMessage string 67 | }{} 68 | if err = tmpl.Execute(w, tmplData); err != nil { 69 | log.Print(err) 70 | } 71 | } 72 | 73 | func signupHandlerPost(w http.ResponseWriter, r *http.Request) { 74 | var err error 75 | ev := hnyEventFromRequest(r) 76 | defer addFinalErr(&err, ev) 77 | 78 | tmpl := template.Must(template. 79 | ParseFiles( 80 | filepath.Join(templatesDir, "base.html"), 81 | filepath.Join(templatesDir, "signup.html"), 82 | )) 83 | tmplData := struct { 84 | ErrorMessage string 85 | }{} 86 | if err = r.ParseForm(); err != nil { 87 | log.Print(err) 88 | w.WriteHeader(http.StatusBadRequest) 89 | tmplData.ErrorMessage = "Couldn't parse form" 90 | if err = tmpl.Execute(w, tmplData); err != nil { 91 | log.Print(err) 92 | } 93 | return 94 | } 95 | 96 | var user User 97 | if err = decoder.Decode(&user, r.PostForm); err != nil { 98 | log.Print(err) 99 | w.WriteHeader(http.StatusBadRequest) 100 | 101 | tmplData.ErrorMessage = "An error occurred" 102 | if err = tmpl.Execute(w, tmplData); err != nil { 103 | log.Print(err) 104 | } 105 | return 106 | } 107 | 108 | ev.AddField("user.email", user.Email) 109 | 110 | if err = validation.ValidateStruct(&user, 111 | validation.Field(&user.FirstName, is.Alpha), 112 | validation.Field(&user.LastName, is.Alpha), 113 | validation.Field(&user.Username, is.Alphanumeric), 114 | validation.Field(&user.Email, is.Email), 115 | ); err != nil { 116 | log.Print(err) 117 | tmplData.ErrorMessage = "Validation failure" 118 | if err = tmpl.Execute(w, tmplData); err != nil { 119 | log.Print(err) 120 | } 121 | return 122 | } 123 | 124 | queryStart := time.Now() 125 | 126 | res, err := db.Exec(`INSERT INTO users 127 | (first_name, last_name, username, email) 128 | VALUES 129 | (?, ?, ?, ?) 130 | `, user.FirstName, user.LastName, user.Username, user.Email) 131 | if err != nil { 132 | log.Print(err) 133 | http.Error(w, "Something went wrong", http.StatusInternalServerError) 134 | return 135 | } 136 | 137 | ev.AddField("timers.db.users_insert_ms", time.Since(queryStart)/time.Millisecond) 138 | 139 | session, _ := sessionStore.Get(r, sessionName) 140 | userID, err := res.LastInsertId() 141 | if err != nil { 142 | log.Print(err) 143 | http.Error(w, "Something went wrong", http.StatusInternalServerError) 144 | return 145 | } 146 | session.Values["user_id"] = int(userID) 147 | 148 | ev.AddField("user.id", int(userID)) 149 | 150 | session.Save(r, w) 151 | http.Redirect(w, r, "/", http.StatusSeeOther) 152 | } 153 | 154 | func loginHandlerGet(w http.ResponseWriter, r *http.Request) { 155 | var err error 156 | ev := hnyEventFromRequest(r) 157 | defer addFinalErr(&err, ev) 158 | 159 | tmpl := template.Must(template. 160 | ParseFiles( 161 | filepath.Join(templatesDir, "base.html"), 162 | filepath.Join(templatesDir, "login.html"), 163 | )) 164 | tmplData := struct { 165 | ErrorMessage string 166 | }{} 167 | 168 | if err = tmpl.Execute(w, tmplData); err != nil { 169 | log.Print(err) 170 | } 171 | } 172 | 173 | func loginHandlerPost(w http.ResponseWriter, r *http.Request) { 174 | var err error 175 | ev := hnyEventFromRequest(r) 176 | defer addFinalErr(&err, ev) 177 | 178 | tmpl := template.Must(template. 179 | ParseFiles( 180 | filepath.Join(templatesDir, "base.html"), 181 | filepath.Join(templatesDir, "login.html"), 182 | )) 183 | tmplData := struct { 184 | ErrorMessage string 185 | }{} 186 | 187 | if r.Method == "GET" { 188 | if err = tmpl.Execute(w, tmplData); err != nil { 189 | log.Print(err) 190 | } 191 | return 192 | } 193 | 194 | user := User{} 195 | 196 | if err = r.ParseForm(); err != nil { 197 | log.Print(err) 198 | w.WriteHeader(http.StatusBadRequest) 199 | tmplData.ErrorMessage = "Couldn't parse form properly" 200 | if err = tmpl.Execute(w, tmplData); err != nil { 201 | log.Print(err) 202 | } 203 | return 204 | } 205 | 206 | username := r.FormValue("username") 207 | 208 | if err = db.Get(&user, `SELECT id FROM users WHERE username = ?`, username); err != nil { 209 | w.WriteHeader(http.StatusBadRequest) 210 | tmplData.ErrorMessage = "Couldn't log you in." 211 | if err = tmpl.Execute(w, tmplData); err != nil { 212 | log.Print(err) 213 | } 214 | return 215 | } 216 | 217 | session, _ := sessionStore.Get(r, sessionName) 218 | session.Values["user_id"] = user.ID 219 | session.Save(r, w) 220 | 221 | http.Redirect(w, r, "/", http.StatusSeeOther) 222 | } 223 | 224 | func shoutHandler(w http.ResponseWriter, r *http.Request) { 225 | var err error 226 | ev := hnyEventFromRequest(r) 227 | defer addFinalErr(&err, ev) 228 | 229 | session, _ := sessionStore.Get(r, sessionName) 230 | userID := session.Values["user_id"] 231 | if err = r.ParseForm(); err != nil { 232 | log.Print(err) 233 | http.Redirect(w, r, "/", http.StatusSeeOther) 234 | return 235 | } 236 | 237 | content := r.FormValue("content") 238 | ev.AddField("shout.content_length", len(content)) 239 | 240 | if len(content) > maxShoutLength { 241 | session, _ := sessionStore.Get(r, sessionName) 242 | session.AddFlash("Your shout is too long!") 243 | session.Save(r, w) 244 | ev.AddField("shout.content", content[:140]) 245 | http.Redirect(w, r, "/", http.StatusSeeOther) 246 | return 247 | } 248 | 249 | ev.AddField("shout.content", content) 250 | 251 | if _, err = db.Exec(`INSERT INTO shouts (content, user_id) VALUES (?, ?)`, content, userID); err != nil { 252 | log.Print(err) 253 | http.Redirect(w, r, "/", http.StatusSeeOther) 254 | return 255 | } 256 | 257 | http.Redirect(w, r, "/", http.StatusSeeOther) 258 | } 259 | 260 | func logoutHandler(w http.ResponseWriter, r *http.Request) { 261 | var err error 262 | ev := hnyEventFromRequest(r) 263 | defer addFinalErr(&err, ev) 264 | session, _ := sessionStore.Get(r, sessionName) 265 | delete(session.Values, "user_id") 266 | session.Save(r, w) 267 | http.Redirect(w, r, "/", http.StatusSeeOther) 268 | } 269 | 270 | func mainHandler(w http.ResponseWriter, r *http.Request) { 271 | var err error 272 | ev := hnyEventFromRequest(r) 273 | defer addFinalErr(&err, ev) 274 | tmpl := template.Must(template.ParseFiles( 275 | filepath.Join(templatesDir, "base.html"), 276 | filepath.Join(templatesDir, "home.html"), 277 | )) 278 | session, _ := sessionStore.Get(r, sessionName) 279 | tmplData := struct { 280 | User User 281 | Shouts []RenderedShout 282 | ErrorMessage string 283 | }{} 284 | 285 | flashes := session.Flashes() 286 | if len(flashes) == 1 { 287 | flash, ok := flashes[0].(string) 288 | if !ok { 289 | ev.AddField("flash.err", "Flash didn't assert to type string, got "+reflect.TypeOf(flash).String()) 290 | } else { 291 | tmplData.ErrorMessage = flash 292 | ev.AddField("flash.value", flash) 293 | } 294 | session.Save(r, w) 295 | } 296 | 297 | // Not logged in 298 | if userID, ok := session.Values["user_id"]; !ok { 299 | if err = tmpl.Execute(w, tmplData); err != nil { 300 | log.Print(err) 301 | } 302 | return 303 | } else { 304 | if err = db.Get(&tmplData.User, `SELECT * FROM users WHERE id = ?`, userID); err != nil { 305 | log.Print(err) 306 | http.Error(w, "Something went wrong", http.StatusInternalServerError) 307 | return 308 | } 309 | 310 | if err = db.Select(&tmplData.Shouts, ` 311 | SELECT users.first_name, users.last_name, users.username, shouts.content, shouts.created_at 312 | FROM shouts 313 | INNER JOIN users 314 | ON shouts.user_id = users.id 315 | ORDER BY created_at DESC 316 | `); err != nil { 317 | log.Print(err) 318 | http.Error(w, "Something went wrong", http.StatusInternalServerError) 319 | return 320 | } 321 | 322 | if err = tmpl.Execute(w, tmplData); err != nil { 323 | log.Print(err) 324 | } 325 | return 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /examples/webapp/honeycomb_helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "time" 11 | 12 | libhoney "github.com/honeycombio/libhoney-go" 13 | ) 14 | 15 | var ( 16 | hostname string 17 | hnyDatasetName = "examples.golang-webapp" 18 | hnyContextKey = "honeycombEvent" 19 | ) 20 | 21 | func init() { 22 | hcConfig := libhoney.Config{ 23 | APIKey: os.Getenv("HONEYCOMB_API_KEY"), 24 | Dataset: hnyDatasetName, 25 | } 26 | 27 | if err := libhoney.Init(hcConfig); err != nil { 28 | log.Print(err) 29 | os.Exit(1) 30 | } 31 | 32 | if hnyTeam, err := libhoney.VerifyAPIKey(hcConfig); err != nil { 33 | log.Print(err) 34 | log.Print("Please make sure the HONEYCOMB_API_KEY environment variable is set.") 35 | os.Exit(1) 36 | } else { 37 | log.Print(fmt.Sprintf("Sending Honeycomb events to the %q dataset on %q team", hnyDatasetName, hnyTeam)) 38 | } 39 | 40 | // Initialize fields that every sent event will have. 41 | 42 | // Getting hostname on every event can be very useful if, e.g., only a 43 | // particular host or set of hosts are the source of an issue. 44 | if hostname, err := os.Hostname(); err == nil { 45 | libhoney.AddField("system.hostname", hostname) 46 | } 47 | libhoney.AddDynamicField("runtime.num_goroutines", func() interface{} { 48 | return runtime.NumGoroutine() 49 | }) 50 | libhoney.AddDynamicField("runtime.memory_inuse", func() interface{} { 51 | // This will ensure that every event includes information about 52 | // the memory usage of the process at the time the event was 53 | // sent. 54 | var mem runtime.MemStats 55 | runtime.ReadMemStats(&mem) 56 | return mem.Alloc 57 | }) 58 | } 59 | 60 | type HoneyResponseWriter struct { 61 | *libhoney.Event 62 | http.ResponseWriter 63 | StatusCode int 64 | } 65 | 66 | func (hrw *HoneyResponseWriter) WriteHeader(status int) { 67 | // Mark this down for adding to the libhoney event later. 68 | hrw.StatusCode = status 69 | hrw.ResponseWriter.WriteHeader(status) 70 | } 71 | 72 | func addRequestProps(req *http.Request, ev *libhoney.Event) { 73 | // Add a variety of details about the HTTP request, such as user agent 74 | // and method, to any created libhoney event. 75 | ev.AddField("request.method", req.Method) 76 | ev.AddField("request.path", req.URL.Path) 77 | ev.AddField("request.host", req.URL.Host) 78 | ev.AddField("request.proto", req.Proto) 79 | ev.AddField("request.content_length", req.ContentLength) 80 | ev.AddField("request.remote_addr", req.RemoteAddr) 81 | ev.AddField("request.user_agent", req.UserAgent()) 82 | } 83 | 84 | // HoneycombMiddleware will wrap our HTTP handle funcs to automatically 85 | // generate an event-per-request and set properties on them. 86 | func HoneycombMiddleware(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 87 | return func(w http.ResponseWriter, r *http.Request) { 88 | // We'll time each HTTP request and add that as a property to 89 | // the sent Honeycomb event, so start the timer for that. 90 | startHandler := time.Now() 91 | ev := libhoney.NewEvent() 92 | 93 | defer func() { 94 | if err := ev.Send(); err != nil { 95 | log.Print("Error sending libhoney event: ", err) 96 | } 97 | }() 98 | 99 | addRequestProps(r, ev) 100 | 101 | // Create a context where we will store the libhoney event. We 102 | // will add default values to this event for every HTTP 103 | // request, and the user can access it to add their own 104 | // (powerful, custom) fields. 105 | ctx := context.WithValue(r.Context(), hnyContextKey, ev) 106 | reqWithContext := r.WithContext(ctx) 107 | 108 | honeyResponseWriter := &HoneyResponseWriter{ 109 | Event: ev, 110 | ResponseWriter: w, 111 | StatusCode: 200, 112 | } 113 | 114 | fn(honeyResponseWriter, reqWithContext) 115 | 116 | ev.AddField("response.status_code", honeyResponseWriter.StatusCode) 117 | handlerDuration := time.Since(startHandler) 118 | ev.AddField("timers.total_time_ms", handlerDuration/time.Millisecond) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /examples/webapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | func main() { 11 | r := mux.NewRouter() 12 | r.HandleFunc("/", HoneycombMiddleware(mainHandler)).Methods("GET") 13 | 14 | r.HandleFunc("/signup", HoneycombMiddleware(signupHandlerGet)).Methods("GET") 15 | r.HandleFunc("/signup", HoneycombMiddleware(signupHandlerPost)).Methods("POST") 16 | 17 | r.HandleFunc("/login", HoneycombMiddleware(loginHandlerGet)).Methods("GET") 18 | r.HandleFunc("/login", HoneycombMiddleware(loginHandlerPost)).Methods("POST") 19 | 20 | r.HandleFunc("/logout", HoneycombMiddleware(logoutHandler)).Methods("POST") 21 | r.HandleFunc("/shout", HoneycombMiddleware(shoutHandler)).Methods("POST") 22 | 23 | log.Print("Serving app on localhost:8888 ....") 24 | log.Fatal(http.ListenAndServe(":8888", r)) 25 | } 26 | -------------------------------------------------------------------------------- /examples/webapp/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shoutr 5 | 6 | 7 | {{template "body" .}} 8 | 9 | -------------------------------------------------------------------------------- /examples/webapp/templates/home.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 | {{if ne .User.ID 0}} 3 |
4 | 5 |
6 |

7 | Welcome {{.User.FirstName}}. 8 |

9 |

Get shoutin':

10 | {{if .ErrorMessage}} 11 |

{{.ErrorMessage}}

12 | {{end}} 13 |
14 | 16 | 17 |
18 | {{if .Shouts}} 19 | {{range $shout := .Shouts}} 20 |
21 |
{{$shout.FirstName}} {{$shout.LastName}} @{{$shout.Username}} | {{$shout.CreatedAt.Time.Format "Jan 02, 2006 15:04:05"}}
22 | {{$shout.Content}} 23 |
24 | {{end}} 25 | {{else}} 26 | Once you or others do some shouting, the shouts will appear here. 27 | {{end}} 28 | {{else}} 29 |

Shoutr

30 |

Shoutr is a new kind of web 3.0 social media platform.

31 |

With Shoutr, you can shout your opinions on the Internet!

32 |

Sign up for an account today to access the content in our walled garden.

33 | Sign Up | 34 | Login 35 | {{end}} 36 | {{end}} 37 | -------------------------------------------------------------------------------- /examples/webapp/templates/login.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 | {{if .ErrorMessage}} 3 |

{{.ErrorMessage}}

4 | {{end}} 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 | {{end}} 16 | -------------------------------------------------------------------------------- /examples/webapp/templates/signup.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |

Sign Up

3 | {{if .ErrorMessage}} 4 |

{{.ErrorMessage}}

5 | {{end}} 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | {{end}} 32 | -------------------------------------------------------------------------------- /examples/webapp/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-sql-driver/mysql" 4 | 5 | type User struct { 6 | ID int `db:"id"` 7 | FirstName string `db:"first_name" schema:"first_name"` 8 | LastName string `db:"last_name" schema:"last_name"` 9 | Username string `db:"username" schema:"username"` 10 | Email string `db:"email" schema:"email"` 11 | } 12 | 13 | type Shout struct { 14 | ID int `db:"int"` 15 | UserID int `db:"user_id"` 16 | Content string `db:"content"` 17 | CreatedAt mysql.NullTime `db:"created_at"` 18 | } 19 | 20 | // Used to read the data from a MySQL JOIN query and render it on the 21 | // front-end. 22 | type RenderedShout struct { 23 | FirstName string `db:"first_name"` 24 | LastName string `db:"last_name" schema:"last_name"` 25 | Username string `db:"username" schema:"username"` 26 | Content string `db:"content"` 27 | CreatedAt mysql.NullTime `db:"created_at"` 28 | } 29 | -------------------------------------------------------------------------------- /examples/wiki-manual-tracing/README.md: -------------------------------------------------------------------------------- 1 | # golang-wiki-tracing 2 | 3 | This example illustrates a simple wiki application instrumented with the **bare minimum necessary** to utilize Honeycomb's tracing functionality, using the finished code from the ["Writing Web Applications" Golang tutorial](https://go.dev/doc/articles/wiki/). 4 | 5 | ## What We'll Do in This Example 6 | 7 | We'll instrument a simple application for tracing by following a few general steps: 8 | 9 | 1. Set a top-level `trace.trace_id` at the origin of the request and set it on the request context. Generate a root span indicated by omitting a `trace.parent_id` field. 10 | 2. To represent a unit of work within a trace as a span, add code to generate a span ID and capture the start time. At the **call site** of the unit of work, pass down a new request context with the newly-generated span ID as the `trace.parent_id`. Upon work completion, send the span with a calculated `duration_ms`. 11 | 3. Rinse and repeat. 12 | 13 | **Note**: Sound complicated? [OpenTelemetry for Go](https://docs.honeycomb.io/getting-data-in/opentelemetry/go/) handles all of this propagation magic for you :) 14 | 15 | ## Usage 16 | 17 | You can [find your API key](https://docs.honeycomb.io/getting-data-in/api-keys/#find-api-keys) in your Environment Settings. 18 | If you do not have an API key yet, sign up for a [free Honeycomb account](https://ui.honeycomb.io/signup). 19 | 20 | Once you have your API key, run: 21 | 22 | ```bash 23 | $ HONEYCOMB_API_KEY=foobarbaz go run wiki.go 24 | ``` 25 | 26 | And load [`http://localhost:8080/view/MyFirstWikiPage`](http://localhost:8080/view/MyFirstWikiPage) to create (then view) your first wiki page. 27 | 28 | Methods within the simple wiki application have been instrumented with tracing-like calls, with context and tracing identifiers propagated via a `context.Context`. 29 | 30 | ## Tips for Instrumenting Your Own Service 31 | 32 | - For a given span (e.g. `"loadPage"`), remember that the span definition lives in its parent, and the instrumentation is around the **call site** of `loadPage`: 33 | ```go 34 | loadPageID := fmt.Sprintf("%x", rand.Int63()) 35 | start := time.Now() 36 | // Make sure to pass newContextWithParentID() -- with loadPageID as the 37 | // parent ID -- to loadPage in order to propagate span inheritance correctly 38 | loadPage(newContextWithParentID(r.Context(), loadPageID), title) 39 | sendSpan("loadPage", loadPageID, start, r.Context()) 40 | ``` 41 | - If emitting Honeycomb events or structured logs, make sure that the **start** time gets used as the canonical timestamp, not the time at event emission. 42 | - Remember, the root span should **not** have a `trace.parent_id`. 43 | - Don't forget to add some metadata of your own! It's helpful to identify metadata in the surrounding code that might be interesting when debugging your application. 44 | - Check out our [OpenTelemetry for Go](https://docs.honeycomb.io/getting-data-in/opentelemetry/go/) to get this context propagation for free! 45 | 46 | ## A Note on Code Style 47 | 48 | The purpose of this example is to illustrate the **bare minimum necessary** to propagate and set identifiers to enable tracing on an application for consumption by Honeycomb, illustrating the steps described in the top section of this README. We prioritized legibility over style and intentionally resisted refactoring that would sacrifice clarity. :) 49 | -------------------------------------------------------------------------------- /examples/wiki-manual-tracing/edit.html: -------------------------------------------------------------------------------- 1 |

Editing {{.Title}}

2 | 3 |
4 |
5 |
6 |
7 | -------------------------------------------------------------------------------- /examples/wiki-manual-tracing/view.html: -------------------------------------------------------------------------------- 1 |

{{.Title}}

2 | 3 |

[edit]

4 | 5 |
{{printf "%s" .Body}}
6 | -------------------------------------------------------------------------------- /examples/wiki-manual-tracing/wiki.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "html/template" 11 | "log" 12 | "math/rand" 13 | "net/http" 14 | "os" 15 | "regexp" 16 | "time" 17 | 18 | "github.com/honeycombio/libhoney-go" 19 | ) 20 | 21 | type key int 22 | 23 | const ( 24 | requestIDKey key = 0 25 | parentIDKey key = 1 26 | ) 27 | 28 | // Define some wrappers to propagate "trace" or "request" identifiers down the 29 | // call stack, to unify the various spans within a trace. 30 | func newContextWithRequestID(ctx context.Context, req *http.Request) context.Context { 31 | reqID := req.Header.Get("X-Request-ID") 32 | if reqID == "" { 33 | reqID = newID() 34 | } 35 | return context.WithValue(ctx, requestIDKey, reqID) 36 | } 37 | 38 | func requestIDFromContext(ctx context.Context) string { 39 | return ctx.Value(requestIDKey).(string) 40 | } 41 | 42 | // Define some wrappers to propagate "parent" identifiers down the call stack, 43 | // for defining relationships between spans within a trace. 44 | func newContextWithParentID(ctx context.Context, id string) context.Context { 45 | return context.WithValue(ctx, parentIDKey, id) 46 | } 47 | 48 | func parentIDFromContext(ctx context.Context) string { 49 | if id, ok := ctx.Value(parentIDKey).(string); ok { 50 | return id 51 | } 52 | return "" 53 | } 54 | 55 | // Generate a new unique identifier for our spans and traces. This can be any 56 | // unique string -- Zipkin uses hex-encoded base64 ints, as we do here; other 57 | // folks may prefer to use their UUID library of choice. 58 | func newID() string { 59 | return fmt.Sprintf("%x", rand.Int63()) 60 | } 61 | 62 | // Page represents the data (and some basic operations) on a wiki page. 63 | // 64 | // While the tracing instrumentation in this example is constrained to the 65 | // handlers, we could just as easily propagate context down directly into this 66 | // class if needed. 67 | type Page struct { 68 | Title string 69 | Body []byte 70 | } 71 | 72 | func (p *Page) save(ctx context.Context) error { 73 | filename := p.Title + ".txt" 74 | return os.WriteFile(filename, p.Body, 0600) 75 | } 76 | 77 | func loadPage(ctx context.Context, title string) (*Page, error) { 78 | filename := title + ".txt" 79 | id := newID() 80 | start := time.Now() 81 | body, err := os.ReadFile(filename) 82 | sendSpan("os.ReadFile", id, start, ctx, map[string]interface{}{"title": title, "bodylen": len(body), "error": err}) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return &Page{Title: title, Body: body}, nil 87 | } 88 | 89 | // Our "View" handler. Tries to load a page from disk and render it. Falls back 90 | // to the Edit handler if the page does not yet exist. 91 | func viewHandler(w http.ResponseWriter, r *http.Request, title string) { 92 | loadPageID := newID() 93 | loadPageStart := time.Now() 94 | p, err := loadPage(newContextWithParentID(r.Context(), loadPageID), title) 95 | sendSpan("loadPage", loadPageID, loadPageStart, r.Context(), map[string]interface{}{"title": title, "error": err}) 96 | if err != nil { 97 | http.Redirect(w, r, "/edit/"+title, http.StatusFound) 98 | return 99 | } 100 | renderID := newID() 101 | renderStart := time.Now() 102 | renderTemplate(newContextWithParentID(r.Context(), renderID), w, "view", p) 103 | sendSpan("renderTemplate", renderID, renderStart, r.Context(), map[string]interface{}{"template": "view"}) 104 | } 105 | 106 | // Our "Edit" handler. Tries to load a page from disk to seed the edit screen, 107 | // then renders a form to allow the user to define the content of the requested 108 | // wiki page. 109 | func editHandler(w http.ResponseWriter, r *http.Request, title string) { 110 | loadPageID := newID() 111 | loadPageStart := time.Now() 112 | p, err := loadPage(newContextWithParentID(r.Context(), loadPageID), title) 113 | sendSpan("loadPage", loadPageID, loadPageStart, r.Context(), map[string]interface{}{"title": title, "error": err}) 114 | if err != nil { 115 | p = &Page{Title: title} 116 | } 117 | renderID := newID() 118 | renderStart := time.Now() 119 | renderTemplate(newContextWithParentID(r.Context(), renderID), w, "edit", p) 120 | sendSpan("renderTemplate", renderID, renderStart, r.Context(), map[string]interface{}{"template": "edit"}) 121 | } 122 | 123 | // Our "Save" handler simply persists a page to disk. 124 | func saveHandler(w http.ResponseWriter, r *http.Request, title string) { 125 | body := r.FormValue("body") 126 | p := &Page{Title: title, Body: []byte(body)} 127 | id := newID() 128 | start := time.Now() 129 | err := p.save(newContextWithParentID(r.Context(), id)) 130 | sendSpan("os.WriteFile", id, start, r.Context(), map[string]interface{}{"title": title, "bodylen": len(body), "error": err}) 131 | if err != nil { 132 | http.Error(w, err.Error(), http.StatusInternalServerError) 133 | return 134 | } 135 | http.Redirect(w, r, "/view/"+title, http.StatusFound) 136 | } 137 | 138 | var templates = template.Must(template.ParseFiles("edit.html", "view.html")) 139 | 140 | func renderTemplate(ctx context.Context, w http.ResponseWriter, tmpl string, p *Page) { 141 | err := templates.ExecuteTemplate(w, tmpl+".html", p) 142 | if err != nil { 143 | http.Error(w, err.Error(), http.StatusInternalServerError) 144 | } 145 | } 146 | 147 | var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$") 148 | 149 | // This middleware treats each HTTP request as a distinct "trace." Each trace 150 | // begins with a top-level ("root") span indicating that the HTTP request has 151 | // begun. 152 | func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { 153 | return func(w http.ResponseWriter, r *http.Request) { 154 | m := validPath.FindStringSubmatch(r.URL.Path) 155 | if m == nil { 156 | http.NotFound(w, r) 157 | return 158 | } 159 | start := time.Now() 160 | id := newID() 161 | ctx := newContextWithRequestID(r.Context(), r) 162 | fn(w, r.WithContext(newContextWithParentID(ctx, id)), m[2]) 163 | sendSpan(m[1], id, start, ctx, nil) 164 | } 165 | } 166 | 167 | // This wrapper takes a span name and some optional metadata, then emits a 168 | // "span" to Honeycomb as part of the trace begun in the HTTP middleware. 169 | func sendSpan(name, id string, start time.Time, ctx context.Context, metadata map[string]interface{}) { 170 | if metadata == nil { 171 | metadata = map[string]interface{}{} 172 | } 173 | // Field keys to trigger Honeycomb's tracing functionality on this dataset 174 | // defined at: 175 | // https://docs.honeycomb.io/getting-data-in/tracing/send-trace-data/ 176 | metadata["name"] = name 177 | metadata["trace.span_id"] = id 178 | metadata["trace.trace_id"] = requestIDFromContext(ctx) 179 | metadata["service.name"] = "wiki" 180 | metadata["duration_ms"] = float64(time.Since(start)) / float64(time.Millisecond) 181 | if parentID := parentIDFromContext(ctx); parentID != "" { 182 | metadata["trace.parent_id"] = parentID 183 | } 184 | 185 | ev := libhoney.NewEvent() 186 | // NOTE: Don't forget to set the timestamp to `start` -- because spans are 187 | // emitted at the *end* of their execution, we want to be doubly sure that 188 | // our manually-emitted events are timestamped with the time that the work 189 | // (the span's actual execution) really begun. 190 | ev.Timestamp = start 191 | ev.Add(metadata) 192 | ev.Send() 193 | } 194 | 195 | // Let's go! 196 | func main() { 197 | libhoney.Init(libhoney.Config{ 198 | APIKey: os.Getenv("HONEYCOMB_API_KEY"), 199 | Dataset: "golang-wiki-tracing-example", 200 | }) 201 | defer libhoney.Close() 202 | 203 | http.HandleFunc("/view/", makeHandler(viewHandler)) 204 | http.HandleFunc("/edit/", makeHandler(editHandler)) 205 | http.HandleFunc("/save/", makeHandler(saveHandler)) 206 | // Redirect to a default wiki page. 207 | http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 208 | if req.URL.Path != "/" { 209 | http.NotFound(w, req) 210 | return 211 | } 212 | h := http.RedirectHandler("/view/Index", http.StatusTemporaryRedirect) 213 | h.ServeHTTP(w, req) 214 | }) 215 | 216 | log.Print("Serving app on localhost:8080 ....") 217 | log.Fatal(http.ListenAndServe(":8080", nil)) 218 | } 219 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honeycombio/libhoney-go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/DataDog/zstd v1.5.7 7 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 8 | github.com/klauspost/compress v1.17.11 9 | github.com/stretchr/testify v1.10.0 10 | github.com/vmihailenco/msgpack/v5 v5.4.1 11 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 17 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect 18 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect 19 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect 20 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= 2 | github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= 6 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= 7 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= 8 | github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= 9 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= 10 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= 11 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= 12 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= 13 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= 14 | github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= 15 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= 16 | github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= 17 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 18 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 24 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 25 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 26 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 27 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= 28 | gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "path/filepath" 7 | "reflect" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func testOK(t testing.TB, err error) { 15 | if err != nil { 16 | _, file, line, _ := runtime.Caller(1) 17 | t.Fatalf("%s:%d: unexpected error: %s", filepath.Base(file), line, err.Error()) 18 | } 19 | } 20 | func testErr(t testing.TB, err error) { 21 | if err == nil { 22 | _, file, line, _ := runtime.Caller(1) 23 | t.Fatalf("%s:%d: error expected!", filepath.Base(file), line) 24 | } 25 | } 26 | 27 | func testEquals(t testing.TB, actual, expected interface{}, msg ...string) { 28 | if !reflect.DeepEqual(actual, expected) { 29 | testCommonErr(t, actual, expected, msg) 30 | } 31 | } 32 | 33 | func testNotEquals(t testing.TB, actual, expected interface{}, msg ...string) { 34 | if reflect.DeepEqual(actual, expected) { 35 | testCommonErr(t, actual, expected, msg) 36 | } 37 | } 38 | 39 | func testCommonErr(t testing.TB, actual, expected interface{}, msg []string) { 40 | message := strings.Join(msg, ", ") 41 | _, file, line, _ := runtime.Caller(2) 42 | 43 | t.Errorf( 44 | "%s:%d: %s -- actual(%T): %v, expected(%T): %v", 45 | filepath.Base(file), 46 | line, 47 | message, 48 | testDeref(actual), 49 | testDeref(actual), 50 | testDeref(expected), 51 | testDeref(expected), 52 | ) 53 | } 54 | 55 | func testGetResponse(t testing.TB, ch chan Response) Response { 56 | _, file, line, _ := runtime.Caller(2) 57 | var resp Response 58 | select { 59 | case resp = <-ch: 60 | case <-time.After(50 * time.Millisecond): // block on read but prevent deadlocking tests 61 | t.Errorf("%s:%d: expected response on channel and timed out waiting for it!", filepath.Base(file), line) 62 | } 63 | return resp 64 | } 65 | 66 | func testIsPlaceholderResponse(t testing.TB, actual Response, msg ...string) { 67 | if actual.StatusCode != http.StatusTeapot { 68 | message := strings.Join(msg, ", ") 69 | _, file, line, _ := runtime.Caller(1) 70 | t.Errorf( 71 | "%s:%d placeholder expected -- %s", 72 | filepath.Base(file), 73 | line, 74 | message, 75 | ) 76 | } 77 | } 78 | 79 | func testDeref(v interface{}) interface{} { 80 | switch t := v.(type) { 81 | case *string: 82 | return fmt.Sprintf("*(%v)", *t) 83 | case *int64: 84 | return fmt.Sprintf("*(%v)", *t) 85 | case *float64: 86 | return fmt.Sprintf("*(%v)", *t) 87 | case *bool: 88 | return fmt.Sprintf("*(%v)", *t) 89 | default: 90 | return v 91 | } 92 | } 93 | 94 | // for easy time manipulation during tests 95 | type fakeNower struct { 96 | iter int 97 | } 98 | 99 | // Now() supports changing/increasing the returned Now() based on the number of 100 | // times it's called in succession 101 | func (f *fakeNower) Now() time.Time { 102 | now := time.Unix(1277132645, 0).Add(time.Second * 10 * time.Duration(f.iter)) 103 | f.iter++ 104 | return now 105 | } 106 | -------------------------------------------------------------------------------- /libhoney_test.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "math/rand" 10 | "net/http" 11 | "net/http/httptest" 12 | "runtime" 13 | "strings" 14 | "sync" 15 | "testing" 16 | "time" 17 | 18 | "github.com/honeycombio/libhoney-go/transmission" 19 | "github.com/stretchr/testify/assert" 20 | 21 | statsd "gopkg.in/alexcesaro/statsd.v2" 22 | ) 23 | 24 | // because package level vars get initialized on package inclusion, subsequent 25 | // tests interact with the same variables in a way that is not like how it 26 | // would be used. This function resets things to a blank state. 27 | func resetPackageVars() { 28 | tx := &transmission.MockSender{} 29 | dc, _ = NewClient(ClientConfig{ 30 | APIKey: "twerk", 31 | Dataset: "twdds", 32 | SampleRate: 1, 33 | APIHost: "http://localhost:1234", 34 | Transmission: tx, 35 | }) 36 | sd, _ = statsd.New(statsd.Mute(true)) 37 | } 38 | 39 | func TestLibhoney(t *testing.T) { 40 | resetPackageVars() 41 | conf := Config{ 42 | WriteKey: "aoeu", 43 | Dataset: "oeui", 44 | SampleRate: 1, 45 | APIHost: "http://localhost:8081/", 46 | } 47 | err := Init(conf) 48 | testOK(t, err) 49 | testEquals(t, cap(dc.TxResponses()), 2*DefaultPendingWorkCapacity) 50 | } 51 | 52 | func TestCloseWithoutInit(t *testing.T) { 53 | // before Init() is called, tx is an unpopulated nil interface 54 | dc = &Client{} 55 | defer func() { 56 | if r := recover(); r != nil { 57 | t.Errorf("recover should not have caught anything: got %v", r) 58 | } 59 | }() 60 | Close() 61 | } 62 | 63 | func TestResponsesRace(t *testing.T) { 64 | wg := sync.WaitGroup{} 65 | wg.Add(2) 66 | go func() { 67 | Responses() 68 | wg.Done() 69 | }() 70 | go func() { 71 | Responses() 72 | wg.Done() 73 | }() 74 | 75 | wg.Wait() 76 | } 77 | 78 | func TestNewEvent(t *testing.T) { 79 | resetPackageVars() 80 | conf := Config{ 81 | WriteKey: "aoeu", 82 | Dataset: "oeui", 83 | SampleRate: 1, 84 | APIHost: "http://localhost:8081/", 85 | } 86 | Init(conf) 87 | ev := NewEvent() 88 | testEquals(t, ev.WriteKey, "aoeu") 89 | testEquals(t, ev.Dataset, "oeui") 90 | testEquals(t, ev.SampleRate, uint(1)) 91 | testEquals(t, ev.APIHost, "http://localhost:8081/") 92 | } 93 | 94 | func TestNewEventRace(t *testing.T) { 95 | resetPackageVars() 96 | conf := Config{ 97 | WriteKey: "aoeu", 98 | Dataset: "oeui", 99 | SampleRate: 1, 100 | APIHost: "http://localhost:8081/", // this will be ignored 101 | Transmission: &transmission.DiscardSender{}, 102 | } 103 | Init(conf) 104 | wg := sync.WaitGroup{} 105 | wg.Add(3) 106 | // set up a race between adding a package-level field, creating a new event, 107 | // and creating a new builder (and event from that builder). 108 | go func() { 109 | AddField("gleeble", "glooble") 110 | wg.Done() 111 | }() 112 | go func() { 113 | ev := NewEvent() 114 | ev.AddField("glarble", "glorble") 115 | ev.Send() 116 | wg.Done() 117 | }() 118 | go func() { 119 | b := NewBuilder() 120 | b.AddField("buildarble", "buildeeble") 121 | ev := b.NewEvent() 122 | ev.AddField("eveeble", "evooble") 123 | ev.Send() 124 | wg.Done() 125 | }() 126 | wg.Wait() 127 | } 128 | 129 | func TestAddField(t *testing.T) { 130 | resetPackageVars() 131 | conf := Config{ 132 | WriteKey: "aoeu", 133 | Dataset: "oeui", 134 | SampleRate: 1, 135 | APIHost: "http://localhost:8081/", 136 | } 137 | Init(conf) 138 | ev := NewEvent() 139 | ev.AddField("strVal", "bar") 140 | ev.AddField("intVal", 5) 141 | ev.AddField("floatVal", 3.123) 142 | ev.AddField("uintVal", uint(4)) 143 | ev.AddField("boolVal", true) 144 | testEquals(t, ev.data["strVal"], "bar") 145 | testEquals(t, ev.data["intVal"], 5) 146 | testEquals(t, ev.data["floatVal"], 3.123) 147 | testEquals(t, ev.data["uintVal"], uint(4)) 148 | testEquals(t, ev.data["boolVal"], true) 149 | } 150 | 151 | type Aich struct { 152 | F1 string 153 | F2 int 154 | F3 int `json:"effthree"` 155 | F4 int `json:"-"` 156 | F5 int `json:"f5,omitempty"` 157 | h1 int 158 | h2 []string 159 | P1 *int 160 | P2 *int 161 | P3 []int 162 | P4 map[string]int 163 | } 164 | 165 | func TestAddStruct(t *testing.T) { 166 | intPtr := new(int) 167 | conf := Config{} 168 | Init(conf) 169 | ev := NewEvent() 170 | r := Aich{ 171 | F1: "snth", 172 | F2: 5, 173 | F3: 6, 174 | F4: 7, 175 | h1: 9, 176 | h2: []string{"a", "b"}, 177 | P2: intPtr, 178 | } 179 | ev.Add(r) 180 | marshalled, err := json.Marshal(ev.data) 181 | assert.Nil(t, err) 182 | assert.JSONEq(t, 183 | `{ 184 | "F1": "snth", 185 | "F2": 5, 186 | "P2": 0, 187 | "effthree": 6 188 | }`, 189 | string(marshalled)) 190 | } 191 | 192 | func TestAddStructPtr(t *testing.T) { 193 | resetPackageVars() 194 | intPtr := new(int) 195 | conf := Config{} 196 | Init(conf) 197 | ev := NewEvent() 198 | r := Aich{ 199 | F1: "snth", 200 | F2: 5, 201 | F3: 6, 202 | F4: 7, 203 | F5: 8, 204 | h1: 9, 205 | h2: []string{"a", "b"}, 206 | P2: intPtr, 207 | } 208 | ev.Add(&r) 209 | 210 | marshalled, err := json.Marshal(ev.data) 211 | assert.Nil(t, err) 212 | assert.JSONEq(t, 213 | `{ 214 | "F1": "snth", 215 | "F2": 5, 216 | "P2": 0, 217 | "effthree": 6, 218 | "f5": 8 219 | }`, 220 | string(marshalled)) 221 | } 222 | 223 | type Jay struct { 224 | F1 string 225 | F2 Aich 226 | F3 struct{ A []int } 227 | F4 []string 228 | } 229 | 230 | func TestAddDeepStruct(t *testing.T) { 231 | resetPackageVars() 232 | conf := Config{} 233 | Init(conf) 234 | ev := NewEvent() 235 | r := Aich{ 236 | F1: "snth", 237 | F2: 5, 238 | F3: 6, 239 | } 240 | j := Jay{ 241 | F1: "ntdh", 242 | F2: r, 243 | F3: struct{ A []int }{[]int{2, 3}}, 244 | F4: []string{"eoeoe", "ththt"}, 245 | } 246 | err := ev.Add(j) 247 | testOK(t, err) 248 | testEquals(t, ev.data["F1"], j.F1) 249 | testEquals(t, ev.data["F2"], r) 250 | testEquals(t, ev.data["F3"], struct{ A []int }{[]int{2, 3}}) 251 | testEquals(t, ev.data["F4"], []string{"eoeoe", "ththt"}) 252 | } 253 | 254 | func TestAddSlice(t *testing.T) { 255 | resetPackageVars() 256 | conf := Config{} 257 | Init(conf) 258 | ev := NewEvent() 259 | sl := []string{"a", "b", "c"} 260 | err := ev.Add(sl) 261 | testErr(t, err) 262 | } 263 | 264 | func TestAddMap(t *testing.T) { 265 | resetPackageVars() 266 | conf := Config{} 267 | Init(conf) 268 | r := Aich{ 269 | F1: "snth", 270 | F2: 5, 271 | F3: 6, 272 | } 273 | mStr := map[string]interface{}{ 274 | "a": "valA", 275 | "b": 2, 276 | "c": 5.123, 277 | "d": []string{"d_a", "d_b"}, 278 | "e": r, 279 | } 280 | mInts := map[int64]interface{}{ 281 | 1: "foo", 282 | 2: "bar", 283 | } 284 | ev := NewEvent() 285 | err := ev.Add(mStr) 286 | testOK(t, err) 287 | err = ev.Add(mInts) 288 | testOK(t, err) 289 | testEquals(t, ev.data["a"], mStr["a"].(string)) 290 | testEquals(t, ev.data["b"], int(mStr["b"].(int))) 291 | testEquals(t, ev.data["c"], float64(mStr["c"].(float64))) 292 | testEquals(t, ev.data["d"], mStr["d"]) 293 | testEquals(t, ev.data["e"], r) 294 | testEquals(t, ev.data["1"], "foo") 295 | testEquals(t, ev.data["2"], "bar") 296 | 297 | mInt := map[uint8]interface{}{ 298 | 1: "valA", 299 | 2: 2, 300 | 3: 5.123, 301 | 4: []string{"d_a", "d_b"}, 302 | 6: r, 303 | } 304 | ev = NewEvent() 305 | err = ev.Add(mInt) 306 | t.Logf("ev.data is %+v", ev.data) 307 | testOK(t, err) 308 | testEquals(t, ev.data["1"], mInt[1].(string)) 309 | testEquals(t, ev.data["2"], mInt[2].(int)) 310 | testEquals(t, ev.data["3"], float64(mInt[3].(float64))) 311 | testEquals(t, ev.data["4"], mInt[4]) 312 | testEquals(t, ev.data["6"], mInt[6]) 313 | 314 | ev = NewEvent() 315 | mStrStr := map[string]string{ 316 | "1": "2", 317 | } 318 | 319 | err = ev.Add(mStrStr) 320 | testOK(t, err) 321 | testEquals(t, ev.data["1"], "2") 322 | } 323 | 324 | func TestAddMapPtr(t *testing.T) { 325 | resetPackageVars() 326 | conf := Config{} 327 | Init(conf) 328 | r := Aich{ 329 | F1: "snth", 330 | F2: 5, 331 | F3: 6, 332 | } 333 | mStr := map[string]interface{}{ 334 | "a": "valA", 335 | "b": 2, 336 | "c": 5.123, 337 | "d": []string{"d_a", "d_b"}, 338 | "e": r, 339 | } 340 | ev := NewEvent() 341 | err := ev.Add(&mStr) 342 | t.Logf("ev.data is %+v", ev.data) 343 | testOK(t, err) 344 | testEquals(t, ev.data["a"], mStr["a"].(string)) 345 | testEquals(t, ev.data["b"], int(mStr["b"].(int))) 346 | testEquals(t, ev.data["c"], float64(mStr["c"].(float64))) 347 | testEquals(t, ev.data["d"], mStr["d"]) 348 | testEquals(t, ev.data["e"], r) 349 | 350 | } 351 | 352 | func TestAddFunc(t *testing.T) { 353 | resetPackageVars() 354 | conf := Config{} 355 | Init(conf) 356 | keys := []string{ 357 | "aoeu", 358 | "oeui", 359 | "euid", 360 | } 361 | vals := []interface{}{ 362 | "str", 363 | 5, 364 | []string{"d_a", "d_b"}, 365 | } 366 | i := 0 367 | myFn := func() (string, interface{}, error) { 368 | if i >= 3 { 369 | return "", nil, errors.New("all done") 370 | } 371 | str := keys[i] 372 | val := vals[i] 373 | i++ 374 | return str, val, nil 375 | } 376 | 377 | ev := NewEvent() 378 | ev.AddFunc(myFn) 379 | t.Logf("data has %+v", ev.data) 380 | testEquals(t, ev.data["aoeu"], vals[0].(string)) 381 | testEquals(t, ev.data["oeui"], int(vals[1].(int))) 382 | testEquals(t, ev.data["euid"], vals[2]) 383 | testEquals(t, len(ev.data), 3) 384 | } 385 | 386 | func TestAddFuncUsingAdd(t *testing.T) { 387 | resetPackageVars() 388 | conf := Config{} 389 | Init(conf) 390 | myFn := func() (string, interface{}, error) { 391 | return "", "", nil 392 | } 393 | ev := NewEvent() 394 | err := ev.Add(myFn) 395 | testErr(t, err) 396 | } 397 | 398 | func TestAddDynamicField(t *testing.T) { 399 | resetPackageVars() 400 | Init(Config{}) 401 | i := 0 402 | myFn := func() interface{} { 403 | v := i 404 | i++ 405 | return v 406 | } 407 | AddDynamicField("incrementingInt", myFn) 408 | ev1 := NewEvent() 409 | testEquals(t, ev1.data["incrementingInt"], 0) 410 | ev2 := NewEvent() 411 | testEquals(t, ev2.data["incrementingInt"], 1) 412 | } 413 | 414 | func TestNewBuilder(t *testing.T) { 415 | resetPackageVars() 416 | conf := Config{ 417 | WriteKey: "aoeu", 418 | Dataset: "oeui", 419 | SampleRate: 1, 420 | APIHost: "http://localhost:8081/", 421 | } 422 | Init(conf) 423 | b := NewBuilder() 424 | testEquals(t, b.WriteKey, "aoeu") 425 | testEquals(t, b.Dataset, "oeui") 426 | testEquals(t, b.SampleRate, uint(1)) 427 | testEquals(t, b.APIHost, "http://localhost:8081/") 428 | } 429 | 430 | func TestCloneBuilder(t *testing.T) { 431 | resetPackageVars() 432 | conf := Config{ 433 | WriteKey: "aoeu", 434 | Dataset: "oeui", 435 | SampleRate: 1, 436 | APIHost: "http://localhost:8081/", 437 | } 438 | Init(conf) 439 | b := NewBuilder() 440 | b2 := b.Clone() 441 | b2.WriteKey = "newAAAA" 442 | b2.Dataset = "newoooo" 443 | b2.SampleRate = 2 444 | b2.APIHost = "differentAPIHost" 445 | // old builder didn't change 446 | testEquals(t, b.WriteKey, "aoeu") 447 | testEquals(t, b.Dataset, "oeui") 448 | testEquals(t, b.SampleRate, uint(1)) 449 | testEquals(t, b.APIHost, "http://localhost:8081/") 450 | // cloned builder has new values 451 | testEquals(t, b2.WriteKey, "newAAAA") 452 | testEquals(t, b2.Dataset, "newoooo") 453 | testEquals(t, b2.SampleRate, uint(2)) 454 | testEquals(t, b2.APIHost, "differentAPIHost") 455 | } 456 | 457 | func TestBuilderDynFields(t *testing.T) { 458 | resetPackageVars() 459 | var i int 460 | myIntFn := func() interface{} { 461 | v := i 462 | i++ 463 | return v 464 | } 465 | strs := []string{ 466 | "aoeu", 467 | "oeui", 468 | "euid", 469 | } 470 | var j int 471 | myStrFn := func() interface{} { 472 | v := j 473 | j++ 474 | return strs[v] 475 | } 476 | f := 1.0 477 | myFloatFn := func() interface{} { 478 | v := f 479 | f += 1.2 480 | return v 481 | } 482 | AddDynamicField("ints", myIntFn) 483 | b := NewBuilder() 484 | b.AddDynamicField("strs", myStrFn) 485 | testEquals(t, len(dc.builder.dynFields), 1) 486 | testEquals(t, len(b.dynFields), 2) 487 | 488 | ev1 := NewEvent() 489 | testEquals(t, ev1.data["ints"], 0) 490 | ev2 := b.NewEvent() 491 | testEquals(t, ev2.data["ints"], 1) 492 | testEquals(t, ev2.data["strs"], "aoeu") 493 | 494 | b2 := b.Clone() 495 | b2.AddDynamicField("floats", myFloatFn) 496 | ev3 := NewEvent() 497 | testEquals(t, ev3.data["ints"], 2) 498 | testEquals(t, ev3.data["strs"], nil) 499 | ev4 := b.NewEvent() 500 | testEquals(t, ev4.data["ints"], 3) 501 | testEquals(t, ev4.data["strs"], "oeui") 502 | ev5 := b2.NewEvent() 503 | testEquals(t, ev5.data["ints"], 4) 504 | testEquals(t, ev5.data["strs"], "euid") 505 | testEquals(t, ev5.data["floats"], 1.0) 506 | } 507 | 508 | func TestBuilderStaticFields(t *testing.T) { 509 | resetPackageVars() 510 | // test you can add fields to a builder and events get them 511 | b := NewBuilder() 512 | b.AddField("intF", 1) 513 | b.AddField("strF", "aoeu") 514 | ev := b.NewEvent() 515 | testEquals(t, ev.data["intF"], 1) 516 | testEquals(t, ev.data["strF"], "aoeu") 517 | // test you can clone a builder and events get the cloned data 518 | b2 := b.Clone() 519 | ev2 := b2.NewEvent() 520 | testEquals(t, ev2.data["intF"], 1) 521 | testEquals(t, ev2.data["strF"], "aoeu") 522 | // test that you can add new fields to the cloned builder 523 | // and events get them 524 | b2.AddField("floatF", 1.234) 525 | ev3 := b2.NewEvent() 526 | testEquals(t, ev3.data["intF"], 1) 527 | testEquals(t, ev3.data["strF"], "aoeu") 528 | testEquals(t, ev3.data["floatF"], 1.234) 529 | // test that the old builder didn't get the metrics a5dded to 530 | // the new builder 531 | ev4 := b.NewEvent() 532 | testEquals(t, ev4.data["intF"], 1) 533 | testEquals(t, ev4.data["strF"], "aoeu") 534 | testEquals(t, ev4.data["floatF"], nil) 535 | } 536 | 537 | func TestBuilderDynFieldsCloneRace(t *testing.T) { 538 | resetPackageVars() 539 | 540 | b := NewBuilder() 541 | 542 | const interations = 100 543 | 544 | var wg sync.WaitGroup 545 | wg.Add(1) 546 | go func() { 547 | defer wg.Done() 548 | for i := 0; i < interations; i++ { 549 | b.Clone() 550 | } 551 | }() 552 | wg.Add(1) 553 | go func() { 554 | defer wg.Done() 555 | for i := 0; i < interations; i++ { 556 | b.AddDynamicField("dyn_field", nil) 557 | } 558 | }() 559 | 560 | wg.Wait() 561 | } 562 | 563 | func TestOutputInterface(t *testing.T) { 564 | resetPackageVars() 565 | testTx := &MockOutput{} 566 | Init(Config{ 567 | WriteKey: "foo", 568 | Dataset: "bar", 569 | Output: testTx, 570 | }) 571 | 572 | ev := NewEvent() 573 | ev.AddField("mock", "mick") 574 | err := ev.Send() 575 | testOK(t, err) 576 | testEquals(t, len(testTx.Events()), 1) 577 | testEquals(t, testTx.Events()[0].Fields(), map[string]interface{}{"mock": "mick"}) 578 | } 579 | 580 | func TestSendTime(t *testing.T) { 581 | resetPackageVars() 582 | testTx := &transmission.MockSender{} 583 | Init(Config{ 584 | WriteKey: "foo", 585 | Dataset: "bar", 586 | Transmission: testTx, 587 | }) 588 | 589 | now := time.Now().Truncate(time.Millisecond) 590 | expected := map[string]interface{}{"event_time": now} 591 | 592 | tsts := []struct { 593 | key string 594 | val interface{} 595 | }{ 596 | {"event_time", now}, 597 | {"", map[string]interface{}{"event_time": now}}, 598 | {"", struct { 599 | Time time.Time `json:"event_time"` 600 | }{now}}, 601 | } 602 | 603 | for i, tt := range tsts { 604 | ev := NewEvent() 605 | if tt.key != "" { 606 | ev.AddField(tt.key, tt.val) 607 | } else { 608 | ev.Add(tt.val) 609 | } 610 | err := ev.Send() 611 | testOK(t, err) 612 | testEquals(t, len(testTx.Events()), i+1) 613 | testEquals(t, testTx.Events()[i].Data, expected) 614 | } 615 | } 616 | 617 | func TestSendPresampledErrors(t *testing.T) { 618 | resetPackageVars() 619 | testTx := &transmission.MockSender{} 620 | Init(Config{Transmission: testTx}) 621 | 622 | tsts := []struct { 623 | ev *Event 624 | expErr error 625 | }{ 626 | { 627 | ev: &Event{client: dc}, 628 | expErr: errors.New("No metrics added to event. Won't send empty event."), 629 | }, 630 | { 631 | ev: &Event{ 632 | fieldHolder: fieldHolder{ 633 | data: map[string]interface{}{"a": 1}, 634 | }, 635 | client: dc, 636 | }, 637 | expErr: errors.New("No APIHost for Honeycomb. Can't send to the Great Unknown."), 638 | }, 639 | { 640 | ev: &Event{ 641 | fieldHolder: fieldHolder{ 642 | data: map[string]interface{}{"a": 1}, 643 | }, 644 | APIHost: "foo", 645 | client: dc, 646 | }, 647 | expErr: errors.New("No WriteKey specified. Can't send event."), 648 | }, 649 | { 650 | ev: &Event{ 651 | fieldHolder: fieldHolder{ 652 | data: map[string]interface{}{"a": 1}, 653 | }, 654 | APIHost: "foo", 655 | WriteKey: "bar", 656 | client: dc, 657 | }, 658 | expErr: errors.New("No Dataset for Honeycomb. Can't send datasetless."), 659 | }, 660 | { 661 | ev: &Event{ 662 | fieldHolder: fieldHolder{ 663 | data: map[string]interface{}{"a": 1}, 664 | }, 665 | APIHost: "foo", 666 | WriteKey: "bar", 667 | Dataset: "baz", 668 | client: dc, 669 | }, 670 | expErr: nil, 671 | }, 672 | } 673 | for i, tst := range tsts { 674 | err := tst.ev.SendPresampled() 675 | testEquals(t, err, tst.expErr, fmt.Sprintf("testing expected error from test object %d", i)) 676 | } 677 | } 678 | 679 | // TestPresampledSendSamplerate verifies that SendPresampled does no sampling 680 | func TestPresampledSendSamplerate(t *testing.T) { 681 | resetPackageVars() 682 | Init(Config{}) 683 | testTx := &transmission.MockSender{} 684 | 685 | dc, _ = NewClient(ClientConfig{ 686 | Transmission: testTx, 687 | }) 688 | 689 | ev := &Event{ 690 | fieldHolder: fieldHolder{ 691 | data: map[string]interface{}{"a": 1}, 692 | }, 693 | APIHost: "foo", 694 | WriteKey: "bar", 695 | Dataset: "baz", 696 | SampleRate: 5, 697 | client: dc, 698 | } 699 | 700 | for i := 0; i < 5; i++ { 701 | err := ev.SendPresampled() 702 | testOK(t, err) 703 | 704 | testEquals(t, len(testTx.Events()), i+1) 705 | testEquals(t, testTx.Events()[i].SampleRate, uint(5)) 706 | } 707 | } 708 | 709 | // TestSendSamplerate verifies that Send samples 710 | func TestSendSamplerate(t *testing.T) { 711 | resetPackageVars() 712 | Init(Config{}) 713 | testTx := &transmission.MockSender{} 714 | rand.Seed(1) 715 | 716 | dc, _ = NewClient(ClientConfig{ 717 | Transmission: testTx, 718 | }) 719 | 720 | ev := &Event{ 721 | fieldHolder: fieldHolder{ 722 | data: map[string]interface{}{"a": 1}, 723 | }, 724 | APIHost: "foo", 725 | WriteKey: "bar", 726 | Dataset: "baz", 727 | SampleRate: 2, 728 | client: dc, 729 | } 730 | for i := 0; i < 10; i++ { 731 | err := ev.Send() 732 | testOK(t, err) 733 | } 734 | testEquals(t, len(testTx.Events()), 4, "expected testTx num events incorrect") 735 | for _, ev := range testTx.Events() { 736 | testEquals(t, ev.SampleRate, uint(2)) 737 | } 738 | } 739 | 740 | type testTransport struct { 741 | invoked bool 742 | } 743 | 744 | func (tr *testTransport) RoundTrip(r *http.Request) (*http.Response, error) { 745 | tr.invoked = true 746 | return &http.Response{Body: io.NopCloser(bytes.NewReader(nil))}, nil 747 | } 748 | 749 | func TestSendTestTransport(t *testing.T) { 750 | tr := &testTransport{} 751 | Init(Config{ 752 | WriteKey: "foo", 753 | Dataset: "bar", 754 | Transport: tr, 755 | }) 756 | 757 | err := SendNow(map[string]interface{}{"foo": 3}) 758 | dc.transmission.Stop() // flush unsent events 759 | dc.transmission.Start() // reopen tx.muster channel 760 | testOK(t, err) 761 | testEquals(t, tr.invoked, true) 762 | } 763 | 764 | func TestChannelMembers(t *testing.T) { 765 | resetPackageVars() 766 | Init(Config{}) 767 | 768 | // adding channels directly using .AddField 769 | ev := NewEvent() 770 | ev.AddField("intChan", make(chan int)) 771 | marshalled, err := json.Marshal(ev.data) 772 | assert.Nil(t, err) 773 | assert.JSONEq(t, "{}", string(marshalled)) 774 | 775 | // adding a struct with a channel in it to an event 776 | type StructWithChan struct { 777 | A int 778 | B string 779 | C chan int 780 | } 781 | structWithChan := &StructWithChan{ 782 | A: 1, 783 | B: "hello", 784 | C: make(chan int), 785 | } 786 | 787 | ev2 := NewEvent() 788 | ev2.Add(structWithChan) 789 | 790 | marshalled2, err := json.Marshal(ev2.data) 791 | assert.JSONEq(t, `{"A": 1, "B": "hello"}`, string(marshalled2)) 792 | 793 | // adding a struct with a struct-valued field containing a channel 794 | type ChanInField struct { 795 | CStruct *StructWithChan 796 | D int 797 | } 798 | 799 | chanInField := &ChanInField{} 800 | chanInField.CStruct = &StructWithChan{ 801 | A: 1, 802 | B: "hello", 803 | C: make(chan int), 804 | } 805 | chanInField.D = 2 806 | 807 | ev3 := NewEvent() 808 | ev3.Add(chanInField) 809 | 810 | testEquals(t, ev3.data["A"], nil) 811 | testEquals(t, ev3.data["B"], nil) 812 | testEquals(t, ev3.data["C"], nil) 813 | testEquals(t, ev3.data["D"], 2) 814 | 815 | // adding a struct containing an embedded struct containing a channel 816 | type ChanInEmbedded struct { 817 | StructWithChan 818 | D int 819 | } 820 | 821 | chanInEmbedded := &ChanInEmbedded{} 822 | chanInEmbedded.A = 1 823 | chanInEmbedded.B = "hello" 824 | chanInEmbedded.C = make(chan int) 825 | chanInEmbedded.D = 2 826 | 827 | ev4 := NewEvent() 828 | ev4.Add(chanInField) 829 | 830 | testEquals(t, ev4.data["A"], nil) 831 | testEquals(t, ev4.data["B"], nil) 832 | testEquals(t, ev4.data["C"], nil) 833 | testEquals(t, ev4.data["D"], 2) 834 | } 835 | 836 | func TestDataRace1(t *testing.T) { 837 | e := &Event{SampleRate: 1} 838 | e.data = map[string]interface{}{"a": 1} 839 | 840 | var err error 841 | var wg sync.WaitGroup 842 | 843 | wg.Add(2) 844 | go func() { 845 | _, err = json.Marshal(e) 846 | wg.Done() 847 | }() 848 | 849 | go func() { 850 | mStr := map[string]interface{}{ 851 | "a": "valA", 852 | "b": 2, 853 | "c": 5.123, 854 | "d": []string{"d_a", "d_b"}, 855 | } 856 | e.AddField("b", 2) 857 | e.Add(mStr) 858 | wg.Done() 859 | }() 860 | 861 | wg.Wait() 862 | testOK(t, err) 863 | } 864 | 865 | func TestDataRace2(t *testing.T) { 866 | b := &Builder{} 867 | b.data = map[string]interface{}{"a": 1} 868 | 869 | var wg sync.WaitGroup 870 | 871 | wg.Add(2) 872 | go func() { 873 | _ = b.NewEvent() 874 | wg.Done() 875 | }() 876 | 877 | go func() { 878 | b.AddField("b", 2) 879 | wg.Done() 880 | }() 881 | 882 | wg.Wait() 883 | } 884 | 885 | func TestDataRace3(t *testing.T) { 886 | resetPackageVars() 887 | testTx := &transmission.MockSender{} 888 | Init(Config{ 889 | Transmission: testTx, 890 | }) 891 | 892 | ev := &Event{ 893 | fieldHolder: fieldHolder{ 894 | data: map[string]interface{}{"a": 1}, 895 | }, 896 | APIHost: "foo", 897 | WriteKey: "bar", 898 | Dataset: "baz", 899 | SampleRate: 1, 900 | client: dc, 901 | } 902 | 903 | var wg sync.WaitGroup 904 | 905 | wg.Add(2) 906 | 907 | go func() { 908 | err := ev.Send() 909 | testOK(t, err) 910 | wg.Done() 911 | }() 912 | 913 | go func() { 914 | ev.AddField("newField", 1) 915 | wg.Done() 916 | }() 917 | 918 | wg.Wait() 919 | 920 | testEquals(t, len(testTx.Events()), 1, "expected testTx num datas incorrect") 921 | } 922 | 923 | func TestEndToEnd(t *testing.T) { 924 | eventCount := 3 925 | 926 | server := startFakeServer(t, eventCount) 927 | defer server.Close() 928 | 929 | hc, err := NewClient(ClientConfig{ 930 | APIKey: "e2e", 931 | Dataset: "e2e", 932 | SampleRate: 1, 933 | APIHost: server.URL, 934 | }) 935 | testOK(t, err) 936 | defer hc.Close() 937 | 938 | responseChan := hc.TxResponses() 939 | 940 | for i := 0; i < eventCount; i++ { 941 | ev := hc.NewEvent() 942 | ev.AddField("event", i) 943 | ev.AddField("method", "get") 944 | ev.Send() 945 | } 946 | hc.Flush() 947 | 948 | deadline := time.After(time.Second) 949 | for i := 0; i < eventCount; i++ { 950 | select { 951 | case got := <-responseChan: 952 | testEquals(t, got.StatusCode, http.StatusCreated) 953 | case <-deadline: 954 | t.Error("timed out waiting for response") 955 | return 956 | } 957 | } 958 | } 959 | 960 | // 961 | // Examples 962 | // 963 | 964 | func Example() { 965 | // call Init before using libhoney 966 | Init(Config{ 967 | WriteKey: "abcabc123123defdef456456", 968 | Dataset: "Example Service", 969 | SampleRate: 1, 970 | }) 971 | // when all done, call close 972 | defer Close() 973 | 974 | // create an event, add fields 975 | ev := NewEvent() 976 | ev.AddField("duration_ms", 153.12) 977 | ev.AddField("method", "get") 978 | // send the event 979 | ev.Send() 980 | } 981 | 982 | func ExampleAddDynamicField() { 983 | // adds the number of goroutines running at event 984 | // creation time to every event sent to Honeycomb. 985 | AddDynamicField("num_goroutines", 986 | func() interface{} { return runtime.NumGoroutine() }) 987 | } 988 | 989 | func BenchmarkInit(b *testing.B) { 990 | for n := 0; n < b.N; n++ { 991 | Init(Config{ 992 | WriteKey: "aoeu", 993 | Dataset: "oeui", 994 | SampleRate: 1, 995 | APIHost: "http://localhost:8081/", 996 | Transmission: &transmission.MockSender{}, 997 | }) 998 | // create an event, add fields 999 | ev := NewEvent() 1000 | ev.AddField("duration_ms", 153.12) 1001 | ev.AddField("method", "get") 1002 | // send the event 1003 | ev.Send() 1004 | Close() 1005 | } 1006 | } 1007 | 1008 | func BenchmarkFlush(b *testing.B) { 1009 | Init(Config{ 1010 | WriteKey: "aoeu", 1011 | Dataset: "oeui", 1012 | SampleRate: 1, 1013 | APIHost: "http://localhost:8081/", 1014 | Transmission: &transmission.MockSender{}, 1015 | }) 1016 | for n := 0; n < b.N; n++ { 1017 | // create an event, add fields 1018 | ev := NewEvent() 1019 | ev.AddField("duration_ms", 153.12) 1020 | ev.AddField("method", "get") 1021 | // send the event 1022 | ev.Send() 1023 | Flush() 1024 | } 1025 | Close() 1026 | } 1027 | 1028 | func BenchmarkEndToEnd(b *testing.B) { 1029 | // extra response values are ignored 1030 | server := startFakeServer(b, DefaultMaxBatchSize) 1031 | defer server.Close() 1032 | 1033 | hc, err := NewClient(ClientConfig{ 1034 | APIKey: "e2e", 1035 | Dataset: "e2e", 1036 | SampleRate: 1, 1037 | APIHost: server.URL, 1038 | }) 1039 | testOK(b, err) 1040 | defer hc.Close() 1041 | 1042 | b.ResetTimer() 1043 | for n := 0; n < b.N; n++ { 1044 | ev := hc.NewEvent() 1045 | ev.AddField("event", n) 1046 | ev.AddField("method", "get") 1047 | ev.Send() 1048 | } 1049 | } 1050 | 1051 | // Starts a minimalist fake server for end-to-end tests, similar to 1052 | // transmission's FakeRoundTripper. 1053 | func startFakeServer(t testing.TB, assumeEventCount int) *httptest.Server { 1054 | var cannedResponse []byte 1055 | if assumeEventCount > 0 { 1056 | responses := make([]struct { 1057 | Status int 1058 | }, assumeEventCount) 1059 | for i := range responses { 1060 | responses[i].Status = http.StatusCreated 1061 | } 1062 | var err error 1063 | cannedResponse, err = json.Marshal(responses) 1064 | testOK(t, err) 1065 | } 1066 | 1067 | handler := func(w http.ResponseWriter, r *http.Request) { 1068 | if r.Method != "POST" { 1069 | t.Fatalf("unsupported method %s", r.Method) 1070 | } 1071 | if !strings.HasPrefix(r.URL.Path, "/1/batch") { 1072 | t.Fatalf("unsupported path %s", r.URL.Path) 1073 | } 1074 | w.WriteHeader(http.StatusOK) 1075 | if cannedResponse != nil { 1076 | w.Write(cannedResponse) 1077 | return 1078 | } 1079 | t.Fatal("dynamic responses not yet supported") 1080 | } 1081 | 1082 | return httptest.NewServer(http.HandlerFunc(handler)) 1083 | } 1084 | 1085 | func TestEventStringReturnsMaskedApiKey(t *testing.T) { 1086 | tests := []struct { 1087 | ev *Event 1088 | expStr string 1089 | }{ 1090 | { 1091 | ev: &Event{ 1092 | fieldHolder: fieldHolder{ 1093 | data: map[string]interface{}{"a": 1}, 1094 | }, 1095 | APIHost: "foo", 1096 | WriteKey: "", 1097 | Dataset: "baz", 1098 | SampleRate: 1, 1099 | client: dc, 1100 | }, 1101 | expStr: "{WriteKey: Dataset:baz SampleRate:1 APIHost:foo Timestamp:0001-01-01 00:00:00 +0000 UTC fieldHolder:map[a:1] sent:false}", 1102 | }, 1103 | { 1104 | ev: &Event{ 1105 | fieldHolder: fieldHolder{ 1106 | data: map[string]interface{}{"a": 1}, 1107 | }, 1108 | APIHost: "foo", 1109 | WriteKey: "woop", 1110 | Dataset: "baz", 1111 | SampleRate: 1, 1112 | client: dc, 1113 | }, 1114 | expStr: "{WriteKey:woop Dataset:baz SampleRate:1 APIHost:foo Timestamp:0001-01-01 00:00:00 +0000 UTC fieldHolder:map[a:1] sent:false}", 1115 | }, 1116 | { 1117 | ev: &Event{ 1118 | fieldHolder: fieldHolder{ 1119 | data: map[string]interface{}{"a": 1}, 1120 | }, 1121 | APIHost: "foo", 1122 | WriteKey: "fibble", 1123 | Dataset: "baz", 1124 | SampleRate: 1, 1125 | client: dc, 1126 | }, 1127 | expStr: "{WriteKey:XXbble Dataset:baz SampleRate:1 APIHost:foo Timestamp:0001-01-01 00:00:00 +0000 UTC fieldHolder:map[a:1] sent:false}", 1128 | }, 1129 | { 1130 | ev: &Event{ 1131 | fieldHolder: fieldHolder{ 1132 | data: map[string]interface{}{"a": 1}, 1133 | }, 1134 | APIHost: "foo", 1135 | WriteKey: "fibblewibble", 1136 | Dataset: "baz", 1137 | SampleRate: 1, 1138 | client: dc, 1139 | }, 1140 | expStr: "{WriteKey:XXXXXXXXbble Dataset:baz SampleRate:1 APIHost:foo Timestamp:0001-01-01 00:00:00 +0000 UTC fieldHolder:map[a:1] sent:false}", 1141 | }, 1142 | } 1143 | 1144 | for _, test := range tests { 1145 | testEquals(t, test.ev.String(), test.expStr) 1146 | } 1147 | } 1148 | 1149 | func TestConfigVariationsForClassicNonClassic(t *testing.T) { 1150 | tests := []struct { 1151 | apikey string 1152 | dataset string 1153 | expectedDataset string 1154 | }{ 1155 | { 1156 | apikey: "", 1157 | dataset: "", 1158 | expectedDataset: defaultClassicDataset, 1159 | }, 1160 | { 1161 | apikey: "c1a551c000d68f9ed1e96432ac1a3380", 1162 | dataset: "", 1163 | expectedDataset: defaultClassicDataset, 1164 | }, 1165 | { 1166 | apikey: "c1a551c000d68f9ed1e96432ac1a3380", 1167 | dataset: " my-service ", 1168 | expectedDataset: " my-service ", 1169 | }, 1170 | { 1171 | apikey: "d68f9ed1e96432ac1a3380", 1172 | dataset: "", 1173 | expectedDataset: defaultDataset, 1174 | }, 1175 | { 1176 | apikey: "d68f9ed1e96432ac1a3380", 1177 | dataset: " my-service ", 1178 | expectedDataset: "my-service", 1179 | }, 1180 | { 1181 | apikey: "hcxik_1234567890123456789012345678901234567890123456789012345678", 1182 | dataset: "", 1183 | expectedDataset: defaultDataset, 1184 | }, 1185 | { 1186 | apikey: "hcxik_1234567890123456789012345678901234567890123456789012345678", 1187 | dataset: "my-service", 1188 | expectedDataset: "my-service", 1189 | }, 1190 | { 1191 | apikey: "hcxic_1234567890123456789012345678901234567890123456789012345678", 1192 | dataset: "", 1193 | expectedDataset: defaultClassicDataset, 1194 | }, 1195 | { 1196 | apikey: "hcxic_1234567890123456789012345678901234567890123456789012345678", 1197 | dataset: "my-service", 1198 | expectedDataset: "my-service", 1199 | }, 1200 | } 1201 | 1202 | for _, tc := range tests { 1203 | config := Config{ 1204 | APIKey: tc.apikey, 1205 | Dataset: tc.dataset, 1206 | } 1207 | testEquals(t, config.getDataset(), tc.expectedDataset) 1208 | } 1209 | } 1210 | 1211 | func TestVerifyAPIKey(t *testing.T) { 1212 | testCases := []struct { 1213 | Name string 1214 | APIKey string 1215 | expectedEnvironment string 1216 | }{ 1217 | {Name: "classic", APIKey: "f2b9746602fd36049b222d3e8c6c48c9", expectedEnvironment: ""}, 1218 | {Name: "non-classic", APIKey: "lcYrFflRUR6rHbIifwqhfG", expectedEnvironment: "test_env"}, 1219 | } 1220 | 1221 | for _, tc := range testCases { 1222 | t.Run(tc.Name, func(t *testing.T) { 1223 | config := Config{ 1224 | APIKey: tc.APIKey, 1225 | } 1226 | 1227 | server := httptest.NewServer( 1228 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1229 | assert.Equal(t, "/1/auth", r.URL.Path) 1230 | assert.Equal(t, []string{tc.APIKey}, r.Header["X-Honeycomb-Team"]) 1231 | 1232 | if config.IsClassic() { 1233 | w.Write([]byte(`{"team":{"slug":"test_team"}}`)) 1234 | } else { 1235 | w.Write([]byte(`{"team":{"slug":"test_team"},"environment":{"slug":"test_env"}}`)) 1236 | } 1237 | }), 1238 | ) 1239 | defer server.Close() 1240 | config.APIHost = server.URL 1241 | 1242 | // There are 3 places we can verify and/or get the team and environment given 1243 | // an APIkey: VerifyWriteKey, VerifyAPIKey and GetTeamAndEnvironment 1244 | team, err := VerifyWriteKey(config) 1245 | assert.Equal(t, "test_team", team) 1246 | assert.Nil(t, err) 1247 | 1248 | team, err = VerifyAPIKey(config) 1249 | assert.Equal(t, "test_team", team) 1250 | assert.Nil(t, err) 1251 | 1252 | team, env, err := GetTeamAndEnvironment(config) 1253 | assert.Equal(t, tc.expectedEnvironment, env) 1254 | }) 1255 | } 1256 | } 1257 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | // Logger is used to log extra info within the SDK detailing what's happening. 9 | // You can set a logger during initialization. If you leave it unititialized, no 10 | // logging will happen. If you set it to the DefaultLogger, you'll get 11 | // timestamped lines sent to STDOUT. Pass in your own implementation of the 12 | // interface to send it in to your own logger. An instance of the go package 13 | // log.Logger satisfies this interface. 14 | type Logger interface { 15 | // Printf accepts the same msg, args style as fmt.Printf(). 16 | Printf(msg string, args ...interface{}) 17 | } 18 | 19 | // DefaultLogger implements Logger and prints messages to stdout prepended by a 20 | // timestamp (RFC3339 formatted) 21 | type DefaultLogger struct{} 22 | 23 | // Printf prints the message to stdout. 24 | func (d *DefaultLogger) Printf(msg string, args ...interface{}) { 25 | // use the same format as the python libhoney: 26 | // '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 27 | // except for go's more friendly rfc3339nano rather than asctime 28 | msg = fmt.Sprintf("%s - %s - %s", "libhoney", "DEBUG", msg) 29 | log.Printf(msg+"\n", args...) 30 | } 31 | 32 | type nullLogger struct{} 33 | 34 | // Printf swallows messages 35 | func (n *nullLogger) Printf(msg string, args ...interface{}) { 36 | // nothing to see here. 37 | } 38 | -------------------------------------------------------------------------------- /mockoutput.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import "github.com/honeycombio/libhoney-go/transmission" 4 | 5 | // MockOutput implements the Output interface and passes it along to the 6 | // transmission.MockSender. 7 | // 8 | // Deprecated: Please use the transmission.MockSender directly instead. 9 | // It is provided here for backwards compatibility and will be removed eventually. 10 | type MockOutput struct { 11 | transmission.MockSender 12 | } 13 | 14 | func (w *MockOutput) Add(ev *Event) { 15 | transEv := &transmission.Event{ 16 | APIHost: ev.APIHost, 17 | APIKey: ev.WriteKey, 18 | Dataset: ev.Dataset, 19 | SampleRate: ev.SampleRate, 20 | Timestamp: ev.Timestamp, 21 | Metadata: ev.Metadata, 22 | Data: ev.data, 23 | } 24 | w.MockSender.Add(transEv) 25 | } 26 | 27 | func (w *MockOutput) Events() []*Event { 28 | evs := []*Event{} 29 | for _, ev := range w.MockSender.Events() { 30 | transEv := &Event{ 31 | APIHost: ev.APIHost, 32 | WriteKey: ev.APIKey, 33 | Dataset: ev.Dataset, 34 | SampleRate: ev.SampleRate, 35 | Timestamp: ev.Timestamp, 36 | Metadata: ev.Metadata, 37 | } 38 | transEv.data = ev.Data 39 | evs = append(evs, transEv) 40 | } 41 | return evs 42 | } 43 | -------------------------------------------------------------------------------- /transmission/discard.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | // DiscardSender implements the Sender interface and drops all events. 4 | type DiscardSender struct { 5 | WriterSender 6 | } 7 | 8 | func (d *DiscardSender) Add(ev *Event) {} 9 | -------------------------------------------------------------------------------- /transmission/event.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | "time" 10 | 11 | "github.com/vmihailenco/msgpack/v5" 12 | ) 13 | 14 | type Event struct { 15 | // APIKey, if set, overrides whatever is found in Config 16 | APIKey string 17 | // Dataset, if set, overrides whatever is found in Config 18 | Dataset string 19 | // SampleRate, if set, overrides whatever is found in Config 20 | SampleRate uint 21 | // APIHost, if set, overrides whatever is found in Config 22 | APIHost string 23 | // Timestamp, if set, specifies the time for this event. If unset, defaults 24 | // to Now() 25 | Timestamp time.Time 26 | // Metadata is a field for you to add in data that will be handed back to you 27 | // on the Response object read off the Responses channel. It is not sent to 28 | // Honeycomb with the event. 29 | Metadata interface{} 30 | 31 | // Data contains the content of the event (all the fields and their values) 32 | Data map[string]interface{} 33 | } 34 | 35 | // Marshaling an Event for batching up to the Honeycomb servers. Omits fields 36 | // that aren't specific to this particular event, and allows for behavior like 37 | // omitempty'ing a zero'ed out time.Time. 38 | func (e *Event) MarshalJSON() ([]byte, error) { 39 | tPointer := &(e.Timestamp) 40 | if e.Timestamp.IsZero() { 41 | tPointer = nil 42 | } 43 | 44 | // don't include sample rate if it's 1; this is the default 45 | sampleRate := e.SampleRate 46 | if sampleRate == 1 { 47 | sampleRate = 0 48 | } 49 | 50 | return json.Marshal(struct { 51 | Data marshallableMap `json:"data"` 52 | SampleRate uint `json:"samplerate,omitempty"` 53 | Timestamp *time.Time `json:"time,omitempty"` 54 | }{e.Data, sampleRate, tPointer}) 55 | } 56 | 57 | func (e *Event) MarshalMsgpack() (byts []byte, err error) { 58 | tPointer := &(e.Timestamp) 59 | if e.Timestamp.IsZero() { 60 | tPointer = nil 61 | } 62 | 63 | // don't include sample rate if it's 1; this is the default 64 | sampleRate := e.SampleRate 65 | if sampleRate == 1 { 66 | sampleRate = 0 67 | } 68 | 69 | defer func() { 70 | if p := recover(); p != nil { 71 | byts = nil 72 | err = fmt.Errorf("msgpack panic: %v, trying to encode: %#v", p, e) 73 | } 74 | }() 75 | 76 | var buf bytes.Buffer 77 | encoder := msgpack.NewEncoder(&buf) 78 | encoder.SetCustomStructTag("json") 79 | err = encoder.Encode(struct { 80 | Data map[string]interface{} `msgpack:"data"` 81 | SampleRate uint `msgpack:"samplerate,omitempty"` 82 | Timestamp *time.Time `msgpack:"time,omitempty"` 83 | }{e.Data, sampleRate, tPointer}) 84 | return buf.Bytes(), err 85 | } 86 | 87 | type marshallableMap map[string]interface{} 88 | 89 | func (m marshallableMap) MarshalJSON() ([]byte, error) { 90 | keys := make([]string, len(m)) 91 | i := 0 92 | for k := range m { 93 | keys[i] = k 94 | i++ 95 | } 96 | sort.Strings(keys) 97 | out := bytes.NewBufferString("{") 98 | 99 | first := true 100 | for _, k := range keys { 101 | b, ok := maybeMarshalValue(m[k]) 102 | if ok { 103 | if first { 104 | first = false 105 | } else { 106 | out.WriteByte(',') 107 | } 108 | 109 | out.WriteByte('"') 110 | out.Write([]byte(k)) 111 | out.WriteByte('"') 112 | out.WriteByte(':') 113 | out.Write(b) 114 | } 115 | } 116 | out.WriteByte('}') 117 | return out.Bytes(), nil 118 | } 119 | 120 | var ( 121 | ptrKinds = []reflect.Kind{reflect.Ptr, reflect.Slice, reflect.Map} 122 | ) 123 | 124 | func maybeMarshalValue(v interface{}) ([]byte, bool) { 125 | if v == nil { 126 | return nil, false 127 | } 128 | val := reflect.ValueOf(v) 129 | kind := val.Type().Kind() 130 | for _, ptrKind := range ptrKinds { 131 | if kind == ptrKind && val.IsNil() { 132 | return nil, false 133 | } 134 | } 135 | b, err := json.Marshal(v) 136 | if err != nil { 137 | return nil, false 138 | } 139 | return b, true 140 | } 141 | -------------------------------------------------------------------------------- /transmission/event_test.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | 9 | "github.com/vmihailenco/msgpack/v5" 10 | ) 11 | 12 | func TestEventMarshal(t *testing.T) { 13 | e := &Event{SampleRate: 1} 14 | e.Data = map[string]interface{}{ 15 | "a": int64(1), 16 | "b": float64(1.0), 17 | "c": true, 18 | "d": "foo", 19 | "e": time.Microsecond, 20 | "f": struct { 21 | Foo int64 `json:"f"` 22 | }{1}, 23 | "g": map[string]interface{}{ 24 | "g": 1, 25 | }, 26 | } 27 | b, err := json.Marshal(e) 28 | testOK(t, err) 29 | testEquals(t, string(b), `{"data":{"a":1,"b":1,"c":true,"d":"foo","e":1000,"f":{"f":1},"g":{"g":1}}}`) 30 | 31 | e.Timestamp = time.Unix(1476309645, 0).UTC() 32 | e.SampleRate = 5 33 | b, err = json.Marshal(e) 34 | testOK(t, err) 35 | testEquals(t, string(b), `{"data":{"a":1,"b":1,"c":true,"d":"foo","e":1000,"f":{"f":1},"g":{"g":1}},"samplerate":5,"time":"2016-10-12T22:00:45Z"}`) 36 | 37 | var buf bytes.Buffer 38 | err = msgpack.NewEncoder(&buf).Encode(e) 39 | testOK(t, err) 40 | 41 | var decoded interface{} 42 | err = msgpack.NewDecoder(&buf).Decode(&decoded) 43 | testOK(t, err) 44 | localTime := e.Timestamp.Local() 45 | 46 | testEquals(t, decoded, map[string]interface{}{ 47 | "time": localTime, 48 | "samplerate": int8(5), 49 | "data": map[string]interface{}{ 50 | "a": int64(1), 51 | "b": float64(1.0), 52 | "c": true, 53 | "d": "foo", 54 | "e": int64(time.Microsecond), 55 | "f": map[string]interface{}{ 56 | "f": int64(1), 57 | }, 58 | "g": map[string]interface{}{ 59 | "g": int8(1), 60 | }, 61 | }, 62 | }) 63 | } 64 | 65 | func BenchmarkEventEncode(b *testing.B) { 66 | tm, err := time.Parse(time.RFC3339, "2001-02-03T04:05:06Z") 67 | testOK(b, err) 68 | evt := Event{ 69 | SampleRate: 2, 70 | Timestamp: tm, 71 | Data: map[string]interface{}{ 72 | "a": int64(1), 73 | "b": float64(1.0), 74 | "c": true, 75 | "d": "foo", 76 | }, 77 | } 78 | 79 | var buf bytes.Buffer 80 | b.Run("json", func(b *testing.B) { 81 | for n := 0; n < b.N; n++ { 82 | buf.Reset() 83 | err := json.NewEncoder(&buf).Encode(&evt) 84 | testOK(b, err) 85 | } 86 | }) 87 | 88 | b.Run("msgpack", func(b *testing.B) { 89 | for n := 0; n < b.N; n++ { 90 | buf.Reset() 91 | err := msgpack.NewEncoder(&buf).Encode(&evt) 92 | testOK(b, err) 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /transmission/helpers_test.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "path/filepath" 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func testOK(t testing.TB, err error) { 16 | if err != nil { 17 | _, file, line, _ := runtime.Caller(1) 18 | t.Fatalf("%s:%d: unexpected error: %s", filepath.Base(file), line, err.Error()) 19 | } 20 | } 21 | func testErr(t testing.TB, err error) { 22 | if err == nil { 23 | _, file, line, _ := runtime.Caller(1) 24 | t.Fatalf("%s:%d: error expected!", filepath.Base(file), line) 25 | } 26 | } 27 | 28 | func testEquals(t testing.TB, actual, expected interface{}, msg ...string) { 29 | if !reflect.DeepEqual(actual, expected) { 30 | testCommonErr(t, actual, expected, msg) 31 | } 32 | } 33 | 34 | func testNotEquals(t testing.TB, actual, expected interface{}, msg ...string) { 35 | if reflect.DeepEqual(actual, expected) { 36 | testCommonErr(t, actual, expected, msg) 37 | } 38 | } 39 | 40 | func testCommonErr(t testing.TB, actual, expected interface{}, msg []string) { 41 | message := strings.Join(msg, ", ") 42 | _, file, line, _ := runtime.Caller(2) 43 | 44 | t.Errorf( 45 | "%s:%d: %s -- actual(%T): %v, expected(%T): %v", 46 | filepath.Base(file), 47 | line, 48 | message, 49 | testDeref(actual), 50 | testDeref(actual), 51 | testDeref(expected), 52 | testDeref(expected), 53 | ) 54 | } 55 | 56 | func testGetResponse(t testing.TB, ch chan Response) Response { 57 | _, file, line, _ := runtime.Caller(2) 58 | var resp Response 59 | select { 60 | case resp = <-ch: 61 | case <-time.After(50 * time.Millisecond): // block on read but prevent deadlocking tests 62 | t.Errorf("%s:%d: expected response on channel and timed out waiting for it!", filepath.Base(file), line) 63 | } 64 | return resp 65 | } 66 | 67 | func testIsPlaceholderResponse(t testing.TB, actual Response, msg ...string) { 68 | if actual.StatusCode != http.StatusTeapot { 69 | message := strings.Join(msg, ", ") 70 | _, file, line, _ := runtime.Caller(1) 71 | t.Errorf( 72 | "%s:%d placeholder expected -- %s", 73 | filepath.Base(file), 74 | line, 75 | message, 76 | ) 77 | } 78 | } 79 | 80 | func testDeref(v interface{}) interface{} { 81 | switch t := v.(type) { 82 | case *string: 83 | return fmt.Sprintf("*(%v)", *t) 84 | case *int64: 85 | return fmt.Sprintf("*(%v)", *t) 86 | case *float64: 87 | return fmt.Sprintf("*(%v)", *t) 88 | case *bool: 89 | return fmt.Sprintf("*(%v)", *t) 90 | default: 91 | return v 92 | } 93 | } 94 | 95 | // for easy time manipulation during tests 96 | type fakeNower struct { 97 | iter int 98 | } 99 | 100 | // Now() supports changing/increasing the returned Now() based on the number of 101 | // times it's called in succession 102 | func (f *fakeNower) Now() time.Time { 103 | now := time.Unix(1277132645, 0).Add(time.Second * 10 * time.Duration(f.iter)) 104 | f.iter++ 105 | return now 106 | } 107 | 108 | func fakePayload(fields int) map[string]interface{} { 109 | m := make(map[string]interface{}, fields) 110 | for i := 0; i < fields; i++ { 111 | k := randomString(10) 112 | switch i % 4 { 113 | case 0: 114 | m[k] = randomString(20) 115 | case 1: 116 | m[k] = rand.Float64() 117 | case 2: 118 | m[k] = "POST" 119 | case 3: 120 | m[k] = []int{200, 201, 203, 401, 402, 404, 500}[rand.Int31n(6)] 121 | } 122 | } 123 | return m 124 | } 125 | -------------------------------------------------------------------------------- /transmission/logger.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | type Logger interface { 4 | // Printf accepts the same msg, args style as fmt.Printf(). 5 | Printf(msg string, args ...interface{}) 6 | } 7 | 8 | type nullLogger struct{} 9 | 10 | // Printf swallows messages 11 | func (n *nullLogger) Printf(msg string, args ...interface{}) { 12 | // nothing to see here. 13 | } 14 | -------------------------------------------------------------------------------- /transmission/metrics.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | // Metrics is an interface that can be fulfilled by something like statsd 4 | type Metrics interface { 5 | Gauge(string, interface{}) 6 | Increment(string) 7 | Count(string, interface{}) 8 | } 9 | 10 | type nullMetrics struct{} 11 | 12 | func (nm *nullMetrics) Gauge(string, interface{}) {} 13 | func (nm *nullMetrics) Increment(string) {} 14 | func (nm *nullMetrics) Count(string, interface{}) {} 15 | -------------------------------------------------------------------------------- /transmission/mock.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // MockSender implements the Sender interface by retaining a slice of added 8 | // events, for use in unit tests. 9 | type MockSender struct { 10 | Started int 11 | Stopped int 12 | Flushed int 13 | EventsCalled int 14 | events []*Event 15 | responses chan Response 16 | BlockOnResponses bool 17 | sync.Mutex 18 | } 19 | 20 | func (m *MockSender) Add(ev *Event) { 21 | m.Lock() 22 | m.events = append(m.events, ev) 23 | m.Unlock() 24 | } 25 | 26 | func (m *MockSender) Start() error { 27 | m.Started += 1 28 | m.responses = make(chan Response, 1) 29 | return nil 30 | } 31 | func (m *MockSender) Stop() error { 32 | m.Stopped += 1 33 | return nil 34 | } 35 | func (m *MockSender) Flush() error { 36 | m.Flushed += 1 37 | return nil 38 | } 39 | 40 | func (m *MockSender) Events() []*Event { 41 | m.EventsCalled += 1 42 | m.Lock() 43 | defer m.Unlock() 44 | output := make([]*Event, len(m.events)) 45 | copy(output, m.events) 46 | return output 47 | } 48 | 49 | func (m *MockSender) TxResponses() chan Response { 50 | return m.responses 51 | } 52 | 53 | func (m *MockSender) SendResponse(r Response) bool { 54 | if m.BlockOnResponses { 55 | m.responses <- r 56 | } else { 57 | select { 58 | case m.responses <- r: 59 | default: 60 | return true 61 | } 62 | } 63 | return false 64 | } 65 | -------------------------------------------------------------------------------- /transmission/response.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | 8 | "github.com/vmihailenco/msgpack/v5" 9 | ) 10 | 11 | // Response is a record of an event sent. It includes information about sending 12 | // the event - how long it took, whether it succeeded, and so on. It also has a 13 | // metadata field that is just a pass through - populate an Event's Metadata 14 | // field and what you put there will be on the Response that corresponds to 15 | // that Event. This allows you to track specific events. 16 | type Response struct { 17 | 18 | // Err contains any error returned by the httpClient on sending or an error 19 | // indicating queue overflow 20 | Err error 21 | 22 | // StatusCode contains the HTTP Status Code returned by the Honeycomb API 23 | // server 24 | StatusCode int 25 | 26 | // Body is the body of the HTTP response from the Honeycomb API server. 27 | Body []byte 28 | 29 | // Duration is a measurement of how long the HTTP request to send an event 30 | // took to process. The actual time it takes libhoney to send an event may 31 | // be longer due to any time the event spends waiting in the queue before 32 | // being sent. 33 | Duration time.Duration 34 | 35 | // Metadata is whatever content you put in the Metadata field of the event for 36 | // which this is the response. It is passed through unmodified. 37 | Metadata interface{} 38 | } 39 | 40 | func (r *Response) UnmarshalJSON(b []byte) error { 41 | aux := struct { 42 | Error string 43 | Status int 44 | }{} 45 | if err := json.Unmarshal(b, &aux); err != nil { 46 | return err 47 | } 48 | r.StatusCode = aux.Status 49 | if aux.Error != "" { 50 | r.Err = errors.New(aux.Error) 51 | } 52 | return nil 53 | } 54 | 55 | func (r *Response) MarshalMsgpack() ([]byte, error) { 56 | aux := struct { 57 | Error string `msgpack:"error,omitempty"` 58 | Status int `msgpack:"status,omitempty"` 59 | }{Status: r.StatusCode} 60 | if r.Err != nil { 61 | aux.Error = r.Err.Error() 62 | } 63 | return msgpack.Marshal(&aux) 64 | } 65 | 66 | func (r *Response) UnmarshalMsgpack(b []byte) error { 67 | aux := struct { 68 | Error string `msgpack:"error"` 69 | Status int `msgpack:"status"` 70 | }{} 71 | if err := msgpack.Unmarshal(b, &aux); err != nil { 72 | return err 73 | } 74 | r.StatusCode = aux.Status 75 | if aux.Error != "" { 76 | r.Err = errors.New(aux.Error) 77 | } 78 | return nil 79 | } 80 | 81 | // writeToResponse adds the response to the response queue. Returns true if it 82 | // dropped the response because it's set to not block on the queue being full 83 | // and the queue was full. 84 | func writeToResponse(responses chan Response, resp Response, block bool) (dropped bool) { 85 | if block { 86 | responses <- resp 87 | } else { 88 | select { 89 | case responses <- resp: 90 | default: 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /transmission/response_test.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/vmihailenco/msgpack/v5" 9 | ) 10 | 11 | // response struct sent from API 12 | type responseInBatch struct { 13 | ErrorStr string `json:"error,omitempty"` 14 | Status int `json:"status,omitempty"` 15 | } 16 | 17 | var ( 18 | srcBatchResponseSingle = []responseInBatch{ 19 | {ErrorStr: "something bad happened", Status: 500}, 20 | } 21 | srcBatchResponseMultiple = []responseInBatch{ 22 | {ErrorStr: "something bad happened", Status: 500}, 23 | {Status: 202}, 24 | } 25 | ) 26 | 27 | func TestUnmarshalJSONResponse(t *testing.T) { 28 | buf := &bytes.Buffer{} 29 | encoder := json.NewEncoder(buf) 30 | encoder.SetEscapeHTML(false) 31 | err := encoder.Encode(srcBatchResponseMultiple) 32 | testOK(t, err) 33 | 34 | var responses []Response 35 | err = json.NewDecoder(buf).Decode(&responses) 36 | testOK(t, err) 37 | 38 | testEquals(t, responses[0].StatusCode, 500) 39 | testEquals(t, responses[0].Err.Error(), "something bad happened") 40 | testEquals(t, responses[1].StatusCode, 202) 41 | testEquals(t, responses[1].Err, nil) 42 | } 43 | 44 | func TestUnmarshalJSONResponseSingle(t *testing.T) { 45 | buf := &bytes.Buffer{} 46 | encoder := json.NewEncoder(buf) 47 | encoder.SetEscapeHTML(false) 48 | err := encoder.Encode(srcBatchResponseSingle) 49 | testOK(t, err) 50 | 51 | var responses []Response 52 | err = json.NewDecoder(buf).Decode(&responses) 53 | testOK(t, err) 54 | 55 | testEquals(t, responses[0].StatusCode, 500) 56 | testEquals(t, responses[0].Err.Error(), "something bad happened") 57 | } 58 | 59 | func TestUnmarshalMsgpackResponse(t *testing.T) { 60 | buf := &bytes.Buffer{} 61 | encoder := msgpack.NewEncoder(buf) 62 | encoder.SetCustomStructTag("json") 63 | err := encoder.Encode(srcBatchResponseMultiple) 64 | testOK(t, err) 65 | 66 | var responses []Response 67 | err = msgpack.NewDecoder(buf).Decode(&responses) 68 | testOK(t, err) 69 | 70 | testEquals(t, responses[0].StatusCode, 500) 71 | testEquals(t, responses[0].Err.Error(), "something bad happened") 72 | testEquals(t, responses[1].StatusCode, 202) 73 | testEquals(t, responses[1].Err, nil) 74 | } 75 | 76 | func TestUnmarshalMsgpackResponseSingle(t *testing.T) { 77 | buf := &bytes.Buffer{} 78 | encoder := msgpack.NewEncoder(buf) 79 | encoder.SetCustomStructTag("json") 80 | err := encoder.Encode(srcBatchResponseSingle) 81 | testOK(t, err) 82 | 83 | var responses []Response 84 | err = msgpack.NewDecoder(buf).Decode(&responses) 85 | testOK(t, err) 86 | 87 | testEquals(t, responses[0].StatusCode, 500) 88 | testEquals(t, responses[0].Err.Error(), "something bad happened") 89 | } 90 | -------------------------------------------------------------------------------- /transmission/sender.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | // Sender is responsible for handling events after Send() is called. 4 | // Implementations of Add() must be safe for concurrent calls. 5 | type Sender interface { 6 | 7 | // Add queues up an event to be sent 8 | Add(ev *Event) 9 | 10 | // Start initializes any background processes necessary to send events 11 | Start() error 12 | 13 | // Stop flushes any pending queues and blocks until everything in flight has 14 | // been sent. Once called, you cannot call Add unless Start has subsequently 15 | // been called. 16 | Stop() error 17 | 18 | // Flush flushes any pending queues and blocks until everything in flight has 19 | // been sent. 20 | Flush() error 21 | 22 | // Responses returns a channel that will contain a single Response for each 23 | // Event added. Note that they may not be in the same order as they came in 24 | TxResponses() chan Response 25 | 26 | // SendResponse adds a Response to the Responses queue. It should be added 27 | // for events handed to libhoney that are dropped before they even make it 28 | // to the Transmission Sender (for example because of sampling) to maintain 29 | // libhoney's guarantee that each event given to it will generate one event 30 | // in the Responses channel. 31 | SendResponse(Response) bool 32 | } 33 | -------------------------------------------------------------------------------- /transmission/transmission.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | // txClient handles the transmission of events to Honeycomb. 4 | // 5 | // Overview 6 | // 7 | // Create a new instance of Client. 8 | // Set any of the public fields for which you want to override the defaults. 9 | // Call Start() to spin up the background goroutines necessary for transmission 10 | // Call Add(Event) to queue an event for transmission 11 | // Ensure Stop() is called to flush all in-flight messages. 12 | 13 | import ( 14 | "bytes" 15 | "encoding/json" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "net/http" 20 | "net/url" 21 | "runtime" 22 | "strings" 23 | "sync" 24 | "time" 25 | 26 | "github.com/facebookgo/muster" 27 | "github.com/honeycombio/libhoney-go/version" 28 | "github.com/klauspost/compress/zstd" 29 | "github.com/vmihailenco/msgpack/v5" 30 | ) 31 | 32 | const ( 33 | // Size limit for a serialized request body sent for a batch. 34 | apiMaxBatchSize int = 5000000 // 5MB 35 | // Size limit for a single serialized event within a batch. 36 | apiEventSizeMax int = 1_000_000 // 1MB, which is the limit for a single event in Shepherd 37 | maxOverflowBatches int = 10 38 | // Default start-to-finish timeout for batch send HTTP requests. 39 | defaultSendTimeout = time.Second * 60 40 | ) 41 | 42 | var ( 43 | // Libhoney's portion of the User-Agent header, e.g. "libhoney/1.2.3" 44 | baseUserAgent = fmt.Sprintf("libhoney-go/%s", version.Version) 45 | // Information about the runtime environment for inclusion in User-Agent 46 | runtimeInfo = fmt.Sprintf("%s (%s/%s)", strings.Replace(runtime.Version(), "go", "go/", 1), runtime.GOOS, runtime.GOARCH) 47 | // The default User-Agent when no additions have been given 48 | defaultUserAgent = fmt.Sprintf("%s %s", baseUserAgent, runtimeInfo) 49 | ) 50 | 51 | // Return a user-agent value including any additions made in the configuration 52 | func fmtUserAgent(addition string) string { 53 | if addition != "" { 54 | return fmt.Sprintf("%s %s %s", baseUserAgent, strings.TrimSpace(addition), runtimeInfo) 55 | } else { 56 | return defaultUserAgent 57 | } 58 | } 59 | 60 | type Honeycomb struct { 61 | // How many events to collect into a batch before sending. A 62 | // batch could be sent before achieving this item limit if the 63 | // BatchTimeout has elapsed since the last batch send. If set 64 | // to zero, batches will only be sent upon reaching the 65 | // BatchTimeout. It is an error for both this and 66 | // the BatchTimeout to be zero. 67 | // Default: 50 (from Config.MaxBatchSize) 68 | MaxBatchSize uint 69 | 70 | // How often to send batches. Events queue up into a batch until 71 | // this time has elapsed or the batch item limit is reached 72 | // (MaxBatchSize), then the batch is sent to Honeycomb API. 73 | // If set to zero, batches will only be sent upon reaching the 74 | // MaxBatchSize item limit. It is an error for both this and 75 | // the MaxBatchSize to be zero. 76 | // Default: 100 milliseconds (from Config.SendFrequency) 77 | BatchTimeout time.Duration 78 | 79 | // The start-to-finish timeout for HTTP requests sending event 80 | // batches to the Honeycomb API. Transmission will retry once 81 | // when receiving a timeout, so total time spent attempting to 82 | // send events could be twice this value. 83 | // Default: 60 seconds. 84 | BatchSendTimeout time.Duration 85 | 86 | // how many batches can be inflight simultaneously 87 | MaxConcurrentBatches uint 88 | 89 | // how many events to allow to pile up 90 | // if not specified, then the work channel becomes blocking 91 | // and attempting to add an event to the queue can fail 92 | PendingWorkCapacity uint 93 | 94 | // whether to block or drop events when the queue fills 95 | BlockOnSend bool 96 | 97 | // whether to block or drop responses when the queue fills 98 | BlockOnResponse bool 99 | 100 | UserAgentAddition string 101 | 102 | // toggles compression when sending batches of events 103 | DisableCompression bool 104 | 105 | // Deprecated, synonymous with DisableCompression 106 | DisableGzipCompression bool 107 | 108 | // set true to send events with msgpack encoding 109 | EnableMsgpackEncoding bool 110 | 111 | batchMaker func() muster.Batch 112 | responses chan Response 113 | 114 | // Transport defines the behavior of the lower layer transport details. 115 | // It is used as the Transport value for the constructed HTTP client that 116 | // sends batches of events. 117 | // Default: http.DefaultTransport 118 | Transport http.RoundTripper 119 | 120 | muster *muster.Client 121 | musterLock sync.RWMutex 122 | 123 | Logger Logger 124 | Metrics Metrics 125 | } 126 | 127 | func (h *Honeycomb) Start() error { 128 | if h.Logger == nil { 129 | h.Logger = &nullLogger{} 130 | } 131 | h.Logger.Printf("default transmission starting") 132 | h.responses = make(chan Response, h.PendingWorkCapacity*2) 133 | if h.Metrics == nil { 134 | h.Metrics = &nullMetrics{} 135 | } 136 | if h.BatchSendTimeout == 0 { 137 | h.BatchSendTimeout = defaultSendTimeout 138 | } 139 | if h.batchMaker == nil { 140 | h.batchMaker = func() muster.Batch { 141 | return &batchAgg{ 142 | userAgentAddition: h.UserAgentAddition, 143 | batches: map[string][]*Event{}, 144 | httpClient: &http.Client{ 145 | Transport: h.Transport, 146 | Timeout: h.BatchSendTimeout, 147 | }, 148 | blockOnResponse: h.BlockOnResponse, 149 | responses: h.responses, 150 | metrics: h.Metrics, 151 | disableCompression: h.DisableGzipCompression || h.DisableCompression, 152 | enableMsgpackEncoding: h.EnableMsgpackEncoding, 153 | logger: h.Logger, 154 | } 155 | } 156 | } 157 | 158 | m := h.createMuster() 159 | h.muster = m 160 | return h.muster.Start() 161 | } 162 | 163 | func (h *Honeycomb) createMuster() *muster.Client { 164 | m := new(muster.Client) 165 | m.MaxBatchSize = h.MaxBatchSize 166 | m.BatchTimeout = h.BatchTimeout 167 | m.MaxConcurrentBatches = h.MaxConcurrentBatches 168 | m.PendingWorkCapacity = h.PendingWorkCapacity 169 | m.BatchMaker = h.batchMaker 170 | return m 171 | } 172 | 173 | func (h *Honeycomb) Stop() error { 174 | h.Logger.Printf("Honeycomb transmission stopping") 175 | err := h.muster.Stop() 176 | close(h.responses) 177 | return err 178 | } 179 | 180 | func (h *Honeycomb) Flush() (err error) { 181 | // There isn't a way to flush a muster.Client directly, so we have to stop 182 | // the old one (which has a side-effect of flushing the data) and make a new 183 | // one. We start the new one and swap it with the old one so that we minimize 184 | // the time we hold the musterLock for. 185 | newMuster := h.createMuster() 186 | err = newMuster.Start() 187 | if err != nil { 188 | return err 189 | } 190 | h.musterLock.Lock() 191 | m := h.muster 192 | h.muster = newMuster 193 | h.musterLock.Unlock() 194 | return m.Stop() 195 | } 196 | 197 | // Add enqueues ev to be sent. If a Flush is in-progress, this will block until 198 | // it completes. Similarly, if BlockOnSend is set and the pending work is more 199 | // than the PendingWorkCapacity, this will block a Flush until more pending 200 | // work can be enqueued. 201 | func (h *Honeycomb) Add(ev *Event) { 202 | if h.tryAdd(ev) { 203 | h.Metrics.Increment("messages_queued") 204 | return 205 | } 206 | h.Metrics.Increment("queue_overflow") 207 | r := Response{ 208 | Err: errors.New("queue overflow"), 209 | Metadata: ev.Metadata, 210 | } 211 | h.Logger.Printf("got response code %d, error %s, and body %s", 212 | r.StatusCode, r.Err, string(r.Body)) 213 | writeToResponse(h.responses, r, h.BlockOnResponse) 214 | } 215 | 216 | // tryAdd attempts to add ev to the underlying muster. It returns false if this 217 | // was unsucessful because the muster queue (muster.Work) is full. 218 | func (h *Honeycomb) tryAdd(ev *Event) bool { 219 | h.musterLock.RLock() 220 | defer h.musterLock.RUnlock() 221 | 222 | // Even though this queue is locked against changing h.Muster, the Work queue length 223 | // could change due to actions on the worker side, so make sure we only measure it once. 224 | qlen := len(h.muster.Work) 225 | h.Logger.Printf("adding event to transmission; queue length %d", qlen) 226 | h.Metrics.Gauge("queue_length", qlen) 227 | 228 | if h.BlockOnSend { 229 | h.muster.Work <- ev 230 | return true 231 | } else { 232 | select { 233 | case h.muster.Work <- ev: 234 | return true 235 | default: 236 | return false 237 | } 238 | } 239 | } 240 | 241 | func (h *Honeycomb) TxResponses() chan Response { 242 | return h.responses 243 | } 244 | 245 | func (h *Honeycomb) SendResponse(r Response) bool { 246 | if h.BlockOnResponse { 247 | h.responses <- r 248 | } else { 249 | select { 250 | case h.responses <- r: 251 | default: 252 | return true 253 | } 254 | } 255 | return false 256 | } 257 | 258 | // batchAgg is a batch aggregator - it's actually collecting what will 259 | // eventually be one or more batches sent to the /1/batch/dataset endpoint. 260 | type batchAgg struct { 261 | // map of batch key to a list of events destined for that batch 262 | batches map[string][]*Event 263 | // Used to reenque events when an initial batch is too large 264 | overflowBatches map[string][]*Event 265 | httpClient *http.Client 266 | blockOnResponse bool 267 | userAgentAddition string 268 | disableCompression bool 269 | enableMsgpackEncoding bool 270 | 271 | responses chan Response 272 | // numEncoded int 273 | 274 | metrics Metrics 275 | 276 | // allows manipulation of the value of "now" for testing 277 | testNower nower 278 | testBlocker *sync.WaitGroup 279 | 280 | logger Logger 281 | } 282 | 283 | // batch is a collection of events that will all be POSTed as one HTTP call 284 | // type batch []*Event 285 | 286 | func (b *batchAgg) Add(ev interface{}) { 287 | // from muster godoc: "The Batch does not need to be safe for concurrent 288 | // access; synchronization will be handled by the Client." 289 | if b.batches == nil { 290 | b.batches = map[string][]*Event{} 291 | } 292 | e := ev.(*Event) 293 | // collect separate buckets of events to send based on the trio of api/wk/ds 294 | // if all three of those match it's safe to send all the events in one batch 295 | key := fmt.Sprintf("%s_%s_%s", e.APIHost, e.APIKey, e.Dataset) 296 | b.batches[key] = append(b.batches[key], e) 297 | } 298 | 299 | func (b *batchAgg) enqueueResponse(resp Response) { 300 | if writeToResponse(b.responses, resp, b.blockOnResponse) { 301 | if b.testBlocker != nil { 302 | b.testBlocker.Done() 303 | } 304 | } 305 | } 306 | 307 | func (b *batchAgg) reenqueueEvents(events []*Event) { 308 | if b.overflowBatches == nil { 309 | b.overflowBatches = make(map[string][]*Event) 310 | } 311 | for _, e := range events { 312 | key := fmt.Sprintf("%s_%s_%s", e.APIHost, e.APIKey, e.Dataset) 313 | b.overflowBatches[key] = append(b.overflowBatches[key], e) 314 | } 315 | } 316 | 317 | func (b *batchAgg) Fire(notifier muster.Notifier) { 318 | defer notifier.Done() 319 | 320 | // send each batchKey's collection of event as a POST to /1/batch/ 321 | // we don't need the batch key anymore; it's done its sorting job 322 | for _, events := range b.batches { 323 | b.fireBatch(events) 324 | } 325 | // The initial batches could have had payloads that were greater than 5MB. 326 | // The remaining events will have overflowed into overflowBatches 327 | // Process these until complete. Overflow batches can also overflow, so we 328 | // have to prepare to process it multiple times 329 | overflowCount := 0 330 | if b.overflowBatches != nil { 331 | for len(b.overflowBatches) > 0 { 332 | // We really shouldn't get here but defensively avoid an endless 333 | // loop of re-enqueued events 334 | if overflowCount > maxOverflowBatches { 335 | break 336 | } 337 | overflowCount++ 338 | // fetch the keys in this map - we can't range over the map 339 | // because it's possible that fireBatch will reenqueue more overflow 340 | // events 341 | keys := make([]string, len(b.overflowBatches)) 342 | i := 0 343 | for k := range b.overflowBatches { 344 | keys[i] = k 345 | i++ 346 | } 347 | 348 | for _, k := range keys { 349 | events := b.overflowBatches[k] 350 | // fireBatch may append more overflow events 351 | // so we want to clear this key before firing the batch 352 | delete(b.overflowBatches, k) 353 | b.fireBatch(events) 354 | } 355 | } 356 | } 357 | } 358 | 359 | type httpError interface { 360 | Timeout() bool 361 | } 362 | 363 | func (b *batchAgg) fireBatch(events []*Event) { 364 | start := time.Now().UTC() 365 | if b.testNower != nil { 366 | start = b.testNower.Now() 367 | } 368 | if len(events) == 0 { 369 | // we managed to create a batch key with no events. odd. move on. 370 | return 371 | } 372 | 373 | var numEncoded int 374 | var encEvs []byte 375 | var contentType string 376 | if b.enableMsgpackEncoding { 377 | contentType = "application/msgpack" 378 | encEvs, numEncoded = b.encodeBatchMsgp(events) 379 | } else { 380 | contentType = "application/json" 381 | encEvs, numEncoded = b.encodeBatchJSON(events) 382 | } 383 | // if we failed to encode any events skip this batch 384 | if numEncoded == 0 { 385 | return 386 | } 387 | 388 | // get some attributes common to this entire batch up front off the first 389 | // valid event (some may be nil) 390 | var apiHost, writeKey, dataset string 391 | for _, ev := range events { 392 | if ev != nil { 393 | apiHost = ev.APIHost 394 | writeKey = ev.APIKey 395 | dataset = ev.Dataset 396 | break 397 | } 398 | } 399 | 400 | url, err := buildRequestURL(apiHost, dataset) 401 | if err != nil { 402 | end := time.Now().UTC() 403 | if b.testNower != nil { 404 | end = b.testNower.Now() 405 | } 406 | dur := end.Sub(start) 407 | b.metrics.Increment("send_errors") 408 | for _, ev := range events { 409 | // Pass the parsing error down responses channel for each event that 410 | // didn't already error during encoding 411 | if ev != nil { 412 | b.enqueueResponse(Response{ 413 | Duration: dur / time.Duration(numEncoded), 414 | Metadata: ev.Metadata, 415 | Err: err, 416 | }) 417 | } 418 | } 419 | return 420 | } 421 | 422 | // One retry allowed for connection timeouts. 423 | var resp *http.Response 424 | for try := 0; try < 2; try++ { 425 | if try > 0 { 426 | b.metrics.Increment("send_retries") 427 | } 428 | 429 | var req *http.Request 430 | reqBody, zipped := buildReqReader(encEvs, !b.disableCompression) 431 | if reader, ok := reqBody.(*pooledReader); ok { 432 | // Pass the underlying bytes.Reader to http.Request so that 433 | // GetBody and ContentLength fields are populated on Request. 434 | // See https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/net/http/request.go;l=898 435 | req, err = http.NewRequest("POST", url, &reader.Reader) 436 | } else { 437 | req, err = http.NewRequest("POST", url, reqBody) 438 | } 439 | req.Header.Set("Content-Type", contentType) 440 | if zipped { 441 | req.Header.Set("Content-Encoding", "zstd") 442 | } 443 | 444 | req.Header.Set("User-Agent", fmtUserAgent(b.userAgentAddition)) 445 | req.Header.Add("X-Honeycomb-Team", writeKey) 446 | // send off batch! 447 | resp, err = b.httpClient.Do(req) 448 | if reader, ok := reqBody.(*pooledReader); ok { 449 | reader.Release() 450 | } 451 | // Handle 429 or 503 with Retry-After 452 | if resp != nil && (resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable) { 453 | retryAfter := resp.Header.Get("Retry-After") 454 | sleepDur := time.Second // default 1s 455 | if retryAfter != "" { 456 | if secs, err := time.ParseDuration(retryAfter + "s"); err == nil { 457 | sleepDur = secs 458 | } else if t, err := http.ParseTime(retryAfter); err == nil { 459 | sleepDur = time.Until(t) 460 | } 461 | } 462 | if sleepDur > 0 && sleepDur < 60*time.Second { 463 | resp.Body.Close() 464 | time.Sleep(sleepDur) 465 | continue // retry in the loop 466 | } 467 | } 468 | 469 | if httpErr, ok := err.(httpError); ok && httpErr.Timeout() { 470 | continue 471 | } 472 | break 473 | } 474 | end := time.Now().UTC() 475 | if b.testNower != nil { 476 | end = b.testNower.Now() 477 | } 478 | dur := end.Sub(start) 479 | 480 | // if the entire HTTP POST failed, send a failed response for every event 481 | if err != nil { 482 | b.metrics.Increment("send_errors") 483 | // Pass the top-level send error down responses channel for each event 484 | // that didn't already error during encoding 485 | b.enqueueErrResponses(err, events, dur/time.Duration(numEncoded)) 486 | // the POST failed so we're done with this batch key's worth of events 487 | return 488 | } 489 | 490 | // ok, the POST succeeded, let's process each individual response 491 | b.metrics.Increment("batches_sent") 492 | b.metrics.Count("messages_sent", numEncoded) 493 | defer resp.Body.Close() 494 | 495 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { 496 | b.metrics.Increment("send_errors") 497 | 498 | var err error 499 | var body []byte 500 | if resp.Header.Get("Content-Type") == "application/msgpack" { 501 | var errorBody interface{} 502 | decoder := msgpack.NewDecoder(resp.Body) 503 | err = decoder.Decode(&errorBody) 504 | if err == nil { 505 | body, err = json.Marshal(&errorBody) 506 | } 507 | } else { 508 | body, err = io.ReadAll(resp.Body) 509 | } 510 | if err != nil { 511 | b.enqueueErrResponses( 512 | fmt.Errorf("Got HTTP error code but couldn't read response body: %v", err), 513 | events, 514 | dur/time.Duration(numEncoded), 515 | ) 516 | return 517 | } 518 | // log if write key was rejected because of invalid Write / API key 519 | if resp.StatusCode == http.StatusUnauthorized { 520 | b.logger.Printf("APIKey '%s' was rejected. Please verify APIKey is correct.", writeKey) 521 | } 522 | for _, ev := range events { 523 | err := fmt.Errorf( 524 | "got unexpected HTTP status %d: %s", 525 | resp.StatusCode, 526 | http.StatusText(resp.StatusCode), 527 | ) 528 | if ev != nil { 529 | b.enqueueResponse(Response{ 530 | StatusCode: resp.StatusCode, 531 | Body: body, 532 | Duration: dur / time.Duration(numEncoded), 533 | Metadata: ev.Metadata, 534 | Err: err, 535 | }) 536 | } 537 | } 538 | return 539 | } 540 | 541 | // decode the responses 542 | var batchResponses []Response 543 | if resp.Header.Get("Content-Type") == "application/msgpack" { 544 | err = msgpack.NewDecoder(resp.Body).Decode(&batchResponses) 545 | } else { 546 | err = json.NewDecoder(resp.Body).Decode(&batchResponses) 547 | } 548 | if err != nil { 549 | // if we can't decode the responses, just error out all of them 550 | b.metrics.Increment("response_decode_errors") 551 | b.enqueueErrResponses(fmt.Errorf( 552 | "got OK HTTP response, but couldn't read response body: %v", err), 553 | events, 554 | dur/time.Duration(numEncoded), 555 | ) 556 | return 557 | } 558 | 559 | // Go through the responses and send them down the queue. If an Event 560 | // triggered a JSON error, it wasn't sent to the server and won't have a 561 | // returned response... so we have to be a bit more careful matching up 562 | // responses with Events. 563 | var eIdx int 564 | for _, resp := range batchResponses { 565 | resp.Duration = dur / time.Duration(numEncoded) 566 | for eIdx < len(events) && events[eIdx] == nil { 567 | b.logger.Printf("incr, eIdx: %d, len(evs): %d", eIdx, len(events)) 568 | eIdx++ 569 | } 570 | if eIdx == len(events) { // just in case 571 | break 572 | } 573 | resp.Metadata = events[eIdx].Metadata 574 | b.enqueueResponse(resp) 575 | eIdx++ 576 | } 577 | } 578 | 579 | // create the JSON for this event list manually so that we can send 580 | // responses down the response queue for any that fail to marshal 581 | func (b *batchAgg) encodeBatchJSON(events []*Event) ([]byte, int) { 582 | // track first vs. rest events for commas 583 | first := true 584 | // track how many we successfully encode for later bookkeeping 585 | var numEncoded int 586 | buf := bytes.Buffer{} 587 | buf.WriteByte('[') 588 | bytesTotal := 1 589 | // ok, we've got our array, let's populate it with JSON events 590 | for i, ev := range events { 591 | evByt, err := json.Marshal(ev) 592 | // check all our errors first in case we need to skip batching this event 593 | if err != nil { 594 | b.enqueueResponse(Response{ 595 | Err: err, 596 | Metadata: ev.Metadata, 597 | }) 598 | // nil out the invalid Event so we can line up sent Events with server 599 | // responses if needed. don't delete to preserve slice length. 600 | events[i] = nil 601 | continue 602 | } 603 | // if the event is too large to ever send, add an error to the queue 604 | if len(evByt) > apiEventSizeMax { 605 | // if this event happens to have a `name` or `service.name` field, include them for easier debugging 606 | extraInfo := getExceedsMaxEventSizeExtraInfo(ev) 607 | // log the error and enqueue a response 608 | b.enqueueResponse(Response{ 609 | Err: fmt.Errorf("event exceeds max event size of %d bytes, API will not accept this event.%s", apiEventSizeMax, extraInfo), 610 | Metadata: ev.Metadata, 611 | }) 612 | events[i] = nil 613 | continue 614 | } 615 | 616 | bytesTotal += len(evByt) 617 | // count for the trailing ] 618 | if bytesTotal+1 > apiMaxBatchSize { 619 | b.reenqueueEvents(events[i:]) 620 | break 621 | } 622 | 623 | // ok, we have valid JSON and it'll fit in this batch; add ourselves a comma and the next value 624 | if !first { 625 | buf.WriteByte(',') 626 | bytesTotal++ 627 | } 628 | first = false 629 | buf.Write(evByt) 630 | numEncoded++ 631 | } 632 | buf.WriteByte(']') 633 | return buf.Bytes(), numEncoded 634 | } 635 | 636 | func (b *batchAgg) encodeBatchMsgp(events []*Event) ([]byte, int) { 637 | // Msgpack arrays need to be prefixed with the number of elements, but we 638 | // don't know in advance how many we'll encode, because the msgpack lib 639 | // doesn't do size estimation. Also, the array header is of variable size 640 | // based on array length, so we'll need to do some []byte shenanigans at 641 | // at the end of this to properly prepend the header. 642 | 643 | var arrayHeader [5]byte 644 | var numEncoded int 645 | var buf bytes.Buffer 646 | 647 | // Prepend space for largest possible msgpack array header. 648 | buf.Write(arrayHeader[:]) 649 | for i, ev := range events { 650 | evByt, err := msgpack.Marshal(ev) 651 | if err != nil { 652 | b.enqueueResponse(Response{ 653 | Err: err, 654 | Metadata: ev.Metadata, 655 | }) 656 | // nil out the invalid Event so we can line up sent Events with server 657 | // responses if needed. don't delete to preserve slice length. 658 | events[i] = nil 659 | continue 660 | } 661 | // if the event is too large to ever send, add an error to the queue 662 | if len(evByt) > apiEventSizeMax { 663 | // if this event happens to have a `name` or `service.name` field, include them for easier debugging 664 | extraInfo := getExceedsMaxEventSizeExtraInfo(ev) 665 | // log the error and enqueue a response 666 | b.enqueueResponse(Response{ 667 | Err: fmt.Errorf("event exceeds max event size of %d bytes, API will not accept this event.%s", apiEventSizeMax, extraInfo), 668 | Metadata: ev.Metadata, 669 | }) 670 | events[i] = nil 671 | continue 672 | } 673 | 674 | if buf.Len()+len(evByt) > apiMaxBatchSize { 675 | b.reenqueueEvents(events[i:]) 676 | break 677 | } 678 | 679 | buf.Write(evByt) 680 | numEncoded++ 681 | } 682 | 683 | headerBuf := bytes.NewBuffer(arrayHeader[:0]) 684 | msgpack.NewEncoder(headerBuf).EncodeArrayLen(numEncoded) 685 | 686 | // Shenanigans. Chop off leading bytes we don't need, then copy in header. 687 | byts := buf.Bytes()[len(arrayHeader)-headerBuf.Len():] 688 | copy(byts, headerBuf.Bytes()) 689 | 690 | return byts, numEncoded 691 | } 692 | 693 | func (b *batchAgg) enqueueErrResponses(err error, events []*Event, duration time.Duration) { 694 | for _, ev := range events { 695 | if ev != nil { 696 | b.enqueueResponse(Response{ 697 | Err: err, 698 | Duration: duration, 699 | Metadata: ev.Metadata, 700 | }) 701 | } 702 | } 703 | } 704 | 705 | var zstdBufferPool sync.Pool 706 | 707 | type pooledReader struct { 708 | bytes.Reader 709 | buf []byte 710 | } 711 | 712 | func (r *pooledReader) Release() error { 713 | // Ensure further attempts to read will return io.EOF 714 | r.Reset(nil) 715 | // Then reset and give up ownership of the buffer. 716 | zstdBufferPool.Put(r.buf[:0]) 717 | r.buf = nil 718 | return nil 719 | } 720 | 721 | // Instantiating a new encoder is expensive, so use a global one. 722 | // EncodeAll() is concurrency-safe. 723 | var zstdEncoder *zstd.Encoder 724 | 725 | func init() { 726 | var err error 727 | zstdEncoder, err = zstd.NewWriter( 728 | nil, 729 | // Compression level 2 gives a good balance of speed and compression. 730 | zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(2)), 731 | // zstd allocates 2 * GOMAXPROCS * window size, so use a small window. 732 | // Most honeycomb messages are smaller than this. 733 | zstd.WithWindowSize(1<<16), 734 | ) 735 | if err != nil { 736 | panic(err) 737 | } 738 | } 739 | 740 | // buildReqReader returns an io.Reader and a boolean, indicating whether or not 741 | // the underlying bytes.Reader is compressed. 742 | func buildReqReader(jsonEncoded []byte, compress bool) (io.Reader, bool) { 743 | if compress { 744 | var buf []byte 745 | if found, ok := zstdBufferPool.Get().([]byte); ok { 746 | buf = found[:0] 747 | } 748 | 749 | buf = zstdEncoder.EncodeAll(jsonEncoded, buf) 750 | reader := pooledReader{ 751 | buf: buf, 752 | } 753 | reader.Reset(reader.buf) 754 | return &reader, true 755 | } 756 | return bytes.NewReader(jsonEncoded), false 757 | } 758 | 759 | // nower to make testing easier 760 | type nower interface { 761 | Now() time.Time 762 | } 763 | 764 | func buildRequestURL(apiHost, dataset string) (string, error) { 765 | return url.JoinPath(apiHost, "/1/batch", url.PathEscape(dataset)) 766 | } 767 | 768 | func getExceedsMaxEventSizeExtraInfo(ev *Event) string { 769 | var extraInfo string 770 | if evName, ok := ev.Data["name"]; ok { 771 | extraInfo = fmt.Sprintf(" Name: \"%v\"", evName) 772 | } 773 | if evServiceName, ok := ev.Data["service.name"]; ok { 774 | extraInfo = fmt.Sprintf("%s Service Name: \"%v\"", extraInfo, evServiceName) 775 | } 776 | return extraInfo 777 | } 778 | -------------------------------------------------------------------------------- /transmission/writer.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // WriterSender implements the Sender interface by marshalling events to JSON 12 | // and writing to STDOUT, or to the writer W if one is specified. 13 | type WriterSender struct { 14 | W io.Writer 15 | 16 | BlockOnResponses bool 17 | ResponseQueueSize uint 18 | responses chan Response 19 | 20 | sync.Mutex 21 | } 22 | 23 | func (w *WriterSender) Start() error { 24 | if w.ResponseQueueSize == 0 { 25 | w.ResponseQueueSize = 100 26 | } 27 | w.responses = make(chan Response, w.ResponseQueueSize) 28 | return nil 29 | } 30 | 31 | func (w *WriterSender) Stop() error { return nil } 32 | 33 | func (w *WriterSender) Flush() error { return nil } 34 | 35 | func (w *WriterSender) Add(ev *Event) { 36 | var m []byte 37 | tPointer := &(ev.Timestamp) 38 | if ev.Timestamp.IsZero() { 39 | tPointer = nil 40 | } 41 | 42 | // don't include sample rate if it's 1; this is the default 43 | sampleRate := ev.SampleRate 44 | if sampleRate == 1 { 45 | sampleRate = 0 46 | } 47 | 48 | m, _ = json.Marshal(struct { 49 | Data map[string]interface{} `json:"data"` 50 | SampleRate uint `json:"samplerate,omitempty"` 51 | Timestamp *time.Time `json:"time,omitempty"` 52 | Dataset string `json:"dataset,omitempty"` 53 | }{ev.Data, sampleRate, tPointer, ev.Dataset}) 54 | m = append(m, '\n') 55 | 56 | w.Lock() 57 | defer w.Unlock() 58 | if w.W == nil { 59 | w.W = os.Stdout 60 | } 61 | w.W.Write(m) 62 | resp := Response{ 63 | // TODO what makes sense to set in the response here? 64 | Metadata: ev.Metadata, 65 | } 66 | w.SendResponse(resp) 67 | } 68 | 69 | func (w *WriterSender) TxResponses() chan Response { 70 | return w.responses 71 | } 72 | 73 | func (w *WriterSender) SendResponse(r Response) bool { 74 | if w.BlockOnResponses { 75 | w.responses <- r 76 | } else { 77 | select { 78 | case w.responses <- r: 79 | default: 80 | return true 81 | } 82 | } 83 | return false 84 | } 85 | -------------------------------------------------------------------------------- /transmission/writer_test.go: -------------------------------------------------------------------------------- 1 | package transmission 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestWriterSenderStart(t *testing.T) { 14 | // Starting a writer does nothing but creates the responses channel. 15 | w := &WriterSender{} 16 | assert.Nil(t, w.responses, "before starting, responses should be nil") 17 | w.Start() 18 | assert.NotNil(t, w.responses, "after starting, responses should be a real channel") 19 | } 20 | 21 | type mockIO struct { 22 | written []byte 23 | } 24 | 25 | func (m *mockIO) Write(p []byte) (int, error) { 26 | m.written = p 27 | return 0, nil 28 | } 29 | 30 | func TestWriterSender(t *testing.T) { 31 | buf := bytes.NewBuffer(nil) 32 | writer := WriterSender{ 33 | W: buf, 34 | } 35 | ev := Event{ 36 | Timestamp: time.Time{}.Add(time.Second), 37 | SampleRate: 2, 38 | Dataset: "dataset", 39 | Data: map[string]interface{}{"key": "val"}, 40 | } 41 | 42 | writer.Add(&ev) 43 | testEquals( 44 | t, 45 | strings.TrimSpace(buf.String()), 46 | `{"data":{"key":"val"},"samplerate":2,"time":"0001-01-01T00:00:01Z","dataset":"dataset"}`, 47 | ) 48 | } 49 | 50 | func TestWriterSenderAdd(t *testing.T) { 51 | // Adding an event to a WriterSender should write the event in json form to 52 | // the writer that is configured on the output. It should also generate one 53 | // Response per event Added. 54 | mocked := &mockIO{} 55 | w := &WriterSender{ 56 | W: mocked, 57 | ResponseQueueSize: 1, 58 | } 59 | w.Start() 60 | 61 | ev := &Event{ 62 | Metadata: "adder", 63 | Data: map[string]interface{}{"foo": "bar"}, 64 | } 65 | w.Add(ev) 66 | assert.Equal(t, "{\"data\":{\"foo\":\"bar\"}}\n", string(mocked.written), "emty event should still make JSON") 67 | 68 | select { 69 | case res := <-w.TxResponses(): 70 | assert.Equal(t, ev.Metadata, res.Metadata, "Response off the queue should have the right metadata") 71 | case <-time.After(1 * time.Second): 72 | t.Error("timed out waiting on channel") 73 | } 74 | } 75 | 76 | func TestWriterSenderAddingResponsesNonblocking(t *testing.T) { 77 | // using the public SendRespanse method should add the response to the queue 78 | // while honoring the block setting 79 | w := &WriterSender{ 80 | W: io.Discard, 81 | BlockOnResponses: false, 82 | ResponseQueueSize: 1, 83 | } 84 | w.Start() 85 | ev := &Event{ 86 | Metadata: "adder", 87 | } 88 | 89 | // one ev should get one response 90 | w.Add(ev) 91 | select { 92 | case res := <-w.TxResponses(): 93 | assert.Equal(t, ev.Metadata, res.Metadata, "Response off the queue should have the right metadata") 94 | case <-time.After(1 * time.Second): 95 | t.Error("timed out waiting on channel") 96 | } 97 | 98 | // three evs should get one response (because the response queue size is 99 | // one; the second and third should be dropped) 100 | w.Add(ev) 101 | w.Add(ev) 102 | w.Add(ev) 103 | 104 | select { 105 | case res := <-w.TxResponses(): 106 | assert.Equal(t, ev.Metadata, res.Metadata, "Response off the queue should have the right metadata") 107 | case <-time.After(1 * time.Second): 108 | t.Error("timed out waiting on channel") 109 | } 110 | 111 | select { 112 | case res := <-w.TxResponses(): 113 | t.Errorf("shouldn't have gotten a second response, but got %+v", res) 114 | default: 115 | // good, we shouldn't have had a second one. 116 | } 117 | 118 | } 119 | 120 | func TestWriterSenderAddingResponsesBlocking(t *testing.T) { 121 | // this test has a few timeout checks. don't wait to run other tests. 122 | t.Parallel() 123 | // using the public SendRespanse method should add the response to the queue 124 | // while honoring the block setting 125 | w := &WriterSender{ 126 | W: io.Discard, 127 | BlockOnResponses: true, 128 | ResponseQueueSize: 1, 129 | } 130 | w.Start() 131 | ev := &Event{ 132 | Metadata: "adder", 133 | } 134 | 135 | happenings := make(chan interface{}, 2) 136 | 137 | // push one thing in the queue. This should fill it up; the next call should block. 138 | w.Add(ev) 139 | go func() { 140 | // indicate that we've successfully started the goroutine 141 | happenings <- struct{}{} 142 | // push a second event in the responses queue. This should block 143 | w.Add(ev) 144 | // indicate that we've gotten past the blocking portion 145 | happenings <- struct{}{} 146 | }() 147 | 148 | // Ok, now we have a situation where: 149 | // * we can block until something comes in to happeninings, to confirm we started the goroutine 150 | // * we can wait a bit on happenings to verify that we have _not_ gotten past w.Add() 151 | // * then we unblock the responses channel by reading the thing off it 152 | // * then verify that happenings has gotten its second message and the goroutine has gotten past the block 153 | 154 | // block until we have proof the goroutine has run 155 | select { 156 | case <-happenings: 157 | // cool, the goroutine has started. 158 | case <-time.After(1 * time.Second): 159 | // bummer, the goroutine never started. 160 | t.Error("timed out waiting for the blocking Add to start") 161 | } 162 | 163 | // verify we have _not_ gotten a second message on the happenings channel, 164 | // meaning that the Add didn't block 165 | select { 166 | case <-happenings: 167 | t.Error("w.Add() didn't block on the response channel") 168 | case <-time.After(1 * time.Second): 169 | // ok, it took a second to make sure, but we can continue now. 170 | } 171 | 172 | // unblock the response queue by reading the event off it 173 | select { 174 | case <-w.TxResponses(): 175 | // good, this is expected 176 | case <-time.After(1 * time.Second): 177 | // ehh... there was supposed to be something there. 178 | t.Error("timed out waiting for the async w.Add to happen") 179 | } 180 | 181 | // now verify that we get through the second happenings to confirm that we're past the blocked Add 182 | select { 183 | case <-happenings: 184 | // yay 185 | case <-time.After(1 * time.Second): 186 | t.Error("timed out waiting for the second happening. we must still be blocked on the response queue") 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ( 4 | Version string = "1.25.0" 5 | ) 6 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package libhoney 2 | 3 | import ( 4 | "github.com/honeycombio/libhoney-go/transmission" 5 | ) 6 | 7 | // WriterOutput implements the Output interface and passes it along to the 8 | // transmission.WriterSender. 9 | // 10 | // Deprecated: Please use the transmission.WriterSender directly instead. 11 | // It is provided here for backwards compatibility and will be removed eventually. 12 | type WriterOutput struct { 13 | transmission.WriterSender 14 | } 15 | 16 | func (w *WriterOutput) Add(ev *Event) { 17 | transEv := &transmission.Event{ 18 | APIHost: ev.APIHost, 19 | APIKey: ev.WriteKey, 20 | Dataset: ev.Dataset, 21 | SampleRate: ev.SampleRate, 22 | Timestamp: ev.Timestamp, 23 | Metadata: ev.Metadata, 24 | Data: ev.data, 25 | } 26 | w.WriterSender.Add(transEv) 27 | } 28 | 29 | // DiscardWriter implements the Output interface and drops all events. 30 | // 31 | // Deprecated: Please use the transmission.DiscardSender directly instead. 32 | // It is provided here for backwards compatibility and will be removed eventually. 33 | type DiscardOutput struct { 34 | WriterOutput 35 | } 36 | 37 | func (d *DiscardOutput) Add(ev *Event) {} 38 | --------------------------------------------------------------------------------