Skip to content

Commit b03e48a

Browse files
authored
Support a Unix Domain Socket for the StatsD and Datadog metrics clients (#70517)
Airflow's metrics clients could only be pointed at a host and port, so deployments that expose the daemon over a Unix Domain Socket (for example the Datadog Kubernetes operator, which mounts a socket and sets DD_DOGSTATSD_URL) had no way to use it from Airflow. Add a ``[metrics] statsd_socket_path`` option, honored by both backends: the DataDog client is built with ``socket_path`` and the StatsD client uses ``statsd.UnixSocketStatsClient``. ``statsd_host`` and ``statsd_port`` are now nullable, and only explicitly-configured values are passed to the client; when nothing is set the DataDog client falls back to its own environment variables (DD_AGENT_HOST / DD_DOGSTATSD_URL). ``statsd_socket_path`` takes precedence over ``statsd_host`` / ``statsd_port`` when both are set — the DataDog client is given all of them and prefers the socket, the StatsD client is built on the socket — and a warning is logged in that case.
1 parent f44bb16 commit b03e48a

11 files changed

Lines changed: 250 additions & 88 deletions

File tree

‎airflow-core/docs/administration-and-deployment/logging-monitoring/metrics.rst‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,36 @@ then add the following lines to your configuration file e.g. ``airflow.cfg``
4242
statsd_port = 8125
4343
statsd_prefix = airflow
4444
45+
Sending metrics over a Unix Domain Socket
46+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47+
48+
Metrics can be sent over a Unix Domain Socket instead of UDP by setting
49+
``statsd_socket_path``:
50+
51+
.. code-block:: ini
52+
53+
[metrics]
54+
statsd_on = True
55+
statsd_socket_path = /var/run/statsd/statsd.sock
56+
statsd_prefix = airflow
57+
58+
When ``statsd_socket_path`` is set, ``statsd_host``, ``statsd_port``, and
59+
``statsd_ipv6`` are ignored.
60+
61+
The standard StatsD backend uses a stream Unix socket. When the Datadog backend
62+
is enabled, both stream and datagram Unix sockets are supported:
63+
64+
.. code-block:: ini
65+
66+
[metrics]
67+
statsd_datadog_enabled = True
68+
statsd_socket_path = /var/run/datadog/dsd.socket
69+
statsd_prefix = airflow
70+
71+
For maximum compatibility, configure a plain filesystem path. The Datadog
72+
backend additionally accepts ``unix://``, ``unixgram://``, and
73+
``unixstream://`` URLs.
74+
4575
If you want to use a custom StatsD client instead of the default one provided by Airflow,
4676
the following key must be added to the configuration file alongside the module path of your
4777
custom StatsD client. This module must be available on your :envvar:`PYTHONPATH`.
@@ -51,6 +81,10 @@ custom StatsD client. This module must be available on your :envvar:`PYTHONPATH`
5181
[metrics]
5282
statsd_custom_client_path = x.y.customclient
5383
84+
When ``statsd_socket_path`` is configured, a custom client must inherit from
85+
``statsd.UnixSocketStatsClient``. Otherwise, it must inherit from
86+
``statsd.StatsClient``.
87+
5488
See :doc:`../modules_management` for details on how Python and Airflow manage modules.
5589

5690
.. note::

‎airflow-core/src/airflow/config_templates/config.yml‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,8 @@ metrics:
12061206
default: "False"
12071207
statsd_host:
12081208
description: |
1209-
Specifies the host address where the StatsD daemon (or server) is running
1209+
Specifies the host address where the StatsD daemon (or server) is running.
1210+
Ignored when ``statsd_socket_path`` is set.
12101211
version_added: 2.0.0
12111212
type: string
12121213
example: ~
@@ -1220,11 +1221,25 @@ metrics:
12201221
default: "False"
12211222
statsd_port:
12221223
description: |
1223-
Specifies the port on which the StatsD daemon (or server) is listening to
1224+
Specifies the port on which the StatsD daemon (or server) is listening to.
1225+
Ignored when ``statsd_socket_path`` is set.
12241226
version_added: 2.0.0
12251227
type: integer
12261228
example: ~
12271229
default: "8125"
1230+
statsd_socket_path:
1231+
description: |
1232+
Path to a Unix Domain Socket used to submit metrics, instead of ``statsd_host``
1233+
and ``statsd_port``. Applies to both the StatsD and Datadog backends. Takes
1234+
precedence over ``statsd_host`` / ``statsd_port`` when set. For the Datadog
1235+
backend (``statsd_datadog_enabled = True``) a plain path or a ``unix://`` /
1236+
``unixgram://`` / ``unixstream://`` URL is accepted; for the StatsD backend a
1237+
plain filesystem path is expected. Note that ``statsd_ipv6`` has no effect when
1238+
this option is set.
1239+
version_added: 3.4.0
1240+
type: string
1241+
example: "/var/run/statsd/statsd.sock"
1242+
default: ~
12281243
statsd_prefix:
12291244
description: |
12301245
Defines the namespace for all metrics sent from Airflow to StatsD

‎airflow-core/src/airflow/observability/metrics/datadog_logger.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def get_dogstatsd_logger() -> SafeDogStatsdLogger:
3030
tags_in_string=conf.get("metrics", "statsd_datadog_tags"),
3131
host=conf.get("metrics", "statsd_host"),
3232
port=conf.getint("metrics", "statsd_port"),
33+
socket_path=conf.get("metrics", "statsd_socket_path", fallback=None) or None,
3334
namespace=conf.get("metrics", "statsd_prefix"),
3435
datadog_metrics_tags=conf.getboolean("metrics", "statsd_datadog_metrics_tags", fallback=True),
3536
statsd_disabled_tags=conf.get("metrics", "statsd_disabled_tags", fallback=None),

‎airflow-core/src/airflow/observability/metrics/statsd_logger.py‎

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,48 @@
2323
from airflow._shared.observability.metrics import statsd_logger
2424
from airflow.configuration import conf
2525

26+
log = logging.getLogger(__name__)
27+
2628
if TYPE_CHECKING:
2729
from airflow._shared.observability.metrics.statsd_logger import SafeStatsdLogger
2830

29-
log = logging.getLogger(__name__)
30-
3131

3232
def get_statsd_logger() -> SafeStatsdLogger:
33-
stats_class = conf.getimport("metrics", "statsd_custom_client_path", fallback=None)
33+
# Local import to avoid requiring statsd when other backends are used (e.g. Datadog)
34+
from statsd import StatsClient, UnixSocketStatsClient
3435

35-
# no need to check for the scheduler/statsd_on -> this method is only called when it is set
36-
# and previously it would crash with None is callable if it was called without it.
37-
from statsd import StatsClient
36+
socket_path = conf.get("metrics", "statsd_socket_path", fallback=None) or None
37+
custom_class = conf.getimport("metrics", "statsd_custom_client_path", fallback=None)
3838

39-
if stats_class:
40-
if not issubclass(stats_class, StatsClient):
39+
if custom_class is not None:
40+
if socket_path is not None:
41+
if not issubclass(custom_class, UnixSocketStatsClient):
42+
raise AirflowConfigException(
43+
"Your custom StatsD client must extend the statsd.UnixSocketStatsClient "
44+
"when using a socket path in order to ensure backwards compatibility."
45+
)
46+
elif not issubclass(custom_class, StatsClient):
4147
raise AirflowConfigException(
42-
"Your custom StatsD client must extend the statsd.StatsClient in order to ensure "
43-
"backwards compatibility."
48+
"Your custom StatsD client must extend the statsd.StatsClient in order "
49+
"to ensure backwards compatibility."
4450
)
4551
log.info("Successfully loaded custom StatsD client")
4652

47-
else:
48-
stats_class = StatsClient
49-
50-
return statsd_logger.get_statsd_logger(
51-
stats_class=stats_class,
53+
common_kwargs = {
54+
"stats_class": custom_class,
55+
"prefix": conf.get("metrics", "statsd_prefix"),
56+
"influxdb_tags_enabled": conf.getboolean("metrics", "statsd_influxdb_enabled", fallback=False),
57+
"statsd_disabled_tags": conf.get("metrics", "statsd_disabled_tags", fallback=None),
58+
"metrics_allow_list": conf.get("metrics", "metrics_allow_list", fallback=None),
59+
"metrics_block_list": conf.get("metrics", "metrics_block_list", fallback=None),
60+
"stat_name_handler": conf.getimport("metrics", "stat_name_handler"),
61+
"statsd_influxdb_enabled": conf.getboolean("metrics", "statsd_influxdb_enabled", fallback=False),
62+
}
63+
if socket_path is not None:
64+
return statsd_logger.get_socket_statsd_logger(socket_path=socket_path, **common_kwargs)
65+
return statsd_logger.get_udp_statsd_logger(
5266
host=conf.get("metrics", "statsd_host"),
5367
port=conf.getint("metrics", "statsd_port"),
54-
prefix=conf.get("metrics", "statsd_prefix"),
5568
ipv6=conf.getboolean("metrics", "statsd_ipv6", fallback=False),
56-
influxdb_tags_enabled=conf.getboolean("metrics", "statsd_influxdb_enabled", fallback=False),
57-
statsd_disabled_tags=conf.get("metrics", "statsd_disabled_tags", fallback=None),
58-
metrics_allow_list=conf.get("metrics", "metrics_allow_list", fallback=None),
59-
metrics_block_list=conf.get("metrics", "metrics_block_list", fallback=None),
60-
stat_name_handler=conf.getimport("metrics", "stat_name_handler"),
61-
statsd_influxdb_enabled=conf.getboolean("metrics", "statsd_influxdb_enabled", fallback=False),
69+
**common_kwargs,
6270
)

‎airflow-core/tests/unit/observability/metrics/test_stats.py‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,35 @@ def __init__(self, host=None, port=None, prefix=None):
3939

4040

4141
class TestStats:
42-
def test_load_invalid_custom_stats_client(self):
43-
with conf_vars(
44-
{
45-
("metrics", "statsd_on"): "True",
46-
("metrics", "statsd_custom_client_path"): f"{__name__}.InvalidCustomStatsd",
47-
}
48-
):
42+
@pytest.mark.parametrize(
43+
("extra_conf", "expected_message"),
44+
[
45+
(
46+
{},
47+
"Your custom StatsD client must extend the statsd.StatsClient in order "
48+
"to ensure backwards compatibility.",
49+
),
50+
(
51+
{("metrics", "statsd_socket_path"): "/var/run/datadog/dsd.socket"},
52+
"Your custom StatsD client must extend the statsd.UnixSocketStatsClient "
53+
"when using a socket path in order to ensure backwards compatibility.",
54+
),
55+
],
56+
ids=["udp", "socket"],
57+
)
58+
def test_load_invalid_custom_stats_client(self, extra_conf, expected_message):
59+
conf = {
60+
("metrics", "statsd_on"): "True",
61+
("metrics", "statsd_custom_client_path"): f"{__name__}.InvalidCustomStatsd",
62+
**extra_conf,
63+
}
64+
with conf_vars(conf):
4965
importlib.reload(airflow._shared.observability.metrics.stats)
5066
airflow.observability.stats.initialize(
5167
factory=stats_utils.get_stats_factory(), export_legacy_names=True
5268
)
53-
error_message = re.escape(
54-
"Your custom StatsD client must extend the statsd."
55-
"StatsClient in order to ensure backwards compatibility."
56-
)
5769
# we assert for Exception here instead of AirflowConfigException to not import from shared configuration
58-
with pytest.raises(Exception, match=error_message):
70+
with pytest.raises(Exception, match=re.escape(expected_message)):
5971
airflow.observability.stats.incr("empty_key")
6072
importlib.reload(airflow._shared.observability.metrics.stats)
6173

‎docs/spelling_wordlist.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ dataframes
396396
DataFusion
397397
Datafusion
398398
datafusion
399+
Datagram
400+
datagram
399401
Datalake
400402
datamodel
401403
Datanodes

‎shared/observability/src/airflow_shared/observability/metrics/datadog_logger.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def get_dogstatsd_logger(
148148
tags_in_string: str | None = None,
149149
host: str | None = None,
150150
port: int | None = None,
151+
socket_path: str | None = None,
151152
namespace: str | None = None,
152153
datadog_metrics_tags: bool = True,
153154
statsd_disabled_tags: str | None = None,
@@ -156,7 +157,7 @@ def get_dogstatsd_logger(
156157
stat_name_handler: Callable[[str], str] | None = None,
157158
statsd_influxdb_enabled: bool = False,
158159
) -> SafeDogStatsdLogger:
159-
"""Get DataDog StatsD logger."""
160+
"""Get Datadog StatsD logger."""
160161
from datadog import DogStatsd
161162

162163
dogstatsd_kwargs: dict[str, Any] = {
@@ -166,6 +167,8 @@ def get_dogstatsd_logger(
166167
dogstatsd_kwargs["host"] = host
167168
if port is not None:
168169
dogstatsd_kwargs["port"] = port
170+
if socket_path is not None:
171+
dogstatsd_kwargs["socket_path"] = socket_path
169172
if namespace is not None:
170173
dogstatsd_kwargs["namespace"] = namespace
171174
dogstatsd = DogStatsd(**dogstatsd_kwargs)

‎shared/observability/src/airflow_shared/observability/metrics/statsd_logger.py‎

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from functools import wraps
2323
from typing import TYPE_CHECKING, TypeVar, cast
2424

25+
from statsd import StatsClient, UnixSocketStatsClient
26+
2527
from .protocols import Timer
2628
from .validators import (
2729
PatternAllowListValidator,
@@ -31,8 +33,6 @@
3133
)
3234

3335
if TYPE_CHECKING:
34-
from statsd import StatsClient
35-
3636
from .protocols import DeltaType
3737
from .validators import ListValidator
3838

@@ -156,11 +156,32 @@ def timer(
156156
return Timer()
157157

158158

159-
def get_statsd_logger(
159+
def _make_safe_statsd_logger(
160+
statsd_client: StatsClient,
161+
influxdb_tags_enabled: bool,
162+
statsd_disabled_tags: str | None,
163+
metrics_allow_list: str | None,
164+
metrics_block_list: str | None,
165+
stat_name_handler: Callable[[str], str] | None,
166+
statsd_influxdb_enabled: bool,
167+
) -> SafeStatsdLogger:
168+
metric_tags_validator = PatternBlockListValidator(statsd_disabled_tags)
169+
validator = get_validator(metrics_allow_list, metrics_block_list)
170+
return SafeStatsdLogger(
171+
statsd_client,
172+
validator,
173+
influxdb_tags_enabled,
174+
metric_tags_validator,
175+
stat_name_handler,
176+
statsd_influxdb_enabled,
177+
)
178+
179+
180+
def get_udp_statsd_logger(
160181
*,
161-
stats_class: type[StatsClient],
162-
host: str | None = None,
163-
port: int | None = None,
182+
stats_class: type[StatsClient] | None = None,
183+
host: str,
184+
port: int,
164185
prefix: str | None = None,
165186
ipv6: bool = False,
166187
influxdb_tags_enabled: bool = False,
@@ -170,21 +191,41 @@ def get_statsd_logger(
170191
stat_name_handler: Callable[[str], str] | None = None,
171192
statsd_influxdb_enabled: bool = False,
172193
) -> SafeStatsdLogger:
173-
"""Return logger for StatsD."""
174-
statsd = stats_class(
175-
host=host,
176-
port=port,
177-
prefix=prefix,
178-
ipv6=ipv6,
194+
"""Return a UDP StatsD logger."""
195+
resolved: type[StatsClient] = stats_class if stats_class is not None else StatsClient
196+
statsd_client = resolved(host=host, port=port, prefix=prefix, ipv6=ipv6)
197+
return _make_safe_statsd_logger(
198+
statsd_client,
199+
influxdb_tags_enabled,
200+
statsd_disabled_tags,
201+
metrics_allow_list,
202+
metrics_block_list,
203+
stat_name_handler,
204+
statsd_influxdb_enabled,
179205
)
180206

181-
metric_tags_validator = PatternBlockListValidator(statsd_disabled_tags)
182-
validator = get_validator(metrics_allow_list, metrics_block_list)
183-
return SafeStatsdLogger(
184-
statsd,
185-
validator,
207+
208+
def get_socket_statsd_logger(
209+
*,
210+
stats_class: type[UnixSocketStatsClient] | None = None,
211+
socket_path: str,
212+
prefix: str | None = None,
213+
influxdb_tags_enabled: bool = False,
214+
statsd_disabled_tags: str | None = None,
215+
metrics_allow_list: str | None = None,
216+
metrics_block_list: str | None = None,
217+
stat_name_handler: Callable[[str], str] | None = None,
218+
statsd_influxdb_enabled: bool = False,
219+
) -> SafeStatsdLogger:
220+
"""Return a Unix Domain Socket StatsD logger. Note: statsd_ipv6 has no effect in socket mode."""
221+
resolved: type[UnixSocketStatsClient] = stats_class if stats_class is not None else UnixSocketStatsClient
222+
statsd_client = resolved(socket_path=socket_path, prefix=prefix)
223+
return _make_safe_statsd_logger(
224+
statsd_client,
186225
influxdb_tags_enabled,
187-
metric_tags_validator,
226+
statsd_disabled_tags,
227+
metrics_allow_list,
228+
metrics_block_list,
188229
stat_name_handler,
189230
statsd_influxdb_enabled,
190231
)

0 commit comments

Comments
 (0)