Skip to content

Commit

Permalink
Support OpenTelemetry End User attributes added as Span attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed May 7, 2024
1 parent fb12a4d commit aa8da2c
Show file tree
Hide file tree
Showing 22 changed files with 1,198 additions and 12 deletions.
19 changes: 19 additions & 0 deletions docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,25 @@ public class CustomConfiguration {
}
----

==== End User attributes

When enabled, Quarkus adds OpenTelemetry End User attributes as Span attributes.

Check warning on line 403 in docs/src/main/asciidoc/opentelemetry.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/opentelemetry.adoc", "range": {"start": {"line": 403, "column": 13}}}, "severity": "INFO"}
The attributes are only added when authentication has already happened on a best-efforts basis.
Before you enable this feature, verify that Quarkus Security extension is present and configured.
More information about the Quarkus Security can be found in the xref:security-overview.adoc[Quarkus Security overview].

[source,application.properties]
----
quarkus.otel.traces.eusp.enabled=true <1>
quarkus.http.auth.proactive=true <2>
----
<1> Enable the End User Attributes feature so that the `SecurityIdentity` principal and roles are added as Span attributes.
The End User attributes are personally identifiable information, therefore make sure you want to export them before you enable this feature.

Check warning on line 414 in docs/src/main/asciidoc/opentelemetry.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/opentelemetry.adoc", "range": {"start": {"line": 414, "column": 27}}}, "severity": "WARNING"}
<2> Optionally enable proactive authentication.
The best possible results are achieved when proactive authentication is enabled because the authentication happens sooner.

IMPORTANT: This feature is not supported when a custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContexts] is used.

[[sampler]]
=== Sampler
A https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling[sampler] decides whether a trace should be discarded or forwarded, effectively managing noise and reducing overhead by limiting the number of collected traces sent to the collector.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.quarkus.opentelemetry.deployment.tracing;

import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.ALL;
import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHENTICATION_SUCCESS;
import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_FAILURE;
import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType.AUTHORIZATION_SUCCESS;

import java.net.URL;
import java.util.ArrayList;
Expand Down Expand Up @@ -49,6 +52,7 @@
import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.SecurityEvents.SecurityEventType;
import io.quarkus.opentelemetry.runtime.tracing.TracerRecorder;
import io.quarkus.opentelemetry.runtime.tracing.cdi.TracerProducer;
import io.quarkus.opentelemetry.runtime.tracing.security.EndUserSpanProcessor;
import io.quarkus.opentelemetry.runtime.tracing.security.SecurityEventUtil;
import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem;
import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem;
Expand Down Expand Up @@ -198,6 +202,28 @@ void registerSecurityEventObserver(Capabilities capabilities, OTelBuildConfig bu
}
}

@BuildStep(onlyIf = EndUserAttributesEnabled.class)
void addEndUserAttributesSpanProcessor(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
Capabilities capabilities) {
if (capabilities.isPresent(Capability.SECURITY)) {
additionalBeanProducer.produce(AdditionalBeanBuildItem.unremovableOf(EndUserSpanProcessor.class));
}
}

@BuildStep(onlyIf = EndUserAttributesEnabled.class)
void registerEndUserAttributesEventObserver(Capabilities capabilities,
ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
BuildProducer<ObserverConfiguratorBuildItem> observerProducer) {
if (capabilities.isPresent(Capability.SECURITY)) {
observerProducer
.produce(createEventObserver(observerRegistrationPhase, AUTHENTICATION_SUCCESS, "addEndUserAttributes"));
observerProducer
.produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_SUCCESS, "updateEndUserAttributes"));
observerProducer
.produce(createEventObserver(observerRegistrationPhase, AUTHORIZATION_FAILURE, "updateEndUserAttributes"));
}
}

private static ObserverConfiguratorBuildItem createEventObserver(
ObserverRegistrationPhaseBuildItem observerRegistrationPhase, SecurityEventType eventType, String utilMethodName) {
return new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext()
Expand Down Expand Up @@ -232,4 +258,18 @@ public boolean getAsBoolean() {
return enabled;
}
}

static final class EndUserAttributesEnabled implements BooleanSupplier {

private final boolean enabled;

EndUserAttributesEnabled(OTelBuildConfig config) {
this.enabled = config.traces().addEndUserAttributes();
}

@Override
public boolean getAsBoolean() {
return enabled;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import io.quarkus.runtime.annotations.ConfigGroup;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;

/**
* Tracing build time configuration
Expand Down Expand Up @@ -51,4 +52,15 @@ public interface TracesBuildConfig {
*/
@WithDefault(SamplerType.Constants.PARENT_BASED_ALWAYS_ON)
String sampler();

/**
* If OpenTelemetry End User attributes should be added as Span attributes on a best-efforts basis.
*
* @see <a href="https://opentelemetry.io/docs/specs/semconv/attributes-registry/enduser/">OpenTelemetry End User
* attributes</a>
*/
@WithName("eusp.enabled")
@WithDefault("false")
boolean addEndUserAttributes();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.quarkus.opentelemetry.runtime.tracing.security;

import jakarta.enterprise.context.Dependent;

import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;

/**
* Main purpose of this processor is to cover adding of the End User attributes to user-created Spans.
*/
@Dependent
public class EndUserSpanProcessor implements SpanProcessor {

@Override
public void onStart(Context context, ReadWriteSpan span) {
SecurityEventUtil.addEndUserAttributes(span);
}

@Override
public boolean isStartRequired() {
return true;
}

@Override
public void onEnd(ReadableSpan readableSpan) {

}

@Override
public boolean isEndRequired() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.semconv.SemanticAttributes;
import io.quarkus.arc.Arc;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.vertx.ext.web.RoutingContext;

/**
* Synthetic CDI observers for various {@link SecurityEvent} types configured during the build time use this util class
* to export the events as the OpenTelemetry Span events.
* to export the events as the OpenTelemetry Span events, or authenticated user Span attributes.
*/
public final class SecurityEventUtil {
public static final String QUARKUS_SECURITY_NAMESPACE = "quarkus.security.";
Expand All @@ -38,8 +43,58 @@ private SecurityEventUtil() {
// UTIL CLASS
}

/**
* Adds Span attributes describing authenticated user if the user is authenticated and CDI request context is active.
* This will be true for example inside JAX-RS resources when the CDI request context is already setup and user code
* creates a new Span.
*
* @param span valid and recording Span; must not be null
*/
static void addEndUserAttributes(Span span) {
if (Arc.container().requestContext().isActive()) {
var currentVertxRequest = Arc.container().instance(CurrentVertxRequest.class).get();
if (currentVertxRequest.getCurrent() != null) {
addEndUserAttribute(currentVertxRequest.getCurrent(), span);
}
}
}

/**
* Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*
* @param event {@link AuthorizationFailureEvent}
*/
public static void updateEndUserAttributes(AuthorizationFailureEvent event) {
addEndUserAttribute(event.getSecurityIdentity(), getSpan());
}

/**
* Updates authenticated user Span attributes if the {@link SecurityIdentity} got augmented during authorization.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*
* @param event {@link AuthorizationSuccessEvent}
*/
public static void updateEndUserAttributes(AuthorizationSuccessEvent event) {
addEndUserAttribute(event.getSecurityIdentity(), getSpan());
}

/**
* If there is already valid recording {@link Span}, attributes describing authenticated user are added to it.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*
* @param event {@link AuthenticationSuccessEvent}
*/
public static void addEndUserAttributes(AuthenticationSuccessEvent event) {
addEndUserAttribute(event.getSecurityIdentity(), getSpan());
}

/**
* Adds {@link SecurityEvent} as Span event.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addAllEvents(SecurityEvent event) {
Expand All @@ -57,20 +112,26 @@ public static void addAllEvents(SecurityEvent event) {
}

/**
* Adds {@link AuthenticationSuccessEvent} as Span event.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthenticationSuccessEvent event) {
addEvent(AUTHN_SUCCESS_EVENT_NAME, attributesBuilder(event).build());
}

/**
* Adds {@link AuthenticationFailureEvent} as Span event.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthenticationFailureEvent event) {
addEvent(AUTHN_FAILURE_EVENT_NAME, attributesBuilder(event, AUTHENTICATION_FAILURE_KEY).build());
}

/**
* Adds {@link AuthorizationSuccessEvent} as Span event.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthorizationSuccessEvent event) {
Expand All @@ -79,6 +140,8 @@ public static void addEvent(AuthorizationSuccessEvent event) {
}

/**
* Adds {@link AuthorizationFailureEvent} as Span event.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(AuthorizationFailureEvent event) {
Expand All @@ -88,6 +151,7 @@ public static void addEvent(AuthorizationFailureEvent event) {

/**
* Adds {@link SecurityEvent} as Span event that is not authN/authZ success/failure.
*
* WARNING: This method is called from synthetic method observer. Any renaming must be reflected in the TracerProcessor.
*/
public static void addEvent(SecurityEvent event) {
Expand All @@ -112,15 +176,14 @@ public void accept(String key, Object value) {
}

private static void addEvent(String eventName, Attributes attributes) {
Span span = Arc.container().select(Span.class).get();
if (span.getSpanContext().isValid() && span.isRecording()) {
Span span = getSpan();
if (spanIsValidAndRecording(span)) {
span.addEvent(eventName, attributes, Instant.now());
}
}

private static AttributesBuilder attributesBuilder(SecurityEvent event, String failureKey) {
Throwable failure = (Throwable) event.getEventProperties().get(failureKey);
if (failure != null) {
if (event.getEventProperties().get(failureKey) instanceof Throwable failure) {
return attributesBuilder(event).put(FAILURE_NAME, failure.getClass().getName());
}
return attributesBuilder(event);
Expand All @@ -146,4 +209,55 @@ private static Attributes withAuthorizationContext(SecurityEvent event, Attribut
}
return builder.build();
}

/**
* Adds Span attributes describing the authenticated user.
*
* @param event {@link RoutingContext}; must not be null
* @param span valid recording Span; must not be null
*/
private static void addEndUserAttribute(RoutingContext event, Span span) {
if (event.user() instanceof QuarkusHttpUser user) {
addEndUserAttribute(user.getSecurityIdentity(), span);
}
}

/**
* Adds End User attributes to the {@code span}. Only authenticated user is added to the {@link Span}.
* Anonymous identity is ignored as it does not represent authenticated user.
* Passed {@code securityIdentity} is attached to the {@link Context} so that we recognize when identity changes.
*
* @param securityIdentity SecurityIdentity
* @param span Span
*/
private static void addEndUserAttribute(SecurityIdentity securityIdentity, Span span) {
if (securityIdentity != null && !securityIdentity.isAnonymous() && spanIsValidAndRecording(span)) {
span.setAllAttributes(Attributes.of(
SemanticAttributes.ENDUSER_ID,
securityIdentity.getPrincipal().getName(),
SemanticAttributes.ENDUSER_ROLE,
getRoles(securityIdentity)));
}
}

private static String getRoles(SecurityIdentity securityIdentity) {
try {
return securityIdentity.getRoles().toString();
} catch (UnsupportedOperationException e) {
// getting roles is not supported when the identity is enhanced by custom jakarta.ws.rs.core.SecurityContext
return "";
}
}

private static Span getSpan() {
if (Arc.container().requestContext().isActive()) {
return Arc.container().select(Span.class).get();
} else {
return Span.current();
}
}

private static boolean spanIsValidAndRecording(Span span) {
return span.isRecording() && span.getSpanContext().isValid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.it.opentelemetry.reactive;

import java.util.Map;

import jakarta.inject.Singleton;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;

@Singleton
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
AuthenticationRequestContext authenticationRequestContext) {
return augment(securityIdentity, authenticationRequestContext, Map.of());
}

@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context,
Map<String, Object> attributes) {
var routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
if (routingContext != null) {
var augmentorScenario = routingContext.normalizedPath().contains("-augmentor");
var configRolesMappingScenario = routingContext.normalizedPath().contains("roles-mapping-http-perm");
if (augmentorScenario || configRolesMappingScenario) {
var builder = QuarkusSecurityIdentity.builder(identity);
if (augmentorScenario) {
builder.addRole("AUGMENTOR");
}
if (configRolesMappingScenario) {
// this role is supposed to be re-mapped by HTTP roles mapping (not path-specific)
builder.addRole("ROLES-ALLOWED-MAPPING-ROLE");
}
return Uni.createFrom().item(builder.build());
}
}
return Uni.createFrom().item(identity);
}
}
Loading

0 comments on commit aa8da2c

Please sign in to comment.