Skip to content

Commit c62688b

Browse files
committed
merge with configId changes made in master
2 parents 08c80b4 + 69196fa commit c62688b

File tree

8 files changed

+272
-25
lines changed

8 files changed

+272
-25
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ public void put(String key, Object value, ConfigOrigin origin, int seqId) {
3737
configMap.put(key, setting); // replaces any previous value for this key at origin
3838
}
3939

40+
public void put(String key, Object value, ConfigOrigin origin, String configId) {
41+
ConfigSetting setting = ConfigSetting.of(key, value, origin, configId);
42+
Map<String, ConfigSetting> configMap =
43+
collected.computeIfAbsent(origin, k -> new ConcurrentHashMap<>());
44+
configMap.put(key, setting); // replaces any previous value for this key at origin
45+
}
46+
47+
public void put(String key, Object value, ConfigOrigin origin, int seqId, String configId) {
48+
ConfigSetting setting = ConfigSetting.of(key, value, origin, seqId, configId);
49+
Map<String, ConfigSetting> configMap =
50+
collected.computeIfAbsent(origin, k -> new ConcurrentHashMap<>());
51+
configMap.put(key, setting); // replaces any previous value for this key at origin
52+
}
53+
4054
public void putAll(Map<String, Object> keysAndValues, ConfigOrigin origin) {
4155
for (Map.Entry<String, Object> entry : keysAndValues.entrySet()) {
4256
put(entry.getKey(), entry.getValue(), origin);

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,41 @@ public final class ConfigSetting {
1111
public final String key;
1212
public final Object value;
1313
public final ConfigOrigin origin;
14-
public final int seqId;
1514

15+
public final int seqId;
1616
public static final int DEFAULT_SEQ_ID = 1;
1717
private static final int ABSENT_SEQ_ID = 0;
1818

19+
/** The config ID associated with this setting, or {@code null} if not applicable. */
20+
public final String configId;
21+
1922
private static final Set<String> CONFIG_FILTER_LIST =
2023
new HashSet<>(
2124
Arrays.asList("DD_API_KEY", "dd.api-key", "dd.profiling.api-key", "dd.profiling.apikey"));
2225

2326
public static ConfigSetting of(String key, Object value, ConfigOrigin origin) {
24-
return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID);
27+
return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null);
2528
}
2629

2730
public static ConfigSetting of(String key, Object value, ConfigOrigin origin, int seqId) {
28-
return new ConfigSetting(key, value, origin, seqId);
31+
return new ConfigSetting(key, value, origin, seqId, null);
32+
}
33+
34+
public static ConfigSetting of(String key, Object value, ConfigOrigin origin, String configId) {
35+
return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, configId);
36+
}
37+
38+
public static ConfigSetting of(
39+
String key, Object value, ConfigOrigin origin, int seqId, String configId) {
40+
return new ConfigSetting(key, value, origin, seqId, configId);
2941
}
3042

31-
private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId) {
43+
private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) {
3244
this.key = key;
3345
this.value = CONFIG_FILTER_LIST.contains(key) ? "<hidden>" : value;
3446
this.origin = origin;
3547
this.seqId = seqId;
48+
this.configId = configId;
3649
}
3750

