Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add monitor_django_management_command #482

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
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.
7.2.0 - 2025-02-18
------------------
Added
~~~~~
* Added ``monitor_django_management_command`` to enable monitoring of Django management commands.

7.1.0 - 2024-12-05
------------------
Added
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__ = "7.1.0"
__version__ = "7.2.0"

default_app_config = (
"edx_django_utils.apps.EdxDjangoUtilsConfig"
Expand Down
25 changes: 25 additions & 0 deletions edx_django_utils/monitoring/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,28 @@ Monitoring Memory Usage
-----------------------

In addition to adding the MonitoringMemoryMiddleware, you will need to enable a waffle switch ``edx_django_utils.monitoring.enable_memory_middleware`` to enable the additional monitoring.

Monitoring Django Management Commands
-------------------------------------

The ``monitor_django_management_command`` utility provides monitoring support for Django management commands.
See the docstring for complete configuration details.

Here's an example of how to integrate it into your ``manage.py``:

.. code-block:: python

if __name__ == "__main__":
...
from django.conf import settings
from django.core.management import execute_from_command_line

monitoring_enabled = getattr(settings, "ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING", False)

if monitoring_enabled and len(sys.argv) > 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a scenario where len(sys.argv) > 1, and would you really want to drop monitoring altogether, or would you want to just pass a different (static) value to monitor_django_management_command?

from edx_django_utils.monitoring import monitor_django_management_command

with monitor_django_management_command(sys.argv[1]):
execute_from_command_line(sys.argv)
else:
execute_from_command_line(sys.argv)
1 change: 1 addition & 0 deletions edx_django_utils/monitoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
accumulate,
function_trace,
increment,
monitor_django_management_command,
record_exception,
set_custom_attribute,
set_custom_attributes_for_course_key,
Expand Down
52 changes: 52 additions & 0 deletions edx_django_utils/monitoring/internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"""
from contextlib import ExitStack, contextmanager

from django.conf import settings

from .backends import configured_backends
from .middleware import CachedCustomMonitoringMiddleware

Expand All @@ -28,6 +30,33 @@
newrelic = None # pylint: disable=invalid-name


# pylint: disable=setting-boolean-default-value
def _get_enable_custom_management_command_monitoring():
"""
Returns whether custom Django management command monitoring is enabled.

.. setting_name: ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING
.. setting_default: False
.. setting_description: A boolean flag that determines whether Django management command executions
should be explicitly monitored.
"""
return getattr(settings, 'ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING', False)


def _get_django_management_monitoring_function_trace_name():
"""
Returns the function trace name used for monitoring Django management commands.

.. setting_name: DJANGO_MANAGEMENT_MONITORING_FUNCTION_TRACE_NAME
.. setting_default: 'django.command'
.. setting_description: This setting specifies the function trace name that will be used when monitoring
Django management commands. It helps group and categorize monitoring spans for better visibility.
.. setting_warning: Ensure this setting is properly configured to align with your monitoring tools,
as an incorrect name may lead to untracked or misclassified spans.
"""
return getattr(settings, 'DJANGO_MANAGEMENT_MONITORING_FUNCTION_TRACE_NAME', 'django.command')


def accumulate(name, value):
"""
Accumulate monitoring custom attribute for the current request.
Expand Down Expand Up @@ -143,3 +172,26 @@ def noop_decorator(func):
return newrelic.agent.background_task(*args, **kwargs)
else:
return noop_decorator


@contextmanager
def monitor_django_management_command(name):
"""
Context manager for monitoring Django management commands.

- Creates a monitoring span using `function_trace`, allowing the execution
of the command to be tracked explicitly in monitoring tools.
- Sets the transaction name using `set_monitoring_transaction_name`
to the name of the command, provided in the `name` argument.
- Only applies monitoring if `ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING` is enabled.
"""

custom_command_monitoring_enabled = _get_enable_custom_management_command_monitoring()
trace_name = _get_django_management_monitoring_function_trace_name()

if custom_command_monitoring_enabled:
with function_trace(trace_name):
set_monitoring_transaction_name(name)
yield
else:
yield
61 changes: 61 additions & 0 deletions edx_django_utils/monitoring/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Tests monitoring utils.
"""
from unittest.mock import patch

from django.test import TestCase, override_settings

from edx_django_utils.monitoring import monitor_django_management_command


class MonitorDjangoManagementCommandTests(TestCase):
"""Test suite for monitor_django_management_command."""

@patch("edx_django_utils.monitoring.internal.utils.function_trace")
@patch("edx_django_utils.monitoring.internal.utils.set_monitoring_transaction_name")
def test_monitoring_enabled(self, mock_set_transaction_name, mock_function_trace):
"""
Test that when custom management command monitoring is enabled:
- function_trace is called with the correct trace name.
- set_monitoring_transaction_name is called with the correct command name.
"""

with override_settings(ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING=True):
with monitor_django_management_command("test_command"):
pass

mock_function_trace.assert_called_once_with("django.command")
mock_set_transaction_name.assert_called_once_with("test_command")

@patch("edx_django_utils.monitoring.internal.utils.function_trace")
@patch("edx_django_utils.monitoring.internal.utils.set_monitoring_transaction_name")
def test_monitoring_disabled(self, mock_set_transaction_name, mock_function_trace):
"""
Test that when custom management command monitoring is disabled:
- function_trace and set_monitoring_transaction_name are not called.
"""

with override_settings(ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING=False):
with monitor_django_management_command("test_command"):
pass

mock_function_trace.assert_not_called()
mock_set_transaction_name.assert_not_called()

@patch("edx_django_utils.monitoring.internal.utils.function_trace")
@patch("edx_django_utils.monitoring.internal.utils.set_monitoring_transaction_name")
def test_custom_trace_name_usage(self, mock_set_name, mock_function_trace):
"""
Test that a custom trace name is used when DJANGO_MANAGEMENT_MONITORING_FUNCTION_TRACE_NAME
is explicitly set.
"""

with override_settings(
ENABLE_CUSTOM_MANAGEMENT_COMMAND_MONITORING=True,
DJANGO_MANAGEMENT_MONITORING_FUNCTION_TRACE_NAME="custom.trace",
):
with monitor_django_management_command("test_command"):
pass

mock_function_trace.assert_called_once_with("custom.trace")
mock_set_name.assert_called_once_with("test_command")