Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
08c80b4
apply all changes from PR #9327, accomodated to master
mtoffl01 Aug 21, 2025
c62688b
merge with configId changes made in master
mtoffl01 Aug 21, 2025
125744a
nits: add configId in ConfigProvider methods where it was missing; fi…
mtoffl01 Aug 21, 2025
8becbc5
Fix 'test config id exists in ConfigCollector when using StableConfig…
mtoffl01 Aug 21, 2025
db60faf
introduce reportDefault
mtoffl01 Aug 21, 2025
750ba64
Call collect in BaseApplication.getLogInjectionEnabled; make getAppli…
mtoffl01 Aug 22, 2025
3006e7c
Fix call to getAppliedConfigSetting in ConfigCollector test
mtoffl01 Aug 22, 2025
bee84bf
Introduce ConfigValueResolver and ConfigMergeResolver
mtoffl01 Aug 22, 2025
bfcfd7e
getString methods only report non-null values to telemetry
mtoffl01 Aug 22, 2025
b411ea9
remove dependency on collectConfig for reReportToCollector and reRepo…
mtoffl01 Aug 22, 2025
d097300
Merge branch 'master' into mtoff/4-config-sources
mtoffl01 Aug 22, 2025
1ca7fd9
updating config collector to not override existing configs
mhlidd Aug 27, 2025
81db67d
Adding putDefault and adding unit tests
mhlidd Aug 28, 2025
1b81e8a
relaxing reReportToCollector restraint and adding unit test
mhlidd Aug 29, 2025
9b22670
cleanup
mhlidd Aug 29, 2025
cdf3a12
updating failing unit test
mhlidd Sep 10, 2025
7053f6c
abstracing ConfigCollector
mhlidd Sep 11, 2025
ff95cdc
adding support for proper seqId with Remote Config
mhlidd Sep 11, 2025
4094c53
abstracting away getString
mhlidd Sep 12, 2025
1d70667
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Sep 16, 2025
bdab76d
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Sep 16, 2025
2482933
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Sep 16, 2025
bc64730
nit: fix javadoc comments
mtoffl01 Sep 16, 2025
11b61f1
Modify getEnum, getList, getIntegerRange and getSet to report default…
mtoffl01 Sep 16, 2025
d7dc744
merge conflicts
mtoffl01 Sep 16, 2025
700f837
Fix merge conflicts in ConfigCollectorTest.groovy
mtoffl01 Sep 16, 2025
b190961
remove remoteConfig methods from ConfigCollector + highestSeqId
mtoffl01 Sep 17, 2025
779551d
remove serializenulls comment in TelemetryRequestBody
mtoffl01 Sep 17, 2025
94f6485
remove 'no usages' comment above unused ConfigCollector put method
mtoffl01 Sep 17, 2025
e796896
Simplify javadoc for putAll
mtoffl01 Sep 17, 2025
3997f56
remove javadoc for ABSENT_SEQ_ID
mtoffl01 Sep 17, 2025
9794631
revert getStringInternal changes
mtoffl01 Sep 17, 2025
84ab423
Introduce new getStringInternal, for getting string from non-default …
mtoffl01 Sep 17, 2025
a9c1f7a
Deprecate ConfigCollector constructor without sequence ID
mtoffl01 Sep 19, 2025
ff39a53
putRemote: ConfigCollector method for 'putting' from Remote Config or…
mtoffl01 Sep 22, 2025
0408e49
ConfigSetting.NON_DEFAULT_SEQ_ID: introduce new constant, migrate all…
mtoffl01 Sep 22, 2025
e3b307d
ConfigCollector.put: Delete unused function
mtoffl01 Sep 22, 2025
81187aa
Update internal-api/src/test/groovy/datadog/trace/api/ConfigCollector…
mtoffl01 Sep 22, 2025
0ffe413
replace 'def origin' assignment in ConfigCollectorTest with in-line use
mtoffl01 Sep 22, 2025
b5e1414
ConfigCollector.put: Remove deprecated function with zero uses
mtoffl01 Sep 22, 2025
b048c91
NEW_SUB_MAP: Define reusable lambda function as static field in Confi…
mtoffl01 Sep 22, 2025
0ef089d
updateAll: rename putAll to updateAll, and scope to Remote origin, only
mtoffl01 Sep 22, 2025
7ef2a9f
Merge branch 'mtoff/4-config-sources' of github.com:DataDog/dd-trace-…
mtoffl01 Sep 22, 2025
7508fc7
Fix updateAll
mtoffl01 Sep 22, 2025
49d3ced
Align naming of methods that report remote config to the ConfigCollector
mcculls Sep 23, 2025
fe9b376
Avoid need to peek into ConfigCollector internals
mcculls Sep 23, 2025
d9dac26
Restore atomic reporting of updates from remote-config
mcculls Sep 23, 2025
474cd40
Merge remote-tracking branch 'origin/master' into mtoff/4-config-sources
mcculls Sep 23, 2025
8e29e03
Merge remote-tracking branch 'origin/master' into mtoff/4-config-sources
mcculls Sep 23, 2025
365f5d6
Update utils/config-utils/src/main/java/datadog/trace/bootstrap/confi…
mtoffl01 Sep 23, 2025
4a91a8e
Update utils/config-utils/src/main/java/datadog/trace/bootstrap/confi…
mtoffl01 Sep 23, 2025
d9fe333
Update utils/config-utils/src/main/java/datadog/trace/bootstrap/confi…
mtoffl01 Sep 23, 2025
2626c62
Fix bug in reReportFinalResult that incorrectly reported CALCULATED f…
mtoffl01 Sep 23, 2025
5b5ad0d
Merge branch 'mtoff/4-config-sources' of github.com:DataDog/dd-trace-…
mtoffl01 Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import datadog.remoteconfig.state.ProductListener;
import datadog.trace.api.Config;
import datadog.trace.api.ConfigCollector;
import datadog.trace.api.ConfigOrigin;
import datadog.trace.api.ProductActivation;
import datadog.trace.api.UserIdCollectionMode;
import datadog.trace.api.telemetry.LogCollector;
Expand Down Expand Up @@ -563,7 +562,7 @@ private void setAppSecActivation(final AppSecFeatures.Asm asm) {
} else {
newState = asm.enabled;
// Report AppSec activation change via telemetry when modified via remote config
ConfigCollector.get().put(APPSEC_ENABLED, asm.enabled, ConfigOrigin.REMOTE);
ConfigCollector.get().putRemoteConfig(APPSEC_ENABLED, asm.enabled);
}
if (AppSecSystem.isActive() != newState) {
log.info("AppSec {} (runtime)", newState ? "enabled" : "disabled");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public void run() throws InterruptedException {
}

private static Object getLogInjectionEnabled() {
ConfigSetting configSetting = ConfigCollector.get().collect().get(LOGS_INJECTION_ENABLED);
ConfigSetting configSetting =
ConfigCollector.getAppliedConfigSetting(
LOGS_INJECTION_ENABLED, ConfigCollector.get().collect());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking into ways to remove this call, as smoke tests shouldn't be relying on internal-api

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (configSetting == null) {
return null;
}
Expand Down
7 changes: 5 additions & 2 deletions internal-api/src/main/java/datadog/trace/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_MESSAGES_SEPARATE_TRACES;
import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_TAG_SESSION_ID;
import static datadog.trace.api.ConfigDefaults.DEFAULT_WRITER_BAGGAGE_INJECT;
import static datadog.trace.api.ConfigSetting.DEFAULT_SEQ_ID;
import static datadog.trace.api.DDTags.APM_ENABLED;
import static datadog.trace.api.DDTags.HOST_TAG;
import static datadog.trace.api.DDTags.INTERNAL_HOST_NAME;
Expand Down Expand Up @@ -5219,7 +5220,8 @@ private static boolean isWindowsOS() {
private static String getEnv(String name) {
String value = EnvironmentVariables.get(name);
if (value != null) {
ConfigCollector.get().put(name, value, ConfigOrigin.ENV);
// Reporting default sequence id to be consistent with ConfigProvider
ConfigCollector.get().put(name, value, ConfigOrigin.ENV, DEFAULT_SEQ_ID);
}
return value;
}
Expand All @@ -5242,7 +5244,8 @@ private static String getProp(String name) {
private static String getProp(String name, String def) {
String value = SystemProperties.getOrDefault(name, def);
if (value != null) {
ConfigCollector.get().put(name, value, ConfigOrigin.JVM_PROP);
// Reporting default sequence id to be consistent with ConfigProvider
ConfigCollector.get().put(name, value, ConfigOrigin.JVM_PROP, DEFAULT_SEQ_ID);
}
return value;
}
Expand Down
82 changes: 61 additions & 21 deletions internal-api/src/main/java/datadog/trace/api/ConfigCollector.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package datadog.trace.api;

import static datadog.trace.api.ConfigOrigin.DEFAULT;
import static datadog.trace.api.ConfigOrigin.REMOTE;
import static datadog.trace.api.ConfigSetting.ABSENT_SEQ_ID;
import static datadog.trace.api.ConfigSetting.DEFAULT_SEQ_ID;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
Expand All @@ -16,47 +22,81 @@ public class ConfigCollector {
private static final AtomicReferenceFieldUpdater<ConfigCollector, Map> COLLECTED_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ConfigCollector.class, Map.class, "collected");

private volatile Map<String, ConfigSetting> collected = new ConcurrentHashMap<>();
private volatile Map<ConfigOrigin, Map<String, ConfigSetting>> collected =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, why is the map ConfigOrigin -> Map<String, ConfigSetting>? would it be more performant for look ups if it was the other way around? Map<ConfigName, Map<ConfigOrigin, ConfigValue>>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reduces the number of nested maps from O(# of configs) to O(# of origins).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like ConfigCollector is a singleton and we're keeping all the configuration information in place the entire time. That could significantly increasing our static overhead which is a bit worrisome.

I think I'd prefer to see us just recompute the information as needed in the telemetry thread instead.

Copy link

@khanayan123 khanayan123 Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhlidd ^ I agree with @dougqh's comment. Most of this information is really only needed for the app-started event on tracer startup, no need to hold it in memory it can just be dropped. The only time telemetry is reported again is infrequently during app-client-configuration events which is mainly just remote config & in that scenario no need to read all sources again you only need to report the remote-config source and value with the current global seqId and the intake will be able to know what is currently active in the tracer.

Copy link
Contributor

@mhlidd mhlidd Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dougqh Is your concern the fact that we are keeping telemetry data throughout the entire tracer lifecycle? The telemetry client will call collect(link) when sending the app-started event, which returns the map of data and sets the ConfigCollector singleton to hold an empty map, effectively resetting the data, so I wouldn't expect there to be overhead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the same pattern as the previous implementation - config gets collected for reporting (but not processed) until the next telemetry cycle. At that point the collected map gets atomically swapped out, so that fresh config can be collected in a new map while telemetry reports the previous batch in the background. That way we avoid contention from having publishing and consuming threads work on the same map at the same time.

Eventually both sides end up with an empty map.

Also as for putting origin vs name first in the mapping - as Matt says, origin has very low cardinality, so that way we end up with only a few sub-maps. Whereas name has high cardinality, putting that first would lead to many tiny maps - each of which would have a lot of overhead compared to the data they contain.

Note we process the collected config data in the background on a separate thread, so any performance impact during processing won't affect application latency. When collecting it doesn't affect performance to put origin first.

new ConcurrentHashMap<>();

private volatile Map<String, AtomicInteger> highestSeqId = new ConcurrentHashMap<>();

public static ConfigCollector get() {
return INSTANCE;
}

// There are no non-test usages of this function
public void put(String key, Object value, ConfigOrigin origin) {
ConfigSetting setting = ConfigSetting.of(key, value, origin);
collected.put(key, setting);
put(key, value, origin, ABSENT_SEQ_ID, null);
}

public void putRemoteConfig(String key, Object value) {
int remoteSeqId =
highestSeqId.containsKey(key) ? highestSeqId.get(key).get() + 1 : DEFAULT_SEQ_ID + 1;
put(key, value, REMOTE, remoteSeqId, null);
}

public void put(String key, Object value, ConfigOrigin origin, int seqId) {
put(key, value, origin, seqId, null);
}

// There are no usages of this function
public void put(String key, Object value, ConfigOrigin origin, String configId) {
Copy link
Contributor Author

@mtoffl01 mtoffl01 Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we delete this since it's in internal-api?

ConfigSetting setting = ConfigSetting.of(key, value, origin, configId);
collected.put(key, setting);
put(key, value, origin, ABSENT_SEQ_ID, configId);
}

public void putAll(Map<String, Object> keysAndValues, ConfigOrigin origin) {
// attempt merge+replace to avoid collector seeing partial update
Map<String, ConfigSetting> merged =
new ConcurrentHashMap<>(keysAndValues.size() + collected.size());
for (Map.Entry<String, Object> entry : keysAndValues.entrySet()) {
ConfigSetting setting = ConfigSetting.of(entry.getKey(), entry.getValue(), origin);
merged.put(entry.getKey(), setting);
public void put(String key, Object value, ConfigOrigin origin, int seqId, String configId) {
ConfigSetting setting = ConfigSetting.of(key, value, origin, seqId, configId);
Map<String, ConfigSetting> configMap =
collected.computeIfAbsent(origin, k -> new ConcurrentHashMap<>());
configMap.put(key, setting); // replaces any previous value for this key at origin
highestSeqId.computeIfAbsent(key, k -> new AtomicInteger()).set(seqId);
}

// put method specifically for DEFAULT origins. We don't allow overrides for configs from DEFAULT
// origins
public void putDefault(String key, Object value) {
ConfigSetting setting = ConfigSetting.of(key, value, DEFAULT, DEFAULT_SEQ_ID);
Map<String, ConfigSetting> configMap =
collected.computeIfAbsent(DEFAULT, k -> new ConcurrentHashMap<>());
if (!configMap.containsKey(key) || configMap.get(key).value == null) {
configMap.put(key, setting);
}
while (true) {
Map<String, ConfigSetting> current = collected;
current.forEach(merged::putIfAbsent);
if (COLLECTED_UPDATER.compareAndSet(this, current, merged)) {
break; // success
}
// roll back to original update before next attempt
merged.keySet().retainAll(keysAndValues.keySet());
}

public void putRemoteConfigPayload(Map<String, Object> keysAndValues, ConfigOrigin origin) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all of the configs that call this method do so for dynamic instrumentation (as opposed to RC sourced pre-startup) then we can probably remove all the remote config logic (including highestSeqId). Since seq ID only counts in the app-started event

for (Map.Entry<String, Object> entry : keysAndValues.entrySet()) {
putRemoteConfig(entry.getKey(), entry.getValue());
}
}

@SuppressWarnings("unchecked")
public Map<String, ConfigSetting> collect() {
public Map<ConfigOrigin, Map<String, ConfigSetting>> collect() {
if (!collected.isEmpty()) {
return COLLECTED_UPDATER.getAndSet(this, new ConcurrentHashMap<>());
} else {
return Collections.emptyMap();
}
}

// NOTE: Only used to preserve legacy behavior for with smoke tests
public static ConfigSetting getAppliedConfigSetting(
String key, Map<ConfigOrigin, Map<String, ConfigSetting>> configMap) {
ConfigSetting best = null;
for (Map<String, ConfigSetting> originConfigMap : configMap.values()) {
ConfigSetting setting = originConfigMap.get(key);
if (setting != null) {
if (best == null || setting.seqId > best.seqId) {
best = setting;
}
}
}
return best;
}
}
26 changes: 22 additions & 4 deletions internal-api/src/main/java/datadog/trace/api/ConfigSetting.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public final class ConfigSetting {
public final String key;
public final Object value;
public final ConfigOrigin origin;

public final int seqId;
public static final int DEFAULT_SEQ_ID = 1;
public static final int ABSENT_SEQ_ID = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ABSENT_SEQ_ID represent?

Copy link
Contributor

@mhlidd mhlidd Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is a placeholder when creating ConfigResolvers in ConfigProvider. This should never actually be reported to telemetry.

Copy link
Contributor

@mhlidd mhlidd Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about this more, I don't see an application where this would be used in a final telemetry payload. Any call to ConfigSetting should be able to provide a valid seqId. The only reason to keep it is for backwards compatibility with existing tests that call it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that sounds good, lets add a comment to clarify the usage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I introduced ABSENT_SEQ_ID for code-as-documentation purposes, for unrelated legacy tests that create ConfigSettings without a seq ID


/** The config ID associated with this setting, or {@code null} if not applicable. */
public final String configId;

Expand All @@ -19,17 +24,27 @@ public final class ConfigSetting {
Arrays.asList("DD_API_KEY", "dd.api-key", "dd.profiling.api-key", "dd.profiling.apikey"));

public static ConfigSetting of(String key, Object value, ConfigOrigin origin) {
return new ConfigSetting(key, value, origin, null);
return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null);
}

public static ConfigSetting of(String key, Object value, ConfigOrigin origin, int seqId) {
return new ConfigSetting(key, value, origin, seqId, null);
}

public static ConfigSetting of(String key, Object value, ConfigOrigin origin, String configId) {
return new ConfigSetting(key, value, origin, configId);
return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, configId);
}

public static ConfigSetting of(
String key, Object value, ConfigOrigin origin, int seqId, String configId) {
return new ConfigSetting(key, value, origin, seqId, configId);
}

private ConfigSetting(String key, Object value, ConfigOrigin origin, String configId) {
private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) {
this.key = key;
this.value = CONFIG_FILTER_LIST.contains(key) ? "<hidden>" : value;
this.origin = origin;
this.seqId = seqId;
this.configId = configId;
}

Expand Down Expand Up @@ -109,12 +124,13 @@ public boolean equals(Object o) {
return key.equals(that.key)
&& Objects.equals(value, that.value)
&& origin == that.origin
&& seqId == that.seqId
&& Objects.equals(configId, that.configId);
}

@Override
public int hashCode() {
return Objects.hash(key, value, origin, configId);
return Objects.hash(key, value, origin, seqId, configId);
}

@Override
Expand All @@ -127,6 +143,8 @@ public String toString() {
+ stringValue()
+ ", origin="
+ origin
+ ", seqId="
+ seqId
+ ", configId="
+ configId
+ '}';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ static void reportConfigChange(Snapshot newSnapshot) {
update.put(TRACE_SAMPLING_RULES, newSnapshot.traceSamplingRulesJson);
maybePut(update, TRACE_SAMPLE_RATE, newSnapshot.traceSampleRate);

ConfigCollector.get().putAll(update, ConfigOrigin.REMOTE);
ConfigCollector.get().putRemoteConfigPayload(update, ConfigOrigin.REMOTE);
}

@SuppressWarnings("SameParameterValue")
Expand Down
Loading