diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java index 6859fd1662aa47..ca0f2d9570b46c 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java @@ -14,7 +14,9 @@ import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; @@ -23,6 +25,7 @@ import io.quarkus.opentelemetry.runtime.config.build.exporter.OtlpExporterBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; +import io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor; import io.quarkus.opentelemetry.runtime.exporter.otlp.LateBoundBatchSpanProcessor; import io.quarkus.opentelemetry.runtime.exporter.otlp.OtlpRecorder; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; @@ -42,6 +45,17 @@ public boolean getAsBoolean() { } } + @BuildStep + void createEndUserSpanProcessor( + BuildProducer buildProducer, + OTelBuildConfig otelBuildConfig) { + if (otelBuildConfig.traces().eusp().enabled().orElse(Boolean.FALSE)) { + buildProducer.produce( + AdditionalBeanBuildItem.unremovableOf( + EndUserSpanProcessor.class)); + } + } + @SuppressWarnings("deprecation") @BuildStep @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java new file mode 100644 index 00000000000000..abc71f26ea2606 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/EndUserSpanProcessorConfig.java @@ -0,0 +1,24 @@ +package io.quarkus.opentelemetry.runtime.config.build; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +/** + * Tracing build time configuration + */ +@ConfigGroup +public interface EndUserSpanProcessorConfig { + + /** + * Enable the {@link io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor}. + *

+ * The {@link io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor} adds + * the {@link io.opentelemetry.semconv.trace.attributes.SemanticAttributes.ENDUSER_ID} + * and {@link io.opentelemetry.semconv.trace.attributes.SemanticAttributes.ENDUSER_ROLE} to the Span. + */ + @WithDefault("false") + Optional enabled(); + +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java index 1c53611a14f1a4..2bbe78e10fbf13 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/TracesBuildConfig.java @@ -51,4 +51,9 @@ public interface TracesBuildConfig { */ @WithDefault(SamplerType.Constants.PARENT_BASED_ALWAYS_ON) String sampler(); + + /** + * EndUser SpanProcessor configurations. + */ + EndUserSpanProcessorConfig eusp(); } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java new file mode 100644 index 00000000000000..c30223f8a737b8 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/EndUserSpanProcessor.java @@ -0,0 +1,54 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.context.ManagedExecutor; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.quarkus.security.identity.SecurityIdentity; + +@ApplicationScoped +public class EndUserSpanProcessor implements SpanProcessor { + + @Inject + protected SecurityIdentity securityIdentity; + + @Inject + protected ManagedExecutor managedExecutor; + + @Override + @ActivateRequestContext + public void onStart(Context parentContext, ReadWriteSpan span) { + managedExecutor.execute( + () -> span.setAllAttributes( + securityIdentity.isAnonymous() + ? Attributes.empty() + : Attributes.of( + SemanticAttributes.ENDUSER_ID, + securityIdentity.getPrincipal().getName(), + SemanticAttributes.ENDUSER_ROLE, + securityIdentity.getRoles().toString()))); + } + + @Override + public boolean isStartRequired() { + return Boolean.TRUE; + } + + @Override + public void onEnd(ReadableSpan span) { + } + + @Override + public boolean isEndRequired() { + return Boolean.FALSE; + } + +} diff --git a/integration-tests/opentelemetry/pom.xml b/integration-tests/opentelemetry/pom.xml index 46a8d7067a58e3..5cc86d32fe9db0 100644 --- a/integration-tests/opentelemetry/pom.xml +++ b/integration-tests/opentelemetry/pom.xml @@ -40,6 +40,12 @@ io.smallrye.reactive smallrye-mutiny-vertx-web-client + + + + io.quarkus + quarkus-security + @@ -53,6 +59,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-test-security + test + io.rest-assured rest-assured @@ -117,6 +128,19 @@ + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 054108ad58ab8c..b6a617b26b25f8 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -6,5 +6,7 @@ quarkus.application.version=999-SNAPSHOT quarkus.otel.bsp.schedule.delay=100 quarkus.otel.bsp.export.timeout=5s +quarkus.otel.traces.eusp.enabled=true + pingpong/mp-rest/url=${test.url} simple/mp-rest/url=${test.url} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserTest.java new file mode 100644 index 00000000000000..e4e9971fda9474 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserTest.java @@ -0,0 +1,69 @@ +package io.quarkus.it.opentelemetry; + +import static io.restassured.RestAssured.given; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.quarkus.it.opentelemetry.util.EndUserResource; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; + +@QuarkusTest +@TestHTTPEndpoint(EndUserResource.class) +public class EndUserTest { + + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @BeforeEach + @AfterEach + void reset() { + inMemorySpanExporter.reset(); + } + + private List getSpans() { + return inMemorySpanExporter.getFinishedSpanItems(); + } + + @Test + @TestSecurity(user = "testUser", roles = { "admin", "user" }) + public void testEndUserInjections() { + given() + .when().get() + .then() + .statusCode(200); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + SpanData spanData = getSpans().get(0); + Attributes attributes = spanData.getAttributes(); + assertEquals(attributes.get(SemanticAttributes.ENDUSER_ID), "testUser"); + assertEquals(attributes.get(SemanticAttributes.ENDUSER_ROLE), "[admin, user]"); + } + + @Test + @TestSecurity(user = "testUser", roles = { "admin", "user" }) + public void testEndUserInjectionsAsync() { + given() + .when().get("/async") + .then() + .statusCode(200); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + SpanData spanData = getSpans().get(0); + Attributes attributes = spanData.getAttributes(); + assertEquals(attributes.get(SemanticAttributes.ENDUSER_ID), "testUser"); + assertEquals(attributes.get(SemanticAttributes.ENDUSER_ROLE), "[admin, user]"); + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserResource.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserResource.java new file mode 100644 index 00000000000000..eeae00a332f2e7 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserResource.java @@ -0,0 +1,37 @@ +package io.quarkus.it.opentelemetry.util; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.Assertions; + +import io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor; +import io.smallrye.mutiny.Uni; + +@Path("/otel/enduser") +@RequestScoped +public class EndUserResource { + + @Inject + EndUserSpanProcessor endUserSpanProcessor; + + @GET + public Response verifyOTelInjections() { + verifyInjections(); + return Response.ok().build(); + } + + @GET + @Path("/async") + public Uni verifyOTelInjectionsAsync() { + verifyInjections(); + return Uni.createFrom().item(Response.ok().build()); + } + + private void verifyInjections() { + Assertions.assertNotNull(endUserSpanProcessor, "EndUserSpanProcessor cannot be injected"); + } +}