Skip to content

Commit b8c0f9c

Browse files
committed
Adhere to new cli span semconv
1 parent 4251b82 commit b8c0f9c

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

Diff for: instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def hello():
3838
---
3939
"""
4040

41+
import os
42+
import sys
4143
from functools import partial
4244
from logging import getLogger
4345
from typing import Collection
@@ -52,6 +54,12 @@ def hello():
5254
from opentelemetry.instrumentation.utils import (
5355
unwrap,
5456
)
57+
from opentelemetry.semconv._incubating.attributes.process_attributes import (
58+
PROCESS_COMMAND_ARGS,
59+
PROCESS_EXECUTABLE_NAME,
60+
PROCESS_EXIT_CODE,
61+
PROCESS_PID,
62+
)
5563
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
5664
from opentelemetry.trace.status import StatusCode
5765

@@ -67,7 +75,12 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer):
6775

6876
ctx = args[0]
6977
span_name = ctx.info_name
70-
span_attributes = {}
78+
span_attributes = {
79+
PROCESS_COMMAND_ARGS: sys.argv,
80+
PROCESS_EXECUTABLE_NAME: sys.argv[0],
81+
PROCESS_EXIT_CODE: 0,
82+
PROCESS_PID: os.getpid(),
83+
}
7184

7285
with tracer.start_as_current_span(
7386
name=span_name,
@@ -78,7 +91,11 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer):
7891
return wrapped(*args, **kwargs)
7992
except Exception as exc:
8093
span.set_status(StatusCode.ERROR, str(exc))
81-
span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__)
94+
if span.is_recording():
95+
span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__)
96+
span.set_attribute(
97+
PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1)
98+
)
8299
raise
83100

84101

Diff for: instrumentation/opentelemetry-instrumentation-click/tests/test_click.py

+88
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
16+
from unittest import mock
17+
1518
import click
1619
from click.testing import CliRunner
1720

@@ -32,6 +35,7 @@ def tearDown(self):
3235
super().tearDown()
3336
ClickInstrumentor().uninstrument()
3437

38+
@mock.patch("sys.argv", ["command.py"])
3539
def test_cli_command_wrapping(self):
3640
@click.command()
3741
def command():
@@ -45,7 +49,44 @@ def command():
4549
self.assertEqual(span.status.status_code, StatusCode.UNSET)
4650
self.assertEqual(span.kind, SpanKind.INTERNAL)
4751
self.assertEqual(span.name, "command")
52+
self.assertEqual(
53+
dict(span.attributes),
54+
{
55+
"process.executable.name": "command.py",
56+
"process.command_args": ("command.py",),
57+
"process.exit.code": 0,
58+
"process.pid": os.getpid(),
59+
},
60+
)
61+
62+
@mock.patch("sys.argv", ["flask", "command"])
63+
def test_flask_run_command_wrapping(self):
64+
@click.command()
65+
def command():
66+
pass
4867

68+
runner = CliRunner()
69+
result = runner.invoke(command)
70+
self.assertEqual(result.exit_code, 0)
71+
72+
(span,) = self.memory_exporter.get_finished_spans()
73+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
74+
self.assertEqual(span.kind, SpanKind.INTERNAL)
75+
self.assertEqual(span.name, "command")
76+
self.assertEqual(
77+
dict(span.attributes),
78+
{
79+
"process.executable.name": "flask",
80+
"process.command_args": (
81+
"flask",
82+
"command",
83+
),
84+
"process.exit.code": 0,
85+
"process.pid": os.getpid(),
86+
},
87+
)
88+
89+
@mock.patch("sys.argv", ["command.py"])
4990
def test_cli_command_wrapping_with_name(self):
5091
@click.command("mycommand")
5192
def renamedcommand():
@@ -59,7 +100,44 @@ def renamedcommand():
59100
self.assertEqual(span.status.status_code, StatusCode.UNSET)
60101
self.assertEqual(span.kind, SpanKind.INTERNAL)
61102
self.assertEqual(span.name, "mycommand")
103+
self.assertEqual(
104+
dict(span.attributes),
105+
{
106+
"process.executable.name": "command.py",
107+
"process.command_args": ("command.py",),
108+
"process.exit.code": 0,
109+
"process.pid": os.getpid(),
110+
},
111+
)
112+
113+
@mock.patch("sys.argv", ["command.py", "--opt", "argument"])
114+
def test_cli_command_wrapping_with_options(self):
115+
@click.command()
116+
@click.argument("argument")
117+
@click.option("--opt/--no-opt", default=False)
118+
def command(argument, opt):
119+
pass
120+
121+
argv = ["command.py", "--opt", "argument"]
122+
runner = CliRunner()
123+
result = runner.invoke(command, argv[1:])
124+
self.assertEqual(result.exit_code, 0)
62125

126+
(span,) = self.memory_exporter.get_finished_spans()
127+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
128+
self.assertEqual(span.kind, SpanKind.INTERNAL)
129+
self.assertEqual(span.name, "command")
130+
self.assertEqual(
131+
dict(span.attributes),
132+
{
133+
"process.executable.name": "command.py",
134+
"process.command_args": tuple(argv),
135+
"process.exit.code": 0,
136+
"process.pid": os.getpid(),
137+
},
138+
)
139+
140+
@mock.patch("sys.argv", ["command-raises.py"])
63141
def test_cli_command_raises_error(self):
64142
@click.command()
65143
def command_raises():
@@ -73,6 +151,16 @@ def command_raises():
73151
self.assertEqual(span.status.status_code, StatusCode.ERROR)
74152
self.assertEqual(span.kind, SpanKind.INTERNAL)
75153
self.assertEqual(span.name, "command-raises")
154+
self.assertEqual(
155+
dict(span.attributes),
156+
{
157+
"process.executable.name": "command-raises.py",
158+
"process.command_args": ("command-raises.py",),
159+
"process.exit.code": 1,
160+
"process.pid": os.getpid(),
161+
"error.type": "ValueError",
162+
},
163+
)
76164

77165
def test_uninstrument(self):
78166
ClickInstrumentor().uninstrument()

0 commit comments

Comments
 (0)