Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
02ad9fb
fix: Add management command monitoring pipeline
ktyagiapphelix2u Mar 27, 2026
1c76575
fix: Add management command monitoring pipeline
ktyagiapphelix2u Mar 27, 2026
59a6f4a
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 20, 2026
75ceb43
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 22, 2026
fa0c178
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 22, 2026
2d7cff4
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 23, 2026
cbf2c90
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 28, 2026
4fb153c
fix: Add management command monitoring pipeline
ktyagiapphelix2u Jul 30, 2026
f576d94
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 3, 2026
d898455
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 3, 2026
f85f733
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 6, 2026
f158b15
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 6, 2026
aa5521e
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 7, 2026
5ea2b55
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 7, 2026
79aec8c
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 11, 2026
5e28326
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 11, 2026
94c8d4a
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 12, 2026
58369d5
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 12, 2026
137b948
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 28, 2026
3964f01
fix: Add management command monitoring pipeline
ktyagiapphelix2u Aug 28, 2026
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
1 change: 1 addition & 0 deletions edx_filters_pipelines/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Management command filter pipeline integrations."""
1 change: 1 addition & 0 deletions edx_filters_pipelines/management/pipelines/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Pipeline steps for management command integrations."""
72 changes: 72 additions & 0 deletions edx_filters_pipelines/management/pipelines/monitoring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Pipeline steps for management command observability."""

import logging
import time
from contextlib import contextmanager, nullcontext

import django
from edx_django_utils.monitoring import function_trace, set_custom_attribute, set_monitoring_transaction_name
from openedx_filters import PipelineStep

from edx_filters_pipelines.waffle import ENABLE_MANAGEMENT_COMMAND_MONITORING

log = logging.getLogger(__name__)

DEFAULT_TRACE_NAME = 'django.management.command'


@contextmanager
def monitor_management_command(command_name, service_variant, trace_name=DEFAULT_TRACE_NAME):
"""Wrap a management command execution with Datadog monitoring metadata."""
transaction_name = f'{service_variant}.management.{command_name}'

set_monitoring_transaction_name(transaction_name)
set_custom_attribute('management_command.name', command_name)
set_custom_attribute('management_command.service_variant', service_variant)
set_custom_attribute('management_command.transaction_name', transaction_name)

start_time = time.monotonic()
status = 'failure'

try:
with function_trace(trace_name):
yield
status = 'success'
except BaseException as exc:
set_custom_attribute('management_command.exception_class', exc.__class__.__name__)
if isinstance(exc, SystemExit):
set_custom_attribute('management_command.exit_code', exc.code)
raise
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
finally:
set_custom_attribute('management_command.status', status)
set_custom_attribute('management_command.duration_seconds', time.monotonic() - start_time)


class ManagementCommandMonitoringPipelineStep(PipelineStep):
"""Add Datadog monitoring around Django management command execution."""

def run_filter(self, command_name, service_variant, command_runner): # pylint: disable=arguments-differ
"""Return a wrapped command runner that applies monitoring when enabled."""
trace_name = self.extra_config.get('trace_name', DEFAULT_TRACE_NAME)

def wrapped_runner():
monitor_context = nullcontext()

try:
django.setup()
if ENABLE_MANAGEMENT_COMMAND_MONITORING.is_enabled():
monitor_context = monitor_management_command(command_name, service_variant, trace_name)
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
except Exception: # pylint: disable=broad-except
log.exception(
'Failed to initialize management command monitoring for %s; continuing without monitoring.',
command_name,
)

with monitor_context:
return command_runner()

return {
'command_name': command_name,
'service_variant': service_variant,
'command_runner': wrapped_runner,
}
12 changes: 12 additions & 0 deletions edx_filters_pipelines/waffle.py
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,15 @@
# .. toggle_target_removal_date: None because this is a long-term feature
# .. toggle_warning: When the flag is ON, recaptcha validation is enabled on registration.
ENABLE_RECAPTCHA_VALIDATION = WaffleFlag(f'{WAFFLE_NAMESPACE}.enable_registration_recaptcha_validation', __name__)

# .. toggle_name: filters_pipelines.enable_management_command_monitoring
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable Datadog monitoring for Django management commands.
# .. toggle_use_cases: opt_in
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
# .. toggle_creation_date: 2026-03-26
# .. toggle_target_removal_date: None because this is a long-term feature
# .. toggle_warning: When the flag is ON, management command execution is wrapped with Datadog monitoring.
ENABLE_MANAGEMENT_COMMAND_MONITORING = WaffleFlag(
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
f'{WAFFLE_NAMESPACE}.enable_management_command_monitoring', __name__
)
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated
Comment thread
Copilot marked this conversation as resolved.
1 change: 1 addition & 0 deletions requirements/test.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

pytest
pytest-cov # pytest extension for code coverage statistics
pytest-mock # pytest plugin providing mocker fixture
edx-lint
pylint-celery
setuptools
2 changes: 2 additions & 0 deletions requirements/test.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In base.in, you could add a minimum requirement with a comment based on: "Requires edx-django-utils 8.1.0+ for operation_name support.".

@ktyagiapphelix2u ktyagiapphelix2u Aug 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I checked this and agree with the intent, but CI currently cannot resolve edx-django-utils>=8.1.0 from the available package index (latest visible there is 8.0.1), so adding that minimum in base.in causes install failures. I have not added the 8.1.0 floor for now to keep the pipeline green.

Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ pytest-cov==6.2.1
# via
# -r requirements/base.txt
# -r requirements/test.in
pytest-mock
# via -r requirements/test.in
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
python-slugify==8.0.4
# via
# -r requirements/base.txt
Expand Down
83 changes: 83 additions & 0 deletions tests/test_edx-filters-pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
Tests for edx-filters-pipelines.py.
"""

