├── .arcconfig ├── .bumpversion.cfg ├── .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 ├── COPYRIGHT ├── DEVELOPMENT.md ├── LICENSE ├── Makefile ├── NOTICE ├── OSSMETADATA ├── README.md ├── RELEASING.md ├── SECURITY.md ├── SUPPORT.md ├── examples ├── README.md ├── django_dynamic_fields │ ├── django_dynamic_fields │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── my_app │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── honey_middleware.py │ │ ├── urls.py │ │ └── views.py │ ├── poetry.lock │ └── pyproject.toml ├── django_response_time │ ├── django_response_time │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── my_app │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── honey_middleware.py │ │ ├── urls.py │ │ └── views.py │ ├── poetry.lock │ └── pyproject.toml ├── django_simple │ ├── django_simple │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── my_app │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── honey_middleware.py │ │ ├── urls.py │ │ └── views.py │ ├── poetry.lock │ └── pyproject.toml ├── factorial │ ├── FactorialDockerfile │ ├── docker-compose.yml │ ├── example.py │ ├── example_tornado.py │ ├── poetry.lock │ └── pyproject.toml └── flask │ ├── Dockerfile │ ├── README.md │ ├── app.py │ ├── docker-compose.yml │ ├── poetry.lock │ └── pyproject.toml ├── libhoney ├── __init__.py ├── builder.py ├── client.py ├── errors.py ├── event.py ├── fields.py ├── internal.py ├── state.py ├── test_client.py ├── test_libhoney.py ├── test_tornado.py ├── test_transmission.py ├── transmission.py └── version.py ├── poetry.lock ├── push_docs.sh ├── pylint.rc └── pyproject.toml /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "phabricator.uri" : "https://hound.phacility.com/" 3 | } 4 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | commit = True 3 | tag = False 4 | current_version = 2.4.0 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{release}{build} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:part:release] 11 | optional_value = prod 12 | first_value = dev 13 | values = 14 | dev 15 | prod 16 | 17 | [bumpversion:part:build] 18 | 19 | [bumpversion:file:libhoney/version.py] 20 | 21 | [bumpversion:file:pyproject.toml] 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | python: 5 | docker: 6 | - image: cimg/python:3.8 7 | github: 8 | docker: 9 | - image: cibuilds/github:0.13.0 10 | 11 | commands: 12 | setup: 13 | steps: 14 | - checkout 15 | - run: 16 | name: poetry_install 17 | command: poetry install --no-root --no-ansi 18 | run_lint: 19 | steps: 20 | - run: 21 | name: run_lint 22 | command: poetry run pylint --rcfile=pylint.rc libhoney 23 | - run: 24 | name: run_pycodestyle 25 | command: poetry run pycodestyle libhoney --max-line-length=140 26 | run_tests: 27 | steps: 28 | - run: 29 | name: run_tests 30 | command: poetry run coverage run -m unittest discover -v 31 | run_coverage: 32 | steps: 33 | - run: 34 | name: coverage_report 35 | command: poetry run coverage report --include="libhoney/*" 36 | - run: 37 | name: coverage_html 38 | command: poetry run coverage html --include="libhoney/*" 39 | 40 | runtests: 41 | steps: 42 | - setup 43 | - run_lint 44 | - run_tests 45 | - run_coverage 46 | - store_artifacts: 47 | path: htmlcov 48 | build: 49 | steps: 50 | - setup 51 | - run: mkdir -p ~/artifacts 52 | - run: 53 | name: poetry_build 54 | command: poetry build 55 | - run: 56 | name: copy_binaries 57 | command: cp dist/* ~/artifacts 58 | - persist_to_workspace: 59 | root: ~/ 60 | paths: 61 | - artifacts 62 | - store_artifacts: 63 | path: ~/artifacts 64 | 65 | makesmoke: 66 | steps: 67 | - checkout 68 | - attach_workspace: 69 | at: ./ 70 | - run: 71 | name: Spin up example in Docker 72 | command: make smoke 73 | - run: 74 | name: Spin down example 75 | command: make unsmoke 76 | 77 | publish_github: 78 | steps: 79 | - attach_workspace: 80 | at: ~/ 81 | - run: 82 | name: "Artifacts being published" 83 | command: | 84 | echo "about to publish to tag ${CIRCLE_TAG}" 85 | ls -l ~/artifacts/* 86 | - run: 87 | name: ghr_draft 88 | command: ghr -draft -n ${CIRCLE_TAG} -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} ~/artifacts 89 | 90 | publish_pypi: 91 | steps: 92 | - setup 93 | - run: 94 | name: poetry_publish 95 | command: poetry publish --build -u '__token__' -p ${PYPI_TOKEN} 96 | 97 | # required as all of the jobs need to have a tag filter for some reason 98 | tag_filters: &tag_filters 99 | filters: 100 | tags: 101 | only: /.*/ 102 | 103 | jobs: 104 | test: 105 | parameters: 106 | python-version: 107 | type: string 108 | docker: 109 | - image: cimg/python:<> 110 | steps: 111 | - runtests 112 | build: 113 | executor: python 114 | steps: 115 | - build 116 | smoke_test: 117 | machine: 118 | image: ubuntu-2204:2024.01.1 119 | steps: 120 | - makesmoke 121 | 122 | publish_github: 123 | executor: github 124 | steps: 125 | - publish_github 126 | publish_pypi: 127 | executor: python 128 | steps: 129 | - publish_pypi 130 | 131 | workflows: 132 | version: 2 133 | nightly: 134 | triggers: 135 | - schedule: 136 | cron: "0 0 * * *" 137 | filters: 138 | branches: 139 | only: 140 | - main 141 | jobs: 142 | - test: 143 | matrix: 144 | parameters: 145 | python-version: ["3.7", "3.8", "3.9", "3.10"] 146 | filters: 147 | tags: 148 | only: /.*/ 149 | - build: 150 | filters: 151 | tags: 152 | only: /.*/ 153 | requires: 154 | - test 155 | 156 | build_libhoney: 157 | jobs: 158 | - test: 159 | matrix: 160 | parameters: 161 | python-version: ["3.7", "3.8", "3.9", "3.10"] 162 | filters: 163 | tags: 164 | only: /.*/ 165 | - build: 166 | filters: 167 | tags: 168 | only: /.*/ 169 | requires: 170 | - test 171 | - smoke_test: 172 | filters: 173 | tags: 174 | only: /.*/ 175 | requires: 176 | - build 177 | - publish_github: 178 | context: Honeycomb Secrets for Public Repos 179 | requires: 180 | - build 181 | filters: 182 | tags: 183 | only: /v[0-9].*/ 184 | branches: 185 | ignore: /.*/ 186 | - publish_pypi: 187 | context: Honeycomb Secrets for Public Repos 188 | requires: 189 | - build 190 | filters: 191 | tags: 192 | only: /v[0-9].*/ 193 | branches: 194 | ignore: /.*/ 195 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{py}] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | 10 | [*.md] 11 | 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 | - Python: 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 | - Python: 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: "pip" # 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/telemetry-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 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | db.sqlite3 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | #Ipython Notebook 60 | .ipynb_checkpoints 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # libhoney-py Changelog 2 | 3 | ## 2.4.0 2024-03-06 4 | 5 | ### Improvements 6 | 7 | - feat: support classic-flavored ingest keys (#231) | [@jharley](https://github.com/jharley) 8 | 9 | ### Maintenance 10 | 11 | - maint: Update poetry publish to use API token (#230) | [Mike Goldsmith](https://github.com/MikeGoldsmith) 12 | - maint: update codeowners to pipeline-team (#229) | [Jamie Danielson](https://github.com/JamieDanielson) 13 | - maint: update codeowners to pipeline (#228) | [Jamie Danielson](https://github.com/JamieDanielson) 14 | - build(deps): bump certifi from 2022.12.7 to 2023.7.22 in /examples/flask (#207) | [dependabot[bot]](https://github.com/dependabot) 15 | - build(deps): bump certifi from 2022.12.7 to 2023.7.22 in /examples/factorial (#206) | [dependabot[bot]](https://github.com/dependabot) 16 | - maint(deps-dev): bump requests-mock from 1.10.0 to 1.11.0 (#200) | [dependabot[bot]](https://github.com/dependabot) 17 | - maint(deps-dev): bump autopep8 from 1.7.0 to 2.0.2 (#178) | [dependabot[bot]](https://github.com/dependabot) 18 | - maint(deps): bump urllib3 from 1.26.13 to 2.0.4 (#205) | [dependabot[bot]](https://github.com/dependabot) 19 | - maint: Bump statsd from 3.3.0 to 4.0.1 (#153) | [dependabot[bot]](https://github.com/dependabot) 20 | - build(deps): bump django from 4.1.9 to 4.1.10 in /examples/django_simple (#202) | [dependabot[bot]](https://github.com/dependabot) 21 | - build(deps): bump django from 4.1.9 to 4.1.10 in /examples/django_dynamic_fields (#203) | [dependabot[bot]](https://github.com/dependabot) 22 | - build(deps): bump django from 4.1.9 to 4.1.10 in /examples/django_response_time (#204) | [dependabot[bot]](https://github.com/dependabot) 23 | - docs: add development.md (#199) | [Vera Reynolds](https://github.com/vreynolds) 24 | - maint(deps-dev): bump coverage from 7.2.5 to 7.2.7 (#197) | [dependabot[bot]](https://github.com/dependabot) 25 | - Bump requests from 2.28.1 to 2.31.0 in /examples/django_dynamic_fields (#191) | [dependabot[bot]](https://github.com/dependabot) 26 | - Bump requests from 2.28.1 to 2.31.0 in /examples/django_simple (#192) | [dependabot[bot]](https://github.com/dependabot) 27 | - Bump requests from 2.28.1 to 2.31.0 in /examples/django_response_time (#193) | [dependabot[bot]](https://github.com/dependabot) 28 | - Bump requests from 2.28.1 to 2.31.0 in /examples/factorial (#194) | [dependabot[bot]](https://github.com/dependabot) 29 | - Bump requests from 2.28.1 to 2.31.0 in /examples/flask (#195) | [dependabot[bot]](https://github.com/dependabot) 30 | - maint(deps): bump requests from 2.30.0 to 2.31.0 (#196) | [dependabot[bot]](https://github.com/dependabot) 31 | - maint(deps): bump requests from 2.28.2 to 2.30.0 (#190) | [dependabot[bot]](https://github.com/dependabot) 32 | - maint(deps-dev): bump coverage from 7.2.2 to 7.2.5 (#184) | [dependabot[bot]](https://github.com/dependabot) 33 | - Bump django from 4.1.7 to 4.1.9 in /examples/django_response_time (#187) | [dependabot[bot]](https://github.com/dependabot) 34 | - Bump django from 4.1.7 to 4.1.9 in /examples/django_dynamic_fields (#189) | [dependabot[bot]](https://github.com/dependabot) 35 | - Bump django from 4.1.7 to 4.1.9 in /examples/django_simple (#188) | [dependabot[bot]](https://github.com/dependabot) 36 | - Bump flask from 2.2.2 to 2.3.2 in /examples/flask (#186) | [dependabot[bot]](https://github.com/dependabot) 37 | - Bump sqlparse from 0.4.2 to 0.4.4 in /examples/django_dynamic_fields (#182) | [dependabot[bot]](https://github.com/dependabot) 38 | - Bump sqlparse from 0.4.2 to 0.4.4 in /examples/django_response_time (#181) | [dependabot[bot]](https://github.com/dependabot) 39 | - Bump sqlparse from 0.4.2 to 0.4.4 in /examples/django_simple (#180) | [dependabot[bot]](https://github.com/dependabot) 40 | - maint(deps): bump requests from 2.28.1 to 2.28.2 (#168) | [dependabot[bot]](https://github.com/dependabot) 41 | - maint(deps-dev): bump coverage from 6.4.4 to 7.2.2 (#179) | [dependabot[bot]](https://github.com/dependabot) 42 | - Bump werkzeug from 2.2.2 to 2.2.3 in /examples/flask (#176) | [dependabot[bot]](https://github.com/dependabot) 43 | - Bump django from 4.1.6 to 4.1.7 in /examples/django_simple (#175) | [dependabot[bot]](https://github.com/dependabot) 44 | - Bump django from 4.1.6 to 4.1.7 in /examples/django_dynamic_fields (#174) | [dependabot[bot]](https://github.com/dependabot) 45 | - Bump django from 4.1.6 to 4.1.7 in /examples/django_response_time (#173) | [dependabot[bot]](https://github.com/dependabot) 46 | - Bump django from 4.1.2 to 4.1.6 in /examples/django_response_time (#172) | [dependabot[bot]](https://github.com/dependabot) 47 | - Bump django from 4.1.2 to 4.1.6 in /examples/django_simple (#171) | [dependabot[bot]](https://github.com/dependabot) 48 | - Bump django from 4.1.2 to 4.1.6 in /examples/django_dynamic_fields (#170) | [dependabot[bot]](https://github.com/dependabot) 49 | - maint: dockerize example and run in ci (#167) | [Jamie Danielson](https://github.com/JamieDanielson) 50 | - maint: remove buildevents from circle (#166) | [Jamie Danielson](https://github.com/JamieDanielson) 51 | - maint: Bump pycodestyle from 2.9.1 to 2.10.0 (#154) | [dependabot[bot]](https://github.com/dependabot) 52 | - maint: Bump urllib3 from 1.26.12 to 1.26.13 (#155) | [dependabot[bot]](https://github.com/dependabot) 53 | - Bump certifi from 2022.6.15.1 to 2022.12.7 (#160) | [dependabot[bot]](https://github.com/dependabot) 54 | - fix: update dependabot tiltes to semantic commit format (#164) | [Purvi Kanal](https://github.com/pkanal) 55 | - maint: fix build (#162) | [Vera Reynolds](https://github.com/vreynolds) 56 | - Bump certifi from 2022.6.15 to 2022.12.7 in /examples/django_dynamic_fields (#158) | [dependabot[bot]](https://github.com/dependabot) 57 | - Bump certifi from 2022.6.15 to 2022.12.7 in /examples/django_response_time (#157) | [dependabot[bot]](https://github.com/dependabot) 58 | - Bump certifi from 2022.6.15 to 2022.12.7 in /examples/django_simple (#156) | [dependabot[bot]](https://github.com/dependabot) 59 | - Bump certifi from 2022.6.15 to 2022.12.7 in /examples/factorial (#159) | [dependabot[bot]](https://github.com/dependabot) 60 | - Bump certifi from 2022.9.24 to 2022.12.7 in /examples/flask (#161) | [dependabot[bot]](https://github.com/dependabot) 61 | - ci: update validate PR title workflow (#152) | [Purvi Kanal](https://github.com/pkanal) 62 | - maint: update buildevents orb and python convenience image (#150) | [Jamie Danielson](https://github.com/JamieDanielson) 63 | - ci: validate PR title (#151) | [Purvi Kanal](https://github.com/pkanal) 64 | - maint: add flask example (#148) | [Vera Reynolds](https://github.com/vreynolds) 65 | - maint: delete workflows for old board (#144) | [Vera Reynolds](https://github.com/vreynolds) 66 | - Bump django from 4.1 to 4.1.2 in /examples/django_simple (#146) | [dependabot[bot]](https://github.com/dependabot) 67 | - Bump django from 4.1 to 4.1.2 in /examples/django_response_time (#147) | [dependabot[bot]](https://github.com/dependabot) 68 | - Bump django from 4.1 to 4.1.2 in /examples/django_dynamic_fields (#145) | [dependabot[bot]](https://github.com/dependabot) 69 | - Be more specific on required versions of urllib3 #141 (#142) | [Emily Ashley](https://github.com/emilyashley) 70 | - maint: add release file (#143) | [Vera Reynolds](https://github.com/vreynolds) 71 | - maint: add new project workflow (#140) | [Vera Reynolds](https://github.com/vreynolds) 72 | - update releasing notes (#138) | [Emily Ashley](https://github.com/emilyashley) 73 | 74 | ## 2.3.0 2022-09-09 75 | 76 | ⚠️ Minimum supported Python version is now 3.7 ⚠️ 77 | ### Maintenance 78 | 79 | - Drop Python 3.5, 3.6 Support (#136)| [@emilyashley](https://github.com/emilyashley) 80 | 81 | ## 2.2.0 2022-09-02 82 | 83 | ### Improvements 84 | 85 | - Add Python version to the user-agent header (#131) | [@emilyashley](https://github.com/emilyashley) 86 | - Add Tornado version to user-agent header if sent via Tornado. (#128) | [@emilyashley](https://github.com/emilyashley) 87 | - Retry once on send timeout (#126) | [@emilyashley](https://github.com/emilyashley) 88 | 89 | ### Maintenance 90 | 91 | - Bump requests-mock from 1.9.3 to 1.10.0 (#132) | [dependabot](https://github.com/dependabot) 92 | - Bump django from 4.0.6 to 4.0.7 in /examples (#123, #124, #125) | [dependabot](https://github.com/dependabot) 93 | 94 | ## 2.1.1 2022-07-13 95 | 96 | ### Improvements 97 | 98 | - Lazy load requests in Transmission class (#121) | [@danvendia](https://github.com/danvendia) 99 | - Remove six and mock dependencies. (#117) | [@iurisilvio](https://github.com/iurisilvio) 100 | 101 | ### Maintenance 102 | 103 | - Bump django from 4.0.2 to 4.0.6 in /examples/django_response_time (#115,#118) | [dependabot](https://github.com/dependabot) 104 | - Bump django from 4.0.2 to 4.0.6 in /examples/django_dynamic_fields (#114,#120) | [dependabot](https://github.com/dependabot) 105 | - Bump django from 4.0.2 to 4.0.6 in /examples/django_simple (#113,#119) | [dependabot](https://github.com/dependabot) 106 | 107 | ## 2.1.0 2022-04-08 108 | 109 | ### Added 110 | 111 | - feat: Add environment & services support (#111) | [@JamieDanielson](https://github.com/JamieDanielson) 112 | 113 | ### Maintenance 114 | 115 | - Bump django from 4.0.1 to 4.0.2 in /examples/django_response_time (#108) 116 | - Bump django from 4.0.1 to 4.0.2 in /examples/django_simple (#107) 117 | - Bump django from 4.0.1 to 4.0.2 in /examples/django_dynamic_fields (#106) 118 | 119 | ## 2.0.0 2022-01-12 120 | 121 | ### !!! Breaking Changes !!! 122 | 123 | Minimum supported Python version is now 3.5 124 | 125 | ### Maintenance 126 | 127 | - drop python 2.7 support (#103)| [@vreynolds](https://github.com/vreynolds) 128 | - bump urllib3, and other dependencies (#102) | [@vreynolds](https://github.com/vreynolds) 129 | - gh: add re-triage workflow (#101) | [@vreynolds](https://github.com/vreynolds) 130 | - docs: upgrade django examples: simple (#96) | [@vreynolds](https://github.com/vreynolds) 131 | - docs: upgrade django examples: response time (#97) | [@vreynolds](https://github.com/vreynolds) 132 | - docs: upgrade examples: dynamic fields (#98) | [@vreynolds](https://github.com/vreynolds) 133 | 134 | ## 1.11.2 2021-12-01 135 | 136 | Fixes 137 | 138 | - Fix tornado error handling (#88) 139 | 140 | Maintenance 141 | 142 | - ci: fix dependabot builds (#93) 143 | - Update dependabot to monthly (#92) 144 | - ci: add python 3.9 and 3.10 to test matrix (#90) 145 | - ci: allow dependabot build to run (#89) 146 | - fix local exampels, use poetry (#84) 147 | - empower apply-labels action to apply labels (#82) 148 | 149 | ## 1.11.1 2021-10-08 150 | 151 | Fixes 152 | 153 | - Drop use of long-deprecated `warn()` method (#77) 154 | 155 | Maintenance 156 | 157 | - Remove API Reference broken link from README (#79) 158 | - Change maintenance badge to maintained (#75) 159 | - Adds Stalebot (#76) 160 | - Add issue and PR templates (#74) 161 | - Add OSS lifecycle badge (#73) 162 | - Add community health files (#72) 163 | 164 | ## 1.11.0 2021-07-14 165 | 166 | Improvements 167 | 168 | - Make transmission queue sizes configurable (#69) 169 | 170 | Maintenance 171 | 172 | - Updates Github Action Workflows (#68) 173 | - Adds dependabot (#67) 174 | - Switches CODEOWNERS to telemetry-team (#66) 175 | - add our custom action to manage project labels (#64) 176 | - Use public CircleCI context for build secrets (#63) 177 | 178 | ## 1.10.0 2020-09-24 179 | 180 | Improvements 181 | 182 | - Schedule nightly builds on CirleCI (#57) 183 | - Add .editorconfig to help provide consistent IDE styling (#59) 184 | 185 | ## 1.9.1 2020-07-23 186 | 187 | Improvements 188 | 189 | - Now using [poetry](https://python-poetry.org/) for packaging and dependency management. 190 | - Updated to use current CircleCI badge instead of outdated TravisCI badge 191 | 192 | ## 1.9.0 2019-08-28 193 | 194 | Features 195 | 196 | - The default Transmission implementation now supports a `proxies` argument, which accepts a map defining http/https proxies. See the [requests](https://2.python-requests.org/en/master/user/advanced/#proxies) docs on proxies for more information. 197 | 198 | ## 1.8.0 2019-7-16 - Update recommended 199 | 200 | Improvements 201 | 202 | - Default Transmission implementation now compresses payloads by default (using gzip compression level 1). Compression offers significant savings in network egress at the cost of some CPU. Can be disabled by overriding `transmission_impl` when calling `libhoney.init()` and specifying `gzip_enabled=False`. See our official [docs](https://docs.honeycomb.io/getting-data-in/python/sdk/#customizing-event-transmission) for more information about overriding the default transmission. 203 | 204 | ## 1.7.2 2019-7-11 205 | 206 | Fixes 207 | 208 | - Switches default `send_frequency` type in the Tornado transmission implementation from float to timedelta, the correct type to use when fetching from tornado queues. Use of the float resulted in higher than expected CPU utilization. See [#49](https://github.com/honeycombio/libhoney-py/pull/49) for more details. 209 | 210 | ## 1.7.1 2019-03-29 211 | 212 | Documentation updates only. 213 | 214 | ## 1.7.0 2019-02-10 - Update recommended 215 | 216 | Improvements 217 | 218 | - JSON encoder now defaults to string encoding of types not handled by the default encoder (such as datetime). Previously, these would result in a `TypeError` being raised during event serialization, causing the event to be dropped. 219 | 220 | Security Updates 221 | 222 | - Updates example Django app to 1.8.19 in response to [CVE-2017-7233](https://nvd.nist.gov/vuln/detail/CVE-2017-7233). 223 | 224 | ## 1.6.2 2018-10-09 225 | 226 | Improvements 227 | 228 | - Adds default HTTP timeout of 10s. Previously, connections to the Honeycomb API would never timeout, blocking sending threads forever if communication or API service was disrupted. 229 | 230 | ## 1.6.1 2018-10-01 231 | 232 | Fixes 233 | 234 | - Prevents rare RuntimeError leak during shutdown. 235 | 236 | ## 1.6.0 2018-09-21 237 | 238 | Features 239 | 240 | - Adds debug mode for verbose logging of libhoney activities to stderr. This can be enabled by passing `debug=True` to `init`. 241 | 242 | Improvements 243 | 244 | - Improperly configured events now log an error rather than raise a `SendError` exception. 245 | 246 | Deprecations 247 | 248 | - `send_now` is deprecated, you should use `new_event` to create a new event combined with `Event.send()` to enqueue the event. `send_now` does not block the application to send events immediately, but its name had generated significant confusion. 249 | 250 | ## 1.5.0 2018-08-29 251 | 252 | Features 253 | 254 | - Adds new, optional `FileTransmission` transmission type for outputing to a file. Defaults to stdout. [#38](https://github.com/honeycombio/libhoney-py/pull/38) 255 | 256 | ## 1.4.0 2018-07-12 257 | 258 | Features 259 | 260 | - Adds new `flush` method to instantly send all pending events to Honeycomb. [#37](https://github.com/honeycombio/libhoney-py/pull/37) 261 | 262 | ## 1.3.3 2018-06-26 263 | 264 | Fixes 265 | 266 | - Fixes a positional/keyword argument mixup in the `Client` class. [#36](https://github.com/honeycombio/libhoney-py/pull/37) 267 | 268 | ## 1.3.2 2018-06-22 269 | 270 | Fixes 271 | 272 | - `Client` class now supports `user_agent_addition` argument. [#35](https://github.com/honeycombio/libhoney-py/pull/35) 273 | 274 | ## 1.3.0 2018-06-19 275 | 276 | Features 277 | 278 | - Adds `Client` class. Previously, the libhoney library operated arounda single global state. This state is now packaged in the `Client` class, enabling multiple simultaneous configurations. The global state is now backed by a default `Client` instance. 279 | 280 | ## 1.2.3 2018-06-01 281 | 282 | Features 283 | 284 | - Adds a `Transmission` implementation for Tornado. 285 | 286 | ## 1.2.2 2018-04-23 287 | 288 | Fixes 289 | 290 | - Support older versions of requests package. 291 | 292 | ## 1.2.1 2018-03-27 Update Recommended 293 | 294 | Fixes 295 | 296 | - Batch payloads were not passing timestamp information to the API correctly. 297 | 298 | ## 1.2.0 2018-03-08 299 | 300 | Improvements 301 | 302 | - Libhoney now transmits multiple events using the batch API. Previously, each event was sent as a separate request to the events API. 303 | -------------------------------------------------------------------------------- /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 | Ben Hartshorne 4 | Charity Majors 5 | Chris Toshok 6 | Christine Yen 7 | David Cain 8 | Ian Wilkes 9 | Steve Huff 10 | JJ Fliegelman 11 | Ashwini Balnaves 12 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2016 Honeycomb, Hound Technology, Inc. All rights reserved. 2 | 3 | Use of this source code is governed by the Apache License 2.0 4 | license that can be found in the LICENSE file. 5 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | ## Requirements 4 | 5 | Python: https://www.python.org/downloads/ 6 | 7 | Poetry: https://python-poetry.org/docs/#installation 8 | 9 | ## Install Dependencies 10 | 11 | ```shell 12 | poetry install 13 | ``` 14 | 15 | ## Run Tests 16 | 17 | To run all tests: 18 | 19 | ```shell 20 | poetry run coverage run -m unittest discover -v 21 | ``` 22 | 23 | To run individual tests: 24 | 25 | ```shell 26 | poetry run coverage run -m unittest libhoney/test_transmission.py 27 | ``` 28 | -------------------------------------------------------------------------------- /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 | build: 2 | poetry build 3 | 4 | clean: 5 | rm -rf libhoney/__pycache__/ 6 | rm -rf dist/* 7 | 8 | install: 9 | poetry install --no-root --no-ansi 10 | 11 | lint: 12 | poetry run pylint --rcfile=pylint.rc libhoney 13 | 14 | format: 15 | poetry run pycodestyle libhoney --max-line-length=140 16 | 17 | test: 18 | poetry run coverage run -m unittest discover -v 19 | 20 | smoke: 21 | @echo "" 22 | @echo "+++ Running example app in docker" 23 | @echo "" 24 | cd examples/factorial && docker-compose up --build --exit-code-from factorial-example 25 | 26 | unsmoke: 27 | @echo "" 28 | @echo "+++ Spinning down example app in docker" 29 | @echo "" 30 | cd examples/factorial && docker-compose down 31 | 32 | .PHONY: build clean install lint format test smoke unsmoke 33 | -------------------------------------------------------------------------------- /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-py 2 | 3 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/libhoney-py?color=success)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | [![Build Status](https://circleci.com/gh/honeycombio/libhoney-py.svg?style=svg)](https://app.circleci.com/pipelines/github/honeycombio/libhoney-py) 5 | 6 | Python 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/python/) 9 | 10 | For tracing support and automatic instrumentation of Django, Flask, AWS Lambda, and other frameworks, check out our [Beeline for Python](https://github.com/honeycombio/beeline-python). 11 | 12 | ## Contributions 13 | 14 | See [DEVELOPMENT.md](./DEVELOPMENT.md) 15 | 16 | Features, bug fixes and other changes to libhoney are gladly accepted. Please 17 | open issues or a pull request with your change. Remember to add your name to the 18 | CONTRIBUTORS file! 19 | 20 | All contributions will be released under the Apache License 2.0. 21 | 22 | ## Releases 23 | 24 | You may need to install the `bump2version` utility by running `pip install bump2version`. 25 | 26 | To update the version number, do 27 | 28 | ``` 29 | bump2version [major|minor|patch|release|build] 30 | ``` 31 | 32 | If you want to release the version publicly, you will need to manually create a tag `v` and push it in order to 33 | cause CircleCI to automatically push builds to github releases and PyPI. 34 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | 1. Update version using `bump2version --new-version 1.12.0 patch` (NOTE: the `patch` is required for the command to execute but doesn't mean anything as you're supplying a full version) 4 | 2. Update examples' lock files (run `poetry update` in each example directory) 5 | 6 | - Want to feel powerful? 7 | 8 | ```shell 9 | for dir in `find ./ -type d -depth 1`; do (cd $dir && poetry update); done 10 | ``` 11 | 12 | 3. Add release entry to [changelog](./CHANGELOG.md) 13 | 4. Open a PR with the above, and merge that into main 14 | 5. Create new tag on merged commit with the new version (e.g. `git tag -a v2.3.1 -m "v2.3.1"`) 15 | 6. Push the tag upstream (this will kick off the release pipeline in CI) e.g. `git push origin v2.3.1` 16 | 7. Copy change log entry for newest version into draft GitHub release created as part of CI publish steps 17 | - generate release notes via github release draft for full changelog notes and any new contributors 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | - [Django simple example](django_simple/my_app/honey_middleware.py) - captures basic metadata about each request coming through the django app 4 | - [Django response time example](django_response_time/my_app/honey_middleware.py) - captures response time as well as basic metadata for requests to the django app 5 | - [Django dynamic fields example](django_dynamic_fields/my_app/honey_middleware.py) - uses dynamic fields to generate values for the metric when `new_event()` is called, rather than the time of definition 6 | 7 | - [Factorial](factorial/example.py) - examples of how to use some of the features of libhoney in python, a single file that uses a factorial to generate events 8 | - [Tornado Factorial](factorial/example_tornado.py) - examples of how to use some of the features of libhoney in python, a single file that uses a factorial to generate events and sends them with Tornado async http client 9 | 10 | ## Installation 11 | 12 | Inside each example django directory: 13 | 14 | 1. `poetry install` 15 | 2. `poetry run python manage.py migrate # initialize the project` 16 | 3. `HONEYCOMB_API_KEY=api-key HONEYCOMB_DATASET=django-example poetry run python manage.py runserver` 17 | 18 | For the Factorial examples, there's no need to run the migrate step. Do only this: 19 | 1. `poetry install` 20 | 2. `HONEYCOMB_API_KEY=api-key poetry run python3 example_tornado.py` or `example.py` 21 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/django_dynamic_fields/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_dynamic_fields/django_dynamic_fields/__init__.py -------------------------------------------------------------------------------- /examples/django_dynamic_fields/django_dynamic_fields/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_dynamic_fields.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/django_dynamic_fields/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'django-insecure-6sa&*$0&xd8%zo2&tyum44wn4r^aentrcepc!1)*wh!#r+)@yb' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | 49 | 'my_app.honey_middleware.HoneyMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'django_dynamic_fields.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'django_dynamic_fields.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 74 | 75 | DATABASES = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.sqlite3', 78 | 'NAME': BASE_DIR / 'db.sqlite3', 79 | } 80 | } 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [ 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 97 | }, 98 | ] 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'en-us' 104 | 105 | TIME_ZONE = 'UTC' 106 | 107 | USE_I18N = True 108 | 109 | USE_TZ = True 110 | 111 | # Static files (CSS, JavaScript, Images) 112 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 113 | 114 | STATIC_URL = 'static/' 115 | 116 | # Default primary key field type 117 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 118 | 119 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 120 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/django_dynamic_fields/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path('', include('my_app.urls')), 5 | ] 6 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/django_dynamic_fields/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_dynamic_fields.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_dynamic_fields.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/my_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_dynamic_fields/my_app/__init__.py -------------------------------------------------------------------------------- /examples/django_dynamic_fields/my_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'my_app' 7 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/my_app/honey_middleware.py: -------------------------------------------------------------------------------- 1 | import os 2 | import resource 3 | 4 | import libhoney 5 | 6 | 7 | class HoneyMiddleware(object): 8 | def __init__(self, get_response): 9 | self.get_response = get_response 10 | libhoney.init(writekey=os.environ["HONEYCOMB_API_KEY"], 11 | dataset=os.environ.get("HONEYCOMB_DATASET", "django-example"), 12 | api_host=os.environ.get("HONEYCOMB_API_ENDPOINT", "https://api.honeycomb.io")) 13 | 14 | def __call__(self, request): 15 | def usertime_after(): 16 | return resource.getrusage(resource.RUSAGE_SELF).ru_utime 17 | 18 | def kerneltime_after(): 19 | return resource.getrusage(resource.RUSAGE_SELF).ru_stime 20 | 21 | def maxrss_after(): 22 | return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss 23 | 24 | is_ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' 25 | honey_builder = libhoney.Builder({ 26 | "method": request.method, 27 | "scheme": request.scheme, 28 | "path": request.path, 29 | "query": request.GET, 30 | "isSecure": request.is_secure(), 31 | "isAjax": is_ajax, 32 | "isUserAuthenticated": request.user.is_authenticated, 33 | "username": request.user.username, 34 | "host": request.get_host(), 35 | "ip": request.META['REMOTE_ADDR'], 36 | 37 | "usertime_before": resource.getrusage(resource.RUSAGE_SELF).ru_utime, 38 | "kerneltime_before": resource.getrusage(resource.RUSAGE_SELF).ru_stime, 39 | "maxrss_before": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, 40 | }, [ 41 | usertime_after, 42 | kerneltime_after, 43 | maxrss_after, 44 | ]) 45 | response = self.get_response(request) 46 | # creating new event will call the dynamic fields functions 47 | event = honey_builder.new_event() 48 | event.send() 49 | 50 | return response 51 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/my_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/my_app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("Hello, world!") 6 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.5.2" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 11 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 12 | ] 13 | 14 | [package.extras] 15 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 16 | 17 | [[package]] 18 | name = "backports.zoneinfo" 19 | version = "0.2.1" 20 | description = "Backport of the standard library zoneinfo module" 21 | optional = false 22 | python-versions = ">=3.6" 23 | files = [ 24 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, 25 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, 26 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, 27 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, 28 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, 29 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, 30 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, 31 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, 32 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, 33 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, 34 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, 35 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, 36 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, 37 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, 38 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, 39 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, 40 | ] 41 | 42 | [package.extras] 43 | tzdata = ["tzdata"] 44 | 45 | [[package]] 46 | name = "certifi" 47 | version = "2022.12.7" 48 | description = "Python package for providing Mozilla's CA Bundle." 49 | optional = false 50 | python-versions = ">=3.6" 51 | files = [ 52 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 53 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 54 | ] 55 | 56 | [[package]] 57 | name = "charset-normalizer" 58 | version = "2.1.1" 59 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 60 | optional = false 61 | python-versions = ">=3.6.0" 62 | files = [ 63 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 64 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 65 | ] 66 | 67 | [package.extras] 68 | unicode-backport = ["unicodedata2"] 69 | 70 | [[package]] 71 | name = "django" 72 | version = "4.1.10" 73 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 74 | optional = false 75 | python-versions = ">=3.8" 76 | files = [ 77 | {file = "Django-4.1.10-py3-none-any.whl", hash = "sha256:26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c"}, 78 | {file = "Django-4.1.10.tar.gz", hash = "sha256:56343019a9fd839e2e5bf203daf45f25af79d5bffa4c71d56eae4f4404d82ade"}, 79 | ] 80 | 81 | [package.dependencies] 82 | asgiref = ">=3.5.2,<4" 83 | "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} 84 | sqlparse = ">=0.2.2" 85 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 86 | 87 | [package.extras] 88 | argon2 = ["argon2-cffi (>=19.1.0)"] 89 | bcrypt = ["bcrypt"] 90 | 91 | [[package]] 92 | name = "idna" 93 | version = "3.3" 94 | description = "Internationalized Domain Names in Applications (IDNA)" 95 | optional = false 96 | python-versions = ">=3.5" 97 | files = [ 98 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 99 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 100 | ] 101 | 102 | [[package]] 103 | name = "libhoney" 104 | version = "2.3.0" 105 | description = "Python library for sending data to Honeycomb" 106 | optional = false 107 | python-versions = ">=3.7, <4" 108 | files = [] 109 | develop = true 110 | 111 | [package.dependencies] 112 | requests = "^2.24.0" 113 | statsd = "^3.3.0" 114 | urllib3 = "^1.26" 115 | 116 | [package.source] 117 | type = "directory" 118 | url = "../.." 119 | 120 | [[package]] 121 | name = "requests" 122 | version = "2.31.0" 123 | description = "Python HTTP for Humans." 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 128 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 129 | ] 130 | 131 | [package.dependencies] 132 | certifi = ">=2017.4.17" 133 | charset-normalizer = ">=2,<4" 134 | idna = ">=2.5,<4" 135 | urllib3 = ">=1.21.1,<3" 136 | 137 | [package.extras] 138 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 139 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 140 | 141 | [[package]] 142 | name = "sqlparse" 143 | version = "0.4.4" 144 | description = "A non-validating SQL parser." 145 | optional = false 146 | python-versions = ">=3.5" 147 | files = [ 148 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 149 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 150 | ] 151 | 152 | [package.extras] 153 | dev = ["build", "flake8"] 154 | doc = ["sphinx"] 155 | test = ["pytest", "pytest-cov"] 156 | 157 | [[package]] 158 | name = "statsd" 159 | version = "3.3.0" 160 | description = "A simple statsd client." 161 | optional = false 162 | python-versions = "*" 163 | files = [ 164 | {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, 165 | {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, 166 | ] 167 | 168 | [[package]] 169 | name = "tzdata" 170 | version = "2022.2" 171 | description = "Provider of IANA time zone data" 172 | optional = false 173 | python-versions = ">=2" 174 | files = [ 175 | {file = "tzdata-2022.2-py2.py3-none-any.whl", hash = "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b"}, 176 | {file = "tzdata-2022.2.tar.gz", hash = "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451"}, 177 | ] 178 | 179 | [[package]] 180 | name = "urllib3" 181 | version = "1.26.12" 182 | description = "HTTP library with thread-safe connection pooling, file post, and more." 183 | optional = false 184 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 185 | files = [ 186 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 187 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 188 | ] 189 | 190 | [package.extras] 191 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 192 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 193 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 194 | 195 | [metadata] 196 | lock-version = "2.0" 197 | python-versions = "^3.8" 198 | content-hash = "4fc132cdd593958678d5567cba395e644464a15a4da10424e7db817075b6de0a" 199 | -------------------------------------------------------------------------------- /examples/django_dynamic_fields/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney-django-dynamic-fields-example" 3 | version = "0.1.0" 4 | description = "Django example with dynamic fields using libhoney" 5 | authors = ["honeycombio"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | Django = "^4.1" 10 | libhoney = {path = "../..", develop = true} 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry-core>=1.0.0"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /examples/django_response_time/django_response_time/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_response_time/django_response_time/__init__.py -------------------------------------------------------------------------------- /examples/django_response_time/django_response_time/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_response_time.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_response_time/django_response_time/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'django-insecure-6sa&*$0&xd8%zo2&tyum44wn4r^aentrcepc!1)*wh!#r+)@yb' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | 49 | 'my_app.honey_middleware.HoneyMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'django_response_time.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'django_response_time.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 74 | 75 | DATABASES = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.sqlite3', 78 | 'NAME': BASE_DIR / 'db.sqlite3', 79 | } 80 | } 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [ 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 97 | }, 98 | ] 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'en-us' 104 | 105 | TIME_ZONE = 'UTC' 106 | 107 | USE_I18N = True 108 | 109 | USE_TZ = True 110 | 111 | # Static files (CSS, JavaScript, Images) 112 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 113 | 114 | STATIC_URL = 'static/' 115 | 116 | # Default primary key field type 117 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 118 | 119 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 120 | -------------------------------------------------------------------------------- /examples/django_response_time/django_response_time/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path('', include('my_app.urls')), 5 | ] 6 | -------------------------------------------------------------------------------- /examples/django_response_time/django_response_time/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_response_time.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_response_time/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_response_time.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/django_response_time/my_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_response_time/my_app/__init__.py -------------------------------------------------------------------------------- /examples/django_response_time/my_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'my_app' 7 | -------------------------------------------------------------------------------- /examples/django_response_time/my_app/honey_middleware.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import libhoney 5 | 6 | 7 | class HoneyMiddleware(object): 8 | def __init__(self, get_response): 9 | self.get_response = get_response 10 | libhoney.init(writekey=os.environ["HONEYCOMB_API_KEY"], 11 | dataset=os.environ.get("HONEYCOMB_DATASET", "django-example"), 12 | api_host=os.environ.get("HONEYCOMB_API_ENDPOINT", "https://api.honeycomb.io")) 13 | 14 | def __call__(self, request): 15 | start_time = time.time() 16 | response = self.get_response(request) 17 | response_time = time.time() - start_time 18 | 19 | is_ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' 20 | ev = libhoney.new_event(data={ 21 | "method": request.method, 22 | "scheme": request.scheme, 23 | "path": request.path, 24 | "query": request.GET, 25 | "isSecure": request.is_secure(), 26 | "isAjax": is_ajax, 27 | "isUserAuthenticated": request.user.is_authenticated, 28 | "username": request.user.username, 29 | "host": request.get_host(), 30 | "ip": request.META['REMOTE_ADDR'], 31 | "responseTime_ms": response_time * 1000, 32 | }) 33 | ev.send() 34 | 35 | return response 36 | -------------------------------------------------------------------------------- /examples/django_response_time/my_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /examples/django_response_time/my_app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("Hello, world!") 6 | -------------------------------------------------------------------------------- /examples/django_response_time/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.5.2" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 11 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 12 | ] 13 | 14 | [package.extras] 15 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 16 | 17 | [[package]] 18 | name = "backports.zoneinfo" 19 | version = "0.2.1" 20 | description = "Backport of the standard library zoneinfo module" 21 | optional = false 22 | python-versions = ">=3.6" 23 | files = [ 24 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, 25 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, 26 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, 27 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, 28 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, 29 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, 30 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, 31 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, 32 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, 33 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, 34 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, 35 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, 36 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, 37 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, 38 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, 39 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, 40 | ] 41 | 42 | [package.extras] 43 | tzdata = ["tzdata"] 44 | 45 | [[package]] 46 | name = "certifi" 47 | version = "2022.12.7" 48 | description = "Python package for providing Mozilla's CA Bundle." 49 | optional = false 50 | python-versions = ">=3.6" 51 | files = [ 52 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 53 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 54 | ] 55 | 56 | [[package]] 57 | name = "charset-normalizer" 58 | version = "2.1.1" 59 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 60 | optional = false 61 | python-versions = ">=3.6.0" 62 | files = [ 63 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 64 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 65 | ] 66 | 67 | [package.extras] 68 | unicode-backport = ["unicodedata2"] 69 | 70 | [[package]] 71 | name = "django" 72 | version = "4.1.10" 73 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 74 | optional = false 75 | python-versions = ">=3.8" 76 | files = [ 77 | {file = "Django-4.1.10-py3-none-any.whl", hash = "sha256:26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c"}, 78 | {file = "Django-4.1.10.tar.gz", hash = "sha256:56343019a9fd839e2e5bf203daf45f25af79d5bffa4c71d56eae4f4404d82ade"}, 79 | ] 80 | 81 | [package.dependencies] 82 | asgiref = ">=3.5.2,<4" 83 | "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} 84 | sqlparse = ">=0.2.2" 85 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 86 | 87 | [package.extras] 88 | argon2 = ["argon2-cffi (>=19.1.0)"] 89 | bcrypt = ["bcrypt"] 90 | 91 | [[package]] 92 | name = "idna" 93 | version = "3.3" 94 | description = "Internationalized Domain Names in Applications (IDNA)" 95 | optional = false 96 | python-versions = ">=3.5" 97 | files = [ 98 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 99 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 100 | ] 101 | 102 | [[package]] 103 | name = "libhoney" 104 | version = "2.3.0" 105 | description = "Python library for sending data to Honeycomb" 106 | optional = false 107 | python-versions = ">=3.7, <4" 108 | files = [] 109 | develop = true 110 | 111 | [package.dependencies] 112 | requests = "^2.24.0" 113 | statsd = "^3.3.0" 114 | urllib3 = "^1.26" 115 | 116 | [package.source] 117 | type = "directory" 118 | url = "../.." 119 | 120 | [[package]] 121 | name = "requests" 122 | version = "2.31.0" 123 | description = "Python HTTP for Humans." 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 128 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 129 | ] 130 | 131 | [package.dependencies] 132 | certifi = ">=2017.4.17" 133 | charset-normalizer = ">=2,<4" 134 | idna = ">=2.5,<4" 135 | urllib3 = ">=1.21.1,<3" 136 | 137 | [package.extras] 138 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 139 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 140 | 141 | [[package]] 142 | name = "sqlparse" 143 | version = "0.4.4" 144 | description = "A non-validating SQL parser." 145 | optional = false 146 | python-versions = ">=3.5" 147 | files = [ 148 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 149 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 150 | ] 151 | 152 | [package.extras] 153 | dev = ["build", "flake8"] 154 | doc = ["sphinx"] 155 | test = ["pytest", "pytest-cov"] 156 | 157 | [[package]] 158 | name = "statsd" 159 | version = "3.3.0" 160 | description = "A simple statsd client." 161 | optional = false 162 | python-versions = "*" 163 | files = [ 164 | {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, 165 | {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, 166 | ] 167 | 168 | [[package]] 169 | name = "tzdata" 170 | version = "2022.2" 171 | description = "Provider of IANA time zone data" 172 | optional = false 173 | python-versions = ">=2" 174 | files = [ 175 | {file = "tzdata-2022.2-py2.py3-none-any.whl", hash = "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b"}, 176 | {file = "tzdata-2022.2.tar.gz", hash = "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451"}, 177 | ] 178 | 179 | [[package]] 180 | name = "urllib3" 181 | version = "1.26.12" 182 | description = "HTTP library with thread-safe connection pooling, file post, and more." 183 | optional = false 184 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 185 | files = [ 186 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 187 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 188 | ] 189 | 190 | [package.extras] 191 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 192 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 193 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 194 | 195 | [metadata] 196 | lock-version = "2.0" 197 | python-versions = "^3.8" 198 | content-hash = "4fc132cdd593958678d5567cba395e644464a15a4da10424e7db817075b6de0a" 199 | -------------------------------------------------------------------------------- /examples/django_response_time/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney-django-response-time-example" 3 | version = "0.1.0" 4 | description = "Django example with response time using libhoney" 5 | authors = ["honeycombio"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | Django = "^4.1" 10 | libhoney = {path = "../..", develop = true} 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry-core>=1.0.0"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /examples/django_simple/django_simple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_simple/django_simple/__init__.py -------------------------------------------------------------------------------- /examples/django_simple/django_simple/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_simple.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_simple/django_simple/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'django-insecure-6sa&*$0&xd8%zo2&tyum44wn4r^aentrcepc!1)*wh!#r+)@yb' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | 49 | 'my_app.honey_middleware.HoneyMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'django_simple.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'django_simple.wsgi.application' 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 74 | 75 | DATABASES = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.sqlite3', 78 | 'NAME': BASE_DIR / 'db.sqlite3', 79 | } 80 | } 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [ 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 97 | }, 98 | ] 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'en-us' 104 | 105 | TIME_ZONE = 'UTC' 106 | 107 | USE_I18N = True 108 | 109 | USE_TZ = True 110 | 111 | # Static files (CSS, JavaScript, Images) 112 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 113 | 114 | STATIC_URL = 'static/' 115 | 116 | # Default primary key field type 117 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 118 | 119 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 120 | -------------------------------------------------------------------------------- /examples/django_simple/django_simple/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path('', include('my_app.urls')), 5 | ] 6 | -------------------------------------------------------------------------------- /examples/django_simple/django_simple/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_simple.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_simple/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_simple.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/django_simple/my_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honeycombio/libhoney-py/11b59417c1df1d97384dc5e870f2ea462da02b80/examples/django_simple/my_app/__init__.py -------------------------------------------------------------------------------- /examples/django_simple/my_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MyAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'my_app' 7 | -------------------------------------------------------------------------------- /examples/django_simple/my_app/honey_middleware.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import libhoney 4 | 5 | 6 | class HoneyMiddleware(object): 7 | def __init__(self, get_response): 8 | self.get_response = get_response 9 | libhoney.init(writekey=os.environ["HONEYCOMB_API_KEY"], 10 | dataset=os.environ.get("HONEYCOMB_DATASET", "django-example"), 11 | api_host=os.environ.get("HONEYCOMB_API_ENDPOINT", "https://api.honeycomb.io")) 12 | 13 | def __call__(self, request): 14 | is_ajax = request.headers.get('x-requested-with') == 'XMLHttpRequest' 15 | ev = libhoney.new_event(data={ 16 | "method": request.method, 17 | "scheme": request.scheme, 18 | "path": request.path, 19 | "query": request.GET, 20 | "isSecure": request.is_secure(), 21 | "isAjax": is_ajax, 22 | "isUserAuthenticated": request.user.is_authenticated, 23 | "username": request.user.username, 24 | "host": request.get_host(), 25 | "ip": request.META['REMOTE_ADDR'], 26 | }) 27 | ev.send() 28 | 29 | response = self.get_response(request) 30 | return response 31 | -------------------------------------------------------------------------------- /examples/django_simple/my_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /examples/django_simple/my_app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("Hello, world!") 6 | -------------------------------------------------------------------------------- /examples/django_simple/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.5.2" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 11 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 12 | ] 13 | 14 | [package.extras] 15 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 16 | 17 | [[package]] 18 | name = "backports.zoneinfo" 19 | version = "0.2.1" 20 | description = "Backport of the standard library zoneinfo module" 21 | optional = false 22 | python-versions = ">=3.6" 23 | files = [ 24 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, 25 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, 26 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, 27 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, 28 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, 29 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, 30 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, 31 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, 32 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, 33 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, 34 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, 35 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, 36 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, 37 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, 38 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, 39 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, 40 | ] 41 | 42 | [package.extras] 43 | tzdata = ["tzdata"] 44 | 45 | [[package]] 46 | name = "certifi" 47 | version = "2022.12.7" 48 | description = "Python package for providing Mozilla's CA Bundle." 49 | optional = false 50 | python-versions = ">=3.6" 51 | files = [ 52 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 53 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 54 | ] 55 | 56 | [[package]] 57 | name = "charset-normalizer" 58 | version = "2.1.1" 59 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 60 | optional = false 61 | python-versions = ">=3.6.0" 62 | files = [ 63 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 64 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 65 | ] 66 | 67 | [package.extras] 68 | unicode-backport = ["unicodedata2"] 69 | 70 | [[package]] 71 | name = "django" 72 | version = "4.1.10" 73 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 74 | optional = false 75 | python-versions = ">=3.8" 76 | files = [ 77 | {file = "Django-4.1.10-py3-none-any.whl", hash = "sha256:26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c"}, 78 | {file = "Django-4.1.10.tar.gz", hash = "sha256:56343019a9fd839e2e5bf203daf45f25af79d5bffa4c71d56eae4f4404d82ade"}, 79 | ] 80 | 81 | [package.dependencies] 82 | asgiref = ">=3.5.2,<4" 83 | "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} 84 | sqlparse = ">=0.2.2" 85 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 86 | 87 | [package.extras] 88 | argon2 = ["argon2-cffi (>=19.1.0)"] 89 | bcrypt = ["bcrypt"] 90 | 91 | [[package]] 92 | name = "idna" 93 | version = "3.3" 94 | description = "Internationalized Domain Names in Applications (IDNA)" 95 | optional = false 96 | python-versions = ">=3.5" 97 | files = [ 98 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 99 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 100 | ] 101 | 102 | [[package]] 103 | name = "libhoney" 104 | version = "2.3.0" 105 | description = "Python library for sending data to Honeycomb" 106 | optional = false 107 | python-versions = ">=3.7, <4" 108 | files = [] 109 | develop = true 110 | 111 | [package.dependencies] 112 | requests = "^2.24.0" 113 | statsd = "^3.3.0" 114 | urllib3 = "^1.26" 115 | 116 | [package.source] 117 | type = "directory" 118 | url = "../.." 119 | 120 | [[package]] 121 | name = "requests" 122 | version = "2.31.0" 123 | description = "Python HTTP for Humans." 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 128 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 129 | ] 130 | 131 | [package.dependencies] 132 | certifi = ">=2017.4.17" 133 | charset-normalizer = ">=2,<4" 134 | idna = ">=2.5,<4" 135 | urllib3 = ">=1.21.1,<3" 136 | 137 | [package.extras] 138 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 139 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 140 | 141 | [[package]] 142 | name = "sqlparse" 143 | version = "0.4.4" 144 | description = "A non-validating SQL parser." 145 | optional = false 146 | python-versions = ">=3.5" 147 | files = [ 148 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 149 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 150 | ] 151 | 152 | [package.extras] 153 | dev = ["build", "flake8"] 154 | doc = ["sphinx"] 155 | test = ["pytest", "pytest-cov"] 156 | 157 | [[package]] 158 | name = "statsd" 159 | version = "3.3.0" 160 | description = "A simple statsd client." 161 | optional = false 162 | python-versions = "*" 163 | files = [ 164 | {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, 165 | {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, 166 | ] 167 | 168 | [[package]] 169 | name = "tzdata" 170 | version = "2022.2" 171 | description = "Provider of IANA time zone data" 172 | optional = false 173 | python-versions = ">=2" 174 | files = [ 175 | {file = "tzdata-2022.2-py2.py3-none-any.whl", hash = "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b"}, 176 | {file = "tzdata-2022.2.tar.gz", hash = "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451"}, 177 | ] 178 | 179 | [[package]] 180 | name = "urllib3" 181 | version = "1.26.12" 182 | description = "HTTP library with thread-safe connection pooling, file post, and more." 183 | optional = false 184 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 185 | files = [ 186 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 187 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 188 | ] 189 | 190 | [package.extras] 191 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 192 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 193 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 194 | 195 | [metadata] 196 | lock-version = "2.0" 197 | python-versions = "^3.8" 198 | content-hash = "4fc132cdd593958678d5567cba395e644464a15a4da10424e7db817075b6de0a" 199 | -------------------------------------------------------------------------------- /examples/django_simple/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney-django-simple-example" 3 | version = "0.1.0" 4 | description = "Django example using libhoney" 5 | authors = ["honeycombio"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | Django = "^4.1" 10 | libhoney = {path = "../..", develop = true} 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry-core>=1.0.0"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /examples/factorial/FactorialDockerfile: -------------------------------------------------------------------------------- 1 | # this dockerfile should be used with docker-compose.yml in root directory 2 | 3 | FROM python:alpine 4 | 5 | WORKDIR /app 6 | 7 | RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo 8 | 9 | ENV PYTHONFAULTHANDLER=1 \ 10 | PYTHONUNBUFFERED=1 \ 11 | PYTHONHASHSEED=random \ 12 | PIP_NO_CACHE_DIR=off \ 13 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 14 | PIP_DEFAULT_TIMEOUT=100 \ 15 | POETRY_VERSION=1.3.2 16 | 17 | RUN pip install "poetry==$POETRY_VERSION" 18 | 19 | COPY . . 20 | RUN poetry install 21 | RUN cd ./examples/factorial && poetry install 22 | 23 | EXPOSE 7000 24 | CMD ["poetry", "run", "python3", "./examples/factorial/example.py"] 25 | -------------------------------------------------------------------------------- /examples/factorial/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | x-env-base: &env_base 4 | HONEYCOMB_API_ENDPOINT: test_endpoint 5 | HONEYCOMB_API_KEY: test_key 6 | HONEYCOMB_DATASET: test_dataset 7 | 8 | x-app-base: &app_base 9 | build: 10 | context: ../../ 11 | dockerfile: ./examples/factorial/FactorialDockerfile 12 | image: hnyexample/factorial-example 13 | 14 | services: 15 | factorial-example: 16 | <<: *app_base 17 | environment: 18 | <<: *env_base 19 | -------------------------------------------------------------------------------- /examples/factorial/example.py: -------------------------------------------------------------------------------- 1 | '''This example shows how to use some of the features of libhoney in python''' 2 | import os 3 | import datetime 4 | import libhoney 5 | import threading 6 | 7 | writekey=os.environ.get("HONEYCOMB_API_KEY") 8 | dataset=os.environ.get("HONEYCOMB_DATASET", "factorial") 9 | api_host=os.environ.get("HONEYCOMB_API_ENDPOINT", "https://api.honeycomb.io") 10 | 11 | def factorial(n): 12 | if n < 0: 13 | return -1 * factorial(abs(n)) 14 | if n == 0: 15 | return 1 16 | return n * factorial(n - 1) 17 | 18 | 19 | def num_threads(): 20 | '''add information about the number of threads currently running to the 21 | event''' 22 | return threading.active_count() 23 | 24 | 25 | # run factorial. libh_builder comes with some fields already populated 26 | # (namely, "version", "num_threads", and "range") 27 | def run_fact(low, high, libh_builder): 28 | for i in range(low, high): 29 | ev = libh_builder.new_event() 30 | ev.metadata = {"fn": "run_fact", "i": i} 31 | with ev.timer("fact"): 32 | res = factorial(10 + i) 33 | ev.add_field("retval", res) 34 | print("About to send event: %s" % ev) 35 | ev.send() 36 | 37 | 38 | def read_responses(resp_queue): 39 | '''read responses from the libhoney queue, print them out.''' 40 | while True: 41 | resp = resp_queue.get() 42 | # libhoney will enqueue a None value after we call libhoney.close() 43 | if resp is None: 44 | break 45 | status = "sending event with metadata {} took {}ms and got response code {} with message \"{}\" and error message \"{}\"".format( 46 | resp["metadata"], resp["duration"], resp["status_code"], 47 | resp["body"].rstrip(), resp["error"]) 48 | print(status) 49 | 50 | 51 | if __name__ == "__main__": 52 | hc = libhoney.Client(writekey=writekey, 53 | dataset=dataset, 54 | api_host=api_host, 55 | max_concurrent_batches=1) 56 | resps = hc.responses() 57 | t = threading.Thread(target=read_responses, args=(resps,)) 58 | 59 | # Mark this thread as a daemon so we don't wait for this thread to exit 60 | # before shutting down. Alternatively, to be sure you read all the 61 | # responses before exiting, omit this line and explicitly call 62 | # libhoney.close() at the end of the script. 63 | t.daemon = True 64 | 65 | t.start() 66 | 67 | # Attach fields to top-level instance 68 | hc.add_field("version", "3.4.5") 69 | hc.add_dynamic_field(num_threads) 70 | 71 | ev = hc.new_event() 72 | ev.add_field("start_time", datetime.datetime.now().isoformat()) 73 | 74 | # wrap our calls with timers 75 | with ev.timer(name="run_fact_1_2_dur_ms"): 76 | run_fact(1, 2, hc.new_builder({"range": "low"})) 77 | with ev.timer(name="run_fact_31_32_dur_ms"): 78 | run_fact(31, 32, hc.new_builder({"range": "high"})) 79 | ev.add_field("end_time", datetime.datetime.now().isoformat()) 80 | # sends event with fields: version, num_threads, start_time, end_time, 81 | # run_fact_1_2_dur_ms, run_fact_31_32_dur_ms 82 | ev.send() 83 | 84 | # Optionally tell libhoney there are no more events coming. This ensures 85 | # the read_responses thread will terminate. 86 | hc.close() 87 | -------------------------------------------------------------------------------- /examples/factorial/example_tornado.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tornado import ioloop, gen 3 | import libhoney 4 | from libhoney.transmission import TornadoTransmission 5 | 6 | g_hc = None 7 | 8 | def factorial(n): 9 | if n < 0: 10 | return -1 * factorial(abs(n)) 11 | if n == 0: 12 | return 1 13 | return n * factorial(n - 1) 14 | 15 | def run_fact(low, high, libh_builder): 16 | for i in range(low, high): 17 | ev = libh_builder.new_event() 18 | ev.metadata = {"fn": "run_fact", "i": i} 19 | with ev.timer("fact_timer"): 20 | res = factorial(10 + i) 21 | ev.add_field("retval", res) 22 | ev.send() 23 | print("About to send event: %s" % ev) 24 | 25 | @gen.coroutine 26 | def event_routine(): 27 | event_counter = 1 28 | while event_counter <= 100: 29 | run_fact(1, event_counter, g_hc.new_builder({"event_counter": event_counter})) 30 | 31 | event_counter += 1 32 | yield gen.sleep(0.1) 33 | 34 | @gen.coroutine 35 | def main(): 36 | global g_hc 37 | g_hc = libhoney.Client(writekey=os.environ["HONEYCOMB_API_KEY"], dataset="factorial.tornado", 38 | transmission_impl=TornadoTransmission()) 39 | ioloop.IOLoop.current().spawn_callback(event_routine) 40 | 41 | while True: 42 | r = yield g_hc.responses().get() 43 | print("Got response: %s" % r) 44 | 45 | if __name__ == "__main__": 46 | ioloop.IOLoop.current().run_sync(main) 47 | 48 | -------------------------------------------------------------------------------- /examples/factorial/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2023.7.22" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 11 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 12 | ] 13 | 14 | [[package]] 15 | name = "charset-normalizer" 16 | version = "2.1.1" 17 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 18 | optional = false 19 | python-versions = ">=3.6.0" 20 | files = [ 21 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 22 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 23 | ] 24 | 25 | [package.extras] 26 | unicode-backport = ["unicodedata2"] 27 | 28 | [[package]] 29 | name = "idna" 30 | version = "3.3" 31 | description = "Internationalized Domain Names in Applications (IDNA)" 32 | optional = false 33 | python-versions = ">=3.5" 34 | files = [ 35 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 36 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 37 | ] 38 | 39 | [[package]] 40 | name = "libhoney" 41 | version = "2.3.0" 42 | description = "Python library for sending data to Honeycomb" 43 | optional = false 44 | python-versions = ">=3.7, <4" 45 | files = [] 46 | develop = true 47 | 48 | [package.dependencies] 49 | requests = "^2.24.0" 50 | statsd = "^3.3.0" 51 | urllib3 = "^1.26" 52 | 53 | [package.source] 54 | type = "directory" 55 | url = "../.." 56 | 57 | [[package]] 58 | name = "requests" 59 | version = "2.31.0" 60 | description = "Python HTTP for Humans." 61 | optional = false 62 | python-versions = ">=3.7" 63 | files = [ 64 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 65 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 66 | ] 67 | 68 | [package.dependencies] 69 | certifi = ">=2017.4.17" 70 | charset-normalizer = ">=2,<4" 71 | idna = ">=2.5,<4" 72 | urllib3 = ">=1.21.1,<3" 73 | 74 | [package.extras] 75 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 76 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 77 | 78 | [[package]] 79 | name = "statsd" 80 | version = "3.3.0" 81 | description = "A simple statsd client." 82 | optional = false 83 | python-versions = "*" 84 | files = [ 85 | {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, 86 | {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, 87 | ] 88 | 89 | [[package]] 90 | name = "tornado" 91 | version = "6.2" 92 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 93 | optional = false 94 | python-versions = ">= 3.7" 95 | files = [ 96 | {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, 97 | {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, 98 | {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, 99 | {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, 100 | {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, 101 | {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, 102 | {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, 103 | {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, 104 | {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, 105 | {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, 106 | {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, 107 | ] 108 | 109 | [[package]] 110 | name = "urllib3" 111 | version = "1.26.12" 112 | description = "HTTP library with thread-safe connection pooling, file post, and more." 113 | optional = false 114 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 115 | files = [ 116 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 117 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 118 | ] 119 | 120 | [package.extras] 121 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 122 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 123 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 124 | 125 | [metadata] 126 | lock-version = "2.0" 127 | python-versions = "^3.7" 128 | content-hash = "af0f628735f24b6d813f0e2036d4e89447879f8d8f9a1b3898c4b3f14b7ba9f9" 129 | -------------------------------------------------------------------------------- /examples/factorial/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney-tornado-example" 3 | version = "0.1.0" 4 | description = "Tornado example using libhoney" 5 | authors = ["Your Name "] 6 | license = "n" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | requests = "^2.31.0" 11 | statsd = "^3.3.0" 12 | tornado = [{version = "^6.0.4", python = ">=3.5"}] 13 | libhoney = {path = "../..", develop = true} 14 | 15 | 16 | [tool.poetry.dev-dependencies] 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /examples/flask/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | WORKDIR /app 4 | 5 | RUN apk update && apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo mariadb-dev 6 | 7 | ENV PYTHONFAULTHANDLER=1 \ 8 | PYTHONUNBUFFERED=1 \ 9 | PYTHONHASHSEED=random \ 10 | PIP_NO_CACHE_DIR=off \ 11 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 12 | PIP_DEFAULT_TIMEOUT=100 \ 13 | POETRY_VERSION=1.0.0 \ 14 | FLASK_APP=app.py 15 | 16 | RUN pip install "poetry==$POETRY_VERSION" 17 | RUN pip install mysqlclient 18 | 19 | COPY poetry.lock pyproject.toml ./ 20 | RUN poetry check 21 | 22 | RUN poetry config virtualenvs.create false \ 23 | && poetry install --no-root --no-interaction --no-ansi 24 | 25 | COPY app.py /app/app.py 26 | 27 | CMD ["poetry", "run", "flask", "run", "--host=0.0.0.0"] 28 | -------------------------------------------------------------------------------- /examples/flask/README.md: -------------------------------------------------------------------------------- 1 | ## Flask example 2 | 3 | The Python API example shows how to do Libhoney instrumentation with a Flask app. 4 | 5 | Events are created per-HTTP-request (following the Honeycomb _one event per unit of work_ 6 | model) using the `@app.before_request` decorator and sent after the request using the 7 | `@app.after_request` decorator. Because the events are stored on `g`, Flask's thread 8 | local storage, the events contain a variety of default properties describing the request 9 | as well as custom fields that can be added by any handler using `g.ev.add_field`. 10 | 11 | ## Run Locally 12 | 13 | MySQL and Flask must be installed (`pip install flask`). 14 | 15 | First, create the database used by the app. 16 | 17 | ```shell 18 | mysql -uroot -e 'create database example-python-api;' 19 | ``` 20 | 21 | Install dependencies: 22 | 23 | ```shell 24 | poetry install 25 | ``` 26 | 27 | Then run the Python app on `localhost`: 28 | 29 | ```shell 30 | HONEYCOMB_API_KEY=api-key FLASK_APP=app.py poetry run flask run 31 | ``` 32 | 33 | A basic REST API for todos is exposed on port 5000. 34 | 35 | ```sh 36 | $ curl \ 37 | -H 'Content-Type: application/json' \ 38 | -X POST -d '{"description": "Walk the dog", "due": 1518816723}' \ 39 | localhost:5000/todos/ 40 | ... 41 | 42 | $ curl localhost:5000/todos/ 43 | [ 44 | { 45 | "completed": false, 46 | "description": "Walk the dog", 47 | "due": "Fri, 16 Feb 2018 21:32:03 GMT", 48 | "id": 1 49 | } 50 | ] 51 | 52 | $ curl -X PUT \ 53 | -H 'Content-Type: application/json' \ 54 | -d '{"description": "Walk the cat"}' \ 55 | localhost:5000/todos/1/ 56 | { 57 | "completed": false, 58 | "description": "Walk the cat", 59 | "due": "Fri, 16 Feb 2018 21:32:03 GMT", 60 | "id": 1 61 | } 62 | 63 | $ curl -X DELETE localhost:5000/todos/1/ 64 | { 65 | "id": 1, 66 | "success": true 67 | } 68 | 69 | $ curl localhost:5000/todos/ 70 | [] 71 | ``` 72 | 73 | ## Run in Docker 74 | 75 | This example can be run in Docker (Compose). 76 | 77 | ``` 78 | docker-compose up --build 79 | ``` 80 | 81 | ## Event Fields 82 | 83 | | **Name** | **Description** | **Example Value** | 84 | |-------------------------------|------------------------------------------------------------|-------------------------| 85 | | `errors.message` | Message in the error encountered, if applicable | `undefined` | 86 | | `request.endpoint` | Endpoint requested | `/todos/` | 87 | | `request.method` | HTTP method | `POST` | 88 | | `request.path` | Request path | `/todos/` | 89 | | `request.python_function` | Python function serving the request | `index` | 90 | | `request.url_pattern` | ` Underlying routing pattern of the URL | `/todos//` | 91 | | `request.user_agent` | User agent for the request | `curl/7.54.0` | 92 | | `request.user_agent.browser` | Web browser the request was served to | `chrome` | 93 | | `request.user_agent.platform` | OS of the user agent | `macos` | 94 | | `request.user_agent.string` | Literal user agent string | `curl/7.54.0` | 95 | | `request.user_agent.version` | Version of the user agent | `64.0.3282.186` | 96 | | `response.status_code` | HTTP status code of the response | 404 | 97 | | `timers.db.delete_todo` | Time in milliseconds for DB call to delete a todo | 23 | 98 | | `timers.db.insert_todo_ms` | Time in milliseconds for DB call to insert a todo | 50 | 99 | | `timers.db.select_all_todos` | Time in milliseconds for DB call to select all todos | 11 | 100 | | `timers.db.select_todo` | Time in milliseconds for DB call to select a todo | 4 | 101 | | `timers.db.update_todo` | Time in milliseconds for DB call to update a todo | 50 | 102 | | `timers.flask_time_ms` | Total time in milliseconds Flask spent serving the request | 75 | 103 | | `todo.id` | ID of the associated TODO | 1 | 104 | 105 | -------------------------------------------------------------------------------- /examples/flask/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import datetime 4 | 5 | import libhoney 6 | from flask import Flask, jsonify, g, request 7 | import logging 8 | 9 | from flask_sqlalchemy import SQLAlchemy 10 | 11 | app = Flask(__name__) 12 | 13 | app.logger.addHandler(logging.StreamHandler(sys.stderr)) 14 | app.logger.setLevel(logging.DEBUG) 15 | 16 | db_user = "root" 17 | db_pass = "" 18 | db_host = os.getenv("DB_HOST", default="localhost") 19 | db_name = "example-python-api" 20 | app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://{}:{}@{}:3306/{}".format(db_user, db_pass, db_host, db_name) 21 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 22 | db = SQLAlchemy(app) 23 | 24 | libhoney.init(writekey=os.getenv("HONEYCOMB_API_KEY"), dataset="examples.python-api") 25 | libhoney_builder = libhoney.Builder() 26 | 27 | 28 | class Todo(db.Model): 29 | id = db.Column(db.Integer, primary_key=True) 30 | description = db.Column(db.String(200), nullable=False) 31 | completed = db.Column(db.Boolean(False), nullable=False) 32 | due = db.Column(db.DateTime()) 33 | 34 | def serialize(self): 35 | return { 36 | "id": self.id, 37 | "description": self.description, 38 | "completed": self.completed, 39 | "due": self.due, 40 | } 41 | 42 | def __repr__(self): 43 | return "".format(self.id) 44 | 45 | 46 | with app.app_context(): 47 | db.create_all() 48 | 49 | 50 | class InvalidUsage(Exception): 51 | status_code = 400 52 | 53 | def __init__(self, message, status_code=None, payload=None): 54 | Exception.__init__(self) 55 | self.message = message 56 | if status_code is not None: 57 | self.status_code = status_code 58 | self.payload = payload 59 | 60 | def to_dict(self): 61 | rv = dict(self.payload or ()) 62 | rv['message'] = self.message 63 | return rv 64 | 65 | 66 | def milliseconds_since(start): 67 | delta = (datetime.datetime.now() - start).total_seconds() 68 | return delta * 1000 69 | 70 | 71 | @app.errorhandler(InvalidUsage) 72 | def handle_invalid_usage(error): 73 | g.ev.add_field("errors.message", error.message) 74 | response = jsonify(error.to_dict()) 75 | response.status_code = error.status_code 76 | return response 77 | 78 | 79 | @app.before_request 80 | def before(): 81 | # g is the thread-local / request-local variable, we will use it to store 82 | # information that will be used when we eventually send the event to 83 | # Honeycomb, including a timer for the whole request duration. 84 | g.req_start = datetime.datetime.now() 85 | g.ev = libhoney_builder.new_event() 86 | g.ev.add_field("request.path", request.path) 87 | g.ev.add_field("request.method", request.method) 88 | g.ev.add_field("request.user_agent.browser", request.user_agent.browser) 89 | g.ev.add_field("request.user_agent.platform", request.user_agent.platform) 90 | g.ev.add_field("request.user_agent.language", request.user_agent.language) 91 | g.ev.add_field("request.user_agent.string", request.user_agent.string) 92 | g.ev.add_field("request.user_agent.version", request.user_agent.version) 93 | g.ev.add_field("request.python_function", request.endpoint) 94 | g.ev.add_field("request.url_pattern", str(request.url_rule)) 95 | 96 | 97 | @app.after_request 98 | def after(response): 99 | g.ev.add_field("response.status_code", response.status_code) 100 | 101 | # Note that this isn't the total time to serve the request, i.e., how long 102 | # the end user is waiting. It accounts for the time spent in the Flask 103 | # handlers but not Werkzeug, etc. Ingesting edge data from ELB or 104 | # nginx etc. is usually much better for that kind of (total request time) info. 105 | g.ev.add_field("timers.flask_time_ms", milliseconds_since(g.req_start)) 106 | 107 | app.logger.debug(g.ev) 108 | g.ev.send() 109 | return response 110 | 111 | 112 | @app.route("/") 113 | def index(): 114 | return jsonify(up=True) 115 | 116 | 117 | @app.route("/todos/", defaults={"todo_id": None}, methods=["GET", "POST"]) 118 | @app.route("/todos//", methods=["GET", "DELETE", "PUT"]) 119 | def todo(todo_id): 120 | if todo_id is None: 121 | if request.method == "POST": 122 | json = request.get_json() 123 | todo = Todo( 124 | description=json.get("description", ""), 125 | completed=json.get("completed", False), 126 | due=datetime.datetime.fromtimestamp(json.get("due", None)), 127 | ) 128 | 129 | insert_todo_start = datetime.datetime.now() 130 | 131 | db.session.add(todo) 132 | db.session.commit() 133 | 134 | g.ev.add_field("timers.db.insert_todo_ms", milliseconds_since(insert_todo_start)) 135 | 136 | # Augment events with high cardinality information! 137 | g.ev.add_field("todo.id", todo.id) 138 | 139 | return jsonify(todo.serialize()) 140 | 141 | if request.method == "GET": 142 | select_all_todos_start = datetime.datetime.now() 143 | todos = [todo.serialize() for todo in Todo.query.all()] 144 | g.ev.add_field("timers.db.select_all_todos", milliseconds_since(select_all_todos_start)) 145 | return jsonify(todos) 146 | 147 | raise InvalidUsage("Method not allowed", status_code=405) 148 | else: 149 | # Augment events with high cardinality information! 150 | g.ev.add_field("todo.id", todo_id) 151 | 152 | select_todo_start = datetime.datetime.now() 153 | todo = Todo.query.get(todo_id) 154 | g.ev.add_field("timers.db.select_todo", milliseconds_since(select_todo_start)) 155 | 156 | if todo is None: 157 | err = "Todo not found" 158 | g.ev.add_field("errors.message", err) 159 | return jsonify({"error": err}), 404 160 | 161 | if request.method == "GET": 162 | return jsonify(todo.serialize()) 163 | 164 | elif request.method == "DELETE": 165 | delete_todo_start = datetime.datetime.now() 166 | db.session.delete(todo) 167 | db.session.commit() 168 | g.ev.add_field("timers.db.delete_todo", milliseconds_since(delete_todo_start)) 169 | 170 | return jsonify({"success": True, "id": todo_id}) 171 | 172 | elif request.method == "PUT": 173 | json = request.get_json() 174 | todo.description = json.get("description", todo.description) 175 | todo.completed = json.get("completed", todo.completed) 176 | app.logger.debug(json.get("completed", todo.completed)) 177 | todo.due = json.get("due", todo.due), 178 | 179 | update_todo_start = datetime.datetime.now() 180 | db.session.add(todo) 181 | db.session.commit() 182 | g.ev.add_field("timers.db.update_todo", milliseconds_since(update_todo_start)) 183 | 184 | return jsonify(todo.serialize()) 185 | 186 | raise InvalidUsage("Method not allowed", status_code=405) 187 | -------------------------------------------------------------------------------- /examples/flask/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - "5000:5000" 8 | networks: 9 | - main 10 | environment: 11 | DB_HOST: db 12 | HONEYCOMB_API_KEY: 13 | restart: on-failure 14 | depends_on: 15 | - "db" 16 | 17 | db: 18 | image: mysql 19 | networks: 20 | - main 21 | volumes: 22 | - example-python-api:/var/lib 23 | environment: 24 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 25 | MYSQL_DATABASE: "example-python-api" 26 | command: ["mysqld"] 27 | 28 | volumes: 29 | example-python-api: 30 | 31 | networks: 32 | main: 33 | driver: bridge 34 | -------------------------------------------------------------------------------- /examples/flask/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney-flask-example" 3 | version = "0.1.0" 4 | description = "Flask app instrumented with libhoney" 5 | authors = ["Your Name "] 6 | packages = [{include = "libhoney-flask-example"}] 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | Flask = "^2.3.2" 11 | flask-sqlalchemy = "^3.0.2" 12 | libhoney = "^2.3.0" 13 | mysqlclient = "^2.1.1" 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /libhoney/__init__.py: -------------------------------------------------------------------------------- 1 | '''libhoney is a library to allow you to send events to Honeycomb from within 2 | your python application. 3 | 4 | Basic usage: 5 | 6 | - initialize libhoney with your Honeycomb writekey and dataset name 7 | - create an event object and populate it with fields 8 | - send the event object 9 | - close libhoney when your program is finished 10 | 11 | Sending on a closed or uninitialized libhoney will throw a `libhoney.SendError` 12 | exception. 13 | 14 | You can find an example demonstrating usage in example.py''' 15 | 16 | import atexit 17 | import random 18 | from queue import Queue 19 | 20 | from libhoney import state 21 | from libhoney.client import Client, IsClassicKey 22 | from libhoney.builder import Builder 23 | from libhoney.event import Event 24 | from libhoney.fields import FieldHolder 25 | from libhoney.errors import SendError 26 | 27 | random.seed() 28 | 29 | 30 | def init(writekey="", dataset="", sample_rate=1, 31 | api_host="https://api.honeycomb.io", max_concurrent_batches=10, 32 | max_batch_size=100, send_frequency=0.25, 33 | block_on_send=False, block_on_response=False, transmission_impl=None, 34 | debug=False): 35 | '''Initialize libhoney and prepare it to send events to Honeycomb. This creates 36 | a global Client object that is configured with the supplied parameters. For some 37 | advanced used cases, you might consider creating a Client object directly, but 38 | `init` is the quickest path to getting data into Honeycomb. 39 | 40 | Note that libhoney initialization initializes a number of threads to handle 41 | sending payloads to Honeycomb. Be mindful of where you're calling 42 | `libhoney.init()` in order to ensure correct enqueueing + processing of 43 | events on the spawned threads. 44 | 45 | Args: 46 | 47 | - `writekey`: the authorization key for your team on Honeycomb. Find your team 48 | write key at [https://ui.honeycomb.io/account](https://ui.honeycomb.io/account) 49 | - `dataset`: the name of the default dataset to which to write 50 | - `sample_rate`: the default sample rate. 1 / `sample_rate` events will be sent. 51 | - `api_host`: the protocol and Honeycomb api endpoint to send to; defaults to `https://api.honeycomb.io`. 52 | - `max_concurrent_batches`: the maximum number of concurrent threads sending events. 53 | - `max_batch_size`: the maximum number of events to batch before sendinga. 54 | - `send_frequency`: how long to wait before sending a batch of events, in seconds. 55 | - `block_on_send`: if true, block when send queue fills. If false, drop 56 | events until there's room in the queue 57 | - `block_on_response`: if true, block when the response queue fills. If 58 | false, drop response objects. 59 | - `transmission_impl`: if set, override the default transmission implementation (for example, TornadoTransmission) 60 | 61 | -------- 62 | 63 | **Configuration recommendations**: 64 | 65 | **For gunicorn**, use a [`post_worker_init` config hook](http://docs.gunicorn.org/en/stable/settings.html#post-worker-init) 66 | to initialize Honeycomb: 67 | 68 | # conf.py 69 | import logging 70 | import os 71 | 72 | def post_worker_init(worker): 73 | logging.info(f'libhoney initialization in process pid {os.getpid()}') 74 | libhoney.init(writekey="YOUR_WRITE_KEY", dataset="dataset_name") 75 | 76 | Then start gunicorn with the `-c` option: 77 | 78 | gunicorn -c /path/to/conf.py 79 | ''' 80 | state.G_CLIENT = Client( 81 | writekey=writekey, 82 | dataset=dataset, 83 | sample_rate=sample_rate, 84 | api_host=api_host, 85 | max_concurrent_batches=max_concurrent_batches, 86 | max_batch_size=max_batch_size, 87 | send_frequency=send_frequency, 88 | block_on_send=block_on_send, 89 | block_on_response=block_on_response, 90 | transmission_impl=transmission_impl, 91 | debug=debug, 92 | ) 93 | 94 | 95 | def responses(): 96 | '''Returns a queue from which you can read a record of response info from 97 | each event sent by the global client. Responses will be dicts with the 98 | following keys: 99 | 100 | - `status_code` - the HTTP response from the api (eg. 200 or 503) 101 | - `duration` - how long it took to POST this event to the api, in ms 102 | - `metadata` - pass through the metadata you added on the initial event 103 | - `body` - the content returned by API (will be empty on success) 104 | - `error` - in an error condition, this is filled with the error message 105 | 106 | When a None object appears on the queue the reader should exit''' 107 | if state.G_CLIENT is None: 108 | state.warn_uninitialized() 109 | # return an empty queue rather than None. While not ideal, it is 110 | # better than returning None and introducing AttributeErrors into 111 | # the caller's code 112 | return Queue() 113 | 114 | return state.G_CLIENT.responses() 115 | 116 | 117 | def add_field(name, val): 118 | '''Add a field to the global client. This field will be sent with every event.''' 119 | if state.G_CLIENT is None: 120 | state.warn_uninitialized() 121 | return 122 | state.G_CLIENT.add_field(name, val) 123 | 124 | 125 | def add_dynamic_field(fn): 126 | '''Add a dynamic field to the global client. This function will be executed every time an 127 | event is created. The key/value pair of the function's name and its 128 | return value will be sent with every event.''' 129 | if state.G_CLIENT is None: 130 | state.warn_uninitialized() 131 | return 132 | state.G_CLIENT.add_dynamic_field(fn) 133 | 134 | 135 | def add(data): 136 | '''Add takes a mappable object and adds each key/value pair to the global client. 137 | These key/value pairs will be sent with every event created by the global client.''' 138 | if state.G_CLIENT is None: 139 | state.warn_uninitialized() 140 | return 141 | state.G_CLIENT.add(data) 142 | 143 | 144 | def new_event(data={}): 145 | ''' Creates a new event with the global client. If libhoney has not been 146 | initialized, sending this event will be a no-op. 147 | ''' 148 | return Event(data=data, client=state.G_CLIENT) 149 | 150 | 151 | def send_now(data): 152 | ''' 153 | DEPRECATED - This will likely be removed in a future major version. 154 | 155 | Creates an event with the data passed in and enqueues it to be sent. 156 | Contrary to the name, it does not block the application when called. 157 | 158 | Shorthand for: 159 | 160 | ev = libhoney.Event() 161 | ev.add(data) 162 | ev.send() 163 | ''' 164 | if state.G_CLIENT is None: 165 | state.warn_uninitialized() 166 | return 167 | ev = Event(client=state.G_CLIENT) 168 | ev.add(data) 169 | ev.send() 170 | 171 | 172 | def flush(): 173 | '''Closes and restarts the transmission, sending all enqueued events 174 | created by the global client. Use this if you want to perform a blocking 175 | send of all events in your application. 176 | 177 | Note: does not work with asynchronous Transmission implementations such 178 | as TornadoTransmission. 179 | ''' 180 | if state.G_CLIENT: 181 | state.G_CLIENT.flush() 182 | 183 | 184 | def close(): 185 | '''Wait for in-flight events to be transmitted then shut down cleanly. 186 | Optional (will be called automatically at exit) unless your 187 | application is consuming from the responses queue and needs to know 188 | when all responses have been received.''' 189 | if state.G_CLIENT: 190 | state.G_CLIENT.close() 191 | 192 | # we should error on post-close sends 193 | state.G_CLIENT = None 194 | 195 | 196 | atexit.register(close) # safe because it's a no-op unless init() was called 197 | 198 | # export everything 199 | __all__ = [ 200 | "Builder", "Event", "Client", "IsClassicKey", "FieldHolder", 201 | "SendError", "add", "add_dynamic_field", 202 | "add_field", "close", "init", "responses", "send_now", 203 | ] 204 | -------------------------------------------------------------------------------- /libhoney/builder.py: -------------------------------------------------------------------------------- 1 | from libhoney import state 2 | from libhoney.event import Event 3 | from libhoney.fields import FieldHolder 4 | 5 | 6 | class Builder(object): 7 | '''A Builder is a scoped object to which you can add fields and dynamic 8 | fields. Events created from this builder will inherit all fields 9 | and dynamic fields from this builder and the global environment''' 10 | 11 | def __init__(self, data={}, dyn_fields=[], fields=FieldHolder(), client=None): 12 | # if no client is specified, use the global client if possible 13 | if client is None: 14 | client = state.G_CLIENT 15 | 16 | # copy configuration from client if possible 17 | self.client = client 18 | if self.client: 19 | self.writekey = client.writekey 20 | self.dataset = client.dataset 21 | self.api_host = client.api_host 22 | self.sample_rate = client.sample_rate 23 | else: 24 | self.writekey = None 25 | self.dataset = None 26 | self.api_host = 'https://api.honeycomb.io' 27 | self.sample_rate = 1 28 | 29 | self._fields = FieldHolder() # get an empty FH 30 | if self.client: 31 | self._fields += self.client.fields # fill it with the client fields 32 | self._fields.add(data) # and anything passed in 33 | [self._fields.add_dynamic_field(fn) for fn in dyn_fields] 34 | self._fields += fields 35 | 36 | def add_field(self, name, val): 37 | self._fields.add_field(name, val) 38 | 39 | def add_dynamic_field(self, fn): 40 | '''`add_dynamic_field` adds a function to the builder. When you create an 41 | event from this builder, the function will be executed. The function 42 | name is the key and it should return one value.''' 43 | self._fields.add_dynamic_field(fn) 44 | 45 | def add(self, data): 46 | '''add takes a dict-like object and adds each key/value pair to the 47 | builder.''' 48 | self._fields.add(data) 49 | 50 | def send_now(self, data): 51 | ''' 52 | DEPRECATED - This will likely be removed in a future major version. 53 | 54 | Creates an event with the data passed in and enqueues it to be sent. 55 | Contrary to the name, it does not block the application when called. 56 | 57 | Shorthand for: 58 | 59 | ev = builder.new_event() 60 | ev.add(data) 61 | ev.send() 62 | ''' 63 | ev = self.new_event() 64 | ev.add(data) 65 | ev.send() 66 | 67 | def new_event(self): 68 | '''creates a new event from this builder, inheriting all fields and 69 | dynamic fields present in the builder''' 70 | ev = Event(fields=self._fields, client=self.client) 71 | ev.writekey = self.writekey 72 | ev.dataset = self.dataset 73 | ev.api_host = self.api_host 74 | ev.sample_rate = self.sample_rate 75 | return ev 76 | 77 | def clone(self): 78 | '''creates a new builder from this one, creating its own scope to 79 | which additional fields and dynamic fields can be added.''' 80 | c = Builder(fields=self._fields, client=self.client) 81 | c.writekey = self.writekey 82 | c.dataset = self.dataset 83 | c.sample_rate = self.sample_rate 84 | c.api_host = self.api_host 85 | return c 86 | -------------------------------------------------------------------------------- /libhoney/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import queue 4 | 5 | from libhoney.event import Event 6 | from libhoney.builder import Builder 7 | from libhoney.fields import FieldHolder 8 | from libhoney.transmission import Transmission 9 | 10 | 11 | def IsClassicKey(key): 12 | '''Returns true if the API key is a Classic key or a Classic Ingest Key''' 13 | if not key: 14 | return True 15 | if re.match(r'^[a-f0-9]{32}$', key): 16 | return True 17 | if re.match(r'^hc[a-z]ic_[a-z0-9]{58}$', key): 18 | return True 19 | return False 20 | 21 | 22 | class Client(object): 23 | '''Instantiate a libhoney Client that can prepare and send events to Honeycomb. 24 | 25 | Note that libhoney Clients initialize a number of threads to handle 26 | sending payloads to Honeycomb. Client initialization is heavy, and re-use 27 | of Client objects is encouraged. If you must use multiple clients, consider 28 | reducing `max_concurrent_batches` to reduce the number of threads per client. 29 | 30 | When using a Client instance, you need to use the Client to generate Event and Builder 31 | objects. Examples: 32 | 33 | ``` 34 | c = Client(writekey="mywritekey", dataset="mydataset") 35 | ev = c.new_event() 36 | ev.add_field("foo", "bar") 37 | ev.send() 38 | ``` 39 | 40 | ``` 41 | c = Client(writekey="mywritekey", dataset="mydataset") 42 | b = c.new_builder() 43 | b.add_field("foo", "bar") 44 | ev = b.new_event() 45 | ev.send() 46 | ``` 47 | 48 | To ensure that events are flushed before program termination, you should explicitly call `close()` 49 | on your Client instance. 50 | 51 | Args: 52 | 53 | - `writekey`: the authorization key for your team on Honeycomb. Find your team 54 | write key at [https://ui.honeycomb.io/account](https://ui.honeycomb.io/account) 55 | - `dataset`: the name of the default dataset to which to write 56 | - `sample_rate`: the default sample rate. 1 / `sample_rate` events will be sent. 57 | - `max_concurrent_batches`: the maximum number of concurrent threads sending events. 58 | - `max_batch_size`: the maximum number of events to batch before sending. 59 | - `send_frequency`: how long to wait before sending a batch of events, in seconds. 60 | - `block_on_send`: if true, block when send queue fills. If false, drop 61 | events until there's room in the queue 62 | - `block_on_response`: if true, block when the response queue fills. If 63 | false, drop response objects. 64 | - `transmission_impl`: if set, override the default transmission implementation 65 | (for example, TornadoTransmission) 66 | - `user_agent_addition`: if set, its contents will be appended to the 67 | User-Agent string, separated by a space. The expected format is 68 | product-name/version, eg "myapp/1.0" 69 | ''' 70 | 71 | def __init__(self, writekey="", dataset="", sample_rate=1, 72 | api_host="https://api.honeycomb.io", 73 | max_concurrent_batches=10, max_batch_size=100, 74 | send_frequency=0.25, block_on_send=False, 75 | block_on_response=False, transmission_impl=None, 76 | user_agent_addition='', debug=False): 77 | 78 | self.xmit = transmission_impl 79 | if self.xmit is None: 80 | self.xmit = Transmission( 81 | max_concurrent_batches=max_concurrent_batches, block_on_send=block_on_send, block_on_response=block_on_response, 82 | user_agent_addition=user_agent_addition, debug=debug, 83 | ) 84 | 85 | self.xmit.start() 86 | self.writekey = writekey 87 | self.dataset = dataset 88 | self.api_host = api_host 89 | self.sample_rate = sample_rate 90 | self._responses = self.xmit.get_response_queue() 91 | self.block_on_response = block_on_response 92 | 93 | self.fields = FieldHolder() 94 | 95 | self.debug = debug 96 | if debug: 97 | self._init_logger() 98 | 99 | self.log('initialized honeycomb client: writekey=%s dataset=%s', 100 | writekey, dataset) 101 | if not writekey: 102 | self.log( 103 | 'writekey not set! set the writekey if you want to send data to honeycomb') 104 | if not dataset: 105 | if IsClassicKey(writekey): 106 | self.log( 107 | 'dataset not set! set a value for dataset if you want to send data to honeycomb') 108 | else: 109 | logging.error( 110 | 'dataset not set! sending to unknown_dataset' 111 | ) 112 | self.dataset = "unknown_dataset" 113 | 114 | # whitespace detected. trim whitespace, warn on diff 115 | if dataset.strip() != dataset and not IsClassicKey(writekey): 116 | logging.error( 117 | 'dataset has unexpected spaces' 118 | ) 119 | self.dataset = dataset.strip() 120 | 121 | # enable use in a context manager 122 | def __enter__(self): 123 | return self 124 | 125 | def __exit__(self, typ, value, tb): 126 | '''Clean up Transmission if client gets garbage collected''' 127 | self.close() 128 | 129 | def _init_logger(self): 130 | import logging # pylint: disable=bad-option-value,import-outside-toplevel 131 | self._logger = logging.getLogger('honeycomb-sdk') 132 | self._logger.setLevel(logging.DEBUG) 133 | ch = logging.StreamHandler() 134 | ch.setLevel(logging.DEBUG) 135 | formatter = logging.Formatter( 136 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 137 | ch.setFormatter(formatter) 138 | self._logger.addHandler(ch) 139 | 140 | def log(self, msg, *args, **kwargs): 141 | if self.debug: 142 | self._logger.debug(msg, *args, **kwargs) 143 | 144 | def responses(self): 145 | '''Returns a queue from which you can read a record of response info from 146 | each event sent. Responses will be dicts with the following keys: 147 | 148 | - `status_code` - the HTTP response from the api (eg. 200 or 503) 149 | - `duration` - how long it took to POST this event to the api, in ms 150 | - `metadata` - pass through the metadata you added on the initial event 151 | - `body` - the content returned by API (will be empty on success) 152 | - `error` - in an error condition, this is filled with the error message 153 | 154 | When the Client's `close` method is called, a None will be inserted on 155 | the queue, indicating that no further responses will be written. 156 | ''' 157 | return self._responses 158 | 159 | def add_field(self, name, val): 160 | '''add a global field. This field will be sent with every event.''' 161 | self.fields.add_field(name, val) 162 | 163 | def add_dynamic_field(self, fn): 164 | '''add a global dynamic field. This function will be executed every time an 165 | event is created. The key/value pair of the function's name and its 166 | return value will be sent with every event.''' 167 | self.fields.add_dynamic_field(fn) 168 | 169 | def add(self, data): 170 | '''add takes a mappable object and adds each key/value pair to the 171 | global scope''' 172 | self.fields.add(data) 173 | 174 | def send(self, event): 175 | '''Enqueues the given event to be sent to Honeycomb. 176 | 177 | Should not be called directly. Instead, use Event: 178 | ev = client.new_event() 179 | ev.add(data) 180 | ev.send() 181 | ''' 182 | if self.xmit is None: 183 | self.log( 184 | "tried to send on a closed or uninitialized libhoney client," 185 | " ev = %s", event.fields()) 186 | return 187 | 188 | self.log("send enqueuing event ev = %s", event.fields()) 189 | self.xmit.send(event) 190 | 191 | def send_now(self, data): 192 | ''' 193 | DEPRECATED - This will likely be removed in a future major version. 194 | 195 | Creates an event with the data passed in and enqueues it to be sent. 196 | Contrary to the name, it does not block the application when called. 197 | 198 | Shorthand for: 199 | 200 | ev = client.new_event() 201 | ev.add(data) 202 | ev.send() 203 | ''' 204 | ev = self.new_event() 205 | ev.add(data) 206 | self.log("send_now enqueuing event ev = %s", ev.fields()) 207 | ev.send() 208 | 209 | def send_dropped_response(self, event): 210 | '''push the dropped event down the responses queue''' 211 | response = { 212 | "status_code": 0, 213 | "duration": 0, 214 | "metadata": event.metadata, 215 | "body": "", 216 | "error": "event dropped due to sampling", 217 | } 218 | self.log("enqueuing response = %s", response) 219 | try: 220 | if self.block_on_response: 221 | self._responses.put(response) 222 | else: 223 | self._responses.put_nowait(response) 224 | except queue.Full: 225 | pass 226 | 227 | def close(self): 228 | '''Wait for in-flight events to be transmitted then shut down cleanly. 229 | Optional (will be called automatically at exit) unless your 230 | application is consuming from the responses queue and needs to know 231 | when all responses have been received.''' 232 | 233 | if self.xmit: 234 | self.xmit.close() 235 | 236 | # we should error on post-close sends 237 | self.xmit = None 238 | 239 | def flush(self): 240 | '''Closes and restarts the transmission, sending all events. Use this 241 | if you want to perform a blocking send of all events in your 242 | application. 243 | 244 | Note: does not work with asynchronous Transmission implementations such 245 | as TornadoTransmission. 246 | ''' 247 | if self.xmit and isinstance(self.xmit, Transmission): 248 | self.xmit.close() 249 | self.xmit.start() 250 | 251 | def new_event(self, data={}): 252 | '''Return an Event, initialized to be sent with this client''' 253 | ev = Event(data=data, client=self) 254 | return ev 255 | 256 | def new_builder(self, data=None, dyn_fields=None, fields=None): 257 | '''Return a Builder. Events built from this builder will be sent with 258 | this client''' 259 | if data is None: 260 | data = {} 261 | if dyn_fields is None: 262 | dyn_fields = [] 263 | if fields is None: 264 | fields = FieldHolder() 265 | builder = Builder(data, dyn_fields, fields, self) 266 | return builder 267 | -------------------------------------------------------------------------------- /libhoney/errors.py: -------------------------------------------------------------------------------- 1 | class SendError(Exception): 2 | ''' raised when send is called on an event that cannot be sent, such as: 3 | - when it lacks a writekey 4 | - dataset is not specified 5 | - no fields are set 6 | ''' 7 | pass 8 | -------------------------------------------------------------------------------- /libhoney/event.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | from contextlib import contextmanager 4 | 5 | from libhoney import state 6 | from libhoney.fields import FieldHolder 7 | 8 | 9 | class Event(object): 10 | '''An Event is a collection of fields that will be sent to Honeycomb.''' 11 | 12 | def __init__(self, data={}, dyn_fields=[], fields=FieldHolder(), client=None): 13 | if client is None: 14 | client = state.G_CLIENT 15 | 16 | # copy configuration from client 17 | self.client = client 18 | if self.client: 19 | self.writekey = client.writekey 20 | self.dataset = client.dataset 21 | self.api_host = client.api_host 22 | self.sample_rate = client.sample_rate 23 | else: 24 | self.writekey = None 25 | self.dataset = None 26 | self.api_host = 'https://api.honeycomb.io' 27 | self.sample_rate = 1 28 | 29 | # populate the event's fields 30 | self._fields = FieldHolder() # get an empty FH 31 | if self.client: 32 | self._fields += self.client.fields # fill it with the client fields 33 | self._fields.add(data) # and anything passed in 34 | [self._fields.add_dynamic_field(fn) for fn in dyn_fields] 35 | self._fields += fields 36 | 37 | # fill in other info 38 | self.created_at = datetime.datetime.utcnow() 39 | self.metadata = None 40 | # execute all the dynamic functions and add their data 41 | for fn in self._fields._dyn_fields: 42 | self._fields.add_field(fn.__name__, fn()) 43 | 44 | def add_field(self, name, val): 45 | self._fields.add_field(name, val) 46 | 47 | def add_metadata(self, md): 48 | '''Add metadata to an event. This metadata is handed back to you in 49 | the response queue. It is not transmitted to Honeycomb; it is a place 50 | for you to put identifying information to understand which event a 51 | response queue object represents.''' 52 | self.metadata = md 53 | 54 | def add(self, data): 55 | self._fields.add(data) 56 | 57 | @contextmanager 58 | def timer(self, name): 59 | '''timer is a context for timing (in milliseconds) a function call. 60 | 61 | Example: 62 | 63 | ev = Event() 64 | with ev.timer("database_dur_ms"): 65 | do_database_work() 66 | 67 | will add a field (name, duration) indicating how long it took to run 68 | do_database_work()''' 69 | start = datetime.datetime.now() 70 | yield 71 | duration = datetime.datetime.now() - start 72 | # report in ms 73 | self.add_field(name, duration.total_seconds() * 1000) 74 | 75 | def send(self): 76 | '''send queues this event for transmission to Honeycomb. 77 | 78 | Will drop sampled events when sample_rate > 1, 79 | and ensure that the Honeycomb datastore correctly considers it 80 | as representing `sample_rate` number of similar events.''' 81 | # warn if we're not using a client instance and global libhoney 82 | # is not initialized. This will result in a noop, but is better 83 | # than crashing the caller if they forget to initialize 84 | if self.client is None: 85 | state.warn_uninitialized() 86 | return 87 | 88 | if _should_drop(self.sample_rate): 89 | self.client.send_dropped_response(self) 90 | return 91 | 92 | self.send_presampled() 93 | 94 | def send_presampled(self): 95 | '''send_presampled queues this event for transmission to Honeycomb. 96 | 97 | Caller is responsible for sampling logic - will not drop any events 98 | for sampling. Defining a `sample_rate` will ensure that the Honeycomb 99 | datastore correctly considers it as representing `sample_rate` number 100 | of similar events. 101 | 102 | Raises SendError if no fields are defined or critical attributes not 103 | set (writekey, dataset, api_host).''' 104 | if self._fields.is_empty(): 105 | self.client.log( 106 | "No metrics added to event. Won't send empty event.") 107 | return 108 | if self.api_host == "": 109 | self.client.log( 110 | "No api_host for Honeycomb. Can't send to the Great Unknown.") 111 | return 112 | if self.writekey == "": 113 | self.client.log("No writekey specified. Can't send event.") 114 | return 115 | if self.dataset == "": 116 | self.client.log( 117 | "No dataset for Honeycomb. Can't send event without knowing which dataset it belongs to.") 118 | return 119 | 120 | if self.client: 121 | self.client.send(self) 122 | else: 123 | state.warn_uninitialized() 124 | 125 | def __str__(self): 126 | return str(self._fields) 127 | 128 | def fields(self): 129 | return self._fields._data 130 | 131 | 132 | def _should_drop(rate): 133 | '''returns true if the sample should be dropped''' 134 | return random.randint(1, rate) != 1 135 | -------------------------------------------------------------------------------- /libhoney/fields.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | from libhoney.internal import json_default_handler 4 | 5 | 6 | class FieldHolder: 7 | '''A FieldHolder is the generalized class that stores fields and dynamic 8 | fields. It should not be used directly; only through the subclasses''' 9 | 10 | def __init__(self): 11 | self._data = {} 12 | self._dyn_fields = set() 13 | 14 | def __add__(self, other): 15 | '''adding two field holders merges the data with other overriding 16 | any fields they have in common''' 17 | self._data.update(other._data) 18 | self._dyn_fields.update(other._dyn_fields) 19 | return self 20 | 21 | def __eq__(self, other): 22 | '''two FieldHolders are equal if their datasets are equal''' 23 | return ((self._data, self._dyn_fields) == 24 | (other._data, other._dyn_fields)) 25 | 26 | def __ne__(self, other): 27 | '''two FieldHolders are equal if their datasets are equal''' 28 | return not self.__eq__(other) 29 | 30 | def add_field(self, name, val): 31 | self._data[name] = val 32 | 33 | def add_dynamic_field(self, fn): 34 | if not inspect.isroutine(fn): 35 | raise TypeError("add_dynamic_field requires function argument") 36 | self._dyn_fields.add(fn) 37 | 38 | def add(self, data): 39 | try: 40 | for k, v in data.items(): 41 | self.add_field(k, v) 42 | except AttributeError: 43 | raise TypeError("add requires a dict-like argument") from None 44 | 45 | def is_empty(self): 46 | '''returns true if there is no data in this FieldHolder''' 47 | return len(self._data) == 0 48 | 49 | def __str__(self): 50 | '''returns a JSON blob of the fields in this holder''' 51 | return json.dumps(self._data, default=json_default_handler) 52 | -------------------------------------------------------------------------------- /libhoney/internal.py: -------------------------------------------------------------------------------- 1 | def json_default_handler(obj): 2 | ''' this function handles values that the json encoder does not understand 3 | by attempting to call the object's __str__ method. ''' 4 | try: 5 | return str(obj) 6 | except Exception: 7 | return 'libhoney was unable to encode value' 8 | -------------------------------------------------------------------------------- /libhoney/state.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | G_CLIENT = None 4 | WARNED_UNINITIALIZED = False 5 | 6 | 7 | def warn_uninitialized(): 8 | # warn once if we attempt to use the global state before initialized 9 | log = logging.getLogger(__name__) 10 | global WARNED_UNINITIALIZED 11 | if not WARNED_UNINITIALIZED: 12 | log.warning("global libhoney method used before initialization") 13 | WARNED_UNINITIALIZED = True 14 | -------------------------------------------------------------------------------- /libhoney/test_client.py: -------------------------------------------------------------------------------- 1 | '''Tests for libhoney/client.py''' 2 | 3 | import unittest 4 | from unittest import mock 5 | 6 | import libhoney 7 | from libhoney import client 8 | from libhoney.client import IsClassicKey 9 | 10 | 11 | def sample_dyn_fn(): 12 | return "dyna", "magic" 13 | 14 | 15 | class TestClient(unittest.TestCase): 16 | def setUp(self): 17 | libhoney.close() 18 | 19 | self.tx = mock.Mock() 20 | self.m_xmit = mock.patch('libhoney.client.Transmission') 21 | self.m_xmit.start().return_value = self.tx 22 | 23 | def tearDown(self): 24 | self.m_xmit.stop() 25 | 26 | def test_init(self): 27 | c = client.Client(writekey="foo", dataset="bar", api_host="blup", 28 | sample_rate=2) 29 | self.assertEqual(c.writekey, "foo") 30 | self.assertEqual(c.dataset, "bar") 31 | self.assertEqual(c.api_host, "blup") 32 | self.assertEqual(c.sample_rate, 2) 33 | self.assertEqual(c.xmit, self.tx) 34 | 35 | def test_init_sets_no_default_key_if_missing(self): 36 | c = client.Client(writekey="", dataset="test") 37 | self.assertEqual(c.writekey, "") 38 | 39 | def test_init_sets_no_default_dataset_if_classic_key(self): 40 | c = client.Client(writekey="c1a551c1111111111111111111111111", dataset="") 41 | self.assertEqual(c.writekey, "c1a551c1111111111111111111111111") 42 | self.assertEqual(c.dataset, "") 43 | 44 | def test_init_sets_no_default_dataset_if_missing_key(self): 45 | c = client.Client(writekey="", dataset="") 46 | self.assertEqual(c.writekey, "") 47 | self.assertEqual(c.dataset, "") 48 | 49 | def test_init_treats_classic_ingest_key_as_classic_key(self): 50 | c = client.Client(writekey="hcxic_1234567890123456789012345678901234567890123456789012345678", dataset="") 51 | self.assertEqual(c.writekey, "hcxic_1234567890123456789012345678901234567890123456789012345678") 52 | self.assertEqual(c.dataset, "") 53 | 54 | def test_init_treats_ingest_key_as_non_classic_key(self): 55 | c = client.Client(writekey="hcxik_1234567890123456789012345678901234567890123456789012345678", dataset="") 56 | self.assertEqual(c.writekey, "hcxik_1234567890123456789012345678901234567890123456789012345678") 57 | self.assertEqual(c.dataset, "unknown_dataset") 58 | 59 | def test_init_sets_default_dataset_with_non_classic_key(self): 60 | c = client.Client(writekey="shinynewenvironmentkey", dataset="") 61 | self.assertEqual(c.writekey, "shinynewenvironmentkey") 62 | self.assertEqual(c.dataset, "unknown_dataset") 63 | 64 | def test_init_does_not_trim_dataset_with_classic_key(self): 65 | c = client.Client(writekey="c1a551c1111111111111111111111111", dataset=" my dataset ") 66 | self.assertEqual(c.writekey, "c1a551c1111111111111111111111111") 67 | self.assertEqual(c.dataset, " my dataset ") 68 | 69 | def test_init_trims_dataset_with_non_classic_key(self): 70 | c = client.Client(writekey="shinynewenvironmentkey", dataset=" my dataset ") 71 | self.assertEqual(c.writekey, "shinynewenvironmentkey") 72 | self.assertEqual(c.dataset, "my dataset") 73 | 74 | def test_init_sets_default_api_host(self): 75 | c = client.Client(writekey="foo", dataset="bar") 76 | self.assertEqual(c.api_host, "https://api.honeycomb.io") 77 | 78 | def test_close(self): 79 | c = client.Client(writekey="foo", dataset="bar", api_host="blup", 80 | sample_rate=2) 81 | c.close() 82 | self.tx.close.assert_called_with() 83 | self.assertEqual(c.xmit, None) 84 | 85 | def test_close_noop_if_not_init(self): 86 | """ 87 | libhoney.close() should noop if never initialized 88 | """ 89 | try: 90 | c = client.Client() 91 | c.close() 92 | # close again, should be a noop and not cause an exception 93 | c.close() 94 | except AttributeError: 95 | self.fail('libhoney threw an exception on ' 96 | 'an uninitialized close.') 97 | 98 | def test_add_field(self): 99 | ed = {"whomp": True} 100 | with client.Client() as c: 101 | c.add_field("whomp", True) 102 | self.assertEqual(c.fields._data, ed) 103 | 104 | def test_add_dynamic_field(self): 105 | ed = set([sample_dyn_fn]) 106 | with client.Client() as c: 107 | c.add_dynamic_field(sample_dyn_fn) 108 | self.assertEqual(c.fields._dyn_fields, ed) 109 | 110 | def test_add(self): 111 | ed = {"whomp": True} 112 | with client.Client() as c: 113 | c.add(ed) 114 | self.assertEqual(c.fields._data, ed) 115 | 116 | def test_new_event(self): 117 | with client.Client(writekey="client_key", dataset="client_dataset") as c: 118 | c.add_field("whomp", True) 119 | c.add_dynamic_field(sample_dyn_fn) 120 | 121 | ev = c.new_event({"field1": 1}) 122 | ev.add_field("field2", 2) 123 | # ensure client config is passed through 124 | self.assertEqual(ev.client, c) 125 | self.assertEqual(ev.writekey, "client_key") 126 | self.assertEqual(ev.dataset, "client_dataset") 127 | # ensure client fields are passed on 128 | self.assertEqual(ev._fields._data, { 129 | "field1": 1, "field2": 2, "whomp": True, "sample_dyn_fn": ("dyna", "magic")}) 130 | self.assertEqual(ev._fields._dyn_fields, set([sample_dyn_fn])) 131 | 132 | def test_new_builder(self): 133 | with client.Client(writekey="client_key", dataset="client_dataset") as c: 134 | c.add_field("whomp", True) 135 | c.add_dynamic_field(sample_dyn_fn) 136 | 137 | b = c.new_builder() 138 | # ensure client config is passed through 139 | self.assertEqual(b.client, c) 140 | self.assertEqual(b.writekey, "client_key") 141 | self.assertEqual(b.dataset, "client_dataset") 142 | b.add_field("builder_field", 1) 143 | 144 | ev = b.new_event() 145 | # ensure client config gets passed on 146 | self.assertEqual(ev.client, c) 147 | self.assertEqual(ev.writekey, "client_key") 148 | self.assertEqual(ev.dataset, "client_dataset") 149 | ev.add_field("event_field", 2) 150 | self.assertEqual(ev.client, c) 151 | # ensure client and builder fields are passed on 152 | self.assertEqual(ev._fields._data, { 153 | "builder_field": 1, "event_field": 2, "whomp": True, "sample_dyn_fn": ("dyna", "magic")}) 154 | self.assertEqual(ev._fields._dyn_fields, set([sample_dyn_fn])) 155 | 156 | def test_send(self): 157 | ''' ensure that Event's `send()` calls the client `send()` method ''' 158 | with client.Client(writekey="mykey", dataset="something") as c: 159 | # explicitly use a different object for Transmission than is 160 | # defined in setUp, to ensure we aren't using the global 161 | # xmit in libhoney 162 | c.xmit = mock.Mock() 163 | 164 | ev = c.new_event() 165 | ev.add_field("foo", "bar") 166 | ev.send() 167 | c.xmit.send.assert_called_with(ev) 168 | 169 | def test_send_global(self): 170 | ''' ensure that events sent using the global libhoney config work ''' 171 | with client.Client(writekey="mykey", dataset="something") as c: 172 | # explicitly use a different object for Transmission than is 173 | # defined in setUp, to ensure we aren't using the global 174 | # xmit in libhoney 175 | c.xmit = mock.Mock() 176 | 177 | libhoney.init(writekey="someotherkey", dataset="somethingelse") 178 | ev = libhoney.Event() 179 | self.assertEqual(ev.writekey, "someotherkey") 180 | self.assertEqual(ev.dataset, "somethingelse") 181 | ev.add_field("global", "event") 182 | ev.send() 183 | # test our assumption about what's actually mocked 184 | self.assertEqual(libhoney.state.G_CLIENT.xmit, self.tx) 185 | # check that we used the global xmit 186 | self.tx.send.assert_called_with(ev) 187 | # check that the client xmit was not used 188 | self.assertFalse(c.xmit.send.called) 189 | 190 | def test_send_dropped_response(self): 191 | with mock.patch('libhoney.event._should_drop') as m_drop: 192 | m_drop.return_value = True 193 | 194 | with client.Client(writekey="mykey", dataset="something") as c: 195 | ev = c.new_event() 196 | ev.add_field("a", "b") 197 | ev.send() 198 | 199 | c.responses().put_nowait.assert_called_with({ 200 | "status_code": 0, 201 | "duration": 0, 202 | "metadata": ev.metadata, 203 | "body": "", 204 | "error": "event dropped due to sampling", 205 | }) 206 | 207 | def test_xmit_override(self): 208 | '''verify that the client accepts an alternative Transmission''' 209 | mock_xmit = mock.Mock() 210 | with client.Client(transmission_impl=mock_xmit) as c: 211 | self.assertEqual(c.xmit, mock_xmit) 212 | 213 | def test_is_classic_with_empty_key(self): 214 | self.assertEqual(IsClassicKey(""), True) 215 | 216 | def test_is_classic_with_classic_configuration_key(self): 217 | self.assertEqual(IsClassicKey("c1a551c1111111111111111111111111"), True) 218 | 219 | def test_is_classic_with_classic_ingest_key(self): 220 | self.assertEqual(IsClassicKey("hcxic_1234567890123456789012345678901234567890123456789012345678"), True) 221 | 222 | def test_is_classic_with_configuration_key(self): 223 | self.assertEqual(IsClassicKey("shinynewenvironmentkey"), False) 224 | 225 | def test_is_classic_with_ingest_key(self): 226 | self.assertEqual(IsClassicKey("hcxik_1234567890123456789012345678901234567890123456789012345678"), False) 227 | 228 | 229 | class TestClientFlush(unittest.TestCase): 230 | ''' separate test class because we don't want to mock transmission''' 231 | 232 | def test_flush(self): 233 | mock_xmit = mock.Mock(spec=libhoney.transmission.Transmission) 234 | with client.Client(transmission_impl=mock_xmit) as c: 235 | # start gets called when the class is initialized 236 | mock_xmit.start.assert_called_once_with() 237 | mock_xmit.reset_mock() 238 | c.flush() 239 | 240 | mock_xmit.close.assert_called_once_with() 241 | mock_xmit.start.assert_called_once_with() 242 | 243 | mock_xmit = mock.Mock(spec=libhoney.transmission.TornadoTransmission) 244 | with client.Client(transmission_impl=mock_xmit) as c: 245 | # start gets called when the class is initialized 246 | mock_xmit.start.assert_called_once_with() 247 | mock_xmit.reset_mock() 248 | c.flush() 249 | 250 | # we don't call close/start on TornadoTransmission because we can't 251 | # force a flush in an async environment. 252 | mock_xmit.close.assert_not_called() 253 | mock_xmit.start.assert_not_called() 254 | -------------------------------------------------------------------------------- /libhoney/test_libhoney.py: -------------------------------------------------------------------------------- 1 | '''Tests for libhoney/__init__.py''' 2 | 3 | import datetime 4 | import json 5 | import unittest 6 | from unittest import mock 7 | 8 | import libhoney 9 | 10 | 11 | def sample_dyn_fn(): 12 | return "dyna", "magic" 13 | 14 | 15 | class FakeTransmitter(): 16 | def __init__(self, id): 17 | self.id = id 18 | 19 | def start(self): 20 | pass 21 | 22 | def close(self): 23 | pass 24 | 25 | def get_response_queue(self): 26 | return None 27 | 28 | 29 | class TestGlobalScope(unittest.TestCase): 30 | def setUp(self): 31 | # reset global state with each test 32 | libhoney.close() 33 | self.mock_xmit = mock.Mock(return_value=mock.Mock()) 34 | 35 | def test_init(self): 36 | ft = FakeTransmitter(3) 37 | with mock.patch('libhoney.client.Transmission') as m_xmit: 38 | m_xmit.return_value = ft 39 | libhoney.init(writekey="wk", dataset="ds", sample_rate=3, 40 | api_host="uuu", max_concurrent_batches=5, 41 | block_on_response=True) 42 | self.assertEqual(libhoney.state.G_CLIENT.writekey, "wk") 43 | self.assertEqual(libhoney.state.G_CLIENT.dataset, "ds") 44 | self.assertEqual(libhoney.state.G_CLIENT.api_host, "uuu") 45 | self.assertEqual(libhoney.state.G_CLIENT.sample_rate, 3) 46 | self.assertEqual(libhoney.state.G_CLIENT.xmit, ft) 47 | self.assertEqual(libhoney.state.G_CLIENT._responses, None) 48 | m_xmit.assert_called_with( 49 | block_on_response=True, block_on_send=False, 50 | max_concurrent_batches=5, user_agent_addition='', 51 | debug=False, 52 | ) 53 | 54 | def test_close(self): 55 | mock_client = mock.Mock() 56 | libhoney.state.G_CLIENT = mock_client 57 | libhoney.close() 58 | mock_client.close.assert_called_with() 59 | self.assertEqual(libhoney.state.G_CLIENT, None) 60 | 61 | def test_close_noop_if_not_init(self): 62 | """ 63 | libhoney.close() should noop if never initialized 64 | """ 65 | try: 66 | libhoney.close() 67 | except AttributeError: 68 | self.fail('libhoney threw an exception on ' 69 | 'an uninitialized close.') 70 | 71 | def test_add_field(self): 72 | libhoney.init() 73 | ed = {"whomp": True} 74 | libhoney.add_field("whomp", True) 75 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, ed) 76 | 77 | def test_add_dynamic_field(self): 78 | libhoney.init() 79 | ed = set([sample_dyn_fn]) 80 | libhoney.add_dynamic_field(sample_dyn_fn) 81 | self.assertEqual(libhoney.state.G_CLIENT.fields._dyn_fields, ed) 82 | 83 | def test_add(self): 84 | libhoney.init() 85 | ed = {"whomp": True} 86 | libhoney.add(ed) 87 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, ed) 88 | 89 | 90 | class TestFieldHolder(unittest.TestCase): 91 | def setUp(self): 92 | # reset global state with each test 93 | libhoney.close() 94 | 95 | def test_add_field(self): 96 | libhoney.init() 97 | expected_data = {} 98 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, expected_data) 99 | self.assertTrue(libhoney.state.G_CLIENT.fields.is_empty()) 100 | libhoney.add_field("foo", 4) 101 | expected_data["foo"] = 4 102 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, expected_data) 103 | self.assertFalse(libhoney.state.G_CLIENT.fields.is_empty()) 104 | libhoney.add_field("bar", "baz") 105 | expected_data["bar"] = "baz" 106 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, expected_data) 107 | libhoney.add_field("foo", 6) 108 | expected_data["foo"] = 6 109 | self.assertEqual(libhoney.state.G_CLIENT.fields._data, expected_data) 110 | 111 | def test_add_dynamic_field(self): 112 | libhoney.init() 113 | expected_dyn_fns = set() 114 | self.assertEqual( 115 | libhoney.state.G_CLIENT.fields._dyn_fields, expected_dyn_fns) 116 | libhoney.add_dynamic_field(sample_dyn_fn) 117 | expected_dyn_fns.add(sample_dyn_fn) 118 | self.assertEqual( 119 | libhoney.state.G_CLIENT.fields._dyn_fields, expected_dyn_fns) 120 | # adding a second time should still only have one element 121 | libhoney.add_dynamic_field(sample_dyn_fn) 122 | self.assertEqual( 123 | libhoney.state.G_CLIENT.fields._dyn_fields, expected_dyn_fns) 124 | with self.assertRaises(TypeError): 125 | libhoney.add_dynamic_field("foo") 126 | 127 | 128 | class TestBuilder(unittest.TestCase): 129 | def setUp(self): 130 | # reset global state with each test 131 | libhoney.close() 132 | 133 | def test_new_builder(self): 134 | libhoney.init() 135 | # new builder, no arguments 136 | b = libhoney.Builder() 137 | self.assertEqual(b._fields._data, {}) 138 | self.assertEqual(b._fields._dyn_fields, set()) 139 | # new builder, passed in data and dynfields 140 | expected_data = {"aa": 1} 141 | expected_dyn_fns = set([sample_dyn_fn]) 142 | b = libhoney.Builder(expected_data, expected_dyn_fns) 143 | self.assertEqual(b._fields._data, expected_data) 144 | self.assertEqual(b._fields._dyn_fields, expected_dyn_fns) 145 | # new builder, inherited data and dyn_fields 146 | libhoney.state.G_CLIENT.fields._data = expected_data 147 | libhoney.state.G_CLIENT.fields._dyn_fields = expected_dyn_fns 148 | b = libhoney.Builder() 149 | self.assertEqual(b._fields._data, expected_data) 150 | self.assertEqual(b._fields._dyn_fields, expected_dyn_fns) 151 | # new builder, merge inherited data and dyn_fields and arguments 152 | 153 | def sample_dyn_fn2(): 154 | return 5 155 | expected_data = {"aa": 1, "b": 2} 156 | expected_dyn_fns = set([sample_dyn_fn, sample_dyn_fn2]) 157 | b = libhoney.Builder({"b": 2}, [sample_dyn_fn2]) 158 | self.assertEqual(b._fields._data, expected_data) 159 | self.assertEqual(b._fields._dyn_fields, expected_dyn_fns) 160 | 161 | def test_add_field(self): 162 | libhoney.init() 163 | b = libhoney.Builder() 164 | expected_data = {} 165 | self.assertEqual(b._fields._data, expected_data) 166 | b.add_field("foo", 4) 167 | expected_data["foo"] = 4 168 | self.assertEqual(b._fields._data, expected_data) 169 | b.add_field("bar", "baz") 170 | expected_data["bar"] = "baz" 171 | self.assertEqual(b._fields._data, expected_data) 172 | b.add_field("foo", 6) 173 | expected_data["foo"] = 6 174 | self.assertEqual(b._fields._data, expected_data) 175 | 176 | def test_add_dynamic_field(self): 177 | libhoney.init() 178 | b = libhoney.Builder() 179 | expected_dyn_fns = set() 180 | self.assertEqual(b._fields._dyn_fields, expected_dyn_fns) 181 | b.add_dynamic_field(sample_dyn_fn) 182 | expected_dyn_fns.add(sample_dyn_fn) 183 | self.assertEqual(b._fields._dyn_fields, expected_dyn_fns) 184 | with self.assertRaises(TypeError): 185 | b.add_dynamic_field("foo") 186 | 187 | def test_add(self): 188 | libhoney.init() 189 | b = libhoney.Builder() 190 | expected_data = {"a": 1, "b": 3} 191 | b.add(expected_data) 192 | self.assertEqual(b._fields._data, expected_data) 193 | expected_data.update({"c": 3, "d": 4}) 194 | b.add({"c": 3, "d": 4}) 195 | self.assertEqual(b._fields._data, expected_data) 196 | 197 | def test_new_event(self): 198 | libhoney.init() 199 | b = libhoney.Builder() 200 | b.sample_rate = 5 201 | expected_data = {"a": 1, "b": 3} 202 | b.add(expected_data) 203 | ev = b.new_event() 204 | self.assertEqual(b._fields, ev._fields) 205 | self.assertEqual(ev.sample_rate, 5) 206 | ev.add_field("3", "c") 207 | self.assertNotEqual(b._fields, ev._fields) 208 | # move to event testing when written 209 | self.assertEqual(json.loads(str(ev)), {"a": 1, "3": "c", "b": 3}) 210 | 211 | def test_clone_builder(self): 212 | libhoney.init() 213 | 214 | b = libhoney.Builder() 215 | b.dataset = "newds" 216 | b.add_field("e", 9) 217 | b.add_dynamic_field(sample_dyn_fn) 218 | c = b.clone() 219 | self.assertEqual(b._fields, c._fields) 220 | c.add_field("f", 10) 221 | b.add_field("g", 11) 222 | self.assertEqual(b._fields._data, {"e": 9, "g": 11}) 223 | self.assertEqual(c._fields._data, {"e": 9, "f": 10}) 224 | self.assertEqual(c.dataset, "newds") 225 | 226 | 227 | class TestEvent(unittest.TestCase): 228 | def setUp(self): 229 | # reset global state with each test 230 | libhoney.close() 231 | 232 | def test_timer(self): 233 | libhoney.init() 234 | 235 | class fakeDate: 236 | def setNow(self, time): 237 | self.time = time 238 | 239 | def now(self): 240 | return self.time 241 | 242 | def utcnow(self): 243 | return self.time 244 | 245 | with mock.patch('libhoney.event.datetime') as m_datetime: 246 | fakeStart = datetime.datetime(2016, 1, 2, 3, 4, 5, 6) 247 | fakeEnd = fakeStart + datetime.timedelta(milliseconds=5) 248 | fd = fakeDate() 249 | fd.setNow(fakeStart) 250 | m_datetime.datetime = fd 251 | ev = libhoney.Event() 252 | with ev.timer("howlong"): 253 | fd.setNow(fakeEnd) 254 | self.assertEqual(ev._fields._data, {"howlong": 5}) 255 | self.assertEqual(ev.created_at, fakeStart) 256 | 257 | def test_str(self): 258 | libhoney.init() 259 | ev = libhoney.Event() 260 | ev.add_field("obj", {"a": 1}) 261 | ev.add_field("string", "a:1") 262 | ev.add_field("number", 5) 263 | ev.add_field("boolean", True) 264 | ev.add_field("null", None) 265 | 266 | serialized = str(ev) 267 | self.assertTrue('"obj": {"a": 1}' in serialized) 268 | self.assertTrue('"string": "a:1"' in serialized) 269 | self.assertTrue('"number": 5' in serialized) 270 | self.assertTrue('"boolean": true' in serialized) 271 | self.assertTrue('"null": null' in serialized) 272 | 273 | def test_send(self): 274 | with mock.patch('libhoney.client.Transmission') as m_xmit: 275 | libhoney.init() 276 | ev = libhoney.Event() 277 | # override inherited api_host from client 278 | ev.api_host = "" 279 | ev.add_field("f", "g") 280 | ev.api_host = "myhost" 281 | ev.writekey = "letmewrite" 282 | ev.dataset = "storeme" 283 | ev.send() 284 | m_xmit.return_value.send.assert_called_with(ev) 285 | 286 | def test_send_sampling(self): 287 | with mock.patch('libhoney.client.Transmission') as m_xmit,\ 288 | mock.patch('libhoney.event._should_drop') as m_sd: 289 | m_sd.return_value = True 290 | libhoney.init(writekey="wk", dataset="ds") 291 | 292 | # test that send() drops when should_drop is true 293 | ev = libhoney.Event() 294 | ev.add_field("foo", 1) 295 | ev.send() 296 | m_xmit.return_value.send.assert_not_called() 297 | m_sd.assert_called_with(1) 298 | ev = libhoney.Event() 299 | ev.add_field("foo", 1) 300 | ev.sample_rate = 5 301 | ev.send() 302 | m_xmit.return_value.send.assert_not_called() 303 | m_sd.assert_called_with(5) 304 | 305 | # and actually sends them along when should_drop is false 306 | m_sd.reset_mock() 307 | m_xmit.reset_mock() 308 | m_sd.return_value = False 309 | 310 | ev = libhoney.Event() 311 | ev.add_field("f", "g") 312 | ev.api_host = "myhost" 313 | ev.writekey = "letmewrite" 314 | ev.dataset = "storeme" 315 | ev.send() 316 | m_xmit.return_value.send.assert_called_with(ev) 317 | m_sd.assert_called_with(1) 318 | ev.sample_rate = 5 319 | ev.send() 320 | m_xmit.return_value.send.assert_called_with(ev) 321 | m_sd.assert_called_with(5) 322 | 323 | # test that send_presampled() does not drop 324 | m_sd.reset_mock() 325 | m_xmit.reset_mock() 326 | ev.send_presampled() 327 | m_xmit.return_value.send.assert_called_with(ev) 328 | m_sd.assert_not_called() 329 | 330 | m_sd.reset_mock() 331 | m_xmit.reset_mock() 332 | ev.sample_rate = 5 333 | ev.send_presampled() 334 | m_xmit.return_value.send.assert_called_with(ev) 335 | m_sd.assert_not_called() 336 | -------------------------------------------------------------------------------- /libhoney/test_tornado.py: -------------------------------------------------------------------------------- 1 | '''Tests for libhoney/transmission.py''' 2 | import datetime 3 | import unittest 4 | from unittest import mock 5 | 6 | import tornado 7 | 8 | import libhoney 9 | from libhoney import transmission 10 | from platform import python_version 11 | 12 | 13 | class TestTornadoTransmissionInit(unittest.TestCase): 14 | def test_defaults(self): 15 | t = transmission.TornadoTransmission() 16 | self.assertIsInstance(t.batch_sem, tornado.locks.Semaphore) 17 | self.assertIsInstance(t.pending, tornado.queues.Queue) 18 | self.assertIsInstance(t.responses, tornado.queues.Queue) 19 | self.assertEqual(t.block_on_send, False) 20 | self.assertEqual(t.block_on_response, False) 21 | self.assertEqual(transmission.has_tornado, True) 22 | 23 | def test_args(self): 24 | t = transmission.TornadoTransmission( 25 | max_concurrent_batches=4, block_on_send=True, block_on_response=True) 26 | t.start() 27 | self.assertEqual(t.block_on_send, True) 28 | self.assertEqual(t.block_on_response, True) 29 | t.close() 30 | 31 | def test_user_agent_addition(self): 32 | ''' ensure user_agent_addition is included in the User-Agent header ''' 33 | with mock.patch('libhoney.transmission.AsyncHTTPClient') as m_client: 34 | transmission.TornadoTransmission(user_agent_addition='foo/1.0') 35 | expected = f"libhoney-py/{libhoney.version.VERSION} (tornado/{tornado.version}) foo/1.0 python/{python_version()}" 36 | m_client.assert_called_once_with( 37 | force_instance=True, 38 | defaults=dict(user_agent=expected), 39 | ) 40 | 41 | 42 | class TestTornadoTransmissionSend(unittest.TestCase): 43 | def test_send(self): 44 | with mock.patch('libhoney.transmission.AsyncHTTPClient.fetch') as fetch_mock,\ 45 | mock.patch('statsd.StatsClient') as m_statsd: 46 | future = tornado.concurrent.Future() 47 | future.set_result("OK") 48 | fetch_mock.return_value = future 49 | m_statsd.return_value = mock.Mock() 50 | 51 | @tornado.gen.coroutine 52 | def _test(): 53 | t = transmission.TornadoTransmission() 54 | t.start() 55 | 56 | ev = mock.Mock(metadata=None, writekey="abc123", 57 | dataset="blargh", api_host="https://example.com", 58 | sample_rate=1, created_at=datetime.datetime.now()) 59 | ev.fields.return_value = {"foo": "bar"} 60 | t.send(ev) 61 | 62 | # wait on the batch to be "sent" 63 | # we can detect this when data has been inserted into the 64 | # batch data dictionary 65 | start_time = datetime.datetime.now() 66 | while not t.batch_data: 67 | if datetime.datetime.now() - start_time > datetime.timedelta(0, 10): 68 | self.fail("timed out waiting on batch send") 69 | yield tornado.gen.sleep(0.01) 70 | t.close() 71 | 72 | tornado.ioloop.IOLoop.current().run_sync(_test) 73 | m_statsd.return_value.incr.assert_any_call("messages_queued") 74 | self.assertTrue(fetch_mock.called) 75 | 76 | 77 | class TestTornadoTransmissionSendError(unittest.TestCase): 78 | def test_send(self): 79 | with mock.patch('libhoney.transmission.AsyncHTTPClient.fetch') as fetch_mock,\ 80 | mock.patch('statsd.StatsClient') as m_statsd: 81 | future = tornado.concurrent.Future() 82 | ex = Exception("oh poo!") 83 | future.set_exception(ex) 84 | fetch_mock.return_value = future 85 | m_statsd.return_value = mock.Mock() 86 | 87 | @tornado.gen.coroutine 88 | def _test(): 89 | t = transmission.TornadoTransmission() 90 | t.start() 91 | 92 | ev = mock.Mock(metadata=None, writekey="abc123", 93 | dataset="blargh", api_host="https://example.com", 94 | sample_rate=1, created_at=datetime.datetime.now()) 95 | ev.fields.return_value = {"foo": "bar"} 96 | t.send(ev) 97 | 98 | try: 99 | resp = yield t.responses.get(datetime.timedelta(0, 10)) 100 | self.assertEqual(resp["error"], ex) 101 | except tornado.util.TimeoutError: 102 | self.fail("timed out waiting on response queue") 103 | finally: 104 | t.close() 105 | 106 | tornado.ioloop.IOLoop.current().run_sync(_test) 107 | 108 | 109 | class TestTornadoTransmissionQueueOverflow(unittest.TestCase): 110 | def test_send(self): 111 | with mock.patch('statsd.StatsClient') as m_statsd: 112 | m_statsd.return_value = mock.Mock() 113 | 114 | t = transmission.TornadoTransmission() 115 | t.pending = tornado.queues.Queue(maxsize=2) 116 | t.responses = tornado.queues.Queue(maxsize=1) 117 | # we don't call start on transmission here, which will cause 118 | # the queue to pile up 119 | 120 | t.send(mock.Mock()) 121 | t.send(mock.Mock()) 122 | t.send(mock.Mock()) # should overflow sending and land on response 123 | m_statsd.return_value.incr.assert_any_call("queue_overflow") 124 | # shouldn't throw exception when response is full 125 | t.send(mock.Mock()) 126 | -------------------------------------------------------------------------------- /libhoney/test_transmission.py: -------------------------------------------------------------------------------- 1 | '''Tests for libhoney/transmission.py''' 2 | 3 | import libhoney 4 | from libhoney import transmission 5 | from libhoney.version import VERSION 6 | from platform import python_version 7 | 8 | import datetime 9 | import gzip 10 | import httpretty 11 | import io 12 | import json 13 | from unittest import mock 14 | import requests_mock 15 | import time 16 | import unittest 17 | import queue 18 | 19 | 20 | class TestTransmissionInit(unittest.TestCase): 21 | def test_defaults(self): 22 | t = transmission.Transmission() 23 | self.assertEqual(t.max_concurrent_batches, 10) 24 | self.assertIsInstance(t.pending, queue.Queue) 25 | self.assertEqual(t.pending.maxsize, 1000) 26 | self.assertIsInstance(t.responses, queue.Queue) 27 | self.assertEqual(t.responses.maxsize, 2000) 28 | self.assertEqual(t.block_on_send, False) 29 | self.assertEqual(t.block_on_response, False) 30 | 31 | def test_args(self): 32 | t = transmission.Transmission( 33 | max_concurrent_batches=4, block_on_send=True, block_on_response=True) 34 | t.start() 35 | self.assertEqual(t.max_concurrent_batches, 4) 36 | self.assertEqual(t.block_on_send, True) 37 | self.assertEqual(t.block_on_response, True) 38 | t.close() 39 | 40 | def test_user_agent_addition(self): 41 | ''' ensure user_agent_addition is included in the User-Agent header ''' 42 | with mock.patch('libhoney.transmission.Transmission._get_requests_session') as m_session: 43 | transmission.Transmission(gzip_enabled=False) 44 | expected = "libhoney-py/" + libhoney.version.VERSION + " python/" + python_version() 45 | m_session.return_value.headers.update.assert_called_once_with({ 46 | 'User-Agent': expected 47 | }) 48 | with mock.patch('libhoney.transmission.Transmission._get_requests_session') as m_session: 49 | transmission.Transmission( 50 | user_agent_addition='foo/1.0', gzip_enabled=False) 51 | expected = "libhoney-py/" + libhoney.version.VERSION + " foo/1.0" + " python/" + python_version() 52 | m_session.return_value.headers.update.assert_called_once_with({ 53 | 'User-Agent': expected 54 | }) 55 | 56 | 57 | class FakeEvent(): 58 | def __init__(self): 59 | self.created_at = datetime.datetime.now() 60 | self.metadata = {} 61 | 62 | 63 | class TestTransmissionSend(unittest.TestCase): 64 | def test_send(self): 65 | t = transmission.Transmission() 66 | t.sd = mock.Mock() 67 | qsize = 4 68 | t.pending.qsize = mock.Mock(return_value=qsize) 69 | t.pending.put = mock.Mock() 70 | t.pending.put_nowait = mock.Mock() 71 | t.responses.put = mock.Mock() 72 | t.responses.put_nowait = mock.Mock() 73 | # put an event non-blocking 74 | ev = FakeEvent() 75 | ev.metadata = None 76 | t.send(ev) 77 | t.sd.gauge.assert_called_with("queue_length", 4) 78 | t.pending.put_nowait.assert_called_with(ev) 79 | t.pending.put.assert_not_called() 80 | t.sd.incr.assert_called_with("messages_queued") 81 | t.pending.put.reset_mock() 82 | t.pending.put_nowait.reset_mock() 83 | t.sd.reset_mock() 84 | # put an event blocking 85 | t.block_on_send = True 86 | t.send(ev) 87 | t.pending.put.assert_called_with(ev) 88 | t.pending.put_nowait.assert_not_called() 89 | t.sd.incr.assert_called_with("messages_queued") 90 | t.sd.reset_mock() 91 | # put an event non-blocking queue full 92 | t.block_on_send = False 93 | t.pending.put_nowait = mock.Mock(side_effect=queue.Full()) 94 | t.send(ev) 95 | t.sd.incr.assert_called_with("queue_overflow") 96 | t.responses.put_nowait.assert_called_with({ 97 | "status_code": 0, "duration": 0, 98 | "metadata": None, "body": "", 99 | "error": "event dropped; queue overflow", 100 | }) 101 | 102 | @httpretty.activate 103 | def test_send_batch_will_retry_once(self): 104 | libhoney.init() 105 | # create two responses to the batch event post 106 | # first timeout, then accept the batch 107 | httpretty.register_uri( 108 | httpretty.POST, 109 | "http://urlme/1/batch/datame", 110 | responses=[ 111 | httpretty.Response( 112 | body='{"message": "Timeout"}', 113 | status=500, 114 | ), 115 | httpretty.Response( 116 | body=json.dumps([{"status": 202}]), 117 | status=200, 118 | ), 119 | ] 120 | ) 121 | 122 | t = transmission.Transmission() 123 | t.start() 124 | ev = libhoney.Event() 125 | ev.writekey = "writeme" 126 | ev.dataset = "datame" 127 | ev.api_host = "http://urlme/" 128 | ev.metadata = "metadaaata" 129 | ev.created_at = datetime.datetime(2013, 1, 1, 11, 11, 11) 130 | t.send(ev) 131 | t.close() 132 | 133 | resp_count = 0 134 | while not t.responses.empty(): 135 | resp = t.responses.get() 136 | if resp is None: 137 | break 138 | # verify the batch was accepted 139 | assert resp["status_code"] == 202 140 | assert resp["metadata"] == "metadaaata" 141 | resp_count += 1 142 | 143 | def test_send_gzip(self): 144 | libhoney.init() 145 | with requests_mock.Mocker() as m: 146 | m.post("http://urlme/1/batch/datame", 147 | text=json.dumps([{"status": 202}]), status_code=200, 148 | request_headers={"X-Honeycomb-Team": "writeme"}) 149 | 150 | t = transmission.Transmission(block_on_send=True) 151 | t.start() 152 | ev = libhoney.Event() 153 | ev.writekey = "writeme" 154 | ev.dataset = "datame" 155 | ev.api_host = "http://urlme/" 156 | ev.metadata = "metadaaata" 157 | ev.sample_rate = 3 158 | ev.created_at = datetime.datetime(2013, 1, 1, 11, 11, 11) 159 | ev.add_field("key", "asdf") 160 | t.send(ev) 161 | 162 | # sending is async even with the mock so block until it happens 163 | resp_received = False 164 | while not resp_received: 165 | resp = t.responses.get() 166 | if resp is None: 167 | break 168 | 169 | self.assertEqual(resp["status_code"], 202) 170 | self.assertEqual(resp["metadata"], "metadaaata") 171 | resp_received = True 172 | 173 | for req in m.request_history: 174 | # verify gzip payload is sane by decompressing and checking contents 175 | self.assertEqual( 176 | req.headers['Content-Encoding'], 'gzip', "content encoding should be gzip") 177 | gz = gzip.GzipFile(fileobj=io.BytesIO(req.body), mode='rb') 178 | # json.load in python 3.5 doesn't like binary files, so we can't pass 179 | # the gzip stream directly to it 180 | uncompressed = gz.read().decode() 181 | data = json.loads(uncompressed) 182 | self.assertEqual(data[0]['samplerate'], 3) 183 | self.assertEqual(data[0]['data']['key'], 'asdf') 184 | 185 | 186 | class TestTransmissionQueueOverflow(unittest.TestCase): 187 | def test_send(self): 188 | t = transmission.Transmission(max_pending=2, max_responses=1) 189 | 190 | t.send(FakeEvent()) 191 | t.send(FakeEvent()) 192 | t.send(FakeEvent()) # should overflow sending and land on response 193 | t.send(FakeEvent()) # shouldn't throw exception when response is full 194 | 195 | 196 | class TestTransmissionPrivateSend(unittest.TestCase): 197 | def setUp(self): 198 | # reset global state with each test 199 | libhoney.close() 200 | 201 | def test_batching(self): 202 | libhoney.init() 203 | with requests_mock.Mocker() as m: 204 | m.post("http://urlme/1/batch/datame", 205 | text=json.dumps(200 * [{"status": 202}]), status_code=200, 206 | request_headers={"X-Honeycomb-Team": "writeme"}) 207 | 208 | t = transmission.Transmission(gzip_enabled=False) 209 | t.start() 210 | for i in range(300): 211 | ev = libhoney.Event() 212 | ev.writekey = "writeme" 213 | ev.dataset = "datame" 214 | ev.api_host = "http://urlme/" 215 | ev.metadata = "metadaaata" 216 | ev.sample_rate = 3 217 | ev.created_at = datetime.datetime(2013, 1, 1, 11, 11, 11) 218 | ev.add_field("key", i) 219 | t.send(ev) 220 | t.close() 221 | 222 | resp_count = 0 223 | while not t.responses.empty(): 224 | resp = t.responses.get() 225 | if resp is None: 226 | break 227 | assert resp["status_code"] == 202 228 | assert resp["metadata"] == "metadaaata" 229 | resp_count += 1 230 | assert resp_count == 300 231 | 232 | for req in m.request_history: 233 | body = req.json() 234 | for event in body: 235 | assert event["time"] == "2013-01-01T11:11:11Z" 236 | assert event["samplerate"] == 3 237 | 238 | def test_grouping(self): 239 | libhoney.init() 240 | with requests_mock.Mocker() as m: 241 | m.post("http://urlme/1/batch/dataset", 242 | text=json.dumps(100 * [{"status": 202}]), status_code=200, 243 | request_headers={"X-Honeycomb-Team": "writeme"}) 244 | 245 | m.post("http://urlme/1/batch/alt_dataset", 246 | text=json.dumps(100 * [{"status": 202}]), status_code=200, 247 | request_headers={"X-Honeycomb-Team": "writeme"}) 248 | 249 | t = transmission.Transmission( 250 | max_concurrent_batches=1, gzip_enabled=False) 251 | t.start() 252 | 253 | builder = libhoney.Builder() 254 | builder.writekey = "writeme" 255 | builder.dataset = "dataset" 256 | builder.api_host = "http://urlme/" 257 | for i in range(100): 258 | ev = builder.new_event() 259 | ev.created_at = datetime.datetime(2013, 1, 1, 11, 11, 11) 260 | ev.add_field("key", i) 261 | t.send(ev) 262 | 263 | builder.dataset = "alt_dataset" 264 | for i in range(100): 265 | ev = builder.new_event() 266 | ev.created_at = datetime.datetime(2013, 1, 1, 11, 11, 11) 267 | ev.add_field("key", i) 268 | t.send(ev) 269 | 270 | t.close() 271 | resp_count = 0 272 | while not t.responses.empty(): 273 | resp = t.responses.get() 274 | if resp is None: 275 | break 276 | assert resp["status_code"] == 202 277 | resp_count += 1 278 | assert resp_count == 200 279 | 280 | assert ({h.url for h in m.request_history} == 281 | {"http://urlme/1/batch/dataset", "http://urlme/1/batch/alt_dataset"}) 282 | 283 | def test_flush_after_timeout(self): 284 | libhoney.init() 285 | with requests_mock.Mocker() as m: 286 | m.post("http://urlme/1/batch/dataset", 287 | text=json.dumps(100 * [{"status": 202}]), status_code=200, 288 | request_headers={"X-Honeycomb-Team": "writeme"}) 289 | 290 | t = transmission.Transmission( 291 | max_concurrent_batches=1, send_frequency=0.1, gzip_enabled=False) 292 | t.start() 293 | 294 | ev = libhoney.Event() 295 | ev.writekey = "writeme" 296 | ev.dataset = "dataset" 297 | ev.add_field("key", "value") 298 | ev.api_host = "http://urlme/" 299 | 300 | t.send(ev) 301 | 302 | time.sleep(0.2) 303 | resp = t.responses.get() 304 | assert resp["status_code"] == 202 305 | t.close() 306 | 307 | 308 | class TestFileTransmissionSend(unittest.TestCase): 309 | def test_send(self): 310 | t = transmission.FileTransmission(user_agent_addition='test') 311 | t._output = mock.Mock() 312 | ev = mock.Mock() 313 | ev.fields.return_value = {'abc': 1, 'xyz': 2} 314 | ev.sample_rate = 2.0 315 | ev.dataset = "exciting-dataset!" 316 | ev.user_agent = f"libhoney-py/{VERSION} test python/{python_version()}" 317 | ev.created_at = datetime.datetime.now() 318 | 319 | expected_event_time = ev.created_at.isoformat() 320 | if ev.created_at.tzinfo is None: 321 | expected_event_time += "Z" 322 | 323 | expected_payload = { 324 | "data": {'abc': 1, 'xyz': 2}, 325 | "samplerate": 2.0, 326 | "dataset": "exciting-dataset!", 327 | "time": expected_event_time, 328 | "user_agent": ev.user_agent, 329 | } 330 | t.send(ev) 331 | # hard to compare json because dict ordering is not determanistic, 332 | # so convert back to dict 333 | args, _ = t._output.write.call_args 334 | actual_payload = json.loads(args[0]) 335 | self.assertDictEqual(actual_payload, expected_payload) 336 | 337 | def test_send_datetime_value(self): 338 | t = transmission.FileTransmission(user_agent_addition='test') 339 | t._output = mock.Mock() 340 | ev = mock.Mock() 341 | dt = datetime.datetime.now() 342 | ev.fields.return_value = {'abc': 1, 'xyz': 2, 'dt': dt} 343 | ev.sample_rate = 2.0 344 | ev.dataset = "exciting-dataset!" 345 | ev.user_agent = f"libhoney-py/{VERSION} test python/{python_version()}" 346 | ev.created_at = datetime.datetime.now() 347 | 348 | expected_event_time = ev.created_at.isoformat() 349 | if ev.created_at.tzinfo is None: 350 | expected_event_time += "Z" 351 | 352 | expected_payload = { 353 | "data": {'abc': 1, 'xyz': 2, 'dt': str(dt)}, 354 | "samplerate": 2.0, 355 | "dataset": "exciting-dataset!", 356 | "time": expected_event_time, 357 | "user_agent": ev.user_agent, 358 | } 359 | t.send(ev) 360 | # hard to compare json because dict ordering is not determanistic, 361 | # so convert back to dict 362 | args, _ = t._output.write.call_args 363 | actual_payload = json.loads(args[0]) 364 | self.assertDictEqual(actual_payload, expected_payload) 365 | -------------------------------------------------------------------------------- /libhoney/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "2.4.0" # Update using bump2version 2 | -------------------------------------------------------------------------------- /push_docs.sh: -------------------------------------------------------------------------------- 1 | # Unfortunately not as nice as godoc.org/rubydoc.info for now: https://www.pydoc.io/about/ 2 | set -e 3 | 4 | # set up git 5 | git config --global user.email "accounts+circleci@honeycomb.io" 6 | git config --global user.name "Honeycomb CI" 7 | 8 | # build and commit website files 9 | python setup.py install 10 | pip install pdoc 11 | PYTHONPATH=. pdoc libhoney --html --html-dir=./docs 12 | 13 | # Check out orphan gh-pages branch, get it set up correctly 14 | git checkout --orphan gh-pages 15 | git reset 16 | git add docs/ 17 | git mv docs/libhoney/*.html ./ 18 | git add .gitignore 19 | git clean -fd 20 | git commit -m "CircleCI build: $CIRCLE_BUILD_NUM" 21 | 22 | # Pushing via secure GITHUB_TOKEN in CircleCI project 23 | git remote add origin-pages https://${GITHUB_TOKEN}@github.com/honeycombio/libhoney-py.git > /dev/null 2>&1 24 | git push --force --quiet --set-upstream origin-pages gh-pages 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "libhoney" 3 | version = "2.4.0" # Update using bump2version 4 | description = "Python library for sending data to Honeycomb" 5 | authors = ["Honeycomb.io "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/honeycombio/libhoney-py" 9 | repository = "https://github.com/honeycombio/libhoney-py" 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.7, <4" 13 | requests = "^2.24.0" 14 | statsd = ">=3.3,<5.0" 15 | urllib3 = ">=1.26,<3.0" 16 | 17 | [tool.poetry.dev-dependencies] 18 | coverage = "^7.2.7" 19 | pylint = [{version = "^2.13", python = ">=3.7,<4"}] 20 | pycodestyle = "^2.10.0" 21 | requests-mock = "^1.11.0" 22 | tornado = "^6.2" 23 | autopep8 = "^2.0.2" 24 | bump2version = "^1.0.1" 25 | httpretty = "^1.1.4" 26 | 27 | [build-system] 28 | requires = ["poetry>=0.12"] 29 | build-backend = "poetry.masonry.api" 30 | --------------------------------------------------------------------------------