Repository: benoitc/gunicorn

Issue:
Add option to report stats over a unix domain socket
I have a setup where DataDog's **statsd** agent listens on a **unix domain socket** (as opposed to a network UDP). Reasons for using UDS are listed [here](https://docs.datadoghq.com/developers/dogstatsd/unix_socket/)

It would be useful if gunicorn allowed me to report its statsd metrics in such a setup.


Discussion:

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -144,7 +144,11 @@ def logger_class(self):
         # if default logger is in use, and statsd is on, automagically switch
         # to the statsd logger
         if uri == LoggerClass.default:
-            if 'statsd_host' in self.settings and self.settings['statsd_host'].value is not None:
+            statsd_address = 'statsd_socket' in self.settings and \
+                             self.settings['statsd_socket'].value or \
+                             'statsd_host' in self.settings and \
+                             self.settings['statsd_host'].value
+            if statsd_address is not None:
                 uri = "gunicorn.instrument.statsd.Statsd"
 
         logger_class = util.load_class(
@@ -1478,6 +1482,18 @@ class StatsdHost(Setting):
     .. versionadded:: 19.1
     """
 
+class StatsdSocket(Setting):
+    name = "statsd_socket"
+    section = "Logging"
+    cli = ["--statsd-socket"]
+    meta = "STATSD_SOCKET"
+    default = None
+    validator = validate_string
+    desc = """\
+    Unix domain socket of the statsd server to log to.
+    Supersedes ``statsd_host`` if provided.
+    """
+
 # Datadog Statsd (dogstatsd) tags. https://docs.datadoghq.com/developers/dogstatsd/
 class DogstatsdTags(Setting):
     name = "dogstatsd_tags"
diff --git a/gunicorn/instrument/statsd.py b/gunicorn/instrument/statsd.py
--- a/gunicorn/instrument/statsd.py
+++ b/gunicorn/instrument/statsd.py
@@ -27,10 +27,18 @@ def __init__(self, cfg):
         """
         Logger.__init__(self, cfg)
         self.prefix = sub(r"^(.+[^.]+)\.*$", "\\g<1>.", cfg.statsd_prefix)
-        try:
+
+        if cfg.statsd_socket:
+            address_family = socket.AF_UNIX
+            address = cfg.statsd_socket
+        elif cfg.statsd_host:
+            address_family = socket.AF_INET
             host, port = cfg.statsd_host
-            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-            self.sock.connect((host, int(port)))
+            address = (host, int(port))
+
+        try:
+            self.sock = socket.socket(address_family, socket.SOCK_DGRAM)
+            self.sock.connect(address)
         except Exception:
             self.sock = None
 


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -306,13 +306,20 @@ def nworkers_changed_3(server, new_value, old_value):
     assert c.nworkers_changed(1, 2, 3) == 3
 
 
-def test_statsd_changes_logger():
+def test_statsd_host_changes_logger():
     c = config.Config()
     assert c.logger_class == glogging.Logger
     c.set('statsd_host', 'localhost:12345')
     assert c.logger_class == statsd.Statsd
 
 
+def test_statsd_socket_changes_logger():
+    c = config.Config()
+    assert c.logger_class == glogging.Logger
+    c.set('statsd_socket', '/var/run/sock')
+    assert c.logger_class == statsd.Statsd
+
+
 class MyLogger(glogging.Logger):
     # dummy custom logger class for testing
     pass