Skip to content

Commit 9342700

Browse files
committed
feat: Add root span tagging for exceptions.
Datadog has issues with tagging the root span with error information. This change adds the functionality to tag the root span on exceptions. This also renames the CachedCustomMonitoringMiddleware into MonitoringSupportMiddleware so that it can be a central place for most monitoring middleware changes instead of adding a new one every time. At some point, CachedCustomMonitoringMiddleware will be removed. edx/edx-arch-experiments#647
1 parent 51b2d4a commit 9342700

File tree

6 files changed

+77
-9
lines changed

6 files changed

+77
-9
lines changed

CHANGELOG.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ Change Log
1111

1212
.. There should always be an "Unreleased" section for changes pending release.
1313
14+
[5.16.0] - 2024-09-27
15+
---------------------
16+
Added
17+
~~~~~
18+
* Added a new method to backends for ``tag_root_span_with_error`` and added Datadog implementation of the functionality.
19+
* Uses the new method to tag the root span when processing exceptions.
20+
21+
Changed
22+
~~~~~~~
23+
* Renamed ``CachedCustomMonitoringMiddleware`` to ``MonitoringSupportMiddleware`` and deprecated the old name. It will be removed in a future release.
24+
25+
1426
[5.15.0] - 2024-07-29
1527
---------------------
1628
Added

edx_django_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
EdX utilities for Django Application development..
33
"""
44

5-
__version__ = "5.15.0"
5+
__version__ = "5.16.0"
66

77
default_app_config = (
88
"edx_django_utils.apps.EdxDjangoUtilsConfig"

edx_django_utils/monitoring/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
CookieMonitoringMiddleware,
1616
DeploymentMonitoringMiddleware,
1717
FrontendMonitoringMiddleware,
18-
MonitoringMemoryMiddleware
18+
MonitoringMemoryMiddleware,
19+
MonitoringSupportMiddleware
1920
)
2021
from .internal.transactions import get_current_transaction, ignore_transaction, set_monitoring_transaction_name
2122
from .internal.utils import (

edx_django_utils/monitoring/internal/backends.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ def create_span(self, name):
6262
or create a new root span if not currently in a span.
6363
"""
6464

65+
@abstractmethod
66+
def tag_root_span_with_error(self, exception):
67+
"""
68+
Tags the root span with the given exception. This is primarily useful for
69+
Datadog as New Relic handles this behavior correctly. Unclear if this is also needs to
70+
be implemented for OTEL.
71+
"""
72+
6573

6674
class NewRelicBackend(TelemetryBackend):
6775
"""
@@ -96,6 +104,10 @@ def create_span(self, name):
96104
nr_transaction = newrelic.agent.current_transaction()
97105
return newrelic.agent.FunctionTrace(nr_transaction, name)
98106

107+
def tag_root_span_with_error(self, exception):
108+
# Does not need to be implemented for NewRelic, because it is handled automatically.
109+
pass
110+
99111

100112
class OpenTelemetryBackend(TelemetryBackend):
101113
"""
@@ -121,6 +133,10 @@ def create_span(self, name):
121133
# Currently, this is not implemented.
122134
pass
123135

136+
def tag_root_span_with_error(self, exception):
137+
# Currently, this is not implemented for OTel
138+
pass
139+
124140

125141
class DatadogBackend(TelemetryBackend):
126142
"""
@@ -145,6 +161,10 @@ def record_exception(self):
145161
def create_span(self, name):
146162
return self.dd_tracer.trace(name)
147163

164+
def tag_root_span_with_error(self, exception):
165+
root_span = self.dd_tracer.current_root_span()
166+
root_span.set_exc_info(type(exception), exception, exception.__traceback__)
167+
148168

149169
# We're using an lru_cache instead of assigning the result to a variable on
150170
# module load. With the default settings (pointing to a TelemetryBackend

edx_django_utils/monitoring/internal/middleware.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,12 @@ def record_python_version():
7070
_set_custom_attribute('python_version', platform.python_version())
7171

7272

73-
class CachedCustomMonitoringMiddleware(MiddlewareMixin):
73+
class MonitoringSupportMiddleware(MiddlewareMixin):
7474
"""
75-
Middleware batch reports cached custom attributes at the end of a request.
75+
Middleware to support monitoring.
76+
77+
1. Middleware batch reports cached custom attributes at the end of a request.
78+
2. Middleware adds error span tags to the root span.
7679
7780
Make sure to add below the request cache in MIDDLEWARE.
7881
@@ -130,6 +133,13 @@ def _batch_report(cls):
130133
for key, value in attributes_cache.data.items():
131134
_set_custom_attribute(key, value)
132135

