Skip to content

Commit 4ffc5ab

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

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

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.orig_argv,
80+
PROCESS_EXECUTABLE_NAME: sys.orig_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

instrumentation/opentelemetry-instrumentation-click/tests/test_click.py

+62-1
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.orig_argv", ["command"])
3539
def test_cli_command_wrapping(self):
3640
@click.command()
3741
def command():
@@ -45,7 +49,17 @@ def command():
4549
self.assertEqual(span.status.status_code, StatusCode.UNSET)
4650
self.assertEqual(span.kind, SpanKind.INTERNAL)
4751
self.assertEqual(span.name, "command")
48-
52+
self.assertEqual(
53+
dict(span.attributes),
54+
{
55+
"process.executable.name": "command",
56+
"process.command_args": ("command",),
57+
"process.exit.code": 0,
58+
"process.pid": os.getpid(),
59+
},
60+
)
61+
62+
@mock.patch("sys.orig_argv", ["mycommand"])
4963
def test_cli_command_wrapping_with_name(self):
5064
@click.command("mycommand")
5165
def renamedcommand():
@@ -59,7 +73,44 @@ def renamedcommand():
5973
self.assertEqual(span.status.status_code, StatusCode.UNSET)
6074
self.assertEqual(span.kind, SpanKind.INTERNAL)
6175
self.assertEqual(span.name, "mycommand")
76+
self.assertEqual(
77+
dict(span.attributes),
78+
{
79+
"process.executable.name": "mycommand",
80+
"process.command_args": ("mycommand",),
81+
"process.exit.code": 0,
82+
"process.pid": os.getpid(),
83+
},
84+
)
85+
86+
@mock.patch("sys.orig_argv", ["command", "--opt", "argument"])
87+
def test_cli_command_wrapping_with_options(self):
88+
@click.command()
89+
@click.argument("argument")
90+
@click.option("--opt/--no-opt", default=False)
91+
def command(argument, opt):
92+
pass
6293

94+
argv = ["command", "--opt", "argument"]
95+
runner = CliRunner()
96+
result = runner.invoke(command, argv[1:])
97+
self.assertEqual(result.exit_code, 0)
98+
99+
(span,) = self.memory_exporter.get_finished_spans()
100+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
101+
self.assertEqual(span.kind, SpanKind.INTERNAL)
102+
self.assertEqual(span.name, "command")
103+
self.assertEqual(
104+
dict(span.attributes),
105+
{
106+
"process.executable.name": "command",
107+
"process.command_args": tuple(argv),
108+
"process.exit.code": 0,
109+
"process.pid": os.getpid(),
110+
},
111+
)
112+
113+
@mock.patch("sys.orig_argv", ["command-raises"])
63114
def test_cli_command_raises_error(self):
64115
@click.command()
65116
def command_raises():
@@ -73,6 +124,16 @@ def command_raises():
73124
self.assertEqual(span.status.status_code, StatusCode.ERROR)
74125
self.assertEqual(span.kind, SpanKind.INTERNAL)
75126
self.assertEqual(span.name, "command-raises")
127+
self.assertEqual(
128+
dict(span.attributes),
129+
{
130+
"process.executable.name": "command-raises",
131+
"process.command_args": ("command-raises",),
132+
"process.exit.code": 1,
133+
"process.pid": os.getpid(),
134+
"error.type": "ValueError",
135+
},
136+
)
76137

77138
def test_uninstrument(self):
78139
ClickInstrumentor().uninstrument()

0 commit comments

Comments
 (0)