Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OpenTelemetry End User attributes added as Span attributes #40466

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/src/main/asciidoc/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,32 @@
}
----

==== 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"}
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
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].

The attributes are only added when authentication has already happened on a best-efforts basis.
Whether the End User attributes are added as Span attributes depends on authentication and authorization configuration of your Quarkus application.
If you create custom Spans prior to the authentication, Quarkus cannot add the End User attributes to them.
Quarkus is only able to add the attributes to the Span that is current after the authentication has been finished.
Another important consideration regarding custom Spans is active CDI request context that is used to propagate Quarkus `SecurityIdentity`.
In principle, Quarkus is able to add the End User attributes when the CDI request context has been activated for you before the custom Spans are created.

[source,application.properties]
----
quarkus.otel.traces.eusp.enabled=true <1>
quarkus.http.auth.proactive=true <2>
brunobat marked this conversation as resolved.
Show resolved Hide resolved
----
<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 420 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": 420, "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.
A good way to determine whether proactive authentication should be enabled in your Quarkus application is to read the Quarkus xref:security-proactive-authentication.adoc[Proactive authentication] guide.

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();
}
}
Loading
Loading