diff --git a/src/main/java/dev/bannmann/restflow/AbstractRequester.java b/src/main/java/dev/bannmann/restflow/AbstractRequester.java index 9dcb1b7..d97ef7e 100644 --- a/src/main/java/dev/bannmann/restflow/AbstractRequester.java +++ b/src/main/java/dev/bannmann/restflow/AbstractRequester.java @@ -2,14 +2,17 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; +import com.google.common.collect.ImmutableList; import dev.failsafe.Failsafe; import dev.failsafe.Policy; @@ -20,6 +23,8 @@ abstract class AbstractRequester implements Requester protected final ClientConfig clientConfig; protected final ConcurrentMap diagnosticsData = new ConcurrentHashMap<>(); + private ImmutableList callerFrames; + @Override public final CompletableFuture start() { @@ -30,9 +35,24 @@ public final CompletableFuture start() diagnosticsData.putAll(values); } + int callerFrameCount = clientConfig.getCallerFrameCount(); + if (callerFrameCount > 0) + { + var stackWalker = StackWalker.getInstance(Collections.emptySet(), callerFrameCount); + callerFrames = stackWalker.walk(stream -> captureCallerFrames(callerFrameCount, stream)); + } + return send().thenApply(this::extractValue); } + private ImmutableList captureCallerFrames(int count, Stream stream) + { + return stream.dropWhile(stackFrame -> stackFrame.getClassName() + .startsWith(getClass().getPackageName())) + .limit(count) + .collect(ImmutableList.toImmutableList()); + } + private CompletableFuture> send() { List>> policies = clientConfig.getPolicies(); @@ -60,7 +80,7 @@ private T addDetailsForLowLevelExceptions(T result, Throwable throwable) if (throwable != null) { String message = String.format("Request to URL %s failed", request.uri()); - throw new RequestFailureException(request, message, throwable, diagnosticsData); + throw new RequestFailureException(request, message, throwable, diagnosticsData, callerFrames); } return result; } diff --git a/src/main/java/dev/bannmann/restflow/ClientConfig.java b/src/main/java/dev/bannmann/restflow/ClientConfig.java index 506bcf9..5cd1345 100644 --- a/src/main/java/dev/bannmann/restflow/ClientConfig.java +++ b/src/main/java/dev/bannmann/restflow/ClientConfig.java @@ -33,4 +33,14 @@ public final class ClientConfig @Singular private final List requestCustomizers; + + /** + * The number of caller stack frames to capture when starting a request. The captured frames will be included in + * any {@link RequestException} (or subclass) instance thrown by restflow. This is useful if the application is + * supposed to log intermediate exceptions, which will otherwise not carry any stack information relating to the + * original thread (due to neither being thrown there nor being set as the cause of an exception thrown in that + * thread). + */ + @Builder.Default + private final int callerFrameCount = 5; } diff --git a/src/main/java/dev/bannmann/restflow/RequestException.java b/src/main/java/dev/bannmann/restflow/RequestException.java index b0b690e..5c9f020 100644 --- a/src/main/java/dev/bannmann/restflow/RequestException.java +++ b/src/main/java/dev/bannmann/restflow/RequestException.java @@ -1,6 +1,7 @@ package dev.bannmann.restflow; import java.net.http.HttpRequest; +import java.util.List; import java.util.Map; import lombok.Getter; @@ -14,12 +15,18 @@ public abstract class RequestException extends RuntimeException { private final transient HttpRequest request; private final transient Map diagnosticsData; + private final transient List callerFrames; protected RequestException( - HttpRequest request, String message, Throwable cause, Map diagnosticsData) + HttpRequest request, + String message, + Throwable cause, + Map diagnosticsData, + List callerFrames) { super(message, cause); this.request = request; this.diagnosticsData = diagnosticsData; + this.callerFrames = callerFrames; } } diff --git a/src/main/java/dev/bannmann/restflow/RequestFailureException.java b/src/main/java/dev/bannmann/restflow/RequestFailureException.java index 96a3ddb..c1863b2 100644 --- a/src/main/java/dev/bannmann/restflow/RequestFailureException.java +++ b/src/main/java/dev/bannmann/restflow/RequestFailureException.java @@ -1,6 +1,7 @@ package dev.bannmann.restflow; import java.net.http.HttpRequest; +import java.util.List; import java.util.Map; /** @@ -11,8 +12,12 @@ public class RequestFailureException extends RequestException { public RequestFailureException( - HttpRequest request, String message, Throwable cause, Map diagnosticsData) + HttpRequest request, + String message, + Throwable cause, + Map diagnosticsData, + List callerFrames) { - super(request, message, cause, diagnosticsData); + super(request, message, cause, diagnosticsData, callerFrames); } } diff --git a/src/main/java/dev/bannmann/restflow/RequestStatusException.java b/src/main/java/dev/bannmann/restflow/RequestStatusException.java index 0eadbde..b2cfad8 100644 --- a/src/main/java/dev/bannmann/restflow/RequestStatusException.java +++ b/src/main/java/dev/bannmann/restflow/RequestStatusException.java @@ -2,6 +2,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.List; import java.util.Map; import lombok.Builder; @@ -27,9 +28,10 @@ public RequestStatusException( HttpResponse response, int status, String body, - Map diagnosticsData) + Map diagnosticsData, + List callerFrames) { - super(request, message, cause, diagnosticsData); + super(request, message, cause, diagnosticsData, callerFrames); this.response = response; this.status = status;