From b8c0f9c6f1517104f46d696356a6d4a0784ea095 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 14 Nov 2024 18:36:28 +0100 Subject: [PATCH] Adhere to new cli span semconv --- .../instrumentation/click/__init__.py | 21 ++++- .../tests/test_click.py | 88 +++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py index b16453679a..8222bfdf5a 100644 --- a/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-click/src/opentelemetry/instrumentation/click/__init__.py @@ -38,6 +38,8 @@ def hello(): --- """ +import os +import sys from functools import partial from logging import getLogger from typing import Collection @@ -52,6 +54,12 @@ def hello(): 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 @@ -67,7 +75,12 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): ctx = args[0] span_name = ctx.info_name - span_attributes = {} + 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, @@ -78,7 +91,11 @@ def _command_invoke_wrapper(wrapped, instance, args, kwargs, tracer): return wrapped(*args, **kwargs) except Exception as exc: span.set_status(StatusCode.ERROR, str(exc)) - span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__) + if span.is_recording(): + span.set_attribute(ERROR_TYPE, exc.__class__.__qualname__) + span.set_attribute( + PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1) + ) raise diff --git a/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py index d90b748750..41d01a5bb4 100644 --- a/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py +++ b/instrumentation/opentelemetry-instrumentation-click/tests/test_click.py @@ -12,6 +12,9 @@ # 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 @@ -32,6 +35,7 @@ def tearDown(self): super().tearDown() ClickInstrumentor().uninstrument() + @mock.patch("sys.argv", ["command.py"]) def test_cli_command_wrapping(self): @click.command() def command(): @@ -45,7 +49,44 @@ def command(): 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(): @@ -59,7 +100,44 @@ def renamedcommand(): 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(): @@ -73,6 +151,16 @@ def command_raises(): 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()