Skip to content

Added special lightweight pre-main class that skips installation on incompatible JVMs. #8855

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 21 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e9b6ba
Added special lightweight pre-main class to enable SSI inject for Jav…
AlexeyKuznetsov-DD May 20, 2025
c40db79
Fixed SuppressForbidden test.
AlexeyKuznetsov-DD May 20, 2025
125a148
Refactored pre-check logic.
AlexeyKuznetsov-DD May 23, 2025
1e7ecd1
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 23, 2025
99abf99
Refactored build.gradle to reuse existing functions.
AlexeyKuznetsov-DD May 23, 2025
2eb2753
Minor cleanup.
AlexeyKuznetsov-DD May 23, 2025
0c2e817
Added missing `@SuppressForbidden`.
AlexeyKuznetsov-DD May 23, 2025
45fb91f
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 23, 2025
78dc42b
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 23, 2025
ea5bea7
Added 'java.home' to warning. That would help to identify problematic…
AlexeyKuznetsov-DD May 23, 2025
5695279
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 23, 2025
9238f7d
Added test to ensure Java 6 & 8 compilation.
AlexeyKuznetsov-DD May 24, 2025
10d60a0
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 24, 2025
5f61b62
Fixed test.
AlexeyKuznetsov-DD May 24, 2025
5fa1bb6
Drop support for 0.x version.
AlexeyKuznetsov-DD May 27, 2025
985412f
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 27, 2025
be58d6b
Fixed small typo.
AlexeyKuznetsov-DD May 27, 2025
7c2a597
Fixed test.
AlexeyKuznetsov-DD May 27, 2025
8192ac4
Update dd-java-agent/src/main/java6/datadog/trace/bootstrap/AgentPreC…
AlexeyKuznetsov-DD May 28, 2025
1037295
Merge branch 'master' into ssi-inject-java-6
AlexeyKuznetsov-DD May 28, 2025
e1df6fe
Fixed test.
AlexeyKuznetsov-DD May 28, 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
27 changes: 23 additions & 4 deletions dd-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,27 @@ configurations {
traceShadowInclude
}

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
// The special pre-check should be compiled with Java 6 to detect unsupported Java versions
// and prevent issues for users that still using them.
sourceSets {
"main_java6" {
java.srcDirs "${project.projectDir}/src/main/java6"
}
}

compileMain_java6Java.configure {
setJavaVersion(it, 8)
sourceCompatibility = JavaVersion.VERSION_1_6
targetCompatibility = JavaVersion.VERSION_1_6
destinationDirectory = layout.buildDirectory.dir("classes/java/main")
}

tasks.compileJava.dependsOn compileMain_java6Java

dependencies {
main_java6CompileOnly 'de.thetaphi:forbiddenapis:3.8'
testImplementation sourceSets.main_java6.output
}

