Skip to content

Stable Config improvements #9259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
370e894
Refactor matchOperator to use method reference comparator for efficiency
mtoffl01 Jul 28, 2025
df4da2f
Implement better exception handling and debug logging for StableConfi…
mtoffl01 Jul 28, 2025
7049d36
Add a debug log about no rules getting applied
mtoffl01 Jul 28, 2025
e730cbe
Improve test file logic and cleanup in StableConfigSourceTest class
mtoffl01 Jul 29, 2025
5b639aa
Github suggestions: Use static factory methods as constructors for R…
mtoffl01 Jul 31, 2025
85d11cf
Improve exception message: include value of object that caused the fa…
mtoffl01 Aug 1, 2025
6ab4b6c
Add more test cases to StableConfigSourceTest to improve codecov
mtoffl01 Aug 1, 2025
007f58a
Add test coverage for StableConfigMappingException
mtoffl01 Aug 1, 2025
ac69f2d
Merge branch 'master' into mtoff/scfg-improvements
mtoffl01 Aug 4, 2025
473c59b
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Aug 8, 2025
feb1ba0
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Aug 8, 2025
f93546a
Use safeToString when printing stableconfig map
mtoffl01 Aug 8, 2025
780aee3
rearrange logic in selectors creation for better readability
mtoffl01 Aug 8, 2025
ac5e05a
delete superfluous else
mtoffl01 Aug 8, 2025
bcebbcb
Use constant to represent max length in safeToString
mtoffl01 Aug 8, 2025
0baccfd
use throwStableConfigMappingException helper function
mtoffl01 Aug 11, 2025
957d9c9
refactor exception catching in StableConfigSource
mtoffl01 Aug 11, 2025
b9870bd
restore semicolon in calls to throwStableConfigMappingException
mtoffl01 Aug 11, 2025
5019584
Use static import for throwStableConfigMappingException
mtoffl01 Aug 11, 2025
e8d6e8f
test other kinds of exceptions in StableConfigSource
mtoffl01 Aug 11, 2025
1fefb17
remove duplicate cfg assignments
mtoffl01 Aug 11, 2025
058cbda
fix missing operator test case
mtoffl01 Aug 12, 2025
5460c8a
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Aug 12, 2025
37da711
Improve readability of StableConfigMappingException and its function …
mtoffl01 Aug 12, 2025
5dac7f8
modify safeToString logic to print first 50 and last 50 chars
mtoffl01 Aug 12, 2025
5104ac5
Merge branch 'mtoff/scfg-improvements' of github.com:DataDog/dd-trace…
mtoffl01 Aug 12, 2025
13bef8d
remove extra placeholder in string message when printing exception
mtoffl01 Aug 12, 2025
2706bca
Update internal-api/src/main/java/datadog/trace/bootstrap/config/prov…
mtoffl01 Aug 12, 2025
4dde216
refactor Rules.from to perform type assertions before heavyweight lis…
mtoffl01 Aug 14, 2025
bfd5d7b
Merge branch 'mtoff/scfg-improvements' of github.com:DataDog/dd-trace…
mtoffl01 Aug 14, 2025
16d5f36
run spotless
mtoffl01 Aug 15, 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 @@ -16,6 +16,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -75,6 +76,7 @@ public static StableConfigSource.StableConfig parse(String filePath) throws IOEx
return new StableConfigSource.StableConfig(configId, mergedConfigMap);
}
}
log.debug("No matching rule found in stable configuration file {}", filePath);
}
// If configs were found in apm_configuration_default, use them
if (!configMap.isEmpty()) {
Expand All @@ -85,7 +87,6 @@ public static StableConfigSource.StableConfig parse(String filePath) throws IOEx
if (configId != null) {
return new StableConfigSource.StableConfig(configId, Collections.emptyMap());
}

} catch (IOException e) {
log.debug("Failed to read the stable configuration file: {}", filePath, e);
}
Expand Down Expand Up @@ -117,22 +118,32 @@ private static boolean matchOperator(String value, String operator, List<String>
return false;
}
value = value.toLowerCase(Locale.ROOT);

Predicate<String> comparator;
switch (operator) {
case "equals":
comparator = value::equals;
break;
case "starts_with":
comparator = value::startsWith;
break;
case "ends_with":
comparator = value::endsWith;
break;
case "contains":
comparator = value::contains;
break;
default:
return false;
}

