From f3e6632157ed49811c1e045f5a5436c98aa0053b Mon Sep 17 00:00:00 2001 From: Robert Raposa Date: Mon, 10 Aug 2026 16:45:29 -0400 Subject: [PATCH] feat: add operation_name to create_span Adds optional operation_name to create_span. --- CHANGELOG.rst | 4 +++ edx_django_utils/__init__.py | 2 +- .../monitoring/internal/backends.py | 23 ++++++++++---- edx_django_utils/monitoring/internal/utils.py | 4 +-- .../monitoring/tests/test_backends.py | 31 ++++++++++++++++++- 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e905df43..949574cf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Change Log Unreleased ---------- +8.1.0 - 2026-08-07 +------------------ +* Adds optional ``operation_name`` argument to ``function_trace`` and ``TelemetryBackend.create_span``. + 8.0.1 - 2025-09-29 ------------------ * Stop using deprecated newrelic function calls that were removed in newrelic 11.0.0 (use newer names instead) diff --git a/edx_django_utils/__init__.py b/edx_django_utils/__init__.py index c000c754..1e5d92ea 100644 --- a/edx_django_utils/__init__.py +++ b/edx_django_utils/__init__.py @@ -2,4 +2,4 @@ EdX utilities for Django Application development.. """ -__version__ = "8.0.1" +__version__ = "8.1.0" diff --git a/edx_django_utils/monitoring/internal/backends.py b/edx_django_utils/monitoring/internal/backends.py index 6a1f1986..2d490fea 100644 --- a/edx_django_utils/monitoring/internal/backends.py +++ b/edx_django_utils/monitoring/internal/backends.py @@ -51,7 +51,7 @@ def record_exception(self): """ @abstractmethod - def create_span(self, name): + def create_span(self, name, operation_name=None): """ Start a tracing span with the given name, returning a context manager instance. @@ -60,6 +60,11 @@ def create_span(self, name): Implementations should create a new child span parented to the current span, or create a new root span if not currently in a span. + + Arguments: + name: label for the specific span (e.g. a function or method + path). Maps to OpenTelemetry's span name field. + operation_name: optional label for the class/type of operation (e.g. "django.request"). """ @abstractmethod @@ -98,7 +103,8 @@ def set_attribute(self, key, value): def record_exception(self): newrelic.agent.notice_error() - def create_span(self, name): + def create_span(self, name, operation_name=None): # pylint: disable=unused-argument + # The operation_name is ignored, but could be added as a custom attribute. if newrelic.version_info[0] >= 5: return newrelic.agent.FunctionTrace(name) else: @@ -133,8 +139,9 @@ def set_attribute(self, key, value): def record_exception(self): self.otel_trace.get_current_span().record_exception(sys.exc_info()[1]) - def create_span(self, name): - # Currently, this is not implemented. + def create_span(self, name, operation_name=None): + # Currently, this is not implemented. If implemented, operation_name + # (if provided) could be set as a span attribute. pass def tag_root_span_with_error(self, exception): @@ -166,8 +173,12 @@ def record_exception(self): if span := self.dd_tracer.current_span(): span.set_traceback() - def create_span(self, name): - return self.dd_tracer.trace(name) + def create_span(self, name, operation_name=None): + # The name is always used as the resource name. If the operation_name + # is provided, it will be used in place of the resource name as ddtrace's + # name argument. + dd_operation_name = operation_name if operation_name is not None else name + return self.dd_tracer.trace(dd_operation_name, resource=name) def tag_root_span_with_error(self, exception): root_span = self.dd_tracer.current_root_span() diff --git a/edx_django_utils/monitoring/internal/utils.py b/edx_django_utils/monitoring/internal/utils.py index 288fb435..abe7f1f7 100644 --- a/edx_django_utils/monitoring/internal/utils.py +++ b/edx_django_utils/monitoring/internal/utils.py @@ -95,7 +95,7 @@ def record_exception(): @contextmanager -def function_trace(function_name): +def function_trace(function_name, operation_name=None): """ Wraps a chunk of code that we want to appear as a separate, explicit, segment in our monitoring tools. @@ -106,7 +106,7 @@ def function_trace(function_name): # ExitStack handles the underlying context managers. with ExitStack() as stack: for backend in configured_backends(): - context = backend.create_span(function_name) + context = backend.create_span(function_name, operation_name=operation_name) if context is not None: stack.enter_context(context) yield diff --git a/edx_django_utils/monitoring/tests/test_backends.py b/edx_django_utils/monitoring/tests/test_backends.py index feed6209..7d0e955c 100644 --- a/edx_django_utils/monitoring/tests/test_backends.py +++ b/edx_django_utils/monitoring/tests/test_backends.py @@ -8,7 +8,12 @@ from django.test import TestCase, override_settings from edx_django_utils.monitoring import record_exception, set_custom_attribute -from edx_django_utils.monitoring.internal.backends import configured_backends +from edx_django_utils.monitoring.internal.backends import ( + DatadogBackend, + NewRelicBackend, + OpenTelemetryBackend, + configured_backends +) @ddt.ddt @@ -151,3 +156,27 @@ def test_record_exception( mock_nr_notice_error.assert_called_once() mock_otel_record_exception.assert_called_once() mock_dd_span.assert_called_once() + + +class TestCreateSpan(TestCase): + """ + Test create_span's optional operation_name argument per backend. + """ + + @patch('ddtrace._trace.tracer.Tracer.trace') + def test_datadog_without_operation_name(self, mock_dd_trace): + DatadogBackend().create_span('some_name') + mock_dd_trace.assert_called_once_with('some_name', resource='some_name') + + @patch('ddtrace._trace.tracer.Tracer.trace') + def test_datadog_with_operation_name(self, mock_dd_trace): + DatadogBackend().create_span('some_name', operation_name='some_operation') + mock_dd_trace.assert_called_once_with('some_operation', resource='some_name') + + @patch('newrelic.agent.FunctionTrace') + def test_newrelic_with_operation_name(self, mock_nr_function_trace): + NewRelicBackend().create_span('some_name', operation_name='some_operation') + mock_nr_function_trace.assert_called_once_with('some_name') + + def test_opentelemetry_with_operation_name(self): + assert OpenTelemetryBackend().create_span('some_name', operation_name='some_operation') is None