Skip to content

Commit eae4c58

Browse files
committed
feat(sparkjava-2.3): toolkit-generated sparkjava-2.3 instrumentation v3 [DO NOT MERGE]
Generated by APM Instrumentation Toolkit new_integration workflow. Reviewer approved (todos_fixed=1, todos_remaining=0). Cost: ~$8.28. Java tests: SparkJavaTest.java, SparkJavaForkedTest.java. R20 fix applied (Java tests, no Groovy). 1 reviewer iter vs v2's 10. Blind test on master 04a3a80.
1 parent 91f239d commit eae4c58

5 files changed

Lines changed: 1034 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// building against 2.3 and testing against 2.4 because JettyHandler is available since 2.4 only
2+
muzzle {
3+
pass {
4+
group = "com.sparkjava"
5+
module = 'spark-core'
6+
versions = "[2.3,)"
7+
assertInverse = true
8+
}
9+
}
10+
11+
apply from: "$rootDir/gradle/java.gradle"
12+
13+
addTestSuiteForDir('latestDepTest', 'test')
14+
15+
dependencies {
16+
compileOnly group: 'com.sparkjava', name: 'spark-core', version: '2.3'
17+
18+
testImplementation project(':dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.0')
19+
20+
testImplementation group: 'com.sparkjava', name: 'spark-core', version: '2.4'
21+
22+
latestDepTestImplementation group: 'com.sparkjava', name: 'spark-core', version: '+'
23+
}
24+
25+
tasks.withType(Test).configureEach {
26+
jvmArgs += ['-Ddd.trace.enabled=true']
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package datadog.trace.instrumentation.sparkjava;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan;
5+
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR;
6+
import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.DECORATE;
7+
import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.SPARK_JAVA;
8+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
9+
import static net.bytebuddy.matcher.ElementMatchers.returns;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
11+
12+
import com.google.auto.service.AutoService;
13+
import datadog.trace.agent.tooling.Instrumenter;
14+
import datadog.trace.agent.tooling.InstrumenterModule;
15+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
16+
import datadog.trace.bootstrap.instrumentation.api.Tags;
17+
import net.bytebuddy.asm.Advice;
18+
import spark.route.HttpMethod;
19+
import spark.routematch.RouteMatch;
20+
21+
@AutoService(InstrumenterModule.class)
22+
public class RoutesInstrumentation extends InstrumenterModule.Tracing
23+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
24+
25+
public RoutesInstrumentation() {
26+
super("sparkjava", "sparkjava-2.3");
27+
}
28+
29+
@Override
30+
public String[] helperClassNames() {
31+
return new String[] {packageName + ".SparkJavaDecorator"};
32+
}
33+
34+
@Override
35+
public String instrumentedType() {
36+
return "spark.route.Routes";
37+
}
38+
39+
@Override
40+
public void methodAdvice(MethodTransformer transformer) {
41+
transformer.applyAdvice(
42+
named("find")
43+
.and(takesArgument(0, named("spark.route.HttpMethod")))
44+
.and(returns(named("spark.routematch.RouteMatch")))
45+
.and(isPublic()),
46+
RoutesInstrumentation.class.getName() + "$RoutesAdvice");
47+
}
48+
49+
public static class RoutesAdvice {
50+
51+
@Advice.OnMethodExit(suppress = Throwable.class)
52+
public static void routeMatchEnricher(
53+
@Advice.Argument(0) final HttpMethod method, @Advice.Return final RouteMatch routeMatch) {
54+
55+
final AgentSpan span = activeSpan();
56+
if (span != null && routeMatch != null) {
57+
HTTP_RESOURCE_DECORATOR.withRoute(span, method.name(), routeMatch.getMatchUri());
58+
span.setSpanName(DECORATE.spanName());
59+
span.setTag(Tags.COMPONENT, SPARK_JAVA);
60+
}
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package datadog.trace.instrumentation.sparkjava;
2+
3+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
4+
import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator;
5+
6+
public class SparkJavaDecorator extends BaseDecorator {
7+
8+
public static final SparkJavaDecorator DECORATE = new SparkJavaDecorator();
9+
10+
public static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java");
11+
public static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request");
12+
13+
@Override
14+
protected String[] instrumentationNames() {
15+
return new String[] {"sparkjava"};
16+
}
17+
18+
@Override
19+
protected CharSequence spanType() {
20+
return "web";
21+
}
22+
23+
@Override
24+
protected CharSequence component() {
25+
return SPARK_JAVA;
26+
}
27+
28+
public CharSequence spanName() {
29+
return SPARK_REQUEST;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package datadog.trace.instrumentation.sparkjava;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
6+
import datadog.trace.agent.test.AbstractInstrumentationTest;
7+
import datadog.trace.agent.test.utils.PortUtils;
8+
import datadog.trace.core.DDSpan;
9+
import java.io.BufferedReader;
10+
import java.io.InputStream;
11+
import java.io.InputStreamReader;
12+
import java.net.HttpURLConnection;
13+
import java.net.URL;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.concurrent.TimeoutException;
17+
import org.junit.jupiter.api.AfterAll;
18+
import org.junit.jupiter.api.BeforeAll;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.TestInstance;
21+
import spark.Request;
22+
import spark.Response;
23+
import spark.Route;
24+
import spark.Spark;
25+
26+
/**
27+
* Forked test for the SparkJava 2.x instrumentation, running in an isolated JVM. This validates
28+
* that the {@link RoutesInstrumentation} loads and enriches Jetty server spans correctly when the
29+
* agent starts from scratch — no leftover state from other test classes.
30+
*
31+
* <p>This test focuses on the core enrichment contract: when a request matches a SparkJava route,
32+
* the server span gets operation name {@code spark.request}, component {@code spark-java}, and the
33+
* resource name / http.route reflect the parameterized route pattern.
34+
*/
35+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
36+
public class SparkJavaForkedTest extends AbstractInstrumentationTest {
37+
38+
private int actualPort;
39+
40+
@BeforeAll
41+
void setupServer() {
42+
actualPort = PortUtils.randomOpenPort();
43+
Spark.port(actualPort);
44+
45+
Spark.get(
46+
"/ping",
47+
new Route() {
48+
@Override
49+
public Object handle(Request request, Response response) {
50+
response.type("text/plain");
51+
return "pong";
52+
}
53+
});
54+
55+
Spark.get(
56+
"/items/:id",
57+
new Route() {
58+
@Override
59+
public Object handle(Request request, Response response) {
60+
response.type("application/json");
61+
return "{\"id\": \"" + request.params(":id") + "\"}";
62+
}
63+
});
64+
65+
Spark.get(
66+
"/fail",
67+
new Route() {
68+
@Override
69+
public Object handle(Request request, Response response) {
70+
throw new RuntimeException("Forked test error");
71+
}
72+
});
73+
74+
Spark.awaitInitialization();
75+
}
76+
77+
@AfterAll
78+
void tearDownServer() throws InterruptedException {
79+
Spark.stop();
80+
Thread.sleep(500);
81+
}
82+
83+
@Test
84+
void simpleRouteEnrichesServerSpan() throws InterruptedException, TimeoutException {
85+
httpGet("/ping");
86+
87+
DDSpan serverSpan = waitForServerSpan();
88+
assertServerSpan(serverSpan, "GET", "/ping", 200, false);
89+
}
90+
91+
@Test
92+
void parameterizedRoutePatternInResourceName() throws InterruptedException, TimeoutException {
93+
httpGet("/items/42");
94+
95+
DDSpan serverSpan = waitForServerSpan();
96+
assertServerSpan(serverSpan, "GET", "/items/:id", 200, false);
97+
}
98+
99+
@Test
100+
void errorRouteProducesErrorSpan() throws InterruptedException, TimeoutException {
101+
httpGet("/fail");
102+
103+
DDSpan serverSpan = waitForServerSpan();
104+
assertServerSpan(serverSpan, "GET", "/fail", 500, true);
105+
}
106+
107+
// ---------------------------------------------------------------
108+
// Helper methods
109+
// ---------------------------------------------------------------
110+
111+
/**
112+
* Validates the complete structure of a server span, covering both SparkJava enrichment and the
113+
* underlying Jetty server span baseline. This single-point-of-assertion prevents regressions when
114+
* new required tags are added.
115+
*
116+
* <p>SparkJava enrichment (set by {@link RoutesInstrumentation}):
117+
*
118+
* <ul>
119+
* <li>operation name = {@code spark.request}
120+
* <li>component = {@code spark-java}
121+
* <li>resource name = {@code HTTP_METHOD route_pattern}
122+
* <li>http.route = parameterized route pattern
123+
* </ul>
124+
*
125+
* <p>Jetty baseline (set by the Jetty server instrumentation):
126+
*
127+
* <ul>
128+
* <li>span type = {@code web}
129+
* <li>span.kind = {@code server}
130+
* <li>http.method, http.status_code, http.url
131+
* <li>error flag (from HTTP status code)
132+
* </ul>
133+
*
134+
* @param span the server span to validate
135+
* @param httpMethod the expected HTTP method (e.g., "GET", "POST")
136+
* @param route the expected route pattern (e.g., "/items/:id")
137+
* @param statusCode the expected HTTP status code
138+
* @param isError whether the span should be marked as errored
139+
*/
140+
private void assertServerSpan(
141+
DDSpan span, String httpMethod, String route, int statusCode, boolean isError) {
142+
assertNotNull(span, "Expected a server span for " + httpMethod + " " + route);
143+
144+
// SparkJava enrichment assertions
145+
assertEquals(
146+
"spark.request",
147+
span.getOperationName().toString(),
148+
"Operation name should be 'spark.request'");
149+
assertEquals(
150+
"spark-java",
151+
String.valueOf(span.getTag("component")),
152+
"component tag should be 'spark-java'");
153+
assertEquals(
154+
httpMethod + " " + route,
155+
span.getResourceName().toString(),
156+
"Resource name should be HTTP_METHOD + route_pattern");
157+
assertEquals(
158+
route,
159+
String.valueOf(span.getTag("http.route")),
160+
"http.route should contain the route pattern, not the actual path");
161+
162+
// Jetty baseline assertions
163+
assertEquals("web", span.getSpanType(), "Span type should be 'web'");
164+
assertEquals(
165+
"server", String.valueOf(span.getTag("span.kind")), "span.kind should be 'server'");
166+
assertEquals(httpMethod, String.valueOf(span.getTag("http.method")), "http.method tag");
167+
assertEquals(statusCode, span.getTag("http.status_code"), "http.status_code tag");
168+
assertNotNull(span.getTag("http.url"), "http.url tag should be set");
169+
assertEquals(isError, span.isError(), "error flag");
170+
}
171+
172+
/**
173+
* Waits for at least one trace to be written and returns the server span.
174+
*
175+
* @return the server span (never null — fails assertion if not found)
176+
* @throws InterruptedException if the thread is interrupted while waiting
177+
* @throws TimeoutException if no trace is written within the timeout
178+
*/
179+
private DDSpan waitForServerSpan() throws InterruptedException, TimeoutException {
180+
writer.waitForTraces(1);
181+
List<DDSpan> spans = new ArrayList<>();
182+
for (List<DDSpan> trace : writer) {
183+
spans.addAll(trace);
184+
}
185+
DDSpan serverSpan = null;
186+
for (DDSpan span : spans) {
187+
if ("server".equals(String.valueOf(span.getTag("span.kind")))
188+
|| "web".equals(span.getSpanType())) {
189+
serverSpan = span;
190+
break;
191+
}
192+
}
193+
assertNotNull(serverSpan, "Expected to find a server span in the collected traces");
194+
return serverSpan;
195+
}
196+
197+
/**
198+
* Makes an HTTP GET request to the SparkJava server.
199+
*
200+
* @param path the request path
201+
* @return the HTTP status code
202+
*/
203+
private int httpGet(String path) {
204+
try {
205+
URL url = new URL("http://localhost:" + actualPort + path);
206+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
207+
conn.setRequestMethod("GET");
208+
conn.setConnectTimeout(5000);
209+
conn.setReadTimeout(5000);
210+
int status = conn.getResponseCode();
211+
InputStream is =
212+
conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream();
213+
if (is != null) {
214+
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
215+
while (reader.readLine() != null) {
216+
// drain
217+
}
218+
reader.close();
219+
}
220+
conn.disconnect();
221+
return status;
222+
} catch (Exception e) {
223+
throw new RuntimeException("HTTP GET failed for path " + path, e);
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)