diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b0744c27..bb3896f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,11 @@ Change Log .. There should always be an "Unreleased" section for changes pending release. +[5.13.0] - 2024-04-30 +--------------------- +Added +~~~~~ +* Initial support for sending monitoring data to OpenTelemetry collector or Datadog agent, configured by new Django setting ``OPENEDX_TELEMETRY``. See monitoring README for details. [5.12.0] - 2024-03-29 --------------------- diff --git a/edx_django_utils/__init__.py b/edx_django_utils/__init__.py index dba7a3da..449b8ce7 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.12.0" +__version__ = "5.13.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 a1ba59bd..14d0937a 100644 --- a/edx_django_utils/monitoring/README.rst +++ b/edx_django_utils/monitoring/README.rst @@ -7,6 +7,40 @@ See ``__init__.py`` for a list of everything included in the public API. If, for some reason, you need low level access to the newrelic agent, please extend this library to implement the feature that you want. Applications should never include ``import newrelic.agent`` directly. +Choice of monitoring tools +-------------------------- + +The most complete feature support is for New Relic (the default), but there is also initial support for OpenTelemetry and Datadog. + +The Django setting ``OPENEDX_TELEMETRY`` can be set to a list of implementations, e.g. ``['edx_django_utils.monitoring.NewRelicBackend', 'edx_django_utils.monitoring.OpenTelemetryBackend']``. All of the implementations that can be loaded will be used for all applicable telemetry calls. + +Feature support matrix for built-in telemetry backends: + +.. list-table:: + :header-rows: 1 + :widths: 55, 15, 15, 15 + + * - + - New Relic + - OpenTelemetry + - Datadog + * - Custom span attributes (``set_custom_attribute``, ``accumulate``, ``increment``, etc.) + - ✅ (on root span) + - ✅ (on current span) + - ✅ (on root span) + * - Retrieve and manipulate spans (``function_trace``, ``get_current_transaction``, ``ignore_transaction``, ``set_monitoring_transaction_name``) + - ✅ + - ❌ + - ❌ + * - Record exceptions (``record_exception``) + - ✅ + - ✅ + - ✅ + * - Instrument non-web tasks (``background_task``) + - ✅ + - ❌ + - ❌ + Using Custom Attributes ----------------------- diff --git a/edx_django_utils/monitoring/__init__.py b/edx_django_utils/monitoring/__init__.py index 70dcbf4a..431acc2e 100644 --- a/edx_django_utils/monitoring/__init__.py +++ b/edx_django_utils/monitoring/__init__.py @@ -3,6 +3,7 @@ See README.rst for details. """ +from .internal.backends import DatadogBackend, NewRelicBackend, OpenTelemetryBackend, TelemetryBackend from .internal.code_owner.middleware import CodeOwnerMonitoringMiddleware from .internal.code_owner.utils import ( get_code_owner_from_module, diff --git a/edx_django_utils/monitoring/internal/backends.py b/edx_django_utils/monitoring/internal/backends.py new file mode 100644 index 00000000..a6190156 --- /dev/null +++ b/edx_django_utils/monitoring/internal/backends.py @@ -0,0 +1,180 @@ +""" +Telemetry abstraction and backends that implement it. + +Only a certain subset of the monitoring functions have been made +configurable via this module. +""" + +import logging +import sys +from abc import ABC, abstractmethod +from functools import lru_cache + +from django.conf import settings +from django.dispatch import receiver +from django.test.signals import setting_changed +from django.utils.module_loading import import_string + +log = logging.getLogger(__name__) + +# The newrelic package used to not be part of the requirements files +# and so a try-import was used here. This situation is no longer true, +# but we're still preserving that pattern until someone feels like +# doing the work to remove it. (Should just be a major version bump +# and communication to anyone who might be specifically removing the +# package for some reason.) +# +# Ticket for just doing an unconditional import: +# https://github.com/openedx/edx-django-utils/issues/396 +try: + import newrelic.agent +except ImportError: # pragma: no cover + newrelic = None # pylint: disable=invalid-name + + +class TelemetryBackend(ABC): + """ + Base class for telemetry sinks. + """ + @abstractmethod + def set_attribute(self, key, value): + """ + Set a key-value attribute on a span. This might be the current + span or it might the root span of the process, depending on + the backend. + """ + + @abstractmethod + def record_exception(self): + """ + Record the exception that is currently being handled. + """ + + +class NewRelicBackend(TelemetryBackend): + """ + Send telemetry to New Relic. + + https://docs.newrelic.com/docs/apm/agents/python-agent/python-agent-api/guide-using-python-agent-api/ + """ + def __init__(self): + if newrelic is None: + raise Exception("Could not load New Relic monitoring backend; package not present.") + + def set_attribute(self, key, value): + # Sets attribute on the transaction, rather than the current + # span, matching historical behavior. There is also an + # `add_custom_span_attribute` that would better match + # OpenTelemetry's behavior, which we could try exposing + # through a new, more specific TelemetryBackend method. + # + # TODO: Update to newer name `add_custom_attribute` + # https://docs.newrelic.com/docs/apm/agents/python-agent/python-agent-api/addcustomparameter-python-agent-api/ + newrelic.agent.add_custom_parameter(key, value) + + def record_exception(self): + # TODO: Replace with newrelic.agent.notice_error() + # https://docs.newrelic.com/docs/apm/agents/python-agent/python-agent-api/recordexception-python-agent-api/ + newrelic.agent.record_exception() + + +class OpenTelemetryBackend(TelemetryBackend): + """ + Send telemetry via OpenTelemetry. + + Requirements to use: + + - Install `opentelemetry-api` Python package + - Configure and initialize OpenTelemetry + + API reference: https://opentelemetry-python.readthedocs.io/en/latest/ + """ + # pylint: disable=import-outside-toplevel + def __init__(self): + # If import fails, the backend won't be used. + from opentelemetry import trace + self.otel_trace = trace + + def set_attribute(self, key, value): + # Sets the value on the current span, not necessarily the root + # span in the process. + self.otel_trace.get_current_span().set_attribute(key, value) + + def record_exception(self): + self.otel_trace.get_current_span().record_exception(sys.exc_info()[1]) + + +class DatadogBackend(TelemetryBackend): + """ + Send telemetry to Datadog via ddtrace. + + Requirements to use: + + - Install `ddtrace` Python package + - Initialize ddtrace, either via ddtrace-run or ddtrace.auto + + API reference: https://ddtrace.readthedocs.io/en/stable/api.html + """ + # pylint: disable=import-outside-toplevel + def __init__(self): + # If import fails, the backend won't be used. + from ddtrace import tracer + self.dd_tracer = tracer + + def set_attribute(self, key, value): + if root_span := self.dd_tracer.current_root_span(): + root_span.set_tag(key, value) + + def record_exception(self): + if span := self.dd_tracer.current_span(): + span.set_traceback() + + +# 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 +# in this very module), this function can't be successfully called until +# the module finishes loading, otherwise we get a circular import error +# that will cause the backend to be dropped from the list. +@lru_cache +def configured_backends(): + """ + Produce a list of TelemetryBackend instances from Django settings. + """ + # .. setting_name: OPENEDX_TELEMETRY + # .. setting_default: ['edx_django_utils.monitoring.NewRelicBackend'] + # .. setting_description: List of telemetry backends to send data to. Allowable values + # are dotted module paths to classes implementing `edx_django_utils.monitoring.TelemetryBackend`, + # such as the built-in `NewRelicBackend`, `OpenTelemetryBackend`, and `DatadogBackend` + # (in the same module). For historical reasons, this defaults to just + # New Relic, and not all monitoring features will report to all backends (New Relic + # having the broadest support). Unusable options are ignored. Configuration + # of the backends themselves is via environment variables and system config files + # rather than via Django settings. + backend_classes = getattr(settings, 'OPENEDX_TELEMETRY', None) + if isinstance(backend_classes, str): + # Prevent a certain kind of easy mistake. + raise Exception("OPENEDX_TELEMETRY must be a list, not a string.") + if backend_classes is None: + backend_classes = ['edx_django_utils.monitoring.NewRelicBackend'] + + backends = [] + for backend_class in backend_classes: + try: + cls = import_string(backend_class) + if issubclass(cls, TelemetryBackend): + backends.append(cls()) + else: + log.warning( + f"Could not load OPENEDX_TELEMETRY option {backend_class!r}: " + f"{cls} is not a subclass of TelemetryBackend" + ) + except BaseException as e: + log.warning(f"Could not load OPENEDX_TELEMETRY option {backend_class!r}: {e!r}") + + return backends + + +@receiver(setting_changed) +def _reset_state(sender, **kwargs): # pylint: disable=unused-argument + """Reset caches when settings change during unit tests.""" + configured_backends.cache_clear() diff --git a/edx_django_utils/monitoring/internal/middleware.py b/edx_django_utils/monitoring/internal/middleware.py index 2e2fd88d..fb517efd 100644 --- a/edx_django_utils/monitoring/internal/middleware.py +++ b/edx_django_utils/monitoring/internal/middleware.py @@ -1,8 +1,5 @@ """ Middleware for monitoring. - -At this time, monitoring details can only be reported to New Relic. - """ import base64 import hashlib @@ -23,12 +20,9 @@ from edx_django_utils.cache import RequestCache from edx_django_utils.logging import encrypt_for_log +from .backends import configured_backends + log = logging.getLogger(__name__) -try: - import newrelic.agent -except ImportError: # pragma: no cover - log.warning("Unable to load NewRelic agent module") - newrelic = None # pylint: disable=invalid-name _DEFAULT_NAMESPACE = 'edx_django_utils.monitoring' @@ -77,17 +71,14 @@ class CachedCustomMonitoringMiddleware(MiddlewareMixin): Make sure to add below the request cache in MIDDLEWARE. - This middleware will only call on the newrelic agent if there are any attributes + This middleware will only call on the telemetry collector if there are any attributes to report for this request, so it will not incur any processing overhead for request handlers which do not record custom attributes. - - Note: New Relic adds custom attributes to events, which is what is being used here. - """ @classmethod def _get_attributes_cache(cls): """ - Get a request cache specifically for New Relic custom attributes. + Get a request cache specifically for custom attributes. """ return RequestCache(namespace=_REQUEST_CACHE_NAMESPACE) @@ -126,9 +117,9 @@ def accumulate_metric(cls, name, value): # pragma: no cover @classmethod def _batch_report(cls): """ - Report the collected custom attributes to New Relic. + Report the collected custom attributes. """ - if not newrelic: # pragma: no cover + if not configured_backends(): # pragma: no cover return attributes_cache = cls._get_attributes_cache() for key, value in attributes_cache.data.items(): @@ -157,8 +148,8 @@ def _set_custom_attribute(key, value): Note: Can't use public method in ``utils.py`` due to circular reference. """ - if newrelic: # pragma: no cover - newrelic.agent.add_custom_parameter(key, value) + for backend in configured_backends(): + backend.set_attribute(key, value) class MonitoringMemoryMiddleware(MiddlewareMixin): @@ -499,7 +490,7 @@ def split_ascii_log_message(msg, chunk_size): yield msg # no need for continuation messages else: # Generate a unique-enough collation ID for this message. - h = hashlib.shake_128(msg.encode()).digest(6) # pylint/#4039 pylint: disable=too-many-function-args + h = hashlib.shake_128(msg.encode()).digest(6) group_id = base64.b64encode(h).decode().rstrip('=') for i in range(chunk_count): diff --git a/edx_django_utils/monitoring/internal/utils.py b/edx_django_utils/monitoring/internal/utils.py index 43a6e347..2dc2cfb1 100644 --- a/edx_django_utils/monitoring/internal/utils.py +++ b/edx_django_utils/monitoring/internal/utils.py @@ -17,6 +17,7 @@ At this time, the custom monitoring will only be reported to New Relic. """ +from .backends import configured_backends from .middleware import CachedCustomMonitoringMiddleware try: @@ -62,40 +63,33 @@ def set_custom_attributes_for_course_key(course_key): """ Set monitoring custom attributes related to a course key. - This is not cached, and only support reporting to New Relic Insights. + This is not cached. """ - if newrelic: # pragma: no cover - newrelic.agent.add_custom_parameter('course_id', str(course_key)) - newrelic.agent.add_custom_parameter('org', str(course_key.org)) + set_custom_attribute('course_id', str(course_key)) + set_custom_attribute('org', str(course_key.org)) def set_custom_attribute(key, value): """ Set monitoring custom attribute. - This is not cached, and only support reporting to New Relic Insights. - + This is not cached. """ - if newrelic: # pragma: no cover - # note: parameter is new relic's older name for attributes - newrelic.agent.add_custom_parameter(key, value) + for backend in configured_backends(): + backend.set_attribute(key, value) def record_exception(): """ - Records a caught exception to the monitoring system. + Record a caught exception to the monitoring system. Note: By default, only unhandled exceptions are monitored. This function can be called to record exceptions as monitored errors, even if you handle the exception gracefully from a user perspective. - - For more details, see: - https://docs.newrelic.com/docs/agents/python-agent/python-agent-api/recordexception-python-agent-api - """ - if newrelic: # pragma: no cover - newrelic.agent.record_exception() + for backend in configured_backends(): + backend.record_exception() def background_task(*args, **kwargs): @@ -103,8 +97,10 @@ def background_task(*args, **kwargs): Handles monitoring for background tasks that are not passed in through the web server like celery and event consuming tasks. + This function only supports New Relic. + For more details, see: - https://docs.newrelic.com/docs/apm/agents/python-agent/supported-features/monitor-non-web-scripts-worker-processes-tasks-functions + https://docs.newrelic.com/docs/apm/agents/python-agent/python-agent-api/backgroundtask-python-agent-api/ """ def noop_decorator(func): diff --git a/edx_django_utils/monitoring/tests/test_backends.py b/edx_django_utils/monitoring/tests/test_backends.py new file mode 100644 index 00000000..29b65d3c --- /dev/null +++ b/edx_django_utils/monitoring/tests/test_backends.py @@ -0,0 +1,153 @@ +""" +Tests for TelemetryBackend and implementations. +""" +from unittest.mock import patch + +import ddt +import pytest +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 + + +@ddt.ddt +class TestBackendsConfig(TestCase): + """ + Test configuration of the backends list. + """ + + def _get_configured_classnames(self): + return [b.__class__.__name__ for b in configured_backends()] + + @ddt.data( + # Default + (None, ['NewRelicBackend']), + # Empty list removes all + ([], []), + # New Relic not required + ( + ['edx_django_utils.monitoring.OpenTelemetryBackend'], + ['OpenTelemetryBackend'] + ), + # All known classes + ( + [ + 'edx_django_utils.monitoring.NewRelicBackend', + 'edx_django_utils.monitoring.OpenTelemetryBackend', + 'edx_django_utils.monitoring.DatadogBackend', + ], + ['NewRelicBackend', 'OpenTelemetryBackend', 'DatadogBackend'], + ), + ) + @ddt.unpack + def test_configured_backends(self, setting, expected_classnames): + """ + Test that backends are loaded as expected. + """ + with override_settings(OPENEDX_TELEMETRY=setting): + backend_classnames = self._get_configured_classnames() + assert sorted(backend_classnames) == sorted(expected_classnames) + + def test_type(self): + """ + Test that we detect the misuse of a string instead of a list. + """ + with override_settings(OPENEDX_TELEMETRY='edx_django_utils.monitoring.NewRelicBackend'): + with pytest.raises(Exception, match='must be a list, not a string'): + self._get_configured_classnames() + + @patch('edx_django_utils.monitoring.internal.backends.log') + def test_import_failure(self, mock_log): + """ + Test that backends that can't be imported are ignored, with warning. + """ + with override_settings(OPENEDX_TELEMETRY=[ + 'nonsense', + 'edx_django_utils.monitoring.OpenTelemetryBackend', + ]): + assert self._get_configured_classnames() == ['OpenTelemetryBackend'] + mock_log.warning.assert_called_once_with( + "Could not load OPENEDX_TELEMETRY option 'nonsense': " + """ImportError("nonsense doesn't look like a module path")""" + ) + + @patch('edx_django_utils.monitoring.internal.backends.log') + def test_wrong_class(self, mock_log): + """ + Test that backend classes of an unexpected ancestor are ignored, with warning. + """ + with override_settings(OPENEDX_TELEMETRY=[ + 'builtins.dict', + 'edx_django_utils.monitoring.OpenTelemetryBackend', + ]): + assert self._get_configured_classnames() == ['OpenTelemetryBackend'] + mock_log.warning.assert_called_once_with( + "Could not load OPENEDX_TELEMETRY option 'builtins.dict': " + " is not a subclass of TelemetryBackend" + ) + + @patch('edx_django_utils.monitoring.internal.backends.log') + @patch('edx_django_utils.monitoring.internal.backends.newrelic', None) + def test_newrelic_package(self, mock_log): + """ + Test that New Relic backend is skipped if package not present. + """ + with override_settings(OPENEDX_TELEMETRY=['edx_django_utils.monitoring.NewRelicBackend']): + assert self._get_configured_classnames() == [] + mock_log.warning.assert_called_once_with( + "Could not load OPENEDX_TELEMETRY option 'edx_django_utils.monitoring.NewRelicBackend': " + "Exception('Could not load New Relic monitoring backend; package not present.')" + ) + + def test_default_config(self): + """ + We need to keep the same unconfigured default for now. + """ + assert [b.__class__.__name__ for b in configured_backends()] == ['NewRelicBackend'] + + +class TestBackendsFanOut(TestCase): + """ + Test that certain utility functions fan out to the backends. + """ + + @patch('newrelic.agent.add_custom_parameter') + @patch('opentelemetry.trace.span.NonRecordingSpan.set_attribute') + # Patch out the span-getter, not the set_attribute call, because + # it doesn't give us a span unless one is active. And I didn't + # feel like setting that up. + # + # This does at least assert that we're getting the *root* span for DD. + @patch('ddtrace._trace.tracer.Tracer.current_root_span') + def test_set_custom_attribute( + self, mock_dd_root_span, + mock_otel_set_attribute, mock_nr_add_custom_parameter, + ): + with override_settings(OPENEDX_TELEMETRY=[ + 'edx_django_utils.monitoring.NewRelicBackend', + 'edx_django_utils.monitoring.OpenTelemetryBackend', + 'edx_django_utils.monitoring.DatadogBackend', + ]): + set_custom_attribute('some_key', 'some_value') + mock_nr_add_custom_parameter.assert_called_once_with('some_key', 'some_value') + mock_otel_set_attribute.assert_called_once() + mock_dd_root_span.assert_called_once() + + @patch('newrelic.agent.record_exception') + @patch('opentelemetry.trace.span.NonRecordingSpan.record_exception') + # Record exception on current span, not root span. + @patch('ddtrace._trace.tracer.Tracer.current_span') + def test_record_exception( + self, mock_dd_span, + mock_otel_record_exception, mock_nr_record_exception, + ): + with override_settings(OPENEDX_TELEMETRY=[ + 'edx_django_utils.monitoring.NewRelicBackend', + 'edx_django_utils.monitoring.OpenTelemetryBackend', + 'edx_django_utils.monitoring.DatadogBackend', + ]): + record_exception() + mock_nr_record_exception.assert_called_once() + mock_otel_record_exception.assert_called_once() + mock_dd_span.assert_called_once() diff --git a/edx_django_utils/monitoring/tests/test_custom_monitoring.py b/edx_django_utils/monitoring/tests/test_custom_monitoring.py index b0bb1954..59a4a12c 100644 --- a/edx_django_utils/monitoring/tests/test_custom_monitoring.py +++ b/edx_django_utils/monitoring/tests/test_custom_monitoring.py @@ -9,13 +9,7 @@ from django.test import TestCase from edx_django_utils.cache import RequestCache -from edx_django_utils.monitoring import ( - CachedCustomMonitoringMiddleware, - accumulate, - get_current_transaction, - increment, - record_exception -) +from edx_django_utils.monitoring import CachedCustomMonitoringMiddleware, accumulate, get_current_transaction, increment from ..middleware import CachedCustomMonitoringMiddleware as DeprecatedCachedCustomMonitoringMiddleware from ..middleware import MonitoringCustomMetricsMiddleware as DeprecatedMonitoringCustomMetricsMiddleware @@ -148,8 +142,3 @@ def test_deprecated_set_custom_attribute(self, mock_set_custom_attribute): def test_deprecated_set_custom_attributes_for_course_key(self, mock_set_custom_attributes_for_course_key): deprecated_set_custom_attributes_for_course_key('key') mock_set_custom_attributes_for_course_key.assert_called_with('key') - - @patch('newrelic.agent.record_exception') - def test_record_exception(self, mock_record_exception): - record_exception() - mock_record_exception.assert_called_once() diff --git a/requirements/base.txt b/requirements/base.txt index d629c98b..e20f7453 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,7 +24,7 @@ django-crum==0.7.9 # via -r requirements/base.in django-waffle==4.1.0 # via -r requirements/base.in -newrelic==9.7.1 +newrelic==9.8.0 # via -r requirements/base.in pbr==6.0.0 # via stevedore diff --git a/requirements/ci.txt b/requirements/ci.txt index c9dd7491..c085cbcf 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -12,7 +12,7 @@ colorama==0.4.6 # via tox distlib==0.3.8 # via virtualenv -filelock==3.13.1 +filelock==3.13.3 # via # tox # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index 8de51785..bcbf3ab6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -15,19 +15,32 @@ astroid==3.1.0 # -r requirements/quality.txt # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/quality.txt + # cattrs + # ddtrace backports-zoneinfo==0.2.1 ; python_version < "3.9" # via # -c requirements/constraints.txt # -r requirements/quality.txt # django -build==1.1.1 +build==1.2.1 # via # -r requirements/pip-tools.txt # pip-tools +bytecode==0.15.1 + # via + # -r requirements/quality.txt + # ddtrace cachetools==5.3.3 # via # -r requirements/ci.txt # tox +cattrs==23.2.3 + # via + # -r requirements/quality.txt + # ddtrace cffi==1.16.0 # via # -r requirements/quality.txt @@ -61,8 +74,18 @@ coverage[toml]==7.4.4 # via # -r requirements/quality.txt # pytest-cov +ddsketch==2.0.4 + # via + # -r requirements/quality.txt + # ddtrace ddt==1.7.2 # via -r requirements/quality.txt +ddtrace==2.7.5 + # via -r requirements/quality.txt +deprecated==1.2.14 + # via + # -r requirements/quality.txt + # opentelemetry-api diff-cover==6.2.1 # via # -c requirements/constraints.txt @@ -90,17 +113,22 @@ edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.6 # via -r requirements/quality.txt +envier==0.5.1 + # via + # -r requirements/quality.txt + # ddtrace exceptiongroup==1.2.0 # via # -r requirements/quality.txt + # cattrs # pytest factory-boy==3.3.0 # via -r requirements/quality.txt -faker==24.3.0 +faker==24.4.0 # via # -r requirements/quality.txt # factory-boy -filelock==3.13.1 +filelock==3.13.3 # via # -r requirements/ci.txt # tox @@ -109,7 +137,9 @@ importlib-metadata==6.11.0 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/pip-tools.txt + # -r requirements/quality.txt # build + # opentelemetry-api inflect==7.0.0 # via jinja2-pluralize iniconfig==2.0.0 @@ -128,7 +158,7 @@ jinja2==3.1.3 # jinja2-pluralize jinja2-pluralize==0.3.0 # via diff-cover -lxml==5.1.0 +lxml==5.1.1 # via edx-i18n-tools markupsafe==2.1.5 # via @@ -140,8 +170,12 @@ mccabe==0.7.0 # pylint mock==5.1.0 # via -r requirements/quality.txt -newrelic==9.7.1 +newrelic==9.8.0 # via -r requirements/quality.txt +opentelemetry-api==1.24.0 + # via + # -r requirements/quality.txt + # ddtrace packaging==24.0 # via # -r requirements/ci.txt @@ -175,6 +209,11 @@ pluggy==1.4.0 # tox polib==1.2.0 # via edx-i18n-tools +protobuf==5.26.1 + # via + # -r requirements/quality.txt + # ddsketch + # ddtrace psutil==5.9.8 # via -r requirements/quality.txt pycodestyle==2.11.1 @@ -248,6 +287,8 @@ pyyaml==6.0.1 six==1.16.0 # via # -r requirements/quality.txt + # ddsketch + # ddtrace # edx-lint # python-dateutil snowballstemmer==2.2.0 @@ -257,6 +298,7 @@ snowballstemmer==2.2.0 sqlparse==0.4.4 # via # -r requirements/quality.txt + # ddtrace # django stevedore==5.2.0 # via @@ -291,6 +333,9 @@ typing-extensions==4.10.0 # annotated-types # asgiref # astroid + # bytecode + # cattrs + # ddtrace # faker # inflect # pydantic @@ -304,9 +349,18 @@ wheel==0.43.0 # via # -r requirements/pip-tools.txt # pip-tools +wrapt==1.16.0 + # via + # -r requirements/quality.txt + # deprecated +xmltodict==0.13.0 + # via + # -r requirements/quality.txt + # ddtrace zipp==3.18.1 # via # -r requirements/pip-tools.txt + # -r requirements/quality.txt # importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/doc.txt b/requirements/doc.txt index a91ba7df..cfced93b 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,6 +12,11 @@ asgiref==3.8.1 # via # -r requirements/test.txt # django +attrs==23.2.0 + # via + # -r requirements/test.txt + # cattrs + # ddtrace babel==2.14.0 # via # pydata-sphinx-theme @@ -23,6 +28,14 @@ backports-zoneinfo==0.2.1 ; python_version < "3.9" # django beautifulsoup4==4.12.3 # via pydata-sphinx-theme +bytecode==0.15.1 + # via + # -r requirements/test.txt + # ddtrace +cattrs==23.2.3 + # via + # -r requirements/test.txt + # ddtrace certifi==2024.2.2 # via requests cffi==1.16.0 @@ -40,8 +53,18 @@ coverage[toml]==7.4.4 # pytest-cov cryptography==42.0.5 # via secretstorage +ddsketch==2.0.4 + # via + # -r requirements/test.txt + # ddtrace ddt==1.7.2 # via -r requirements/test.txt +ddtrace==2.7.5 + # via -r requirements/test.txt +deprecated==1.2.14 + # via + # -r requirements/test.txt + # opentelemetry-api django==4.2.11 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt @@ -63,15 +86,20 @@ docutils==0.17.1 # readme-renderer # restructuredtext-lint # sphinx +envier==0.5.1 + # via + # -r requirements/test.txt + # ddtrace exceptiongroup==1.2.0 # via # -r requirements/test.txt + # cattrs # pytest factory-boy==3.3.0 # via # -r requirements/doc.in # -r requirements/test.txt -faker==24.3.0 +faker==24.4.0 # via # -r requirements/test.txt # factory-boy @@ -82,7 +110,9 @@ imagesize==1.4.1 importlib-metadata==6.11.0 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/test.txt # keyring + # opentelemetry-api # twine importlib-resources==6.4.0 # via keyring @@ -116,10 +146,14 @@ more-itertools==10.2.0 # via # jaraco-classes # jaraco-functools -newrelic==9.7.1 +newrelic==9.8.0 # via -r requirements/test.txt -nh3==0.2.15 +nh3==0.2.17 # via readme-renderer +opentelemetry-api==1.24.0 + # via + # -r requirements/test.txt + # ddtrace packaging==24.0 # via # -r requirements/test.txt @@ -136,6 +170,11 @@ pluggy==1.4.0 # via # -r requirements/test.txt # pytest +protobuf==5.26.1 + # via + # -r requirements/test.txt + # ddsketch + # ddtrace psutil==5.9.8 # via -r requirements/test.txt pycparser==2.21 @@ -192,6 +231,8 @@ secretstorage==3.3.3 six==1.16.0 # via # -r requirements/test.txt + # ddsketch + # ddtrace # python-dateutil snowballstemmer==2.2.0 # via sphinx @@ -220,6 +261,7 @@ sphinxcontrib-serializinghtml==1.1.5 sqlparse==0.4.4 # via # -r requirements/test.txt + # ddtrace # django stevedore==5.2.0 # via @@ -236,6 +278,9 @@ typing-extensions==4.10.0 # via # -r requirements/test.txt # asgiref + # bytecode + # cattrs + # ddtrace # faker # pydata-sphinx-theme # rich @@ -243,10 +288,19 @@ urllib3==2.2.1 # via # requests # twine -wheel==0.42.0 +wheel==0.43.0 # via -r requirements/doc.in +wrapt==1.16.0 + # via + # -r requirements/test.txt + # deprecated +xmltodict==0.13.0 + # via + # -r requirements/test.txt + # ddtrace zipp==3.18.1 # via + # -r requirements/test.txt # importlib-metadata # importlib-resources diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index aad9d382..748bf44e 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -4,7 +4,7 @@ # # make upgrade # -build==1.1.1 +build==1.2.1 # via pip-tools click==8.1.7 # via pip-tools diff --git a/requirements/quality.txt b/requirements/quality.txt index 18343f9a..16e952c3 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -12,11 +12,24 @@ astroid==3.1.0 # via # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/test.txt + # cattrs + # ddtrace backports-zoneinfo==0.2.1 ; python_version < "3.9" # via # -c requirements/constraints.txt # -r requirements/test.txt # django +bytecode==0.15.1 + # via + # -r requirements/test.txt + # ddtrace +cattrs==23.2.3 + # via + # -r requirements/test.txt + # ddtrace cffi==1.16.0 # via # -r requirements/test.txt @@ -35,8 +48,18 @@ coverage[toml]==7.4.4 # via # -r requirements/test.txt # pytest-cov +ddsketch==2.0.4 + # via + # -r requirements/test.txt + # ddtrace ddt==1.7.2 # via -r requirements/test.txt +ddtrace==2.7.5 + # via -r requirements/test.txt +deprecated==1.2.14 + # via + # -r requirements/test.txt + # opentelemetry-api dill==0.3.8 # via pylint django==4.2.11 @@ -51,16 +74,26 @@ django-waffle==4.1.0 # via -r requirements/test.txt edx-lint==5.3.6 # via -r requirements/quality.in +envier==0.5.1 + # via + # -r requirements/test.txt + # ddtrace exceptiongroup==1.2.0 # via # -r requirements/test.txt + # cattrs # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==24.3.0 +faker==24.4.0 # via # -r requirements/test.txt # factory-boy +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/test.txt + # opentelemetry-api iniconfig==2.0.0 # via # -r requirements/test.txt @@ -77,8 +110,12 @@ mccabe==0.7.0 # via pylint mock==5.1.0 # via -r requirements/test.txt -newrelic==9.7.1 +newrelic==9.8.0 # via -r requirements/test.txt +opentelemetry-api==1.24.0 + # via + # -r requirements/test.txt + # ddtrace packaging==24.0 # via # -r requirements/test.txt @@ -93,6 +130,11 @@ pluggy==1.4.0 # via # -r requirements/test.txt # pytest +protobuf==5.26.1 + # via + # -r requirements/test.txt + # ddsketch + # ddtrace psutil==5.9.8 # via -r requirements/test.txt pycodestyle==2.11.1 @@ -139,6 +181,8 @@ pyyaml==6.0.1 six==1.16.0 # via # -r requirements/test.txt + # ddsketch + # ddtrace # edx-lint # python-dateutil snowballstemmer==2.2.0 @@ -146,6 +190,7 @@ snowballstemmer==2.2.0 sqlparse==0.4.4 # via # -r requirements/test.txt + # ddtrace # django stevedore==5.2.0 # via @@ -166,5 +211,20 @@ typing-extensions==4.10.0 # -r requirements/test.txt # asgiref # astroid + # bytecode + # cattrs + # ddtrace # faker # pylint +wrapt==1.16.0 + # via + # -r requirements/test.txt + # deprecated +xmltodict==0.13.0 + # via + # -r requirements/test.txt + # ddtrace +zipp==3.18.1 + # via + # -r requirements/test.txt + # importlib-metadata diff --git a/requirements/test.in b/requirements/test.in index 60e3b0eb..45500971 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -8,3 +8,6 @@ factory_boy # Test factory framework mock # Backport of unittest.mock, available in Python 3.3 pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support +newrelic # Required for testing NewRelicBackend +opentelemetry-api # Required for testing OpenTelemetryBackend +ddtrace # Required for testing DatadogBackend diff --git a/requirements/test.txt b/requirements/test.txt index e3a33968..2a0a0338 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,11 +8,19 @@ asgiref==3.8.1 # via # -r requirements/base.txt # django +attrs==23.2.0 + # via + # cattrs + # ddtrace backports-zoneinfo==0.2.1 ; python_version < "3.9" # via # -c requirements/constraints.txt # -r requirements/base.txt # django +bytecode==0.15.1 + # via ddtrace +cattrs==23.2.3 + # via ddtrace cffi==1.16.0 # via # -r requirements/base.txt @@ -21,8 +29,14 @@ click==8.1.7 # via -r requirements/base.txt coverage[toml]==7.4.4 # via pytest-cov +ddsketch==2.0.4 + # via ddtrace ddt==1.7.2 # via -r requirements/test.in +ddtrace==2.7.5 + # via -r requirements/test.in +deprecated==1.2.14 + # via opentelemetry-api # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt @@ -32,18 +46,32 @@ django-crum==0.7.9 # via -r requirements/base.txt django-waffle==4.1.0 # via -r requirements/base.txt +envier==0.5.1 + # via ddtrace exceptiongroup==1.2.0 - # via pytest + # via + # cattrs + # pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==24.3.0 +faker==24.4.0 # via factory-boy +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # opentelemetry-api iniconfig==2.0.0 # via pytest mock==5.1.0 # via -r requirements/test.in -newrelic==9.7.1 - # via -r requirements/base.txt +newrelic==9.8.0 + # via + # -r requirements/base.txt + # -r requirements/test.in +opentelemetry-api==1.24.0 + # via + # -r requirements/test.in + # ddtrace packaging==24.0 # via pytest pbr==6.0.0 @@ -52,6 +80,10 @@ pbr==6.0.0 # stevedore pluggy==1.4.0 # via pytest +protobuf==5.26.1 + # via + # ddsketch + # ddtrace psutil==5.9.8 # via -r requirements/base.txt pycparser==2.21 @@ -71,10 +103,14 @@ pytest-django==4.8.0 python-dateutil==2.9.0.post0 # via faker six==1.16.0 - # via python-dateutil + # via + # ddsketch + # ddtrace + # python-dateutil sqlparse==0.4.4 # via # -r requirements/base.txt + # ddtrace # django stevedore==5.2.0 # via -r requirements/base.txt @@ -86,4 +122,13 @@ typing-extensions==4.10.0 # via # -r requirements/base.txt # asgiref + # bytecode + # cattrs + # ddtrace # faker +wrapt==1.16.0 + # via deprecated +xmltodict==0.13.0 + # via ddtrace +zipp==3.18.1 + # via importlib-metadata