Skip to content

Commit

Permalink
feat: refactor code_owner code from edx-dajango-utils (#838)
Browse files Browse the repository at this point in the history
Initial rollout of moving code_owner monitoring code from
edx-django-utils to this plugin.

- Adds near duplicate of code owner middleware from
  edx-django-utils.
- Adds code owner span tags for celery using Datadog span
  processing of celery.run spans.
- Uses temporary span tags names using `_2`, like `code_owner_2`,
  for rollout and comparison with the original span tags.
- Span tag code_owner_2_module includes the task name, where
  the original code_owner_module does not. In both cases, the
  code owner is computed the same, because it is based on a prefix match.

See #784
  • Loading branch information
robrap authored Nov 21, 2024
1 parent 2a44891 commit ee70bda
Show file tree
Hide file tree
Showing 32 changed files with 1,458 additions and 114 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ Change Log
Unreleased
~~~~~~~~~~

[5.1.0] - 2024-11-21
~~~~~~~~~~~~~~~~~~~~
Added
-----
* Added Datadog monitoring app which adds code owner monitoring. This is the first step in moving code owner code from edx-django-utils to this plugin.

* Adds near duplicate of code owner middleware from edx-django-utils.
* Adds code owner span tags for celery using Datadog span processing of celery.run spans.
* Uses temporary span tags names using ``_2``, like ``code_owner_2``, for rollout and comparison with the original span tags.
* Span tag code_owner_2_module includes the task name, where the original code_owner_module does not. In both cases, the code owner is computed the same, because it is based on a prefix match.

[5.0.0] - 2024-10-22
~~~~~~~~~~~~~~~~~~~~
Removed
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__ = '5.0.0'
__version__ = '5.1.0'
6 changes: 6 additions & 0 deletions edx_arch_experiments/datadog_monitoring/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Datadog Monitoring
###################

When installed in the LMS as a plugin app, the ``datadog_monitoring`` app adds additional monitoring.

This is where our code_owner_2 monitoring code lives, for example.
Empty file.
19 changes: 19 additions & 0 deletions edx_arch_experiments/datadog_monitoring/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
App for 2U-specific edx-platform Datadog monitoring.
"""
from django.apps import AppConfig


class DatadogMonitoring(AppConfig):
"""
Django application to handle 2U-specific Datadog monitoring.
"""
name = 'edx_arch_experiments.datadog_monitoring'

# Mark this as a plugin app
plugin_app = {}

def ready(self):
# Implicitly connect signal handlers decorated with @receiver
# pylint: disable=import-outside-toplevel,unused-import
from edx_arch_experiments.datadog_monitoring.signals import handlers
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
This directory should only be used internally.
Its public API is exposed in the top-level monitoring __init__.py.
See its README.rst for details.
"""
26 changes: 26 additions & 0 deletions edx_arch_experiments/datadog_monitoring/code_owner/datadog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Datadog span processor for celery span code owners.
"""
from .utils import set_code_owner_attribute_from_module


class CeleryCodeOwnerSpanProcessor:
"""
Datadog span processor that adds celery code owner span tags.
"""

def on_span_start(self, span):
"""
Adds code owner span tag for celery run spans at span creation.
"""
if getattr(span, 'name', None) == 'celery.run':
# We can use this for celery spans, because the resource name is more predictable
# and available from the start. For django requests, we'll instead continue to use
# django middleware for setting code owner.
set_code_owner_attribute_from_module(span.resource)

def on_span_finish(self, span):
pass

def shutdown(self, _timeout):
pass
89 changes: 89 additions & 0 deletions edx_arch_experiments/datadog_monitoring/code_owner/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Middleware for code_owner_2 custom attribute
"""
import logging

from django.urls import resolve
from edx_django_utils.monitoring import set_custom_attribute

from .utils import get_code_owner_from_module, is_code_owner_mappings_configured, set_code_owner_custom_attributes

log = logging.getLogger(__name__)


class CodeOwnerMonitoringMiddleware:
"""
Django middleware object to set custom attributes for the owner of each view.
For instructions on usage, see:
https://github.com/edx/edx-arch-experiments/blob/master/edx_arch_experiments/datadog_monitoring/docs/how_tos/add_code_owner_custom_attribute_to_an_ida.rst
Custom attributes set:
- code_owner_2: The owning team mapped to the current view.
- code_owner_2_module: The module found from the request or current transaction.
- code_owner_2_path_error: The error mapping by path, if code_owner_2 isn't found in other ways.
"""
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
self._set_code_owner_attribute(request)
return response

def process_exception(self, request, exception): # pylint: disable=W0613
self._set_code_owner_attribute(request)

def _set_code_owner_attribute(self, request):
"""
Sets the code_owner_2 custom attribute for the request.
"""
code_owner = None
module = self._get_module_from_request(request)
if module:
code_owner = get_code_owner_from_module(module)

if code_owner:
set_code_owner_custom_attributes(code_owner)

def _get_module_from_request(self, request):
"""
Get the module from the request path or the current transaction.
Side-effects:
Sets code_owner_2_module custom attribute, used to determine code_owner_2.
If module was not found, may set code_owner_2_path_error custom attribute
if applicable.
Returns:
str: module name or None if not found
"""
if not is_code_owner_mappings_configured():
return None

module, path_error = self._get_module_from_request_path(request)
if module:
set_custom_attribute('code_owner_2_module', module)
return module

# monitor errors if module was not found
if path_error:
set_custom_attribute('code_owner_2_path_error', path_error)
return None

def _get_module_from_request_path(self, request):
"""
Uses the request path to get the view_func module.
Returns:
(str, str): (module, error_message), where at least one of these should be None
"""
try:
view_func, _, _ = resolve(request.path)
module = view_func.__module__
return module, None
except Exception as e: # pragma: no cover, pylint: disable=broad-exception-caught
return None, str(e)
Loading

0 comments on commit ee70bda

Please sign in to comment.