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: refactor code_owner code from edx-dajango-utils #838

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ Change Log
Unreleased
~~~~~~~~~~

[5.1.0] - 2024-10-23
~~~~~~~~~~~~~~~~~~~~
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 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.

[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.
58 changes: 58 additions & 0 deletions edx_arch_experiments/datadog_monitoring/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
App for 2U-specific edx-platform Datadog monitoring.
"""

import logging

from django.apps import AppConfig

from .code_owner.utils import get_code_owner_from_module

log = logging.getLogger(__name__)


class DatadogMonitoringSpanProcessor:
"""Datadog span processor that adds custom monitoring (e.g. code owner tags)."""

def on_span_start(self, span):
"""
Adds custom monitoring at span creation time.

Specifically, adds code owner span tag for celery run spans.
"""
if not span or not hasattr(span, 'name') or not hasattr(span, 'resource'):
return

if span.name == '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.
get_code_owner_from_module(span.resource)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be set_code_owner_attribute_from_module?


def on_span_finish(self, span):
pass

def shutdown(self, _timeout):
pass


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):
try:
from ddtrace import tracer # pylint: disable=import-outside-toplevel

tracer._span_processors.append(DatadogMonitoringSpanProcessor()) # pylint: disable=protected-access
log.info("Attached DatadogMonitoringSpanProcessor")
except ImportError:
log.warning(
"Unable to attach DatadogMonitoringSpanProcessor"
" -- ddtrace module not found."
)
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.
"""
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
Loading