From f3e59acbf14fa737755e0f1f1839f6f113781464 Mon Sep 17 00:00:00 2001 From: John Eskew Date: Fri, 31 Jul 2015 11:52:53 -0400 Subject: [PATCH] Add DataDog histogram events to DjangoXBlockUserStateClient class. --- .../courseware/user_state_client.py | 71 +++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/courseware/user_state_client.py b/lms/djangoapps/courseware/user_state_client.py index 008dd914dc..4c45b09ddc 100644 --- a/lms/djangoapps/courseware/user_state_client.py +++ b/lms/djangoapps/courseware/user_state_client.py @@ -5,20 +5,18 @@ data in a Django ORM model. import itertools from operator import attrgetter +from time import time try: import simplejson as json except ImportError: import json +import dogstats_wrapper as dog_stats_api from django.contrib.auth.models import User from xblock.fields import Scope, ScopeBase -from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState from courseware.models import StudentModule, StudentModuleHistory -from contracts import contract, new_contract -from opaque_keys.edx.keys import UsageKey - -new_contract('UsageKey', UsageKey) +from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState class DjangoXBlockUserStateClient(XBlockUserStateClient): @@ -41,6 +39,9 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): even though the actual stored state in the database will be ``"{}"``). """ + # Use this sample rate for DataDog events. + API_DATADOG_SAMPLE_RATE = 0.01 + class ServiceUnavailable(XBlockUserStateClient.ServiceUnavailable): """ This error is raised if the service backing this client is currently unavailable. @@ -94,6 +95,27 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): usage_key = student_module.module_state_key.map_into_course(student_module.course_id) yield (student_module, usage_key) + def _ddog_increment(self, evt_time, evt_name): + """ + DataDog increment method. + """ + dog_stats_api.increment( + 'DjangoXBlockUserStateClient.{}'.format(evt_name), + timestamp=evt_time, + sample_rate=self.API_DATADOG_SAMPLE_RATE, + ) + + def _ddog_histogram(self, evt_time, evt_name, value): + """ + DataDog histogram method. + """ + dog_stats_api.histogram( + 'DjangoXBlockUserStateClient.{}'.format(evt_name), + value, + timestamp=evt_time, + sample_rate=self.API_DATADOG_SAMPLE_RATE, + ) + def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): """ Retrieve the stored XBlock state for the specified XBlock usages. @@ -111,12 +133,16 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): if scope != Scope.user_state: raise ValueError("Only Scope.user_state is supported, not {}".format(scope)) + block_count = state_length = 0 + evt_time = time() + modules = self._get_student_modules(username, block_keys) for module, usage_key in modules: if module.state is None: continue state = json.loads(module.state) + state_length += len(module.state) # 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. @@ -129,8 +155,14 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): for field in fields if field in state } + block_count += 1 yield XBlockUserState(username, usage_key, state, module.modified, scope) + # The rest of this method exists only to submit DataDog events. + # Remove it once we're no longer interested in the data. + self._ddog_histogram(evt_time, 'get_many.blks_out', block_count) + self._ddog_histogram(evt_time, 'get_many.blks_size', state_length) + def set_many(self, username, block_keys_to_state, scope=Scope.user_state): """ Set fields for a particular XBlock. @@ -155,6 +187,8 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): else: user = User.objects.get(username=username) + evt_time = time() + for usage_key, state in block_keys_to_state.items(): student_module, created = StudentModule.objects.get_or_create( student=user, @@ -166,16 +200,43 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): }, ) + num_fields_before = num_fields_after = num_new_fields_set = len(state) + num_fields_updated = 0 if not created: if student_module.state is None: current_state = {} else: current_state = json.loads(student_module.state) + num_fields_before = len(current_state) current_state.update(state) + num_fields_after = len(current_state) student_module.state = json.dumps(current_state) # We just read this object, so we know that we can do an update student_module.save(force_update=True) + # The rest of this method exists only to submit DataDog events. + # Remove it once we're no longer interested in the data. + # + # Record whether a state row has been created or updated. + if created: + self._ddog_increment(evt_time, 'set_many.state_created') + else: + self._ddog_increment(evt_time, 'set_many.state_updated') + + # Event to record number of fields sent in to set/set_many. + self._ddog_histogram(evt_time, 'set_many.fields_in', len(state)) + + # Event to record number of new fields set in set/set_many. + num_new_fields_set = num_fields_after - num_fields_before + self._ddog_histogram(evt_time, 'set_many.fields_set', num_new_fields_set) + + # Event to record number of existing fields updated in set/set_many. + num_fields_updated = max(0, len(state) - num_new_fields_set) + self._ddog_histogram(evt_time, 'set_many.fields_updated', num_fields_updated) + + # Event for the entire set_many call. + self._ddog_histogram(evt_time, 'set_many.blks_updated', len(block_keys_to_state)) + def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None): """ Delete the stored XBlock state for a many xblock usages.