Skip to content

Commit

Permalink
Properly populate metrics uri in presence of auth failures
Browse files Browse the repository at this point in the history
We now are able for RESTEasy Reactive to determine
the URI template even when auth failures occur

Fixes: quarkusio#24938
  • Loading branch information
geoand committed Jul 12, 2023
1 parent 2ef9dee commit ab9f67f
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityCustomizer;
import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityIntegrationRecorder;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.runtime.metrics.MetricsFactory;
import io.quarkus.vertx.http.deployment.FilterBuildItem;

public class ObservabilityProcessor {

@BuildStep
MethodScannerBuildItem integrateObservability(Capabilities capabilities,
MethodScannerBuildItem methodScanner(Capabilities capabilities,
Optional<MetricsCapabilityBuildItem> metricsCapability) {
boolean integrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) ||
(metricsCapability.isPresent()
&& metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)));
boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability);
if (!integrationNeeded) {
return null;
}
Expand All @@ -38,4 +40,26 @@ public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndp
});
}

@BuildStep
@Record(value = ExecutionTime.STATIC_INIT)
FilterBuildItem preAuthFailureFilter(Capabilities capabilities,
Optional<MetricsCapabilityBuildItem> metricsCapability,
ObservabilityIntegrationRecorder recorder,
ResteasyReactiveDeploymentBuildItem deployment) {
boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability);
if (!integrationNeeded) {
return null;
}

return FilterBuildItem.ofPreAuthenticationFailureHandler(
recorder.preAuthFailureHandler(deployment.getDeployment()));
}

private boolean integrationNeeded(Capabilities capabilities,
Optional<MetricsCapabilityBuildItem> metricsCapability) {
return capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) ||
(metricsCapability.isPresent()
&& metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER));
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.quarkus.resteasy.reactive.server.runtime.observability;

import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*;

import java.util.regex.Pattern;

import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;

import io.vertx.core.http.impl.HttpServerRequestInternal;
import io.vertx.ext.web.RoutingContext;

public class ObservabilityHandler implements ServerRestHandler {
Expand All @@ -25,8 +26,7 @@ public void setTemplatePath(String templatePath) {

@Override
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {

((HttpServerRequestInternal) (requestContext.unwrap(RoutingContext.class).request())).context()
.putLocal("UrlPathTemplate", templatePath);
setUrlPathTemplate(requestContext.unwrap(RoutingContext.class), templatePath);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.quarkus.resteasy.reactive.server.runtime.observability;

import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*;

import jakarta.ws.rs.HttpMethod;

import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.server.core.Deployment;
import org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler;
import org.jboss.resteasy.reactive.server.mapping.RequestMapper;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.security.AuthenticationException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

@Recorder
public class ObservabilityIntegrationRecorder {

private static final Logger log = Logger.getLogger(ObservabilityIntegrationRecorder.class);

/**
* Returns a handler that sets the special property URI Template path needed by various observability integrations
*/
public Handler<RoutingContext> preAuthFailureHandler(RuntimeValue<Deployment> deploymentRV) {
return new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (shouldHandle(event)) {
try {
setTemplatePath(event, deploymentRV.getValue());
} catch (Exception e) {
log.debug("Unable to set template path for observability", e);
}
}
event.next();
}

private boolean shouldHandle(RoutingContext event) {
if (!event.failed()) {
return false;
}
return event.failure() instanceof AuthenticationException
|| event.failure() instanceof ForbiddenException
|| event.failure() instanceof UnauthorizedException;
}

private void setTemplatePath(RoutingContext rc, Deployment deployment) {
// do what RestInitialHandler does
var initMappers = new RequestMapper<>(deployment.getClassMappers());
var requestMatch = initMappers.map(getPathWithoutPrefix(rc, deployment));
var remaining = requestMatch.remaining.isEmpty() ? "/" : requestMatch.remaining;

var serverRestHandlers = requestMatch.value.handlers;
if (serverRestHandlers == null || serverRestHandlers.length < 1) {
// nothing we can do
return;
}
var firstHandler = serverRestHandlers[0];
if (!(firstHandler instanceof ClassRoutingHandler)) {
// nothing we can do
return;
}

var classRoutingHandler = (ClassRoutingHandler) firstHandler;
var mappers = classRoutingHandler.getMappers();

var requestMethod = rc.request().method().name();

// do what ClassRoutingHandler does
var mapper = mappers.get(requestMethod);
if (mapper == null) {
if (requestMethod.equals(HttpMethod.HEAD) || requestMethod.equals(HttpMethod.OPTIONS)) {
mapper = mappers.get(HttpMethod.GET);
}
if (mapper == null) {
mapper = mappers.get(null);
}
if (mapper == null) {
// can't match the path
return;
}
}
var target = mapper.map(remaining);
if (target == null) {
if (requestMethod.equals(HttpMethod.HEAD)) {
mapper = mappers.get(HttpMethod.GET);
if (mapper != null) {
target = mapper.map(remaining);
}
}

if (target == null) {
// can't match the path
return;
}
}

var templatePath = requestMatch.template.template + target.template.template;
if (templatePath.endsWith("/")) {
templatePath = templatePath.substring(0, templatePath.length() - 1);
}

setUrlPathTemplate(rc, templatePath);
}

public String getPath(RoutingContext rc) {
return rc.normalizedPath();
}

public String getPathWithoutPrefix(RoutingContext rc, Deployment deployment) {
String path = getPath(rc);
if (path != null) {
String prefix = deployment.getPrefix();
if (!prefix.isEmpty()) {
if (path.startsWith(prefix)) {
return path.substring(prefix.length());
}
}
}
return path;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.resteasy.reactive.server.runtime.observability;

import io.vertx.core.http.impl.HttpServerRequestInternal;
import io.vertx.ext.web.RoutingContext;

final class ObservabilityUtil {

private ObservabilityUtil() {
}

static void setUrlPathTemplate(RoutingContext routingContext, String templatePath) {
((HttpServerRequestInternal) (routingContext.request())).context()
.putLocal("UrlPathTemplate", templatePath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public FilterBuildItem(Handler<RoutingContext> handler, int priority) {
this.isFailureHandler = false;
}

private FilterBuildItem(Handler<RoutingContext> handler, int priority, boolean checkPriority, boolean isFailureHandler) {
this.handler = handler;
if (checkPriority) {
checkPriority(priority);
}
this.priority = priority;
this.isFailureHandler = isFailureHandler;
}

/**
* Creates a new instance of {@link FilterBuildItem} with an authentication failure handler.
*
Expand All @@ -54,6 +63,15 @@ public static FilterBuildItem ofAuthenticationFailureHandler(Handler<RoutingCont
return new FilterBuildItem(authFailureHandler);
}

/**
* Creates a new instance of {@link FilterBuildItem} with an authentication failure handler.
* The handler will be added right before any handlers added by
* {@link FilterBuildItem#ofAuthenticationFailureHandler(Handler)}
*/
public static FilterBuildItem ofPreAuthenticationFailureHandler(Handler<RoutingContext> authFailureHandler) {
return new FilterBuildItem(authFailureHandler, AUTH_FAILURE_HANDLER + 1, false, true);
}

private void checkPriority(int priority) {
if (priority < 0) {
throw new IllegalArgumentException("`priority` must be positive");
Expand Down

0 comments on commit ab9f67f

Please sign in to comment.