Skip to content

Commit ed1bc9b

Browse files
ddrthallcarlosroman
authored andcommitted
AMLII-2173 - Add DD_DOGSTATSD_URL support to datadogpy
Adds support for the unix:// and udp:// variants of DD_DOGSTATSD_URL. Will only be applied if the host and port are their default values and no socket_path has been provided.
1 parent e54bbac commit ed1bc9b

4 files changed

Lines changed: 157 additions & 22 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ options = {
9494
initialize(**options)
9595
```
9696

97+
Alternatively, the environment variable `DD_DOGSTATSD_URL` can be used to define a udp connection:
98+
`DD_DOGSTATSD_URL=udp://localhost:8125`
99+
100+
Manually supplying a host/port will take precedence over using this environment variable.
101+
97102
See the full list of available [DogStatsD client instantiation parameters](https://docs.datadoghq.com/developers/dogstatsd/?code-lang=python#client-instantiation-parameters).
98103

99104
#### Instantiate the DogStatsd client with UDS
@@ -110,6 +115,12 @@ options = {
110115
initialize(**options)
111116
```
112117

118+
Alternatively, the environment variable `DD_DOGSTATSD_URL` can be used to define a UDS connection:
119+
`DD_DOGSTATSD_URL=unix:///var/run/datadog/dsd.socket`
120+
121+
As with the udp variant, manually supplying a statsd_socket_path will take precedence over the environment variable.
122+
123+
113124
#### Origin detection over UDP and UDS
114125

115126
Origin detection is a method to detect which pod `DogStatsD` packets are coming from in order to add the pod's tags to the tag list.

datadog/dogstatsd/base.py

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
# pypy has the same module, but capitalized.
2626
import Queue as queue # type: ignore[no-redef]
2727

28+
try:
29+
from urllib.parse import urlparse # type: ignore
30+
except ImportError:
31+
# Python 2 has the same functionality stored under a different module.
32+
from urlparse import urlparse # type: ignore
2833

2934
# pylint: disable=unused-import
3035
from typing import Optional, List, Text, Union
@@ -54,6 +59,7 @@
5459
UNIX_ADDRESS_SCHEME = "unix://"
5560
UNIX_ADDRESS_DATAGRAM_SCHEME = "unixgram://"
5661
UNIX_ADDRESS_STREAM_SCHEME = "unixstream://"
62+
WINDOWS_NAMEDPIPE_SCHEME = "\\\\.\\pipe\\"
5763

5864
# Buffering-related values (in seconds)
5965
DEFAULT_BUFFERING_FLUSH_INTERVAL = 0.3
@@ -188,12 +194,19 @@ def __init__(
188194
189195
>>> statsd = DogStatsd()
190196
197+
:envvar DD_DOGSTATSD_URL: the connection information for the dogstatsd server.
198+
If set, it overrides the default values.
199+
Example for UDP url: `DD_DOGSTATSD_URL=udp://localhost:8125`
200+
Example for UDS: `DD_DOGSTATSD_URL=unix:///var/run/datadog/dsd.socket`
201+
Windows named pipes are currently unsupported.
202+
:type DD_DOGSTATSD_URL: string
203+
191204
:envvar DD_AGENT_HOST: the host of the DogStatsd server.
192-
If set, it overrides default value.
205+
If set, it overrides default value. DD_DOGSTATSD_URL takes precedence over this value.
193206
:type DD_AGENT_HOST: string
194207
195208
:envvar DD_DOGSTATSD_PORT: the port of the DogStatsd server.
196-
If set, it overrides default value.
209+
If set, it overrides default value. DD_DOGSTATSD_URL takes precedence over this value.
197210
:type DD_DOGSTATSD_PORT: integer
198211
199212
:envvar DATADOG_TAGS: Tags to attach to every metric reported by dogstatsd client.
@@ -374,22 +387,8 @@ def __init__(
374387
# Check for deprecated option
375388
if max_buffer_size is not None:
376389
log.warning("The parameter max_buffer_size is now deprecated and is not used anymore")
377-
# Check host and port env vars
378-
agent_host = os.environ.get("DD_AGENT_HOST")
379-
if agent_host and host == DEFAULT_HOST:
380-
host = agent_host
381390

382-
dogstatsd_port = os.environ.get("DD_DOGSTATSD_PORT")
383-
if dogstatsd_port and port == DEFAULT_PORT:
384-
try:
385-
port = int(dogstatsd_port)
386-
except ValueError:
387-
log.warning(
388-
"Port number provided in DD_DOGSTATSD_PORT env var is not an integer: \
389-
%s, using %s as port number",
390-
dogstatsd_port,
391-
port,
392-
)
391+
host, port, socket_path = self._parse_env_connection_overrides(host, port, socket_path)
393392

394393
# Assuming environment variables always override
395394
telemetry_host = os.environ.get("DD_TELEMETRY_HOST", telemetry_host)
@@ -601,6 +600,74 @@ def disable_telemetry(self):
601600
def enable_telemetry(self):
602601
self._telemetry = True
603602

603+
def _parse_env_connection_overrides(self, host, port, socket_path):
604+
dogstatsd_url = os.environ.get("DD_DOGSTATSD_URL")
605+
606+
if (
607+
host == DEFAULT_HOST
608+
and port == DEFAULT_PORT
609+
and socket_path is None
610+
and dogstatsd_url is not None
611+
):
612+
parsed = urlparse(dogstatsd_url)
613+
# If all values are defaults, prefer DD_DOGSTATSD_URL if present.
614+
if parsed.scheme == "unix":
615+
log.debug(
616+
"Found a DD_DOGSTATSD_URL matching the uds syntax, "
617+
"setting socket path %s.", dogstatsd_url
618+
)
619+
return host, port, dogstatsd_url
620+
621+
elif dogstatsd_url.startswith(WINDOWS_NAMEDPIPE_SCHEME):
622+
log.debug(
623+
"DD_DOGSTATSD_URL is configured to utilize a windows named pipe, "
624+
"which is not currently supported by datadogpy. Falling back to "
625+
"alternate connection identifiers."
626+
)
627+
628+
elif parsed.scheme == "udp":
629+
try:
630+
p_port = parsed.port
631+
# Python 2 doesn't automatically perform bounds checking on the port
632+
if p_port is None or p_port < 0 or p_port > 65535:
633+
log.debug("Invalid port number provided, reverting to default port")
634+
p_port = DEFAULT_PORT
635+
except ValueError:
636+
log.debug("Invalid port number provided, reverting to default port")
637+
p_port = DEFAULT_PORT
638+
639+
log.debug(
640+
"Found a DD_DOGSTATSD_URL matching the udp sytnax, "
641+
"setting host and port %s:%d.", parsed.hostname, p_port
642+
)
643+
644+
return parsed.hostname, p_port, socket_path
645+
else:
646+
log.debug(
647+
"Unable to parse DD_DOGSTATSD_URL, did you remember to prefix the url "
648+
"with 'unix://' or 'udp://'? Falling back to alternate "
649+
"connection identifiers."
650+
)
651+
652+
# We either have some non-default values or no DD_DOGSTATSD_URL
653+
# Check host and port env vars
654+
agent_host = os.environ.get("DD_AGENT_HOST")
655+
if agent_host and host == DEFAULT_HOST:
656+
host = agent_host
657+
658+
dogstatsd_port = os.environ.get("DD_DOGSTATSD_PORT")
659+
if dogstatsd_port and port == DEFAULT_PORT:
660+
try:
661+
port = int(dogstatsd_port)
662+
except ValueError:
663+
log.warning(
664+
"Port number provided in DD_DOGSTATSD_PORT env var is not an integer: \
665+
%s, using %s as port number",
666+
dogstatsd_port,
667+
port,
668+
)
669+
return host, port, socket_path
670+
604671
# Note: Invocations of this method should be thread-safe
605672
def _start_flush_thread(self):
606673
if self._disable_aggregation and self.disable_buffering:

doc/source/index.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ Here's an example where the statsd host and port are configured as well::
4040
)
4141

4242

43+
If statsd_host and statsd_port are left at their default values and no socket_path alternative is supplied,
44+
the DD_DOGSTATSD_URL environment variable, if it exists, will be used to determine the connection
45+
information. This must be a URL that start with either `udp://` (to connect using UDP) or with `unix://`
46+
(to use a Unix Domain Socket).
47+
48+
* Example for UDP url: `DD_DOGSTATSD_URL=udp://localhost:8125`
49+
* Example for UDS: `DD_DOGSTATSD_URL=unix:///var/run/datadog/dsd.socket`
50+
51+
4352
.. autofunction:: datadog.initialize
4453

4554

tests/unit/dogstatsd/test_statsd.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
# Datadog libraries
3131
from datadog import initialize, statsd
3232
from datadog import __version__ as version
33-
from datadog.dogstatsd.base import DEFAULT_BUFFERING_FLUSH_INTERVAL, DogStatsd, MIN_SEND_BUFFER_SIZE, UDP_OPTIMAL_PAYLOAD_LENGTH, UDS_OPTIMAL_PAYLOAD_LENGTH
33+
from datadog.dogstatsd.base import DEFAULT_BUFFERING_FLUSH_INTERVAL, DEFAULT_HOST, DEFAULT_PORT, DogStatsd, MIN_SEND_BUFFER_SIZE, UDP_OPTIMAL_PAYLOAD_LENGTH, UDS_OPTIMAL_PAYLOAD_LENGTH
3434
from datadog.dogstatsd.context import TimedContextManagerDecorator
3535
from datadog.util.compat import is_higher_py35, is_p3k
3636
from tests.util.contextmanagers import preserve_environment_variable, EnvVars
@@ -296,7 +296,7 @@ def test_initialization(self):
296296
initialize(**options)
297297
self.assertEqual(statsd.cardinality, 'none')
298298

299-
def test_dogstatsd_initialization_with_env_vars(self):
299+
def test_dogstatsd_initialization_with_env_vars_agent_host(self):
300300
"""
301301
Dogstatsd can retrieve its config from env vars when
302302
not provided in constructor.
@@ -312,6 +312,54 @@ def test_dogstatsd_initialization_with_env_vars(self):
312312
self.assertEqual(dogstatsd.host, "myenvvarhost")
313313
self.assertEqual(dogstatsd.port, 4321)
314314

315+
316+
def test_dogstatsd_initialization_with_env_vars_dogstatsd_url(self):
317+
"""
318+
Dogstatsd can retrieve its config from env vars when
319+
not provided in constructor.
320+
"""
321+
# Setup UDP
322+
with preserve_environment_variable('DD_DOGSTATSD_URL'):
323+
os.environ['DD_DOGSTATSD_URL'] = 'udp://myenvvarhost:4321'
324+
dogstatsd = DogStatsd()
325+
326+
# Assert
327+
self.assertEqual(dogstatsd.host, "myenvvarhost")
328+
self.assertEqual(dogstatsd.port, 4321)
329+
self.assertEqual(dogstatsd.socket_path, None)
330+
331+
# Test UDS
332+
with preserve_environment_variable('DD_DOGSTATSD_URL'):
333+
os.environ['DD_DOGSTATSD_URL'] = 'unix:///hello/world.sock'
334+
dogstatsd = DogStatsd()
335+
self.assertEqual(dogstatsd.socket_path, 'unix:///hello/world.sock')
336+
self.assertEqual(dogstatsd.host, None)
337+
self.assertEqual(dogstatsd.port, None)
338+
339+
# Test non-default host
340+
with preserve_environment_variable('DD_DOGSTATSD_URL'):
341+
os.environ['DD_DOGSTATSD_URL'] = 'unix:///hello/world.sock'
342+
dogstatsd = DogStatsd(host="myhost")
343+
self.assertEqual(dogstatsd.socket_path, None)
344+
self.assertEqual(dogstatsd.host, 'myhost')
345+
self.assertEqual(dogstatsd.port, DEFAULT_PORT)
346+
347+
# Test non-default port
348+
with preserve_environment_variable('DD_DOGSTATSD_URL'):
349+
os.environ['DD_DOGSTATSD_URL'] = 'unix:///hello/world.sock'
350+
dogstatsd = DogStatsd(port=8240)
351+
self.assertEqual(dogstatsd.socket_path, None)
352+
self.assertEqual(dogstatsd.host, DEFAULT_HOST)
353+
self.assertEqual(dogstatsd.port, 8240)
354+
355+
# Test non-default socket_path
356+
with preserve_environment_variable('DD_DOGSTATSD_URL'):
357+
os.environ['DD_DOGSTATSD_URL'] = 'unix:///hello/world.sock'
358+
dogstatsd = DogStatsd(socket_path='unix:///var/run/datadog/dsd.sock')
359+
self.assertEqual(dogstatsd.socket_path, 'unix:///var/run/datadog/dsd.sock')
360+
self.assertEqual(dogstatsd.host, None)
361+
self.assertEqual(dogstatsd.port, None)
362+
315363
def test_initialization_closes_socket(self):
316364
statsd.socket = FakeSocket()
317365
self.assertIsNotNone(statsd.socket)
@@ -2178,13 +2226,13 @@ def test_fake_sockets(self):
21782226
"""
21792227
statsd = DogStatsd(disable_buffering=True)
21802228

2181-
class fakeSock:
2229+
class FakeSock:
21822230
def __init__(self, id):
21832231
self.id = id
21842232
def send(self, _):
21852233
pass
2186-
statsd.socket = fakeSock(5)
2187-
statsd.telemetry_socket = fakeSock(10)
2234+
statsd.socket = FakeSock(5)
2235+
statsd.telemetry_socket = FakeSock(10)
21882236

21892237
assert statsd.socket.id == 5
21902238
assert statsd.telemetry_socket.id == 10

0 commit comments

Comments
 (0)