Skip to content

Commit df4da2f

Browse files
committed
Implement better exception handling and debug logging for StableConfig parsing errors
1 parent 370e894 commit df4da2f

File tree

6 files changed

+161
-12
lines changed

6 files changed

+161
-12
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ public static StableConfigSource.StableConfig parse(String filePath) throws IOEx
8484
if (configId != null) {
8585
return new StableConfigSource.StableConfig(configId, Collections.emptyMap());
8686
}
87-
8887
} catch (IOException e) {
8988
log.debug("Failed to read the stable configuration file: {}", filePath, e);
9089
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static datadog.trace.util.Strings.propertyNameToEnvironmentVariableName;
44

55
import datadog.trace.api.ConfigOrigin;
6+
import datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException;
67
import java.io.File;
78
import java.util.Collections;
89
import java.util.Map;
@@ -37,11 +38,21 @@ public final class StableConfigSource extends ConfigProvider.Source {
3738
try {
3839
log.debug("Stable configuration file found at path: {}", file);
3940
cfg = StableConfigParser.parse(filePath);
40-
} catch (Throwable e) {
41-
log.warn(
42-
"Encountered the following exception when attempting to read stable configuration file at path: {}, dropping configs.",
43-
file,
44-
e);
41+
} catch (StableConfigMappingException
42+
| IllegalArgumentException
43+
| ClassCastException
44+
| NullPointerException e) {
45+
log.warn("YAML mapping error in stable configuration file {}: {}", filePath, e.getMessage());
46+
cfg = StableConfig.EMPTY;
47+
} catch (Exception e) {
48+
if (log.isDebugEnabled()) {
49+
log.error("Unexpected error while reading stable configuration file {}: {}", filePath, e);
50+
} else {
51+
log.error(
52+
"Unexpected error while reading stable configuration file {}: {}",
53+
filePath,
54+
e.getMessage());
55+
}
4556
cfg = StableConfig.EMPTY;
4657
}
4758
this.config = cfg;

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,34 @@ public Rule(List<Selector> selectors, Map<String, Object> configuration) {
2626
}
2727

2828
public Rule(Object yaml) {
29+
if (!(yaml instanceof Map)) {
30+
throw new StableConfigMappingException(
31+
"Rule must be a map, but got: " + yaml.getClass().getSimpleName());
32+
}
2933
Map map = (Map) yaml;
34+
35+
Object selectorsObj = map.get("selectors");
36+
if (selectorsObj == null) {
37+
throw new StableConfigMappingException("Missing 'selectors' in rule: " + map);
38+
}
39+
if (!(selectorsObj instanceof List)) {
40+
throw new StableConfigMappingException(
41+
"'selectors' must be a list, but got: " + selectorsObj.getClass().getSimpleName());
42+
}
3043
selectors =
3144
unmodifiableList(
32-
((List<Object>) map.get("selectors"))
45+
((List<Object>) selectorsObj)
3346
.stream().filter(Objects::nonNull).map(Selector::new).collect(toList()));
34-
configuration = unmodifiableMap((Map<String, Object>) map.get("configuration"));
47+
48+
Object configObj = map.get("configuration");
49+
if (configObj == null) {
50+
throw new StableConfigMappingException("Missing 'configuration' in rule: " + map);
51+
}
52+
if (!(configObj instanceof Map)) {
53+
throw new StableConfigMappingException(
54+
"'configuration' must be a map, but got: " + configObj.getClass().getSimpleName());
55+
}
56+
configuration = unmodifiableMap((Map<String, Object>) configObj);
3557
}
3658

3759
public List<Selector> getSelectors() {

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,43 @@ public Selector(String origin, String key, List<String> matches, String operator
1818
}
1919

2020
public Selector(Object yaml) {
21+
if (!(yaml instanceof Map)) {
22+
throw new StableConfigMappingException(
23+
"Selector must be a map, but got: " + yaml.getClass().getSimpleName());
24+
}
2125
Map map = (Map) yaml;
22-
origin = (String) map.get("origin");
23-
key = (String) map.get("key");
24-
List<String> rawMatches = (List<String>) map.get("matches");
26+
27+
Object originObj = map.get("origin");
28+
if (originObj == null) {
29+
throw new StableConfigMappingException("Missing 'origin' in selector: " + map);
30+
}
31+
if (!(originObj instanceof String)) {
32+
throw new StableConfigMappingException(
33+
"'origin' must be a string, but got: " + originObj.getClass().getSimpleName());
34+
}
35+
origin = (String) originObj;
36+
37+
Object keyObj = map.get("key");
38+
key = (keyObj instanceof String) ? (String) keyObj : null;
39+
40+
Object matchesObj = map.get("matches");
41+
if (matchesObj != null && !(matchesObj instanceof List)) {
42+
throw new StableConfigMappingException(
43+
"'matches' must be a list, but got: " + matchesObj.getClass().getSimpleName());
44+
}
45+
List<String> rawMatches = (List<String>) matchesObj;
2546
matches =
2647
rawMatches != null ? Collections.unmodifiableList(rawMatches) : Collections.emptyList();
27-
operator = (String) map.get("operator");
48+
49+
Object operatorObj = map.get("operator");
50+
if (operatorObj == null) {
51+
throw new StableConfigMappingException("Missing 'operator' in selector: " + map);
52+
}
53+
if (!(operatorObj instanceof String)) {
54+
throw new StableConfigMappingException(
55+
"'operator' must be a string, but got: " + operatorObj.getClass().getSimpleName());
56+
}
57+
operator = (String) operatorObj;
2858
}
2959

3060
public String getOrigin() {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package datadog.trace.bootstrap.config.provider.stableconfig;
2+
3+
public class StableConfigMappingException extends RuntimeException {
4+
public StableConfigMappingException(String message) {
5+
super(message);
6+
}
7+
8+
public StableConfigMappingException(String message, Throwable cause) {
9+
super(message, cause);
10+
}
11+
}

internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/StableConfigSourceTest.groovy

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import datadog.trace.bootstrap.config.provider.stableconfig.StableConfig
99
import datadog.trace.test.util.DDSpecification
1010
import org.snakeyaml.engine.v2.api.Dump
1111
import org.snakeyaml.engine.v2.api.DumpSettings
12+
import ch.qos.logback.classic.Logger
13+
import ch.qos.logback.classic.spi.ILoggingEvent
14+
import ch.qos.logback.core.read.ListAppender
15+
import org.slf4j.LoggerFactory
1216

1317
import java.nio.file.Files
1418
import java.nio.file.Path
@@ -108,6 +112,78 @@ class StableConfigSourceTest extends DDSpecification {
108112
"12345" | ["DD_KEY_ONE": "one", "DD_KEY_TWO": "two"] | Arrays.asList(sampleMatchingRule, sampleNonMatchingRule)
109113
}
110114

115+
def "test parse invalid logs mapping errors"() {
116+
given:
117+
Logger logbackLogger = (Logger) LoggerFactory.getLogger(datadog.trace.bootstrap.config.provider.StableConfigSource)
118+
def listAppender = new ListAppender<ILoggingEvent>()
119+
listAppender.start()
120+
logbackLogger.addAppender(listAppender)
121+
122+
def tempFile = File.createTempFile("testFile_", ".yaml")
123+
tempFile.text = yaml
124+
125+
when:
126+
def stableCfg = new StableConfigSource(tempFile.absolutePath, ConfigOrigin.LOCAL_STABLE_CONFIG)
127+
128+
then:
129+
stableCfg.config == StableConfigSource.StableConfig.EMPTY
130+
def warnLogs = listAppender.list.findAll { it.level.toString() == 'WARN' }
131+
warnLogs.any { it.formattedMessage.contains(expectedLogSubstring) }
132+
133+
cleanup:
134+
tempFile.delete()
135+
logbackLogger.detachAppender(listAppender)
136+
137+
where:
138+
yaml | expectedLogSubstring
139+
// Missing 'origin' in selector
140+
'''apm_configuration_rules:
141+
- selectors:
142+
- key: "someKey"
143+
matches: ["someValue"]
144+
operator: equals
145+
configuration:
146+
DD_SERVICE: "test"
147+
''' | "Missing 'origin' in selector"
148+
// Missing 'configuration' in rule
149+
'''apm_configuration_rules:
150+
- selectors:
151+
- origin: process_arguments
152+
key: "-Dfoo"
153+
matches: ["bar"]
154+
operator: equals
155+
''' | "Missing 'configuration' in rule"
156+
// Missing 'selectors' in rule
157+
'''apm_configuration_rules:
158+
- configuration:
159+
DD_SERVICE: "test"
160+
''' | "Missing 'selectors' in rule"
161+
// Triggers ClassCastException (selectors should be a list, not a string)
162+
'''apm_configuration_rules:
163+
- selectors: "not-a-list"
164+
configuration:
165+
DD_SERVICE: "test"
166+
''' | "'selectors' must be a list, but got: String"
167+
// configuration present but not a map
168+
'''apm_configuration_rules:
169+
- selectors:
170+
- origin: process_arguments
171+
key: "-Dfoo"
172+
matches: ["bar"]
173+
operator: equals
174+
configuration: "not-a-map"
175+
''' | "'configuration' must be a map, but got: String"
176+
// configuration present but not a map (integer)
177+
'''apm_configuration_rules:
178+
- selectors:
179+
- origin: process_arguments
180+
key: "-Dfoo"
181+
matches: ["bar"]
182+
operator: equals
183+
configuration: 12345
184+
''' | "'configuration' must be a map, but got: Integer"
185+
}
186+
111187
// Corrupt YAML string variable used for testing, defined outside the 'where' block for readability
112188
def static corruptYaml = '''
113189
abc: 123

0 commit comments

Comments
 (0)