for (String match : matches) {
if (match == null) {
continue;
}
match = match.toLowerCase(Locale.ROOT);
switch (operator) {
case "equals":
return value.equals(match);
case "starts_with":
return value.startsWith(match);
case "ends_with":
return value.endsWith(match);
case "contains":
return value.contains(match);
default:
return false;
if (comparator.test(match)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

return true;
}
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static datadog.trace.util.Strings.propertyNameToEnvironmentVariableName;

import datadog.trace.api.ConfigOrigin;
import datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException;
import java.io.File;
import java.util.Collections;
import java.util.Map;
Expand Down Expand Up @@ -38,10 +39,23 @@ public final class StableConfigSource extends ConfigProvider.Source {
log.debug("Stable configuration file found at path: {}", file);
cfg = StableConfigParser.parse(filePath);
} catch (Throwable e) {
log.warn(
"Encountered the following exception when attempting to read stable configuration file at path: {}, dropping configs.",
file,
e);
if (e instanceof StableConfigMappingException
|| e instanceof IllegalArgumentException
|| e instanceof ClassCastException
|| e instanceof NullPointerException) {
log.warn(
"YAML mapping error in stable configuration file: {}, error: {}",
filePath,
e.getMessage());
} else if (log.isDebugEnabled()) {
log.error("Unexpected error while reading stable configuration file: {}", filePath, e);
} else {
log.error(
"Unexpected error while reading stable configuration file: {}, error: {}",
filePath,
e.getMessage());
}

cfg = StableConfig.EMPTY;
}
this.config = cfg;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.trace.bootstrap.config.provider.stableconfig;

import static datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException.throwStableConfigMappingException;
import static java.util.Collections.*;
import static java.util.stream.Collectors.toList;

Expand All @@ -25,13 +26,47 @@ public Rule(List<Selector> selectors, Map<String, Object> configuration) {
this.configuration = configuration;
}

public Rule(Object yaml) {
Map map = (Map) yaml;
selectors =
unmodifiableList(
((List<Object>) map.get("selectors"))
.stream().filter(Objects::nonNull).map(Selector::new).collect(toList()));
configuration = unmodifiableMap((Map<String, Object>) map.get("configuration"));
public static Rule from(Map<?, ?> map) {
Object selectorsObj = map.get("selectors");
if (selectorsObj == null) {
throwStableConfigMappingException("Missing 'selectors' in rule:", map);
}

if (!(selectorsObj instanceof List)) {
throwStableConfigMappingException(
"'selectors' must be a list, but got: " + selectorsObj.getClass().getSimpleName() + ": ",
selectorsObj);
}

Object configObj = map.get("configuration");
if (configObj == null) {
throwStableConfigMappingException("Missing 'configuration' in rule:", map);
}
if (!(configObj instanceof Map)) {
throwStableConfigMappingException(
"'configuration' must be a map, but got: " + configObj.getClass().getSimpleName() + ": ",
configObj);
}

List<Selector> selectors =
((List<?>) selectorsObj)
.stream()
.filter(Objects::nonNull)
.map(
s -> {
if (!(s instanceof Map)) {
throwStableConfigMappingException(
"Each selector must be a map, but got: "
+ s.getClass().getSimpleName()
+ ": ",
s);
}

return Selector.from((Map<?, ?>) s);
})
.collect(toList());

return new Rule(unmodifiableList(selectors), (Map<String, Object>) configObj);
}

public List<Selector> getSelectors() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package datadog.trace.bootstrap.config.provider.stableconfig;

import static datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException.throwStableConfigMappingException;

import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -17,14 +19,43 @@ public Selector(String origin, String key, List<String> matches, String operator
this.operator = operator;
}

public Selector(Object yaml) {
Map map = (Map) yaml;
origin = (String) map.get("origin");
key = (String) map.get("key");
List<String> rawMatches = (List<String>) map.get("matches");
matches =
public static Selector from(Map<?, ?> map) {
Object originObj = map.get("origin");
if (originObj == null) {
throwStableConfigMappingException("Missing 'origin' in selector:", map);
}
if (!(originObj instanceof String)) {
throwStableConfigMappingException(
"'origin' must be a string, but got: " + originObj.getClass().getSimpleName() + ": ",
originObj);
}
String origin = (String) originObj;

Object keyObj = map.get("key");
String key = (keyObj instanceof String) ? (String) keyObj : null;

Object matchesObj = map.get("matches");
if (matchesObj != null && !(matchesObj instanceof List)) {
throwStableConfigMappingException(
"'matches' must be a list, but got: " + matchesObj.getClass().getSimpleName() + ": ",
matchesObj);
}
List<String> rawMatches = (List<String>) matchesObj;
List<String> matches =
rawMatches != null ? Collections.unmodifiableList(rawMatches) : Collections.emptyList();
operator = (String) map.get("operator");

Object operatorObj = map.get("operator");
if (operatorObj == null) {
throwStableConfigMappingException("Missing 'operator' in selector:", map);
}
if (!(operatorObj instanceof String)) {
throwStableConfigMappingException(
"'operator' must be a string, but got: " + operatorObj.getClass().getSimpleName() + ": ",
operatorObj);
}
String operator = (String) operatorObj;

return new Selector(origin, key, matches, operator);
}

public String getOrigin() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package datadog.trace.bootstrap.config.provider.stableconfig;

import static datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException.throwStableConfigMappingException;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -21,10 +21,7 @@ public StableConfig(Object yaml) {
this.apmConfigurationDefault =
unmodifiableMap(
(Map<String, Object>) map.getOrDefault("apm_configuration_default", emptyMap()));
this.apmConfigurationRules =
unmodifiableList(
((List<Object>) map.getOrDefault("apm_configuration_rules", emptyList()))
.stream().map(Rule::new).collect(toList()));
this.apmConfigurationRules = parseRules(map);
}

// test only
Expand All @@ -45,4 +42,23 @@ public Map<String, Object> getApmConfigurationDefault() {
public List<Rule> getApmConfigurationRules() {
return apmConfigurationRules;
}

private List<Rule> parseRules(Map<?, ?> map) {
Object rulesObj = map.get("apm_configuration_rules");
if (rulesObj instanceof List) {
List<?> rulesList = (List<?>) rulesObj;
List<Rule> rules = new ArrayList<>();
for (Object ruleObj : rulesList) {
if (ruleObj instanceof Map) {
rules.add(Rule.from((Map<?, ?>) ruleObj));
} else {
throwStableConfigMappingException(
"Rule must be a map, but got: " + ruleObj.getClass().getSimpleName() + ": ", ruleObj);
return emptyList();
}
}
return unmodifiableList(rules);
}
return emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package datadog.trace.bootstrap.config.provider.stableconfig;

public class StableConfigMappingException extends RuntimeException {
private static final int MAX_LEN = 100;

public StableConfigMappingException(String message) {
super(message);
}

/**
* Safely converts an object to a string for error reporting, truncating the result if it exceeds
* a maximum length.
*
* @param value the object to convert to a string
* @return a string representation of the object, truncated if necessary
*/
static String safeToString(Object value) {
if (value == null) return "null";
String str = value.toString();
if (str.length() > MAX_LEN) {
int partLen = MAX_LEN / 2;
return str.substring(0, partLen)
+ "...(truncated)..."
+ str.substring(str.length() - partLen);
}
return str;
}

/**
* Throws a StableConfigMappingException with a message that includes a safe string representation
* of the provided value.
*
* @param message the error message to include
* @param value the value to include in the error message, safely stringified
* @throws StableConfigMappingException always thrown by this method
*/
public static void throwStableConfigMappingException(String message, Object value) {
throw new StableConfigMappingException(message + " " + safeToString(value));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package datadog.trace.bootstrap.config.provider

import datadog.trace.bootstrap.config.provider.stableconfig.StableConfigMappingException
import spock.lang.Specification

class StableConfigMappingExceptionTest extends Specification {

def "constructors work as expected"() {
when:
def ex1 = new StableConfigMappingException("msg")
def ex2 = new StableConfigMappingException("msg2")

then:
ex1.message == "msg"
ex1.cause == null
ex2.message == "msg2"
}

def "safeToString handles null"() {
expect:
StableConfigMappingException.safeToString(null) == "null"
}

def "safeToString handles short string"() {
expect:
StableConfigMappingException.safeToString("short string") == "short string"
}

def "safeToString handles long string"() {
given:
def longStr = "a" * 101
expect:
StableConfigMappingException.safeToString(longStr) == ("a" * 50) + "...(truncated)..." + ("a" * 51).substring(1)
}
}
Loading