Skip to content

Commit

Permalink
update vxsandbox metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
TrevorOctober committed May 25, 2016
1 parent 22897dd commit c98074d
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 373 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ lib64/
parts/
sdist/
var/
_trial_temp/
*.egg-info/
.installed.cfg
*.egg

.DS_Store
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
Expand Down
93 changes: 93 additions & 0 deletions vxsandbox/resources/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- test-case-name: go.apps.jsbox.tests.test_metrics -*-
# -*- coding: utf-8 -*-

"""Metrics for JS Box sandboxes"""

import re

from vxsandbox import SandboxResource

from vumi.blinkenlights.metrics import SUM, AVG, MIN, MAX, LAST


class MetricEventError(Exception):
"""Raised when a command cannot be converted to a metric event."""


class MetricEvent(object):

AGGREGATORS = {
'sum': SUM,
'avg': AVG,
'min': MIN,
'max': MAX,
'last': LAST
}

NAME_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9._-]{,100}$")

def __init__(self, store, metric, value, agg):
self.store = store
self.metric = metric
self.value = value
self.agg = agg

def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return all((self.store == other.store, self.metric == other.metric,
self.value == other.value, self.agg is other.agg))

@classmethod
def _parse_name(cls, name, kind):
if name is None:
raise MetricEventError("Missing %s name." % (kind,))
if not isinstance(name, basestring):
raise MetricEventError("Invalid type for %s name: %r"
% (kind, name))
if not cls.NAME_REGEX.match(name):
raise MetricEventError("Invalid %s name: %r." % (kind, name))
return name

@classmethod
def _parse_value(cls, value):
try:
value = float(value)
except (ValueError, TypeError):
raise MetricEventError("Invalid metric value %r." % (value,))
return value

@classmethod
def _parse_agg(cls, agg):
if not isinstance(agg, basestring):
raise MetricEventError("Invalid metric aggregator %r" % (agg,))
if agg not in cls.AGGREGATORS:
raise MetricEventError("Invalid metric aggregator %r." % (agg,))
return cls.AGGREGATORS[agg]

@classmethod
def from_command(cls, command):
store = cls._parse_name(command.get('store', 'default'), 'store')
metric = cls._parse_name(command.get('metric'), 'metric')
value = cls._parse_value(command.get('value'))
agg = cls._parse_agg(command.get('agg'))
return cls(store, metric, value, agg)


class MetricsResource(SandboxResource):
"""Resource that provides metric storing."""

def _publish_event(self, api, ev):
conversation = self.app_worker.conversation_for_api(api)
self.app_worker.publish_account_metric(conversation.user_account.key,
ev.store, ev.metric, ev.value,
ev.agg)

def handle_fire(self, api, command):
"""Fire a metric value."""
try:
ev = MetricEvent.from_command(command)
except MetricEventError, e:
return self.reply(command, success=False, reason=unicode(e))
self._publish_event(api, ev)
return self.reply(command, success=True)
137 changes: 0 additions & 137 deletions vxsandbox/resources/metrics_worker.py

This file was deleted.

136 changes: 136 additions & 0 deletions vxsandbox/resources/tests/test_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Tests for go.apps.jsbox.metrics."""

import mock

from vxsandbox.resources import SandboxCommand

from vumi.tests.helpers import VumiTestCase

from vxsandbox.resources.metrics import (
MetricEvent, MetricEventError, MetricsResource)


class TestMetricEvent(VumiTestCase):

SUM = MetricEvent.AGGREGATORS['sum']

def test_create(self):
ev = MetricEvent('mystore', 'foo', 2.0, self.SUM)
self.assertEqual(ev.store, 'mystore')
self.assertEqual(ev.metric, 'foo')
self.assertEqual(ev.value, 2.0)
self.assertEqual(ev.agg, self.SUM)

def test_eq(self):
ev1 = MetricEvent('mystore', 'foo', 1.5, self.SUM)
ev2 = MetricEvent('mystore', 'foo', 1.5, self.SUM)
self.assertEqual(ev1, ev2)

def test_neq(self):
ev1 = MetricEvent('mystore', 'foo', 1.5, self.SUM)
ev2 = MetricEvent('mystore', 'bar', 1.5, self.SUM)
self.assertNotEqual(ev1, ev2)

def test_from_command(self):
ev = MetricEvent.from_command({'store': 'mystore', 'metric': 'foo',
'value': 1.5, 'agg': 'sum'})
self.assertEqual(ev, MetricEvent('mystore', 'foo', 1.5, self.SUM))

def test_bad_store(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'foo bar', 'metric': 'foo', 'value': 1.5,
'agg': 'sum'})

def test_bad_type_store(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': {}, 'metric': 'foo', 'value': 1.5,
'agg': 'sum'})

def test_bad_metric(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo bar', 'value': 1.5,
'agg': 'sum'})

def test_bad_type_metric(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': {}, 'value': 1.5,
'agg': 'sum'})

def test_missing_metric(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'value': 1.5, 'agg': 'sum'})

def test_bad_value(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'value': 'abc',
'agg': 'sum'})

def test_bad_type_value(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'value': {},
'agg': 'sum'})

def test_missing_value(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'agg': 'sum'})

def test_bad_agg(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'value': 1.5,
'agg': 'foo'})

def test_bad_type_agg(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'value': 1.5,
'agg': {}})

def test_missing_agg(self):
self.assertRaises(MetricEventError, MetricEvent.from_command, {
'store': 'mystore', 'metric': 'foo', 'value': 1.5})


class TestMetricsResource(VumiTestCase):

SUM = MetricEvent.AGGREGATORS['sum']

def setUp(self):
self.conversation = mock.Mock()
self.app_worker = mock.Mock()
self.dummy_api = object()
self.resource = MetricsResource("test", self.app_worker, {})
self.app_worker.conversation_for_api = mock.Mock(
return_value=self.conversation)

def check_reply(self, reply, cmd, success):
self.assertEqual(reply['reply'], True)
self.assertEqual(reply['cmd_id'], cmd['cmd_id'])
self.assertEqual(reply['success'], success)

def check_publish(self, store, metric, value, agg):
self.app_worker.publish_account_metric.assert_called_once_with(
self.conversation.user_account.key, store, metric, value, agg)

def check_not_published(self):
self.assertFalse(self.app_worker.publish_account_metric.called)

def test_handle_fire(self):
cmd = SandboxCommand(metric="foo", value=1.5, agg='sum')
reply = self.resource.handle_fire(self.dummy_api, cmd)
self.check_reply(reply, cmd, True)
self.check_publish('default', 'foo', 1.5, self.SUM)

def _test_error(self, cmd, expected_error):
reply = self.resource.handle_fire(self.dummy_api, cmd)
self.check_reply(reply, cmd, False)
self.assertEqual(reply['reason'], expected_error)
self.check_not_published()

def test_handle_fire_error(self):
cmd = SandboxCommand(metric="foo bar", value=1.5, agg='sum')
expected_error = "Invalid metric name: 'foo bar'."
self._test_error(cmd, expected_error)

def test_non_ascii_metric_name_error(self):
cmd = SandboxCommand(metric=u"b\xe6r", value=1.5, agg='sum')
expected_error = "Invalid metric name: u'b\\xe6r'."
self._test_error(cmd, expected_error)
Loading

0 comments on commit c98074d

Please sign in to comment.