yannmh: @JohnLZeller can you take a pass on it ?
JohnLZeller: :+1: looks good once conflicts are resolved.
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,

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
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()

pip install -e .
parse_log_pytest
pytest
nose six mock pytest
3.4
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
-e git+https://github.com/DataDog/datadogpy.git@c8bc9d6cce1caebea0be16366f2cd0c3efb47571#egg=datadog
decorator==5.1.1
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
nose==1.3.7
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
requests==2.27.1
simplejson==3.20.1
six==1.17.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: datadogpy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==2.0.12
      - decorator==5.1.1
      - idna==3.10
      - mock==5.2.0
      - nose==1.3.7
      - requests==2.27.1
      - simplejson==3.20.1
      - six==1.17.0
      - urllib3==1.26.20
prefix: /opt/conda/envs/datadogpy

tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_metric_namespace
tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_set tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_gauge tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_counter tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_histogram tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tagged_gauge tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tagged_counter tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tagged_histogram tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_sample_rate tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tags_and_samples tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timing tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_event tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_event_constant_tags tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_service_check tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_metric_namespace tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_gauge_constant_tags tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_counter_constant_tag_with_metric_level_tags tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_gauge_constant_tags_with_metric_level_tags_twice tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_socket_error tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed_in_ms tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed_no_metric tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed_context tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed_context_exception tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_timed_context_no_metric_exception tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_batched
tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_initialization tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_context_manager tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_batched_buffer_autoflush tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_module_level_instance tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_instantiating_does_not_connect tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_accessing_socket_opens_socket tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_accessing_socket_multiple_times_returns_same_socket tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tags_from_environment tests/unit/dogstatsd/test_statsd.py::TestDogStatsd::test_tags_from_environment_and_constant tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_constant_tags tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_counter tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_custom_host_and_device tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_default_host_and_device tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_disabled_mode tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_event tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_event_constant_tags tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_gauge tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_histogram tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_histogram_percentiles tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_host tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_stop tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_tags tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_tags_from_environment tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_tags_from_environment_and_constant tests/unit/threadstats/test_threadstats.py::TestUnitThreadStats::test_timed_decorator
datadog/threadstats/base.py datadog/dogstatsd/base.py
datadog/threadstats/base.py datadog/dogstatsd/base.py