Skip to content

Commit

Permalink
Implement some support for JAX-RS 2.1 additions
Browse files Browse the repository at this point in the history
* HTTP method `PATCH`
* Async resource methods returning a `CompletionStage`
  • Loading branch information
mateuszrzeszutek committed Sep 11, 2020
1 parent 7cf42b1 commit da831c1
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
}

apply from: "$rootDir/gradle/instrumentation.gradle"

muzzle {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.opentelemetry.instrumentation.auto.jaxrs.v2_0;

import static io.opentelemetry.instrumentation.auto.jaxrs.v2_0.JaxRsAnnotationsTracer.TRACER;

import io.opentelemetry.trace.Span;
import java.util.function.BiFunction;

public class CompletionStageFinishCallback<T> implements BiFunction<T, Throwable, T> {
private final Span span;

public CompletionStageFinishCallback(Span span) {
this.span = span;
}

@Override
public T apply(T result, Throwable throwable) {
if (throwable == null) {
TRACER.end(span);
} else {
TRACER.endExceptionally(span, throwable);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,17 @@
import io.opentelemetry.trace.Span;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import javax.ws.rs.Path;
import javax.ws.rs.container.AsyncResponse;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(Instrumenter.class)
public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default {

private static final String JAX_ENDPOINT_OPERATION_NAME = "jax-rs.request";

public JaxRsAnnotationsInstrumentation() {
super("jax-rs", "jaxrs", "jax-rs-annotations");
}
Expand Down Expand Up @@ -76,6 +75,7 @@ public String[] helperClassNames() {
"io.opentelemetry.javaagent.tooling.ClassHierarchyIterable",
"io.opentelemetry.javaagent.tooling.ClassHierarchyIterable$ClassIterator",
packageName + ".JaxRsAnnotationsTracer",
packageName + ".CompletionStageFinishCallback"
};
}

Expand All @@ -92,6 +92,7 @@ public Map<? extends ElementMatcher<? super MethodDescription>, String> transfor
"javax.ws.rs.GET",
"javax.ws.rs.HEAD",
"javax.ws.rs.OPTIONS",
"javax.ws.rs.PATCH",
"javax.ws.rs.POST",
"javax.ws.rs.PUT")))),
JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice");
Expand Down Expand Up @@ -140,6 +141,7 @@ public static void nameSpan(

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Object returnValue,
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope,
Expand All @@ -155,15 +157,23 @@ public static void stopSpan(
return;
}

CompletionStage<?> asyncReturnValue =
returnValue instanceof CompletionStage ? (CompletionStage<?>) returnValue : null;

if (asyncResponse != null && !asyncResponse.isSuspended()) {
// Clear span from the asyncResponse. Logically this should never happen. Added to be safe.
InstrumentationContext.get(AsyncResponse.class, Span.class).put(asyncResponse, null);
}
if (asyncResponse == null || !asyncResponse.isSuspended()) {
if (asyncReturnValue != null) {
// span finished by CompletionStageFinishCallback
asyncReturnValue = asyncReturnValue.handle(new CompletionStageFinishCallback<>(span));
}
if ((asyncResponse == null || !asyncResponse.isSuspended()) && asyncReturnValue == null) {
TRACER.end(span);
}
scope.close();
// else span finished by AsyncResponseAdvice

scope.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ dependencies {
testImplementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
testImplementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner', version: '2.9.10'
}

test {
systemProperty 'testLatestDeps', testLatestDeps
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ dependencies {
exclude group: 'org.jboss.resteasy', module: 'resteasy-client'
}
}

test {
systemProperty 'testLatestDeps', testLatestDeps
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ dependencies {
latestDepTestLibrary group: 'org.jboss.resteasy', name: 'resteasy-core', version: '+'
}

test {
systemProperty 'testLatestDeps', testLatestDeps
}

if (findProperty('testLatestDeps')) {
configurations {
// artifact name changed from 'resteasy-jaxrs' to 'resteasy-core' starting from version 4.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.PATH
import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static io.opentelemetry.trace.Span.Kind.INTERNAL
import static io.opentelemetry.trace.Span.Kind.SERVER
import static java.util.concurrent.TimeUnit.SECONDS
import static org.junit.Assume.assumeTrue

import io.opentelemetry.auto.test.asserts.TraceAssert
import io.opentelemetry.auto.test.base.HttpServerTest
import io.opentelemetry.instrumentation.api.MoreAttributes
import io.opentelemetry.sdk.trace.data.SpanData
import io.opentelemetry.trace.attributes.SemanticAttributes
import java.util.concurrent.CompletableFuture
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.Response
import spock.lang.Timeout
import spock.lang.Unroll

abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> {
@Timeout(10)
@Unroll
def "should handle #desc AsyncResponse"() {
given:
Expand All @@ -37,8 +46,16 @@ abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> {
.build()
def request = request(url, "GET", null).build()

when:
def response = client.newCall(request).execute()
when: "async call is started"
def futureResponse = asyncCall(request)

then: "there are no traces yet"
assertTraces(0) {
}

when: "barrier is released and resource class sends response"
JaxRsTestResource.BARRIER.await(1, SECONDS)
def response = futureResponse.join()

then:
assert response.code() == statusCode
Expand All @@ -58,6 +75,45 @@ abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> {
"canceled" | "cancel" | 503 | { it instanceof String } | true | false | null
}

@Timeout(10)
@Unroll
def "should handle #desc CompletionStage (JAX-RS 2.1+ only)"() {
assumeTrue(shouldTestCompletableStageAsync())
given:
def url = HttpUrl.get(address.resolve("/async-completion-stage")).newBuilder()
.addQueryParameter("action", action)
.build()
def request = request(url, "GET", null).build()
when: "async call is started"
def futureResponse = asyncCall(request)
then: "there are no traces yet"
assertTraces(0) {
}
when: "barrier is released and resource class sends response"
JaxRsTestResource.BARRIER.await(1, SECONDS)
def response = futureResponse.join()

then:
assert response.code() == statusCode
assert bodyPredicate(response.body().string())

assertTraces(1) {
trace(0, 2) {
asyncServerSpan(it, 0, url, statusCode)
handlerSpan(it, 1, span(0), "jaxRs21Async", false, isError, errorMessage)
}
}

where:
desc | action | statusCode | bodyPredicate | isError | errorMessage
"successful" | "succeed" | 200 | { it == "success" } | false | null
"failing" | "throw" | 500 | { it == "failure" } | true | "failure"
}

@Override
boolean hasHandlerSpan() {
true
Expand All @@ -73,6 +129,10 @@ abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> {
true
}

private static boolean shouldTestCompletableStageAsync() {
Boolean.getBoolean("testLatestDeps")
}

@Override
void serverSpan(TraceAssert trace,
int index,
Expand Down Expand Up @@ -175,6 +235,22 @@ abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> {
}
}
}
}

private CompletableFuture<Response> asyncCall(Request request) {
def future = new CompletableFuture()

client.newCall(request).enqueue(new Callback() {
@Override
void onFailure(Call call, IOException e) {
future.completeExceptionally(e)
}

@Override
void onResponse(Call call, Response response) throws IOException {
future.complete(response)
}
})

return future
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.PATH
import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM
import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.REDIRECT
import static io.opentelemetry.auto.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static java.util.concurrent.TimeUnit.SECONDS

import io.opentelemetry.auto.test.base.HttpServerTest
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.CyclicBarrier
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.PathParam
Expand Down Expand Up @@ -87,10 +90,15 @@ class JaxRsTestResource {
}
}

static final BARRIER = new CyclicBarrier(2)

@Path("async")
@GET
void asyncOp(@Suspended AsyncResponse response, @QueryParam("action") String action) {
CompletableFuture.runAsync({
// await for the test method to verify that there are no spans yet
BARRIER.await(1, SECONDS)

switch (action) {
case "succeed":
response.resume("success")
Expand All @@ -107,6 +115,29 @@ class JaxRsTestResource {
}
})
}

@Path("async-completion-stage")
@GET
CompletionStage<String> jaxRs21Async(@QueryParam("action") String action) {
def result = new CompletableFuture<String>()
CompletableFuture.runAsync({
// await for the test method to verify that there are no spans yet
BARRIER.await(1, SECONDS)

switch (action) {
case "succeed":
result.complete("success")
break
case "throw":
result.completeExceptionally(new Exception("failure"))
break
default:
result.completeExceptionally(new AssertionError((Object) ("invalid action value: " + action)))
break
}
})
result
}
}

class JaxRsTestExceptionMapper implements ExceptionMapper<Exception> {
Expand Down

0 comments on commit da831c1

Please sign in to comment.