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: Add Waffle config watcher #481

Merged
merged 3 commits into from
Oct 27, 2023
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
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Change Log

Unreleased
~~~~~~~~~~

[2.2.0] - 2023-10-27
~~~~~~~~~~~~~~~~~~~~

Added
_____

* Add ``edx_arch_experiments.config_watcher`` Django app for monitoring Waffle changes
* Add script to get github action errors
* Add script to republish failed events

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__ = '2.1.0'
__version__ = '2.2.0'
Empty file.
17 changes: 17 additions & 0 deletions edx_arch_experiments/config_watcher/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
App for reporting configuration changes to Slack for operational awareness.
"""

from django.apps import AppConfig


class ConfigWatcherApp(AppConfig):
"""
Django application to report configuration changes to operators.
"""
name = 'edx_arch_experiments.config_watcher'

def ready(self):
from .signals import receivers # pylint: disable=import-outside-toplevel

receivers.connect_receivers()
Empty file.
123 changes: 123 additions & 0 deletions edx_arch_experiments/config_watcher/signals/receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
Signal receivers for the config watcher.

Call ``connect_receivers`` to initialize.
"""

import html
import json
import logging
import urllib.request

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

log = logging.getLogger(__name__)

# .. setting_name: CONFIG_WATCHER_SLACK_WEBHOOK_URL
# .. setting_default: None
# .. setting_description: Slack webhook URL to send config change events to.
# If not configured, this functionality is disabled.
CONFIG_WATCHER_SLACK_WEBHOOK_URL = getattr(settings, 'CONFIG_WATCHER_SLACK_WEBHOOK_URL', 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

# https://api.slack.com/reference/surfaces/formatting
body_data = {
'text': html.escape(message, quote=False)
}

req = urllib.request.Request(
url=CONFIG_WATCHER_SLACK_WEBHOOK_URL, data=json.dumps(body_data).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=2) as resp:
status = resp.getcode()
if status != 200:
log.error(f"Slack rejected the config watcher message. {status=}, body={resp.read().decode('utf-8')}")


def _report_config_change(message):
"""
Report this message string as a configuration change.

Sends to logs and to Slack.
"""
log.info(message)
_send_to_slack(message)


def _report_waffle_change(model_short_name, instance, created, fields):
"""
Report that a model instance has been created or updated.
"""
verb = "created" if created else "updated"
state_desc = ", ".join(f"{field}={repr(getattr(instance, field))}" for field in fields)
_report_config_change(f"Waffle {model_short_name} {instance.name!r} was {verb}. New config: {state_desc}")


def _report_waffle_delete(model_short_name, instance):
"""
Report that a model instance has been deleted.
"""
_report_config_change(f"Waffle {model_short_name} {instance.name!r} was deleted")


# List of models to observe. Each is a dictionary that matches the
# keyword args of _register_waffle_observation.
_WAFFLE_MODELS_TO_OBSERVE = [
{
'model': waffle.models.Flag,
'short_name': 'flag',
'fields': ['everyone', 'percent', 'superusers', 'staff', 'authenticated', 'note', 'languages'],
},
{
'model': waffle.models.Switch,
'short_name': 'switch',
'fields': ['active', 'note'],
},
{
'model': waffle.models.Sample,
'short_name': 'sample',
'fields': ['percent', 'note'],
},
]


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
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
"""
@receiver(signals.post_save, sender=model)
def report_waffle_change(*args, instance, created, **kwargs):
try:
_report_waffle_change(short_name, instance, created, fields)
except: # noqa pylint: disable=bare-except
# Log and suppress error so Waffle change can proceed
log.exception(f"Failed to report change to waffle {short_name}")

@receiver(signals.post_delete, sender=model)
def report_waffle_delete(*args, instance, **kwargs):
try:
_report_waffle_delete(short_name, instance)
except: # noqa pylint: disable=bare-except
log.exception(f"Failed to report deletion of waffle {short_name}")


def connect_receivers():
"""
Initialize application's receivers.
"""
for config in _WAFFLE_MODELS_TO_OBSERVE:
_register_waffle_observation(**config)
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

Django # Web application framework
edx_django_utils
django-waffle # Configuration switches and flags -- used by config_watcher app
4 changes: 3 additions & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ django==3.2.22
django-crum==0.7.9
# via edx-django-utils
django-waffle==4.0.0
# via edx-django-utils
# via
# -r requirements/base.in
# edx-django-utils
edx-django-utils==5.7.0
# via -r requirements/base.in
newrelic==9.1.0
Expand Down
Loading