Resteasy server span naming (#2900)

* Resteasy server span naming
This commit is contained in:
Lauri Tulmin 2021-05-06 15:04:56 +03:00 committed by GitHub
parent 357140c081
commit 1fba62807f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 445 additions and 17 deletions

View File

@ -56,7 +56,7 @@ public final class ServerSpanNaming {
ServerSpanNaming serverSpanNaming = context.get(CONTEXT_KEY);
if (serverSpanNaming == null) {
String name = serverSpanName.get();
if (name != null) {
if (name != null && !name.isEmpty()) {
serverSpan.updateName(name);
}
return;
@ -67,7 +67,9 @@ public final class ServerSpanNaming {
!source.useFirst && source.order == serverSpanNaming.updatedBySource.order;
if (source.order > serverSpanNaming.updatedBySource.order || onlyIfBetterName) {
String name = serverSpanName.get();
if (name != null && (!onlyIfBetterName || serverSpanNaming.isBetterName(name))) {
if (name != null
&& !name.isEmpty()
&& (!onlyIfBetterName || serverSpanNaming.isBetterName(name))) {
serverSpan.updateName(name);
serverSpanNaming.updatedBySource = source;
serverSpanNaming.nameLength = name.length();

View File

@ -10,6 +10,7 @@ import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
import io.opentelemetry.instrumentation.api.servlet.ServletContextPath;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import io.opentelemetry.instrumentation.api.tracer.ServerSpan;
@ -20,6 +21,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
@ -61,19 +63,12 @@ public class JaxRsAnnotationsTracer extends BaseTracer {
public void updateSpanNames(
Context context, Span span, Span serverSpan, Class<?> target, Method method) {
String pathBasedSpanName = getPathSpanName(target, method);
// If path based name is empty skip prepending context path so that path based name would
// remain as an empty string for which we skip updating span name. Path base span name is
// empty when method and class don't have a jax-rs path annotation, this can happen when
// creating an "abort" span, see RequestContextHelper.
if (!pathBasedSpanName.isEmpty()) {
pathBasedSpanName = JaxrsContextPath.prepend(context, pathBasedSpanName);
pathBasedSpanName = ServletContextPath.prepend(context, pathBasedSpanName);
}
Supplier<String> spanNameSupplier = getPathSpanNameSupplier(context, target, method);
if (serverSpan == null) {
updateSpanName(span, pathBasedSpanName);
updateSpanName(span, spanNameSupplier.get());
} else {
updateSpanName(serverSpan, pathBasedSpanName);
ServerSpanNaming.updateServerSpanName(
context, ServerSpanNaming.Source.CONTROLLER, spanNameSupplier);
updateSpanName(span, spanNameForMethod(target, method));
}
}
@ -91,6 +86,22 @@ public class JaxRsAnnotationsTracer extends BaseTracer {
}
}
private Supplier<String> getPathSpanNameSupplier(
Context context, Class<?> target, Method method) {
return () -> {
String pathBasedSpanName = getPathSpanName(target, method);
// If path based name is empty skip prepending context path so that path based name would
// remain as an empty string for which we skip updating span name. Path base span name is
// empty when method and class don't have a jax-rs path annotation, this can happen when
// creating an "abort" span, see RequestContextHelper.
if (!pathBasedSpanName.isEmpty()) {
pathBasedSpanName = JaxrsContextPath.prepend(context, pathBasedSpanName);
pathBasedSpanName = ServletContextPath.prepend(context, pathBasedSpanName);
}
return pathBasedSpanName;
};
}
/**
* Returns the span name given a JaxRS annotated method. Results are cached so this method can be
* called multiple times without significantly impacting performance.

View File

@ -39,4 +39,9 @@ class CxfHttpServerTest extends JaxRsHttpServerTest<Server> {
void stopServer(Server httpServer) {
httpServer.stop()
}
@Override
boolean hasFrameworkInstrumentation() {
false
}
}

View File

@ -5,4 +5,8 @@
class CxfJettyHttpServerTest extends JaxRsJettyHttpServerTest {
@Override
boolean hasFrameworkInstrumentation() {
false
}
}

View File

@ -36,4 +36,9 @@ class JerseyHttpServerTest extends JaxRsHttpServerTest<Server> {
boolean asyncCancelHasSendError() {
true
}
@Override
boolean hasFrameworkInstrumentation() {
false
}
}

View File

@ -9,4 +9,9 @@ class JerseyJettyHttpServerTest extends JaxRsJettyHttpServerTest {
boolean asyncCancelHasSendError() {
true
}
@Override
boolean hasFrameworkInstrumentation() {
false
}
}

View File

@ -25,6 +25,7 @@ dependencies {
library group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.0.0.Final'
implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent')
implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent')
testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent')
testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent')

View File

@ -22,6 +22,9 @@ public class Resteasy30InstrumentationModule extends InstrumentationModule {
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new Resteasy30RequestContextInstrumentation(),
new Resteasy30ServletContainerDispatcherInstrumentation());
new ResteasyServletContainerDispatcherInstrumentation(),
new ResteasyRootNodeTypeInstrumentation(),
new ResteasyResourceMethodInvokerInstrumentation(),
new ResteasyResourceLocatorInvokerInstrumentation());
}
}

View File

@ -25,6 +25,7 @@ dependencies {
library group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.1.0.Final'
implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-common:javaagent')
implementation project(':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent')
testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent')
testInstrumentation project(':instrumentation:servlet:servlet-javax-common:javaagent')

View File

@ -22,6 +22,9 @@ public class Resteasy31InstrumentationModule extends InstrumentationModule {
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new Resteasy31RequestContextInstrumentation(),
new Resteasy31ServletContainerDispatcherInstrumentation());
new ResteasyServletContainerDispatcherInstrumentation(),
new ResteasyRootNodeTypeInstrumentation(),
new ResteasyResourceMethodInvokerInstrumentation(),
new ResteasyResourceLocatorInvokerInstrumentation());
}
}

View File

@ -0,0 +1,6 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
dependencies {
compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0'
compileOnly group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.1.0.Final'
}

View File

@ -0,0 +1,72 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.jboss.resteasy.core.ResourceLocatorInvoker;
public class ResteasyResourceLocatorInvokerInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.jboss.resteasy.core.ResourceLocatorInvoker");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("invokeOnTargetObject")
.and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest")))
.and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse")))
.and(takesArgument(2, Object.class)),
ResteasyResourceLocatorInvokerInstrumentation.class.getName()
+ "$InvokeOnTargetObjectAdvice");
}
public static class InvokeOnTargetObjectAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This ResourceLocatorInvoker resourceInvoker,
@Advice.Local("otelScope") Scope scope) {
Context currentContext = Java8BytecodeBridge.currentContext();
String name =
InstrumentationContext.get(ResourceLocatorInvoker.class, String.class)
.get(resourceInvoker);
ResteasyTracingUtil.updateServerSpanName(currentContext, name);
// subresource locator returns a resources class that may have @Path annotations
// append current path to jax-rs context path so that it would be present in the final path
Context context =
JaxrsContextPath.init(currentContext, JaxrsContextPath.prepend(currentContext, name));
if (context != null) {
scope = context.makeCurrent();
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(@Advice.Local("otelScope") Scope scope) {
if (scope != null) {
scope.close();
}
}
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.jboss.resteasy.core.ResourceMethodInvoker;
public class ResteasyResourceMethodInvokerInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.jboss.resteasy.core.ResourceMethodInvoker");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("invokeOnTarget")
.and(takesArgument(0, named("org.jboss.resteasy.spi.HttpRequest")))
.and(takesArgument(1, named("org.jboss.resteasy.spi.HttpResponse")))
.and(takesArgument(2, Object.class)),
ResteasyResourceMethodInvokerInstrumentation.class.getName() + "$InvokeOnTargetAdvice");
}
public static class InvokeOnTargetAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.This ResourceMethodInvoker resourceInvoker) {
String name =
InstrumentationContext.get(ResourceMethodInvoker.class, String.class)
.get(resourceInvoker);
ResteasyTracingUtil.updateServerSpanName(Java8BytecodeBridge.currentContext(), name);
}
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;
import org.jboss.resteasy.core.ResourceLocatorInvoker;
import org.jboss.resteasy.core.ResourceMethodInvoker;
public class ResteasyRootNodeTypeInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.jboss.resteasy.core.registry.RootNode");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher<MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
named("addInvoker")
.and(takesArgument(0, String.class))
// package of ResourceInvoker was changed in reasteasy 4
.and(
takesArgument(
1,
named("org.jboss.resteasy.core.ResourceInvoker")
.or(named("org.jboss.resteasy.spi.ResourceInvoker")))),
ResteasyRootNodeTypeInstrumentation.class.getName() + "$AddInvokerAdvice");
return transformers;
}
public static class AddInvokerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void addInvoker(
@Advice.Argument(0) String path,
@Advice.Argument(value = 1, typing = Assigner.Typing.DYNAMIC) Object invoker) {
String normalizedPath = ResteasyTracingUtil.normalizePath(path);
if (invoker instanceof ResourceLocatorInvoker) {
ResourceLocatorInvoker resourceLocatorInvoker = (ResourceLocatorInvoker) invoker;
InstrumentationContext.get(ResourceLocatorInvoker.class, String.class)
.put(resourceLocatorInvoker, normalizedPath);
} else if (invoker instanceof ResourceMethodInvoker) {
ResourceMethodInvoker resourceMethodInvoker = (ResourceMethodInvoker) invoker;
InstrumentationContext.get(ResourceMethodInvoker.class, String.class)
.put(resourceMethodInvoker, normalizedPath);
}
}
}
}

View File

@ -20,7 +20,7 @@ import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class Resteasy31ServletContainerDispatcherInstrumentation implements TypeInstrumentation {
public class ResteasyServletContainerDispatcherInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
@ -31,7 +31,7 @@ public class Resteasy31ServletContainerDispatcherInstrumentation implements Type
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return Collections.singletonMap(
isMethod().and(named("service")),
Resteasy31ServletContainerDispatcherInstrumentation.class.getName() + "$ServiceAdvice");
ResteasyServletContainerDispatcherInstrumentation.class.getName() + "$ServiceAdvice");
}
public static class ServiceAdvice {

View File

@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.jaxrs.v2_0;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
import io.opentelemetry.instrumentation.api.servlet.ServletContextPath;
import io.opentelemetry.instrumentation.api.tracer.ServerSpan;
import io.opentelemetry.javaagent.instrumentation.api.jaxrs.JaxrsContextPath;
public final class ResteasyTracingUtil {
private ResteasyTracingUtil() {}
public static String normalizePath(String path) {
// ensure that non-empty path starts with /
if (path == null || "/".equals(path)) {
path = "";
} else if (!path.startsWith("/")) {
path = "/" + path;
}
// remove trailing /
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
public static void updateServerSpanName(Context context, String name) {
if (name == null || name.isEmpty()) {
return;
}
Span serverSpan = ServerSpan.fromContextOrNull(context);
if (serverSpan == null) {
return;
}
serverSpan.updateName(
ServletContextPath.prepend(context, JaxrsContextPath.prepend(context, name)));
// mark span name as updated from controller to avoid JaxRsAnnotationsTracer updating it
ServerSpanNaming.updateSource(context, ServerSpanNaming.Source.CONTROLLER);
}
}

View File

@ -9,6 +9,7 @@ import static io.opentelemetry.api.trace.StatusCode.ERROR
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM
import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan
import static java.util.concurrent.TimeUnit.SECONDS
import static org.junit.Assume.assumeTrue
@ -27,6 +28,94 @@ import spock.lang.Unroll
import test.JaxRsTestResource
abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> implements AgentTestTrait {
def "test super method without @Path"() {
assumeTrue(hasFrameworkInstrumentation())
given:
def url = HttpUrl.get(address.resolve("test-resource-super")).newBuilder()
.build()
def request = request(url, "GET", null).build()
def response = client.newCall(request).execute()
expect:
response.withCloseable {
assert response.code() == SUCCESS.status
assert response.body().string() == SUCCESS.body
true
}
assertTraces(1) {
trace(0, 2) {
span(0) {
hasNoParent()
kind SERVER
name getContextPath() + "/test-resource-super"
}
basicSpan(it, 1, "controller", span(0))
}
}
}
def "test interface method with @Path"() {
assumeTrue(hasFrameworkInstrumentation())
given:
def url = HttpUrl.get(address.resolve("test-resource-interface/call")).newBuilder()
.build()
def request = request(url, "GET", null).build()
def response = client.newCall(request).execute()
expect:
response.withCloseable {
assert response.code() == SUCCESS.status
assert response.body().string() == SUCCESS.body
true
}
assertTraces(1) {
trace(0, 2) {
span(0) {
hasNoParent()
kind SERVER
name getContextPath() + "/test-resource-interface/call"
}
basicSpan(it, 1, "controller", span(0))
}
}
}
def "test sub resource locator"() {
assumeTrue(hasFrameworkInstrumentation())
given:
def url = HttpUrl.get(address.resolve("test-sub-resource-locator/call/sub")).newBuilder()
.build()
def request = request(url, "GET", null).build()
def response = client.newCall(request).execute()
expect:
response.withCloseable {
assert response.code() == SUCCESS.status
assert response.body().string() == SUCCESS.body
true
}
assertTraces(1) {
trace(0, 5) {
span(0) {
hasNoParent()
kind SERVER
name getContextPath() + "/test-sub-resource-locator/call/sub"
}
basicSpan(it, 1,"JaxRsSubResourceLocatorTestResource.call", span(0))
basicSpan(it, 2, "controller", span(1))
basicSpan(it, 3, "SubResource.call", span(0))
basicSpan(it, 4, "controller", span(3))
}
}
}
@Unroll
def "should handle #desc AsyncResponse"() {
given:
@ -130,6 +219,10 @@ abstract class JaxRsHttpServerTest<S> extends HttpServerTest<S> implements Agent
true
}
boolean hasFrameworkInstrumentation() {
true
}
boolean asyncCancelHasSendError() {
false
}

View File

@ -147,6 +147,58 @@ class JaxRsTestResource {
}
}
@Path("test-resource-super")
class JaxRsSuperClassTestResource extends JaxRsSuperClassTestResourceSuper {
}
class JaxRsSuperClassTestResourceSuper {
@GET
Object call() {
HttpServerTest.controller(SUCCESS) {
SUCCESS.body
}
}
}
class JaxRsInterfaceClassTestResource extends JaxRsInterfaceClassTestResourceSuper implements JaxRsInterface {
}
@Path("test-resource-interface")
interface JaxRsInterface {
@Path("call")
@GET
Object call()
}
class JaxRsInterfaceClassTestResourceSuper {
Object call() {
HttpServerTest.controller(SUCCESS) {
SUCCESS.body
}
}
}
@Path("test-sub-resource-locator")
class JaxRsSubResourceLocatorTestResource {
@Path("call")
Object call() {
HttpServerTest.controller(SUCCESS) {
return new SubResource()
}
}
}
class SubResource {
@Path("sub")
@GET
String call() {
HttpServerTest.controller(SUCCESS) {
new Exception().printStackTrace()
SUCCESS.body
}
}
}
class JaxRsTestExceptionMapper implements ExceptionMapper<Exception> {
@Override
Response toResponse(Exception exception) {
@ -161,6 +213,9 @@ class JaxRsTestApplication extends Application {
Set<Class<?>> getClasses() {
def classes = new HashSet()
classes.add(JaxRsTestResource)
classes.add(JaxRsSuperClassTestResource)
classes.add(JaxRsInterfaceClassTestResource)
classes.add(JaxRsSubResourceLocatorTestResource)
classes.add(JaxRsTestExceptionMapper)
return classes
}

View File

@ -135,6 +135,7 @@ include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-jersey-2.0:javaagent'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-payara-testing'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.0:javaagent'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-3.1:javaagent'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-resteasy-common:javaagent'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-testing'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-tomee-testing'
include ':instrumentation:jaxrs:jaxrs-2.0:jaxrs-2.0-wildfly-testing'