diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f5b6c49c..1eb48550 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,12 @@ Change Log .. There should always be an "Unreleased" section for changes pending release. +[5.15.0] - 2024-07-29 +--------------------- +Added +~~~~~ +* Added Datadog implementation of ``function_trace`` and allowed implementation to be configurable. + [5.14.2] - 2024-05-31 --------------------- Fixed diff --git a/edx_django_utils/__init__.py b/edx_django_utils/__init__.py index 825ad372..d2b27a88 100644 --- a/edx_django_utils/__init__.py +++ b/edx_django_utils/__init__.py @@ -2,7 +2,7 @@ EdX utilities for Django Application development.. """ -__version__ = "5.14.2" +__version__ = "5.15.0" default_app_config = ( "edx_django_utils.apps.EdxDjangoUtilsConfig" diff --git a/edx_django_utils/monitoring/README.rst b/edx_django_utils/monitoring/README.rst index 7b01677c..4a882d9c 100644 --- a/edx_django_utils/monitoring/README.rst +++ b/edx_django_utils/monitoring/README.rst @@ -28,7 +28,11 @@ Feature support matrix for built-in telemetry backends: - ✅ (on root span) - ✅ (on current span) - ✅ (on root span) - * - Retrieve and manipulate spans (``function_trace``, ``get_current_transaction``, ``ignore_transaction``, ``set_monitoring_transaction_name``) + * - Create a new span (``function_trace``) + - ✅ + - ❌ + - ✅ + * - Retrieve and manipulate spans (``get_current_transaction``, ``ignore_transaction``, ``set_monitoring_transaction_name``) - ✅ - ❌ - ❌ diff --git a/edx_django_utils/monitoring/__init__.py b/edx_django_utils/monitoring/__init__.py index 696ba167..8b4a34e0 100644 --- a/edx_django_utils/monitoring/__init__.py +++ b/edx_django_utils/monitoring/__init__.py @@ -17,15 +17,11 @@ FrontendMonitoringMiddleware, MonitoringMemoryMiddleware ) -from .internal.transactions import ( - function_trace, - get_current_transaction, - ignore_transaction, - set_monitoring_transaction_name -) +from .internal.transactions import get_current_transaction, ignore_transaction, set_monitoring_transaction_name from .internal.utils import ( accumulate, background_task, + function_trace, increment, record_exception, set_custom_attribute, diff --git a/edx_django_utils/monitoring/internal/backends.py b/edx_django_utils/monitoring/internal/backends.py index c982a3fb..3b184d19 100644 --- a/edx_django_utils/monitoring/internal/backends.py +++ b/edx_django_utils/monitoring/internal/backends.py @@ -50,6 +50,18 @@ def record_exception(self): Record the exception that is currently being handled. """ + @abstractmethod + def create_span(self, name): + """ + Start a tracing span with the given name, returning a context manager instance. + + The caller must use the return value in a `with` statement or similar so that the + span is guaranteed to be closed appropriately. + + Implementations should create a new child span parented to the current span, + or create a new root span if not currently in a span. + """ + class NewRelicBackend(TelemetryBackend): """ @@ -77,6 +89,13 @@ def record_exception(self): # https://docs.newrelic.com/docs/apm/agents/python-agent/python-agent-api/recordexception-python-agent-api/ newrelic.agent.record_exception() + def create_span(self, name): + if newrelic.version_info[0] >= 5: + return newrelic.agent.FunctionTrace(name) + else: + nr_transaction = newrelic.agent.current_transaction() + return newrelic.agent.FunctionTrace(nr_transaction, name) + class OpenTelemetryBackend(TelemetryBackend): """ @@ -98,6 +117,10 @@ 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. + pass + class DatadogBackend(TelemetryBackend): """ @@ -119,6 +142,9 @@ 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) + # We're using an lru_cache instead of assigning the result to a variable on # module load. With the default settings (pointing to a TelemetryBackend diff --git a/edx_django_utils/monitoring/internal/transactions.py b/edx_django_utils/monitoring/internal/transactions.py index 6d712e81..50111da9 100644 --- a/edx_django_utils/monitoring/internal/transactions.py +++ b/edx_django_utils/monitoring/internal/transactions.py @@ -10,9 +10,6 @@ Please remember to expose any new methods in the `__init__.py` file. """ - -from contextlib import contextmanager - try: import newrelic.agent except ImportError: @@ -41,28 +38,6 @@ def ignore_transaction(): newrelic.agent.ignore_transaction() -@contextmanager -def function_trace(function_name): - """ - Wraps a chunk of code that we want to appear as a separate, explicit, - segment in our monitoring tools. - """ - # Not covering this because if we mock it, we're not really testing anything - # anyway. If something did break, it should show up in tests for apps that - # use this code with newrelic enabled, on whatever version of newrelic they - # run. - if newrelic: # pragma: no cover - if newrelic.version_info[0] >= 5: - with newrelic.agent.FunctionTrace(function_name): - yield - else: - nr_transaction = newrelic.agent.current_transaction() - with newrelic.agent.FunctionTrace(nr_transaction, function_name): - yield - else: - yield - - class MonitoringTransaction(): """ Represents a monitoring transaction (likely the current transaction). diff --git a/edx_django_utils/monitoring/internal/utils.py b/edx_django_utils/monitoring/internal/utils.py index 2dc2cfb1..649a95a6 100644 --- a/edx_django_utils/monitoring/internal/utils.py +++ b/edx_django_utils/monitoring/internal/utils.py @@ -17,6 +17,8 @@ At this time, the custom monitoring will only be reported to New Relic. """ +from contextlib import ExitStack, contextmanager + from .backends import configured_backends from .middleware import CachedCustomMonitoringMiddleware @@ -92,6 +94,24 @@ def record_exception(): backend.record_exception() +@contextmanager +def function_trace(function_name): + """ + Wraps a chunk of code that we want to appear as a separate, explicit, + segment in our monitoring tools. + """ + # Not covering this because if we mock it, we're not really testing anything + # anyway. If something did break, it should show up in tests for apps that + # use this code with whatever uses it. + # ExitStack handles the underlying context managers. + with ExitStack() as stack: + for backend in configured_backends(): + context = backend.create_span(function_name) + if context is not None: + stack.enter_context(context) + yield + + def background_task(*args, **kwargs): """ Handles monitoring for background tasks that are not passed in through the web server like