Skip to content

Commit 949a1a5

Browse files
mabdinurerikayasuda
authored andcommitted
chore(telemetry): report telemetry for all config sources (#14170)
## 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)
1 parent ffb2b84 commit 949a1a5

File tree

7 files changed

+173
-88
lines changed

7 files changed

+173
-88
lines changed

ddtrace/internal/telemetry/__init__.py

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,6 @@
2525
__all__ = ["telemetry_writer"]
2626

2727

28-
def report_config_telemetry(effective_env, val, source, otel_env, config_id):
29-
if effective_env == otel_env:
30-
# We only report the raw value for OpenTelemetry configurations, we should make this consistent
31-
raw_val = os.environ.get(effective_env, "").lower()
32-
telemetry_writer.add_configuration(effective_env, raw_val, source)
33-
else:
34-
if otel_env is not None and otel_env in os.environ:
35-
if source in ("fleet_stable_config", "env_var"):
36-
_hiding_otel_config(otel_env, effective_env)
37-
else:
38-
_invalid_otel_config(otel_env)
39-
telemetry_writer.add_configuration(effective_env, val, source, config_id)
40-
41-
4228
def get_config(
4329
envs: t.Union[str, t.List[str]],
4430
default: t.Any = None,
@@ -47,63 +33,75 @@ def get_config(
4733
report_telemetry=True,
4834
) -> t.Any:
4935
"""Retrieve a configuration value in order of precedence:
50-
1. Fleet stable config
36+
1. Fleet stable config (highest)
5137
2. Datadog env vars
5238
3. OpenTelemetry env vars
5339
4. Local stable config
54-
5. Default value
40+
5. Default value (lowest)
41+
42+
Reports telemetry for every detected configuration source.
5543
"""
5644
if isinstance(envs, str):
5745
envs = [envs]
58-
source = ""
59-
effective_env = ""
60-
val = None
61-
config_id = None
62-
# Get configurations from fleet stable config
46+
47+
effective_val = default
48+
telemetry_name = envs[0]
49+
if report_telemetry:
50+
telemetry_writer.add_configuration(telemetry_name, default, "default")
51+
52+
for env in envs:
53+
if env in LOCAL_CONFIG:
54+
val = LOCAL_CONFIG[env]
55+
if modifier:
56+
val = modifier(val)
57+
58+
if report_telemetry:
59+
telemetry_writer.add_configuration(telemetry_name, val, "local_stable_config")
60+
effective_val = val
61+
break
62+
63+
if otel_env is not None and otel_env in os.environ:
64+
raw_val, parsed_val = parse_otel_env(otel_env)
65+
if parsed_val is not None:
66+
val = parsed_val
67+
if modifier:
68+
val = modifier(val)
69+
70+
if report_telemetry:
71+
# OpenTelemetry configurations always report the raw value
72+
telemetry_writer.add_configuration(telemetry_name, raw_val, "otel_env_var")
73+
effective_val = val
74+
else:
75+
_invalid_otel_config(otel_env)
76+
77+
for env in envs:
78+
if env in os.environ:
79+
val = os.environ[env]
80+
if modifier:
81+
val = modifier(val)
82+
83+
if report_telemetry:
84+
telemetry_writer.add_configuration(telemetry_name, val, "env_var")
85+
if otel_env is not None and otel_env in os.environ:
86+
_hiding_otel_config(otel_env, env)
87+
effective_val = val
88+
break
89+
6390
for env in envs:
6491
if env in FLEET_CONFIG:
65-
source = "fleet_stable_config"
66-
effective_env = env
6792
val = FLEET_CONFIG[env]
6893
config_id = FLEET_CONFIG_IDS.get(env)
94+
if modifier:
95+
val = modifier(val)
96+
97+
if report_telemetry:
98+
telemetry_writer.add_configuration(telemetry_name, val, "fleet_stable_config", config_id)
99+
if otel_env is not None and otel_env in os.environ:
100+
_hiding_otel_config(otel_env, env)
101+
effective_val = val
69102
break
70-
# Get configurations from datadog env vars
71-
if val is None:
72-
for env in envs:
73-
if env in os.environ:
74-
source = "env_var"
75-
effective_env = env
76-
val = os.environ[env]
77-
break
78-
# Get configurations from otel env vars
79-
if val is None:
80-
if otel_env is not None and otel_env in os.environ:
81-
parsed_val = parse_otel_env(otel_env)
82-
if parsed_val is not None:
83-
source = "env_var"
84-
effective_env = otel_env
85-
val = parsed_val
86-
# Get configurations from local stable config
87-
if val is None:
88-
for env in envs:
89-
if env in LOCAL_CONFIG:
90-
source = "local_stable_config"
91-
effective_env = env
92-
val = LOCAL_CONFIG[env]
93-
break
94-
# Convert the raw value to expected format, if a modifier is provided
95-
if val is not None and modifier:
96-
val = modifier(val)
97-
# If no value is found, use the default
98-
if val is None:
99-
effective_env = envs[0]
100-
val = default
101-
source = "default"
102-
# Report telemetry
103-
if report_telemetry:
104-
report_config_telemetry(effective_env, val, source, otel_env, config_id)
105103

106-
return val
104+
return effective_val
107105

108106

109107
telemetry_enabled = get_config("DD_INSTRUMENTATION_TELEMETRY_ENABLED", True, asbool, report_telemetry=False)

ddtrace/settings/_core.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ValueSource(str, Enum):
2020
CODE = "code"
2121
DEFAULT = "default"
2222
UNKNOWN = "unknown"
23+
OTEL_ENV_VAR = "otel_env_var"
2324

2425

2526
class DDConfig(Env):
@@ -57,6 +58,8 @@ def __init__(
5758

5859
if env_name in self.fleet_source:
5960
value_source = ValueSource.FLEET_STABLE_CONFIG
61+
elif env_name in self.env_source and env_name.upper().startswith("OTEL_"):
62+
value_source = ValueSource.OTEL_ENV_VAR
6063
elif env_name in self.env_source:
6164
value_source = ValueSource.ENV_VAR
6265
elif env_name in self.local_source:
@@ -75,5 +78,5 @@ def __init__(
7578
else:
7679
self.config_id = None
7780

78-
def value_source(self, env_name: str) -> ValueSource:
81+
def value_source(self, env_name: str) -> str:
7982
return self._value_source.get(env_name, ValueSource.UNKNOWN)

ddtrace/settings/_otel_remapper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,13 @@ def _remap_default(otel_value: str) -> Optional[str]:
155155
}
156156

157157

158-
def parse_otel_env(otel_env: str) -> Optional[str]:
158+
def parse_otel_env(otel_env: str) -> Tuple[str, Optional[str]]:
159159
_, otel_config_validator = ENV_VAR_MAPPINGS[otel_env]
160160
raw_value = os.environ.get(otel_env, "")
161161
if otel_env not in ("OTEL_RESOURCE_ATTRIBUTES", "OTEL_SERVICE_NAME"):
162162
# Resource attributes and service name are case-insensitive
163163
raw_value = raw_value.lower()
164164
mapped_value = otel_config_validator(raw_value)
165165
if mapped_value is None:
166-
return None
167-
return mapped_value
166+
return "", None
167+
return raw_value, mapped_value

tests/conftest.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -638,20 +638,21 @@ def get_configurations(self, name=None, ignores=None, remove_seq_id=False, effec
638638
configurations = []
639639
events_with_configs = self.get_events("app-started") + self.get_events("app-client-configuration-change")
640640
for event in events_with_configs:
641-
for c in event["payload"]["configuration"]:
642-
config = c.copy()
643-
if remove_seq_id:
644-
config.pop("seq_id")
641+
for config in event["payload"]["configuration"]:
645642
if config["name"] == name or (name is None and config["name"] not in ignores):
646643
configurations.append(config)
647644

645+
configurations.sort(key=lambda x: x["seq_id"])
648646
if effective:
649647
config_map = {}
650648
for c in configurations:
651649
config_map[c["name"]] = c
652650
configurations = list(config_map.values())
653651

654-
configurations.sort(key=lambda x: x["name"])
652+
if remove_seq_id:
653+
for c in configurations:
654+
c.pop("seq_id")
655+
655656
return configurations
656657

657658

tests/opentelemetry/test_config.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def _global_sampling_rule():
3737
"OTEL_SDK_DISABLED": "True",
3838
"DD_TRACE_OTEL_ENABLED": "True",
3939
},
40-
err=b"Setting OTEL_LOGS_EXPORTER to warning is not supported by ddtrace, this configuration will be ignored.\n",
40+
err=b"Setting OTEL_LOGS_EXPORTER to warning is not supported by ddtrace, this configuration "
41+
b"will be ignored.\nTrace sampler set from always_off to parentbased_always_off; only parent based "
42+
b"sampling is supported.\nFollowing style not supported by ddtrace: jaegar.\n",
4143
)
4244
def test_dd_otel_mixed_env_configuration():
4345
from ddtrace import config
@@ -69,8 +71,9 @@ def test_dd_otel_mixed_env_configuration():
6971
"service.version=1.0,testtag1=random1,testtag2=random2,testtag3=random3,testtag4=random4",
7072
"OTEL_SDK_DISABLED": "False",
7173
},
72-
err=b"Setting OTEL_LOGS_EXPORTER to warning is not supported by ddtrace, "
73-
b"this configuration will be ignored.\nFollowing style not supported by ddtrace: jaegar.\n",
74+
err=b"Setting OTEL_LOGS_EXPORTER to warning is not supported by ddtrace, this configuration will be ignored.\n"
75+
b"Trace sampler set from always_off to parentbased_always_off; only parent based sampling is supported.\n"
76+
b"Following style not supported by ddtrace: jaegar.\n",
7477
)
7578
def test_dd_otel_missing_dd_env_configuration():
7679
from ddtrace import config

