diff --git a/common/djangoapps/newrelic_custom_metrics/middleware.py b/common/djangoapps/newrelic_custom_metrics/middleware.py index 3af8cb5..3d6bcd8 100644 --- a/common/djangoapps/newrelic_custom_metrics/middleware.py +++ b/common/djangoapps/newrelic_custom_metrics/middleware.py @@ -6,7 +6,14 @@ This middleware will only call on the newrelic agent if there are any metrics to report for this request, so it will not incur any processing overhead for request handlers which do not record custom metrics. """ -import newrelic.agent +import logging +log = logging.getLogger(__name__) +try: + import newrelic.agent +except ImportError: + log.warning("Unable to load NewRelic agent module") + newrelic = None # pylint: disable=invalid-name + import request_cache REQUEST_CACHE_KEY = 'newrelic_custom_metrics' @@ -40,6 +47,8 @@ class NewRelicCustomMetrics(object): """ Report the collected custom metrics to New Relic. """ + if not newrelic: + return metrics_cache = cls._get_metrics_cache() for metric_name, metric_value in metrics_cache.iteritems(): newrelic.agent.add_custom_parameter(metric_name, metric_value) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 8233f2a..ecda5cc 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -14,7 +14,6 @@ from lxml import etree from xblock.core import XBlock from xblock.fields import Integer, Scope, Boolean, String from xblock.fragment import Fragment -import newrelic.agent from .exceptions import NotFoundError from .fields import Date @@ -25,6 +24,11 @@ from .xml_module import XmlDescriptor log = logging.getLogger(__name__) +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name + # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] @@ -385,6 +389,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): """ Capture basic information about this sequence in New Relic. """ + if not newrelic: + return newrelic.agent.add_custom_parameter('seq.block_id', unicode(self.location)) newrelic.agent.add_custom_parameter('seq.display_name', self.display_name or '') newrelic.agent.add_custom_parameter('seq.position', self.position) @@ -396,6 +402,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): the sequence as a whole. We send this information to New Relic so that we can do better performance analysis of courseware. """ + if not newrelic: + return # Basic count of the number of Units (a.k.a. VerticalBlocks) we have in # this learning sequence newrelic.agent.add_custom_parameter('seq.num_units', len(display_items)) @@ -414,6 +422,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): """ Capture information about the current selected Unit within the Sequence. """ + if not newrelic: + return # Positions are stored with indexing starting at 1. If we get into a # weird state where the saved position is out of bounds (e.g. the # content was changed), avoid going into any details about this unit. diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0cef0cb..beb4dd0 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -8,7 +8,6 @@ import logging from collections import OrderedDict from functools import partial -import newrelic.agent from capa.xqueue_interface import XQueueInterface from django.conf import settings from django.contrib.auth.models import User @@ -82,6 +81,10 @@ from .field_overrides import OverrideFieldData log = logging.getLogger(__name__) +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name if settings.XQUEUE_INTERFACE.get('basic_auth') is not None: REQUESTS_AUTH = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth']) @@ -966,9 +969,10 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course except InvalidKeyError: raise Http404 - # Gather metrics for New Relic so we can slice data in New Relic Insights - newrelic.agent.add_custom_parameter('course_id', unicode(course_key)) - newrelic.agent.add_custom_parameter('org', unicode(course_key.org)) + if newrelic: + # Gather metrics for New Relic so we can slice data in New Relic Insights + newrelic.agent.add_custom_parameter('course_id', unicode(course_key)) + newrelic.agent.add_custom_parameter('org', unicode(course_key.org)) with modulestore().bulk_operations(course_key): instance, tracking_context = get_module_by_usage_id(request, course_id, usage_id, course=course) @@ -978,7 +982,8 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course # "handler" in those cases is always just "xmodule_handler". nr_tx_name = "{}.{}".format(instance.__class__.__name__, handler) nr_tx_name += "/{}".format(suffix) if (suffix and handler == "xmodule_handler") else "" - newrelic.agent.set_transaction_name(nr_tx_name, group="Python/XBlock/Handler") + if newrelic: + newrelic.agent.set_transaction_name(nr_tx_name, group="Python/XBlock/Handler") tracking_context_name = 'module_callback_handler' req = django_to_webob_request(request) diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index e07f055..3a93d4e 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -19,7 +19,14 @@ from django.shortcuts import redirect from courseware.url_helpers import get_redirect_url_for_global_staff from edxmako.shortcuts import render_to_response, render_to_string import logging -import newrelic.agent + +log = logging.getLogger("edx.courseware.views.index") + +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name + import urllib from xblock.fragment import Fragment @@ -54,7 +61,6 @@ from ..module_render import toc_for_course, get_module_for_descriptor from .views import get_current_child, registered_for_course -log = logging.getLogger("edx.courseware.views.index") TEMPLATE_IMPORTS = {'urllib': urllib} CONTENT_DEPTH = 2 @@ -174,6 +180,8 @@ class CoursewareIndex(View): """ Initialize metrics for New Relic so we can slice data in New Relic Insights """ + if not newrelic: + return newrelic.agent.add_custom_parameter('course_id', unicode(self.course_key)) newrelic.agent.add_custom_parameter('org', unicode(self.course_key.org)) diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index db5df8d..6822503 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -17,7 +17,13 @@ from django.shortcuts import render_to_response from django.template.loader import render_to_string from django.utils.translation import get_language_bidi from django.views.decorators.http import require_GET -import newrelic.agent + +log = logging.getLogger("edx.discussions") +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name + from rest_framework import status from web_fragments.fragment import Fragment @@ -50,13 +56,27 @@ import lms.lib.comment_client as cc from opaque_keys.edx.keys import CourseKey +from contextlib import contextmanager + THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20 PAGES_NEARBY_DELTA = 2 -log = logging.getLogger("edx.discussions") -@newrelic.agent.function_trace() +@contextmanager +def newrelic_function_trace(function_name): + """ + A wrapper context manager newrelic.agent.FunctionTrace to no-op if the + newrelic package is not installed + """ + if newrelic: + nr_transaction = newrelic.agent.current_transaction() + with newrelic.agent.FunctionTrace(nr_transaction, function_name): + yield + else: + yield + + def make_course_settings(course, user): """ Generate a JSON-serializable model for course settings, which will be used to initialize a @@ -71,7 +91,6 @@ def make_course_settings(course, user): } -@newrelic.agent.function_trace() def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError @@ -184,7 +203,6 @@ def inline_discussion(request, course_key, discussion_id): """ Renders JSON for DiscussionModules """ - nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) cc_user = cc.User.from_django_user(request.user) @@ -195,12 +213,14 @@ def inline_discussion(request, course_key, discussion_id): except ValueError: return HttpResponseServerError("Invalid group_id") - with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): + with newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) + is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] - with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): + with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) + return utils.JsonResponse({ 'is_commentable_cohorted': is_commentable_cohorted(course_key, discussion_id), 'discussion_data': threads, @@ -219,8 +239,6 @@ def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ - nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) if request.is_ajax(): user = cc.User.from_django_user(request.user) @@ -235,10 +253,10 @@ def forum_form_discussion(request, course_key): except ValueError: return HttpResponseServerError("Invalid group_id") - with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): + with newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) - with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): + with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) return utils.JsonResponse({ @@ -265,8 +283,6 @@ def single_thread(request, course_key, discussion_id, thread_id): Depending on the HTTP headers, we'll adjust our response accordingly. """ - nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) if request.is_ajax(): @@ -278,7 +294,7 @@ def single_thread(request, course_key, discussion_id, thread_id): if not thread: raise Http404 - with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): + with newrelic_function_trace("get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, @@ -287,7 +303,7 @@ def single_thread(request, course_key, discussion_id, thread_id): ) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) - with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): + with newrelic_function_trace("add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ @@ -372,7 +388,6 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th """ Returns the template context for rendering the discussion board. """ - nr_transaction = newrelic.agent.current_transaction() context = _create_base_discussion_view_context(request, course_key) course = context['course'] course_settings = context['course_settings'] @@ -401,13 +416,13 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th is_staff = has_permission(user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] - with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): + with newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, user, user_info) - with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): + with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, user) - with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): + with newrelic_function_trace("get_cohort_info"): user_cohort_id = get_cohort_id(user, course_key) context.update({ @@ -435,9 +450,6 @@ def user_profile(request, course_key, user_id): Renders a response to display the user profile page (shown after clicking on a post author's username). """ - - nr_transaction = newrelic.agent.current_transaction() - user = cc.User.from_django_user(request.user) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) @@ -466,13 +478,13 @@ def user_profile(request, course_key, user_id): query_params['page'] = page query_params['num_pages'] = num_pages - with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): + with newrelic_function_trace("get_metadata_for_threads"): user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] - with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): + with newrelic_function_trace("add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ @@ -486,7 +498,7 @@ def user_profile(request, course_key, user_id): course_id=course.id ).order_by("name").values_list("name", flat=True).distinct() - with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): + with newrelic_function_trace("get_cohort_info"): user_cohort_id = get_cohort_id(request.user, course_key) context = _create_base_discussion_view_context(request, course_key) @@ -514,9 +526,6 @@ def followed_threads(request, course_key, user_id): """ Ajax-only endpoint retrieving the threads followed by a specific user. """ - - nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) try: profiled_user = cc.User(id=user_id, course_id=course_key) @@ -557,7 +566,7 @@ def followed_threads(request, course_key, user_id): query_params['num_pages'] = paginated_results.num_pages user_info = cc.User.from_django_user(request.user).to_dict() - with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): + with newrelic_function_trace("get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, paginated_results.collection, diff --git a/lms/djangoapps/grades/tasks.py b/lms/djangoapps/grades/tasks.py index 0359dee..b4c51fd 100644 --- a/lms/djangoapps/grades/tasks.py +++ b/lms/djangoapps/grades/tasks.py @@ -9,7 +9,12 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db.utils import DatabaseError from logging import getLogger -import newrelic.agent + +log = getLogger(__name__) +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name from celery_utils.logged_task import LoggedTask from celery_utils.persist_on_failure import PersistOnFailureTask @@ -31,8 +36,6 @@ from .new.subsection_grade import SubsectionGradeFactory from .signals.signals import SUBSECTION_SCORE_CHANGED from .transformer import GradesTransformer -log = getLogger(__name__) - class DatabaseNotReadyError(IOError): """ @@ -96,8 +99,9 @@ def _recalculate_subsection_grade(self, **kwargs): scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) - newrelic.agent.add_custom_parameter('course_id', unicode(course_key)) - newrelic.agent.add_custom_parameter('usage_id', unicode(scored_block_usage_key)) + if newrelic: + newrelic.agent.add_custom_parameter('course_id', unicode(course_key)) + newrelic.agent.add_custom_parameter('usage_id', unicode(scored_block_usage_key)) # The request cache is not maintained on celery workers, # where this code runs. So we take the values from the diff --git a/openedx/core/djangoapps/contentserver/middleware.py b/openedx/core/djangoapps/contentserver/middleware.py index 9fb41c0..8f6fe3e 100644 --- a/openedx/core/djangoapps/contentserver/middleware.py +++ b/openedx/core/djangoapps/contentserver/middleware.py @@ -4,7 +4,11 @@ Middleware to serve assets. import logging import datetime -import newrelic.agent +log = logging.getLogger(__name__) +try: + import newrelic.agent +except ImportError: + newrelic = None # pylint: disable=invalid-name from django.http import ( HttpResponse, HttpResponseNotModified, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseNotFound, HttpResponsePermanentRedirect) @@ -25,7 +29,6 @@ from .models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig # TODO: Soon as we have a reasonable way to serialize/deserialize AssetKeys, we need # to change this file so instead of using course_id_partial, we're just using asset keys -log = logging.getLogger(__name__) HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" @@ -86,17 +89,18 @@ class StaticContentServer(object): if safe_course_key.run is None: safe_course_key = safe_course_key.replace(run='only') - newrelic.agent.add_custom_parameter('course_id', safe_course_key) - newrelic.agent.add_custom_parameter('org', loc.org) - newrelic.agent.add_custom_parameter('contentserver.path', loc.path) + if newrelic: + newrelic.agent.add_custom_parameter('course_id', safe_course_key) + newrelic.agent.add_custom_parameter('org', loc.org) + newrelic.agent.add_custom_parameter('contentserver.path', loc.path) - # Figure out if this is a CDN using us as the origin. - is_from_cdn = StaticContentServer.is_cdn_request(request) - newrelic.agent.add_custom_parameter('contentserver.from_cdn', is_from_cdn) + # Figure out if this is a CDN using us as the origin. + is_from_cdn = StaticContentServer.is_cdn_request(request) + newrelic.agent.add_custom_parameter('contentserver.from_cdn', is_from_cdn) - # Check if this content is locked or not. - locked = self.is_content_locked(content) - newrelic.agent.add_custom_parameter('contentserver.locked', locked) + # Check if this content is locked or not. + locked = self.is_content_locked(content) + newrelic.agent.add_custom_parameter('contentserver.locked', locked) # Check that user has access to the content. if not self.is_user_authorized(request, content, loc): @@ -153,7 +157,8 @@ class StaticContentServer(object): response['Content-Length'] = str(last - first + 1) response.status_code = 206 # Partial Content - newrelic.agent.add_custom_parameter('contentserver.ranged', True) + if newrelic: + newrelic.agent.add_custom_parameter('contentserver.ranged', True) else: log.warning( u"Cannot satisfy ranges in Range header: %s for content: %s", header_value, unicode(loc) @@ -165,8 +170,9 @@ class StaticContentServer(object): response = HttpResponse(content.stream_data()) response['Content-Length'] = content.length - newrelic.agent.add_custom_parameter('contentserver.content_len', content.length) - newrelic.agent.add_custom_parameter('contentserver.content_type', content.content_type) + if newrelic: + newrelic.agent.add_custom_parameter('contentserver.content_len', content.length) + newrelic.agent.add_custom_parameter('contentserver.content_type', content.content_type) # "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed response['Accept-Ranges'] = 'bytes' @@ -194,12 +200,14 @@ class StaticContentServer(object): # indicate there should be no caching whatsoever. cache_ttl = CourseAssetCacheTtlConfig.get_cache_ttl() if cache_ttl > 0 and not is_locked: - newrelic.agent.add_custom_parameter('contentserver.cacheable', True) + if newrelic: + newrelic.agent.add_custom_parameter('contentserver.cacheable', True) response['Expires'] = StaticContentServer.get_expiration_value(datetime.datetime.utcnow(), cache_ttl) response['Cache-Control'] = "public, max-age={ttl}, s-maxage={ttl}".format(ttl=cache_ttl) elif is_locked: - newrelic.agent.add_custom_parameter('contentserver.cacheable', False) + if newrelic: + newrelic.agent.add_custom_parameter('contentserver.cacheable', False) response['Cache-Control'] = "private, no-cache, no-store"