/*
* Several shadow jars are created
Expand Down Expand Up @@ -68,7 +87,7 @@ ext.generalShadowJarConfig = {
if (!projectName.equals('instrumentation')) {
relocate 'org.snakeyaml.engine', 'datadog.snakeyaml.engine'
relocate 'okhttp3', 'datadog.okhttp3'
relocate 'okio', 'datadog.okio'
relocate 'okio', 'datadog.okio'
}

if (!project.hasProperty("disableShadowRelocate") || !disableShadowRelocate) {
Expand Down Expand Up @@ -176,7 +195,7 @@ shadowJar generalShadowJarConfig >> {
attributes(
"Main-Class": "datadog.trace.bootstrap.AgentBootstrap",
"Agent-Class": "datadog.trace.bootstrap.AgentBootstrap",
"Premain-Class": "datadog.trace.bootstrap.AgentBootstrap",
"Premain-Class": "datadog.trace.bootstrap.AgentPreCheck",
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URI;
Expand Down Expand Up @@ -126,10 +125,6 @@ private static void agentmainImpl(
// since tracer is presumably initialized elsewhere, still considering this complete
return;
}
if (lessThanJava8()) {
initTelemetry.onAbort("incompatible_runtime");
return;
}
if (isJdkTool()) {
initTelemetry.onAbort("jdk_tool");
return;
Expand Down Expand Up @@ -170,7 +165,7 @@ static boolean getConfig(String configName) {
}

static boolean exceptionCauseChainContains(Throwable ex, String exClassName) {
Set<Throwable> stack = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
Set<Throwable> stack = Collections.newSetFromMap(new IdentityHashMap<>());
Throwable t = ex;
while (t != null && stack.add(t) && stack.size() <= MAX_EXCEPTION_CHAIN_LENGTH) {
// cannot do an instanceof check since most of the agent's code is loaded by an isolated CL
Expand All @@ -193,36 +188,6 @@ private static boolean alreadyInitialized() {
return false;
}

@SuppressForbidden
private static boolean lessThanJava8() {
try {
return lessThanJava8(System.getProperty("java.version"), System.err);
} catch (SecurityException e) {
// Hypothetically, we could version sniff the supported version level
// For now, just skip the check and let the JVM handle things instead
return false;
}
}

// Reachable for testing
static boolean lessThanJava8(String version, PrintStream output) {
if (parseJavaMajorVersion(version) < 8) {
String agentRawVersion = AgentJar.tryGetAgentVersion();
String agentVersion = agentRawVersion == null ? "This version" : "Version " + agentRawVersion;

output.println(
"Warning: "
+ agentVersion
+ " of dd-java-agent is not compatible with Java "
+ version
+ " and will not be installed.");
output.println(
"Please upgrade your Java version to 8+ or use the 0.x version of dd-java-agent in your build tool or download it from https://dtdg.co/java-tracer-v0");
return true;
}
return false;
}

private static boolean isJdkTool() {
String moduleMain = SystemUtils.tryGetProperty("jdk.module.main");
if (null != moduleMain && !moduleMain.isEmpty() && moduleMain.charAt(0) == 'j') {
Expand Down Expand Up @@ -263,32 +228,6 @@ private static boolean isJdkTool() {
return false;
}

// Reachable for testing
static int parseJavaMajorVersion(String version) {
int major = 0;
if (null == version || version.isEmpty()) {
return major;
}
int start = 0;
if (version.charAt(0) == '1'
&& version.length() >= 3
&& version.charAt(1) == '.'
&& Character.isDigit(version.charAt(2))) {
start = 2;
}
// Parse the major digit and be a bit lenient, allowing digits followed by any non digit
for (int i = start; i < version.length(); i++) {
char c = version.charAt(i);
if (Character.isDigit(c)) {
major *= 10;
major += Character.digit(c, 10);
} else {
break;
}
}
return major;
}

@SuppressForbidden
static boolean shouldAbortDueToOtherJavaAgents() {
// We don't abort if either
Expand Down Expand Up @@ -333,9 +272,6 @@ static boolean shouldAbortDueToOtherJavaAgents() {
}

public static void main(final String[] args) {
if (lessThanJava8()) {
return;
}
AgentJar.main(args);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package datadog.trace.bootstrap;

import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;

/** Special lightweight pre-main class that skips installation on incompatible JVMs. */
public class AgentPreCheck {
public static void premain(final String agentArgs, final Instrumentation inst) {
agentmain(agentArgs, inst);
}

@SuppressForbidden
public static void agentmain(final String agentArgs, final Instrumentation inst) {
try {
if (compatible()) {
continueBootstrap(agentArgs, inst);
}
} catch (Throwable e) {
// If agent failed we should not fail the application.
// We don't have a log manager here, so just print.
System.err.println("ERROR: " + e.getMessage());
}
}

private static void reportIncompatibleJava(
String javaVersion, String javaHome, String agentVersion, PrintStream output) {
output.println(
"Warning: "
+ (agentVersion == null ? "This version" : "Version " + agentVersion)
+ " of dd-java-agent is not compatible with Java "
+ javaVersion
+ " found at '"
+ javaHome
+ "' and is effectively disabled.");
output.println("Please upgrade your Java version to 8+");
}

static void sendTelemetry(String forwarderPath, String javaVersion, String agentVersion) {
// Hardcoded payload for unsupported Java version.
String payload =
"{\"metadata\":{"
+ "\"runtime_name\":\"jvm\","
+ "\"language_name\":\"jvm\","
+ "\"runtime_version\":\""
+ javaVersion
Copy link
Contributor

Choose a reason for hiding this comment

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

Part of me doesn't like that these aren't escaped properly, but I suppose for this use case it is okay.

+ "\","
+ "\"language_version\":\""
+ javaVersion
+ "\","
+ "\"tracer_version\":\""
+ agentVersion
+ "\"},"
+ "\"points\":[{"
+ "\"name\":\"library_entrypoint.abort\","
+ "\"tags\":[\"reason:incompatible_runtime\"]"
+ "}]"
+ "}";

ForwarderJsonSenderThread t = new ForwarderJsonSenderThread(forwarderPath, payload);
t.setDaemon(true);
t.start();
}

private static String tryGetProperty(String property) {
try {
return System.getProperty(property);
} catch (SecurityException e) {
return null;
}
}

@SuppressForbidden
private static boolean compatible() {
String javaVersion = tryGetProperty("java.version");
String javaHome = tryGetProperty("java.home");

return compatible(javaVersion, javaHome, System.err);
}

// Reachable for testing
static boolean compatible(String javaVersion, String javaHome, PrintStream output) {
int majorJavaVersion = parseJavaMajorVersion(javaVersion);

if (majorJavaVersion >= 8) {
return true;
}

String agentVersion = getAgentVersion();

reportIncompatibleJava(javaVersion, javaHome, agentVersion, output);

String forwarderPath = System.getenv("DD_TELEMETRY_FORWARDER_PATH");
if (forwarderPath != null) {
sendTelemetry(forwarderPath, javaVersion, agentVersion);
}

return false;
}

// Reachable for testing
static int parseJavaMajorVersion(String javaVersion) {
int major = 0;
if (javaVersion == null || javaVersion.isEmpty()) {
return major;
}

int start = 0;
if (javaVersion.charAt(0) == '1'
&& javaVersion.length() >= 3
&& javaVersion.charAt(1) == '.'
&& Character.isDigit(javaVersion.charAt(2))) {
start = 2;
}

// Parse the major digit and be a bit lenient, allowing digits followed by any non digit
for (int i = start; i < javaVersion.length(); i++) {
char c = javaVersion.charAt(i);
if (Character.isDigit(c)) {
major *= 10;
major += Character.digit(c, 10);
} else {
break;
}
}
return major;
}

private static String getAgentVersion() {
try {
// Get the resource as an InputStream
InputStream is = AgentPreCheck.class.getResourceAsStream("/dd-java-agent.version");
if (is == null) {
return null;
}

BufferedReader reader = new BufferedReader(new InputStreamReader(is));
final StringBuilder sb = new StringBuilder();
for (int c = reader.read(); c != -1; c = reader.read()) {
sb.append((char) c);
}
reader.close();

return sb.toString().trim();
} catch (Throwable e) {
return null;
}
}

@SuppressForbidden
private static void continueBootstrap(final String agentArgs, final Instrumentation inst)
throws Exception {
Class<?> clazz = Class.forName("datadog.trace.bootstrap.AgentBootstrap");
Method agentMain = clazz.getMethod("agentmain", String.class, Instrumentation.class);
agentMain.invoke(null, agentArgs, inst);
}

public static final class ForwarderJsonSenderThread extends Thread {
private final String forwarderPath;
private final String payload;

public ForwarderJsonSenderThread(String forwarderPath, String payload) {
super("dd-forwarder-json-sender");
setDaemon(true);
this.forwarderPath = forwarderPath;
this.payload = payload;
}

@SuppressForbidden
@Override
public void run() {
ProcessBuilder builder = new ProcessBuilder(forwarderPath, "library_entrypoint");
try {
Process process = builder.start();
OutputStream out = null;
try {
out = process.getOutputStream();
out.write(payload.getBytes());
} finally {
if (out != null) {
out.close();
}
}
} catch (Throwable e) {
System.err.println("Failed to send telemetry: " + e.getMessage());
}
}
}
}
Loading