from contextlib import nullcontext

import pytest
from openedx_filters.learning.filters import StudentRegistrationRequested

from edx_filters_pipelines.auth.pipelines.registration import PreventForbiddenUsernameRegistration
from edx_filters_pipelines.management.pipelines.monitoring import ManagementCommandMonitoringPipelineStep


def test_username_blocked():
Expand All @@ -20,3 +24,82 @@ def test_username_blocked():
step.run_filter(form_data=form_data)

assert "Usernames can't include words that could be mistaken for course roles." in str(exc_info.value)


def test_management_command_monitoring_step_disabled(mocker):
step = ManagementCommandMonitoringPipelineStep(
'org.openedx.platform.management.command.execute.requested.v1',
'edx_filters_pipelines.management.pipelines.monitoring.ManagementCommandMonitoringPipelineStep',
)
command_runner = mocker.Mock(return_value='ok')
django_setup = mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.django.setup')
toggle = mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.ENABLE_MANAGEMENT_COMMAND_MONITORING.is_enabled',
return_value=False,
)
mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.function_trace')

result = step.run_filter('migrate', 'lms', command_runner)

assert result['command_name'] == 'migrate'
assert result['service_variant'] == 'lms'
assert result['command_runner']() == 'ok'
django_setup.assert_called_once()
toggle.assert_called_once()
command_runner.assert_called_once()


def test_management_command_monitoring_step_enabled(mocker):
step = ManagementCommandMonitoringPipelineStep(
'org.openedx.platform.management.command.execute.requested.v1',
'edx_filters_pipelines.management.pipelines.monitoring.ManagementCommandMonitoringPipelineStep',
)
command_runner = mocker.Mock(return_value='ok')
mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.django.setup')
mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.ENABLE_MANAGEMENT_COMMAND_MONITORING.is_enabled',
return_value=True,
)
function_trace = mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.function_trace',
return_value=nullcontext(),
)
set_transaction_name = mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.set_monitoring_transaction_name'
)
set_custom_attribute = mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.set_custom_attribute'
)

wrapped = step.run_filter('migrate', 'lms', command_runner)['command_runner']

assert wrapped() == 'ok'
function_trace.assert_called_once_with('django.management.command')
set_transaction_name.assert_called_once_with('lms.management.migrate')
set_custom_attribute.assert_any_call('management_command.name', 'migrate')
set_custom_attribute.assert_any_call('management_command.service_variant', 'lms')
set_custom_attribute.assert_any_call('management_command.status', 'success')
Comment thread
ktyagiapphelix2u marked this conversation as resolved.
Outdated


def test_management_command_monitoring_step_uses_configured_trace_name(mocker):
step = ManagementCommandMonitoringPipelineStep(
'org.openedx.platform.management.command.execute.requested.v1',
'edx_filters_pipelines.management.pipelines.monitoring.ManagementCommandMonitoringPipelineStep',
trace_name='custom.management.trace',
)
command_runner = mocker.Mock(return_value=None)
mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.django.setup')
mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.ENABLE_MANAGEMENT_COMMAND_MONITORING.is_enabled',
return_value=True,
)
function_trace = mocker.patch(
'edx_filters_pipelines.management.pipelines.monitoring.function_trace',
return_value=nullcontext(),
)
mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.set_monitoring_transaction_name')
mocker.patch('edx_filters_pipelines.management.pipelines.monitoring.set_custom_attribute')

step.run_filter('collectstatic', 'cms', command_runner)['command_runner']()

function_trace.assert_called_once_with('custom.management.trace')
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D


[pytest]
addopts = --cov edx-filters-pipelines --cov-report term-missing --cov-report xml
addopts = --cov edx_filters_pipelines --cov-report term-missing --cov-report xml
norecursedirs = .* docs requirements site-packages

[testenv]
Expand Down
Loading