diff --git a/docs/source/settings.rst b/docs/source/settings.rst index df5a39d9..a322b615 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -420,7 +420,12 @@ environment variable ``PYTHONUNBUFFERED`` . **Default:** ``None`` -``host:port`` of the statsd server to log to. +The address of the StatsD server to log to. + +Address is a string of the form: + +* ``unix://PATH`` : for a unix domain socket. +* ``HOST:PORT`` : for a network address .. versionadded:: 19.1 diff --git a/gunicorn/config.py b/gunicorn/config.py index 4bc8bcc2..a41bd21c 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -514,15 +514,25 @@ def validate_chdir(val): return path -def validate_hostport(val): +def validate_statsd_address(val): val = validate_string(val) if val is None: return None - elements = val.split(":") - if len(elements) == 2: - return (elements[0], int(elements[1])) - else: - raise TypeError("Value must consist of: hostname:port") + + # As of major release 20, util.parse_address would recognize unix:PORT + # as a UDS address, breaking backwards compatibility. We defend against + # that regression here (this is also unit-tested). + # Feel free to remove in the next major release. + unix_hostname_regression = re.match(r'^unix:(\d+)$', val) + if unix_hostname_regression: + return ('unix', int(unix_hostname_regression.group(1))) + + try: + address = util.parse_address(val, default_port='8125') + except RuntimeError: + raise TypeError("Value must be one of ('host:port', 'unix://PATH')") + + return address def validate_reload_engine(val): @@ -1630,9 +1640,14 @@ class StatsdHost(Setting): cli = ["--statsd-host"] meta = "STATSD_ADDR" default = None - validator = validate_hostport + validator = validate_statsd_address desc = """\ - ``host:port`` of the statsd server to log to. + The address of the StatsD server to log to. + + Address is a string of the form: + + * ``unix://PATH`` : for a unix domain socket. + * ``HOST:PORT`` : for a network address .. versionadded:: 19.1 """ diff --git a/gunicorn/instrument/statsd.py b/gunicorn/instrument/statsd.py index afbfd7b4..2c54b2e7 100644 --- a/gunicorn/instrument/statsd.py +++ b/gunicorn/instrument/statsd.py @@ -24,14 +24,17 @@ class Statsd(Logger): """statsD-based instrumentation, that passes as a logger """ def __init__(self, cfg): - """host, port: statsD server - """ Logger.__init__(self, cfg) self.prefix = sub(r"^(.+[^.]+)\.*$", "\\g<1>.", cfg.statsd_prefix) + + if isinstance(cfg.statsd_host, str): + address_family = socket.AF_UNIX + else: + address_family = socket.AF_INET + try: - host, port = cfg.statsd_host - self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.sock.connect((host, int(port))) + self.sock = socket.socket(address_family, socket.SOCK_DGRAM) + self.sock.connect(cfg.statsd_host) except Exception: self.sock = None diff --git a/tests/test_config.py b/tests/test_config.py index 92cb73c3..211ee017 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -318,6 +318,30 @@ def test_nworkers_changed(): assert c.nworkers_changed(1, 2, 3) == 3 +def test_statsd_host(): + c = config.Config() + assert c.statsd_host is None + c.set("statsd_host", "localhost") + assert c.statsd_host == ("localhost", 8125) + c.set("statsd_host", "statsd:7777") + assert c.statsd_host == ("statsd", 7777) + c.set("statsd_host", "unix:///path/to.sock") + assert c.statsd_host == "/path/to.sock" + pytest.raises(TypeError, c.set, "statsd_host", 666) + pytest.raises(TypeError, c.set, "statsd_host", "host:string") + + +def test_statsd_host_with_unix_as_hostname(): + # This is a regression test for major release 20. After this release + # we should consider modifying the behavior of util.parse_address to + # simplify gunicorn's code + c = config.Config() + c.set("statsd_host", "unix:7777") + assert c.statsd_host == ("unix", 7777) + c.set("statsd_host", "unix://some.socket") + assert c.statsd_host == "some.socket" + + def test_statsd_changes_logger(): c = config.Config() assert c.logger_class == glogging.Logger diff --git a/tests/test_statsd.py b/tests/test_statsd.py index 06c1d964..6f7bf426 100644 --- a/tests/test_statsd.py +++ b/tests/test_statsd.py @@ -59,6 +59,18 @@ def test_statsd_fail(): logger.exception("No impact on logging") +def test_statsd_host_initialization(): + c = Config() + c.set('statsd_host', 'unix:test.sock') + logger = Statsd(c) + logger.info("Can be initialized and used with a UDS socket") + + # Can be initialized and used with a UDP address + c.set('statsd_host', 'host:8080') + logger = Statsd(c) + logger.info("Can be initialized and used with a UDP socket") + + def test_dogstatsd_tags(): c = Config() tags = 'yucatan,libertine:rhubarb'