Skip to content

Commit 46959ab

Browse files
mabdinurwantsui
andauthored
feat(runtime_metrics): add runtime id and gauge metric support (#12555)
- Adds support for sending runtime metrics as gauge metrics (instead of distributions). This feature is disabled by default and can be enabled by setting ``DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED=DD_RUNTIME_METRICS_ENABLED``. - Adds support for tagging runtime metrics with the current runtime ID. This feature is disabled by default and can be enabled by ``DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED=True``. Note: Sending runtime metrics as gauges after sending the same metric names as disitrubtions can cause the old distribution metric to overshadow the new gauge metrics. Since this feature is still in beta we are okay with the breaking change cc: @wantsui ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --------- Co-authored-by: wantsui <[email protected]>
1 parent c9e5ec8 commit 46959ab

File tree

10 files changed

+153
-7
lines changed

10 files changed

+153
-7
lines changed

ddtrace/internal/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,9 @@ class SamplingMechanism(object):
110110
}
111111
_KEEP_PRIORITY_INDEX = 0
112112
_REJECT_PRIORITY_INDEX = 1
113+
114+
115+
# List of support values in DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED
116+
class EXPERIMENTAL_FEATURES:
117+
# Enables submitting runtime metrics as gauges (instead of distributions)
118+
RUNTIME_METRICS = "DD_RUNTIME_METRICS_ENABLED"

ddtrace/internal/runtime/runtime_metrics.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ddtrace
88
from ddtrace.internal import atexit
99
from ddtrace.internal import forksafe
10+
from ddtrace.internal.constants import EXPERIMENTAL_FEATURES
1011
from ddtrace.vendor.dogstatsd import DogStatsd
1112

1213
from .. import periodic
@@ -16,6 +17,7 @@
1617
from .metric_collectors import GCRuntimeMetricCollector
1718
from .metric_collectors import PSUtilRuntimeMetricCollector
1819
from .tag_collectors import PlatformTagCollector
20+
from .tag_collectors import PlatformTagCollectorV2
1921
from .tag_collectors import TracerTagCollector
2022

2123

@@ -45,6 +47,12 @@ class PlatformTags(RuntimeCollectorsIterable):
4547
COLLECTORS = [PlatformTagCollector]
4648

4749

50+
class PlatformTagsV2(RuntimeCollectorsIterable):
51+
# DEV: `None` means to allow all tags generated by PlatformTagCollector and TracerTagCollector
52+
ENABLED = None
53+
COLLECTORS = [PlatformTagCollectorV2]
54+
55+
4856
class TracerTags(RuntimeCollectorsIterable):
4957
# DEV: `None` means to allow all tags generated by PlatformTagCollector and TracerTagCollector
5058
ENABLED = None
@@ -80,7 +88,17 @@ def __init__(self, interval=_get_interval_or_default(), tracer=None, dogstatsd_u
8088
)
8189
self.tracer: ddtrace.trace.Tracer = tracer or ddtrace.tracer
8290
self._runtime_metrics: RuntimeMetrics = RuntimeMetrics()
83-
self._platform_tags: List[str] = self._format_tags(PlatformTags())
91+
if EXPERIMENTAL_FEATURES.RUNTIME_METRICS in ddtrace.config._experimental_features_enabled:
92+
# Enables sending runtime metrics as gauges (instead of distributions with a new metric name)
93+
self.send_metric = self._dogstatsd_client.gauge
94+
else:
95+
self.send_metric = self._dogstatsd_client.distribution
96+
97+
if ddtrace.config._runtime_metrics_runtim_id_enabled:
98+
# Enables tagging runtime metrics with runtime-id (as well as all the v1 tags)
99+
self._platform_tags = self._format_tags(PlatformTagsV2())
100+
else:
101+
self._platform_tags = self._format_tags(PlatformTags())
84102

85103
@classmethod
86104
def disable(cls):
@@ -130,13 +148,13 @@ def flush(self):
130148
# type: () -> None
131149
# Ensure runtime metrics have up-to-date tags (ex: service, env, version)
132150
rumtime_tags = self._format_tags(TracerTags()) + self._platform_tags
133-
log.debug("Updating constant tags %s", rumtime_tags)
151+
log.debug("Sending runtime metrics with the following tags: %s", rumtime_tags)
134152
self._dogstatsd_client.constant_tags = rumtime_tags
135153

136154
with self._dogstatsd_client:
137155
for key, value in self._runtime_metrics:
138-
log.debug("Writing metric %s:%s", key, value)
139-
self._dogstatsd_client.distribution(key, value)
156+
log.debug("Sending ddtrace runtime metric %s:%s", key, value)
157+
self.send_metric(key, value)
140158

