Browse Source

Support of capabilities for AsyncFeign (#1626)

* Support of capabilities for AsyncFeign

* Removed SyncBased interface, added todo for stageExecution, and adopted micrometer client to be async as well.

* Added internal builder flag similar to 'forceDecoding' but for client enrichment

* Added async client enrichment to Dropwizard5 capability

* Added async client enrichment to Dropwizard5 capability + code formatting

* Progress with tests; added decoder condition similar to the client one

* Fixed javadoc

* A different take on skipping enrichment and delagation

* Switcharoo

* Relaxed casting requirements and check it during execution phase

* Create class to hold common Builder fields

* Make sure capabilities are applied to all relevant fields

Co-authored-by: Marvin Froeder <velo@users.noreply.github.com>
Co-authored-by: Marvin Froeder <velo.br@gmail.com>
pull/1636/head
Eduard Dudar 2 years ago committed by GitHub
parent
commit
75a3c1cf6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      core/pom.xml
  2. 19
      core/src/main/java/feign/AsyncContextSupplier.java
  3. 399
      core/src/main/java/feign/AsyncFeign.java
  4. 263
      core/src/main/java/feign/BaseBuilder.java
  5. 35
      core/src/main/java/feign/Capability.java
  6. 204
      core/src/main/java/feign/Feign.java
  7. 27
      core/src/main/java/feign/ReflectiveAsyncFeign.java
  8. 35
      core/src/main/java/feign/Util.java
  9. 102
      core/src/test/java/feign/BaseBuilderTest.java
  10. 42
      core/src/test/java/feign/CapabilityTest.java
  11. 4
      dropwizard-metrics4/pom.xml
  12. 95
      dropwizard-metrics4/src/main/java/feign/metrics4/BaseMeteredClient.java
  13. 63
      dropwizard-metrics4/src/main/java/feign/metrics4/MeteredAsyncClient.java
  14. 54
      dropwizard-metrics4/src/main/java/feign/metrics4/MeteredClient.java
  15. 11
      dropwizard-metrics4/src/main/java/feign/metrics4/Metrics4Capability.java
  16. 56
      dropwizard-metrics4/src/test/java/feign/metrics4/Metrics4CapabilityTest.java
  17. 82
      dropwizard-metrics5/src/main/java/feign/metrics5/BaseMeteredClient.java
  18. 20
      dropwizard-metrics5/src/main/java/feign/metrics5/FeignMetricName.java
  19. 63
      dropwizard-metrics5/src/main/java/feign/metrics5/MeteredAsyncClient.java
  20. 55
      dropwizard-metrics5/src/main/java/feign/metrics5/MeteredClient.java
  21. 11
      dropwizard-metrics5/src/main/java/feign/metrics5/Metrics5Capability.java
  22. 60
      dropwizard-metrics5/src/test/java/feign/metrics5/Metrics5CapabilityTest.java
  23. 77
      micrometer/src/main/java/feign/micrometer/BaseMeteredClient.java
  24. 76
      micrometer/src/main/java/feign/micrometer/MeteredAsyncClient.java
  25. 62
      micrometer/src/main/java/feign/micrometer/MeteredClient.java
  26. 7
      micrometer/src/main/java/feign/micrometer/MicrometerCapability.java
  27. 266
      micrometer/src/test/java/feign/micrometer/AbstractMetricsTestBase.java
  28. 60
      micrometer/src/test/java/feign/micrometer/MicrometerCapabilityTest.java
  29. 40
      mock/src/main/java/feign/mock/MockClient.java

9
core/pom.xml

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2012-2021 The Feign Authors
Copyright 2012-2022 The Feign Authors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
@ -69,6 +69,13 @@ @@ -69,6 +69,13 @@
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

19
core/src/main/java/feign/AsyncContextSupplier.java

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign;
public interface AsyncContextSupplier<C> {
C newContext();
}

399
core/src/main/java/feign/AsyncFeign.java

@ -13,17 +13,17 @@ @@ -13,17 +13,17 @@
*/
package feign;
import feign.Logger.Level;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import feign.Logger.NoOpLogger;
import feign.Request.Options;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
/**
* Enhances {@link Feign} to provide support for asynchronous clients. Context (for example for
@ -39,7 +39,6 @@ import feign.codec.ErrorDecoder; @@ -39,7 +39,6 @@ import feign.codec.ErrorDecoder;
* completion is done by the {@link AsyncClient}, it is important that any subsequent processing on
* the thread be short - generally, this should involve notifying some other thread of the work to
* be done (for example, creating and submitting a task to an {@link ExecutorService}).
*
*/
@Experimental
public abstract class AsyncFeign<C> extends Feign {
@ -50,87 +49,38 @@ public abstract class AsyncFeign<C> extends Feign { @@ -50,87 +49,38 @@ public abstract class AsyncFeign<C> extends Feign {
private static class LazyInitializedExecutorService {
private static final ExecutorService instance = Executors.newCachedThreadPool(r -> {
final Thread result = new Thread(r);
result.setDaemon(true);
return result;
});
private static final ExecutorService instance =
Executors.newCachedThreadPool(
r -> {
final Thread result = new Thread(r);
result.setDaemon(true);
return result;
});
}
public static class AsyncBuilder<C> {
private final Builder builder;
private Supplier<C> defaultContextSupplier = () -> null;
private AsyncClient<C> client;
public static class AsyncBuilder<C> extends BaseBuilder<AsyncBuilder<C>> {
private Logger.Level logLevel = Logger.Level.NONE;
private Logger logger = new NoOpLogger();
private Decoder decoder = new Decoder.Default();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private boolean dismiss404;
private boolean closeAfterDecode = true;
public AsyncBuilder() {
super();
this.builder = Feign.builder();
}
private AsyncContextSupplier<C> defaultContextSupplier = () -> null;
private AsyncClient<C> client = new AsyncClient.Default<>(
new Client.Default(null, null), LazyInitializedExecutorService.instance);
@Deprecated
public AsyncBuilder<C> defaultContextSupplier(Supplier<C> supplier) {
this.defaultContextSupplier = supplier;
this.defaultContextSupplier = supplier::get;
return this;
}
public AsyncBuilder<C> client(AsyncClient<C> client) {
this.client = client;
return this;
}
/**
* @see Builder#mapAndDecode(ResponseMapper, Decoder)
*/
public AsyncBuilder<C> mapAndDecode(ResponseMapper mapper, Decoder decoder) {
this.decoder = (response, type) -> decoder.decode(mapper.map(response, type), type);
return this;
}
/**
* @see Builder#decoder(Decoder)
*/
public AsyncBuilder<C> decoder(Decoder decoder) {
this.decoder = decoder;
return this;
}
/**
* @see Builder#decode404()
* @deprecated
*/
public AsyncBuilder<C> decode404() {
this.dismiss404 = true;
public AsyncBuilder<C> defaultContextSupplier(AsyncContextSupplier<C> supplier) {
this.defaultContextSupplier = supplier;
return this;
}
/**
* @see Builder#dismiss404()
*/
public AsyncBuilder<C> dismiss404() {
this.dismiss404 = true;
return this;
}
/**
* @see Builder#errorDecoder(ErrorDecoder)
*/
public AsyncBuilder<C> errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
public AsyncBuilder<C> client(AsyncClient<C> client) {
this.client = client;
return this;
}
public AsyncBuilder<C> doNotCloseAfterDecode() {
this.closeAfterDecode = false;
return this;
}
public <T> T target(Class<T> apiType, String url) {
return target(new HardCodedTarget<>(apiType, url));
@ -148,207 +98,128 @@ public abstract class AsyncFeign<C> extends Feign { @@ -148,207 +98,128 @@ public abstract class AsyncFeign<C> extends Feign {
return build().newInstance(target, context);
}
private AsyncBuilder<C> lazyInits() {
if (client == null) {
client = new AsyncClient.Default<>(new Client.Default(null, null),
LazyInitializedExecutorService.instance);
}
return this;
}
public AsyncFeign<C> build() {
return new ReflectiveAsyncFeign<>(lazyInits());
}
// start of builder delgates
/**
* @see Builder#logLevel(Logger.Level)
*/
public AsyncBuilder<C> logLevel(Logger.Level logLevel) {
builder.logLevel(logLevel);
this.logLevel = logLevel;
return this;
}
/**
* @see Builder#contract(Contract)
*/
public AsyncBuilder<C> contract(Contract contract) {
builder.contract(contract);
return this;
}
/**
* @see Builder#logLevel(Logger.Level)
*/
public AsyncBuilder<C> logger(Logger logger) {
builder.logger(logger);
this.logger = logger;
return this;
}
/**
* @see Builder#encoder(Encoder)
*/
public AsyncBuilder<C> encoder(Encoder encoder) {
builder.encoder(encoder);
return this;
}
/**
* @see Builder#queryMapEncoder(QueryMapEncoder)
*/
public AsyncBuilder<C> queryMapEncoder(QueryMapEncoder queryMapEncoder) {
builder.queryMapEncoder(queryMapEncoder);
return this;
}
/**
* @see Builder#options(Options)
*/
public AsyncBuilder<C> options(Options options) {
builder.options(options);
return this;
}
/**
* @see Builder#requestInterceptor(RequestInterceptor)
*/
public AsyncBuilder<C> requestInterceptor(RequestInterceptor requestInterceptor) {
builder.requestInterceptor(requestInterceptor);
return this;
}
/**
* @see Builder#requestInterceptors(Iterable)
*/
public AsyncBuilder<C> requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
builder.requestInterceptors(requestInterceptors);
return this;
}
/**
* @see Builder#invocationHandlerFactory(InvocationHandlerFactory)
*/
public AsyncBuilder<C> invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
builder.invocationHandlerFactory(invocationHandlerFactory);
return this;
super.enrich();
ThreadLocal<AsyncInvocation<C>> activeContextHolder = new ThreadLocal<>();
AsyncResponseHandler responseHandler =
(AsyncResponseHandler) Capability.enrich(
new AsyncResponseHandler(
logLevel,
logger,
decoder,
errorDecoder,
dismiss404,
closeAfterDecode),
AsyncResponseHandler.class,
capabilities);
return new ReflectiveAsyncFeign<>(Feign.builder()
.logLevel(logLevel)
.client(stageExecution(activeContextHolder, client))
.decoder(stageDecode(activeContextHolder, logger, logLevel, responseHandler))
.forceDecoding() // force all handling through stageDecode
.contract(contract)
.logger(logger)
.encoder(encoder)
.queryMapEncoder(queryMapEncoder)
.options(options)
.requestInterceptors(requestInterceptors)
.invocationHandlerFactory(invocationHandlerFactory)
.build(), defaultContextSupplier, activeContextHolder);
}
private Client stageExecution(
ThreadLocal<AsyncInvocation<C>> activeContext,
AsyncClient<C> client) {
return (request, options) -> {
final Response result = Response.builder().status(200).request(request).build();
final AsyncInvocation<C> invocationContext = activeContext.get();
invocationContext.setResponseFuture(
client.execute(request, options, Optional.ofNullable(invocationContext.context())));
return result;
};
}
// from SynchronousMethodHandler
long elapsedTime(long start) {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
}
private Decoder stageDecode(
ThreadLocal<AsyncInvocation<C>> activeContext,
Logger logger,
Level logLevel,
AsyncResponseHandler responseHandler) {
return (response, type) -> {
final AsyncInvocation<C> invocationContext = activeContext.get();
final CompletableFuture<Object> result = new CompletableFuture<>();
invocationContext
.responseFuture()
.whenComplete(
(r, t) -> {
final long elapsedTime = elapsedTime(invocationContext.startNanos());
if (t != null) {
if (logLevel != Logger.Level.NONE && t instanceof IOException) {
final IOException e = (IOException) t;
logger.logIOException(invocationContext.configKey(), logLevel, e,
elapsedTime);
}
result.completeExceptionally(t);
} else {
responseHandler.handleResponse(
result,
invocationContext.configKey(),
r,
invocationContext.underlyingType(),
elapsedTime);
}
});
result.whenComplete(
(r, t) -> {
if (result.isCancelled()) {
invocationContext.responseFuture().cancel(true);
}
});
if (invocationContext.isAsyncReturnType()) {
return result;
}
try {
return result.join();
} catch (final CompletionException e) {
final Response r = invocationContext.responseFuture().join();
Throwable cause = e.getCause();
if (cause == null) {
cause = e;
}
throw new AsyncJoinException(r.status(), cause.getMessage(), r.request(), cause);
}
};
}
}
private final ThreadLocal<AsyncInvocation<C>> activeContext;
private final Feign feign;
private AsyncContextSupplier<C> defaultContextSupplier;
private final Supplier<C> defaultContextSupplier;
private final AsyncClient<C> client;
private final Logger.Level logLevel;
private final Logger logger;
private final AsyncResponseHandler responseHandler;
protected AsyncFeign(AsyncBuilder<C> asyncBuilder) {
this.activeContext = new ThreadLocal<>();
this.defaultContextSupplier = asyncBuilder.defaultContextSupplier;
this.client = asyncBuilder.client;
this.logLevel = asyncBuilder.logLevel;
this.logger = asyncBuilder.logger;
this.responseHandler = new AsyncResponseHandler(
asyncBuilder.logLevel,
asyncBuilder.logger,
asyncBuilder.decoder,
asyncBuilder.errorDecoder,
asyncBuilder.dismiss404,
asyncBuilder.closeAfterDecode);
asyncBuilder.builder.client(this::stageExecution);
asyncBuilder.builder.decoder(this::stageDecode);
asyncBuilder.builder.forceDecoding(); // force all handling through stageDecode
this.feign = asyncBuilder.builder.build();
}
private Response stageExecution(Request request, Options options) {
final Response result = Response.builder()
.status(200)
.request(request)
.build();
final AsyncInvocation<C> invocationContext = activeContext.get();
invocationContext.setResponseFuture(
client.execute(request, options, Optional.ofNullable(invocationContext.context())));
return result;
protected AsyncFeign(Feign feign, AsyncContextSupplier<C> defaultContextSupplier) {
this.feign = feign;
this.defaultContextSupplier = defaultContextSupplier;
}
// from SynchronousMethodHandler
long elapsedTime(long start) {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
}
private Object stageDecode(Response response, Type type) {
final AsyncInvocation<C> invocationContext = activeContext.get();
final CompletableFuture<Object> result = new CompletableFuture<>();
invocationContext.responseFuture().whenComplete((r, t) -> {
final long elapsedTime = elapsedTime(invocationContext.startNanos());
if (t != null) {
if (logLevel != Logger.Level.NONE && t instanceof IOException) {
final IOException e = (IOException) t;
logger.logIOException(invocationContext.configKey(), logLevel, e, elapsedTime);
}
result.completeExceptionally(t);
} else {
responseHandler.handleResponse(result, invocationContext.configKey(), r,
invocationContext.underlyingType(), elapsedTime);
}
});
result.whenComplete((r, t) -> {
if (result.isCancelled()) {
invocationContext.responseFuture().cancel(true);
}
});
if (invocationContext.isAsyncReturnType()) {
return result;
}
try {
return result.join();
} catch (final CompletionException e) {
final Response r = invocationContext.responseFuture().join();
Throwable cause = e.getCause();
if (cause == null) {
cause = e;
}
throw new AsyncJoinException(r.status(), cause.getMessage(), r.request(), cause);
}
}
protected void setInvocationContext(AsyncInvocation<C> invocationContext) {
activeContext.set(invocationContext);
}
protected void clearInvocationContext() {
activeContext.remove();
}
@Override
public <T> T newInstance(Target<T> target) {
return newInstance(target, defaultContextSupplier.get());
return newInstance(target, defaultContextSupplier.newContext());
}
public <T> T newInstance(Target<T> target, C context) {

263
core/src/main/java/feign/BaseBuilder.java

@ -0,0 +1,263 @@ @@ -0,0 +1,263 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign;
import static feign.ExceptionPropagationPolicy.NONE;
import feign.Feign.ResponseMappingDecoder;
import feign.Logger.NoOpLogger;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.querymap.FieldQueryMapEncoder;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public abstract class BaseBuilder<B extends BaseBuilder<B>> {
private final B thisB;
protected final List<RequestInterceptor> requestInterceptors =
new ArrayList<>();
protected Logger.Level logLevel = Logger.Level.NONE;
protected Contract contract = new Contract.Default();
protected Retryer retryer = new Retryer.Default();
protected Logger logger = new NoOpLogger();
protected Encoder encoder = new Encoder.Default();
protected Decoder decoder = new Decoder.Default();
protected boolean closeAfterDecode = true;
protected QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
protected ErrorDecoder errorDecoder = new ErrorDecoder.Default();
protected Options options = new Options();
protected InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
protected boolean dismiss404;
protected ExceptionPropagationPolicy propagationPolicy = NONE;
protected List<Capability> capabilities = new ArrayList<>();
public BaseBuilder() {
super();
thisB = (B) this;
}
public B logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return thisB;
}
public B contract(Contract contract) {
this.contract = contract;
return thisB;
}
public B retryer(Retryer retryer) {
this.retryer = retryer;
return thisB;
}
public B logger(Logger logger) {
this.logger = logger;
return thisB;
}
public B encoder(Encoder encoder) {
this.encoder = encoder;
return thisB;
}
public B decoder(Decoder decoder) {
this.decoder = decoder;
return thisB;
}
/**
* This flag indicates that the response should not be automatically closed upon completion of
* decoding the message. This should be set if you plan on processing the response into a
* lazy-evaluated construct, such as a {@link java.util.Iterator}.
*
* </p>
* Feign standard decoders do not have built in support for this flag. If you are using this flag,
* you MUST also use a custom Decoder, and be sure to close all resources appropriately somewhere
* in the Decoder (you can use {@link Util#ensureClosed} for convenience).
*
* @since 9.6
*
*/
public B doNotCloseAfterDecode() {
this.closeAfterDecode = false;
return thisB;
}
public B queryMapEncoder(QueryMapEncoder queryMapEncoder) {
this.queryMapEncoder = queryMapEncoder;
return thisB;
}
/**
* Allows to map the response before passing it to the decoder.
*/
public B mapAndDecode(ResponseMapper mapper, Decoder decoder) {
this.decoder = new ResponseMappingDecoder(mapper, decoder);
return thisB;
}
/**
* This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
* 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
*
* <p/>
* All first-party (ex gson) decoders return well-known empty values defined by
* {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
* decoder} or make your own.
*
* <p/>
* This flag only works with 404, as opposed to all or arbitrary status codes. This was an
* explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
* fallback policy. If your server returns a different status for not-found, correct via a custom
* {@link #client(Client) client}.
*
* @since 11.9
*/
public B dismiss404() {
this.dismiss404 = true;
return thisB;
}
/**
* This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
* 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
*
* <p/>
* All first-party (ex gson) decoders return well-known empty values defined by
* {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
* decoder} or make your own.
*
* <p/>
* This flag only works with 404, as opposed to all or arbitrary status codes. This was an
* explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
* fallback policy. If your server returns a different status for not-found, correct via a custom
* {@link #client(Client) client}.
*
* @since 8.12
* @deprecated
*/
@Deprecated
public B decode404() {
this.dismiss404 = true;
return thisB;
}
public B errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return thisB;
}
public B options(Options options) {
this.options = options;
return thisB;
}
/**
* Adds a single request interceptor to the builder.
*/
public B requestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
return thisB;
}
/**
* Sets the full set of request interceptors for the builder, overwriting any previous
* interceptors.
*/
public B requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return thisB;
}
/**
* Allows you to override how reflective dispatch works inside of Feign.
*/
public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
this.invocationHandlerFactory = invocationHandlerFactory;
return thisB;
}
public B exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
this.propagationPolicy = propagationPolicy;
return thisB;
}
public B addCapability(Capability capability) {
this.capabilities.add(capability);
return thisB;
}
protected B enrich() {
if (capabilities.isEmpty()) {
return thisB;
}
getFieldsToEnrich().forEach(field -> {
field.setAccessible(true);
try {
final Object originalValue = field.get(thisB);
final Object enriched;
if (originalValue instanceof List) {
Type ownerType = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
enriched = ((List) originalValue).stream()
.map(value -> Capability.enrich(value, (Class<?>) ownerType, capabilities))
.collect(Collectors.toList());
} else {
enriched = Capability.enrich(originalValue, field.getType(), capabilities);
}
field.set(thisB, enriched);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException("Unable to enrich field " + field, e);
} finally {
field.setAccessible(false);
}
});
return thisB;
}
List<Field> getFieldsToEnrich() {
return Util.allFields(getClass())
.stream()
// exclude anything generated by compiler
.filter(field -> !field.isSynthetic())
// and capabilities itself
.filter(field -> !Objects.equals(field.getName(), "capabilities"))
// and thisB helper field
.filter(field -> !Objects.equals(field.getName(), "thisB"))
// skip primitive types
.filter(field -> !field.getType().isPrimitive())
// skip enumerations
.filter(field -> !field.getType().isEnum())
.collect(Collectors.toList());
}
}

35
core/src/main/java/feign/Capability.java

@ -13,13 +13,14 @@ @@ -13,13 +13,14 @@
*/
package feign;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import feign.Logger.Level;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
/**
* Capabilities expose core feign artifacts to implementations so parts of core can be customized
@ -32,8 +33,9 @@ import feign.codec.Encoder; @@ -32,8 +33,9 @@ import feign.codec.Encoder;
*/
public interface Capability {
static <E> E enrich(E componentToEnrich, List<Capability> capabilities) {
static Object enrich(Object componentToEnrich,
Class<?> capabilityToEnrich,
List<Capability> capabilities) {
return capabilities.stream()
// invoke each individual capability and feed the result to the next one.
// This is equivalent to:
@ -48,18 +50,18 @@ public interface Capability { @@ -48,18 +50,18 @@ public interface Capability {
// Contract enrichedContract = cap3.enrich(cap2.enrich(cap1.enrich(contract)));
.reduce(
componentToEnrich,
(component, capability) -> invoke(component, capability),
(target, capability) -> invoke(target, capability, capabilityToEnrich),
(component, enrichedComponent) -> enrichedComponent);
}
static <E> E invoke(E target, Capability capability) {
static Object invoke(Object target, Capability capability, Class<?> capabilityToEnrich) {
return Arrays.stream(capability.getClass().getMethods())
.filter(method -> method.getName().equals("enrich"))
.filter(method -> method.getReturnType().isInstance(target))
.filter(method -> method.getReturnType().isAssignableFrom(capabilityToEnrich))
.findFirst()
.map(method -> {
try {
return (E) method.invoke(capability, target);
return method.invoke(capability, target);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
throw new RuntimeException("Unable to enrich " + target, e);
@ -72,6 +74,10 @@ public interface Capability { @@ -72,6 +74,10 @@ public interface Capability {
return client;
}
default AsyncClient<Object> enrich(AsyncClient<Object> client) {
return client;
}
default Retryer enrich(Retryer retryer) {
return retryer;
}
@ -104,6 +110,10 @@ public interface Capability { @@ -104,6 +110,10 @@ public interface Capability {
return decoder;
}
default ErrorDecoder enrich(ErrorDecoder decoder) {
return decoder;
}
default InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
return invocationHandlerFactory;
}
@ -112,4 +122,11 @@ public interface Capability { @@ -112,4 +122,11 @@ public interface Capability {
return queryMapEncoder;
}
default AsyncResponseHandler enrich(AsyncResponseHandler asyncResponseHandler) {
return asyncResponseHandler;
}
default <C> AsyncContextSupplier<C> enrich(AsyncContextSupplier<C> asyncContextSupplier) {
return asyncContextSupplier;
}
}

204
core/src/main/java/feign/Feign.java

@ -13,21 +13,12 @@ @@ -13,21 +13,12 @@
*/
package feign;
import feign.Logger.NoOpLogger;
import feign.ReflectiveFeign.ParseHandlersByName;
import feign.Request.Options;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.querymap.FieldQueryMapEncoder;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static feign.ExceptionPropagationPolicy.NONE;
/**
* Feign's purpose is to ease development against http apis that feign restfulness. <br>
@ -51,7 +42,11 @@ public abstract class Feign { @@ -51,7 +42,11 @@ public abstract class Feign {
*
* <pre>
* <ul>
* <li>{@code Route53}: would match a class {@code route53.Route53}</li>
* <li>{@code
* Route53
* }: would match a class {@code
* route53.Route53
* }</li>
* <li>{@code Route53#list()}: would match a method {@code route53.Route53#list()}</li>
* <li>{@code Route53#listAt(Marker)}: would match a method {@code
* route53.Route53#listAt(Marker)}</li>
@ -94,187 +89,16 @@ public abstract class Feign { @@ -94,187 +89,16 @@ public abstract class Feign {
*/
public abstract <T> T newInstance(Target<T> target);
public static class Builder {
public static class Builder extends BaseBuilder<Builder> {
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
private boolean dismiss404;
private boolean closeAfterDecode = true;
private ExceptionPropagationPolicy propagationPolicy = NONE;
private boolean forceDecoding = false;
private List<Capability> capabilities = new ArrayList<>();
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return this;
}
public Builder contract(Contract contract) {
this.contract = contract;
return this;
}
public Builder client(Client client) {
this.client = client;
return this;
}
public Builder retryer(Retryer retryer) {
this.retryer = retryer;
return this;
}
public Builder logger(Logger logger) {
this.logger = logger;
return this;
}
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
public Builder decoder(Decoder decoder) {
this.decoder = decoder;
return this;
}
public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) {
this.queryMapEncoder = queryMapEncoder;
return this;
}
/**
* Allows to map the response before passing it to the decoder.
*/
public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) {
this.decoder = new ResponseMappingDecoder(mapper, decoder);
return this;
}
/**
* This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
* 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
*
* <p/>
* All first-party (ex gson) decoders return well-known empty values defined by
* {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
* decoder} or make your own.
*
* <p/>
* This flag only works with 404, as opposed to all or arbitrary status codes. This was an
* explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
* fallback policy. If your server returns a different status for not-found, correct via a
* custom {@link #client(Client) client}.
*
* @since 8.12
* @deprecated
*/
public Builder decode404() {
this.dismiss404 = true;
return this;
}
/**
* This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
* 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
*
* <p/>
* All first-party (ex gson) decoders return well-known empty values defined by
* {@link Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder)
* decoder} or make your own.
*
* <p/>
* This flag only works with 404, as opposed to all or arbitrary status codes. This was an
* explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
* fallback policy. If your server returns a different status for not-found, correct via a
* custom {@link #client(Client) client}.
*
* @since 11.9
*/
public Builder dismiss404() {
this.dismiss404 = true;
return this;
}
public Builder errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return this;
}
public Builder options(Options options) {
this.options = options;
return this;
}
/**
* Adds a single request interceptor to the builder.
*/
public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
return this;
}
/**
* Sets the full set of request interceptors for the builder, overwriting any previous
* interceptors.
*/
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return this;
}
/**
* Allows you to override how reflective dispatch works inside of Feign.
*/
public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
this.invocationHandlerFactory = invocationHandlerFactory;
return this;
}
/**
* This flag indicates that the response should not be automatically closed upon completion of
* decoding the message. This should be set if you plan on processing the response into a
* lazy-evaluated construct, such as a {@link java.util.Iterator}.
*
* </p>
* Feign standard decoders do not have built in support for this flag. If you are using this
* flag, you MUST also use a custom Decoder, and be sure to close all resources appropriately
* somewhere in the Decoder (you can use {@link Util#ensureClosed} for convenience).
*
* @since 9.6
*
*/
public Builder doNotCloseAfterDecode() {
this.closeAfterDecode = false;
return this;
}
public Builder exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
this.propagationPolicy = propagationPolicy;
return this;
}
public Builder addCapability(Capability capability) {
this.capabilities.add(capability);
return this;
}
/**
* Internal - used to indicate that the decoder should be immediately called
*/
@ -284,7 +108,7 @@ public abstract class Feign { @@ -284,7 +108,7 @@ public abstract class Feign {
}
public <T> T target(Class<T> apiType, String url) {
return target(new HardCodedTarget<T>(apiType, url));
return target(new HardCodedTarget<>(apiType, url));
}
public <T> T target(Target<T> target) {
@ -292,19 +116,7 @@ public abstract class Feign { @@ -292,19 +116,7 @@ public abstract class Feign {
}
public Feign build() {
Client client = Capability.enrich(this.client, capabilities);
Retryer retryer = Capability.enrich(this.retryer, capabilities);
List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
.map(ri -> Capability.enrich(ri, capabilities))
.collect(Collectors.toList());
Logger logger = Capability.enrich(this.logger, capabilities);
Contract contract = Capability.enrich(this.contract, capabilities);
Options options = Capability.enrich(this.options, capabilities);
Encoder encoder = Capability.enrich(this.encoder, capabilities);
Decoder decoder = Capability.enrich(this.decoder, capabilities);
InvocationHandlerFactory invocationHandlerFactory =
Capability.enrich(this.invocationHandlerFactory, capabilities);
QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
super.enrich();
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,

27
core/src/main/java/feign/ReflectiveAsyncFeign.java

@ -13,7 +13,13 @@ @@ -13,7 +13,13 @@
*/
package feign;
import java.lang.reflect.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@ -55,7 +61,7 @@ public class ReflectiveAsyncFeign<C> extends AsyncFeign<C> { @@ -55,7 +61,7 @@ public class ReflectiveAsyncFeign<C> extends AsyncFeign<C> {
final MethodInfo methodInfo =
methodInfoLookup.computeIfAbsent(method, m -> new MethodInfo(type, m));
setInvocationContext(new AsyncInvocation<C>(context, methodInfo));
setInvocationContext(new AsyncInvocation<>(context, methodInfo));
try {
return method.invoke(instance, args);
} catch (final InvocationTargetException e) {
@ -90,10 +96,23 @@ public class ReflectiveAsyncFeign<C> extends AsyncFeign<C> { @@ -90,10 +96,23 @@ public class ReflectiveAsyncFeign<C> extends AsyncFeign<C> {
}
}
public ReflectiveAsyncFeign(AsyncBuilder<C> asyncBuilder) {
super(asyncBuilder);
private ThreadLocal<AsyncInvocation<C>> activeContextHolder;
public ReflectiveAsyncFeign(Feign feign, AsyncContextSupplier<C> defaultContextSupplier,
ThreadLocal<AsyncInvocation<C>> contextHolder) {
super(feign, defaultContextSupplier);
this.activeContextHolder = contextHolder;
}
protected void setInvocationContext(AsyncInvocation<C> invocationContext) {
activeContextHolder.set(invocationContext);
}
protected void clearInvocationContext() {
activeContextHolder.remove();
}
private String getFullMethodName(Class<?> type, Type retType, Method m) {
return retType.getTypeName() + " " + type.toGenericString() + "." + m.getName();
}

35
core/src/main/java/feign/Util.java

@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
*/
package feign;
import static java.lang.String.format;
import static java.util.Objects.nonNull;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
@ -20,6 +22,7 @@ import java.io.InputStream; @@ -20,6 +22,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
@ -30,12 +33,23 @@ import java.nio.ByteBuffer; @@ -30,12 +33,23 @@ import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static java.lang.String.format;
import static java.util.Objects.nonNull;
/**
* Utilities, typically copied in from guava, so as to avoid dependency conflicts.
@ -170,7 +184,7 @@ public class Util { @@ -170,7 +184,7 @@ public class Util {
if (iterable instanceof Collection) {
collection = (Collection<T>) iterable;
} else {
collection = new ArrayList<T>();
collection = new ArrayList<>();
for (T element : iterable) {
collection.add(element);
}
@ -251,7 +265,7 @@ public class Util { @@ -251,7 +265,7 @@ public class Util {
private static final Map<Class<?>, Supplier<Object>> EMPTIES;
static {
final Map<Class<?>, Supplier<Object>> empties = new LinkedHashMap<Class<?>, Supplier<Object>>();
final Map<Class<?>, Supplier<Object>> empties = new LinkedHashMap<>();
empties.put(boolean.class, () -> false);
empties.put(Boolean.class, () -> false);
empties.put(byte[].class, () -> new byte[0]);
@ -392,4 +406,15 @@ public class Util { @@ -392,4 +406,15 @@ public class Util {
return null;
}
public static List<Field> allFields(Class<?> clazz) {
if (Objects.equals(clazz, Object.class)) {
return Collections.emptyList();
}
List<Field> fields = new ArrayList<>();
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
fields.addAll(allFields(clazz.getSuperclass()));
return fields;
}
}

102
core/src/test/java/feign/BaseBuilderTest.java

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.RETURNS_MOCKS;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.Test;
import org.mockito.Mockito;
public class BaseBuilderTest {
@Test
public void avoidDuplicationsBetweenBuilders() {
List<String> builderA = getExclusiveMethods(Feign.Builder.class);
List<String> builderB = getExclusiveMethods(AsyncFeign.AsyncBuilder.class);
assertThat(builderA).noneMatch(m -> builderB.contains(m));
assertThat(builderB).noneMatch(m -> builderA.contains(m));
}
@Test
public void avoidDuplicationsBetweenAsyncBuilderAndBaseBuilder() {
List<String> builderA = getExclusiveMethods(BaseBuilder.class);
List<String> builderB = getExclusiveMethods(AsyncFeign.AsyncBuilder.class);
assertThat(builderA).noneMatch(m -> builderB.contains(m));
assertThat(builderB).noneMatch(m -> builderA.contains(m));
}
@Test
public void avoidDuplicationsBetweenBuilderAndBaseBuilder() {
List<String> builderA = getExclusiveMethods(Feign.Builder.class);
List<String> builderB = getExclusiveMethods(BaseBuilder.class);
assertThat(builderA).noneMatch(m -> builderB.contains(m));
assertThat(builderB).noneMatch(m -> builderA.contains(m));
}
private List<String> getExclusiveMethods(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> !Objects.equals(m.getName(), "target"))
.filter(m -> !Objects.equals(m.getName(), "build"))
.filter(m -> !m.isSynthetic())
.map(m -> m.getName() + "#" + Arrays.toString(m.getParameterTypes()))
.collect(Collectors.toList());
}
@Test
public void checkEnrichTouchesAllAsyncBuilderFields()
throws IllegalArgumentException, IllegalAccessException {
test(AsyncFeign.asyncBuilder().requestInterceptor(template -> {
}), 12);
}
private void test(BaseBuilder<?> builder, int expectedFieldsCount)
throws IllegalArgumentException, IllegalAccessException {
Capability mockingCapability = Mockito.mock(Capability.class, RETURNS_MOCKS);
BaseBuilder<?> enriched = builder.addCapability(mockingCapability).enrich();
List<Field> fields = enriched.getFieldsToEnrich();
assertThat(fields).hasSize(expectedFieldsCount);
for (Field field : fields) {
field.setAccessible(true);
Object mockedValue = field.get(enriched);
if (mockedValue instanceof List) {
mockedValue = ((List<Object>) mockedValue).get(0);
}
assertTrue("Field was not enriched " + field, Mockito.mockingDetails(mockedValue)
.isMock());
}
}
@Test
public void checkEnrichTouchesAllBuilderFields()
throws IllegalArgumentException, IllegalAccessException {
test(Feign.builder().requestInterceptor(template -> {
}), 11);
}
}

42
core/src/test/java/feign/CapabilityTest.java

@ -13,19 +13,24 @@ @@ -13,19 +13,24 @@
*/
package feign;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import feign.Request.Options;
import java.io.IOException;
import java.util.Arrays;
import feign.Request.Options;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
public class CapabilityTest {
private class AClient implements Client {
public AClient(Client client) {}
public AClient(Client client) {
if (!(client instanceof Client.Default)) {
throw new RuntimeException(
"Test is chaining invokations, expected Default Client instace here");
}
}
@Override
public Response execute(Request request, Options options) throws IOException {
@ -37,7 +42,7 @@ public class CapabilityTest { @@ -37,7 +42,7 @@ public class CapabilityTest {
public BClient(Client client) {
if (!(client instanceof AClient)) {
throw new RuntimeException("Test is chaing invokations, expected AClient instace here");
throw new RuntimeException("Test is chaining invokations, expected AClient instace here");
}
}
@ -50,18 +55,19 @@ public class CapabilityTest { @@ -50,18 +55,19 @@ public class CapabilityTest {
@Test
public void enrichClient() {
Client enriched = Capability.enrich(new Client.Default(null, null), Arrays.asList(
new Capability() {
@Override
public Client enrich(Client client) {
return new AClient(client);
}
}, new Capability() {
@Override
public Client enrich(Client client) {
return new BClient(client);
}
}));
Client enriched =
(Client) Capability.enrich(new Client.Default(null, null), Client.class, Arrays.asList(
new Capability() {
@Override
public Client enrich(Client client) {
return new AClient(client);
}
}, new Capability() {
@Override
public Client enrich(Client client) {
return new BClient(client);
}
}));
assertThat(enriched, CoreMatchers.instanceOf(BClient.class));
}

4
dropwizard-metrics4/pom.xml

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2012-2021 The Feign Authors
Copyright 2012-2022 The Feign Authors
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
@ -42,7 +42,7 @@ @@ -42,7 +42,7 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>4.1.9</version>
<version>4.2.9</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>

95
dropwizard-metrics4/src/main/java/feign/metrics4/BaseMeteredClient.java

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.metrics4;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import feign.FeignException;
import feign.RequestTemplate;
import feign.Response;
class BaseMeteredClient {
protected final MetricRegistry metricRegistry;
protected final FeignMetricName metricName;
protected final MetricSuppliers metricSuppliers;
public BaseMeteredClient(
MetricRegistry metricRegistry, FeignMetricName metricName, MetricSuppliers metricSuppliers) {
this.metricRegistry = metricRegistry;
this.metricName = metricName;
this.metricSuppliers = metricSuppliers;
}
protected Timer.Context createTimer(RequestTemplate template) {
return metricRegistry
.timer(
MetricRegistry.name(
metricName.metricName(template.methodMetadata(), template.feignTarget()),
"uri",
template.methodMetadata().template().path()),
metricSuppliers.timers())
.time();
}
protected void recordSuccess(RequestTemplate template, Response response) {
metricRegistry
.meter(
MetricRegistry.name(
httpResponseCode(template),
"status_group",
response.status() / 100 + "xx",
"http_status",
String.valueOf(response.status()),
"uri",
template.methodMetadata().template().path()),
metricSuppliers.meters())
.mark();
}
protected void recordFailure(RequestTemplate template, FeignException e) {
metricRegistry
.meter(
MetricRegistry.name(
httpResponseCode(template),
"exception_name",
e.getClass().getSimpleName(),
"status_group",
e.status() / 100 + "xx",
"http_status",
String.valueOf(e.status()),
"uri",
template.methodMetadata().template().path()),
metricSuppliers.meters())
.mark();
}
protected void recordFailure(RequestTemplate template, Exception e) {
metricRegistry
.meter(
MetricRegistry.name(
httpResponseCode(template),
"exception_name",
e.getClass().getSimpleName(),
"uri",
template.methodMetadata().template().path()),
metricSuppliers.meters())
.mark();
}
private String httpResponseCode(RequestTemplate template) {
return metricName.metricName(
template.methodMetadata(), template.feignTarget(), "http_response_code");
}
}

63
dropwizard-metrics4/src/main/java/feign/metrics4/MeteredAsyncClient.java

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.metrics4;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import feign.AsyncClient;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/** Warp feign {@link AsyncClient} with metrics. */
public class MeteredAsyncClient extends BaseMeteredClient implements AsyncClient<Object> {
private final AsyncClient<Object> asyncClient;
public MeteredAsyncClient(
AsyncClient<Object> asyncClient,
MetricRegistry metricRegistry,
MetricSuppliers metricSuppliers) {
super(metricRegistry, new FeignMetricName(AsyncClient.class), metricSuppliers);
this.asyncClient = asyncClient;
}
@Override
public CompletableFuture<Response> execute(
Request request,
Options options,
Optional<Object> requestContext) {
final RequestTemplate template = request.requestTemplate();
final Timer.Context timer = createTimer(template);
return asyncClient
.execute(request, options, requestContext)
.whenComplete(
(response, th) -> {
if (th == null) {
recordSuccess(template, response);
} else if (th instanceof FeignException) {
FeignException e = (FeignException) th;
recordFailure(template, e);
} else if (th instanceof Exception) {
Exception e = (Exception) th;
recordFailure(template, e);
}
})
.whenComplete((response, th) -> timer.close());
}
}

54
dropwizard-metrics4/src/main/java/feign/metrics4/MeteredClient.java

@ -13,65 +13,43 @@ @@ -13,65 +13,43 @@
*/
package feign.metrics4;
import java.io.IOException;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import feign.*;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response;
import java.io.IOException;
/**
* Warp feign {@link Client} with metrics.
*/
public class MeteredClient implements Client {
/** Warp feign {@link Client} with metrics. */
public class MeteredClient extends BaseMeteredClient implements Client {
private final Client client;
private final MetricRegistry metricRegistry;
private final FeignMetricName metricName;
private final MetricSuppliers metricSuppliers;
public MeteredClient(Client client, MetricRegistry metricRegistry,
MetricSuppliers metricSuppliers) {
public MeteredClient(
Client client, MetricRegistry metricRegistry, MetricSuppliers metricSuppliers) {
super(metricRegistry, new FeignMetricName(Client.class), metricSuppliers);
this.client = client;
this.metricRegistry = metricRegistry;
this.metricSuppliers = metricSuppliers;
this.metricName = new FeignMetricName(Client.class);
}
@Override
public Response execute(Request request, Options options) throws IOException {
final RequestTemplate template = request.requestTemplate();
try (final Timer.Context classTimer =
metricRegistry.timer(
MetricRegistry.name(
metricName.metricName(template.methodMetadata(), template.feignTarget()),
"uri", template.methodMetadata().template().path()),
metricSuppliers.timers()).time()) {
try (final Timer.Context classTimer = createTimer(template)) {
Response response = client.execute(request, options);
metricRegistry.meter(
MetricRegistry.name(
metricName.metricName(template.methodMetadata(), template.feignTarget(),
"http_response_code"),
"status_group", response.status() / 100 + "xx",
"http_status", String.valueOf(response.status()),
"uri", template.methodMetadata().template().path()),
metricSuppliers.meters()).mark();
recordSuccess(template, response);
return response;
} catch (FeignException e) {
metricRegistry.meter(
MetricRegistry.name(
metricName.metricName(template.methodMetadata(), template.feignTarget(),
"http_response_code"),
"status_group", e.status() / 100 + "xx",
"http_status", String.valueOf(e.status()),
"uri", template.methodMetadata().template().path()),
metricSuppliers.meters()).mark();
recordFailure(template, e);
throw e;
} catch (IOException | RuntimeException e) {
recordFailure(template, e);
throw e;
} catch (Exception e) {
recordFailure(template, e);
throw new IOException(e);
}
}
}

11
dropwizard-metrics4/src/main/java/feign/metrics4/Metrics4Capability.java

@ -15,6 +15,7 @@ package feign.metrics4; @@ -15,6 +15,7 @@ package feign.metrics4;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import feign.AsyncClient;
import feign.Capability;
import feign.Client;
import feign.InvocationHandlerFactory;
@ -44,6 +45,11 @@ public class Metrics4Capability implements Capability { @@ -44,6 +45,11 @@ public class Metrics4Capability implements Capability {
return new MeteredClient(client, metricRegistry, metricSuppliers);
}
@Override
public AsyncClient<Object> enrich(AsyncClient<Object> client) {
return new MeteredAsyncClient(client, metricRegistry, metricSuppliers);
}
@Override
public Encoder enrich(Encoder encoder) {
return new MeteredEncoder(encoder, metricRegistry, metricSuppliers);
@ -56,8 +62,7 @@ public class Metrics4Capability implements Capability { @@ -56,8 +62,7 @@ public class Metrics4Capability implements Capability {
@Override
public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
return new MeteredInvocationHandleFactory(invocationHandlerFactory, metricRegistry,
metricSuppliers);
return new MeteredInvocationHandleFactory(
invocationHandlerFactory, metricRegistry, metricSuppliers);
}
}

56
dropwizard-metrics4/src/test/java/feign/metrics4/Metrics4CapabilityTest.java

@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
*/
package feign.metrics4;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import com.codahale.metrics.Metered;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
@ -22,6 +24,7 @@ import feign.micrometer.AbstractMetricsTestBase; @@ -22,6 +24,7 @@ import feign.micrometer.AbstractMetricsTestBase;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import org.hamcrest.Matcher;
public class Metrics4CapabilityTest
extends AbstractMetricsTestBase<MetricRegistry, String, Metric> {
@ -31,6 +34,7 @@ public class Metrics4CapabilityTest @@ -31,6 +34,7 @@ public class Metrics4CapabilityTest
return new MetricRegistry();
}
@Override
protected Capability createMetricCapability() {
return new Metrics4Capability(metricsRegistry);
}
@ -42,7 +46,9 @@ public class Metrics4CapabilityTest @@ -42,7 +46,9 @@ public class Metrics4CapabilityTest
@Override
protected boolean doesMetricIdIncludeClient(String metricId) {
return metricId.contains("feign.micrometer.AbstractMetricsTestBase$SimpleSource");
Matcher<String> containsBase = containsString("feign.micrometer.AbstractMetricsTestBase$");
Matcher<String> containsSource = containsString("Source");
return allOf(containsBase, containsSource).matches(metricId);
}
@Override
@ -56,30 +62,28 @@ public class Metrics4CapabilityTest @@ -56,30 +62,28 @@ public class Metrics4CapabilityTest
return true;
}
@Override
protected Metric getMetric(String suffix, String... tags) {
Util.checkArgument(tags.length % 2 == 0, "tags must contain key-value pairs %s",
Arrays.toString(tags));
return getFeignMetrics().entrySet()
.stream()
.filter(entry -> {
String name = entry.getKey();
if (!name.contains(suffix)) {
return false;
}
for (int i = 0; i < tags.length; i += 2) {
// metrics 4 doesn't support tags, for that reason we don't include tag name
if (!name.contains(tags[i + 1])) {
return false;
}
}
return true;
})
Util.checkArgument(
tags.length % 2 == 0, "tags must contain key-value pairs %s", Arrays.toString(tags));
return getFeignMetrics().entrySet().stream()
.filter(
entry -> {
String name = entry.getKey();
if (!name.contains(suffix)) {
return false;
}
for (int i = 0; i < tags.length; i += 2) {
// metrics 4 doesn't support tags, for that reason we don't include tag name
if (!name.contains(tags[i + 1])) {
return false;
}
}
return true;
})
.findAny()
.map(Entry::getValue)
.orElse(null);
@ -90,6 +94,11 @@ public class Metrics4CapabilityTest @@ -90,6 +94,11 @@ public class Metrics4CapabilityTest
return metricId.startsWith("feign.Client");
}
@Override
protected boolean isAsyncClientMetric(String metricId) {
return metricId.startsWith("feign.AsyncClient");
}
@Override
protected boolean isDecoderMetric(String metricId) {
return metricId.startsWith("feign.codec.Decoder");
@ -109,5 +118,4 @@ public class Metrics4CapabilityTest @@ -109,5 +118,4 @@ public class Metrics4CapabilityTest
protected long getMetricCounter(Metric metric) {
return ((Metered) metric).getCount();
}
}

82
dropwizard-metrics5/src/main/java/feign/metrics5/BaseMeteredClient.java

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.metrics5;
import feign.AsyncClient;
import feign.FeignException;
import feign.RequestTemplate;
import feign.Response;
import io.dropwizard.metrics5.MetricName;
import io.dropwizard.metrics5.MetricRegistry;
import io.dropwizard.metrics5.Timer;
public class BaseMeteredClient {
protected final MetricRegistry metricRegistry;
protected final FeignMetricName metricName;
protected final MetricSuppliers metricSuppliers;
public BaseMeteredClient(
MetricRegistry metricRegistry, FeignMetricName metricName, MetricSuppliers metricSuppliers) {
super();
this.metricRegistry = metricRegistry;
this.metricName = metricName;
this.metricSuppliers = metricSuppliers;
}
protected Timer.Context createTimer(RequestTemplate template) {
return metricRegistry
.timer(
metricName
.metricName(template.methodMetadata(), template.feignTarget())
.tagged("uri", template.methodMetadata().template().path()),
metricSuppliers.timers())
.time();
}
protected void recordSuccess(RequestTemplate template, Response response) {
metricRegistry
.counter(
httpResponseCode(template)
.tagged("http_status", String.valueOf(response.status()))
.tagged("status_group", response.status() / 100 + "xx")
.tagged("uri", template.methodMetadata().template().path()))
.inc();
}
protected void recordFailure(RequestTemplate template, FeignException e) {
metricRegistry
.counter(
httpResponseCode(template)
.tagged("exception_name", e.getClass().getSimpleName())
.tagged("http_status", String.valueOf(e.status()))
.tagged("status_group", e.status() / 100 + "xx")
.tagged("uri", template.methodMetadata().template().path()))
.inc();
}
protected void recordFailure(RequestTemplate template, Exception e) {
metricRegistry
.counter(
httpResponseCode(template)
.tagged("exception_name", e.getClass().getSimpleName())
.tagged("uri", template.methodMetadata().template().path()))
.inc();
}
private MetricName httpResponseCode(RequestTemplate template) {
return metricName.metricName(
template.methodMetadata(), template.feignTarget(), "http_response_code");
}
}

20
dropwizard-metrics5/src/main/java/feign/metrics5/FeignMetricName.java

@ -13,28 +13,24 @@ @@ -13,28 +13,24 @@
*/
package feign.metrics5;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import feign.MethodMetadata;
import feign.Target;
import io.dropwizard.metrics5.MetricName;
import io.dropwizard.metrics5.MetricRegistry;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
public final class FeignMetricName {
final class FeignMetricName {
private final Class<?> meteredComponent;
public FeignMetricName(Class<?> meteredComponent) {
this.meteredComponent = meteredComponent;
}
public MetricName metricName(MethodMetadata methodMetadata, Target<?> target, String suffix) {
return metricName(methodMetadata, target)
.resolve(suffix);
return metricName(methodMetadata, target).resolve(suffix);
}
public MetricName metricName(MethodMetadata methodMetadata, Target<?> target) {
@ -53,11 +49,7 @@ public final class FeignMetricName { @@ -53,11 +49,7 @@ public final class FeignMetricName {
return new URI(targetUrl).getHost();
} catch (final URISyntaxException e) {
// can't get the host, in that case, just read first 20 chars from url
return targetUrl.length() <= 20
? targetUrl
: targetUrl.substring(0, 20);
return targetUrl.length() <= 20 ? targetUrl : targetUrl.substring(0, 20);
}
}
}

63
dropwizard-metrics5/src/main/java/feign/metrics5/MeteredAsyncClient.java

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.metrics5;
import feign.AsyncClient;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response;
import io.dropwizard.metrics5.MetricRegistry;
import io.dropwizard.metrics5.Timer;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/** Warp feign {@link AsyncClient} with metrics. */
public class MeteredAsyncClient extends BaseMeteredClient implements AsyncClient<Object> {
private final AsyncClient<Object> asyncClient;
public MeteredAsyncClient(
AsyncClient<Object> asyncClient,
MetricRegistry metricRegistry,
MetricSuppliers metricSuppliers) {
super(metricRegistry, new FeignMetricName(AsyncClient.class), metricSuppliers);
this.asyncClient = asyncClient;
}
@Override
public CompletableFuture<Response> execute(
Request request,
Options options,
Optional<Object> requestContext) {
final RequestTemplate template = request.requestTemplate();
final Timer.Context timer = createTimer(template);
return asyncClient
.execute(request, options, requestContext)
.whenComplete(
(response, th) -> {
if (th == null) {
recordSuccess(template, response);
} else if (th instanceof FeignException) {
FeignException e = (FeignException) th;
recordFailure(template, e);
} else if (th instanceof Exception) {
Exception e = (Exception) th;
recordFailure(template, e);
}
})
.whenComplete((response, th) -> timer.close());
}
}

55
dropwizard-metrics5/src/main/java/feign/metrics5/MeteredClient.java

@ -13,63 +13,44 @@ @@ -13,63 +13,44 @@
*/
package feign.metrics5;
import java.io.IOException;
import feign.*;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response;
import io.dropwizard.metrics5.MetricRegistry;
import io.dropwizard.metrics5.Timer.Context;
import io.dropwizard.metrics5.Timer;
import java.io.IOException;
/**
* Warp feign {@link Client} with metrics.
*/
public class MeteredClient implements Client {
/** Warp feign {@link Client} with metrics. */
public class MeteredClient extends BaseMeteredClient implements Client {
private final Client client;
private final MetricRegistry metricRegistry;
private final FeignMetricName metricName;
private final MetricSuppliers metricSuppliers;
public MeteredClient(Client client, MetricRegistry metricRegistry,
MetricSuppliers metricSuppliers) {
public MeteredClient(
Client client, MetricRegistry metricRegistry, MetricSuppliers metricSuppliers) {
super(metricRegistry, new FeignMetricName(Client.class), metricSuppliers);
this.client = client;
this.metricRegistry = metricRegistry;
this.metricSuppliers = metricSuppliers;
this.metricName = new FeignMetricName(Client.class);
}
@Override
public Response execute(Request request, Options options) throws IOException {
final RequestTemplate template = request.requestTemplate();
try (final Context classTimer =
metricRegistry.timer(
metricName.metricName(template.methodMetadata(),
template.feignTarget())
.tagged("uri", template.methodMetadata().template().path()),
metricSuppliers.timers()).time()) {
try (final Timer.Context timer = createTimer(template)) {
Response response = client.execute(request, options);
metricRegistry.counter(
metricName
.metricName(template.methodMetadata(), template.feignTarget(), "http_response_code")
.tagged("http_status", String.valueOf(response.status()))
.tagged("status_group", response.status() / 100 + "xx")
.tagged("uri", template.methodMetadata().template().path()))
.inc();
recordSuccess(template, response);
return response;
} catch (FeignException e) {
metricRegistry.counter(
metricName
.metricName(template.methodMetadata(), template.feignTarget(), "http_response_code")
.tagged("http_status", String.valueOf(e.status()))
.tagged("status_group", e.status() / 100 + "xx")
.tagged("uri", template.methodMetadata().template().path()))
.inc();
recordFailure(template, e);
throw e;
} catch (IOException | RuntimeException e) {
recordFailure(template, e);
throw e;
} catch (Exception e) {
recordFailure(template, e);
throw new IOException(e);
}
}
}

11
dropwizard-metrics5/src/main/java/feign/metrics5/Metrics5Capability.java

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
*/
package feign.metrics5;
import feign.AsyncClient;
import feign.Capability;
import feign.Client;
import feign.InvocationHandlerFactory;
@ -44,6 +45,11 @@ public class Metrics5Capability implements Capability { @@ -44,6 +45,11 @@ public class Metrics5Capability implements Capability {
return new MeteredClient(client, metricRegistry, metricSuppliers);
}
@Override
public AsyncClient<Object> enrich(AsyncClient<Object> client) {
return new MeteredAsyncClient(client, metricRegistry, metricSuppliers);
}
@Override
public Encoder enrich(Encoder encoder) {
return new MeteredEncoder(encoder, metricRegistry, metricSuppliers);
@ -56,8 +62,7 @@ public class Metrics5Capability implements Capability { @@ -56,8 +62,7 @@ public class Metrics5Capability implements Capability {
@Override
public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
return new MeteredInvocationHandleFactory(invocationHandlerFactory, metricRegistry,
metricSuppliers);
return new MeteredInvocationHandleFactory(
invocationHandlerFactory, metricRegistry, metricSuppliers);
}
}

60
dropwizard-metrics5/src/test/java/feign/metrics5/Metrics5CapabilityTest.java

@ -13,10 +13,9 @@ @@ -13,10 +13,9 @@
*/
package feign.metrics5;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasEntry;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import feign.Capability;
import feign.Util;
import feign.micrometer.AbstractMetricsTestBase;
@ -24,16 +23,20 @@ import io.dropwizard.metrics5.Metered; @@ -24,16 +23,20 @@ import io.dropwizard.metrics5.Metered;
import io.dropwizard.metrics5.Metric;
import io.dropwizard.metrics5.MetricName;
import io.dropwizard.metrics5.MetricRegistry;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import org.hamcrest.Matcher;
public class Metrics5CapabilityTest
extends AbstractMetricsTestBase<MetricRegistry, MetricName, Metric> {
@Override
protected MetricRegistry createMetricsRegistry() {
return new MetricRegistry();
}
@Override
protected Capability createMetricCapability() {
return new Metrics5Capability(metricsRegistry);
}
@ -45,8 +48,10 @@ public class Metrics5CapabilityTest @@ -45,8 +48,10 @@ public class Metrics5CapabilityTest
@Override
protected boolean doesMetricIdIncludeClient(MetricName metricId) {
return hasEntry("client", "feign.micrometer.AbstractMetricsTestBase$SimpleSource")
.matches(metricId.getTags());
String tag = metricId.getTags().get("client");
Matcher<String> containsBase = containsString("feign.micrometer.AbstractMetricsTestBase$");
Matcher<String> containsSource = containsString("Source");
return allOf(containsBase, containsSource).matches(tag);
}
@Override
@ -62,28 +67,27 @@ public class Metrics5CapabilityTest @@ -62,28 +67,27 @@ public class Metrics5CapabilityTest
@Override
protected Metric getMetric(String suffix, String... tags) {
Util.checkArgument(tags.length % 2 == 0, "tags must contain key-value pairs %s",
Arrays.toString(tags));
return getFeignMetrics().entrySet()
.stream()
.filter(entry -> {
MetricName name = entry.getKey();
if (!name.getKey().endsWith(suffix)) {
return false;
}
for (int i = 0; i < tags.length; i += 2) {
if (name.getTags().containsKey(tags[i])) {
if (!name.getTags().get(tags[i]).equals(tags[i + 1])) {
Util.checkArgument(
tags.length % 2 == 0, "tags must contain key-value pairs %s", Arrays.toString(tags));
return getFeignMetrics().entrySet().stream()
.filter(
entry -> {
MetricName name = entry.getKey();
if (!name.getKey().endsWith(suffix)) {
return false;
}
}
}
return true;
})
for (int i = 0; i < tags.length; i += 2) {
if (name.getTags().containsKey(tags[i])) {
if (!name.getTags().get(tags[i]).equals(tags[i + 1])) {
return false;
}
}
}
return true;
})
.findAny()
.map(Entry::getValue)
.orElse(null);
@ -94,6 +98,11 @@ public class Metrics5CapabilityTest @@ -94,6 +98,11 @@ public class Metrics5CapabilityTest
return metricId.getKey().startsWith("feign.Client");
}
@Override
protected boolean isAsyncClientMetric(MetricName metricId) {
return metricId.getKey().startsWith("feign.AsyncClient");
}
@Override
protected boolean isDecoderMetric(MetricName metricId) {
return metricId.getKey().startsWith("feign.codec.Decoder");
@ -113,5 +122,4 @@ public class Metrics5CapabilityTest @@ -113,5 +122,4 @@ public class Metrics5CapabilityTest
protected long getMetricCounter(Metric metric) {
return ((Metered) metric).getCount();
}
}

77
micrometer/src/main/java/feign/micrometer/BaseMeteredClient.java

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.micrometer;
import static feign.micrometer.MetricTagResolver.EMPTY_TAGS_ARRAY;
import feign.Request;
import feign.Request.Options;
import feign.RequestTemplate;
import feign.Response;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
abstract class BaseMeteredClient {
protected final MeterRegistry meterRegistry;
protected final MetricName metricName;
protected final MetricTagResolver metricTagResolver;
public BaseMeteredClient(
MeterRegistry meterRegistry, MetricName metricName, MetricTagResolver metricTagResolver) {
super();
this.meterRegistry = meterRegistry;
this.metricName = metricName;
this.metricTagResolver = metricTagResolver;
}
protected void countResponseCode(
Request request,
Response response,
Options options,
int responseStatus,
Exception e) {
final Tag[] extraTags = extraTags(request, response, options, e);
final RequestTemplate template = request.requestTemplate();
final Tags allTags =
metricTagResolver
.tag(
template.methodMetadata(),
template.feignTarget(),
e,
Tag.of("http_status", String.valueOf(responseStatus)),
Tag.of("status_group", responseStatus / 100 + "xx"),
Tag.of("uri", template.methodMetadata().template().path()))
.and(extraTags);
meterRegistry.counter(metricName.name("http_response_code"), allTags).increment();
}
protected Timer createTimer(Request request, Response response, Options options, Exception e) {
final RequestTemplate template = request.requestTemplate();
final Tags allTags =
metricTagResolver
.tag(
template.methodMetadata(),
template.feignTarget(),
e,
Tag.of("uri", template.methodMetadata().template().path()))
.and(extraTags(request, response, options, e));
return meterRegistry.timer(metricName.name(e), allTags);
}
protected Tag[] extraTags(Request request, Response response, Options options, Exception e) {
return EMPTY_TAGS_ARRAY;
}
}

76
micrometer/src/main/java/feign/micrometer/MeteredAsyncClient.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package feign.micrometer;
import feign.AsyncClient;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.Response;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/** Warp feign {@link Client} with metrics. */
public class MeteredAsyncClient extends BaseMeteredClient implements AsyncClient<Object> {
private final AsyncClient<Object> client;
public MeteredAsyncClient(AsyncClient<Object> client, MeterRegistry meterRegistry) {
this(
client,
meterRegistry,
new FeignMetricName(AsyncClient.class),
new FeignMetricTagResolver());
}
public MeteredAsyncClient(
AsyncClient<Object> client,
MeterRegistry meterRegistry,
MetricName metricName,
MetricTagResolver metricTagResolver) {
super(meterRegistry, metricName, metricTagResolver);
this.client = client;
}
@Override
public CompletableFuture<Response> execute(
Request request,
Options options,
Optional<Object> requestContext) {
final Timer.Sample sample = Timer.start(meterRegistry);
return client
.execute(request, options, requestContext)
.whenComplete(
(response, th) -> {
Timer timer;
if (th == null) {
countResponseCode(request, response, options, response.status(), null);
timer = createTimer(request, response, options, null);
} else if (th instanceof FeignException) {
FeignException e = (FeignException) th;
timer = createTimer(request, response, options, e);
countResponseCode(request, response, options, e.status(), e);
} else if (th instanceof Exception) {
Exception e = (Exception) th;
timer = createTimer(request, response, options, e);
} else {
timer = createTimer(request, response, options, null);
}
sample.stop(timer);
});
}
}

62
micrometer/src/main/java/feign/micrometer/MeteredClient.java

@ -13,37 +13,31 @@ @@ -13,37 +13,31 @@
*/
package feign.micrometer;
import feign.*;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.Request.Options;
import feign.Response;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import java.io.IOException;
import static feign.micrometer.MetricTagResolver.EMPTY_TAGS_ARRAY;
/**
* Warp feign {@link Client} with metrics.
*/
public class MeteredClient implements Client {
/** Warp feign {@link Client} with metrics. */
public class MeteredClient extends BaseMeteredClient implements Client {
private final Client client;
private final MeterRegistry meterRegistry;
private final MetricName metricName;
private final MetricTagResolver metricTagResolver;
public MeteredClient(Client client, MeterRegistry meterRegistry) {
this(client, meterRegistry, new FeignMetricName(Client.class), new FeignMetricTagResolver());
}
public MeteredClient(Client client,
public MeteredClient(
Client client,
MeterRegistry meterRegistry,
MetricName metricName,
MetricTagResolver metricTagResolver) {
super(meterRegistry, metricName, metricTagResolver);
this.client = client;
this.meterRegistry = meterRegistry;
this.metricName = metricName;
this.metricTagResolver = metricTagResolver;
}
@Override
@ -72,42 +66,4 @@ public class MeteredClient implements Client { @@ -72,42 +66,4 @@ public class MeteredClient implements Client {
sample.stop(timer);
}
}
protected void countResponseCode(Request request,
Response response,
Options options,
int responseStatus,
Exception e) {
final Tag[] extraTags = extraTags(request, response, options, e);
final RequestTemplate template = request.requestTemplate();
final Tags allTags = metricTagResolver
.tag(template.methodMetadata(), template.feignTarget(), e,
Tag.of("http_status", String.valueOf(responseStatus)),
Tag.of("status_group", responseStatus / 100 + "xx"),
Tag.of("uri", template.methodMetadata().template().path()))
.and(extraTags);
meterRegistry.counter(
metricName.name("http_response_code"),
allTags)
.increment();
}
protected Timer createTimer(Request request,
Response response,
Options options,
Exception e) {
final RequestTemplate template = request.requestTemplate();
final Tags allTags = metricTagResolver
.tag(template.methodMetadata(), template.feignTarget(), e,
Tag.of("uri", template.methodMetadata().template().path()))
.and(extraTags(request, response, options, e));
return meterRegistry.timer(metricName.name(e), allTags);
}
protected Tag[] extraTags(Request request,
Response response,
Options options,
Exception e) {
return EMPTY_TAGS_ARRAY;
}
}

7
micrometer/src/main/java/feign/micrometer/MicrometerCapability.java

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
*/
package feign.micrometer;
import feign.AsyncClient;
import feign.Capability;
import feign.Client;
import feign.InvocationHandlerFactory;
@ -42,6 +43,11 @@ public class MicrometerCapability implements Capability { @@ -42,6 +43,11 @@ public class MicrometerCapability implements Capability {
return new MeteredClient(client, meterRegistry);
}
@Override
public AsyncClient<Object> enrich(AsyncClient<Object> client) {
return new MeteredAsyncClient(client, meterRegistry);
}
@Override
public Encoder enrich(Encoder encoder) {
return new MeteredEncoder(encoder, meterRegistry);
@ -56,5 +62,4 @@ public class MicrometerCapability implements Capability { @@ -56,5 +62,4 @@ public class MicrometerCapability implements Capability {
public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
return new MeteredInvocationHandleFactory(invocationHandlerFactory, meterRegistry);
}
}

266
micrometer/src/test/java/feign/micrometer/AbstractMetricsTestBase.java

@ -13,18 +13,29 @@ @@ -13,18 +13,29 @@
*/
package feign.micrometer;
import feign.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import feign.AsyncFeign;
import feign.Capability;
import feign.Feign;
import feign.FeignException;
import feign.Param;
import feign.RequestLine;
import feign.mock.HttpMethod;
import feign.mock.MockClient;
import feign.mock.MockTarget;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
@ -32,7 +43,12 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -32,7 +43,12 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
@RequestLine("GET /get")
String get(String body);
}
public interface CompletableSource {
@RequestLine("GET /get")
CompletableFuture<String> get(String body);
}
protected MR metricsRegistry;
@ -46,38 +62,76 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -46,38 +62,76 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
@Test
public final void addMetricsCapability() {
final SimpleSource source = Feign.builder()
.client(new MockClient()
.ok(HttpMethod.GET, "/get", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SimpleSource.class));
final SimpleSource source =
Feign.builder()
.client(new MockClient().ok(HttpMethod.GET, "/get", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SimpleSource.class));
source.get("0x3456789");
assertMetricsCapability(false);
}
@Test
public final void addAsyncMetricsCapability() {
final CompletableSource source =
AsyncFeign.asyncBuilder()
.client(new MockClient().ok(HttpMethod.GET, "/get", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(CompletableSource.class));
source.get("0x3456789").join();
assertMetricsCapability(true);
}
private void assertMetricsCapability(boolean asyncClient) {
Map<METRIC_ID, METRIC> metrics = getFeignMetrics();
assertThat(metrics, aMapWithSize(7));
metrics.keySet().forEach(metricId -> assertThat(
"Expect all metric names to include client name:" + metricId,
doesMetricIdIncludeClient(metricId)));
metrics.keySet().forEach(metricId -> assertThat(
"Expect all metric names to include method name:" + metricId,
doesMetricIncludeVerb(metricId, "get")));
metrics.keySet().forEach(metricId -> assertThat(
"Expect all metric names to include host name:" + metricId,
doesMetricIncludeHost(metricId)));
final Map<METRIC_ID, METRIC> clientMetrics = getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
metrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all metric names to include client name:" + metricId,
doesMetricIdIncludeClient(metricId)));
metrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all metric names to include method name:" + metricId,
doesMetricIncludeVerb(metricId, "get")));
metrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all metric names to include host name:" + metricId,
doesMetricIncludeHost(metricId)));
final Map<METRIC_ID, METRIC> clientMetrics;
if (asyncClient) {
clientMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isAsyncClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} else {
clientMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
assertThat(clientMetrics, aMapWithSize(2));
clientMetrics.values().stream()
.filter(this::doesMetricHasCounter)
.forEach(metric -> assertEquals(1, getMetricCounter(metric)));
final Map<METRIC_ID, METRIC> decoderMetrics = getFeignMetrics().entrySet().stream()
.filter(entry -> isDecoderMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
final Map<METRIC_ID, METRIC> decoderMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isDecoderMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertThat(decoderMetrics, aMapWithSize(2));
decoderMetrics.values().stream()
.filter(this::doesMetricHasCounter)
.forEach(metric -> assertEquals(1, getMetricCounter(metric)));
@ -93,18 +147,19 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -93,18 +147,19 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
protected abstract Map<METRIC_ID, METRIC> getFeignMetrics();
@Test
public void clientPropagatesUncheckedException() {
final AtomicReference<FeignException.NotFound> notFound = new AtomicReference<>();
final SimpleSource source = Feign.builder()
.client((request, options) -> {
notFound.set(new FeignException.NotFound("test", request, null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
final SimpleSource source =
Feign.builder()
.client(
(request, options) -> {
notFound.set(new FeignException.NotFound("test", request, null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
try {
source.get("0x3456789");
@ -113,42 +168,43 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -113,42 +168,43 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
assertSame(notFound.get(), e);
}
assertThat(getMetric("http_response_code", "http_status", "404", "status_group", "4xx"),
assertThat(
getMetric("http_response_code", "http_status", "404", "status_group", "4xx"),
notNullValue());
}
protected abstract METRIC getMetric(String suffix, String... tags);
@Test
public void decoderPropagatesUncheckedException() {
final AtomicReference<FeignException.NotFound> notFound = new AtomicReference<>();
final SimpleSource source = Feign.builder()
.client(new MockClient()
.ok(HttpMethod.GET, "/get", "1234567890abcde"))
.decoder((response, type) -> {
notFound.set(new FeignException.NotFound("test", response.request(), null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
final SimpleSource source =
Feign.builder()
.client(new MockClient().ok(HttpMethod.GET, "/get", "1234567890abcde"))
.decoder(
(response, type) -> {
notFound.set(new FeignException.NotFound("test", response.request(), null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
FeignException.NotFound thrown =
assertThrows(FeignException.NotFound.class, () -> source.get("0x3456789"));
assertSame(notFound.get(), thrown);
}
@Test
public void shouldMetricCollectionWithCustomException() {
final SimpleSource source = Feign.builder()
.client((request, options) -> {
throw new RuntimeException("Test error");
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
final SimpleSource source =
Feign.builder()
.client(
(request, options) -> {
throw new RuntimeException("Test error");
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SimpleSource.class));
RuntimeException thrown = assertThrows(RuntimeException.class, () -> source.get("0x3456789"));
assertThat(thrown.getMessage(), equalTo("Test error"));
@ -158,81 +214,96 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -158,81 +214,96 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
notNullValue());
}
@Test
public void clientMetricsHaveUriLabel() {
final SimpleSource source = Feign.builder()
.client(new MockClient()
.ok(HttpMethod.GET, "/get", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SimpleSource.class));
final SimpleSource source =
Feign.builder()
.client(new MockClient().ok(HttpMethod.GET, "/get", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SimpleSource.class));
source.get("0x3456789");
final Map<METRIC_ID, METRIC> clientMetrics = getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
clientMetrics.keySet().forEach(metricId -> assertThat(
"Expect all Client metric names to include uri:" + metricId,
doesMetricIncludeUri(metricId, "/get")));
final Map<METRIC_ID, METRIC> clientMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
clientMetrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all Client metric names to include uri:" + metricId,
doesMetricIncludeUri(metricId, "/get")));
}
public interface SourceWithPathExpressions {
@RequestLine("GET /get/{id}")
String get(@Param("id") String id, String body);
}
@Test
public void clientMetricsHaveUriLabelWithPathExpression() {
final SourceWithPathExpressions source = Feign.builder()
.client(new MockClient()
.ok(HttpMethod.GET, "/get/123", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SourceWithPathExpressions.class));
final SourceWithPathExpressions source =
Feign.builder()
.client(new MockClient().ok(HttpMethod.GET, "/get/123", "1234567890abcde"))
.addCapability(createMetricCapability())
.target(new MockTarget<>(SourceWithPathExpressions.class));
source.get("123", "0x3456789");
final Map<METRIC_ID, METRIC> clientMetrics = getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
clientMetrics.keySet().forEach(metricId -> assertThat(
"Expect all Client metric names to include uri as aggregated path expression:" + metricId,
doesMetricIncludeUri(metricId, "/get/{id}")));
final Map<METRIC_ID, METRIC> clientMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isClientMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
clientMetrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all Client metric names to include uri as aggregated path expression:"
+ metricId,
doesMetricIncludeUri(metricId, "/get/{id}")));
}
@Test
public void decoderExceptionCounterHasUriLabelWithPathExpression() {
final AtomicReference<FeignException.NotFound> notFound = new AtomicReference<>();
final SourceWithPathExpressions source = Feign.builder()
.client(new MockClient()
.ok(HttpMethod.GET, "/get/123", "1234567890abcde"))
.decoder((response, type) -> {
notFound.set(new FeignException.NotFound("test", response.request(), null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SourceWithPathExpressions.class));
final SourceWithPathExpressions source =
Feign.builder()
.client(new MockClient().ok(HttpMethod.GET, "/get/123", "1234567890abcde"))
.decoder(
(response, type) -> {
notFound.set(new FeignException.NotFound("test", response.request(), null, null));
throw notFound.get();
})
.addCapability(createMetricCapability())
.target(new MockTarget<>(MicrometerCapabilityTest.SourceWithPathExpressions.class));
FeignException.NotFound thrown =
assertThrows(FeignException.NotFound.class, () -> source.get("123", "0x3456789"));
assertSame(notFound.get(), thrown);
final Map<METRIC_ID, METRIC> decoderMetrics = getFeignMetrics().entrySet().stream()
.filter(entry -> isDecoderMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
decoderMetrics.keySet().forEach(metricId -> assertThat(
"Expect all Decoder metric names to include uri as aggregated path expression:" + metricId,
doesMetricIncludeUri(metricId, "/get/{id}")));
final Map<METRIC_ID, METRIC> decoderMetrics =
getFeignMetrics().entrySet().stream()
.filter(entry -> isDecoderMetric(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
decoderMetrics
.keySet()
.forEach(
metricId -> assertThat(
"Expect all Decoder metric names to include uri as aggregated path expression:"
+ metricId,
doesMetricIncludeUri(metricId, "/get/{id}")));
}
protected abstract boolean isClientMetric(METRIC_ID metricId);
protected abstract boolean isAsyncClientMetric(METRIC_ID metricId);
protected abstract boolean isDecoderMetric(METRIC_ID metricId);
protected abstract boolean doesMetricIncludeUri(METRIC_ID metricId, String uri);
@ -240,5 +311,4 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> { @@ -240,5 +311,4 @@ public abstract class AbstractMetricsTestBase<MR, METRIC_ID, METRIC> {
protected abstract boolean doesMetricHasCounter(METRIC metric);
protected abstract long getMetricCounter(METRIC metric);
}

60
micrometer/src/test/java/feign/micrometer/MicrometerCapabilityTest.java

@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
*/
package feign.micrometer;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import feign.Capability;
import feign.Util;
import io.micrometer.core.instrument.Measurement;
@ -28,16 +30,17 @@ import java.util.Map; @@ -28,16 +30,17 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.hamcrest.Matcher;
public class MicrometerCapabilityTest
extends AbstractMetricsTestBase<SimpleMeterRegistry, Id, Meter> {
@Override
protected SimpleMeterRegistry createMetricsRegistry() {
return new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
}
@Override
protected Capability createMetricCapability() {
return new MicrometerCapability(metricsRegistry);
}
@ -47,16 +50,15 @@ public class MicrometerCapabilityTest @@ -47,16 +50,15 @@ public class MicrometerCapabilityTest
List<Meter> metrics = new ArrayList<>();
metricsRegistry.forEachMeter(metrics::add);
metrics.removeIf(meter -> !meter.getId().getName().startsWith("feign."));
return metrics.stream()
.collect(Collectors.toMap(
Meter::getId,
Function.identity()));
return metrics.stream().collect(Collectors.toMap(Meter::getId, Function.identity()));
}
@Override
protected boolean doesMetricIdIncludeClient(Id metricId) {
return metricId.getTag("client")
.contains("feign.micrometer.AbstractMetricsTestBase$SimpleSource");
String tag = metricId.getTag("client");
Matcher<String> containsBase = containsString("feign.micrometer.AbstractMetricsTestBase$");
Matcher<String> containsSource = containsString("Source");
return allOf(containsBase, containsSource).matches(tag);
}
@Override
@ -69,31 +71,29 @@ public class MicrometerCapabilityTest @@ -69,31 +71,29 @@ public class MicrometerCapabilityTest
return metricId.getTag("host").equals("");
}
@Override
protected Meter getMetric(String suffix, String... tags) {
Util.checkArgument(tags.length % 2 == 0, "tags must contain key-value pairs %s",
Arrays.toString(tags));
return getFeignMetrics().entrySet()
.stream()
.filter(entry -> {
Id name = entry.getKey();
if (!name.getName().endsWith(suffix)) {
return false;
}
for (int i = 0; i < tags.length; i += 2) {
if (name.getTag(tags[i]) != null) {
if (!name.getTag(tags[i]).equals(tags[i + 1])) {
Util.checkArgument(
tags.length % 2 == 0, "tags must contain key-value pairs %s", Arrays.toString(tags));
return getFeignMetrics().entrySet().stream()
.filter(
entry -> {
Id name = entry.getKey();
if (!name.getName().endsWith(suffix)) {
return false;
}
}
}
return true;
})
for (int i = 0; i < tags.length; i += 2) {
if (name.getTag(tags[i]) != null) {
if (!name.getTag(tags[i]).equals(tags[i + 1])) {
return false;
}
}
}
return true;
})
.findAny()
.map(Entry::getValue)
.orElse(null);
@ -104,6 +104,11 @@ public class MicrometerCapabilityTest @@ -104,6 +104,11 @@ public class MicrometerCapabilityTest
return metricId.getName().startsWith("feign.Client");
}
@Override
protected boolean isAsyncClientMetric(Id metricId) {
return metricId.getName().startsWith("feign.AsyncClient");
}
@Override
protected boolean isDecoderMetric(Id metricId) {
return metricId.getName().startsWith("feign.codec.Decoder");
@ -133,5 +138,4 @@ public class MicrometerCapabilityTest @@ -133,5 +138,4 @@ public class MicrometerCapabilityTest
}
return 0;
}
}

40
mock/src/main/java/feign/mock/MockClient.java

@ -17,22 +17,14 @@ import static feign.Util.UTF_8; @@ -17,22 +17,14 @@ import static feign.Util.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import feign.Client;
import feign.Request;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import feign.*;
import feign.Request.ProtocolVersion;
import feign.Response;
import feign.Util;
public class MockClient implements Client {
public class MockClient implements Client, AsyncClient<Object> {
class RequestResponse {
static class RequestResponse {
private final RequestKey requestKey;
@ -45,9 +37,9 @@ public class MockClient implements Client { @@ -45,9 +37,9 @@ public class MockClient implements Client {
}
private final List<RequestResponse> responses = new ArrayList<RequestResponse>();
private final List<RequestResponse> responses = new ArrayList<>();
private final Map<RequestKey, List<Request>> requests = new HashMap<RequestKey, List<Request>>();
private final Map<RequestKey, List<Request>> requests = new HashMap<>();
private boolean sequential;
@ -74,6 +66,22 @@ public class MockClient implements Client { @@ -74,6 +66,22 @@ public class MockClient implements Client {
return responseBuilder.request(request).build();
}
@Override
public CompletableFuture<Response> execute(Request request,
Request.Options options,
Optional<Object> requestContext) {
RequestKey requestKey = RequestKey.create(request);
Response.Builder responseBuilder;
if (sequential) {
responseBuilder = executeSequential(requestKey);
} else {
responseBuilder = executeAny(request, requestKey);
}
responseBuilder.protocolVersion(ProtocolVersion.MOCK);
return CompletableFuture.completedFuture(responseBuilder.request(request).build());
}
private Response.Builder executeSequential(RequestKey requestKey) {
Response.Builder responseBuilder;
if (responseIterator == null) {
@ -99,7 +107,7 @@ public class MockClient implements Client { @@ -99,7 +107,7 @@ public class MockClient implements Client {
if (requests.containsKey(requestKey)) {
requests.get(requestKey).add(request);
} else {
requests.put(requestKey, new ArrayList<Request>(Arrays.asList(request)));
requests.put(requestKey, new ArrayList<>(Arrays.asList(request)));
}
responseBuilder = getResponseBuilder(request, requestKey);

Loading…
Cancel
Save