diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..912f5aa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,41 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.asciidoc] +trim_trailing_whitespace = false + +[Jenkinsfile] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.groovy] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.dsl] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{Makefile,**.mk}] +# Use tabs for indentation (Makefiles require tabs) +indent_style = tab diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f4691ef --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# GitHub CODEOWNERS definition +# See: https://help.github.com/articles/about-codeowners + +* @kuisathaverat \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..724cf98 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## What does this PR do? + + + +## Why is it important? + + + +## Related issues +Closes #ISSUE diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..472acb9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +--- +version: 2 +updates: + # Enable version updates for pytest_otel + - package-ecosystem: "pip" + directory: "/" + # Check for updates once a month + schedule: + interval: "weekly" + day: "sunday" + time: "22:00" + reviewers: + - "kuisathaverat" \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..8fc49a8 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,38 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bug' + - title: '📚 Documentation' + labels: + - 'docs' + - 'question' + - title: '🧰 Maintenance' + label: + - 'chore' + - 'ci' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ### Changes + + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..83d8e54 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,60 @@ +--- +## continous deployment workflow +name: cd + +on: + # push: + # branches: + # - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + name: Run linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint + run: make lint + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: false + ref: ${{ github.sha || 'main' }} + - uses: actions/setup-python@v5 + with: + python-version-file: .python-version + cache: 'pip' + cache-dependency-path: requirements.txt + - name: test + run: make test + - name: it-test + run: make it-test + release: + runs-on: ubuntu-latest + permissions: + # write permission is required to create a github release + contents: write + steps: + - name: Release + id: release + run: | + make publish + grep "version = " setup.cfg | tr -d " " >> ${GITHUB_OUTPUT} + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + - uses: release-drafter/release-drafter@v6 + with: + version: ${{ steps.release.outputs.version }} + publish: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9d25ce0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +--- +## continous integration workflow +name: ci + +on: + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Run linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: false + ref: ${{ github.sha || 'main' }} + - name: Lint + run: make lint + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: false + ref: ${{ github.sha || 'main' }} + - uses: actions/setup-python@v5 + with: + python-version-file: .python-version + cache: 'pip' + cache-dependency-path: requirements.txt + - name: test + run: make test + - name: it-test + run: make it-test + - uses: actions/upload-artifact@v2 + if: success() || failure() + with: + name: test-results + path: "**/junit-*.xml" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d07011 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__init__.py +.gradle +.idea +.venv +.vscode +*.bck +*.iml +**/__pycache__ +build +gha-creds-*.json +release.properties +target +venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9f96862 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# version 1.4.1 + +* Move to a new repository + +# version 1.4.0 + +* Remove support for Python 3.6 and 3.7 [2015](https://github.com/elastic/apm-pipeline-library/pull/2015) +* add support for Python 3.11 [2015](https://github.com/elastic/apm-pipeline-library/pull/2015) +* Bump OpenTelemetry SDK to 1.15.0 [2015](https://github.com/elastic/apm-pipeline-library/pull/2015) +* Update Elastic demo to 8.6.0 [2015](https://github.com/elastic/apm-pipeline-library/pull/2015) + +# version 1.3.0 + +* Bump OpenTelemetry SDK to 1.13.0 [#1917](https://github.com/elastic/apm-pipeline-library/pull/1917) +* Update the Demo to Elastic Stack 8.4.2 [#1917](https://github.com/elastic/apm-pipeline-library/pull/1917) +* chore(deps): bump coverage in /resources/scripts/pytest_otel [#1916](https://github.com/elastic/apm-pipeline-library/pull/1916) +* chore(deps): bump mypy in /resources/scripts/pytest_otel [#1915](https://github.com/elastic/apm-pipeline-library/pull/1915) +* chore(deps): bump pytest in /resources/scripts/pytest_otel [#1892](https://github.com/elastic/apm-pipeline-library/pull/1892) + +# version 1.2.1 + +* fix: pytest_otel: pytest.fail is not captured as an error [#1840](https://github.com/elastic/apm-pipeline-library/pull/1840) [#1843](https://github.com/elastic/apm-pipeline-library/pull/1843) +* fix: update the pytest_otel demo [#1804](https://github.com/elastic/apm-pipeline-library/pull/1804) + +* chore(deps): bump mypy in /resources/scripts/pytest_otel [#1893](https://github.com/elastic/apm-pipeline-library/pull/1893) +* chore(deps): bump pytest-docker in /resources/scripts/pytest_otel [#1894](https://github.com/elastic/apm-pipeline-library/pull/1894) +* chore(deps): bump pre-commit in /resources/scripts/pytest_otel [#1877](https://github.com/elastic/apm-pipeline-library/pull/1877) +* chore(deps): bump coverage in /resources/scripts/pytest_otel [#1891](https://github.com/elastic/apm-pipeline-library/pull/1891) +* chore(deps): bump psutil in /resources/scripts/pytest_otel [#1879](https://github.com/elastic/apm-pipeline-library/pull/1879) +* chore: bump Otel 1.12.0 [#1890](https://github.com/elastic/apm-pipeline-library/pull/1890) +* pytest_otel: add hard dependencies [#1841](https://github.com/elastic/apm-pipeline-library/pull/1841) + +# Version 1.1.1 + +* fix: Update setup.cfg dependencies. + +# Version 1.1.0 + +* feat: Update OpenTelemetry SDK to 1.11.0 [#1664](https://github.com/elastic/apm-pipeline-library/pull/1664) +* fix: pytest-otel seems to be broken on Python >= 3.10.0 [#1687](https://github.com/elastic/apm-pipeline-library/issues/1687) + +# Version 1.0.3 + +* fix: remove pytest timing import [#1623](https://github.com/elastic/apm-pipeline-library/pull/1623) + +# Version 1.0.2 + +* fix: show traces only on debug mode [#1621](https://github.com/elastic/apm-pipeline-library/pull/1621) +* fix: use different tests for the demos [#1523](https://github.com/elastic/apm-pipeline-library/pull/1523) + +# Version 1.0.1 + +* fix: Pytest update attr [#1521](https://github.com/elastic/apm-pipeline-library/pull/1521) + +# Version 1.0.0 + +* feat: update the attribute references [#1496](https://github.com/elastic/apm-pipeline-library/pull/1496) +* Update main [#1468](https://github.com/elastic/apm-pipeline-library/pull/1468) +* fix: Otel avoid conflicts [#1464](https://github.com/elastic/apm-pipeline-library/pull/1464) + +# Version 0.0.6 + +* fix: improve attributes access [#1462](https://github.com/elastic/apm-pipeline-library/pull/1462) + +# Version 0.0.4 + +* fix: fix pytest_otel authentication [#1452](https://github.com/elastic/apm-pipeline-library/pull/1452) + +# Version 0.0.3 + +* feat: add demos [#1441](https://github.com/elastic/apm-pipeline-library/pull/1441) + +# Version 0.0.3 + +* feat: publish pytest_otel [#1392](https://github.com/elastic/apm-pipeline-library/pull/1392) + +# Version 0.0.2 + +* feat: Otel pytest plugin [#1217](https://github.com/elastic/apm-pipeline-library/pull/1217) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5cffc6f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2020] [Elastic B.V.] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c4045d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,158 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +OTEL_EXPORTER_OTLP_ENDPOINT ?= http://127.0.0.1:4317 +OTEL_SERVICE_NAME ?= "pytest_otel_test" +OTEL_EXPORTER_OTLP_INSECURE ?= True +OTEL_EXPORTER_OTLP_HEADERS ?= +TRACEPARENT ?= +REPO_URL ?= https://upload.pypi.org/legacy/ +DEMO_DIR ?= docs/demos + +VENV ?= .venv +PYTHON ?= python3 +PIP ?= pip3 +GH_VERSION = 1.0.0 + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Linux) + OS_FLAG := linux +endif +ifeq ($(UNAME_S),Darwin) + OS_FLAG := macOS +endif +UNAME_P := $(shell uname -m) +ifeq ($(UNAME_P),x86_64) + ARCH_FLAG := amd64 +endif +ifneq ($(filter %86,$(UNAME_P)),) + ARCH_FLAG := i386 +endif +GH_BINARY = gh_$(GH_VERSION)_$(OS_FLAG)_$(ARCH_FLAG) +GH = $(CURDIR)/bin/gh + +export UID=$(shell id -u) +export GID=$(shell id -g) + +SHELL = /bin/bash +.SILENT: + +.PHONY: help +help: + @echo "Targets:" + @echo "" + @grep '^## @help' Makefile|cut -d ":" -f 2-3|( (sort|column -s ":" -t) || (sort|tr ":" "\t") || (tr ":" "\t")) + +bin: + mkdir bin + +bin/gh: bin + curl -sSfL https://github.com/cli/cli/releases/download/v$(GH_VERSION)/$(GH_BINARY).tar.gz|tar xz + mv $(GH_BINARY)/bin/gh bin/gh + rm -fr $(GH_BINARY) + +## @help:virtualenv:Create a Python virtual environment. +.PHONY: virtualenv +virtualenv: + $(PYTHON) --version + test -d $(VENV) || $(PYTHON) -m venv $(VENV);\ + source $(VENV)/bin/activate;\ + $(PIP) install -q -r requirements.txt; + +## @help:install:Install APM CLI in a Python virtual environment. +.PHONY: install +install: virtualenv + source $(VENV)/bin/activate;\ + $(PIP) install .; + +## @help:test:Run the test. +.PHONY: test +test: virtualenv install + source $(VENV)/bin/activate;\ + pytest --capture=no -p pytester --runpytest=subprocess \ + --junitxml $(CURDIR)/junit-test_pytest_otel.xml \ + tests/test_pytest_otel.py; + +## @help:test:Run the test. +.PHONY: test +it-test: virtualenv install + set -e;\ + source $(VENV)/bin/activate;\ + for test in tests/it/test_*.py; \ + do \ + pytest --capture=no -p pytester --runpytest=subprocess \ + --junitxml $(CURDIR)/junit-$$(basename $${test}).xml \ + $${test}; \ + done; + #pytest --capture=no -p pytester --runpytest=subprocess tests/it/test_*.py; + +## @help:coverage:Report coverage. +.PHONY: coverage +coverage: virtualenv + source $(VENV)/bin/activate;\ + coverage run --source=otel -m pytest; \ + coverage report -m; + +## @precomit:pre-commit:Run precommit hooks. +lint: virtualenv + source $(VENV)/bin/activate;\ + pre-commit run; \ + mypy --namespace-packages src/pytest_otel; \ + mypy --namespace-packages tests; + +## @help:clean:Remove Python file artifacts. +.PHONY: clean +clean: + @echo "+ $@" + @find . -type f -name "*.py[co]" -delete + @find . -type d -name "__pycache__" -delete + @find . -name '*~' -delete + -@rm -fr src/pytest_otel.egg-info *.egg-info build dist $(VENV) bin .tox .mypy_cache .pytest_cache otel-traces-file-output.json test_spans.json temp junit-*.xml + +package: virtualenv + source $(VENV)/bin/activate;\ + set +xe; \ + pip install wheel; \ + $(PYTHON) setup.py sdist bdist_wheel + +## @help:run-otel-collector:Run OpenTelemetry collector in debug mode. +.PHONY: run-otel-collector +run-otel-collector: + mkdir -p "$(CURDIR)/temp" + docker run --rm -p 4317:4317 -u "$(id -u):$(id -g)" \ + -v "$(CURDIR)/temp:/tmp" \ + -v "$(CURDIR)/tests/otel-collector.yaml":/otel-config.yaml \ + --name otelcol otel/opentelemetry-collector \ + --config otel-config.yaml; \ + +#https://upload.pypi.org/legacy/ +#https://test.pypi.org/legacy/ +#secret/observability-team/ci/apm-agent-python-pypi-test +#secret/observability-team/ci/apm-agent-python-pypi-prod +## @help:publish REPO_URL=${REPO_URL} TWINE_USER=${TWINE_USER} TWINE_PASSWORD=${TWINE_PASSWORD}:Publish the Python project in a PyPI repository. +.PHONY: publish +publish: package + set +xe; \ + source $(VENV)/bin/activate;\ + $(PYTHON) -m pip install twine;\ + echo "Uploading to $${REPO_URL} with user $${TWINE_USER}";\ + python -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.tar.gz;\ + python -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.whl + +## @help:demo-start-DEMO_NAME:Starts the demo from the demo folder, DEMO_NAME is the name of the demo type folder in the docs/demos folder (jaeger, elastic). +.PHONY: demo-start-% +demo-start-%: virtualenv install + $(MAKE) demo-stop-$* + mkdir -p $(DEMO_DIR)/$*/build + touch $(DEMO_DIR)/$*/build/tests.json + docker-compose -f $(DEMO_DIR)/$*/docker-compose.yml up -d + . $(DEMO_DIR)/$*/demo.env;\ + env | grep OTEL;\ + source $(VENV)/bin/activate;\ + pytest --capture=no docs/demos/test/test_demo.py || echo "Demo execution finished you can access to http://localhost:5601 to check the traces, the user is 'admin' and the password 'changeme'"; + +## @help:demo-stop-DEMO_NAME:Stops the demo from the demo folder, DEMO_NAME is the name of the demo type folder in the docs/demos folder (jaeger, elastic). +.PHONY: demo-stop-% +demo-stop-%: + -docker-compose -f $(DEMO_DIR)/$*/docker-compose.yml down --remove-orphans --volumes + -rm -fr $(DEMO_DIR)/$*/build diff --git a/README.md b/README.md new file mode 100644 index 0000000..656700b --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# pytest_otel + +## Features + +pytest-otel plugin for reporting OpenTelemetry spans of tests executed. + +[OpenTelemetry](https://opentelemetry.io/docs/) + +## Requirements + +* opentelemetry-api == 1.15.0 +* opentelemetry-exporter-otlp == 1.15.0 +* opentelemetry-sdk == 1.15.0 +* pytest >= 7.1.3 + +## Installation + +You can install "pytest-otel" via `pip` or using the `setup.py` script. + +```bash +pip install pytest-otel +``` + +## Usage + +`pytest_otel` is configured by adding some parameters to the pytest command line. Below are the descriptions: + +* --otel-endpoint: URL for the OpenTelemetry server. (Required). Env variable: `OTEL_EXPORTER_OTLP_ENDPOINT` +* --otel-headers: Additional headers to send (i.e.: key1=value1,key2=value2). Env variable: `OTEL_EXPORTER_OTLP_HEADERS` +* --otel-service-name: Name of the service. Env variable: `OTEL_SERVICE_NAME` +* --otel-session-name: Name for the main span. +* --otel-traceparent: Trace parent ID. Env variable: `TRACEPARENT`. See https://www.w3.org/TR/trace-context-1/#trace-context-http-headers-format +* --otel-insecure: Disables TLS. Env variable: `OTEL_EXPORTER_OTLP_INSECURE` + +```bash +pytest --otel-endpoint https://otelcollector.example.com:4317 \ + --otel-headers "authorization=Bearer ASWDCcCRFfr" \ + --otel-service-name pytest_otel \ + --otel-session-name='My_Test_cases' \ + --otel-traceparent=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 \ + --otel-insecure=False +``` + +**IMPORTANT**: If you use `--otel-headers` the transaction metadata might expose those arguments +with their values. In order to avoid any credentials to be exposed, it's recommended to use the environment variables. +For instance, given the above example, a similar one with environment variables can be seen below: + +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=https://apm.example.com:8200 \ +OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer ASWDCcCRFfr" \ +OTEL_SERVICE_NAME=pytest_otel \ +TRACEPARENT=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 \ +OTEL_EXPORTER_OTLP_INSECURE=False \ +pytest --otel-session-name='My_Test_cases' +``` + +## Demos + +* [Jaeger](https://github.com/kuisathaverat/pytest_otel/docs/demos/jaeger/README.md) +* [Elastic Stack](https://github.com/kuisathaverat/pytest_otel/docs/demos/elastic/README.md) + +## License + +Distributed under the terms of the `Apache License Version 2.0`_ license, "pytest-otel" is free and open source software diff --git a/docs/demos/elastic/README.md b/docs/demos/elastic/README.md new file mode 100644 index 0000000..1684086 --- /dev/null +++ b/docs/demos/elastic/README.md @@ -0,0 +1,34 @@ +# Elastic Stack demo + +In this demo we start an APM service, +to show how to send the OpenTelemetry spans to an APM Service. +Then we start the pytest-otel tests with the environment variables configured to hit the APM service. + +To start the demo you have to execute the following command in the root of the pytest-otel project folder: + +```shell +make demo-start-elastic +``` + +When the execution ends you can go to the Kibana service (http://localhost:5601/) in a browser to show the spans, +Kibana is secure by default, the user is `admin` and the password `changeme`. + +![](images/elastic-services.png) + +![](images/elastic-overview.png) + +![](images/elastic-transactions.png) + +![](images/elastic-spans-detail.png) + +Finally you can stop the demo + +```shell +make demo-stop-elastic +``` + +* [OpenTelemetry](https://opentelemetry.io/docs/) +* [APM app](https://www.elastic.co/guide/en/kibana/current/xpack-apm.html) +* [APM Server](https://www.elastic.co/guide/en/apm/get-started/current/overview.html) +* [Elasticserach](https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html) +* [Kibana](https://www.elastic.co/guide/en/kibana/current/introduction.html) diff --git a/docs/demos/elastic/demo.env b/docs/demos/elastic/demo.env new file mode 100644 index 0000000..6caee21 --- /dev/null +++ b/docs/demos/elastic/demo.env @@ -0,0 +1,3 @@ +export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer ToKeN123" +export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:8200" +export OTEL_SERVICE_NAME="elastic-srv" diff --git a/docs/demos/elastic/docker-compose.yml b/docs/demos/elastic/docker-compose.yml new file mode 100644 index 0000000..041eee3 --- /dev/null +++ b/docs/demos/elastic/docker-compose.yml @@ -0,0 +1,38 @@ +--- +version: "2.4" +networks: + default: + name: integration-testing +services: + elasticsearch: + extends: + file: elastic-stack.yml + service: elasticsearch + + kibana: + extends: + file: elastic-stack.yml + service: kibana + depends_on: + elasticsearch: + condition: service_healthy + + fleet-server: + extends: + file: elastic-stack.yml + service: fleet-server + depends_on: + elasticsearch: + condition: service_healthy + kibana: + condition: service_healthy + + wait: + image: busybox + depends_on: + elasticsearch: + condition: service_healthy + kibana: + condition: service_healthy + fleet-server: + condition: service_healthy diff --git a/docs/demos/elastic/elastic-stack.yml b/docs/demos/elastic/elastic-stack.yml new file mode 100644 index 0000000..d60e8d6 --- /dev/null +++ b/docs/demos/elastic/elastic-stack.yml @@ -0,0 +1,90 @@ +--- +version: "2.4" +networks: + default: + name: integration-testing +services: + elasticsearch: + environment: + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + - "transport.host=127.0.0.1" + - "http.host=0.0.0.0" + - "cluster.routing.allocation.disk.threshold_enabled=false" + - "discovery.type=single-node" + - "xpack.security.authc.anonymous.roles=remote_monitoring_collector" + - "xpack.security.authc.realms.file.file1.order=0" + - "xpack.security.authc.realms.native.native1.order=1" + - "xpack.security.enabled=true" + - "xpack.license.self_generated.type=trial" + - "xpack.security.authc.token.enabled=true" + - "xpack.security.authc.api_key.enabled=true" + - "logger.org.elasticsearch=${ES_LOG_LEVEL:-error}" + - "action.destructive_requires_name=false" + image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_STACK_VERSION:-8.6.0} + ports: + - "9200" + healthcheck: + interval: 20s + retries: 10 + test: + [ + "CMD-SHELL", + "curl -s http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=500ms", + ] + volumes: + - "./elasticsearch/roles.yml:/usr/share/elasticsearch/config/roles.yml" + - "./elasticsearch/users:/usr/share/elasticsearch/config/users" + - "./elasticsearch/users_roles:/usr/share/elasticsearch/config/users_roles" + - "./elasticsearch/service_tokens:/usr/share/elasticsearch/config/service_tokens" + + kibana: + environment: + ELASTICSEARCH_HOSTS: http://elasticsearch:9200 + ELASTICSEARCH_USERNAME: "${KIBANA_ES_USER:-kibana_system_user}" + ELASTICSEARCH_PASSWORD: "${KIBANA_ES_PASS:-changeme}" + STATUS_ALLOWANONYMOUS: "true" + image: docker.elastic.co/kibana/kibana:${ELASTIC_STACK_VERSION:-8.6.0} + ports: + - "5601:5601" + volumes: + - ./kibana/kibana.yml:/usr/share/kibana/config/kibana.yml + healthcheck: + interval: 10s + retries: 20 + test: + [ + "CMD-SHELL", + "curl -s http://localhost:5601/api/status | grep -q 'All services are available'", + ] + + fleet-server: + image: docker.elastic.co/beats/elastic-agent:${ELASTIC_STACK_VERSION:-8.6.0} + privileged: true + entrypoint: "/bin/bash" + command: + - "-l" + - "-c" + - "elastic-agent container" + ports: + - "8220" + - "8200:8200" + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s -k http://localhost:8220/api/status | grep -q 'HEALTHY'", + ] + retries: 300 + interval: 1s + environment: + FLEET_ELASTICSEARCH_HOST: "http://elasticsearch:9200" + FLEET_SERVER_ENABLE: "1" + FLEET_SERVER_HOST: "0.0.0.0" + FLEET_SERVER_POLICY_ID: "${FLEET_SERVER_POLICY_ID:-fleet-server-apm-policy}" + FLEET_SERVER_PORT: "8220" + KIBANA_FLEET_HOST: "http://kibana:5601" + KIBANA_FLEET_SETUP: "1" + FLEET_SERVER_INSECURE_HTTP: "1" + FLEET_SERVER_ELASTICSEARCH_INSECURE: "1" + FLEET_SERVER_SERVICE_TOKEN: AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL2VsYXN0aWMtcGFja2FnZS1mbGVldC1zZXJ2ZXItdG9rZW46bmgtcFhoQzRRQ2FXbms2U0JySGlWQQ + KIBANA_FLEET_SERVICE_TOKEN: AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL2VsYXN0aWMtcGFja2FnZS1mbGVldC1zZXJ2ZXItdG9rZW46bmgtcFhoQzRRQ2FXbms2U0JySGlWQQ diff --git a/docs/demos/elastic/elasticsearch/roles.yml b/docs/demos/elastic/elasticsearch/roles.yml new file mode 100644 index 0000000..6309c0b --- /dev/null +++ b/docs/demos/elastic/elasticsearch/roles.yml @@ -0,0 +1,9 @@ +apm_server: + cluster: ['manage_ilm', 'manage_security', 'manage_api_key'] + indices: + - names: ['apm-*', 'traces-apm*', 'logs-apm*', 'metrics-apm*'] + privileges: ['write', 'create_index', 'manage', 'manage_ilm'] + applications: + - application: 'apm' + privileges: ['sourcemap:write', 'event:write', 'config_agent:read'] + resources: '*' diff --git a/docs/demos/elastic/elasticsearch/service_tokens b/docs/demos/elastic/elasticsearch/service_tokens new file mode 100644 index 0000000..02c39a6 --- /dev/null +++ b/docs/demos/elastic/elasticsearch/service_tokens @@ -0,0 +1,2 @@ +elastic/fleet-server/elastic-package-fleet-server-token:{PBKDF2_STRETCH}10000$PNiVyY96dHwRfoDszBvYPAz+mSLbC+NhtPh63dblDZU=$dAY1tXX1U5rXB+2Lt7m0L2LUNSb1q5nRaIqPNZTBxb8= +elastic/kibana/elastic-package-kibana-token:{PBKDF2_STRETCH}10000$wIEFHIIIZ2ap0D0iQsyw0MfB7YuFA1bHnXAmlCoL4Gg=$YxvIJnasjLZyDQZpmFBiJHdR/CGXd5BnVm013Jty6p0= diff --git a/docs/demos/elastic/elasticsearch/users b/docs/demos/elastic/elasticsearch/users new file mode 100644 index 0000000..fecf0a8 --- /dev/null +++ b/docs/demos/elastic/elasticsearch/users @@ -0,0 +1,4 @@ +admin:$2a$10$xiY0ZzOKmDDN1p3if4t4muUBwh2.bFHADoMRAWQgSClm4ZJ4132Y. +apm_server_user:$2a$10$iTy29qZaCSVn4FXlIjertuO8YfYVLCbvoUAJ3idaXfLRclg9GXdGG +apm_user_ro:$2a$10$hQfy2o2u33SapUClsx8NCuRMpQyHP9b2l4t3QqrBA.5xXN2S.nT4u +kibana_system_user:$2a$10$nN6sRtQl2KX9Gn8kV/.NpOLSk6Jwn8TehEDnZ7aaAgzyl/dy5PYzW diff --git a/docs/demos/elastic/elasticsearch/users_roles b/docs/demos/elastic/elasticsearch/users_roles new file mode 100644 index 0000000..16142cd --- /dev/null +++ b/docs/demos/elastic/elasticsearch/users_roles @@ -0,0 +1,6 @@ +apm_server:apm_server_user +apm_system:apm_server_user +apm_user:apm_server_user,apm_user_ro +ingest_admin:apm_server_user +kibana_system:kibana_system_user +superuser:admin diff --git a/docs/demos/elastic/images/elastic-overview.png b/docs/demos/elastic/images/elastic-overview.png new file mode 100644 index 0000000..c54a7c0 Binary files /dev/null and b/docs/demos/elastic/images/elastic-overview.png differ diff --git a/docs/demos/elastic/images/elastic-services.png b/docs/demos/elastic/images/elastic-services.png new file mode 100644 index 0000000..c9ace16 Binary files /dev/null and b/docs/demos/elastic/images/elastic-services.png differ diff --git a/docs/demos/elastic/images/elastic-span-details.png b/docs/demos/elastic/images/elastic-span-details.png new file mode 100644 index 0000000..33e3922 Binary files /dev/null and b/docs/demos/elastic/images/elastic-span-details.png differ diff --git a/docs/demos/elastic/images/elastic-transactions.png b/docs/demos/elastic/images/elastic-transactions.png new file mode 100644 index 0000000..9f74afe Binary files /dev/null and b/docs/demos/elastic/images/elastic-transactions.png differ diff --git a/docs/demos/elastic/kibana/kibana.yml b/docs/demos/elastic/kibana/kibana.yml new file mode 100644 index 0000000..bdf52b9 --- /dev/null +++ b/docs/demos/elastic/kibana/kibana.yml @@ -0,0 +1,79 @@ +--- +server.name: kibana +server.host: "0.0.0.0" + +telemetry.enabled: false + +elasticsearch.hosts: "${ELASTICSEARCH_HOSTS}" +elasticsearch.username: "${ELASTICSEARCH_USERNAME}" +elasticsearch.password: "${ELASTICSEARCH_PASSWORD}" +xpack.monitoring.ui.container.elasticsearch.enabled: true + +xpack.fleet.registryUrl: "https://epr-staging.elastic.co" +xpack.fleet.agents.elasticsearch.host: "${ELASTICSEARCH_HOSTS}" +xpack.fleet.agents.fleet_server.hosts: ["http://fleet-server:8220"] +xpack.fleet.agents.kibana.hosts: ["http://kibana:5601"] + +xpack.encryptedSavedObjects.encryptionKey: "12345678901234567890123456789012" +xpack.security.encryptionKey: "12345678901234567890123456789012" +xpack.fleet.agents.tlsCheckDisabled: true + + +#xpack.fleet.outputs: +# - name: "Test output" +# type: "elasticsearch" +# id: "output-123" +# hosts: ["http://elasticsearch:9200"] + +xpack.security.authc.providers: + basic.basic1: + order: 1 +xpack.fleet.agents.enabled: true +xpack.fleet.packages: + - name: system + version: latest + - name: elastic_agent + version: latest + - name: apm + version: latest + - name: fleet_server + version: latest + +xpack.fleet.agentPolicies: + - name: Fleet Server + APM policy + id: fleet-server-apm-policy + description: Fleet server policy with APM and System logs and metrics enabled + namespace: default + is_default_fleet_server: true + is_managed: false + monitoring_enabled: + - logs + - metrics + package_policies: + - name: apm-1 + id: default-apm + package: + name: apm + inputs: + - type: apm + keep_enabled: true + vars: + - name: host + value: 0.0.0.0:8200 + frozen: true + - name: url + value: http://fleet-server:8200 + frozen: true + - name: Fleet Server + package: + name: fleet_server + inputs: + - type: fleet-server + keep_enabled: true + vars: + - name: host + value: 0.0.0.0 + frozen: true + - name: port + value: 8220 + frozen: true diff --git a/docs/demos/jaeger/README.md b/docs/demos/jaeger/README.md new file mode 100644 index 0000000..a2187ae --- /dev/null +++ b/docs/demos/jaeger/README.md @@ -0,0 +1,29 @@ +# Jaeger demo + +In this demo we start an OpenTelemetry collector using the Jaeger exporter and a Jaeger service, +to show how to send the OpenTelemetry spans to a Jaeger service. +Then we start the pytest-otel tests with the environment variables configured to hit the OpenTelementry collector service. + +To start the demo you have to execute the following command in the root of the pytest-otel project folder: + +```shell +make demo-start-jaeger +``` + +When the execution ends you can go to the Jaeger service (http://localhost:16686/) in a browser to show the spans + +![](images/jaeger-spans.png) + +![](images/jaeger-spans-all.png) + +![](images/jaeger-spans-detail.png) + +Finally you can stop the demo + +```shell +make demo-stop-jaeger +``` + +* [OpenTelemetry](https://opentelemetry.io/docs/) +* [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) +* [Jaeger](https://www.jaegertracing.io/docs/) diff --git a/docs/demos/jaeger/config/otel-collector-config.yaml b/docs/demos/jaeger/config/otel-collector-config.yaml new file mode 100644 index 0000000..295acea --- /dev/null +++ b/docs/demos/jaeger/config/otel-collector-config.yaml @@ -0,0 +1,37 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# imported from https://github.com/mdelapenya/junit2otlp +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + loglevel: debug + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true + +processors: + batch: + +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 + +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging, jaeger] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging] diff --git a/docs/demos/jaeger/demo.env b/docs/demos/jaeger/demo.env new file mode 100644 index 0000000..f6fa7e2 --- /dev/null +++ b/docs/demos/jaeger/demo.env @@ -0,0 +1,6 @@ +export OTEL_EXPORTER_OTLP_HEADERS="" +export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:14317" +export OTEL_EXPORTER_OTLP_INSECURE="true" +export OTEL_EXPORTER_OTLP_SPAN_INSECURE="true" +export OTEL_EXPORTER_OTLP_METRIC_INSECURE="true" +export OTEL_SERVICE_NAME="jaeger-srv" diff --git a/docs/demos/jaeger/docker-compose.yml b/docs/demos/jaeger/docker-compose.yml new file mode 100644 index 0000000..74f66ff --- /dev/null +++ b/docs/demos/jaeger/docker-compose.yml @@ -0,0 +1,36 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# imported from https://github.com/mdelapenya/junit2otlp +version: '3.7' +services: + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + networks: + - pytest_otel + + otel-collector: + image: otel/opentelemetry-collector-contrib-dev:latest + command: ["--config=/etc/otel-collector-config.yaml", "${OTELCOL_ARGS}"] + healthcheck: + interval: 10s + retries: 20 + test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:13133/ + volumes: + - ./config/otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "14317:4317" # OTLP gRPC receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one + networks: + - pytest_otel + +networks: + pytest_otel: + driver: bridge diff --git a/docs/demos/jaeger/images/jaeger-spans-all.png b/docs/demos/jaeger/images/jaeger-spans-all.png new file mode 100644 index 0000000..17ef801 Binary files /dev/null and b/docs/demos/jaeger/images/jaeger-spans-all.png differ diff --git a/docs/demos/jaeger/images/jaeger-spans-detail.png b/docs/demos/jaeger/images/jaeger-spans-detail.png new file mode 100644 index 0000000..3121bf3 Binary files /dev/null and b/docs/demos/jaeger/images/jaeger-spans-detail.png differ diff --git a/docs/demos/jaeger/images/jaeger-spans.png b/docs/demos/jaeger/images/jaeger-spans.png new file mode 100644 index 0000000..69e4e86 Binary files /dev/null and b/docs/demos/jaeger/images/jaeger-spans.png differ diff --git a/docs/demos/test/test_demo.py b/docs/demos/test/test_demo.py new file mode 100644 index 0000000..7d04e53 --- /dev/null +++ b/docs/demos/test/test_demo.py @@ -0,0 +1,35 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +pytest_plugins = ["pytester"] + +import time +import logging +import pytest + + +def test_basic(): + time.sleep(5) + pass + +def test_success(): + assert True + +def test_failure(): + assert 1 < 0 + +def test_failure_code(): + d = 1/0 + pass + +@pytest.mark.skip +def test_skip(): + assert True + +@pytest.mark.xfail(reason="foo bug") +def test_xfail(): + assert False + +@pytest.mark.xfail(run=False) +def test_xfail_no_run(): + assert False diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..fa83284 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,18 @@ +[mypy] +python_version = 3.8 +allow_untyped_defs = True +allow_untyped_calls = True +disallow_any_generics = True +disallow_subclassing_any = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True +show_error_codes = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = False +warn_no_return = True +warn_return_any = True +implicit_reexport = False +strict_equality = True +warn_unused_configs = True +pretty = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..db26f99 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +dependencies = [ + "opentelemetry-api==1.15.0", + "opentelemetry-exporter-otlp==1.15.0", + "opentelemetry-sdk==1.15.0", + "pytest==7.1.3", +] + +[build-system] +requires = ["setuptools >= 44.0.0", "wheel >= 0.30"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | _build + | build + | dist + | elasticapm/utils/wrapt + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data +)/ +''' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d27246 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +coverage==7.0.5 +opentelemetry-api==1.15.0 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-sdk==1.15.0 +psutil==5.9.3 +pytest==7.2.1 +pre-commit==2.21.0 +mypy==0.982 +pytest-docker==1.0.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..361196f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,103 @@ +[metadata] +name = pytest_otel +description = pytest-otel report OpenTelemetry traces about test executed +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/elastic/apm-pipeline-library/tree/main/resources/scripts/pytest_otel +maintainer = Ivan Fernandez Calvo +version = 1.4.0 +license = Apache-2.0 +license_file = LICENSE.txt +platforms = any +classifiers = + Environment :: Plugins + Framework :: Pytest + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Topic :: Software Development :: Libraries + Topic :: Software Development :: Testing + Topic :: Utilities +keywords = pytest, otel, opentelemetry, debug +project_urls = + Source=https://github.com/elastic/apm-pipeline-library/tree/main/resources/scripts/pytest_otel + Tracker=https://github.com/elastic/apm-pipeline-library/issues + +[options] +packages = find: +install_requires = + opentelemetry-api==1.15.0 + opentelemetry-exporter-otlp==1.15.0 + opentelemetry-sdk==1.15.0 + pytest==7.2.1 +python_requires = >=3.6 +include_package_data = True +package_dir = + =src +zip_safe = True + +[options.packages.find] +where = src + +[options.entry_points] +pytest11 = otel = pytest_otel + +[options.extras_require] +test = + coverage>=5 + +[options.package_data] +pytest_otel = py.typed + +[sdist] +formats = gztar + +[bdist_wheel] +universal = true + +[flake8] +max-line-length = 120 +ignore = F401, H301, E203, SC200, SC100, W503 +exclude = .venv,.git,__pycache__,.tox,.mypy_cache + +[coverage:run] +source = + ${_COVERAGE_SRC} + ${_COVERAGE_TEST} +parallel = True +branch = True +dynamic_context = test_function + +[coverage:report] +fail_under = 100 +skip_covered = true +show_missing = true +omit = + tests/example.py + +[coverage:html] +show_contexts = True +skip_covered = False +skip_empty = False + +[coverage:paths] +source = + src + .tox*/*/lib/python*/site-packages + .tox*/pypy*/site-packages + .tox*\*\Lib\site-packages\ + */src + *\src + +[tool:pytest] +addopts = -ra --showlocals -vv +testpaths = tests +xfail_strict = True +junit_family = xunit2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..767de11 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from setuptools import setup + +setup() diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..3c10999 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +--- +version: "3.7" +services: + otel-collector: + image: otel/opentelemetry-collector:latest + user: ${UID}:${GID} + command: ["--config=/etc/otel-collector.yaml"] + volumes: + - ./otel-collector.yaml:/etc/otel-collector.yaml + - ./:/tmp + ports: + - "4317:4317" # OTLP gRPC receiver + networks: + - default_net + +volumes: + default_net: + driver: local + esdata: + driver: local + +networks: + default_net: + driver: bridge diff --git a/tests/it/conftest.py b/tests/it/conftest.py new file mode 100644 index 0000000..42ae34e --- /dev/null +++ b/tests/it/conftest.py @@ -0,0 +1,14 @@ +import pytest +from utils import is_portListening + + +@pytest.fixture(scope="session") +def otel_service(docker_ip, docker_services): + """Ensure that port is listening.""" + + # `port_for` takes a container port and returns the corresponding host port + port = docker_services.port_for("otel-collector", 4317) + docker_services.wait_until_responsive( + timeout=30.0, pause=5, check=lambda: is_portListening(docker_ip, port) + ) + return True diff --git a/tests/it/test_basic_plugin.py b/tests/it/test_basic_plugin.py new file mode 100644 index 0000000..9320191 --- /dev/null +++ b/tests/it/test_basic_plugin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_OK + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_basic_plugin(pytester, otel_service): + """test a simple test""" + pytester.makepyfile( + common_code + + """ +def test_basic(): + time.sleep(5) + pass +""") + assertTest(pytester, "test_basic", "passed", STATUS_CODE_OK, "passed", STATUS_CODE_OK) diff --git a/tests/it/test_failure_code_plugin.py b/tests/it/test_failure_code_plugin.py new file mode 100644 index 0000000..7262078 --- /dev/null +++ b/tests/it/test_failure_code_plugin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_ERROR + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_failure_code_plugin(pytester, otel_service): + """test a test with a code exception""" + pytester.makepyfile( + common_code + + """ +def test_failure_code(): + d = 1/0 + pass +""") + assertTest(pytester, "test_failure_code", "failed", STATUS_CODE_ERROR, "failed", STATUS_CODE_ERROR) diff --git a/tests/it/test_failure_plugin.py b/tests/it/test_failure_plugin.py new file mode 100644 index 0000000..2acfd51 --- /dev/null +++ b/tests/it/test_failure_plugin.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_ERROR + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_failure_plugin(pytester, otel_service): + """test a failed test""" + pytester.makepyfile( + common_code + + """ +def test_failure(): + assert 1 < 0 +""") + assertTest(pytester, "test_failure", "failed", STATUS_CODE_ERROR, "failed", STATUS_CODE_ERROR) diff --git a/tests/it/test_skip_plugin.py b/tests/it/test_skip_plugin.py new file mode 100644 index 0000000..ead0967 --- /dev/null +++ b/tests/it/test_skip_plugin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_OK + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_skip_plugin(pytester, otel_service): + """test a skipped test""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.skip +def test_skip(): + assert True +""") + assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/it/test_success_plugin.py b/tests/it/test_success_plugin.py new file mode 100644 index 0000000..047feda --- /dev/null +++ b/tests/it/test_success_plugin.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_OK + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_success_plugin(pytester, otel_service): + """test a success test""" + pytester.makepyfile( + common_code + + """ +def test_success(): + assert True +""") + assertTest(pytester, "test_success", "passed", STATUS_CODE_OK, "passed", STATUS_CODE_OK) diff --git a/tests/it/test_xfail_no_run_plugin.py b/tests/it/test_xfail_no_run_plugin.py new file mode 100644 index 0000000..3151261 --- /dev/null +++ b/tests/it/test_xfail_no_run_plugin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_OK + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_xfail_no_run_plugin(pytester, otel_service): + """test a marked as xfail test with run==false""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.xfail(run=False) +def test_xfail_no_run(): + assert False +""") + assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/it/test_xfail_plugin.py b/tests/it/test_xfail_plugin.py new file mode 100644 index 0000000..223a980 --- /dev/null +++ b/tests/it/test_xfail_plugin.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from utils import assertTest, STATUS_CODE_OK + +pytest_plugins = ["pytester"] + +common_code = """ +import os +import time +import logging +import pytest + +""" + + +def test_xfail_plugin(pytester, otel_service): + """test a marked as xfail test""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.xfail(reason="foo bug") +def test_xfail(): + assert False +""") + assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/otel-collector.yaml b/tests/otel-collector.yaml new file mode 100644 index 0000000..413c5b7 --- /dev/null +++ b/tests/otel-collector.yaml @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +--- +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + logging: + logLevel: debug + file: + path: /tmp/tests.json +processors: + batch: + +service: + pipelines: + traces: + receivers: + - otlp + processors: + - batch + exporters: + - logging + - file diff --git a/tests/test_pytest_otel.py b/tests/test_pytest_otel.py new file mode 100644 index 0000000..394fd10 --- /dev/null +++ b/tests/test_pytest_otel.py @@ -0,0 +1,131 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import json + +pytest_plugins = ["pytester"] + +common_code = """ +import time +import logging +import pytest + +""" + + +def assertTestSuit(span, outcome, status): + assert span["kind"] == "SpanKind.SERVER" + assert span["status"]["status_code"] == status + if outcome is not None: + assert span["attributes"]["tests.status"] == outcome + assert span["parent_id"] is None + return True + + +def assertSpan(span, name, outcome, status): + assert span["kind"] == "SpanKind.INTERNAL" + assert span["status"]["status_code"] == status + assert span["attributes"]["tests.name"] == name + if outcome is not None: + assert span["attributes"]["tests.status"] == outcome + assert len(span["parent_id"]) > 0 + return True + + +def assertTest(pytester, name, ts_outcome, ts_status, outcome, status): + pytester.runpytest("--otel-span-file-output=./test_spans.json", "--otel-debug=True", "-rsx") + span_list = None + with open("test_spans.json", encoding='utf-8') as input: + span_list = json.loads(input.read()) + foundTest = False + foundTestSuit = False + for span in span_list: + if span["name"] == "Running {}".format(name): + foundTest = assertSpan(span, name, outcome, status) + if span["name"] == "Test Suite": + foundTestSuit = assertTestSuit(span, ts_outcome, ts_status) + assert foundTest or name is None + assert foundTestSuit + + +def test_basic_plugin(pytester): + """test a simple test""" + pytester.makepyfile( + common_code + + """ +def test_basic(): + time.sleep(5) + pass +""") + assertTest(pytester, "test_basic", "passed", "OK", "passed", "OK") + + +def test_success_plugin(pytester): + """test a success test""" + pytester.makepyfile( + common_code + + """ +def test_success(): + assert True +""") + assertTest(pytester, "test_success", "passed", "OK", "passed", "OK") + + +def test_failure_plugin(pytester): + """test a failed test""" + pytester.makepyfile( + common_code + + """ +def test_failure(): + assert 1 < 0 +""") + assertTest(pytester, "test_failure", "failed", "ERROR", "failed", "ERROR") + + +def test_failure_code_plugin(pytester): + """test a test with a code exception""" + pytester.makepyfile( + common_code + + """ +def test_failure_code(): + d = 1/0 + pass +""") + assertTest(pytester, "test_failure_code", "failed", "ERROR", "failed", "ERROR") + + +def test_skip_plugin(pytester): + """test a skipped test""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.skip +def test_skip(): + assert True +""") + assertTest(pytester, None, "passed", "OK", None, None) + + +def test_xfail_plugin(pytester): + """test a marked as xfail test""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.xfail(reason="foo bug") +def test_xfail(): + assert False +""") + assertTest(pytester, None, "passed", "OK", None, None) + + +def test_xfail_no_run_plugin(pytester): + """test a marked as xfail test with run==false""" + pytester.makepyfile( + common_code + + """ +@pytest.mark.xfail(run=False) +def test_xfail_no_run(): + assert False +""") + assertTest(pytester, None, "passed", "OK", None, None) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..af44248 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +envlist = + py311 + py310 + py39 + py38 + +[testenv] +deps = + pytest==7.1.3 + pytest-docker==1.0.1 +commands = + pytest {tty:--color=yes} --capture=no \ + -p pytester --runpytest=subprocess \ + --junitxml {toxworkdir}{/}junit-{envname}.xml \ + tests/test_pytest_otel.py + +[testenv:linting] +basepython = python3 +skip_install = true +deps = + pre-commit==2.20.0 +commands = + pre-commit run