Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions src/main/java/com/datadog/NamedPipeNames.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.datadog;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

/**
* Chooses the trace and DogStatsD pipe names for one function, following the .NET
* compatibility layer: the binary's *_WINDOWS_PIPE_NAME setting, then a name the customer
* already set for the tracer, then a generated per-function name.
*/
final class NamedPipeNames {
static final int MAX_PIPE_NAME_LENGTH = 256;

static final String TRACE_WINDOWS_PIPE_NAME_ENV = "DD_APM_WINDOWS_PIPE_NAME";
static final String TRACE_PIPE_NAME_ENV = "DD_TRACE_PIPE_NAME";
static final String TRACE_PIPE_NAME_PROPERTY = "dd.trace.pipe.name";

static final String DOGSTATSD_WINDOWS_PIPE_NAME_ENV = "DD_DOGSTATSD_WINDOWS_PIPE_NAME";
static final String DOGSTATSD_PIPE_NAME_ENV = "DD_DOGSTATSD_PIPE_NAME";
static final String DOGSTATSD_PIPE_NAME_PROPERTY = "dd.dogstatsd.pipe.name";

final String tracePipeName;
final String dogstatsdPipeName;
final List<String> warnings;

private NamedPipeNames(String tracePipeName, String dogstatsdPipeName, List<String> warnings) {
this.tracePipeName = tracePipeName;
this.dogstatsdPipeName = dogstatsdPipeName;
this.warnings = Collections.unmodifiableList(warnings);
}

static NamedPipeNames choose(Map<String, String> env, Properties systemProperties) {
List<String> warnings = new ArrayList<>();
String tracePipeName = choosePipeName(env, systemProperties, TRACE_WINDOWS_PIPE_NAME_ENV,
TRACE_PIPE_NAME_PROPERTY, TRACE_PIPE_NAME_ENV, "dd_trace_", warnings);
String dogstatsdPipeName = choosePipeName(env, systemProperties, DOGSTATSD_WINDOWS_PIPE_NAME_ENV,
DOGSTATSD_PIPE_NAME_PROPERTY, DOGSTATSD_PIPE_NAME_ENV, "dd_dogstatsd_", warnings);
return new NamedPipeNames(tracePipeName, dogstatsdPipeName, warnings);
}

private static String choosePipeName(Map<String, String> env, Properties systemProperties,
String windowsPipeNameEnv, String pipeNameProperty, String pipeNameEnv,
String generatedPrefix, List<String> warnings) {
// The tracer reads system properties before environment variables, so a JVM option
// the customer passed is the customer-set name when both are present.
String customerSetName = nonEmpty(systemProperties.getProperty(pipeNameProperty));
String customerSetSource = pipeNameProperty;
if (customerSetName == null) {
customerSetName = nonEmpty(env.get(pipeNameEnv));
customerSetSource = pipeNameEnv;
}

String windowsPipeName = nonEmpty(env.get(windowsPipeNameEnv));
if (windowsPipeName != null) {
if (customerSetName != null && !customerSetName.equals(windowsPipeName)) {
warnings.add(String.format("%s ('%s') differs from %s ('%s'). Using %s.",
customerSetSource, customerSetName, windowsPipeNameEnv, windowsPipeName,
windowsPipeNameEnv));
}
warnIfTooLong(windowsPipeNameEnv, windowsPipeName, warnings);
return windowsPipeName;
}

if (customerSetName != null) {
warnIfTooLong(customerSetSource, customerSetName, warnings);
return customerSetName;
}

return generatedPrefix + UUID.randomUUID().toString().replace("-", "");
}

private static void warnIfTooLong(String source, String pipeName, List<String> warnings) {
if (pipeName.length() > MAX_PIPE_NAME_LENGTH) {
warnings.add(String.format(
"%s is %d characters long. Windows pipe names can be at most %d characters.",
source, pipeName.length(), MAX_PIPE_NAME_LENGTH));
}
}

private static String nonEmpty(String value) {
return value == null || value.isEmpty() ? null : value;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/datadog/ServerlessCompatAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,34 @@ public static boolean isAzureFlexWithoutDDAzureResourceGroup() {
&& System.getenv("DD_AZURE_RESOURCE_GROUP") == null;
}

/**
* Gives this function its own named pipes when dd-java-agent supports them. The tracer reads
* these system properties before its environment variables, so they must be set before
* dd-java-agent's premain runs. When the gate fails, nothing is set and the tracer keeps
* whatever it inherited: TCP and UDP by default, or the customer's own pipe settings.
*
* @return the chosen pipe names, or null when named pipes are off
*/
static NamedPipeNames configureNamedPipes() {
TracerVersionGate gate = TracerVersionGate.check();
if (!gate.passed) {
log.info("Named pipes are off, keeping inherited trace and DogStatsD settings: {}", gate.reason);
return null;
}
log.debug("Named pipes are on: {}", gate.reason);

NamedPipeNames pipeNames = NamedPipeNames.choose(System.getenv(), System.getProperties());
for (String warning : pipeNames.warnings) {
log.warn(warning);
}

System.setProperty(NamedPipeNames.TRACE_PIPE_NAME_PROPERTY, pipeNames.tracePipeName);
System.setProperty(NamedPipeNames.DOGSTATSD_PIPE_NAME_PROPERTY, pipeNames.dogstatsdPipeName);
log.info("Configured named pipes - Trace: {}, DogStatsD: {}",
pipeNames.tracePipeName, pipeNames.dogstatsdPipeName);
return pipeNames;
}

public static void premain(String agentArgs, Instrumentation instrumentation) {
CloudEnvironment environment = getEnvironment();
log.debug("Environment detected: {}", environment);
Expand Down Expand Up @@ -169,8 +197,18 @@ public static void premain(String agentArgs, Instrumentation instrumentation) {
String packageVersion = getPackageVersion();
log.debug("Found package version {}", packageVersion);

NamedPipeNames pipeNames = null;
if (environment == CloudEnvironment.AZURE_FUNCTION && isWindows()) {
pipeNames = configureNamedPipes();
}

ProcessBuilder processBuilder = new ProcessBuilder(executableFile.getAbsolutePath());
processBuilder.environment().put("DD_SERVERLESS_COMPAT_VERSION", packageVersion);
if (pipeNames != null) {
processBuilder.environment().put(NamedPipeNames.TRACE_WINDOWS_PIPE_NAME_ENV, pipeNames.tracePipeName);
processBuilder.environment().put(NamedPipeNames.DOGSTATSD_WINDOWS_PIPE_NAME_ENV,
pipeNames.dogstatsdPipeName);
}
processBuilder.inheritIO();
processBuilder.start();
} catch (Exception e) {
Expand Down
131 changes: 131 additions & 0 deletions src/main/java/com/datadog/TracerVersionGate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.datadog;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Decides whether the dd-java-agent in this JVM supports named pipes. Older tracers hang
* uploading traces over a pipe and never send their own metrics over one, so pipe names are
* only handed out when the tracer is at least {@link #MINIMUM_TRACER_VERSION}.
*/
final class TracerVersionGate {
static final String TRACER_PREMAIN_CLASS = "datadog.trace.bootstrap.AgentPreCheck";

// Placeholder: the next unreleased dd-trace-java version on master. Replace it with the
// first dd-trace-java release that includes the named-pipe transport and StatsD fixes.
static final String MINIMUM_TRACER_VERSION = "1.68.0";

private static final String JAVAAGENT_PREFIX = "-javaagent:";
private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?$");

final boolean passed;
final String reason;

private TracerVersionGate(boolean passed, String reason) {
this.passed = passed;
this.reason = reason;
}

static TracerVersionGate check() {
List<String> jvmArguments;
try {
jvmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
} catch (Exception e) {
return new TracerVersionGate(false, "unable to read JVM arguments to find dd-java-agent: " + e);
}
return check(jvmArguments);
}

static TracerVersionGate check(List<String> jvmArguments) {
return check(jvmArguments, MINIMUM_TRACER_VERSION);
}

static TracerVersionGate check(List<String> jvmArguments, String minimumVersion) {
int[] minimum = parseVersion(minimumVersion);
if (minimum == null) {
return new TracerVersionGate(false, "invalid minimum dd-java-agent version " + minimumVersion);
}

for (String argument : jvmArguments) {
if (!argument.startsWith(JAVAAGENT_PREFIX)) {
continue;
}
String jarPath = javaagentJarPath(argument);
Manifest manifest = readManifest(jarPath);
if (manifest == null) {
continue;
}
Attributes attributes = manifest.getMainAttributes();
if (!TRACER_PREMAIN_CLASS.equals(attributes.getValue("Premain-Class"))) {
continue;
}

String version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
if (version == null || version.isEmpty()) {
return new TracerVersionGate(false,
"dd-java-agent at " + jarPath + " has no Implementation-Version in its manifest");
}
int[] tracer = parseVersion(version);
if (tracer == null) {
return new TracerVersionGate(false,
"unable to read dd-java-agent version '" + version + "' at " + jarPath);
}
if (compare(tracer, minimum) < 0) {
return new TracerVersionGate(false, "dd-java-agent " + version + " at " + jarPath
+ " is older than " + minimumVersion + ", the first version that supports named pipes");
}
return new TracerVersionGate(true, "dd-java-agent " + version + " at " + jarPath
+ " supports named pipes (minimum " + minimumVersion + ")");
}

return new TracerVersionGate(false,
"no readable dd-java-agent found among -javaagent options (Premain-Class " + TRACER_PREMAIN_CLASS + ")");
}

// The JVM treats everything after the first '=' as agent options.
private static String javaagentJarPath(String argument) {
String value = argument.substring(JAVAAGENT_PREFIX.length());
int optionsStart = value.indexOf('=');
return optionsStart >= 0 ? value.substring(0, optionsStart) : value;
}

private static Manifest readManifest(String jarPath) {
try (JarFile jarFile = new JarFile(jarPath, false)) {
return jarFile.getManifest();
} catch (IOException | RuntimeException e) {
return null;
}
}

// Reads major.minor.patch and ignores any suffix, so 1.68.0-SNAPSHOT counts as 1.68.0.
private static int[] parseVersion(String version) {
Matcher matcher = VERSION_PATTERN.matcher(version);
if (!matcher.matches()) {
return null;
}
try {
return new int[] {
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)),
Integer.parseInt(matcher.group(3))
};
} catch (NumberFormatException e) {
return null;
}
}

private static int compare(int[] left, int[] right) {
for (int i = 0; i < left.length; i++) {
if (left[i] != right[i]) {
return Integer.compare(left[i], right[i]);
}
}
return 0;
}
}
Loading