Skip to content

Commit 45e0531

Browse files
authored
Merge pull request #73 from edx/robrap/ARCHBOM-1260-code-owner-decorator
ARCHBOM-1260: enable code_owner for celery tasks
2 parents 9b0cdc9 + 8dd16ad commit 45e0531

File tree

8 files changed

+243
-83
lines changed

8 files changed

+243
-83
lines changed

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ Change Log
1515
Unreleased
1616
~~~~~~~~~~
1717

18+
[3.12.0] - 2020-11-17
19+
~~~~~~~~~~~~~~~~~~~~~
20+
21+
Added
22+
_____
23+
24+
* Added set_code_owner_attribute decorator for use with celery tasks.
25+
* Added set_code_owner_attribute_from_module as an alternative to the decorator.
26+
27+
Updated
28+
_______
29+
30+
* Cleaned up some of the code owner middleware code. In doing so, renamed custom attribute code_owner_path_module to code_owner_module. This may affect monitoring dashboards. Also slightly changed when error custom attributes are set.
31+
32+
1833
[3.11.0] - 2020-10-31
1934
~~~~~~~~~~~~~~~~~~~~~
2035

edx_django_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
EdX utilities for Django Application development..
33
"""
44

5-
__version__ = "3.11.0"
5+
__version__ = "3.12.0"
66

77
default_app_config = (
88
"edx_django_utils.apps.EdxDjangoUtilsConfig"

edx_django_utils/monitoring/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
See README.rst for details.
55
"""
66
from .internal.code_owner.middleware import CodeOwnerMonitoringMiddleware
7-
from .internal.code_owner.utils import get_code_owner_from_module
7+
from .internal.code_owner.utils import (
8+
get_code_owner_from_module,
9+
set_code_owner_attribute,
10+
set_code_owner_attribute_from_module
11+
)
812
from .internal.middleware import CachedCustomMonitoringMiddleware, MonitoringMemoryMiddleware
913
from .internal.transactions import (
1014
function_trace,

edx_django_utils/monitoring/docs/how_tos/add_code_owner_custom_attribute_to_an_ida.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ Setting up the Middleware
2121

2222
You simply need to add ``edx_django_utils.monitoring.CodeOwnerMonitoringMiddleware`` as described in the README to make this functionality available. Then it is ready to be configured.
2323

24+
Handling celery tasks
25+
---------------------
26+
27+
Celery tasks require use of a special decorator to set the ``code_owner`` custom attribute because no middleware will be run.
28+
29+
Here is an example::
30+
31+
@task()
32+
@set_code_owner_attribute
33+
def example_task():
34+
...
35+
36+
If the task is not compatible with additional decorators, you can use the following alternative::
37+
38+
@task()
39+
def example_task():
40+
set_code_owner_attribute_from_module(__name__)
41+
...
42+
2443
Configuring your app settings
2544
-----------------------------
2645

edx_django_utils/monitoring/internal/code_owner/middleware.py

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import logging
55

66
from django.urls import resolve
7+
from django.urls.exceptions import Resolver404
78

89
from ..transactions import get_current_transaction
910
from ..utils import set_custom_attribute
10-
from .utils import get_code_owner_from_module, is_code_owner_mappings_configured
11+
from .utils import _get_catch_all_code_owner, get_code_owner_from_module, is_code_owner_mappings_configured
1112

1213
log = logging.getLogger(__name__)
1314

@@ -21,10 +22,8 @@ class CodeOwnerMonitoringMiddleware:
2122
2223
Custom attributes set:
2324
- code_owner: The owning team mapped to the current view.
24-
- code_owner_mapping_error: If there are any errors when trying to perform the mapping.
25+
- code_owner_module: The module found from the request or current transaction.
2526
- code_owner_path_error: The error mapping by path, if code_owner isn't found in other ways.
26-
- code_owner_path_module: The __module__ of the view_func which was used to try to map to code_owner.
27-
This can be used to find missing mappings.
2827
- code_owner_transaction_error: The error mapping by transaction, if code_owner isn't found in other ways.
2928
- code_owner_transaction_name: The current transaction name used to try to map to code_owner.
3029
This can be used to find missing mappings.
@@ -43,105 +42,91 @@ def process_exception(self, request, exception):
4342

4443
def _set_code_owner_attribute(self, request):
4544
"""
46-
Sets the code_owner custom attribute, as well as several supporting custom attributes.
47-
48-
See CodeOwnerMonitoringMiddleware docstring for a complete list of attributes.
49-
45+
Sets the code_owner custom attribute for the request.
5046
"""
51-
code_owner, path_error = self._set_code_owner_attribute_from_path(request)
52-
if code_owner:
53-
set_custom_attribute('code_owner', code_owner)
54-
return
55-
if not path_error:
56-
# module found, but mapping wasn't configured
57-
code_owner = self._set_code_owner_attribute_catch_all()
58-
if code_owner:
59-
set_custom_attribute('code_owner', code_owner)
60-
return
61-
62-
code_owner, transaction_error = self._set_code_owner_attribute_from_current_transaction(request)
63-
if code_owner:
64-
set_custom_attribute('code_owner', code_owner)
65-
return
66-
if not transaction_error:
67-
# transaction name found, but mapping wasn't configured
68-
code_owner = self._set_code_owner_attribute_catch_all()
69-
if code_owner:
70-
set_custom_attribute('code_owner', code_owner)
71-
return
72-
73-
code_owner = self._set_code_owner_attribute_catch_all()
47+
code_owner = None
48+
module = self._get_module_from_request(request)
49+
if module:
50+
code_owner = get_code_owner_from_module(module)
51+
if not code_owner:
52+
code_owner = _get_catch_all_code_owner()
53+
7454
if code_owner:
7555
set_custom_attribute('code_owner', code_owner)
76-
return
7756

78-
# only report errors if code_owner couldn't be found, including catch-all
57+
def _get_module_from_request(self, request):
58+
"""
59+
Get the module from the request path or the current transaction.
60+
61+
Side-effects:
62+
Sets code_owner_module custom attribute, used to determine code_owner.
63+
If module was not found, may set code_owner_path_error and/or
64+
code_owner_transaction_error custom attributes if applicable.
65+
66+
Returns:
67+
str: module name or None if not found
68+
69+
"""
70+
if not is_code_owner_mappings_configured():
71+
return None
72+
73+
module, path_error = self._get_module_from_request_path(request)
74+
if module:
75+
set_custom_attribute('code_owner_module', module)
76+
return module
77+
78+
module, transaction_error = self._get_module_from_current_transaction()
79+
if module:
80+
set_custom_attribute('code_owner_module', module)
81+
return module
82+
83+
# monitor errors if module was not found
7984
if path_error:
8085
set_custom_attribute('code_owner_path_error', path_error)
8186
if transaction_error:
8287
set_custom_attribute('code_owner_transaction_error', transaction_error)
88+
return None
8389

84-
def _set_code_owner_attribute_from_path(self, request):
90+
def _get_module_from_request_path(self, request):
8591
"""
86-
Uses the request path to find the view_func and then sets code owner attributes based on the view.
87-
88-
Side-effects:
89-
Sets code_owner_path_module custom attribute, used to determine code_owner
92+
Uses the request path to get the view_func module.
9093
9194
Returns:
92-
(str, str): (code_owner, error_message), where at least one of these should be None
95+
(str, str): (module, error_message), where at least one of these should be None
9396
9497
"""
95-
if not is_code_owner_mappings_configured():
96-
return None, None
97-
9898
try:
9999
view_func, _, _ = resolve(request.path)
100-
path_module = view_func.__module__
101-
set_custom_attribute('code_owner_path_module', path_module)
102-
code_owner = get_code_owner_from_module(path_module)
103-
return code_owner, None
104-
except Exception as e: # pylint: disable=broad-except
100+
module = view_func.__module__
101+
return module, None
102+
# TODO: Replace ImportError with ModuleNotFoundError when Python 3.5 support is dropped.
103+
except (ImportError, Resolver404) as e:
104+
return None, str(e)
105+
except Exception as e: # pylint: disable=broad-except; #pragma: no cover
106+
# will remove broad exceptions after ensuring all proper cases are covered
107+
set_custom_attribute('deprecated_broad_except__get_module_from_request_path', e.__class__)
105108
return None, str(e)
106109

107-
def _set_code_owner_attribute_from_current_transaction(self, request):
110+
def _get_module_from_current_transaction(self):
108111
"""
109-
Uses the current transaction name to set the code owner attribute.
112+
Uses the current transaction to get the module.
110113
111114
Side-effects:
112115
Sets code_owner_transaction_name custom attribute, used to determine code_owner
113116
114117
Returns:
115-
(str, str): (code_owner, error_message), where at least one of these should be None
118+
(str, str): (module, error_message), where at least one of these should be None
116119
117120
"""
118-
if not is_code_owner_mappings_configured():
119-
# ensure we don't set code ownership custom attributes if not configured to do so
120-
return None, None # pragma: no cover
121-
122121
try:
123122
# Example: openedx.core.djangoapps.contentserver.middleware:StaticContentServer
124123
transaction_name = get_current_transaction().name
125124
if not transaction_name:
126125
return None, 'No current transaction name found.'
127-
module_name = transaction_name.split(':')[0]
126+
module = transaction_name.split(':')[0]
128127
set_custom_attribute('code_owner_transaction_name', transaction_name)
129-
set_custom_attribute('code_owner_path_module', module_name)
130-
code_owner = get_code_owner_from_module(module_name)
131-
return code_owner, None
128+
return module, None
132129
except Exception as e: # pylint: disable=broad-except
130+
# will remove broad exceptions after ensuring all proper cases are covered
131+
set_custom_attribute('deprecated_broad_except___get_module_from_current_transaction', e.__class__)
133132
return None, str(e)
134-
135-
def _set_code_owner_attribute_catch_all(self):
136-
"""
137-
If the catch-all module "*" is configured, return the code_owner.
138-
139-
Returns:
140-
(str): code_owner or None if no catch-all configured.
141-
142-
"""
143-
try:
144-
code_owner = get_code_owner_from_module('*')
145-
return code_owner
146-
except Exception: # pylint: disable=broad-except; #pragma: no cover
147-
return None

edx_django_utils/monitoring/internal/code_owner/utils.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
"""
44
import logging
55
import re
6+
from functools import wraps
67

78
from django.conf import settings
89

10+
from ..utils import set_custom_attribute
11+
912
log = logging.getLogger(__name__)
1013

1114

@@ -22,6 +25,9 @@ def get_code_owner_from_module(module):
2225
https://github.com/edx/edx-django-utils/blob/master/edx_django_utils/monitoring/docs/how_tos/add_code_owner_custom_attribute_to_an_ida.rst
2326
2427
"""
28+
if not module:
29+
return None
30+
2531
code_owner_mappings = get_code_owner_mappings()
2632
if code_owner_mappings is None:
2733
return None
@@ -97,12 +103,80 @@ def get_code_owner_mappings():
97103
path_without_prefix = path[optional_module_prefix_match.end():]
98104
path_to_code_owner_mapping[path_without_prefix] = code_owner
99105
except Exception as e: # pylint: disable=broad-except
106+
# will remove broad exceptions after ensuring all proper cases are covered
107+
set_custom_attribute('deprecated_broad_except_get_code_owner_mappings', e.__class__)
100108
log.exception('Error processing code_owner_mappings. {}'.format(e))
101109

102110
_PATH_TO_CODE_OWNER_MAPPINGS = path_to_code_owner_mapping
103111
return _PATH_TO_CODE_OWNER_MAPPINGS
104112

105113

114+
def _get_catch_all_code_owner():
115+
"""
116+
If the catch-all module "*" is configured, return the code_owner.
117+
118+
Returns:
119+
(str): code_owner or None if no catch-all configured.
120+
121+
"""
122+
try:
123+
code_owner = get_code_owner_from_module('*')
124+
return code_owner
125+
except Exception as e: # pylint: disable=broad-except; #pragma: no cover
126+
# will remove broad exceptions after ensuring all proper cases are covered
127+
set_custom_attribute('deprecated_broad_except___get_module_from_current_transaction', e.__class__)
128+
return None
129+
130+
131+
def set_code_owner_attribute_from_module(module):
132+
"""
133+
Updates the code_owner and code_owner_module custom attributes.
134+
135+
Celery tasks or other non-web functions do not use middleware, so we need
136+
an alternative way to set the code_owner custom attribute.
137+
138+
Note: These settings will be overridden by the CodeOwnerMonitoringMiddleware.
139+
This method can't be used to override web functions at this time.
140+
141+
Usage::
142+
143+
set_code_owner_attribute_from_module(__name__)
144+
145+
"""
146+
set_custom_attribute('code_owner_module', module)
147+
code_owner = get_code_owner_from_module(module)
148+
if not code_owner:
149+
code_owner = _get_catch_all_code_owner()
150+
151+
if code_owner:
152+
set_custom_attribute('code_owner', code_owner)
153+
154+
155+
def set_code_owner_attribute(wrapped_function):
156+
"""
157+
Decorator to set the code_owner and code_owner_module custom attributes.
158+
159+
Celery tasks or other non-web functions do not use middleware, so we need
160+
an alternative way to set the code_owner custom attribute.
161+
162+
Usage::
163+
164+
@task()
165+
@set_code_owner_attribute
166+
def example_task():
167+
...
168+
169+
Note: If the decorator can't be used for some reason, just call
170+
``set_code_owner_attribute_from_module`` directly.
171+
172+
"""
173+
@wraps(wrapped_function)
174+
def new_function(*args, **kwargs):
175+
set_code_owner_attribute_from_module(wrapped_function.__module__)
176+
return wrapped_function(*args, **kwargs)
177+
return new_function
178+
179+
106180
def clear_cached_mappings():
107181
"""
108182
Clears the cached path to code owner mappings. Useful for testing.
@@ -111,6 +185,6 @@ def clear_cached_mappings():
111185
_PATH_TO_CODE_OWNER_MAPPINGS = None
112186

113187

114-
# TODO: Remove this LMS specific configuration by replacing with a Django Setting named
115-
# CODE_OWNER_OPTIONAL_MODULE_PREFIXES that takes a list of module prefixes (without the final period).
188+
# TODO: Retire this once edx-platform import_shims is no longer used.
189+
# See https://github.com/edx/edx-platform/tree/854502b560bda74ef898501bb2a95ce238cf794c/import_shims
116190
_OPTIONAL_MODULE_PREFIX_PATTERN = re.compile(r'^(lms|common|openedx\.core)\.djangoapps\.')

0 commit comments

Comments
 (0)