Skip to content

Commit befdf9b

Browse files
authored
Merge branch 'master' into alejandro.gonzalez/APPSEC-61873-3
2 parents eb6ca70 + 5ab378f commit befdf9b

File tree

21 files changed

+789
-382
lines changed

21 files changed

+789
-382
lines changed

dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import datadog.trace.core.DDSpan;
2121
import datadog.trace.core.PendingTrace;
2222
import datadog.trace.core.TraceCollector;
23-
import de.thetaphi.forbiddenapis.SuppressForbidden;
23+
import datadog.trace.junit.utils.context.AllowContextTestingExtension;
2424
import java.lang.instrument.ClassFileTransformer;
2525
import java.lang.instrument.Instrumentation;
2626
import java.util.List;
@@ -31,7 +31,6 @@
3131
import java.util.function.Predicate;
3232
import net.bytebuddy.agent.ByteBuddyAgent;
3333
import org.junit.jupiter.api.AfterEach;
34-
import org.junit.jupiter.api.BeforeAll;
3534
import org.junit.jupiter.api.BeforeEach;
3635
import org.junit.jupiter.api.extension.ExtendWith;
3736
import org.opentest4j.AssertionFailedError;
@@ -42,7 +41,7 @@
4241
* current implementation is inspired and kept close to it Groovy / Spock counterpart, the {@code
4342
* InstrumentationSpecification}.
4443
*/
45-
@ExtendWith(TestClassShadowingExtension.class)
44+
@ExtendWith({TestClassShadowingExtension.class, AllowContextTestingExtension.class})
4645
public abstract class AbstractInstrumentationTest {
4746
static final Instrumentation INSTRUMENTATION = ByteBuddyAgent.getInstrumentation();
4847

@@ -55,19 +54,6 @@ public abstract class AbstractInstrumentationTest {
5554
protected ClassFileTransformer activeTransformer;
5655
protected ClassFileTransformerListener transformerLister;
5756

58-
@SuppressForbidden // Class.forName() used to dynamically configure context if present
59-
@BeforeAll
60-
static void allowContextTesting() {
61-
// Allow re-registration of context managers so each test can use a fresh tracer.
62-
// This mirrors DDSpecification.allowContextTesting() for the Spock test framework.
63-
try {
64-
Class.forName("datadog.context.ContextManager").getMethod("allowTesting").invoke(null);
65-
Class.forName("datadog.context.ContextBinder").getMethod("allowTesting").invoke(null);
66-
} catch (Throwable ignore) {
67-
// don't block testing if context types aren't available
68-
}
69-
}
70-
7157
@BeforeEach
7258
public void init() {
7359
// If this fails, it's likely the result of another test loading Config before it can be

dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ abstract class ProcessManager extends Specification {
5050

5151
// Here for backwards compatibility with single process case
5252
@Shared
53-
def logFilePath = logFilePaths[0]
53+
def logFilePath = logFilePaths.length > 0 ? logFilePaths[0] : null
5454

5555
def setup() {
5656
testedProcesses.each {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apply from: "$rootDir/gradle/java.gradle"
2+
3+
dependencies {
4+
testImplementation project(':dd-smoke-tests')
5+
testImplementation libs.testcontainers
6+
}
7+
8+
testJvmConstraints {
9+
// there is no need to run it multiple times since it runs on a container
10+
maxJavaVersion = JavaVersion.VERSION_1_8
11+
}
12+
13+
tasks.withType(Test).configureEach {
14+
usesService(testcontainersLimit)
15+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package datadog.smoketest
2+
3+
4+
import java.time.Duration
5+
import java.util.concurrent.ArrayBlockingQueue
6+
import java.util.concurrent.BlockingQueue
7+
import java.util.concurrent.TimeUnit
8+
import org.slf4j.Logger
9+
import org.slf4j.LoggerFactory
10+
import org.testcontainers.containers.GenericContainer
11+
import org.testcontainers.containers.output.Slf4jLogConsumer
12+
import org.testcontainers.containers.wait.strategy.Wait
13+
import org.testcontainers.utility.MountableFile
14+
import spock.lang.Shared
15+
16+
/**
17+
* Smoke test for the websphere-jmx instrumentation.
18+
*
19+
* Builds and starts a WebSphere traditional (tWAS) container with the dd-java-agent baked in
20+
* via jvm-config.props, then verifies that jmxfetch reports WebSphere thread pool metrics
21+
* via statsd UDP.
22+
*
23+
* Note that the websphere related metrics will only arrive if our instrumentation is applied.
24+
*/
25+
class WebSphereJmxSmokeTest extends AbstractSmokeTest {
26+
27+
private static final Logger LOG = LoggerFactory.getLogger(WebSphereJmxSmokeTest)
28+
29+
@Override
30+
protected int numberOfProcesses() {
31+
return 0
32+
}
33+
34+
@Shared
35+
DatagramSocket statsdSocket
36+
37+
@Shared
38+
int statsdPort
39+
40+
@Shared
41+
BlockingQueue<String> statsdMessages = new ArrayBlockingQueue<>(256)
42+
43+
@Shared
44+
Thread listenerThread
45+
46+
@Shared
47+
GenericContainer websphere
48+
49+
def setupSpec() {
50+
statsdSocket = new DatagramSocket()
51+
statsdPort = statsdSocket.getLocalPort()
52+
LOG.info("StatsDServer listening on UDP port {}", statsdPort)
53+
54+
listenerThread = Thread.start {
55+
byte[] buf = new byte[2048]
56+
while (!Thread.currentThread().interrupted()) {
57+
try {
58+
DatagramPacket packet = new DatagramPacket(buf, buf.length)
59+
statsdSocket.receive(packet)
60+
String msg = new String(packet.getData(), 0, packet.getLength())
61+
LOG.debug("Received statsd: {}", msg)
62+
statsdMessages.offer(msg, 1, TimeUnit.SECONDS)
63+
} catch (Exception ignored) {
64+
break
65+
}
66+
}
67+
}
68+
69+
websphere = new GenericContainer("icr.io/appcafe/websphere-traditional:latest")
70+
// inject wished jvm props for the server we are running
71+
.withCopyFileToContainer(MountableFile.forClasspathResource("jvm-config.props"), "/work/config/")
72+
// copy the agent jar
73+
.withCopyFileToContainer(MountableFile.forHostPath(shadowJarPath), "/opt/dd-java-agent.jar")
74+
// let it run on a macos for dev
75+
.withCreateContainerCmdModifier { it.withPlatform('linux/amd64') }
76+
// this is required to send back udp datagrams to us
77+
.withExtraHost('host.docker.internal', 'host-gateway')
78+
// set jmxfetch props
79+
.withEnv('DD_JMXFETCH_STATSD_HOST', 'host.docker.internal')
80+
.withEnv('DD_JMXFETCH_STATSD_PORT', String.valueOf(statsdPort))
81+
.withLogConsumer(new Slf4jLogConsumer(LOG).withPrefix('websphere'))
82+
// the server will restart 2 times. First to update the jvm props
83+
.waitingFor(Wait.forLogMessage('.*open for e-business.*', 2))
84+
// it can be long
85+
.withStartupTimeout(Duration.ofMinutes(8))
86+
// override the command (by default it's /work/start_server.sh)
87+
.withCommand("bash", "-c", "/work/configure.sh && /work/start_server.sh")
88+
89+
websphere.start()
90+
LOG.info("WebSphere container started")
91+
}
92+
93+
def cleanupSpec() {
94+
websphere?.stop()
95+
statsdSocket?.close()
96+
listenerThread?.join()
97+
}
98+
99+
def "jmxfetch reports WebSphere thread pool metrics via statsd"() {
100+
when: "waiting for websphere.thread_pool metrics"
101+
String metric = waitForMetric('websphere.thread_pool', Duration.ofMinutes(3))
102+
103+
then: "at least one thread pool metric arrives"
104+
metric != null
105+
metric.contains('websphere.thread_pool')
106+
}
107+
108+
def waitForMetric(String prefix, Duration timeout) {
109+
long deadline = System.currentTimeMillis() + timeout.toMillis()
110+
while (System.currentTimeMillis() < deadline) {
111+
String msg = statsdMessages.poll(5, TimeUnit.SECONDS)
112+
if (msg?.contains(prefix)) {
113+
return msg
114+
}
115+
}
116+
return null
117+
}
118+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ResourceType=JavaVirtualMachine
2+
ImplementingResourceType=Server
3+
ResourceId=Cell=!{cellName}:Node=!{nodeName}:Server=!{serverName}:JavaProcessDef=:JavaVirtualMachine=
4+
AttributeInfo=jvmEntries
5+
6+
genericJvmArguments=-javaagent:/opt/dd-java-agent.jar -Ddd.trace.debug=true -Ddd.jmxfetch.websphere.enabled=true

dd-trace-api/src/test/java/datadog/trace/api/DDTraceApiTableTestConverters.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package datadog.trace.api;
22

3-
import datadog.trace.test.util.TableTestTypeConverters;
3+
import datadog.trace.junit.utils.tabletest.TableTestTypeConverters;
44
import org.tabletest.junit.TypeConverter;
55

66
/** TableTest converters shared by dd-trace-api test classes for unparsable constants. */
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package datadog.trace.core;
2+
3+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
4+
5+
import datadog.trace.api.DDTraceId;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.BenchmarkMode;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Level;
10+
import org.openjdk.jmh.annotations.Measurement;
11+
import org.openjdk.jmh.annotations.Mode;
12+
import org.openjdk.jmh.annotations.OutputTimeUnit;
13+
import org.openjdk.jmh.annotations.Scope;
14+
import org.openjdk.jmh.annotations.Setup;
15+
import org.openjdk.jmh.annotations.State;
16+
import org.openjdk.jmh.annotations.TearDown;
17+
import org.openjdk.jmh.annotations.Threads;
18+
import org.openjdk.jmh.annotations.Warmup;
19+
20+
@State(Scope.Thread)
21+
@Warmup(iterations = 3)
22+
@Measurement(iterations = 5)
23+
@BenchmarkMode(Mode.Throughput)
24+
@Threads(8)
25+
@OutputTimeUnit(NANOSECONDS)
26+
@Fork(value = 1)
27+
public class TimeSourceBenchmark {
28+
29+
static final CoreTracer TRACER = CoreTracer.builder().build();
30+
31+
private PendingTrace pendingTrace;
32+
33+
@Setup(Level.Trial)
34+
public void setup() {
35+
TraceCollector collector = TRACER.createTraceCollector(DDTraceId.ONE);
36+
pendingTrace = (PendingTrace) collector;
37+
}
38+
39+
@TearDown(Level.Trial)
40+
public void teardown() {
41+
pendingTrace = null;
42+
}
43+
44+
@Benchmark
45+
public long getCurrentTimeNano() {
46+
return pendingTrace.getCurrentTimeNano();
47+
}
48+
49+
@Benchmark
50+
public long systemNanoTime() {
51+
return System.nanoTime();
52+
}
53+
54+
@Benchmark
55+
public long systemCurrentTimeMillis() {
56+
return System.currentTimeMillis();
57+
}
58+
59+
@Benchmark
60+
public long traceGetTimeWithNanoTicks() {
61+
return TRACER.getTimeWithNanoTicks(System.nanoTime());
62+
}
63+
64+
/**
65+
* Measures a full span start + finish cycle, exercising both the {@code rootSpan} CAS guard in
66+
* {@link PendingTrace#registerSpan} and the {@code lazySet} of {@code lastReferenced} in {@link
67+
* PendingTrace#getCurrentTimeNano}.
68+
*/
69+
@Benchmark
70+
public void startAndFinishSpan() {
71+
TRACER.startSpan("benchmark", "op").finish();
72+
}
73+
74+
@State(Scope.Benchmark)
75+
public static class SharedState {
76+
PendingTrace sharedTrace;
77+
78+
@Setup(Level.Trial)
79+
public void setup() {
80+
TraceCollector collector = TRACER.createTraceCollector(DDTraceId.ONE);
81+
sharedTrace = (PendingTrace) collector;
82+
}
83+
84+
@TearDown(Level.Trial)
85+
public void teardown() {
86+
sharedTrace = null;
87+
}
88+
}
89+
90+
/**
91+
* Measures {@link PendingTrace#getCurrentTimeNano()} under cross-thread contention on a single
92+
* shared {@code PendingTrace}. All threads write to the same {@code lastReferenced} field,
93+
* demonstrating the benefit of {@code lazySet} over a volatile store under contention.
94+
*/
95+
@Benchmark
96+
public long getCurrentTimeNano_contended(SharedState shared) {
97+
return shared.sharedTrace.getCurrentTimeNano();
98+
}
99+
}

dd-trace-core/src/main/java/datadog/trace/core/PendingTrace.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,14 @@ public PendingTrace create(@Nonnull DDTraceId traceId, ConfigSnapshot traceConfi
131131

132132
/**
133133
* Updated with the latest nanoTicks each time getCurrentTimeNano is called (at the start and
134-
* finish of each span).
134+
* finish of each span). Uses lazySet for writes (release-store semantics) since the value is only
135+
* read for approximate timeout detection by the PendingTraceBuffer background thread.
135136
*/
136137
private volatile long lastReferenced = 0;
137138

139+
private static final AtomicLongFieldUpdater<PendingTrace> LAST_REFERENCED =
140+
AtomicLongFieldUpdater.newUpdater(PendingTrace.class, "lastReferenced");
141+
138142
private PendingTrace(
139143
@Nonnull CoreTracer tracer,
140144
@Nonnull DDTraceId traceId,
@@ -163,25 +167,27 @@ private PendingTrace(
163167
@Override
164168
public long getCurrentTimeNano() {
165169
long nanoTicks = timeSource.getNanoTicks();
166-
lastReferenced = nanoTicks;
170+
LAST_REFERENCED.lazySet(this, nanoTicks);
167171
return tracer.getTimeWithNanoTicks(nanoTicks);
168172
}
169173

170174
@Override
171175
void touch() {
172-
lastReferenced = timeSource.getNanoTicks();
176+
LAST_REFERENCED.lazySet(this, timeSource.getNanoTicks());
173177
}
174178

175179
@Override
176180
public boolean lastReferencedNanosAgo(long nanos) {
177181
long currentNanoTicks = timeSource.getNanoTicks();
178-
long age = currentNanoTicks - lastReferenced;
182+
long age = currentNanoTicks - LAST_REFERENCED.get(this);
179183
return nanos < age;
180184
}
181185

182186
@Override
183187
void registerSpan(final DDSpan span) {
184-
ROOT_SPAN.compareAndSet(this, null, span);
188+
if (rootSpan == null) {
189+
ROOT_SPAN.compareAndSet(this, null, span);
190+
}
185191
PENDING_REFERENCE_COUNT.incrementAndGet(this);
186192
healthMetrics.onCreateSpan();
187193
if (pendingTraceBuffer.longRunningSpansEnabled()) {

dd-trace-core/src/main/java/datadog/trace/core/propagation/W3CHttpCodec.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@
3232
class W3CHttpCodec {
3333
private static final Logger log = LoggerFactory.getLogger(W3CHttpCodec.class);
3434

35-
static final String TRACE_PARENT_KEY = "traceparent";
36-
static final String TRACE_STATE_KEY = "tracestate";
37-
static final String OT_BAGGAGE_PREFIX = "ot-baggage-";
38-
private static final String E2E_START_KEY = OT_BAGGAGE_PREFIX + DDTags.TRACE_START_TIME;
39-
4035
private static final int TRACE_PARENT_TID_START = 2 + 1;
4136
private static final int TRACE_PARENT_TID_END = TRACE_PARENT_TID_START + 32;
4237
private static final int TRACE_PARENT_SID_START = TRACE_PARENT_TID_END + 1;
@@ -45,6 +40,12 @@ class W3CHttpCodec {
4540
private static final int TRACE_PARENT_FLAGS_SAMPLED = 1;
4641
private static final int TRACE_PARENT_LENGTH = TRACE_PARENT_FLAGS_START + 2;
4742

43+
// Package-protected for testing
44+
static final String TRACE_PARENT_KEY = "traceparent";
45+
static final String TRACE_STATE_KEY = "tracestate";
46+
static final String OT_BAGGAGE_PREFIX = "ot-baggage-";
47+
static final String E2E_START_KEY = OT_BAGGAGE_PREFIX + DDTags.TRACE_START_TIME;
48+
4849
private W3CHttpCodec() {
4950
// This class should not be created. This also makes code coverage checks happy.
5051
}

0 commit comments

Comments
 (0)