Commit 076c63bc by Troy Sankey

Instrument DjangoXBlockUserStateClient with New Relic

Report to New Relic certain per-request details about the
DjangoXBlockUserStateClient.  The following metrics are reported for the
get_many() call:

xb_user_state.get_many.calls
xb_user_state.get_many.duration
xb_user_state.get_many.blocks_requested
xb_user_state.get_many.blocks_out
xb_user_state.get_many.size
xb_user_state.get_many.<block_type>.blocks_requested
xb_user_state.get_many.<block_type>.blocks_out
xb_user_state.get_many.<block_type>.size

Similarly, for the set_many() call:

xb_user_state.set_many.calls
xb_user_state.set_many.duration
xb_user_state.set_many.blocks_created
xb_user_state.set_many.blocks_updated
xb_user_state.set_many.size
xb_user_state.set_many.<block_type>.blocks_created
xb_user_state.set_many.<block_type>.blocks_updated
xb_user_state.set_many.<block_type>.size

Where <block_type> is one of "chapter", "course", "problem", "video",
etc.

PERF-354
parent ece785ff
...@@ -12,6 +12,7 @@ try: ...@@ -12,6 +12,7 @@ try:
except ImportError: except ImportError:
import json import json
import newrelic_custom_metrics
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
...@@ -120,6 +121,51 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -120,6 +121,51 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
sample_rate=self.API_DATADOG_SAMPLE_RATE, sample_rate=self.API_DATADOG_SAMPLE_RATE,
) )
def _nr_metric_name(self, function_name, stat_name, block_type=None):
"""
Return a metric name (string) representing the provided descriptors.
The return value is directly usable for custom NR metrics.
"""
if block_type is None:
metric_name_parts = ['xb_user_state', function_name, stat_name]
else:
metric_name_parts = ['xb_user_state', function_name, block_type, stat_name]
return '.'.join(metric_name_parts)
def _nr_stat_accumulate(self, function_name, stat_name, value):
"""
Accumulate arbitrary NR stats (not specific to block types).
"""
newrelic_custom_metrics.accumulate(
self._nr_metric_name(function_name, stat_name),
value
)
def _nr_stat_increment(self, function_name, stat_name, count=1):
"""
Increment arbitrary NR stats (not specific to block types).
"""
self._nr_stat_accumulate(function_name, stat_name, count)
def _nr_block_stat_accumulate(self, function_name, block_type, stat_name, value):
"""
Accumulate NR stats related to block types.
"""
newrelic_custom_metrics.accumulate(
self._nr_metric_name(function_name, stat_name),
value,
)
newrelic_custom_metrics.accumulate(
self._nr_metric_name(function_name, stat_name, block_type=block_type),
value,
)
def _nr_block_stat_increment(self, function_name, block_type, stat_name, count=1):
"""
Increment NR stats related to block types.
"""
self._nr_block_stat_accumulate(function_name, block_type, stat_name, count)
def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): def get_many(self, username, block_keys, scope=Scope.user_state, fields=None):
""" """
Retrieve the stored XBlock state for the specified XBlock usages. Retrieve the stored XBlock state for the specified XBlock usages.
...@@ -137,10 +183,15 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -137,10 +183,15 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
if scope != Scope.user_state: if scope != Scope.user_state:
raise ValueError("Only Scope.user_state is supported, not {}".format(scope)) raise ValueError("Only Scope.user_state is supported, not {}".format(scope))
block_count = state_length = 0 total_block_count = 0
evt_time = time() evt_time = time()
# count how many times this function gets called
self._nr_stat_increment('get_many', 'calls')
# keep track of blocks requested
self._ddog_histogram(evt_time, 'get_many.blks_requested', len(block_keys)) self._ddog_histogram(evt_time, 'get_many.blks_requested', len(block_keys))
self._nr_stat_accumulate('get_many', 'blocks_requested', len(block_keys))
modules = self._get_student_modules(username, block_keys) modules = self._get_student_modules(username, block_keys)
for module, usage_key in modules: for module, usage_key in modules:
...@@ -149,29 +200,38 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -149,29 +200,38 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
continue continue
state = json.loads(module.state) state = json.loads(module.state)
state_length += len(module.state) state_length = len(module.state)
self._ddog_histogram(evt_time, 'get_many.block_size', len(module.state)) # record this metric before the check for empty state, so that we
# have some visibility into empty blocks.
self._ddog_histogram(evt_time, 'get_many.block_size', state_length)
# If the state is the empty dict, then it has been deleted, and so # If the state is the empty dict, then it has been deleted, and so
# conformant UserStateClients should treat it as if it doesn't exist. # conformant UserStateClients should treat it as if it doesn't exist.
if state == {}: if state == {}:
continue continue
# collect statistics for metric reporting
self._nr_block_stat_increment('get_many', usage_key.block_type, 'blocks_out')
self._nr_block_stat_accumulate('get_many', usage_key.block_type, 'size', state_length)
total_block_count += 1
# filter state on fields
if fields is not None: if fields is not None:
state = { state = {
field: state[field] field: state[field]
for field in fields for field in fields
if field in state if field in state
} }
block_count += 1
yield XBlockUserState(username, usage_key, state, module.modified, scope) yield XBlockUserState(username, usage_key, state, module.modified, scope)
# The rest of this method exists only to submit DataDog events. # The rest of this method exists only to report metrics.
# Remove it once we're no longer interested in the data.
finish_time = time() finish_time = time()
self._ddog_histogram(evt_time, 'get_many.blks_out', block_count) duration = (finish_time - evt_time) * 1000 # milliseconds
self._ddog_histogram(evt_time, 'get_many.response_time', (finish_time - evt_time) * 1000)
self._ddog_histogram(evt_time, 'get_many.blks_out', total_block_count)
self._ddog_histogram(evt_time, 'get_many.response_time', duration)
self._nr_stat_accumulate('get_many', 'duration', duration)
def set_many(self, username, block_keys_to_state, scope=Scope.user_state): def set_many(self, username, block_keys_to_state, scope=Scope.user_state):
""" """
...@@ -188,6 +248,9 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -188,6 +248,9 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
if scope != Scope.user_state: if scope != Scope.user_state:
raise ValueError("Only Scope.user_state is supported") raise ValueError("Only Scope.user_state is supported")
# count how many times this function gets called
self._nr_stat_increment('set_many', 'calls')
# We do a find_or_create for every block (rather than re-using field objects # We do a find_or_create for every block (rather than re-using field objects
# that were queried in get_many) so that if the score has # that were queried in get_many) so that if the score has
# been changed by some other piece of the code, we don't overwrite # been changed by some other piece of the code, we don't overwrite
...@@ -240,14 +303,18 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -240,14 +303,18 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
len(block_keys_to_state), block_keys_to_state.keys() len(block_keys_to_state), block_keys_to_state.keys()
)) ))
# The rest of this method exists only to submit DataDog events. # DataDog and New Relic reporting
# Remove it once we're no longer interested in the data.
# # record the size of state modifications
self._nr_block_stat_accumulate('set_many', usage_key.block_type, 'size', len(student_module.state))
# Record whether a state row has been created or updated. # Record whether a state row has been created or updated.
if created: if created:
self._ddog_increment(evt_time, 'set_many.state_created') self._ddog_increment(evt_time, 'set_many.state_created')
self._nr_block_stat_increment('set_many', usage_key.block_type, 'blocks_created')
else: else:
self._ddog_increment(evt_time, 'set_many.state_updated') self._ddog_increment(evt_time, 'set_many.state_updated')
self._nr_block_stat_increment('set_many', usage_key.block_type, 'blocks_updated')
# Event to record number of fields sent in to set/set_many. # Event to record number of fields sent in to set/set_many.
self._ddog_histogram(evt_time, 'set_many.fields_in', len(state)) self._ddog_histogram(evt_time, 'set_many.fields_in', len(state))
...@@ -262,8 +329,10 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): ...@@ -262,8 +329,10 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient):
# Events for the entire set_many call. # Events for the entire set_many call.
finish_time = time() finish_time = time()
duration = (finish_time - evt_time) * 1000 # milliseconds
self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state)) self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state))
self._ddog_histogram(evt_time, 'set_many.response_time', (finish_time - evt_time) * 1000) self._ddog_histogram(evt_time, 'set_many.response_time', duration)
self._nr_stat_accumulate('set_many', 'duration', duration)
def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None): def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment