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 6859fd1662aa4..ca0f2d9570b46 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 0000000000000..3a9d988406c91 --- /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 1c53611a14f1a..2bbe78e10fbf1 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 0000000000000..c30223f8a737b --- /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 46a8d7067a58e..5cc86d32fe9db 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/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java new file mode 100644 index 0000000000000..f687364936d60 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/AbstractEndUserTest.java @@ -0,0 +1,66 @@ +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.assertTrue; + +import java.util.List; +import java.util.function.Predicate; + +import jakarta.enterprise.inject.Instance; +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.quarkus.it.opentelemetry.util.EndUserResource; +import io.quarkus.opentelemetry.runtime.exporter.otlp.EndUserSpanProcessor; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.security.TestSecurity; + +@TestHTTPEndpoint(EndUserResource.class) +@TestSecurity(user = "testUser", roles = { "admin", "user" }) +public abstract class AbstractEndUserTest { + + @Inject + InMemorySpanExporter inMemorySpanExporter; + + @Inject + Instance endUserSpanProcessor; + + protected final Predicate> injectionPredicate; + + public AbstractEndUserTest(Predicate> predicate) { + this.injectionPredicate = predicate; + } + + @BeforeEach + @AfterEach + protected void reset() { + inMemorySpanExporter.reset(); + } + + protected List getSpans() { + return inMemorySpanExporter.getFinishedSpanItems(); + } + + protected abstract void evaluateAttributes(Attributes attributes); + + @Test + protected void baseTest() { + assertTrue(this.injectionPredicate.test(endUserSpanProcessor)); + given() + .when().get() + .then() + .statusCode(200); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + SpanData spanData = getSpans().get(0); + evaluateAttributes(spanData.getAttributes()); + } + +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java new file mode 100644 index 0000000000000..2fe83999e1c9e --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserDisabledTest.java @@ -0,0 +1,23 @@ +package io.quarkus.it.opentelemetry; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.enterprise.inject.Instance; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class EndUserDisabledTest extends AbstractEndUserTest { + + public EndUserDisabledTest() { + super(Instance::isUnsatisfied); + } + + @Override + protected void evaluateAttributes(Attributes attributes) { + assertNull(attributes.get(SemanticAttributes.ENDUSER_ID)); + assertNull(attributes.get(SemanticAttributes.ENDUSER_ROLE)); + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java new file mode 100644 index 0000000000000..965b7ddd0c166 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/EndUserEnabledTest.java @@ -0,0 +1,26 @@ +package io.quarkus.it.opentelemetry; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.enterprise.inject.Instance; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.quarkus.it.opentelemetry.util.EndUserProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(EndUserProfile.class) +public class EndUserEnabledTest extends AbstractEndUserTest { + + public EndUserEnabledTest() { + super(Instance::isResolvable); + } + + @Override + protected void evaluateAttributes(Attributes attributes) { + 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/EndUserProfile.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java new file mode 100644 index 0000000000000..f0962667964b3 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserProfile.java @@ -0,0 +1,15 @@ +package io.quarkus.it.opentelemetry.util; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class EndUserProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.otel.traces.eusp.enabled", "true"); + } + +} 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 0000000000000..490012212d6d9 --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/util/EndUserResource.java @@ -0,0 +1,17 @@ +package io.quarkus.it.opentelemetry.util; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +@Path("/otel/enduser") +@RequestScoped +public class EndUserResource { + + @GET + public Response dummy() { + return Response.ok().build(); + } + +}