Add code attributes to spring webmvc controller spans (#12839)

This commit is contained in:
Trask Stalnaker 2024-12-07 09:11:05 -08:00 committed by GitHub
parent 80ccda1883
commit 0c32f85d99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 52 additions and 25 deletions

View File

@ -11,6 +11,8 @@ import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satis
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE;
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE;
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE;
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -27,6 +29,7 @@ import io.opentelemetry.sdk.trace.data.StatusData;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
class SpringBootBasedTest extends AbstractSpringBootBasedTest { class SpringBootBasedTest extends AbstractSpringBootBasedTest {
@ -73,6 +76,9 @@ class SpringBootBasedTest extends AbstractSpringBootBasedTest {
span.hasName(handlerSpanName) span.hasName(handlerSpanName)
.hasKind(SpanKind.INTERNAL) .hasKind(SpanKind.INTERNAL)
.hasStatus(StatusData.error()) .hasStatus(StatusData.error())
.hasAttributesSatisfyingExactly(
equalTo(CODE_NAMESPACE, ResourceHttpRequestHandler.class.getName()),
equalTo(CODE_FUNCTION, "handleRequest"))
.hasEventsSatisfyingExactly( .hasEventsSatisfyingExactly(
event -> event ->
event event

View File

@ -5,48 +5,50 @@
package io.opentelemetry.javaagent.instrumentation.spring.webmvc; package io.opentelemetry.javaagent.instrumentation.spring.webmvc;
import io.opentelemetry.instrumentation.api.incubator.semconv.util.SpanNames; import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.springframework.web.HttpRequestHandler; import org.springframework.web.HttpRequestHandler;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.mvc.Controller;
public class HandlerSpanNameExtractor implements SpanNameExtractor<Object> { public class HandlerCodeAttributesGetter implements CodeAttributesGetter<Object> {
@Nullable private static final Class<?> JAVAX_SERVLET = loadOrNull("javax.servlet.Servlet"); @Nullable private static final Class<?> JAVAX_SERVLET = loadOrNull("javax.servlet.Servlet");
@Nullable private static final Class<?> JAKARTA_SERVLET = loadOrNull("jakarta.servlet.Servlet"); @Nullable private static final Class<?> JAKARTA_SERVLET = loadOrNull("jakarta.servlet.Servlet");
@Nullable
@Override @Override
public String extract(Object handler) { public Class<?> getCodeClass(Object handler) {
Class<?> clazz;
String methodName;
if (handler instanceof HandlerMethod) { if (handler instanceof HandlerMethod) {
// name span based on the class and method name defined in the handler // name span based on the class and method name defined in the handler
Method method = ((HandlerMethod) handler).getMethod(); Method method = ((HandlerMethod) handler).getMethod();
clazz = method.getDeclaringClass(); return method.getDeclaringClass();
methodName = method.getName();
} else if (handler instanceof HttpRequestHandler) {
// org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
clazz = handler.getClass();
methodName = "handleRequest";
} else if (handler instanceof Controller) {
// org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
clazz = handler.getClass();
methodName = "handleRequest";
} else if (isServlet(handler)) {
// org.springframework.web.servlet.handler.SimpleServletHandlerAdapter
clazz = handler.getClass();
methodName = "service";
} else { } else {
// perhaps org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter return handler.getClass();
clazz = handler.getClass(); }
methodName = "<annotation>";
} }
return SpanNames.fromMethod(clazz, methodName); @Nullable
@Override
public String getMethodName(Object handler) {
if (handler instanceof HandlerMethod) {
// name span based on the class and method name defined in the handler
Method method = ((HandlerMethod) handler).getMethod();
return method.getName();
} else if (handler instanceof HttpRequestHandler) {
// org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
return "handleRequest";
} else if (handler instanceof Controller) {
// org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
return "handleRequest";
} else if (isServlet(handler)) {
// org.springframework.web.servlet.handler.SimpleServletHandlerAdapter
return "service";
} else {
// perhaps org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
return "<annotation>";
}
} }
private static boolean isServlet(Object handler) { private static boolean isServlet(Object handler) {

View File

@ -6,6 +6,8 @@
package io.opentelemetry.javaagent.instrumentation.spring.webmvc; package io.opentelemetry.javaagent.instrumentation.spring.webmvc;
import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor;
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig; import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
@ -19,8 +21,12 @@ public final class SpringWebMvcInstrumenterFactory {
} }
public Instrumenter<Object, Void> createHandlerInstrumenter() { public Instrumenter<Object, Void> createHandlerInstrumenter() {
HandlerCodeAttributesGetter codeAttributesGetter = new HandlerCodeAttributesGetter();
return Instrumenter.<Object, Void>builder( return Instrumenter.<Object, Void>builder(
GlobalOpenTelemetry.get(), instrumentationName, new HandlerSpanNameExtractor()) GlobalOpenTelemetry.get(),
instrumentationName,
CodeSpanNameExtractor.create(codeAttributesGetter))
.addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter))
.setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled()) .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled())
.buildInstrumenter(); .buildInstrumenter();
} }

View File

@ -24,7 +24,6 @@ import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.api.internal.HttpConstants;
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest;
@ -46,6 +45,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.security.web.util.OnCommittedResponseWrapper; import org.springframework.security.web.util.OnCommittedResponseWrapper;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
public abstract class AbstractSpringBootBasedTest public abstract class AbstractSpringBootBasedTest
@ -149,7 +149,9 @@ public abstract class AbstractSpringBootBasedTest
span -> span ->
span.hasName("BasicErrorController.error") span.hasName("BasicErrorController.error")
.hasKind(SpanKind.INTERNAL) .hasKind(SpanKind.INTERNAL)
.hasAttributes(Attributes.empty())); .hasAttributesSatisfyingExactly(
satisfies(CODE_NAMESPACE, v -> v.endsWith(".BasicErrorController")),
equalTo(CODE_FUNCTION, "error")));
return spanAssertions; return spanAssertions;
} }
@ -196,10 +198,16 @@ public abstract class AbstractSpringBootBasedTest
protected SpanDataAssert assertHandlerSpan( protected SpanDataAssert assertHandlerSpan(
SpanDataAssert span, String method, ServerEndpoint endpoint) { SpanDataAssert span, String method, ServerEndpoint endpoint) {
String handlerSpanName = getHandlerSpanName(endpoint); String handlerSpanName = getHandlerSpanName(endpoint);
String codeNamespace = TestController.class.getName();
if (endpoint == NOT_FOUND) { if (endpoint == NOT_FOUND) {
handlerSpanName = "ResourceHttpRequestHandler.handleRequest"; handlerSpanName = "ResourceHttpRequestHandler.handleRequest";
codeNamespace = ResourceHttpRequestHandler.class.getName();
} }
span.hasName(handlerSpanName).hasKind(SpanKind.INTERNAL); String codeFunction = handlerSpanName.substring(handlerSpanName.indexOf('.') + 1);
span.hasName(handlerSpanName)
.hasKind(SpanKind.INTERNAL)
.hasAttributesSatisfyingExactly(
equalTo(CODE_NAMESPACE, codeNamespace), equalTo(CODE_FUNCTION, codeFunction));
if (endpoint == EXCEPTION) { if (endpoint == EXCEPTION) {
span.hasStatus(StatusData.error()); span.hasStatus(StatusData.error());
span.hasEventsSatisfyingExactly( span.hasEventsSatisfyingExactly(

View File

@ -13,9 +13,12 @@ import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT; import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.api.internal.HttpConstants; import io.opentelemetry.instrumentation.api.internal.HttpConstants;
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest;
@ -97,7 +100,9 @@ public abstract class AbstractServletFilterTest
span -> span ->
span.hasName("BasicErrorController.error") span.hasName("BasicErrorController.error")
.hasKind(SpanKind.INTERNAL) .hasKind(SpanKind.INTERNAL)
.hasAttributes(Attributes.empty())); .hasAttributesSatisfyingExactly(
satisfies(CODE_NAMESPACE, v -> v.endsWith(".BasicErrorController")),
equalTo(CODE_FUNCTION, "error")));
return spanAssertions; return spanAssertions;
} }