Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion edx_django_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
EdX utilities for Django Application development..
"""

__version__ = "8.0.1"
__version__ = "8.1.0"
23 changes: 17 additions & 6 deletions edx_django_utils/monitoring/internal/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions edx_django_utils/monitoring/internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
31 changes: 30 additions & 1 deletion edx_django_utils/monitoring/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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