Repository: DataDog/datadogpy

Issue:
Prefix support
Please, add prefix support like Java and C#:
https://github.com/DataDog/dogstatsd-csharp-client/blob/master/src/StatsdClient/StatsdConfig.cs#L8
https://github.com/indeedeng/java-dogstatsd-client/blob/master/src/main/java/com/timgroup/statsd/NonBlockingStatsDClient.java#L120

Discussion:
yannmh: @JohnLZeller can you take a pass on it ?
JohnLZeller: :+1: looks good once conflicts are resolved.

Patch:
diff --git a/datadog/dogstatsd/base.py b/datadog/dogstatsd/base.py
index 7e0e11d..2f7725c 100644
--- a/datadog/dogstatsd/base.py
+++ b/datadog/dogstatsd/base.py
@@ -22,7 +22,7 @@ log = logging.getLogger('dogstatsd')
 class DogStatsd(object):
     OK, WARNING, CRITICAL, UNKNOWN = (0, 1, 2, 3)
 
-    def __init__(self, host='localhost', port=8125, max_buffer_size=50,
+    def __init__(self, host='localhost', port=8125, max_buffer_size=50, namespace=None,
                  constant_tags=None, use_ms=False):
         """
         Initialize a DogStatsd object.
@@ -39,7 +39,10 @@ class DogStatsd(object):
         if sending metrics in batch
         :type max_buffer_size: integer
 
-        :param constant_tags: Tags to attach to every metric reported by this client
+        :param namepace: Namespace to prefix all metric names
+        :type namepace: string
+
+        :param constant_tags: Tags to attach to all metrics
         :type constant_tags: list of strings
 
         :param use_ms: Report timed values in milliseconds instead of seconds (default False)
@@ -58,6 +61,7 @@ class DogStatsd(object):
         if constant_tags is None:
             constant_tags = []
         self.constant_tags = constant_tags + env_tags
+        self.namespace = namespace
         self.use_ms = use_ms
 
     def __enter__(self):
@@ -222,24 +226,37 @@ class DogStatsd(object):
         self._report(metric, 's', value, tags, sample_rate)
 
     def _report(self, metric, metric_type, value, tags, sample_rate):
+        """
+        Create a metric packet and send it.
+
+        More information about the packets' format: http://docs.datadoghq.com/guides/dogstatsd/
+        """
         if sample_rate != 1 and random() > sample_rate:
             return
 
-        payload = [metric, ":", value, "|", metric_type]
-        if sample_rate != 1:
-            payload.extend(["|@", sample_rate])
+        payload = []
 
-        # Append all client level tags to every metric
+        # Resolve the full tag list
         if self.constant_tags:
             if tags:
                 tags = tags + self.constant_tags
             else:
                 tags = self.constant_tags
 
+        # Create/format the metric packet
+        if self.namespace:
+            payload.extend([self.namespace, "."])
+        payload.extend([metric, ":", value, "|", metric_type])
+
+        if sample_rate != 1:
+            payload.extend(["|@", sample_rate])
+
         if tags:
             payload.extend(["|#", ",".join(tags)])
 
         encoded = "".join(imap(str, payload))
+
+        # Send it
         self._send(encoded)
 
     def _send_to_server(self, packet):
diff --git a/datadog/threadstats/base.py b/datadog/threadstats/base.py
index 89bbfc9..40aed00 100644
--- a/datadog/threadstats/base.py
+++ b/datadog/threadstats/base.py
@@ -23,7 +23,7 @@ log = logging.getLogger('dd.datadogpy')
 
 class ThreadStats(object):
 
-    def __init__(self, constant_tags=None):
+    def __init__(self, namespace="", constant_tags=None):
         """
         Initialize a dogstats object.
 
@@ -33,13 +33,16 @@ class ThreadStats(object):
         :envvar DATADOG_TAGS: Tags to attach to every metric reported by ThreadStats client
         :type constant_tags: list of strings
         """
-        # Don't collect until start is called.
-        self._disabled = True
+        # Parameters
+        self.namespace = namespace
         env_tags = [tag for tag in os.environ.get('DATADOG_TAGS', '').split(',') if tag]
         if constant_tags is None:
             constant_tags = []
         self.constant_tags = constant_tags + env_tags
 
+        # State
+        self._disabled = True
+
     def start(self, flush_interval=10, roll_up_interval=10, device=None,
               flush_in_thread=True, flush_in_greenlet=False, disabled=False):
         """
@@ -307,23 +310,31 @@ class ThreadStats(object):
             self._is_flush_in_progress = False
 
     def _get_aggregate_metrics(self, flush_time=None):
+        """
+        Get, format and return the rolled up metrics from the aggregator.
+        """
         # Get rolled up metrics
         rolled_up_metrics = self._metric_aggregator.flush(flush_time)
 
         # FIXME: emit a dictionary from the aggregator
         metrics = []
         for timestamp, value, name, tags, host in rolled_up_metrics:
-            # Append all client level tags to every metric
             metric_tags = tags
+            metric_name = name
 
+            # Append all client level tags to every metric
             if self.constant_tags:
                 if tags:
                     metric_tags = tags + self.constant_tags
                 else:
                     metric_tags = self.constant_tags
 
+            # Resolve the metric name
+            if self.namespace:
+                metric_name = self.namespace + "." + name
+
             metric = {
-                'metric': name,
+                'metric': metric_name,
                 'points': [[timestamp, value]],
                 'type': MetricType.Gauge,
                 'host': host,


Test patch:
diff --git a/tests/unit/dogstatsd/test_statsd.py b/tests/unit/dogstatsd/test_statsd.py
index dacf09d..8473b87 100644
--- a/tests/unit/dogstatsd/test_statsd.py
+++ b/tests/unit/dogstatsd/test_statsd.py
@@ -149,6 +149,14 @@ class TestDogStatsd(object):
             u'_sc|my_check.name|{0}|d:{1}|h:i-abcd1234|#key1:val1,key2:val2|m:{2}'
             .format(self.statsd.WARNING, now, u"♬ †øU \\n†øU ¥ºu|m\: T0µ ♪"), self.recv())
 
+    def test_metric_namespace(self):
+        """
+        Namespace prefixes all metric names.
+        """
+        self.statsd.namespace = "foo"
+        self.statsd.gauge('gauge', 123.4)
+        t.assert_equal('foo.gauge:123.4|g', self.recv())
+
     # Test Client level contant tags
     def test_gauge_constant_tags(self):
         self.statsd.constant_tags=['bar:baz', 'foo']
diff --git a/tests/unit/threadstats/test_threadstats.py b/tests/unit/threadstats/test_threadstats.py
index eaa0658..6f2e90b 100644
--- a/tests/unit/threadstats/test_threadstats.py
+++ b/tests/unit/threadstats/test_threadstats.py
@@ -2,18 +2,19 @@
 Tests for the ThreadStats class, using HTTP mode
 """
 
-import os
+# stdlib
 import logging
+import os
 import random
 import time
-import threading
+import unittest
 
+# 3p
+from mock import patch
 import nose.tools as nt
-from nose.plugins.skip import SkipTest
 
+# datadog
 from datadog import ThreadStats
-from datadog.api.exceptions import ApiNotInitialized
-
 from tests.util.contextmanagers import preserve_environment_variable
 
 
@@ -22,12 +23,10 @@ logger = logging.getLogger('dd.datadogpy')
 logger.setLevel(logging.ERROR)
 
 
-#
-# Test fixtures.
-#
-
 class MemoryReporter(object):
-    """ A reporting class that reports to memory for testing. """
+    """
+    A reporting class that reports to memory for testing.
+    """
 
     def __init__(self):
         self.metrics = []
@@ -40,14 +39,20 @@ class MemoryReporter(object):
         self.events += events
 
 
-#
-# Unit tests.
-#
-class TestUnitThreadStats(object):
-    """ Unit tests for the dog stats api. """
+class TestUnitThreadStats(unittest.TestCase):
+    """
+    Unit tests for ThreadStats.
+    """
+    def setUp(self):
+        """
+        Set a mocked reporter.
+        """
+        self.reporter = MemoryReporter()
 
     def sort_metrics(self, metrics):
-        """ Sort metrics by timestamp of first point and then name """
+        """
+        Sort metrics by timestamp of first point and then name.
+        """
         def sort(metric):
             tags = metric['tags'] or []
             host = metric['host'] or ''
@@ -55,6 +60,39 @@ class TestUnitThreadStats(object):
                     metric['points'][0][1])
         return sorted(metrics, key=sort)
 
+    def assertMetric(self, name=None, value=None, tags=None, count=None):
+        """
+        Helper, to make assertions on metrics.
+        """
+        matching_metrics = []
+
+        for metric in self.reporter.metrics:
+            if name and name != metric['metric']:
+                continue
+            if value and value != metric['points'][0][1]:
+                continue
+            if tags and tags != metric['tags']:
+                continue
+            matching_metrics.append(metric)
+
+        if count:
+            self.assertEquals(
+                len(matching_metrics), count,
+                u"Candidate size assertion failure: expected {expected}, found {count}. "
+                u"Metric name={name}, value={value}, tags={tags}.".format(
+                    expected=count, count=len(matching_metrics),
+                    name=name, value=value, tags=tags
+                )
+            )
+        else:
+            self.assertTrue(
+                len(matching_metrics) > 0,
+                u"Candidate size assertion failure: no matching metric found. "
+                u"Metric name={name}, value={value}, tags={tags}.".format(
+                    name=name, value=value, tags=tags
+                )
+            )
+
     def test_timed_decorator(self):
         dog = ThreadStats()
         dog.start(roll_up_interval=1, flush_in_thread=False)
@@ -393,51 +431,75 @@ class TestUnitThreadStats(object):
         nt.assert_equal(g3['points'][0][1], 20)
 
     def test_constant_tags(self):
-        dog = ThreadStats(constant_tags=['type:constant'])
-        dog.start(roll_up_interval=10, flush_in_thread=False)
-        reporter = dog.reporter = MemoryReporter()
+        """
+        Constant tags are attached to all metrics.
+        """
+        dog = ThreadStats(constant_tags=["type:constant"])
+        dog.start(roll_up_interval=1, flush_in_thread=False)
+        dog.reporter = self.reporter
 
         # Post the same metric with different tags.
-        dog.gauge('gauge', 10, timestamp=100.0)
-        dog.gauge('gauge', 15, timestamp=100.0, tags=['env:production', 'db'])
-        dog.gauge('gauge', 20, timestamp=100.0, tags=['env:staging'])
+        dog.gauge("gauge", 10, timestamp=100.0)
+        dog.gauge("gauge", 15, timestamp=100.0, tags=["env:production", 'db'])
+        dog.gauge("gauge", 20, timestamp=100.0, tags=["env:staging"])
 
-        dog.increment('counter', timestamp=100.0)
-        dog.increment('counter', timestamp=100.0, tags=['env:production', 'db'])
-        dog.increment('counter', timestamp=100.0, tags=['env:staging'])
+        dog.increment("counter", timestamp=100.0)
+        dog.increment("counter", timestamp=100.0, tags=["env:production", 'db'])
+        dog.increment("counter", timestamp=100.0, tags=["env:staging"])
 
         dog.flush(200.0)
 
-        metrics = self.sort_metrics(reporter.metrics)
-        nt.assert_equal(len(metrics), 6)
+        # Assertions on all metrics
+        self.assertMetric(count=6)
 
-        [c1, c2, c3, g1, g2, g3] = metrics
-        (nt.assert_equal(c['metric'], 'counter') for c in [c1, c2, c3])
-        nt.assert_equal(c1['tags'], ['env:production', 'db', 'type:constant'])
-        nt.assert_equal(c1['points'][0][1], 1)
-        nt.assert_equal(c2['tags'], ['env:staging', 'type:constant'])
-        nt.assert_equal(c2['points'][0][1], 1)
-        nt.assert_equal(c3['tags'], ['type:constant'])
-        nt.assert_equal(c3['points'][0][1], 1)
+        # Assertions on gauges
+        self.assertMetric(name='gauge', value=10, tags=["type:constant"], count=1)
+        self.assertMetric(name="gauge", value=15, tags=["env:production", "db", "type:constant"], count=1)  # noqa
+        self.assertMetric(name="gauge", value=20, tags=["env:staging", "type:constant"], count=1)
 
-        (nt.assert_equal(c['metric'], 'gauge') for c in [g1, g2, g3])
-        nt.assert_equal(g1['tags'], ['env:production', 'db', 'type:constant'])
-        nt.assert_equal(g1['points'][0][1], 15)
-        nt.assert_equal(g2['tags'], ['env:staging', 'type:constant'])
-        nt.assert_equal(g2['points'][0][1], 20)
-        nt.assert_equal(g3['tags'], ['type:constant'])
-        nt.assert_equal(g3['points'][0][1], 10)
+        # Assertions on counters
+        self.assertMetric(name="counter", value=1, tags=["type:constant"], count=1)
+        self.assertMetric(name="counter", value=1, tags=["env:production", "db", "type:constant"], count=1)  # noqa
+        self.assertMetric(name="counter", value=1, tags=["env:staging", "type:constant"], count=1)
 
         # Ensure histograms work as well.
         @dog.timed('timed', tags=['version:1'])
-        def test():
+        def do_nothing():
+            """
+            A function that does nothing, but being timed.
+            """
             pass
-        test()
+
+        with patch("datadog.threadstats.base.time", return_value=300):
+            do_nothing()
+
         dog.histogram('timed', 20, timestamp=300.0, tags=['db', 'version:2'])
-        reporter.metrics = []
-        dog.flush(400)
-        for metric in reporter.metrics:
-            assert metric['tags']  # this is enough
+
+        self.reporter.metrics = []
+        dog.flush(400.0)
+
+        # Histograms, and related metric types, produce 8 different metrics
+        self.assertMetric(tags=["version:1", "type:constant"], count=8)
+        self.assertMetric(tags=["db", "version:2", "type:constant"], count=8)
+
+    def test_metric_namespace(self):
+        """
+        Namespace prefixes all metric names.
+        """
+        # Set up ThreadStats with a namespace
+        dog = ThreadStats(namespace="foo")
+        dog.start(roll_up_interval=1, flush_in_thread=False)
+        dog.reporter = self.reporter
+
+        # Send a few metrics
+        dog.gauge("gauge", 20, timestamp=100.0)
+        dog.increment("counter", timestamp=100.0)
+        dog.flush(200.0)
+
+        # Metric names are prefixed with the namespace
+        self.assertMetric(count=2)
+        self.assertMetric(name="foo.gauge", count=1)
+        self.assertMetric(name="foo.counter", count=1)
 
     def test_host(self):
         dog = ThreadStats()