141159
def _stop_service(self):
142160
# type: (...) -> None

ddtrace/internal/runtime/tag_collectors.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import List # noqa:F401
22
from typing import Tuple # noqa:F401
33

4+
from ddtrace.internal.runtime import get_runtime_id
5+
46
from ...constants import ENV_KEY
57
from ...constants import VERSION_KEY
68
from ..constants import DEFAULT_SERVICE_NAME
@@ -59,7 +61,6 @@ class PlatformTagCollector(RuntimeTagCollector):
5961
- `lang_version``, eg ``2.7.10``
6062
- ``lang`` e.g. ``Python``
6163
- ``tracer_version`` e.g. ``0.29.0``
62-
6364
"""
6465

6566
required_modules = ["platform", "ddtrace"]
@@ -74,3 +75,25 @@ def collect_fn(self, keys):
7475
(TRACER_VERSION, ddtrace.__version__),
7576
]
7677
return tags
78+
79+
80+
class PlatformTagCollectorV2(PlatformTagCollector):
81+
"""Tag collector for the Python interpreter implementation.
82+
83+
Tags collected:
84+
- ``lang_interpreter``:
85+
86+
* For CPython this is 'CPython'.
87+
* For Pypy this is ``PyPy``
88+
* For Jython this is ``Jython``
89+
90+
- `lang_version``, eg ``2.7.10``
91+
- ``lang`` e.g. ``Python``
92+
- ``tracer_version`` e.g. ``0.29.0``
93+
- ``runtime-id`` e.g. `e4724609efa84cf58424a8b1ef44b17d`
94+
"""
95+
96+
def collect_fn(self, keys):
97+
tags = super(PlatformTagCollectorV2, self).collect_fn(keys)
98+
tags.append(("runtime-id", get_runtime_id()))
99+
return tags

ddtrace/settings/_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,10 @@ def __init__(self):
554554
self._runtime_metrics_enabled = _get_config(
555555
"DD_RUNTIME_METRICS_ENABLED", False, asbool, "OTEL_METRICS_EXPORTER"
556556
)
557+
self._runtime_metrics_runtim_id_enabled = _get_config("DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED", False, asbool)
558+
self._experimental_features_enabled = _get_config(
559+
"DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", set(), lambda x: set(x.strip().upper().split(","))
560+
)
557561

558562
self._128_bit_trace_id_enabled = _get_config("DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", True, asbool)
559563

docs/configuration.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,25 @@ Other
855855
These metrics track the memory management and concurrency of the python runtime.
856856
Refer to the following `docs <https://docs.datadoghq.com/tracing/metrics/runtime_metrics/python/>` _ for more information.
857857

858+
DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED:
859+
type: Boolean
860+
default: False
861+
version_added:
862+
v3.2.0: Adds initial support
863+
864+
description: |
865+
Adds support for tagging runtime metrics with the current runtime ID. This is useful for tracking runtime metrics across multiple processes.
866+
Refer to the following `docs <https://docs.datadoghq.com/tracing/metrics/runtime_metrics/python/>` _ for more information.
867+
868+
DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED:
869+
type: string
870+
version_added:
871+
v3.2.0: Adds initial support and support for enabling experimental runtime metrics.
872+
default: ""
873+
874+
description: |
875+
Enables support for experimental ddtrace configurations. The supported configurations are: ``DD_RUNTIME_METRICS_ENABLED``.
876+
858877
DD_SUBPROCESS_SENSITIVE_WILDCARDS:
859878
type: String
860879

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
runtime_metrics: Adds support for sending runtime metrics as gauge metrics (instead of distributions). To enable this feature set ``DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED=DD_RUNTIME_METRICS_ENABLED``.
5+
- |
6+
runtime_metrics: Adds support for tagging runtime metrics with the current runtime ID. To enable tagging, set ``DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED=True``.

tests/runtime/test_runtime_metrics_api.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,57 @@ def test_runtime_metrics_enable_environ(monkeypatch, environ):
196196
)
197197
finally:
198198
RuntimeMetrics.disable()
199+
200+
201+
@pytest.mark.subprocess(parametrize={"DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED": ["true", "false"]})
202+
def test_runtime_metrics_experimental_runtime_tag():
203+
"""
204+
When runtime metrics is enabled and DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED=DD_RUNTIME_METRICS_ENABLED
205+
Runtime metrics worker starts and submits gauge metrics instead of distribution metrics
206+
"""
207+
import os
208+
209+
from ddtrace.internal.runtime import get_runtime_id
210+
from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker
211+
from ddtrace.internal.service import ServiceStatus
212+
213+
RuntimeWorker.enable()
214+
assert RuntimeWorker._instance is not None
215+
216+
worker_instance = RuntimeWorker._instance
217+
assert worker_instance.status == ServiceStatus.RUNNING
218+
219+
runtime_id_tag = f"runtime-id:{get_runtime_id()}"
220+
if os.environ["DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED"] == "true":
221+
assert runtime_id_tag in worker_instance._platform_tags, worker_instance._platform_tags
222+
elif os.environ["DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED"] == "false":
223+
assert runtime_id_tag not in worker_instance._platform_tags, worker_instance._platform_tags
224+
else:
225+
raise pytest.fail("Invalid value for DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED")
226+
227+
228+
@pytest.mark.subprocess(
229+
parametrize={"DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED": ["DD_RUNTIME_METRICS_ENABLED,someotherfeature", ""]},
230+
err=None,
231+
)
232+
def test_runtime_metrics_experimental_metric_type():
233+
"""
234+
When runtime metrics is enabled and DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED=DD_RUNTIME_METRICS_ENABLED
235+
Runtime metrics worker starts and submits gauge metrics instead of distribution metrics
236+
"""
237+
import os
238+
239+
from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker
240+
from ddtrace.internal.service import ServiceStatus
241+
242+
RuntimeWorker.enable()
243+
assert RuntimeWorker._instance is not None
244+
245+
worker_instance = RuntimeWorker._instance
246+
assert worker_instance.status == ServiceStatus.RUNNING
247+
if "DD_RUNTIME_METRICS_ENABLED" in os.environ["DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED"]:
248+
assert worker_instance.send_metric == worker_instance._dogstatsd_client.gauge, worker_instance.send_metric
249+
else:
250+
assert (
251+
worker_instance.send_metric == worker_instance._dogstatsd_client.distribution
252+
), worker_instance.send_metric

tests/telemetry/test_writer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
465465
{"name": "DD_TRACE_COMPUTE_STATS", "origin": "env_var", "value": True},
466466
{"name": "DD_TRACE_DEBUG", "origin": "env_var", "value": True},
467467
{"name": "DD_TRACE_ENABLED", "origin": "env_var", "value": False},
468+
{"name": "DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "origin": "default", "value": "set()"},
469+
{"name": "DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED", "origin": "default", "value": False},
468470
{"name": "DD_TRACE_HEADER_TAGS", "origin": "default", "value": ""},
469471
{"name": "DD_TRACE_HEALTH_METRICS_ENABLED", "origin": "env_var", "value": True},
470472
{"name": "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING", "origin": "default", "value": "true"},

tests/tracer/runtime/test_metric_collectors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ def collect_fn(self, keys):
2828
class TestPSUtilRuntimeMetricCollector(BaseTestCase):
2929
def test_metrics(self):
3030
collector = PSUtilRuntimeMetricCollector()
31-
for _, value in collector.collect(PSUTIL_RUNTIME_METRICS):
31+
for metric_name, value in collector.collect(PSUTIL_RUNTIME_METRICS):
3232
self.assertIsNotNone(value)
33+
self.assertRegex(metric_name, r"^runtime.python\..*")
3334

3435
def test_static_metrics(self):
3536
import os
@@ -127,8 +128,9 @@ def thread_stopper(stop_event):
127128
class TestGCRuntimeMetricCollector(BaseTestCase):
128129
def test_metrics(self):
129130
collector = GCRuntimeMetricCollector()
130-
for _, value in collector.collect(GC_RUNTIME_METRICS):
131+
for metric_name, value in collector.collect(GC_RUNTIME_METRICS):
131132
self.assertIsNotNone(value)
133+
self.assertRegex(metric_name, r"^runtime.python\..*")
132134

133135
def test_gen1_changes(self):
134136
# disable gc

tests/tracer/runtime/test_runtime_metrics.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ def test_runtime_tags_empty():
8181
assert set(tags.keys()) == set(["lang", "lang_interpreter", "lang_version", "tracer_version"])
8282

8383

84+
@pytest.mark.subprocess()
85+
def test_runtime_platformv2_tags():
86+
from ddtrace.internal.runtime.runtime_metrics import PlatformTagsV2
87+
88+
tags = list(PlatformTagsV2())
89+
assert len(tags) == 5
90+
91+
tags = dict(tags)
92+
# Ensure runtime-id is present along with all the v1 tags
93+
assert set(tags.keys()) == set(["lang", "lang_interpreter", "lang_version", "tracer_version", "runtime-id"])
94+
95+
8496
@pytest.mark.subprocess(env={"DD_SERVICE": "my-service", "DD_ENV": "test-env", "DD_VERSION": "1.2.3"})
8597
def test_runtime_tags_usm():
8698
from ddtrace.internal.runtime.runtime_metrics import TracerTags

0 commit comments

Comments
 (0)