Skip to content

Commit 69196fa

Browse files
authored
Report config_id for Hands Off Config files (#9299)
* initial introduction of config_id * fix tests, fix NPE in ConfigSetting.equals, include configId in telemetryrequestbody when nonnull * report config id for stableconfigsources in get * Fix call to outdated ConfigSetting constructor in EventSourceTest * fix config_id report * fix where null config id was getting treated as string 'null' * report config id for all get methods * remove superfluous file * simplify configId resolution logic in configProvider * report sourceValue instead of value to ConfigCollector in get method * add tests for configid and no configid
1 parent 2fa9986 commit 69196fa

File tree

9 files changed

+246
-13
lines changed

9 files changed

+246
-13
lines changed

boolean-conversion-proposal.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Proposal: Standardize Configuration Type Conversion Behavior
2+
3+
## Summary
4+
5+
We propose standardizing configuration type conversion behavior across all data types in the next major release, specifically changing how invalid boolean values are handled to match the behavior of other numeric types.
6+
7+
## Current Problem
8+
9+
### Inconsistent Behavior Between Data Types
10+
11+
Currently, our configuration system exhibits inconsistent behavior when handling invalid values:
12+
13+
**Numeric Types (Integer, Float, Double, Long):**
14+
```java
15+
// Environment: DD_SOME_INT=invalid_number
16+
int value = configProvider.getInteger("some.int", 42);
17+
// Result: 42 (default value) ✅
18+
```
19+
20+
**Boolean Type:**
21+
```java
22+
// Environment: DD_SOME_BOOL=invalid_boolean
23+
boolean value = configProvider.getBoolean("some.bool", true);
24+
// Result: false (hardcoded fallback) ❌
25+
```
26+
27+
### Why This Is Problematic
28+
29+
1. **Unexpected Behavior**: Users expect invalid values to fall back to their explicitly provided defaults
30+
2. **Silent Failures**: Invalid boolean configurations silently become `false`, making debugging difficult
31+
3. **API Inconsistency**: Different data types behave differently for the same logical error condition
32+
4. **Code Complexity**: Requires custom exception handling and special-case logic
33+
34+
## Current Implementation (Temporary Workaround)
35+
36+
To maintain backward compatibility while fixing our test suite, we implemented a temporary solution:
37+
38+
```java
39+
// ConfigConverter.java
40+
static class InvalidBooleanValueException extends IllegalArgumentException {
41+
// Custom exception for backward compatibility
42+
}
43+
44+
public static Boolean booleanValueOf(String value) {
45+
// ... validation logic ...
46+
throw new InvalidBooleanValueException("Invalid boolean value: " + value);
47+
}
48+
49+
// ConfigProvider.java
50+
catch (ConfigConverter.InvalidBooleanValueException ex) {
51+
// Special case: return false instead of default for backward compatibility
52+
return (T) Boolean.FALSE;
53+
}
54+
```
55+
56+
This approach works but adds complexity and maintains the inconsistent behavior.
57+
58+
## Proposed Solution
59+
60+
### For Next Major Release: Standardize All Type Conversions
61+
62+
1. **Remove Custom Boolean Logic**: Eliminate `InvalidBooleanValueException` and special handling
63+
2. **Consistent Exception Handling**: All invalid type conversions throw `IllegalArgumentException`
64+
3. **Consistent Fallback Behavior**: All types fall back to user-provided defaults
65+
66+
### After the Change
67+
68+
```java
69+
// All types will behave consistently:
70+
int intValue = configProvider.getInteger("key", 42); // invalid → 42
71+
boolean boolValue = configProvider.getBoolean("key", true); // invalid → true
72+
float floatValue = configProvider.getFloat("key", 3.14f); // invalid → 3.14f
73+
```
74+
75+
## Implementation Plan
76+
77+
### Phase 1: Next Major Release
78+
- Remove `InvalidBooleanValueException` class
79+
- Update `ConfigConverter.booleanValueOf()` to throw `IllegalArgumentException`
80+
- Remove special boolean handling from `ConfigProvider.get()`
81+
- Update documentation and migration guide
82+
83+
### Phase 2: Communication
84+
- Release notes highlighting the breaking change
85+
- Update configuration documentation with examples
86+
- Provide migration guidance for affected users
87+
88+
## Breaking Change Impact
89+
90+
### Who Is Affected
91+
- Users who rely on invalid boolean values defaulting to `false`
92+
- Applications that depend on the current behavior for error handling
93+
94+
### Migration Path
95+
Users can adapt by:
96+
1. **Fixing Invalid Configurations**: Update configurations to use valid boolean values
97+
2. **Adjusting Defaults**: If they want `false` as fallback, explicitly set `false` as the default
98+
3. **Adding Validation**: Implement application-level validation if needed
99+
100+
### Example Migration
101+
```java
102+
// Before (relied on invalid → false)
103+
boolean feature = configProvider.getBoolean("feature.enabled", true);
104+
105+
// After (explicit about wanting false for invalid values)
106+
boolean feature = configProvider.getBoolean("feature.enabled", false);
107+
```
108+
109+
## Benefits
110+
111+
### For Users
112+
- **Predictable Behavior**: Invalid values consistently fall back to provided defaults
113+
- **Better Debugging**: Clear error signals when configurations are invalid
114+
- **Explicit Intent**: Default values clearly express intended fallback behavior
115+
116+
### For Maintainers
117+
- **Simplified Codebase**: Remove custom exception types and special-case logic
118+
- **Consistent Testing**: All type conversions can be tested with the same patterns
119+
- **Reduced Complexity**: Fewer edge cases and branches to maintain
120+
121+
## Recommendation
122+
123+
We recommend implementing this change in the next major release (e.g., v2.0) because:
124+
125+
1. **Improves User Experience**: More predictable and debuggable configuration behavior
126+
2. **Reduces Technical Debt**: Eliminates custom workarounds and special cases
127+
3. **Aligns with Principle of Least Surprise**: Consistent behavior across all data types
128+
4. **Proper Breaking Change Window**: Major release is the appropriate time for behavior changes
129+
130+
## Questions for Discussion
131+
132+
1. Do we agree this inconsistency is worth fixing with a breaking change?
133+
2. Should we provide any additional migration tooling or validation?
134+
3. Are there other configuration behaviors we should standardize at the same time?
135+
4. What timeline works best for communicating this change to users?
136+
137+
---
138+
139+
**Next Steps**: If approved, we'll create implementation issues and begin updating documentation for the next major release cycle.

internal-api/src/main/java/datadog/trace/api/ConfigCollector.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public void put(String key, Object value, ConfigOrigin origin) {
2727
collected.put(key, setting);
2828
}
2929

30+
public void put(String key, Object value, ConfigOrigin origin, String configId) {
31+
ConfigSetting setting = ConfigSetting.of(key, value, origin, configId);
32+
collected.put(key, setting);
33+
}
34+
3035
public void putAll(Map<String, Object> keysAndValues, ConfigOrigin origin) {
3136
// attempt merge+replace to avoid collector seeing partial update
3237
Map<String, ConfigSetting> merged =

internal-api/src/main/java/datadog/trace/api/ConfigSetting.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,26 @@ public final class ConfigSetting {
1111
public final String key;
1212
public final Object value;
1313
public final ConfigOrigin origin;
14+
/** The config ID associated with this setting, or {@code null} if not applicable. */
15+
public final String configId;
1416

1517
private static final Set<String> CONFIG_FILTER_LIST =
1618
new HashSet<>(
1719
Arrays.asList("DD_API_KEY", "dd.api-key", "dd.profiling.api-key", "dd.profiling.apikey"));
1820

1921
public static ConfigSetting of(String key, Object value, ConfigOrigin origin) {
20-
return new ConfigSetting(key, value, origin);
22+
return new ConfigSetting(key, value, origin, null);
2123
}
2224

23-
private ConfigSetting(String key, Object value, ConfigOrigin origin) {
25+
public static ConfigSetting of(String key, Object value, ConfigOrigin origin, String configId) {
26+
return new ConfigSetting(key, value, origin, configId);
27+
}
28+
29+
private ConfigSetting(String key, Object value, ConfigOrigin origin, String configId) {
2430
this.key = key;
2531
this.value = CONFIG_FILTER_LIST.contains(key) ? "<hidden>" : value;
2632
this.origin = origin;
33+
this.configId = configId;
2734
}
2835

2936
public String normalizedKey() {
@@ -99,12 +106,15 @@ public boolean equals(Object o) {
99106
if (this == o) return true;
100107
if (o == null || getClass() != o.getClass()) return false;
101108
ConfigSetting that = (ConfigSetting) o;
102-
return key.equals(that.key) && Objects.equals(value, that.value) && origin == that.origin;
109+
return key.equals(that.key)
110+
&& Objects.equals(value, that.value)
111+
&& origin == that.origin
112+
&& Objects.equals(configId, that.configId);
103113
}
104114

105115
@Override
106116
public int hashCode() {
107-
return Objects.hash(key, value, origin);
117+
return Objects.hash(key, value, origin, configId);
108118
}
109119

110120
@Override
@@ -117,6 +127,8 @@ public String toString() {
117127
+ stringValue()
118128
+ ", origin="
119129
+ origin
130+
+ ", configId="
131+
+ configId
120132
+ '}';
121133
}
122134
}

internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public String getString(String key, String defaultValue, String... aliases) {
7979
String value = source.get(key, aliases);
8080
if (value != null) {
8181
if (collectConfig) {
82-
ConfigCollector.get().put(key, value, source.origin());
82+
ConfigCollector.get().put(key, value, source.origin(), getConfigIdFromSource(source));
8383
}
8484
return value;
8585
}
@@ -198,7 +198,8 @@ private <T> T get(String key, T defaultValue, Class<T> type, String... aliases)
198198
T value = ConfigConverter.valueOf(sourceValue, type);
199199
if (value != null) {
200200
if (collectConfig) {
201-
ConfigCollector.get().put(key, sourceValue, source.origin());
201+
ConfigCollector.get()
202+
.put(key, sourceValue, source.origin(), getConfigIdFromSource(source));
202203
}
203204
return value;
204205
}
@@ -257,6 +258,7 @@ public List<String> getSpacedList(String key) {
257258
public Map<String, String> getMergedMap(String key, String... aliases) {
258259
Map<String, String> merged = new HashMap<>();
259260
ConfigOrigin origin = ConfigOrigin.DEFAULT;
261+
String configId = null;
260262
// System properties take precedence over env
261263
// prior art:
262264
// https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html
@@ -265,6 +267,7 @@ public Map<String, String> getMergedMap(String key, String... aliases) {
265267
String value = sources[i].get(key, aliases);
266268
Map<String, String> parsedMap = ConfigConverter.parseMap(value, key);
267269
if (!parsedMap.isEmpty()) {
270+
configId = getConfigIdFromSource(sources[i]);
268271
if (origin != ConfigOrigin.DEFAULT) {
269272
// if we already have a non-default origin, the value is calculated from multiple sources
270273
origin = ConfigOrigin.CALCULATED;
@@ -275,14 +278,15 @@ public Map<String, String> getMergedMap(String key, String... aliases) {
275278
merged.putAll(parsedMap);
276279
}
277280
if (collectConfig) {
278-
ConfigCollector.get().put(key, merged, origin);
281+
ConfigCollector.get().put(key, merged, origin, configId);
279282
}
280283
return merged;
281284
}
282285

283286
public Map<String, String> getMergedTagsMap(String key, String... aliases) {
284287
Map<String, String> merged = new HashMap<>();
285288
ConfigOrigin origin = ConfigOrigin.DEFAULT;
289+
String configId = null;
286290
// System properties take precedence over env
287291
// prior art:
288292
// https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html
@@ -292,6 +296,7 @@ public Map<String, String> getMergedTagsMap(String key, String... aliases) {
292296
Map<String, String> parsedMap =
293297
ConfigConverter.parseTraceTagsMap(value, ':', Arrays.asList(',', ' '));
294298
if (!parsedMap.isEmpty()) {
299+
configId = getConfigIdFromSource(sources[i]);
295300
if (origin != ConfigOrigin.DEFAULT) {
296301
// if we already have a non-default origin, the value is calculated from multiple sources
297302
origin = ConfigOrigin.CALCULATED;
@@ -302,14 +307,15 @@ public Map<String, String> getMergedTagsMap(String key, String... aliases) {
302307
merged.putAll(parsedMap);
303308
}
304309
if (collectConfig) {
305-
ConfigCollector.get().put(key, merged, origin);
310+
ConfigCollector.get().put(key, merged, origin, configId);
306311
}
307312
return merged;
308313
}
309314

310315
public Map<String, String> getOrderedMap(String key) {
311316
LinkedHashMap<String, String> merged = new LinkedHashMap<>();
312317
ConfigOrigin origin = ConfigOrigin.DEFAULT;
318+
String configId = null;
313319
// System properties take precedence over env
314320
// prior art:
315321
// https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html
@@ -318,6 +324,7 @@ public Map<String, String> getOrderedMap(String key) {
318324
String value = sources[i].get(key);
319325
Map<String, String> parsedMap = ConfigConverter.parseOrderedMap(value, key);
320326
if (!parsedMap.isEmpty()) {
327+
configId = getConfigIdFromSource(sources[i]);
321328
if (origin != ConfigOrigin.DEFAULT) {
322329
// if we already have a non-default origin, the value is calculated from multiple sources
323330
origin = ConfigOrigin.CALCULATED;
@@ -328,7 +335,7 @@ public Map<String, String> getOrderedMap(String key) {
328335
merged.putAll(parsedMap);
329336
}
330337
if (collectConfig) {
331-
ConfigCollector.get().put(key, merged, origin);
338+
ConfigCollector.get().put(key, merged, origin, configId);
332339
}
333340
return merged;
334341
}
@@ -337,6 +344,7 @@ public Map<String, String> getMergedMapWithOptionalMappings(
337344
String defaultPrefix, boolean lowercaseKeys, String... keys) {
338345
Map<String, String> merged = new HashMap<>();
339346
ConfigOrigin origin = ConfigOrigin.DEFAULT;
347+
String configId = null;
340348
// System properties take precedence over env
341349
// prior art:
342350
// https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html
@@ -347,6 +355,7 @@ public Map<String, String> getMergedMapWithOptionalMappings(
347355
Map<String, String> parsedMap =
348356
ConfigConverter.parseMapWithOptionalMappings(value, key, defaultPrefix, lowercaseKeys);
349357
if (!parsedMap.isEmpty()) {
358+
configId = getConfigIdFromSource(sources[i]);
350359
if (origin != ConfigOrigin.DEFAULT) {
351360
// if we already have a non-default origin, the value is calculated from multiple
352361
// sources
@@ -358,7 +367,7 @@ public Map<String, String> getMergedMapWithOptionalMappings(
358367
merged.putAll(parsedMap);
359368
}
360369
if (collectConfig) {
361-
ConfigCollector.get().put(key, merged, origin);
370+
ConfigCollector.get().put(key, merged, origin, configId);
362371
}
363372
}
364373
return merged;
@@ -530,6 +539,13 @@ private static Properties loadConfigurationFile(ConfigProvider configProvider) {
530539
return properties;
531540
}
532541

542+
private static String getConfigIdFromSource(Source source) {
543+
if (source instanceof StableConfigSource) {
544+
return ((StableConfigSource) source).getConfigId();
545+
}
546+
return null;
547+
}
548+
533549
public abstract static class Source {
534550
public final String get(String key, String... aliases) {
535551
String value = get(key);

internal-api/src/main/java/datadog/trace/bootstrap/config/provider/stableconfig/StableConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public final class StableConfig {
1717

1818
public StableConfig(Object yaml) {
1919
Map<Object, Object> map = (Map<Object, Object>) yaml;
20-
this.configId = String.valueOf(map.get("config_id"));
20+
this.configId = map.get("config_id") == null ? null : String.valueOf(map.get("config_id"));
2121
this.apmConfigurationDefault =
2222
unmodifiableMap(
2323
(Map<String, Object>) map.getOrDefault("apm_configuration_default", emptyMap()));

internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import datadog.trace.api.config.TraceInstrumentationConfig
99
import datadog.trace.api.config.TracerConfig
1010
import datadog.trace.api.iast.telemetry.Verbosity
1111
import datadog.trace.api.naming.SpanNaming
12+
import datadog.trace.bootstrap.config.provider.ConfigProvider
1213
import datadog.trace.test.util.DDSpecification
1314
import datadog.trace.util.Strings
1415

@@ -228,4 +229,24 @@ class ConfigCollectorTest extends DDSpecification {
228229
"logs.injection.enabled" | "false"
229230
"trace.sample.rate" | "0.3"
230231
}
232+
233+
def "config id is null for non-StableConfigSource"() {
234+
setup:
235+
def key = "test.key"
236+
def value = "test-value"
237+
injectSysConfig(key, value)
238+
239+
when:
240+
// Trigger config collection by getting a value
241+
ConfigProvider.getInstance().getString(key)
242+
def settings = ConfigCollector.get().collect()
243+
244+
then:
245+
// Verify the config was collected but without a config ID
246+
def setting = settings.get(key)
247+
setting != null
248+
setting.configId == null
249+
setting.value == value
250+
setting.origin == ConfigOrigin.JVM_PROP
251+
}
231252
}

0 commit comments

Comments
 (0)