3851
public String normalizedKey() {
@@ -111,12 +124,13 @@ public boolean equals(Object o) {
111124
return key.equals(that.key)
112125
&& Objects.equals(value, that.value)
113126
&& origin == that.origin
114-
&& seqId == that.seqId;
127+
&& seqId == that.seqId
128+
&& Objects.equals(configId, that.configId);
115129
}
116130

117131
@Override
118132
public int hashCode() {
119-
return Objects.hash(key, value, origin, seqId);
133+
return Objects.hash(key, value, origin, seqId, configId);
120134
}
121135

122136
@Override
@@ -131,6 +145,8 @@ public String toString() {
131145
+ origin
132146
+ ", seqId="
133147
+ seqId
148+
+ ", configId="
149+
+ configId
134150
+ '}';
135151
}
136152
}

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

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ public String getString(String key, String defaultValue, String... aliases) {
9090
if (candidate != null) {
9191
value = candidate;
9292
if (collectConfig) {
93-
ConfigCollector.get().put(key, candidate, source.origin(), seqId);
93+
ConfigCollector.get()
94+
.put(key, candidate, source.origin(), seqId, getConfigIdFromSource(source));
9495
}
9596
}
9697
seqId++;
@@ -108,23 +109,26 @@ public String getStringNotEmpty(String key, String defaultValue, String... alias
108109
}
109110
String value = null;
110111
ConfigOrigin origin = null;
112+
String configId = null;
111113
int seqId = DEFAULT_SEQ_ID + 1;
112114
for (int i = sources.length - 1; i >= 0; i--) {
113115
ConfigProvider.Source source = sources[i];
114-
String candidate = source.get(key, aliases);
116+
String candidateValue = source.get(key, aliases);
117+
String candidateConfigId = getConfigIdFromSource(source);
115118
if (collectConfig) {
116-
ConfigCollector.get().put(key, candidate, source.origin(), seqId);
119+
ConfigCollector.get().put(key, candidateValue, source.origin(), seqId, candidateConfigId);
117120
}
118-
if (candidate != null && !candidate.trim().isEmpty()) {
119-
value = candidate;
121+
if (candidateValue != null && !candidateValue.trim().isEmpty()) {
122+
value = candidateValue;
120123
origin = source.origin();
124+
configId = candidateConfigId;
121125
}
122126
seqId++;
123127
}
124128
if (collectConfig) {
125129
// Re-report the chosen value post-trim, with the highest seqId
126130
if (value != null && origin != null) {
127-
ConfigCollector.get().put(key, value, origin, seqId + 1);
131+
ConfigCollector.get().put(key, value, origin, seqId + 1, configId);
128132
}
129133
}
130134
return value != null ? value : defaultValue;
@@ -142,16 +146,16 @@ public String getStringExcludingSource(
142146
int seqId = DEFAULT_SEQ_ID + 1;
143147
for (int i = sources.length - 1; i >= 0; i--) {
144148
ConfigProvider.Source source = sources[i];
145-
// Do we still want to report telemetry in this case?
149+
String candidate = source.get(key, aliases);
150+
if (collectConfig) {
151+
ConfigCollector.get()
152+
.put(key, candidate, source.origin(), seqId, getConfigIdFromSource(source));
153+
}
146154
if (excludedSource.isAssignableFrom(source.getClass())) {
147155
continue;
148156
}
149-
String candidate = source.get(key, aliases);
150157
if (candidate != null) {
151158
value = candidate;
152-
if (collectConfig) {
153-
ConfigCollector.get().put(key, candidate, source.origin(), seqId);
154-
}
155159
}
156160
seqId++;
157161
}
@@ -222,10 +226,12 @@ private <T> T get(String key, T defaultValue, Class<T> type, String... aliases)
222226
T value = null;
223227
ConfigOrigin origin = null;
224228
int seqId = DEFAULT_SEQ_ID + 1;
229+
String configId = null;
225230
for (int i = sources.length - 1; i >= 0; i--) {
226231
String sourceValue = sources[i].get(key, aliases);
232+
configId = getConfigIdFromSource(sources[i]);
227233
if (sourceValue != null && collectConfig) {
228-
ConfigCollector.get().put(key, sourceValue, sources[i].origin(), seqId);
234+
ConfigCollector.get().put(key, sourceValue, sources[i].origin(), seqId, configId);
229235
}
230236
try {
231237
T candidate = ConfigConverter.valueOf(sourceValue, type);
@@ -249,7 +255,7 @@ private <T> T get(String key, T defaultValue, Class<T> type, String... aliases)
249255
if (collectConfig) {
250256
// Re-report the chosen value and origin to ensure its seqId is higher than any error configs
251257
if (value != null && origin != null) {
252-
ConfigCollector.get().put(key, value, origin, seqId + 1);
258+
ConfigCollector.get().put(key, value, origin, seqId + 1, configId);
253259
}
254260
}
255261
return value != null ? value : defaultValue;
@@ -305,7 +311,8 @@ public Map<String, String> getMergedMap(String key, String... aliases) {
305311
if (!parsedMap.isEmpty()) {
306312
if (collectConfig) {
307313
seqId++;
308-
ConfigCollector.get().put(key, parsedMap, sources[i].origin(), seqId);
314+
ConfigCollector.get()
315+
.put(key, parsedMap, sources[i].origin(), seqId, getConfigIdFromSource(sources[i]));
309316
}
310317
if (origin != ConfigOrigin.DEFAULT) {
311318
// if we already have a non-default origin, the value is calculated from multiple sources
@@ -340,7 +347,8 @@ public Map<String, String> getMergedTagsMap(String key, String... aliases) {
340347
if (!parsedMap.isEmpty()) {
341348
if (collectConfig) {
342349
seqId++;
343-
ConfigCollector.get().put(key, parsedMap, sources[i].origin(), seqId);
350+
ConfigCollector.get()
351+
.put(key, parsedMap, sources[i].origin(), seqId, getConfigIdFromSource(sources[i]));
344352
}
345353
if (origin != ConfigOrigin.DEFAULT) {
346354
// if we already have a non-default origin, the value is calculated from multiple sources
@@ -375,7 +383,8 @@ public Map<String, String> getOrderedMap(String key) {
375383
if (!parsedMap.isEmpty()) {
376384
if (collectConfig) {
377385
seqId++;
378-
ConfigCollector.get().put(key, parsedMap, sources[i].origin(), seqId);
386+
ConfigCollector.get()
387+
.put(key, parsedMap, sources[i].origin(), seqId, getConfigIdFromSource(sources[i]));
379388
}
380389
if (origin != ConfigOrigin.DEFAULT) {
381390
// if we already have a non-default origin, the value is calculated from multiple sources
@@ -412,7 +421,8 @@ public Map<String, String> getMergedMapWithOptionalMappings(
412421
if (!parsedMap.isEmpty()) {
413422
if (collectConfig) {
414423
seqId++;
415-
ConfigCollector.get().put(key, parsedMap, sources[i].origin(), seqId);
424+
ConfigCollector.get()
425+
.put(key, parsedMap, sources[i].origin(), seqId, getConfigIdFromSource(sources[i]));
416426
}
417427
if (origin != ConfigOrigin.DEFAULT) {
418428
// if we already have a non-default origin, the value is calculated from multiple
@@ -603,6 +613,13 @@ private static Properties loadConfigurationFile(ConfigProvider configProvider) {
603613
return properties;
604614
}
605615

616+
private static String getConfigIdFromSource(Source source) {
617+
if (source instanceof StableConfigSource) {
618+
return ((StableConfigSource) source).getConfigId();
619+
}
620+
return null;
621+
}
622+
606623
public abstract static class Source {
607624
public final String get(String key, String... aliases) {
608625
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()));

0 commit comments

Comments
 (0)