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

feat: Enable ConfigWatcher in CMS; prefix Slack messages with IDA name #537

Merged
merged 1 commit into from
Jan 23, 2024
Merged
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
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",
],
},
)
Loading