136+
def _tag_root_span_with_error(self, exception):
137+
"""
138+
Tags the root span with the exception information for all configured backends.
139+
"""
140+
for backend in configured_backends():
141+
backend.tag_root_span_with_error(exception)
142+
133143
# Whether or not there was an exception, report any custom NR attributes that
134144
# may have been collected.
135145

@@ -145,6 +155,16 @@ def process_exception(self, request, exception): # pylint: disable=W0613
145155
Django middleware handler to process an exception
146156
"""
147157
self._batch_report()
158+
self._tag_root_span_with_error(exception)
159+
160+
161+
class CachedCustomMonitoringMiddleware(MonitoringSupportMiddleware):
162+
"""
163+
DEPRECATED: Use MonitoringSupportMiddleware instead.
164+
165+
This is the old name for the MonitoringSupportMiddleware. We are keeping it
166+
around for backwards compatibility until it can be fully removed.
167+
"""
148168

149169

150170
def _set_custom_attribute(key, value):

edx_django_utils/monitoring/tests/test_custom_monitoring.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from unittest.mock import Mock, call, patch
77

88
import ddt
9-
from django.test import TestCase
9+
from django.test import TestCase, override_settings
1010

1111
from edx_django_utils.cache import RequestCache
12-
from edx_django_utils.monitoring import CachedCustomMonitoringMiddleware, accumulate, get_current_transaction, increment
12+
from edx_django_utils.monitoring import MonitoringSupportMiddleware, accumulate, get_current_transaction, increment
1313

1414
from ..middleware import CachedCustomMonitoringMiddleware as DeprecatedCachedCustomMonitoringMiddleware
1515
from ..middleware import MonitoringCustomMetricsMiddleware as DeprecatedMonitoringCustomMetricsMiddleware
@@ -31,8 +31,8 @@ def setUp(self):
3131

3232
@patch('newrelic.agent')
3333
@ddt.data(
34-
(CachedCustomMonitoringMiddleware, False, 'process_response'),
35-
(CachedCustomMonitoringMiddleware, False, 'process_exception'),
34+
(MonitoringSupportMiddleware, False, 'process_response'),
35+
(MonitoringSupportMiddleware, False, 'process_exception'),
3636
(DeprecatedCachedCustomMonitoringMiddleware, True, 'process_response'),
3737
(DeprecatedMonitoringCustomMetricsMiddleware, True, 'process_response'),
3838
)
@@ -76,7 +76,7 @@ def test_accumulate_and_increment(
7676

7777
@patch('newrelic.agent')
7878
@ddt.data(
79-
(CachedCustomMonitoringMiddleware, False),
79+
(MonitoringSupportMiddleware, False),
8080
(DeprecatedCachedCustomMonitoringMiddleware, True),
8181
(DeprecatedMonitoringCustomMetricsMiddleware, True),
8282
)
@@ -113,6 +113,21 @@ def test_accumulate_with_illegal_value(
113113
# Assert call args to newrelic.agent.add_custom_parameter().
114114
mock_newrelic_agent.add_custom_parameter.assert_has_calls(nr_agent_calls_expected, any_order=True)
115115

116+
@patch('ddtrace.Tracer.current_root_span')
117+
def test_error_tagging(self, mock_get_root_span):
118+
# Ensure that we continue to support tagging exceptions in MonitoringSupportMiddleware.
119+
# This is only implemented for DatadogBackend at the moment.
120+
fake_exception = Exception()
121+
mock_root_span = Mock()
122+
mock_get_root_span.return_value = mock_root_span
123+
with override_settings(OPENEDX_TELEMETRY=['edx_django_utils.monitoring.DatadogBackend']):
124+
MonitoringSupportMiddleware(self.mock_response).process_exception(
125+
'fake request', fake_exception
126+
)
127+
mock_root_span.set_exc_info.assert_called_with(
128+
type(fake_exception), fake_exception, fake_exception.__traceback__
129+
)
130+
116131
@patch('newrelic.agent')
117132
def test_get_current_transaction(self, mock_newrelic_agent):
118133
mock_newrelic_agent.current_transaction().name = 'fake-transaction'

0 commit comments

Comments
 (0)