tests/telemetry/test_telemetry.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,33 @@ def test_installed_excepthook():
353353
assert telemetry_writer._enabled is True
354354
telemetry_writer.uninstall_excepthook()
355355
assert sys.excepthook.__name__ != "_telemetry_excepthook"
356+
357+
358+
def test_telemetry_multiple_sources(test_agent_session, run_python_code_in_subprocess):
359+
"""Test that a config is submitted for multiple sources with increasing seq_id"""
360+
361+
env = os.environ.copy()
362+
env["OTEL_TRACES_EXPORTER"] = "none"
363+
env["DD_TRACE_ENABLED"] = "false"
364+
env["_DD_INSTRUMENTATION_TELEMETRY_TESTS_FORCE_APP_STARTED"] = "true"
365+
366+
_, err, status, _ = run_python_code_in_subprocess(
367+
"from ddtrace import config; config._tracing_enabled = True", env=env
368+
)
369+
assert status == 0, err
370+
371+
configs = test_agent_session.get_configurations(name="DD_TRACE_ENABLED", remove_seq_id=False, effective=False)
372+
assert len(configs) == 4, configs
373+
374+
sorted_configs = sorted(configs, key=lambda x: x["seq_id"])
375+
assert sorted_configs[0]["value"] is True
376+
assert sorted_configs[0]["origin"] == "default"
377+
378+
assert sorted_configs[1]["value"] == "none"
379+
assert sorted_configs[1]["origin"] == "otel_env_var"
380+
381+
assert sorted_configs[2]["value"] is False
382+
assert sorted_configs[2]["origin"] == "env_var"
383+
384+
assert sorted_configs[3]["value"] is True
385+
assert sorted_configs[3]["origin"] == "code"

