Skip to content

Commit 35f1823

Browse files
committed
Add Remote Config based enablement for Symbol DB
If SymbolDB is enabled we subscribe to Remote Config new product LIVE_DEBUGGING_SYMBOL_DB to receive the RC record indicating if we extract and upload symbol for the current instance. As most classes mat have been loaded by the time we receive the upload symbol flag we need to scan all loaded classes that matches our criteria for symbol extraction and scan the actual jar for all those allowed classes and parse it and extract symbols
1 parent d3b600a commit 35f1823

14 files changed

Lines changed: 588 additions & 154 deletions

File tree

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import com.datadog.debugger.sink.DebuggerSink;
66
import com.datadog.debugger.sink.Sink;
7-
import com.datadog.debugger.symbol.SymbolExtractionTransformer;
7+
import com.datadog.debugger.symbol.SymDBEnablement;
8+
import com.datadog.debugger.symbol.SymbolAggregator;
89
import com.datadog.debugger.uploader.BatchUploader;
910
import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
1011
import datadog.communication.ddagent.SharedCommunicationObjects;
@@ -33,6 +34,7 @@ public class DebuggerAgent {
3334
private static Sink sink;
3435
private static String agentVersion;
3536
private static JsonSnapshotSerializer snapshotSerializer;
37+
private static SymDBEnablement symDBEnablement;
3638

3739
public static synchronized void run(
3840
Instrumentation instrumentation, SharedCommunicationObjects sco) {
@@ -77,7 +79,18 @@ public static synchronized void run(
7779
}
7880
configurationPoller = sco.configurationPoller(config);
7981
if (configurationPoller != null) {
80-
subscribeConfigurationPoller(config, configurationUpdater);
82+
if (config.isDebuggerSymbolEnabled()) {
83+
symDBEnablement =
84+
new SymDBEnablement(
85+
instrumentation,
86+
config,
87+
new SymbolAggregator(
88+
debuggerSink.getSymbolSink(), config.getDebuggerSymbolFlushThreshold()));
89+
if (config.isDebuggerSymbolForceUpload()) {
90+
symDBEnablement.startSymbolExtraction();
91+
}
92+
}
93+
subscribeConfigurationPoller(config, configurationUpdater, symDBEnablement);
8194
try {
8295
/*
8396
Note: shutdown hooks are tricky because JVM holds reference for them forever preventing
@@ -92,10 +105,6 @@ public static synchronized void run(
92105
} else {
93106
log.debug("No configuration poller available from SharedCommunicationObjects");
94107
}
95-
if (config.isDebuggerSymbolEnabled() && config.isDebuggerSymbolForceUpload()) {
96-
instrumentation.addTransformer(
97-
new SymbolExtractionTransformer(debuggerSink.getSymbolSink(), config));
98-
}
99108
}
100109

101110
private static void setupSourceFileTracking(
@@ -128,9 +137,13 @@ private static void loadFromFile(
128137
}
129138

130139
private static void subscribeConfigurationPoller(
131-
Config config, ConfigurationUpdater configurationUpdater) {
140+
Config config, ConfigurationUpdater configurationUpdater, SymDBEnablement symDBEnablement) {
132141
configurationPoller.addListener(
133142
Product.LIVE_DEBUGGING, new DebuggerProductChangesListener(config, configurationUpdater));
143+
if (symDBEnablement != null && !config.isDebuggerSymbolForceUpload()) {
144+
log.debug("Subscribing to Symbol DB...");
145+
configurationPoller.addListener(Product.LIVE_DEBUGGING_SYMBOL_DB, symDBEnablement);
146+
}
134147
}
135148

136149
static ClassFileTransformer setupInstrumentTheWorldTransformer(

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerProductChangesListener.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,27 @@ static class Adapter {
4444
MoshiHelper.createMoshiConfig().adapter(SpanDecorationProbe.class);
4545

4646
static Configuration deserializeConfiguration(byte[] content) throws IOException {
47-
return CONFIGURATION_JSON_ADAPTER.fromJson(
48-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
47+
return deserialize(CONFIGURATION_JSON_ADAPTER, content);
4948
}
5049

5150
static MetricProbe deserializeMetricProbe(byte[] content) throws IOException {
52-
return METRIC_PROBE_JSON_ADAPTER.fromJson(
53-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
51+
return deserialize(METRIC_PROBE_JSON_ADAPTER, content);
5452
}
5553

5654
static LogProbe deserializeLogProbe(byte[] content) throws IOException {
57-
return LOG_PROBE_JSON_ADAPTER.fromJson(
58-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
55+
return deserialize(LOG_PROBE_JSON_ADAPTER, content);
5956
}
6057

6158
static SpanProbe deserializeSpanProbe(byte[] content) throws IOException {
62-
return SPAN_PROBE_JSON_ADAPTER.fromJson(
63-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
59+
return deserialize(SPAN_PROBE_JSON_ADAPTER, content);
6460
}
6561

6662
static SpanDecorationProbe deserializeSpanDecorationProbe(byte[] content) throws IOException {
67-
return SPAN_DECORATION_PROBE_JSON_ADAPTER.fromJson(
68-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
63+
return deserialize(SPAN_DECORATION_PROBE_JSON_ADAPTER, content);
64+
}
65+
66+
private static <T> T deserialize(JsonAdapter<T> adapter, byte[] content) throws IOException {
67+
return adapter.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
6968
}
7069
}
7170

@@ -89,9 +88,7 @@ public void accept(
8988
byte[] content,
9089
datadog.remoteconfig.ConfigurationChangesListener.PollingRateHinter pollingRateHinter)
9190
throws IOException {
92-
9391
String configId = configKey.getConfigId();
94-
9592
if (configId.startsWith("metricProbe_")) {
9693
MetricProbe metricProbe = Adapter.deserializeMetricProbe(content);
9794
configChunks.put(configId, (builder) -> builder.add(metricProbe));
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.datadog.debugger.symbol;
2+
3+
import java.net.URISyntaxException;
4+
import java.net.URL;
5+
import java.nio.file.Path;
6+
import java.nio.file.Paths;
7+
import java.security.CodeSource;
8+
import java.security.ProtectionDomain;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
public class JarScanner {
13+
private static final Logger LOGGER = LoggerFactory.getLogger(JarScanner.class);
14+
private static final String JAR_FILE_PREFIX = "jar:file:";
15+
private static final String FILE_PREFIX = "file:";
16+
// Spring prefixes:
17+
// https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html
18+
private static final String SPRING_CLASSES_PREFIX = "BOOT-INF/classes/";
19+
private static final String SPRING_DEPS_PREFIX = "BOOT-INF/lib/";
20+
21+
public static Path extractJarPath(Class<?> clazz) throws URISyntaxException {
22+
return extractJarPath(clazz.getProtectionDomain());
23+
}
24+
25+
public static Path extractJarPath(ProtectionDomain protectionDomain) throws URISyntaxException {
26+
if (protectionDomain == null) {
27+
return null;
28+
}
29+
CodeSource codeSource = protectionDomain.getCodeSource();
30+
if (codeSource == null) {
31+
return null;
32+
}
33+
URL location = codeSource.getLocation();
34+
if (location == null) {
35+
return null;
36+
}
37+
String locationStr = location.toString();
38+
LOGGER.debug("CodeSource Location={}", locationStr);
39+
if (locationStr.startsWith(JAR_FILE_PREFIX)) {
40+
int idx = locationStr.indexOf("!/");
41+
if (idx != -1) {
42+
return getPathFromPrefixedFileName(locationStr, JAR_FILE_PREFIX, idx);
43+
}
44+
} else if (locationStr.startsWith(FILE_PREFIX)) {
45+
return getPathFromPrefixedFileName(locationStr, FILE_PREFIX, locationStr.length());
46+
}
47+
return null;
48+
}
49+
50+
public static String trimPrefixes(String classFilePath) {
51+
if (classFilePath.startsWith(SPRING_CLASSES_PREFIX)) {
52+
return classFilePath.substring(SPRING_CLASSES_PREFIX.length());
53+
}
54+
return classFilePath;
55+
}
56+
57+
private static Path getPathFromPrefixedFileName(String locationStr, String prefix, int endIdx) {
58+
String fileName = locationStr.substring(prefix.length(), endIdx);
59+
LOGGER.debug("jar filename={}", fileName);
60+
return Paths.get(fileName);
61+
}
62+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.datadog.debugger.symbol;
2+
3+
import static com.datadog.debugger.symbol.JarScanner.trimPrefixes;
4+
5+
import com.datadog.debugger.agent.AllowListHelper;
6+
import com.datadog.debugger.agent.Configuration;
7+
import com.datadog.debugger.util.MoshiHelper;
8+
import com.squareup.moshi.JsonAdapter;
9+
import datadog.remoteconfig.ConfigurationChangesListener;
10+
import datadog.remoteconfig.state.ParsedConfigKey;
11+
import datadog.remoteconfig.state.ProductListener;
12+
import datadog.trace.api.Config;
13+
import datadog.trace.util.Strings;
14+
import java.io.ByteArrayInputStream;
15+
import java.io.ByteArrayOutputStream;
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.lang.instrument.Instrumentation;
19+
import java.net.URISyntaxException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.time.Instant;
23+
import java.time.LocalDateTime;
24+
import java.time.ZoneId;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.HashSet;
28+
import java.util.Set;
29+
import java.util.jar.JarEntry;
30+
import java.util.jar.JarFile;
31+
import java.util.regex.Pattern;
32+
import okio.Okio;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
36+
public class SymDBEnablement implements ProductListener {
37+
private static final Logger LOGGER = LoggerFactory.getLogger(SymDBEnablement.class);
38+
private static final Pattern COMMA_PATTERN = Pattern.compile(",");
39+
private static final JsonAdapter<SymDbRemoteConfigRecord> SYM_DB_JSON_ADAPTER =
40+
MoshiHelper.createMoshiConfig().adapter(SymDbRemoteConfigRecord.class);
41+
42+
private final Instrumentation instrumentation;
43+
private final Config config;
44+
private final SymbolAggregator symbolAggregator;
45+
private SymbolExtractionTransformer symbolExtractionTransformer;
46+
private long lastUploadTimestamp;
47+
48+
public SymDBEnablement(
49+
Instrumentation instrumentation, Config config, SymbolAggregator symbolAggregator) {
50+
this.instrumentation = instrumentation;
51+
this.config = config;
52+
this.symbolAggregator = symbolAggregator;
53+
}
54+
55+
@Override
56+
public void accept(
57+
ParsedConfigKey configKey,
58+
byte[] content,
59+
ConfigurationChangesListener.PollingRateHinter pollingRateHinter)
60+
throws IOException {
61+
if (configKey.getConfigId().equals("symDb")) {
62+
SymDbRemoteConfigRecord symDb = deserializeSymDb(content);
63+
if (symDb.isUploadSymbols()) {
64+
startSymbolExtraction();
65+
} else {
66+
stopSymbolExtraction();
67+
}
68+
} else {
69+
throw new IOException("unsupported configuration id " + configKey.getConfigId());
70+
}
71+
}
72+
73+
@Override
74+
public void remove(
75+
ParsedConfigKey configKey, ConfigurationChangesListener.PollingRateHinter pollingRateHinter)
76+
throws IOException {}
77+
78+
@Override
79+
public void commit(ConfigurationChangesListener.PollingRateHinter pollingRateHinter) {}
80+
81+
private static SymDbRemoteConfigRecord deserializeSymDb(byte[] content) throws IOException {
82+
return SYM_DB_JSON_ADAPTER.fromJson(
83+
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
84+
}
85+
86+
public void stopSymbolExtraction() {
87+
LOGGER.debug("Stopping symbol extraction.");
88+
instrumentation.removeTransformer(symbolExtractionTransformer);
89+
}
90+
91+
public void startSymbolExtraction() {
92+
LOGGER.debug("Starting symbol extraction...");
93+
if (lastUploadTimestamp > 0) {
94+
LOGGER.debug(
95+
"Last upload was on {}",
96+
LocalDateTime.ofInstant(
97+
Instant.ofEpochMilli(lastUploadTimestamp), ZoneId.systemDefault()));
98+
return;
99+
}
100+
String includes = config.getDebuggerSymbolIncludes();
101+
AllowListHelper allowListHelper = new AllowListHelper(buildFilterList(includes));
102+
symbolExtractionTransformer =
103+
new SymbolExtractionTransformer(allowListHelper, symbolAggregator);
104+
instrumentation.addTransformer(symbolExtractionTransformer, true);
105+
extractSymbolForLoadedClasses(allowListHelper);
106+
lastUploadTimestamp = System.currentTimeMillis();
107+
}
108+
109+
private void extractSymbolForLoadedClasses(AllowListHelper allowListHelper) {
110+
Class<?>[] classesToExtract = null;
111+
try {
112+
classesToExtract =
113+
Arrays.stream(instrumentation.getAllLoadedClasses())
114+
.filter(clazz -> allowListHelper.isAllowed(clazz.getTypeName()))
115+
.filter(instrumentation::isModifiableClass)
116+
.toArray(Class<?>[]::new);
117+
} catch (Throwable ex) {
118+
LOGGER.debug("Failed to get all loaded classes", ex);
119+
return;
120+
}
121+
Set<String> alreadyScannedJars = new HashSet<>();
122+
byte[] buffer = new byte[4096];
123+
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
124+
for (Class<?> clazz : classesToExtract) {
125+
Path jarPath;
126+
try {
127+
jarPath = JarScanner.extractJarPath(clazz);
128+
} catch (URISyntaxException e) {
129+
throw new RuntimeException(e);
130+
}
131+
if (jarPath == null) {
132+
continue;
133+
}
134+
if (!Files.exists(jarPath)) {
135+
continue;
136+
}
137+
if (alreadyScannedJars.contains(jarPath.toString())) {
138+
continue;
139+
}
140+
try {
141+
try (JarFile jarFile = new JarFile(jarPath.toFile())) {
142+
jarFile.stream()
143+
.filter(jarEntry -> jarEntry.getName().endsWith(".class"))
144+
.filter(
145+
jarEntry ->
146+
allowListHelper.isAllowed(
147+
Strings.getClassName(trimPrefixes(jarEntry.getName()))))
148+
.forEach(jarEntry -> parseJarEntry(jarEntry, jarFile, jarPath, baos, buffer));
149+
}
150+
alreadyScannedJars.add(jarPath.toString());
151+
} catch (IOException e) {
152+
throw new RuntimeException(e);
153+
}
154+
}
155+
}
156+
157+
private void parseJarEntry(
158+
JarEntry jarEntry, JarFile jarFile, Path jarPath, ByteArrayOutputStream baos, byte[] buffer) {
159+
LOGGER.debug("parsing jarEntry class: {}", jarEntry.getName());
160+
try {
161+
InputStream inputStream = jarFile.getInputStream(jarEntry);
162+
int readBytes;
163+
baos.reset();
164+
while ((readBytes = inputStream.read(buffer)) != -1) {
165+
baos.write(buffer, 0, readBytes);
166+
}
167+
symbolAggregator.parseClass(jarEntry.getName(), baos.toByteArray(), jarPath.toString());
168+
} catch (IOException ex) {
169+
LOGGER.debug("Exception during parsing jarEntry class: {}", jarEntry.getName(), ex);
170+
}
171+
}
172+
173+
private Configuration.FilterList buildFilterList(String includes) {
174+
if (includes == null || includes.isEmpty()) {
175+
return null;
176+
}
177+
String[] includeParts = COMMA_PATTERN.split(includes);
178+
return new Configuration.FilterList(Arrays.asList(includeParts), Collections.emptyList());
179+
}
180+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.datadog.debugger.symbol;
2+
3+
import com.squareup.moshi.Json;
4+
5+
public class SymDbRemoteConfigRecord {
6+
@Json(name = "upload_symbols")
7+
private final boolean uploadSymbols;
8+
9+
public SymDbRemoteConfigRecord(boolean uploadSymbols) {
10+
this.uploadSymbols = uploadSymbols;
11+
}
12+
13+
public boolean isUploadSymbols() {
14+
return uploadSymbols;
15+
}
16+
}

0 commit comments

Comments
 (0)