Skip to content

Commit

Permalink
feat: Add ability to create manual spans for datadog.
Browse files Browse the repository at this point in the history
  • Loading branch information
dianakhuang committed Jul 30, 2024
1 parent 0833182 commit 5e3e623
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 33 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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,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"
Expand Down
6 changes: 5 additions & 1 deletion edx_django_utils/monitoring/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``)
- ✅
- ❌
- ❌
Expand Down
8 changes: 2 additions & 6 deletions edx_django_utils/monitoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions edx_django_utils/monitoring/internal/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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
Expand Down
25 changes: 0 additions & 25 deletions edx_django_utils/monitoring/internal/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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).
Expand Down
20 changes: 20 additions & 0 deletions edx_django_utils/monitoring/internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 5e3e623

Please sign in to comment.