Skip to content

Commit

Permalink
feat: Enable ConfigWatcher in CMS; prefix Slack messages with IDA name (
Browse files Browse the repository at this point in the history
#537)

Introduces `CONFIG_WATCHER_SERVER_NAME` to support messaging from multiple
services.

Also:

- Add a README for the plugin.
- Add start of unit tests in ConfigWatcher
- Soft-import waffle

The new unit tests were failing because waffle isn't actually a
declared dependency, so we switch to `import_string`. This also moves
us closer to being able to configure the plugin app entirely using
Django settings (rather than a hardcoded model dict).
  • Loading branch information
timmc-edx authored Jan 23, 2024
1 parent 06e1277 commit b6c5ee6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ Change Log
Unreleased
~~~~~~~~~~

[3.3.0] - 2024-01-23
~~~~~~~~~~~~~~~~~~~~
Changed
_______
* Updated ``ConfigWatcher`` to include the IDA's name in change messages if ``CONFIG_WATCHER_SERVICE_NAME`` is set
* Enabled ``ConfigWatcher`` as a plugin for CMS

[3.2.0] - 2024-01-11
~~~~~~~~~~~~~~~~~~~~
Added
Expand Down
2 changes: 1 addition & 1 deletion edx_arch_experiments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
A plugin to include applications under development by the architecture team at 2U.
"""

__version__ = '3.2.0'
__version__ = '3.3.0'
8 changes: 8 additions & 0 deletions edx_arch_experiments/config_watcher/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ConfigWatcher
#############

Plugin app that can report on changes to Django model instances via logging and optionally Slack messages. The goal is to help operators who are investigating an outage or other sudden change in behavior by allowing them to easily determine what has changed recently.

Currently specialized to observe Waffle flags, switches, and samples, but could be expanded to other models.

See ``.signals.receivers`` for available settings and ``/setup.py`` for IDA plugin configuration.
23 changes: 18 additions & 5 deletions edx_arch_experiments/config_watcher/signals/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import logging
import urllib.request

import waffle.models
from django.conf import settings
from django.db.models import signals
from django.dispatch import receiver
from django.utils.module_loading import import_string

log = logging.getLogger(__name__)

Expand All @@ -22,12 +22,23 @@
# If not configured, this functionality is disabled.
CONFIG_WATCHER_SLACK_WEBHOOK_URL = getattr(settings, 'CONFIG_WATCHER_SLACK_WEBHOOK_URL', None)

# .. setting_name: CONFIG_WATCHER_SERVICE_NAME
# .. setting_default: None
# .. setting_description: Name of service, to be included in Slack messages in
# in order to distinguish messages from multiple services being aggregated in
# one channel. Can be a regular name ("LMS"), hostname, ("courses.example.com"),
# or any other string. Optional.
CONFIG_WATCHER_SERVICE_NAME = getattr(settings, 'CONFIG_WATCHER_SERVICE_NAME', None)


def _send_to_slack(message):
"""Send this message as plain text to the configured Slack channel."""
if not CONFIG_WATCHER_SLACK_WEBHOOK_URL:
return

if CONFIG_WATCHER_SERVICE_NAME:
message = f"[{CONFIG_WATCHER_SERVICE_NAME}] {message}"

# https://api.slack.com/reference/surfaces/formatting
body_data = {
'text': html.escape(message, quote=False)
Expand Down Expand Up @@ -73,17 +84,17 @@ def _report_waffle_delete(model_short_name, instance):
# keyword args of _register_waffle_observation.
_WAFFLE_MODELS_TO_OBSERVE = [
{
'model': waffle.models.Flag,
'model': 'waffle.models.Flag',
'short_name': 'flag',
'fields': ['everyone', 'percent', 'note'],
},
{
'model': waffle.models.Switch,
'model': 'waffle.models.Switch',
'short_name': 'switch',
'fields': ['active', 'note'],
},
{
'model': waffle.models.Sample,
'model': 'waffle.models.Sample',
'short_name': 'sample',
'fields': ['percent', 'note'],
},
Expand All @@ -95,11 +106,13 @@ def _register_waffle_observation(*, model, short_name, fields):
Register a Waffle model for observation according to config values.
Args:
model (class): The model class to monitor
model (str): The model class to monitor, as a dotted string reference
short_name (str): A short descriptive name for an instance of the model, e.g. "flag"
fields (list): Names of fields to report on in the Slack message
"""

model = import_string(model)

# Note that weak=False is required here. Django by default only
# holds weak references to receiver functions. But these inner
# functions would then be garbage-collected, and Django would drop
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Test ConfigWatcher signal receivers (main code).
"""

import json
from contextlib import ExitStack
from unittest.mock import call, patch

import ddt
from django.test import TestCase, override_settings

from edx_arch_experiments.config_watcher.signals import receivers


@ddt.ddt
class TestConfigWatcherReceivers(TestCase):

@ddt.unpack
@ddt.data(
(
None, None, None,
),
(
'https://localhost', None, "test message",
),
(
'https://localhost', 'my-ida', "[my-ida] test message",
),
)
def test_send_to_slack(self, slack_url, service_name, expected_message):
"""Check that message prefixing is performed as expected."""
# This can be made cleaner in Python 3.10
with ExitStack() as stack:
patches = [
patch('urllib.request.Request'),
patch('urllib.request.urlopen'),
patch.object(receivers, 'CONFIG_WATCHER_SLACK_WEBHOOK_URL', slack_url),
patch.object(receivers, 'CONFIG_WATCHER_SERVICE_NAME', service_name),
]
(mock_req, _, _, _) = [stack.enter_context(cm) for cm in patches]
receivers._send_to_slack("test message")

if expected_message is None:
mock_req.assert_not_called()
else:
assert mock_req.called_once()
(call_args, call_kwargs) = mock_req.call_args_list[0]
assert json.loads(call_kwargs['data'])['text'] == expected_message
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,8 @@ def is_requirement(line):
"config_watcher = edx_arch_experiments.config_watcher.apps:ConfigWatcher",
"codejail_service = edx_arch_experiments.codejail_service.apps:CodejailService",
],
"cms.djangoapp": [
"config_watcher = edx_arch_experiments.config_watcher.apps:ConfigWatcher",
],
},
)

0 comments on commit b6c5ee6

Please sign in to comment.