Add response size capturing interceptor (#1776)
* Work on capturing repsonse sizes * Interceptor complete * Add changelog * Test fix
This commit is contained in:
parent
97ac551c87
commit
fe48313100
|
@ -27,7 +27,12 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.*;
|
||||
import java.io.Writer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Value for {@link Hook#value()}
|
||||
|
@ -374,6 +379,44 @@ public enum Pointcut {
|
|||
),
|
||||
|
||||
|
||||
/**
|
||||
* <b>Server Hook:</b>
|
||||
* This method is called when a stream writer is generated that will be used to stream a non-binary response to
|
||||
* a client. Hooks may return a wrapped writer which adds additional functionality as needed.
|
||||
*
|
||||
* <p>
|
||||
* Hooks may accept the following parameters:
|
||||
* <ul>
|
||||
* <li>
|
||||
* java.io.Writer - The response writing Writer. Typically a hook will wrap this writer and layer additional functionality
|
||||
* into the wrapping writer.
|
||||
* </li>
|
||||
* <li>
|
||||
* ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the
|
||||
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
|
||||
* pulled out of the servlet request.
|
||||
* </li>
|
||||
* <li>
|
||||
* ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the
|
||||
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
|
||||
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
|
||||
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
|
||||
* </li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>
|
||||
* Hook methods should return a {@link Writer} instance that will be used to stream the response. Hook methods
|
||||
* should not throw any exception.
|
||||
* </p>
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
SERVER_OUTGOING_WRITER_CREATED(Writer.class,
|
||||
"java.io.Writer",
|
||||
"ca.uhn.fhir.rest.api.server.RequestDetails",
|
||||
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
|
||||
),
|
||||
|
||||
|
||||
/**
|
||||
* <b>Server Hook:</b>
|
||||
|
@ -1643,12 +1686,12 @@ public enum Pointcut {
|
|||
* </p>
|
||||
* <p>
|
||||
* THIS IS AN EXPERIMENTAL HOOK AND MAY BE REMOVED OR CHANGED WITHOUT WARNING.
|
||||
* </p>
|
||||
* <p>
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that this is a performance tracing hook. Use with caution in production
|
||||
* systems, since calling it may (or may not) carry a cost.
|
||||
* </p>
|
||||
* <p>
|
||||
* </p>
|
||||
* <p>
|
||||
* Hooks may accept the following parameters:
|
||||
* </p>
|
||||
* <ul>
|
||||
|
@ -1722,9 +1765,7 @@ public enum Pointcut {
|
|||
* This pointcut is used only for unit tests. Do not use in production code as it may be changed or
|
||||
* removed at any time.
|
||||
*/
|
||||
TEST_RO(BaseServerResponseException.class, String.class.getName(), String.class.getName())
|
||||
|
||||
;
|
||||
TEST_RO(BaseServerResponseException.class, String.class.getName(), String.class.getName());
|
||||
|
||||
private final List<String> myParameterTypes;
|
||||
private final Class<?> myReturnType;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: add
|
||||
issue: 1776
|
||||
title: A new server interceptor called `ResponseSizeCapturingInterceptor` has been added. This interceptor captures and makes
|
||||
available the number of characters written (pre-compression if Gzip compression is being used) to the HTTP response
|
||||
stream for FHIR responses.
|
|
@ -154,3 +154,10 @@ If you wish to override the value of `Resource.meta.source` using the value supp
|
|||
* [CaptureResourceSourceFromHeaderInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.html)
|
||||
* [CaptureResourceSourceFromHeaderInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.java)
|
||||
|
||||
# Utility: ResponseSizeCapturingInterceptor
|
||||
|
||||
The ResponseSizeCapturingInterceptor can be used to capture the number of characters written in each HTTP FHIR response.
|
||||
|
||||
* [ResponseSizeCapturingInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/ResponseSizeCapturingInterceptor.html)
|
||||
* [ResponseSizeCapturingInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseSizeCapturingInterceptor.java)
|
||||
|
||||
|
|
|
@ -462,6 +462,15 @@ public abstract class RequestDetails {
|
|||
if (myRequestContents == null) {
|
||||
myRequestContents = getByteStreamRequestContents();
|
||||
}
|
||||
return getRequestContentsIfLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request contents if they were loaded, returns <code>null</code> otherwise
|
||||
*
|
||||
* @see #loadRequestContents()
|
||||
*/
|
||||
public byte[] getRequestContentsIfLoaded() {
|
||||
return myRequestContents;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ package ca.uhn.fhir.rest.server;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.model.api.IResource;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
|
@ -42,6 +44,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.method.ElementsParameter;
|
||||
import ca.uhn.fhir.rest.server.method.SummaryEnumParameter;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.util.BinaryUtil;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
|
@ -74,7 +77,7 @@ public class RestfulServerUtils {
|
|||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class);
|
||||
|
||||
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)"));
|
||||
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<FhirVersionEnum, FhirContext>());
|
||||
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private enum NarrativeModeEnum {
|
||||
NORMAL, ONLY, SUPPRESS;
|
||||
|
@ -893,6 +896,19 @@ public class RestfulServerUtils {
|
|||
String charset = Constants.CHARSET_NAME_UTF8;
|
||||
|
||||
Writer writer = response.getResponseWriter(theStatusCode, theStatusMessage, contentType, charset, respondGzip);
|
||||
|
||||
// Interceptor call: SERVER_OUTGOING_WRITER_CREATED
|
||||
if (theServer.getInterceptorService() != null && theServer.getInterceptorService().hasHooks(Pointcut.SERVER_OUTGOING_WRITER_CREATED)) {
|
||||
HookParams params = new HookParams()
|
||||
.add(Writer.class, writer)
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
|
||||
Object newWriter = theServer.getInterceptorService().callHooksAndReturnObject(Pointcut.SERVER_OUTGOING_WRITER_CREATED, params);
|
||||
if (newWriter != null) {
|
||||
writer = (Writer) newWriter;
|
||||
}
|
||||
}
|
||||
|
||||
if (theResource == null) {
|
||||
// No response is being returned
|
||||
} else if (encodingDomainResourceAsText && theResource instanceof IResource) {
|
||||
|
|
|
@ -23,10 +23,12 @@ package ca.uhn.fhir.rest.server.interceptor;
|
|||
public class InterceptorOrders {
|
||||
|
||||
public static final int SERVE_MEDIA_RESOURCE_RAW_INTERCEPTOR = 1000;
|
||||
|
||||
public static final int RESPONSE_HIGHLIGHTER_INTERCEPTOR = 10000;
|
||||
public static final int RESPONSE_SIZE_CAPTURING_INTERCEPTOR_COMPLETED = -1;
|
||||
|
||||
/** Non instantiable */
|
||||
/**
|
||||
* Non instantiable
|
||||
*/
|
||||
private InterceptorOrders() {
|
||||
// nothing
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This interceptor captures and makes
|
||||
* available the number of characters written (pre-compression if Gzip compression is being used) to the HTTP response
|
||||
* stream for FHIR responses.
|
||||
* <p>
|
||||
* Response details are made available in the request {@link RequestDetails#getUserData() RequestDetails UserData map}
|
||||
* with {@link #RESPONSE_RESULT_KEY} as the key.
|
||||
* </p>
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class ResponseSizeCapturingInterceptor {
|
||||
|
||||
/**
|
||||
* If the response was a character stream, a character count will be placed in the
|
||||
* {@link RequestDetails#getUserData() RequestDetails UserData map} with this key, containing
|
||||
* an {@link Result} value.
|
||||
* <p>
|
||||
* The value will be placed at the start of the {@link Pointcut#SERVER_PROCESSING_COMPLETED} pointcut, so it will not
|
||||
* be available before that time.
|
||||
* </p>
|
||||
*/
|
||||
public static final String RESPONSE_RESULT_KEY = ResponseSizeCapturingInterceptor.class.getName() + "_RESPONSE_RESULT_KEY";
|
||||
|
||||
private static final String COUNTING_WRITER_KEY = ResponseSizeCapturingInterceptor.class.getName() + "_COUNTING_WRITER_KEY";
|
||||
private List<Consumer<Result>> myConsumers = new ArrayList<>();
|
||||
|
||||
@Hook(Pointcut.SERVER_OUTGOING_WRITER_CREATED)
|
||||
public Writer capture(RequestDetails theRequestDetails, Writer theWriter) {
|
||||
CountingWriter retVal = new CountingWriter(theWriter);
|
||||
theRequestDetails.getUserData().put(COUNTING_WRITER_KEY, retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
@Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED, order = InterceptorOrders.RESPONSE_SIZE_CAPTURING_INTERCEPTOR_COMPLETED)
|
||||
public void completed(RequestDetails theRequestDetails) {
|
||||
CountingWriter countingWriter = (CountingWriter) theRequestDetails.getUserData().get(COUNTING_WRITER_KEY);
|
||||
if (countingWriter != null) {
|
||||
int charCount = countingWriter.getCount();
|
||||
Result result = new Result(charCount);
|
||||
notifyConsumers(result);
|
||||
|
||||
theRequestDetails.getUserData().put(RESPONSE_RESULT_KEY, result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new consumer. All consumers will be notified each time a request is complete.
|
||||
*
|
||||
* @param theConsumer The consumer
|
||||
*/
|
||||
public void registerConsumer(@Nonnull Consumer<Result> theConsumer) {
|
||||
Validate.notNull(theConsumer);
|
||||
myConsumers.add(theConsumer);
|
||||
}
|
||||
|
||||
private void notifyConsumers(Result theResult) {
|
||||
myConsumers.forEach(t -> t.accept(theResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the results of the capture
|
||||
*/
|
||||
public static class Result {
|
||||
private final int myWrittenChars;
|
||||
|
||||
public Result(int theWrittenChars) {
|
||||
myWrittenChars = theWrittenChars;
|
||||
}
|
||||
|
||||
public int getWrittenChars() {
|
||||
return myWrittenChars;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class CountingWriter extends Writer {
|
||||
|
||||
private final Writer myWrap;
|
||||
private int myCount;
|
||||
|
||||
private CountingWriter(Writer theWrap) {
|
||||
myWrap = theWrap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] cbuf, int off, int len) throws IOException {
|
||||
myCount += len;
|
||||
myWrap.write(cbuf, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
myWrap.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
myWrap.close();
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return myCount;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderRule;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ResponseSizeCapturingInterceptorTest {
|
||||
|
||||
private static FhirContext ourCtx = FhirContext.forR4();
|
||||
@ClassRule
|
||||
public static RestfulServerRule ourServerRule = new RestfulServerRule(ourCtx);
|
||||
private ResponseSizeCapturingInterceptor myInterceptor;
|
||||
@Rule
|
||||
public HashMapResourceProviderRule<Patient> myPatientProviderRule = new HashMapResourceProviderRule<>(ourServerRule, Patient.class);
|
||||
@Mock
|
||||
private Consumer<ResponseSizeCapturingInterceptor.Result> myConsumer;
|
||||
@Captor
|
||||
private ArgumentCaptor<ResponseSizeCapturingInterceptor.Result> myResultCaptor;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myInterceptor = new ResponseSizeCapturingInterceptor();
|
||||
ourServerRule.getRestfulServer().registerInterceptor(myInterceptor);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
ourServerRule.getRestfulServer().unregisterInterceptor(myInterceptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadResource() {
|
||||
Patient resource = new Patient();
|
||||
resource.setActive(true);
|
||||
IIdType id = ourServerRule.getFhirClient().create().resource(resource).execute().getId().toUnqualifiedVersionless();
|
||||
|
||||
myInterceptor.registerConsumer(myConsumer);
|
||||
|
||||
resource = ourServerRule.getFhirClient().read().resource(Patient.class).withId(id).execute();
|
||||
assertEquals(true, resource.getActive());
|
||||
|
||||
verify(myConsumer, times(1)).accept(myResultCaptor.capture());
|
||||
assertEquals(100, myResultCaptor.getValue().getWrittenChars());
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue