Skip to content

Commit 7955b92

Browse files
authored
chore: introduce APM_TRACING RC product (#11980)
We introduce the APM_TRACING remote configuration product that allows dispatching remote configuration to the library for remote enablement/ configuration of library components and features. ## Checklist - [ ] 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 - [ ] 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)
1 parent 85d2bf7 commit 7955b92

File tree

10 files changed

+196
-126
lines changed

10 files changed

+196
-126
lines changed

ddtrace/_trace/product.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import enum
12
import os
3+
import typing as t
24

35
from envier import En
46

57
from ddtrace.internal.utils.formats import asbool
68
from ddtrace.internal.utils.formats import parse_tags_str
9+
from ddtrace.settings.http import HttpConfig
710

811

912
requires = ["remote-configuration"]
@@ -63,3 +66,54 @@ def at_exit(join=False):
6366

6467
if tracer.enabled:
6568
tracer._atexit()
69+
70+
71+
class APMCapabilities(enum.IntFlag):
72+
APM_TRACING_SAMPLE_RATE = 1 << 12
73+
APM_TRACING_LOGS_INJECTION = 1 << 13
74+
APM_TRACING_HTTP_HEADER_TAGS = 1 << 14
75+
APM_TRACING_CUSTOM_TAGS = 1 << 15
76+
APM_TRACING_ENABLED = 1 << 19
77+
APM_TRACING_SAMPLE_RULES = 1 << 29
78+
79+
80+
def apm_tracing_rc(lib_config):
81+
from ddtrace import config
82+
83+
base_rc_config: t.Dict[str, t.Any] = {n: None for n in config._config}
84+
85+
if "tracing_sampling_rules" in lib_config or "tracing_sampling_rate" in lib_config:
86+
global_sampling_rate = lib_config.get("tracing_sampling_rate")
87+
trace_sampling_rules = lib_config.get("tracing_sampling_rules") or []
88+
# returns None if no rules
89+
trace_sampling_rules = config._convert_rc_trace_sampling_rules(trace_sampling_rules, global_sampling_rate)
90+
if trace_sampling_rules:
91+
base_rc_config["_trace_sampling_rules"] = trace_sampling_rules
92+
93+
if "log_injection_enabled" in lib_config:
94+
base_rc_config["_logs_injection"] = lib_config["log_injection_enabled"]
95+
96+
if "tracing_tags" in lib_config:
97+
tags = lib_config["tracing_tags"]
98+
if tags:
99+
tags = config._format_tags(lib_config["tracing_tags"])
100+
base_rc_config["tags"] = tags
101+
102+
if "tracing_enabled" in lib_config and lib_config["tracing_enabled"] is not None:
103+
base_rc_config["_tracing_enabled"] = asbool(lib_config["tracing_enabled"])
104+
105+
if "tracing_header_tags" in lib_config:
106+
tags = lib_config["tracing_header_tags"]
107+
if tags:
108+
tags = config._format_tags(lib_config["tracing_header_tags"])
109+
base_rc_config["_trace_http_header_tags"] = tags
110+
111+
config._set_config_items([(k, v, "remote_config") for k, v in base_rc_config.items()])
112+
113+
# unconditionally handle the case where header tags have been unset
114+
header_tags_conf = config._config["_trace_http_header_tags"]
115+
env_headers = header_tags_conf._env_value or {}
116+
code_headers = header_tags_conf._code_value or {}
117+
non_rc_header_tags = {**code_headers, **env_headers}
118+
selected_header_tags = base_rc_config.get("_trace_http_header_tags") or non_rc_header_tags
119+
config._http = HttpConfig(header_tags=selected_header_tags)

ddtrace/internal/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ gets extended to add support for additional features.
3838
| Attribute | Description |
3939
|-----------|-------------|
4040
| `requires: list[str]` | A list of other product names that the product depends on |
41-
| `config: DDConfig` | A configuration object; when an instance of `DDConfig`, configuration telemetry is automatically reported |
41+
| `config: DDConfig` | A configuration object; when an instance of `DDConfig`, configuration telemetry is automatically reported |
42+
| `APMCapabilities: Type[enum.IntFlag]` | A set of capabilities that the product provides |
43+
| `apm_tracing_rc: (dict) -> None` | Product-specific remote configuration handler (e.g. remote enablement) |

ddtrace/internal/remoteconfig/products/__init__.py

Whitespace-only changes.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import typing as t
2+
3+
from ddtrace import config
4+
from ddtrace.internal.core.event_hub import dispatch
5+
from ddtrace.internal.core.event_hub import on
6+
from ddtrace.internal.logger import get_logger
7+
from ddtrace.internal.remoteconfig import Payload
8+
from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector
9+
from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher
10+
from ddtrace.internal.remoteconfig._pubsub import PubSub
11+
from ddtrace.internal.remoteconfig._subscribers import RemoteConfigSubscriber
12+
13+
14+
requires = ["remote-configuration"]
15+
16+
17+
log = get_logger(__name__)
18+
19+
20+
def _rc_callback(payloads: t.Sequence[Payload]) -> None:
21+
for payload in payloads:
22+
if payload.metadata is None or (content := payload.content) is None:
23+
continue
24+
25+
if (service_target := t.cast(t.Optional[dict], content.get("service_target"))) is not None:
26+
if (service := t.cast(str, service_target.get("service"))) is not None and service != config.service:
27+
continue
28+
29+
if (env := t.cast(str, service_target.get("env"))) is not None and env != config.env:
30+
continue
31+
32+
if (lib_config := t.cast(dict, content.get("lib_config"))) is not None:
33+
dispatch("apm-tracing.rc", (lib_config,))
34+
35+
36+
class APMTracingAdapter(PubSub):
37+
__publisher_class__ = RemoteConfigPublisher
38+
__subscriber_class__ = RemoteConfigSubscriber
39+
__shared_data__ = PublisherSubscriberConnector()
40+
41+
def __init__(self):
42+
self._publisher = self.__publisher_class__(self.__shared_data__)
43+
self._subscriber = self.__subscriber_class__(self.__shared_data__, _rc_callback, "APM_TRACING")
44+
45+
46+
def post_preload():
47+
pass
48+
49+
50+
def start():
51+
if config._remote_config_enabled:
52+
from ddtrace.internal.products import manager
53+
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
54+
55+
remoteconfig_poller.register(
56+
"APM_TRACING",
57+
APMTracingAdapter(),
58+
restart_on_fork=True,
59+
capabilities=[
60+
cap for product in manager.__products__.values() for cap in getattr(product, "APMCapabilities", [])
61+
],
62+
)
63+
64+
# Register remote config handlers
65+
for name, product in manager.__products__.items():
66+
if (rc_handler := getattr(product, "apm_tracing_rc", None)) is not None:
67+
on("apm-tracing.rc", rc_handler, name)
68+
69+
70+
def restart(join=False):
71+
pass
72+
73+
74+
def stop(join=False):
75+
if config._remote_config_enabled:
76+
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
77+
78+
remoteconfig_poller.unregister("APM_TRACING")
79+
80+
81+
def at_exit(join=False):
82+
stop(join=join)

ddtrace/internal/remoteconfig/product.py renamed to ddtrace/internal/remoteconfig/products/client.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,7 @@
1-
import enum
2-
31
from ddtrace import config
4-
from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector
5-
from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher
6-
from ddtrace.internal.remoteconfig._pubsub import PubSub
7-
from ddtrace.internal.remoteconfig._pubsub import RemoteConfigSubscriber
82
from ddtrace.internal.remoteconfig.client import config as rc_config
93

104

11-
class GlobalConfigPubSub(PubSub):
12-
__publisher_class__ = RemoteConfigPublisher
13-
__subscriber_class__ = RemoteConfigSubscriber
14-
__shared_data__ = PublisherSubscriberConnector()
15-
16-
def __init__(self, callback):
17-
self._publisher = self.__publisher_class__(self.__shared_data__, None)
18-
self._subscriber = self.__subscriber_class__(self.__shared_data__, callback, "GlobalConfig")
19-
20-
21-
class Capabilities(enum.IntFlag):
22-
APM_TRACING_SAMPLE_RATE = 1 << 12
23-
APM_TRACING_LOGS_INJECTION = 1 << 13
24-
APM_TRACING_HTTP_HEADER_TAGS = 1 << 14
25-
APM_TRACING_CUSTOM_TAGS = 1 << 15
26-
APM_TRACING_ENABLED = 1 << 19
27-
APM_TRACING_SAMPLE_RULES = 1 << 29
28-
29-
305
# TODO: Modularize better into their own respective components
316
def _register_rc_products() -> None:
327
"""Enable fetching configuration from Datadog."""
@@ -35,10 +10,8 @@ def _register_rc_products() -> None:
3510
from ddtrace.internal.flare.handler import _tracerFlarePubSub
3611
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
3712

38-
remoteconfig_pubsub = GlobalConfigPubSub(config._handle_remoteconfig)
3913
flare = Flare(trace_agent_url=config._trace_agent_url, api_key=config._dd_api_key, ddconfig=config.__dict__)
4014
tracerflare_pubsub = _tracerFlarePubSub()(_handle_tracer_flare, flare)
41-
remoteconfig_poller.register("APM_TRACING", remoteconfig_pubsub, capabilities=Capabilities)
4215
remoteconfig_poller.register("AGENT_CONFIG", tracerflare_pubsub)
4316
remoteconfig_poller.register("AGENT_TASK", tracerflare_pubsub)
4417

ddtrace/settings/_config.py

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -582,10 +582,7 @@ def __init__(self):
582582
x_datadog_tags_max_length = _get_config("DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH", 512, int)
583583
if x_datadog_tags_max_length < 0:
584584
log.warning(
585-
(
586-
"Invalid value %r provided for DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, "
587-
"only non-negative values allowed"
588-
),
585+
("Invalid value %r provided for DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, only non-negative values allowed"),
589586
x_datadog_tags_max_length,
590587
)
591588
x_datadog_tags_max_length = 0
@@ -806,66 +803,6 @@ def _get_source(self, item):
806803
# type: (str) -> str
807804
return self._config[item].source()
808805

809-
def _handle_remoteconfig(self, data_list, test_tracer=None):
810-
# data_list is a list of Payload objects
811-
# type: (Any, Any) -> None
812-
813-
if len(data_list) == 0:
814-
log.warning("unexpected number of RC payloads")
815-
return
816-
data = [payload.content for payload in data_list]
817-
818-
# Check if 'lib_config' is a key in the dictionary since other items can be sent in the payload
819-
config = None
820-
for config_item in data:
821-
if isinstance(config_item, Dict):
822-
if "lib_config" in config_item:
823-
config = config_item
824-
break
825-
826-
# If no data is submitted then the RC config has been deleted. Revert the settings.
827-
base_rc_config = {n: None for n in self._config}
828-
829-
if config and "lib_config" in config:
830-
lib_config = config["lib_config"]
831-
if "tracing_sampling_rules" in lib_config or "tracing_sampling_rate" in lib_config:
832-
global_sampling_rate = lib_config.get("tracing_sampling_rate")
833-
trace_sampling_rules = lib_config.get("tracing_sampling_rules") or []
834-
# returns None if no rules
835-
trace_sampling_rules = self._convert_rc_trace_sampling_rules(trace_sampling_rules, global_sampling_rate)
836-
if trace_sampling_rules:
837-
base_rc_config["_trace_sampling_rules"] = trace_sampling_rules # type: ignore[assignment]
838-
839-
if "log_injection_enabled" in lib_config:
840-
base_rc_config["_logs_injection"] = lib_config["log_injection_enabled"]
841-
842-
if "tracing_tags" in lib_config:
843-
tags = lib_config["tracing_tags"]
844-
if tags:
845-
tags = self._format_tags(lib_config["tracing_tags"])
846-
base_rc_config["tags"] = tags
847-
848-
if "tracing_enabled" in lib_config and lib_config["tracing_enabled"] is not None:
849-
base_rc_config["_tracing_enabled"] = asbool(lib_config["tracing_enabled"]) # type: ignore[assignment]
850-
851-
if "tracing_header_tags" in lib_config:
852-
tags = lib_config["tracing_header_tags"]
853-
if tags:
854-
tags = self._format_tags(lib_config["tracing_header_tags"])
855-
base_rc_config["_trace_http_header_tags"] = tags
856-
self._set_config_items([(k, v, "remote_config") for k, v in base_rc_config.items()])
857-
# called unconditionally to handle the case where header tags have been unset
858-
self._handle_remoteconfig_header_tags(base_rc_config)
859-
860-
def _handle_remoteconfig_header_tags(self, base_rc_config):
861-
"""Implements precedence order between remoteconfig header tags from code, env, and RC"""
862-
header_tags_conf = self._config["_trace_http_header_tags"]
863-
env_headers = header_tags_conf._env_value or {}
864-
code_headers = header_tags_conf._code_value or {}
865-
non_rc_header_tags = {**code_headers, **env_headers}
866-
selected_header_tags = base_rc_config.get("_trace_http_header_tags") or non_rc_header_tags
867-
self._http = HttpConfig(header_tags=selected_header_tags)
868-
869806
def _format_tags(self, tags: List[Union[str, Dict]]) -> Dict[str, str]:
870807
if not tags:
871808
return {}

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ ddtrace = "ddtrace.contrib.internal.pytest.plugin"
5959
"ddtrace.pytest_benchmark" = "ddtrace.contrib.internal.pytest_benchmark.plugin"
6060

6161
[project.entry-points.'ddtrace.products']
62+
"apm-tracing-rc" = "ddtrace.internal.remoteconfig.products.apm_tracing"
6263
"code-origin-for-spans" = "ddtrace.debugging._products.code_origin.span"
6364
"dynamic-instrumentation" = "ddtrace.debugging._products.dynamic_instrumentation"
6465
"exception-replay" = "ddtrace.debugging._products.exception_replay"
6566
"live-debugger" = "ddtrace.debugging._products.live_debugger"
6667
"error-tracking" = "ddtrace.errortracking.product"
67-
"remote-configuration" = "ddtrace.internal.remoteconfig.product"
68+
"remote-configuration" = "ddtrace.internal.remoteconfig.products.client"
6869
"symbol-database" = "ddtrace.internal.symbol_db.product"
6970
"appsec" = "ddtrace.internal.appsec.product"
7071
"iast" = "ddtrace.internal.iast.product"

riotfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT
208208
name="integration",
209209
# Enabling coverage for integration tests breaks certain tests in CI
210210
# Also, running two separate pytest sessions, the ``civisibility`` one with --no-ddtrace
211-
command="pytest --no-ddtrace --no-cov --ignore-glob='*civisibility*' {cmdargs} tests/integration/",
211+
command="pytest -vv --no-ddtrace --no-cov --ignore-glob='*civisibility*' {cmdargs} tests/integration/",
212212
pkgs={"msgpack": [latest], "coverage": latest, "pytest-randomly": latest},
213213
pys=select_pys(),
214214
venvs=[

tests/integration/test_settings.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,28 @@ def test_remoteconfig_sampling_rate_default(test_agent_session, run_python_code_
136136
"""
137137
from ddtrace import config, tracer
138138
from tests.internal.test_settings import _base_rc_config
139+
from tests.internal.test_settings import call_apm_tracing_rc
139140
140141
with tracer.trace("test") as span:
141142
pass
142143
assert span.get_metric("_dd.rule_psr") is None
143144
144-
config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.5}))
145+
call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.5}))
145146
with tracer.trace("test") as span:
146147
pass
147148
assert span.get_metric("_dd.rule_psr") == 0.5
148149
149-
config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": None}))
150+
call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None}))
150151
with tracer.trace("test") as span:
151152
pass
152153
assert span.get_metric("_dd.rule_psr") is None, "Unsetting remote config trace sample rate"
153154
154-
config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.8}))
155+
call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.8}))
155156
with tracer.trace("test") as span:
156157
pass
157158
assert span.get_metric("_dd.rule_psr") == 0.8
158159
159-
config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": None}))
160+
call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None}))
160161
with tracer.trace("test") as span:
161162
pass
162163
assert span.get_metric("_dd.rule_psr") is None, "(second time) unsetting remote config trace sample rate"
@@ -182,8 +183,9 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod
182183
"""
183184
from ddtrace import config, tracer
184185
from tests.internal.test_settings import _base_rc_config
186+
from tests.internal.test_settings import call_apm_tracing_rc
185187
186-
config._handle_remoteconfig(
188+
call_apm_tracing_rc(
187189
_base_rc_config(
188190
{
189191
"tracing_sampling_rules": [
@@ -230,8 +232,9 @@ def test_remoteconfig_header_tags_telemetry(test_agent_session, run_python_code_
230232
from ddtrace import config, tracer
231233
from ddtrace.contrib import trace_utils
232234
from tests.internal.test_settings import _base_rc_config
235+
from tests.internal.test_settings import call_apm_tracing_rc
233236
234-
config._handle_remoteconfig(_base_rc_config({
237+
call_apm_tracing_rc(_base_rc_config({
235238
"tracing_header_tags": [
236239
{"header": "used", "tag_name":"header_tag_69"},
237240
{"header": "unused", "tag_name":"header_tag_70"},

0 commit comments

Comments
 (0)