Skip to content

Commit c508093

Browse files
xrmxanuraaga
andcommitted
Add instrumentation for click based CLI apps
Co-authored-by: Anuraag (Rag) Agrawal <[email protected]>
1 parent 4606cf2 commit c508093

File tree

10 files changed

+304
-0
lines changed

10 files changed

+304
-0
lines changed

instrumentation/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
| [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | experimental
1616
| [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No | experimental
1717
| [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No | experimental
18+
| [opentelemetry-instrumentation-click](./opentelemetry-instrumentation-click) | click < 9.0.0 | No | experimental
1819
| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.4.0 | No | experimental
1920
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental
2021
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
OpenTelemetry click Instrumentation
2+
===========================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-click.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-click/
8+
9+
This library allows tracing requests made by the click library.
10+
11+
Installation
12+
------------
13+
14+
15+
::
16+
17+
pip install opentelemetry-instrumentation-click
18+
19+
20+
References
21+
----------
22+
23+
* `OpenTelemetry click/ Tracing <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/click/click.html>`_
24+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "opentelemetry-instrumentation-click"
7+
dynamic = ["version"]
8+
description = "Click instrumentation for OpenTelemetry"
9+
readme = "README.rst"
10+
license = "Apache-2.0"
11+
requires-python = ">=3.8"
12+
authors = [
13+
{ name = "OpenTelemetry Authors", email = "[email protected]" },
14+
]
15+
classifiers = [
16+
"Development Status :: 4 - Beta",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: Apache Software License",
19+
"Programming Language :: Python",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.8",
22+
"Programming Language :: Python :: 3.9",
23+
"Programming Language :: Python :: 3.10",
24+
"Programming Language :: Python :: 3.11",
25+
"Programming Language :: Python :: 3.12",
26+
]
27+
dependencies = [
28+
"opentelemetry-api ~= 1.12",
29+
"opentelemetry-semantic-conventions ~= 0.50b0",
30+
]
31+
32+
[project.optional-dependencies]
33+
instruments = [
34+
"click < 9.0.0",
35+
]
36+
37+
[project.entry-points.opentelemetry_instrumentor]
38+
click = "opentelemetry.instrumentation.click:ClickInstrumentor>"
39+
40+
[project.urls]
41+
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-click"
42+
43+
[tool.hatch.version]
44+
path = "src/opentelemetry/instrumentation/click/version.py"
45+
46+
[tool.hatch.build.targets.sdist]
47+
include = [
48+
"/src",
49+
"/tests",
50+
]
51+
52+
[tool.hatch.build.targets.wheel]
53+
packages = ["src/opentelemetry"]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Instrument `click`_ CLI applications.
17+
18+
.. _click: https://pypi.org/project/click/
19+
20+
Usage
21+
-----
22+
23+
.. code-block:: python
24+
25+
import click
26+
from opentelemetry.instrumentation.click import ClickInstrumentor
27+
28+
ClickInstrumentor().instrument()
29+
30+
@click.command()
31+
def hello():
32+
click.echo(f'Hello world!')
33+
34+
if __name__ == "__main__":
35+
hello()
36+
37+
API
38+
---
39+
"""
40+
41+
from functools import partial
42+
from logging import getLogger
43+
from typing import Collection
44+
45+
import click
46+
from wrapt import wrap_function_wrapper
47+
48+
from opentelemetry import trace
49+
from opentelemetry.instrumentation.click.package import _instruments
50+
from opentelemetry.instrumentation.click.version import __version__
51+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
52+
from opentelemetry.instrumentation.utils import (
53+
unwrap,
54+
)
55+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
56+
from opentelemetry.trace.status import StatusCode
57+
58+
_logger = getLogger(__name__)
59+
60+
61+
def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer):
62+
# Subclasses of Command include groups and CLI runners, but
63+
# we only want to instrument the actual commands which are
64+
# instances of Command itself.
65+
if instance.__class__ != click.Command:
66+
return wrapped(*args, **kwargs)
67+
68+
ctx = args[0]
69+
span_name = ctx.info_name
70+
span_attributes = {}
71+
72+
with tracer.start_as_current_span(
73+
name=span_name,
74+
kind=trace.SpanKind.INTERNAL,
75+
attributes=span_attributes,
76+
) as span:
77+
try:
78+
return wrapped(*args, **kwargs)
79+
except Exception as exc:
80+
span.set_status(StatusCode.ERROR, str(exc))
81+
span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__)
82+
raise
83+
84+
85+
class ClickInstrumentor(BaseInstrumentor):
86+
"""An instrumentor for click"""
87+
88+
def instrumentation_dependencies(self) -> Collection[str]:
89+
return _instruments
90+
91+
def _instrument(self, **kwargs):
92+
tracer = trace.get_tracer(
93+
__name__,
94+
__version__,
95+
kwargs.get("tracer_provider"),
96+
)
97+
98+
wrap_function_wrapper(
99+
click.core.Command,
100+
"invoke",
101+
partial(_command_invoke_wrapper, tracer=tracer),
102+
)
103+
104+
def _uninstrument(self, **kwargs):
105+
unwrap(click.core.Command, "invoke")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
_instruments = ("click < 9.0.0",)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.50b0.dev"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
asgiref==3.8.1
2+
click==8.1.7
3+
Deprecated==1.2.14
4+
iniconfig==2.0.0
5+
packaging==24.0
6+
pluggy==1.5.0
7+
py-cpuinfo==9.0.0
8+
pytest==7.4.4
9+
pytest-asyncio==0.23.5
10+
tomli==2.0.1
11+
typing_extensions==4.12.2
12+
wrapt==1.16.0
13+
zipp==3.19.2
14+
-e opentelemetry-instrumentation
15+
-e instrumentation/opentelemetry-instrumentation-click
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import click
16+
from click.testing import CliRunner
17+
18+
from opentelemetry.instrumentation.click import ClickInstrumentor
19+
from opentelemetry.test.test_base import TestBase
20+
from opentelemetry.trace.status import StatusCode
21+
22+
23+
class TestAutomatic(TestBase):
24+
def setUp(self):
25+
super().setUp()
26+
27+
ClickInstrumentor().instrument()
28+
29+
def tearDown(self):
30+
super().tearDown()
31+
ClickInstrumentor().uninstrument()
32+
33+
def test_cli_command_wrapping(self):
34+
@click.command()
35+
def command():
36+
pass
37+
38+
runner = CliRunner()
39+
result = runner.invoke(command, ["command"])
40+
self.assertEqual(result.exit_code, 0)
41+
42+
(span,) = self.memory_exporter.get_finished_spans()
43+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
44+
self.assertEqual(span.name, "command")
45+
46+
def test_cli_command_wrapping_with_name(self):
47+
@click.command("mycommand")
48+
def renamedcommand():
49+
pass
50+
51+
runner = CliRunner()
52+
result = runner.invoke(renamedcommand, ["mycommand"])
53+
self.assertEqual(result.exit_code, 0)
54+
55+
(span,) = self.memory_exporter.get_finished_spans()
56+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
57+
self.assertEqual(span.name, "mycommand")
58+
59+
def test_cli_command_raises_error(self):
60+
@click.command()
61+
def command_raises():
62+
raise ValueError()
63+
64+
runner = CliRunner()
65+
result = runner.invoke(command_raises, ["command-raises"])
66+
self.assertEqual(result.exit_code, 2)
67+
68+
(span,) = self.memory_exporter.get_finished_spans()
69+
self.assertEqual(span.status.status_code, StatusCode.ERROR)
70+
self.assertEqual(span.name, "command_raises")

opentelemetry-contrib-instrumentations/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"opentelemetry-instrumentation-botocore==0.50b0.dev",
4444
"opentelemetry-instrumentation-cassandra==0.50b0.dev",
4545
"opentelemetry-instrumentation-celery==0.50b0.dev",
46+
"opentelemetry-instrumentation-click==0.50b0.dev",
4647
"opentelemetry-instrumentation-confluent-kafka==0.50b0.dev",
4748
"opentelemetry-instrumentation-dbapi==0.50b0.dev",
4849
"opentelemetry-instrumentation-django==0.50b0.dev",

opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@
6868
"library": "celery >= 4.0, < 6.0",
6969
"instrumentation": "opentelemetry-instrumentation-celery==0.50b0.dev",
7070
},
71+
{
72+
"library": "click < 9.0.0",
73+
"instrumentation": "opentelemetry-instrumentation-click==0.50b0.dev",
74+
},
7175
{
7276
"library": "confluent-kafka >= 1.8.2, <= 2.4.0",
7377
"instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.50b0.dev",

0 commit comments

Comments
 (0)