tests/telemetry/test_writer.py

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_app_started_event_configuration_override_asm(
7979
_, stderr, status, _ = run_python_code_in_subprocess("import ddtrace.auto", env=env)
8080
assert status == 0, stderr
8181

82-
configuration = test_agent_session.get_configurations(name=env_var, remove_seq_id=True)
82+
configuration = test_agent_session.get_configurations(name=env_var, remove_seq_id=True, effective=True)
8383
assert len(configuration) == 1, configuration
8484
assert configuration[0] == {"name": env_var, "origin": "env_var", "value": expected_value}
8585

@@ -300,6 +300,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
300300
ignores=["DD_TRACE_AGENT_URL", "DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"], remove_seq_id=True, effective=True
301301
)
302302
assert configurations
303+
configurations.sort(key=lambda x: x["name"])
303304

304305
expected = [
305306
{"name": "DD_AGENT_HOST", "origin": "default", "value": None},
@@ -994,32 +995,40 @@ def test_otel_config_telemetry(test_agent_session, run_python_code_in_subprocess
994995
_, stderr, status, _ = run_python_code_in_subprocess("import ddtrace", env=env)
995996
assert status == 0, stderr
996997

997-
configurations = {c["name"]: c for c in test_agent_session.get_configurations(remove_seq_id=True)}
998+
configurations = {c["name"]: c for c in test_agent_session.get_configurations(remove_seq_id=True, effective=True)}
998999

9991000
assert configurations["DD_SERVICE"] == {"name": "DD_SERVICE", "origin": "env_var", "value": "dd_service"}
1000-
assert configurations["OTEL_LOG_LEVEL"] == {"name": "OTEL_LOG_LEVEL", "origin": "env_var", "value": "debug"}
1001-
assert configurations["OTEL_PROPAGATORS"] == {
1002-
"name": "OTEL_PROPAGATORS",
1003-
"origin": "env_var",
1001+
assert configurations["DD_TRACE_DEBUG"] == {"name": "DD_TRACE_DEBUG", "origin": "otel_env_var", "value": "debug"}
1002+
assert configurations["DD_TRACE_PROPAGATION_STYLE_INJECT"] == {
1003+
"name": "DD_TRACE_PROPAGATION_STYLE_INJECT",
1004+
"origin": "otel_env_var",
10041005
"value": "tracecontext",
10051006
}
1006-
assert configurations["OTEL_TRACES_SAMPLER"] == {
1007-
"name": "OTEL_TRACES_SAMPLER",
1008-
"origin": "env_var",
1007+
assert configurations["DD_TRACE_PROPAGATION_STYLE_EXTRACT"] == {
1008+
"name": "DD_TRACE_PROPAGATION_STYLE_EXTRACT",
1009+
"origin": "otel_env_var",
1010+
"value": "tracecontext",
1011+
}
1012+
assert configurations["DD_TRACE_SAMPLING_RULES"] == {
1013+
"name": "DD_TRACE_SAMPLING_RULES",
1014+
"origin": "otel_env_var",
10091015
"value": "always_on",
10101016
}
1011-
assert configurations["OTEL_TRACES_EXPORTER"] == {
1012-
"name": "OTEL_TRACES_EXPORTER",
1013-
"origin": "env_var",
1017+
assert configurations["DD_TRACE_ENABLED"] == {
1018+
"name": "DD_TRACE_ENABLED",
1019+
"origin": "otel_env_var",
10141020
"value": "none",
10151021
}
1016-
assert configurations["OTEL_LOGS_EXPORTER"] == {"name": "OTEL_LOGS_EXPORTER", "origin": "env_var", "value": "otlp"}
1017-
assert configurations["OTEL_RESOURCE_ATTRIBUTES"] == {
1018-
"name": "OTEL_RESOURCE_ATTRIBUTES",
1019-
"origin": "env_var",
1022+
assert configurations["DD_TAGS"] == {
1023+
"name": "DD_TAGS",
1024+
"origin": "otel_env_var",
10201025
"value": "team=apm,component=web",
10211026
}
1022-
assert configurations["OTEL_SDK_DISABLED"] == {"name": "OTEL_SDK_DISABLED", "origin": "env_var", "value": "true"}
1027+
assert configurations["DD_TRACE_OTEL_ENABLED"] == {
1028+
"name": "DD_TRACE_OTEL_ENABLED",
1029+
"origin": "otel_env_var",
1030+
"value": "true",
1031+
}
10231032

10241033
env_hiding_metrics = test_agent_session.get_metrics("otel.env.hiding")
10251034
tags = [m["tags"] for m in env_hiding_metrics]
@@ -1093,3 +1102,44 @@ def test_redact_filename(filename, is_redacted):
10931102
"""Test file redaction logic"""
10941103
writer = TelemetryWriter(is_periodic=False)
10951104
assert writer._should_redact(filename) == is_redacted
1105+
1106+
1107+
def test_telemetry_writer_multiple_sources_config(telemetry_writer, test_agent_session):
1108+
"""Test that telemetry data is submitted for multiple sources with increasing seq_id"""
1109+
1110+
telemetry_writer.add_configuration("DD_SERVICE", "unamed_python_service", "default")
1111+
telemetry_writer.add_configuration("DD_SERVICE", "otel_service", "otel_env_var")
1112+
telemetry_writer.add_configuration("DD_SERVICE", "dd_service", "env_var")
1113+
telemetry_writer.add_configuration("DD_SERVICE", "monkey", "code")
1114+
telemetry_writer.add_configuration("DD_SERVICE", "baboon", "remote_config")
1115+
telemetry_writer.add_configuration("DD_SERVICE", "baboon", "fleet_stable_config")
1116+
1117+
telemetry_writer.periodic(force_flush=True)
1118+
1119+
configs = test_agent_session.get_configurations(name="DD_SERVICE", remove_seq_id=False, effective=False)
1120+
assert len(configs) == 6, configs
1121+
1122+
sorted_configs = sorted(configs, key=lambda x: x["seq_id"])
1123+
assert sorted_configs[0]["value"] == "unamed_python_service"
1124+
assert sorted_configs[0]["origin"] == "default"
1125+
assert sorted_configs[0]["seq_id"] == 1
1126+
1127+
assert sorted_configs[1]["value"] == "otel_service"
1128+
assert sorted_configs[1]["origin"] == "otel_env_var"
1129+
assert sorted_configs[1]["seq_id"] == 2
1130+
1131+
assert sorted_configs[2]["value"] == "dd_service"
1132+
assert sorted_configs[2]["origin"] == "env_var"
1133+
assert sorted_configs[2]["seq_id"] == 3
1134+
1135+
assert sorted_configs[3]["value"] == "monkey"
1136+
assert sorted_configs[3]["origin"] == "code"
1137+
assert sorted_configs[3]["seq_id"] == 4
1138+
1139+
assert sorted_configs[4]["value"] == "baboon"
1140+
assert sorted_configs[4]["origin"] == "remote_config"
1141+
assert sorted_configs[4]["seq_id"] == 5
1142+
1143+
assert sorted_configs[5]["value"] == "baboon"
1144+
assert sorted_configs[5]["origin"] == "fleet_stable_config"
1145+
assert sorted_configs[5]["seq_id"] == 6

0 commit comments

Comments
 (0)