From d330ab3dd055f7da69f273d2cf1074dd128dfd62 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 27 Nov 2024 09:38:38 +0100 Subject: [PATCH] Add click instrumentation (#2994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add instrumentation for click based CLI apps Co-authored-by: Anuraag (Rag) Agrawal * Add tox * Add Changelog * Silence pylint * Update tox.ini Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> * Update instrumentation/opentelemetry-instrumentation-click/pyproject.toml * Add baseline version * Adhere to new cli span semconv * Update workflows --------- Co-authored-by: Anuraag (Rag) Agrawal Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- .github/workflows/core_contrib_test_0.yml | 22 ++ .github/workflows/lint_0.yml | 18 ++ .github/workflows/test_0.yml | 216 +++++++++--------- .github/workflows/test_1.yml | 144 +++++++++--- .github/workflows/test_2.yml | 53 +++++ CHANGELOG.md | 2 + instrumentation/README.md | 1 + .../README.rst | 24 ++ .../pyproject.toml | 54 +++++ .../instrumentation/click/__init__.py | 122 ++++++++++ .../instrumentation/click/package.py | 16 ++ .../instrumentation/click/version.py | 15 ++ .../test-requirements.txt | 15 ++ .../tests/test_click.py | 176 ++++++++++++++ .../pyproject.toml | 1 + .../instrumentation/bootstrap_gen.py | 4 + tox.ini | 14 ++ 17 files changed, 753 insertions(+), 144 deletions(-) create mode 100644 .github/workflows/test_2.yml create mode 100644 instrumentation/opentelemetry-instrumentation-click/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-click/pyproject.toml create mode 100644 instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-click/test-requirements.txt create mode 100644 instrumentation/opentelemetry-instrumentation-click/tests/test_click.py diff --git a/.github/workflows/core_contrib_test_0.yml b/.github/workflows/core_contrib_test_0.yml index b67b9eae42..67bda629ff 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -459,6 +459,28 @@ jobs: - name: Run tests run: tox -e py38-test-instrumentation-boto -- -ra + py38-test-instrumentation-click: + name: instrumentation-click + runs-on: ubuntu-latest + steps: + - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + architecture: "x64" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py38-test-instrumentation-click -- -ra + py38-test-instrumentation-elasticsearch-0: name: instrumentation-elasticsearch-0 runs-on: ubuntu-latest diff --git a/.github/workflows/lint_0.yml b/.github/workflows/lint_0.yml index 1fd3198785..9d77ef5e27 100644 --- a/.github/workflows/lint_0.yml +++ b/.github/workflows/lint_0.yml @@ -286,6 +286,24 @@ jobs: - name: Run tests run: tox -e lint-instrumentation-boto + lint-instrumentation-click: + name: instrumentation-click + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e lint-instrumentation-click + lint-instrumentation-elasticsearch: name: instrumentation-elasticsearch runs-on: ubuntu-latest diff --git a/.github/workflows/test_0.yml b/.github/workflows/test_0.yml index 1b8376fdc2..47c9a19cf3 100644 --- a/.github/workflows/test_0.yml +++ b/.github/workflows/test_0.yml @@ -2032,6 +2032,114 @@ jobs: - name: Run tests run: tox -e py311-test-instrumentation-boto -- -ra + py38-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py38-test-instrumentation-click -- -ra + + py39-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py39-test-instrumentation-click -- -ra + + py310-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py310-test-instrumentation-click -- -ra + + py311-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py311-test-instrumentation-click -- -ra + + py312-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py312-test-instrumentation-click -- -ra + + pypy3-test-instrumentation-click_ubuntu-latest: + name: instrumentation-click pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e pypy3-test-instrumentation-click -- -ra + py38-test-instrumentation-elasticsearch-0_ubuntu-latest: name: instrumentation-elasticsearch-0 3.8 Ubuntu runs-on: ubuntu-latest @@ -4407,111 +4515,3 @@ jobs: - name: Run tests run: tox -e py312-test-instrumentation-psycopg2 -- -ra - - py38-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py38-test-instrumentation-psycopg -- -ra - - py39-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py39-test-instrumentation-psycopg -- -ra - - py310-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py310-test-instrumentation-psycopg -- -ra - - py311-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg 3.11 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.11 - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py311-test-instrumentation-psycopg -- -ra - - py312-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg 3.12 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py312-test-instrumentation-psycopg -- -ra - - pypy3-test-instrumentation-psycopg_ubuntu-latest: - name: instrumentation-psycopg pypy-3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e pypy3-test-instrumentation-psycopg -- -ra diff --git a/.github/workflows/test_1.yml b/.github/workflows/test_1.yml index 227c891d0b..9c5d48aea3 100644 --- a/.github/workflows/test_1.yml +++ b/.github/workflows/test_1.yml @@ -16,6 +16,114 @@ env: jobs: + py38-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py38-test-instrumentation-psycopg -- -ra + + py39-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py39-test-instrumentation-psycopg -- -ra + + py310-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py310-test-instrumentation-psycopg -- -ra + + py311-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py311-test-instrumentation-psycopg -- -ra + + py312-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py312-test-instrumentation-psycopg -- -ra + + pypy3-test-instrumentation-psycopg_ubuntu-latest: + name: instrumentation-psycopg pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e pypy3-test-instrumentation-psycopg -- -ra + py38-test-instrumentation-pymemcache-0_ubuntu-latest: name: instrumentation-pymemcache-0 3.8 Ubuntu runs-on: ubuntu-latest @@ -4407,39 +4515,3 @@ jobs: - name: Run tests run: tox -e py311-test-processor-baggage -- -ra - - py312-test-processor-baggage_ubuntu-latest: - name: processor-baggage 3.12 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e py312-test-processor-baggage -- -ra - - pypy3-test-processor-baggage_ubuntu-latest: - name: processor-baggage pypy-3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e pypy3-test-processor-baggage -- -ra diff --git a/.github/workflows/test_2.yml b/.github/workflows/test_2.yml new file mode 100644 index 0000000000..c23866ffa8 --- /dev/null +++ b/.github/workflows/test_2.yml @@ -0,0 +1,53 @@ +# Do not edit this file. +# This file is generated automatically by executing tox -e generate-workflows + +name: Test 2 + +on: + push: + branches-ignore: + - 'release/*' + pull_request: + +env: + CORE_REPO_SHA: main + CONTRIB_REPO_SHA: main + PIP_EXISTS_ACTION: w + +jobs: + + py312-test-processor-baggage_ubuntu-latest: + name: processor-baggage 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e py312-test-processor-baggage -- -ra + + pypy3-test-processor-baggage_ubuntu-latest: + name: processor-baggage pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e pypy3-test-processor-baggage -- -ra diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cb0ebb53..a28c5039c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2941)) - `opentelemetry-instrumentation-pymysql` Add sqlcommenter support ([#2942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2942)) +- `opentelemetry-instrumentation-click`: new instrumentation to trace click commands + ([#2994](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2994)) ### Fixed diff --git a/instrumentation/README.md b/instrumentation/README.md index fb601266ea..bff37fde6c 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -15,6 +15,7 @@ | [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | experimental | [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No | experimental | [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No | experimental +| [opentelemetry-instrumentation-click](./opentelemetry-instrumentation-click) | click >= 8.1.3, < 9.0.0 | No | experimental | [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.4.0 | No | experimental | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental diff --git a/instrumentation/opentelemetry-instrumentation-click/README.rst b/instrumentation/opentelemetry-instrumentation-click/README.rst new file mode 100644 index 0000000000..bb7ae97147 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry click Instrumentation +=========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-click.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-click/ + +This library allows tracing requests made by the click library. + +Installation +------------ + + +:: + + pip install opentelemetry-instrumentation-click + + +References +---------- + +* `OpenTelemetry click/ Tracing `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-click/pyproject.toml b/instrumentation/opentelemetry-instrumentation-click/pyproject.toml new file mode 100644 index 0000000000..d5b8e71830 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-click" +dynamic = ["version"] +description = "Click instrumentation for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.8" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-semantic-conventions == 0.50b0.dev", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [ + "click >= 8.1.3, < 9.0.0", +] + +[project.entry-points.opentelemetry_instrumentor] +click = "opentelemetry.instrumentation.click:ClickInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-click" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/click/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py new file mode 100644 index 0000000000..8222bfdf5a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py @@ -0,0 +1,122 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +""" +Instrument `click`_ CLI applications. + +.. _click: https://pypi.org/project/click/ + +Usage +----- + +.. code-block:: python + + import click + from opentelemetry.instrumentation.click import ClickInstrumentor + + ClickInstrumentor().instrument() + + @click.command() + def hello(): + click.echo(f'Hello world!') + + if __name__ == "__main__": + hello() + +API +--- +""" + +import os +import sys +from functools import partial +from logging import getLogger +from typing import Collection + +import click +from wrapt import wrap_function_wrapper + +from opentelemetry import trace +from opentelemetry.instrumentation.click.package import _instruments +from opentelemetry.instrumentation.click.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import ( + unwrap, +) +from opentelemetry.semconv._incubating.attributes.process_attributes import ( + PROCESS_COMMAND_ARGS, + PROCESS_EXECUTABLE_NAME, + PROCESS_EXIT_CODE, + PROCESS_PID, +) +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.trace.status import StatusCode + +_logger = getLogger(__name__) + + +def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): + # Subclasses of Command include groups and CLI runners, but + # we only want to instrument the actual commands which are + # instances of Command itself. + if instance.__class__ != click.Command: + return wrapped(*args, **kwargs) + + ctx = args[0] + span_name = ctx.info_name + span_attributes = { + PROCESS_COMMAND_ARGS: sys.argv, + PROCESS_EXECUTABLE_NAME: sys.argv[0], + PROCESS_EXIT_CODE: 0, + PROCESS_PID: os.getpid(), + } + + with tracer.start_as_current_span( + name=span_name, + kind=trace.SpanKind.INTERNAL, + attributes=span_attributes, + ) as span: + try: + return wrapped(*args, **kwargs) + except Exception as exc: + span.set_status(StatusCode.ERROR, str(exc)) + if span.is_recording(): + span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__) + span.set_attribute( + PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1) + ) + raise + + +class ClickInstrumentor(BaseInstrumentor): + """An instrumentor for click""" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + tracer = trace.get_tracer( + __name__, + __version__, + kwargs.get("tracer_provider"), + ) + + wrap_function_wrapper( + click.core.Command, + "invoke", + partial(_command_invoke_wrapper, tracer=tracer), + ) + + def _uninstrument(self, **kwargs): + unwrap(click.core.Command, "invoke") diff --git a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/package.py b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/package.py new file mode 100644 index 0000000000..6e0a1db2b5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/package.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + + +_instruments = ("click >= 8.1.3, < 9.0.0",) diff --git a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/version.py b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/version.py new file mode 100644 index 0000000000..0559ba6227 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +__version__ = "0.50b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt new file mode 100644 index 0000000000..6e9162ccde --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt @@ -0,0 +1,15 @@ +asgiref==3.8.1 +click==8.1.7 +Deprecated==1.2.14 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-asyncio==0.23.5 +tomli==2.0.1 +typing_extensions==4.12.2 +wrapt==1.16.0 +zipp==3.19.2 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-click diff --git a/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py new file mode 100644 index 0000000000..41d01a5bb4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py @@ -0,0 +1,176 @@ +# Copyright The OpenTelemetry Authors +# +# 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. + +import os +from unittest import mock + +import click +from click.testing import CliRunner + +from opentelemetry.instrumentation.click import ClickInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import StatusCode + + +class ClickTestCase(TestBase): + # pylint: disable=unbalanced-tuple-unpacking + def setUp(self): + super().setUp() + + ClickInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + ClickInstrumentor().uninstrument() + + @mock.patch("sys.argv", ["command.py"]) + def test_cli_command_wrapping(self): + @click.command() + def command(): + pass + + runner = CliRunner() + result = runner.invoke(command) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": ("command.py",), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["flask", "command"]) + def test_flask_run_command_wrapping(self): + @click.command() + def command(): + pass + + runner = CliRunner() + result = runner.invoke(command) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "flask", + "process.command_args": ( + "flask", + "command", + ), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command.py"]) + def test_cli_command_wrapping_with_name(self): + @click.command("mycommand") + def renamedcommand(): + pass + + runner = CliRunner() + result = runner.invoke(renamedcommand) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "mycommand") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": ("command.py",), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command.py", "--opt", "argument"]) + def test_cli_command_wrapping_with_options(self): + @click.command() + @click.argument("argument") + @click.option("--opt/--no-opt", default=False) + def command(argument, opt): + pass + + argv = ["command.py", "--opt", "argument"] + runner = CliRunner() + result = runner.invoke(command, argv[1:]) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": tuple(argv), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command-raises.py"]) + def test_cli_command_raises_error(self): + @click.command() + def command_raises(): + raise ValueError() + + runner = CliRunner() + result = runner.invoke(command_raises) + self.assertEqual(result.exit_code, 1) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command-raises") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command-raises.py", + "process.command_args": ("command-raises.py",), + "process.exit.code": 1, + "process.pid": os.getpid(), + "error.type": "ValueError", + }, + ) + + def test_uninstrument(self): + ClickInstrumentor().uninstrument() + + @click.command() + def notracecommand(): + pass + + runner = CliRunner() + result = runner.invoke(notracecommand) + self.assertEqual(result.exit_code, 0) + + self.assertFalse(self.memory_exporter.get_finished_spans()) diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index 11eae92ba6..a0edde1390 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "opentelemetry-instrumentation-botocore==0.50b0.dev", "opentelemetry-instrumentation-cassandra==0.50b0.dev", "opentelemetry-instrumentation-celery==0.50b0.dev", + "opentelemetry-instrumentation-click==0.50b0.dev", "opentelemetry-instrumentation-confluent-kafka==0.50b0.dev", "opentelemetry-instrumentation-dbapi==0.50b0.dev", "opentelemetry-instrumentation-django==0.50b0.dev", diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 6b7eae6b00..a292299d70 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -72,6 +72,10 @@ "library": "celery >= 4.0, < 6.0", "instrumentation": "opentelemetry-instrumentation-celery==0.50b0.dev", }, + { + "library": "click >= 8.1.3, < 9.0.0", + "instrumentation": "opentelemetry-instrumentation-click==0.50b0.dev", + }, { "library": "confluent-kafka >= 1.8.2, <= 2.4.0", "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.50b0.dev", diff --git a/tox.ini b/tox.ini index cc5e509abc..5fa58e5139 100644 --- a/tox.ini +++ b/tox.ini @@ -94,6 +94,11 @@ envlist = ; pypy3-test-instrumentation-boto lint-instrumentation-boto + ; opentelemetry-instrumentation-click + py3{8,9,10,11,12}-test-instrumentation-click + pypy3-test-instrumentation-click + lint-instrumentation-click + ; opentelemetry-instrumentation-elasticsearch ; The numbers at the end of the environment names ; below mean these dependencies are being used: @@ -443,6 +448,12 @@ commands_pre = pypy3-test-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt lint-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt + click: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + click: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + click: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + click: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + click: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt + sio-pika: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api sio-pika: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions sio-pika: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk @@ -876,6 +887,9 @@ commands = test-instrumentation-celery: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests {posargs} lint-instrumentation-celery: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-celery" + test-instrumentation-click: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-click/tests {posargs} + lint-instrumentation-click: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-click" + test-instrumentation-dbapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests {posargs} lint-instrumentation-dbapi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-dbapi"