diff --git a/bom/application/pom.xml b/bom/application/pom.xml index a32f6bc0d3bdc..ef34da4666ce4 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -354,6 +354,13 @@ pom import + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom-alpha + ${opentelemetry-alpha.version} + pom + import + diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 200e236c13cd3..f27f323dee72b 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -1,10 +1,19 @@ package io.quarkus.opentelemetry.deployment; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.BooleanSupplier; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; + +import io.opentelemetry.extension.annotations.WithSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; +import io.quarkus.arc.processor.InterceptorBindingRegistrar; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -19,6 +28,7 @@ import io.quarkus.opentelemetry.runtime.OpenTelemetryProducer; import io.quarkus.opentelemetry.runtime.OpenTelemetryRecorder; import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; +import io.quarkus.opentelemetry.runtime.tracing.cdi.WithSpanInterceptor; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; @@ -56,6 +66,36 @@ void registerOpenTelemetryContextStorage( .produce(new ReflectiveClassBuildItem(true, true, QuarkusContextStorage.class)); } + @BuildStep(onlyIf = OpenTelemetryEnabled.class) + void registerWithSpan( + BuildProducer interceptorBindingRegistrar, + BuildProducer additionalBeans) { + + interceptorBindingRegistrar.produce(new InterceptorBindingRegistrarBuildItem( + new InterceptorBindingRegistrar() { + @Override + public List getAdditionalBindings() { + return List.of(InterceptorBinding.of(WithSpan.class, Set.of("value", "kind"))); + } + })); + + additionalBeans.produce(new AdditionalBeanBuildItem(WithSpanInterceptor.class)); + } + + @BuildStep(onlyIf = OpenTelemetryEnabled.class) + void transformWithSpan( + BuildProducer annotationsTransformer) { + + annotationsTransformer.produce(new AnnotationsTransformerBuildItem(transformationContext -> { + AnnotationTarget target = transformationContext.getTarget(); + if (target.kind().equals(AnnotationTarget.Kind.CLASS)) { + if (target.asClass().name().equals(DotName.createSimple(WithSpanInterceptor.class.getName()))) { + transformationContext.transform().add(DotName.createSimple(WithSpan.class.getName())).done(); + } + } + })); + } + @BuildStep(onlyIf = OpenTelemetryEnabled.class) @Record(ExecutionTime.STATIC_INIT) void createOpenTelemetry(OpenTelemetryConfig openTelemetryConfig, diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java new file mode 100644 index 0000000000000..f137f0eeca416 --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java @@ -0,0 +1,71 @@ +package io.quarkus.opentelemetry.deployment; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.SERVER; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenTelemetryHttpCDITest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClass(HelloResource.class) + .addClass(HelloBean.class) + .addClass(TestSpanExporter.class)); + + @Inject + TestSpanExporter spanExporter; + + @Test + void telemetry() { + RestAssured.when() + .get("/hello").then() + .statusCode(200) + .body(is("hello")); + + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(2, spanItems.size()); + assertEquals("HelloBean.hello", spanItems.get(0).getName()); + assertEquals(INTERNAL, spanItems.get(0).getKind()); + assertEquals("hello", spanItems.get(1).getName()); + assertEquals(SERVER, spanItems.get(1).getKind()); + assertEquals(spanItems.get(0).getParentSpanId(), spanItems.get(1).getSpanId()); + } + + @Path("/hello") + public static class HelloResource { + @Inject + HelloBean helloBean; + + @GET + public String hello() { + return helloBean.hello(); + } + } + + @ApplicationScoped + public static class HelloBean { + @WithSpan + public String hello() { + return "hello"; + } + } +} diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java new file mode 100644 index 0000000000000..71eb337321e69 --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/WithSpanInterceptorTest.java @@ -0,0 +1,126 @@ +package io.quarkus.opentelemetry.deployment; + +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.SERVER; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.extension.annotations.SpanAttribute; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.test.QuarkusUnitTest; + +public class WithSpanInterceptorTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class).addClass(SpanBean.class).addClass(TestSpanExporter.class)); + + @Inject + SpanBean spanBean; + @Inject + TestSpanExporter spanExporter; + + @AfterEach + void tearDown() { + spanExporter.reset(); + } + + @Test + void span() { + spanBean.span(); + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(1, spanItems.size()); + assertEquals("SpanBean.span", spanItems.get(0).getName()); + assertEquals(INTERNAL, spanItems.get(0).getKind()); + } + + @Test + void spanName() { + spanBean.spanName(); + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(1, spanItems.size()); + assertEquals("name", spanItems.get(0).getName()); + assertEquals(INTERNAL, spanItems.get(0).getKind()); + } + + @Test + void spanKind() { + spanBean.spanKind(); + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(1, spanItems.size()); + assertEquals("SpanBean.spanKind", spanItems.get(0).getName()); + assertEquals(SERVER, spanItems.get(0).getKind()); + } + + @Test + void spanArgs() { + spanBean.spanArgs("argument"); + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(1, spanItems.size()); + assertEquals("SpanBean.spanArgs", spanItems.get(0).getName()); + assertEquals(INTERNAL, spanItems.get(0).getKind()); + assertEquals("argument", spanItems.get(0).getAttributes().get(AttributeKey.stringKey("arg"))); + } + + @Test + void spanChild() { + spanBean.spanChild(); + List spanItems = spanExporter.getFinishedSpanItems(); + assertEquals(2, spanItems.size()); + + assertEquals("SpanChildBean.spanChild", spanItems.get(0).getName()); + assertEquals("SpanBean.spanChild", spanItems.get(1).getName()); + assertEquals(spanItems.get(0).getParentSpanId(), spanItems.get(1).getSpanId()); + } + + @ApplicationScoped + public static class SpanBean { + @WithSpan + public void span() { + + } + + @WithSpan("name") + public void spanName() { + + } + + @WithSpan(kind = SERVER) + public void spanKind() { + + } + + @WithSpan + public void spanArgs(@SpanAttribute(value = "arg") String arg) { + + } + + @Inject + SpanChildBean spanChildBean; + + @WithSpan + public void spanChild() { + spanChildBean.spanChild(); + } + } + + @ApplicationScoped + public static class SpanChildBean { + @WithSpan + public void spanChild() { + + } + } +} diff --git a/extensions/opentelemetry/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/opentelemetry/runtime/pom.xml index bb64d869fac0a..1271ff1e96fc2 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/opentelemetry/runtime/pom.xml @@ -44,6 +44,18 @@ io.opentelemetry opentelemetry-sdk + + io.opentelemetry + opentelemetry-extension-annotations + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api-annotation-support + io.opentelemetry opentelemetry-sdk-extension-autoconfigure-spi diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java index ec1ad00138284..e3663dd349240 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java @@ -8,6 +8,7 @@ @ConfigRoot(name = "opentelemetry") public final class OpenTelemetryConfig { + public static final String INSTRUMENTATION_NAME = "io.quarkus.opentelemetry"; /** * OpenTelemetry support. diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerProducer.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerProducer.java index 4b4d0c85d9921..bfc5449f04909 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerProducer.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerProducer.java @@ -1,5 +1,7 @@ package io.quarkus.opentelemetry.runtime.tracing; +import static io.quarkus.opentelemetry.runtime.OpenTelemetryConfig.INSTRUMENTATION_NAME; + import javax.enterprise.inject.Produces; import javax.inject.Singleton; @@ -25,6 +27,6 @@ public LateBoundSampler getLateBoundSampler() { @Singleton @DefaultBean public Tracer getTracer() { - return GlobalOpenTelemetry.getTracer("io.quarkus.opentelemetry"); + return GlobalOpenTelemetry.getTracer(INSTRUMENTATION_NAME); } } diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/MethodRequest.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/MethodRequest.java new file mode 100644 index 0000000000000..6a65fd1abac2f --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/MethodRequest.java @@ -0,0 +1,21 @@ +package io.quarkus.opentelemetry.runtime.tracing.cdi; + +import java.lang.reflect.Method; + +final class MethodRequest { + private final Method method; + private final Object[] args; + + public MethodRequest(final Method method, final Object[] args) { + this.method = method; + this.args = args; + } + + public Method getMethod() { + return method; + } + + public Object[] getArgs() { + return args; + } +} diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java new file mode 100644 index 0000000000000..513a197935774 --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/cdi/WithSpanInterceptor.java @@ -0,0 +1,159 @@ +package io.quarkus.opentelemetry.runtime.tracing.cdi; + +import static io.quarkus.opentelemetry.runtime.OpenTelemetryConfig.INSTRUMENTATION_NAME; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.extension.annotations.SpanAttribute; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.instrumentation.api.annotation.support.MethodSpanAttributesExtractor; +import io.opentelemetry.instrumentation.api.annotation.support.ParameterAttributeNamesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.tracer.SpanNames; + +@SuppressWarnings("CdiInterceptorInspection") +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE) +public class WithSpanInterceptor { + private final Instrumenter instrumenter; + + public WithSpanInterceptor(final OpenTelemetry openTelemetry) { + InstrumenterBuilder builder = Instrumenter.newBuilder(new OpenTelemetryInstrumenter(openTelemetry), + INSTRUMENTATION_NAME, + new MethodRequestSpanNameExtractor()); + + MethodSpanAttributesExtractor attributesExtractor = MethodSpanAttributesExtractor.newInstance( + MethodRequest::getMethod, + new WithSpanParameterAttributeNamesExtractor(), + MethodRequest::getArgs); + + this.instrumenter = builder.addAttributesExtractor(attributesExtractor) + .newInstrumenter(methodRequest -> spanKindFromMethod(methodRequest.getMethod())); + } + + @AroundInvoke + public Object span(final InvocationContext invocationContext) throws Exception { + MethodRequest methodRequest = new MethodRequest(invocationContext.getMethod(), invocationContext.getParameters()); + + Context parentContext = Context.current(); + Context spanContext = null; + Scope scope = null; + boolean shouldStart = instrumenter.shouldStart(parentContext, methodRequest); + if (shouldStart) { + spanContext = instrumenter.start(parentContext, methodRequest); + scope = spanContext.makeCurrent(); + } + + try { + Object result = invocationContext.proceed(); + + if (shouldStart) { + instrumenter.end(spanContext, methodRequest, null, null); + } + + return result; + } finally { + if (scope != null) { + scope.close(); + } + } + } + + private static SpanKind spanKindFromMethod(Method method) { + WithSpan annotation = method.getDeclaredAnnotation(WithSpan.class); + if (annotation == null) { + return SpanKind.INTERNAL; + } + return annotation.kind(); + } + + // To ignore the version and find our Tracer, because the version is hardcoded in the Instrumenter constructor. + private static final class OpenTelemetryInstrumenter implements OpenTelemetry { + private final OpenTelemetry openTelemetry; + + public OpenTelemetryInstrumenter(final OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + @Override + public TracerProvider getTracerProvider() { + return openTelemetry.getTracerProvider(); + } + + @Override + public Tracer getTracer(final String instrumentationName) { + return openTelemetry.getTracer(instrumentationName); + } + + @Override + public Tracer getTracer( + final String instrumentationName, + final String instrumentationVersion) { + return openTelemetry.getTracer(instrumentationName); + } + + @Override + public TracerBuilder tracerBuilder(final String instrumentationName) { + return openTelemetry.tracerBuilder(instrumentationName); + } + + @Override + public ContextPropagators getPropagators() { + return openTelemetry.getPropagators(); + } + } + + private static final class MethodRequestSpanNameExtractor implements SpanNameExtractor { + @Override + public String extract(final MethodRequest methodRequest) { + WithSpan annotation = methodRequest.getMethod().getDeclaredAnnotation(WithSpan.class); + String spanName = annotation.value(); + if (spanName.isEmpty()) { + spanName = SpanNames.fromMethod(methodRequest.getMethod()); + } + return spanName; + } + } + + private static final class WithSpanParameterAttributeNamesExtractor implements ParameterAttributeNamesExtractor { + @Override + public String[] extract(final Method method, final Parameter[] parameters) { + String[] attributeNames = new String[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + attributeNames[i] = attributeName(parameters[i]); + } + return attributeNames; + } + + private static String attributeName(Parameter parameter) { + SpanAttribute spanAttribute = parameter.getDeclaredAnnotation(SpanAttribute.class); + if (spanAttribute == null) { + return null; + } + String value = spanAttribute.value(); + if (!value.isEmpty()) { + return value; + } else if (parameter.isNamePresent()) { + return parameter.getName(); + } else { + return null; + } + } + } +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/TracedService.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/TracedService.java index 4d938e5b97d2a..a901d4ef4f89f 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/TracedService.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/TracedService.java @@ -2,8 +2,11 @@ import javax.enterprise.context.ApplicationScoped; +import io.opentelemetry.extension.annotations.WithSpan; + @ApplicationScoped public class TracedService { + @WithSpan public String call() { return "Chained trace"; } diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java index 9d203cf3c13f3..96c17d9c9a904 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java @@ -328,8 +328,8 @@ void testChainedResourceTracing() { .statusCode(200) .body("message", equalTo("Chained trace")); - Awaitility.await().atMost(Duration.ofMinutes(2)).until(() -> getSpans().size() == 1); - Map spanData = getSpans().get(0); + Awaitility.await().atMost(Duration.ofMinutes(2)).until(() -> getSpans().size() == 2); + Map spanData = getSpans().get(1); Assertions.assertNotNull(spanData); Assertions.assertNotNull(spanData.get("spanId")); @@ -353,7 +353,11 @@ void testChainedResourceTracing() { Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent")); - //TODO Update this when we support internal methods being traced + // CDI call + spanData = getSpans().get(0); + Assertions.assertEquals("TracedService.call", spanData.get("name")); + Assertions.assertEquals(SpanKind.INTERNAL.toString(), spanData.get("kind")); + Assertions.assertEquals(getSpans().get(1).get("spanId"), spanData.get("parent_spanId")); } @Test