Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Micrometer metrics tags extension #1322

Merged
merged 13 commits into from
Jul 20, 2021
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 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
Expand Down
2 changes: 1 addition & 1 deletion micrometer/src/main/java/feign/micrometer/MeteredBody.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 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
Expand Down
104 changes: 74 additions & 30 deletions micrometer/src/main/java/feign/micrometer/MeteredClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
*/
package feign.micrometer;

import java.io.IOException;
import feign.Client;
import feign.FeignException;
import feign.Request;
import feign.*;
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;
import java.io.IOException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice not to change import ordering... but, don't worry too much about it =)

import java.util.ArrayList;
import java.util.List;

/**
* Warp feign {@link Client} with metrics.
Expand All @@ -45,38 +45,82 @@ public MeteredClient(Client client, MeterRegistry meterRegistry, MetricName metr
@Override
public Response execute(Request request, Options options) throws IOException {
final RequestTemplate template = request.requestTemplate();

final Timer.Sample sample = Timer.start(meterRegistry);
Kuvaldis marked this conversation as resolved.
Show resolved Hide resolved
try {
return meterRegistry.timer(
metricName.name(),
metricName.tag(template.methodMetadata(), template.feignTarget()))
.recordCallable(() -> {
Response response = client.execute(request, options);
meterRegistry.counter(
metricName.name("http_response_code"),
metricName.tag(
template.methodMetadata(),
template.feignTarget(),
Tag.of("http_status", String.valueOf(response.status())),
Tag.of("status_group", response.status() / 100 + "xx")))
.increment();
return response;
});
final Response response = client.execute(request, options);
countSuccessResponseCode(template, options, response.status());
final Timer timer = createSuccessTimer(template, options);
sample.stop(timer);
return response;
} catch (FeignException e) {
meterRegistry.counter(
metricName.name("http_response_code"),
metricName.tag(
template.methodMetadata(),
template.feignTarget(),
Tag.of("http_status", String.valueOf(e.status())),
Tag.of("status_group", e.status() / 100 + "xx")))
.increment();
countErrorResponseCode(template, options, e.status(), e);
throw e;
} catch (IOException | RuntimeException e) {
sample.stop(createExceptionTimer(template, options, e));
throw e;
} catch (Exception e) {
sample.stop(createExceptionTimer(template, options, e));
throw new IOException(e);
}
}

private void countSuccessResponseCode(RequestTemplate template,
Options options,
int responseStatus) {
final List<Tag> tags = extraSuccessTags(template, options);
Kuvaldis marked this conversation as resolved.
Show resolved Hide resolved
tags.add(Tag.of("http_status", String.valueOf(responseStatus)));
tags.add(Tag.of("status_group", responseStatus / 100 + "xx"));
final Tags allTags = metricName
.tag(template.methodMetadata(), template.feignTarget(), tags.toArray(new Tag[] {}));
meterRegistry.counter(
metricName.name("http_response_code"),
allTags)
.increment();
}

private void countErrorResponseCode(RequestTemplate template,
Options options,
int responseStatus,
Exception e) {
final List<Tag> tags = extraExceptionTags(template, options, e);
tags.add(Tag.of("http_status", String.valueOf(responseStatus)));
tags.add(Tag.of("status_group", responseStatus / 100 + "xx"));
final Tags allTags = metricName
.tag(template.methodMetadata(), template.feignTarget(), tags.toArray(new Tag[] {}));
meterRegistry.counter(
metricName.name("http_response_code"),
allTags)
.increment();
}

private Timer createSuccessTimer(RequestTemplate template, Options options) {
Kuvaldis marked this conversation as resolved.
Show resolved Hide resolved
final List<Tag> successTags = extraSuccessTags(template, options);
final Tag[] tags = successTags.toArray(new Tag[] {});
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.timer(metricName.name(), allTags);
}

private Timer createExceptionTimer(RequestTemplate template,
Options options,
Exception exception) {
final List<Tag> exceptionTags = extraExceptionTags(template, options, exception);
final Tag[] tags = exceptionTags.toArray(new Tag[] {});
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.timer(metricName.name("exception"), allTags);
}

protected List<Tag> extraSuccessTags(RequestTemplate template, Options options) {
final List<Tag> result = new ArrayList<>();
result.add(Tag.of("uri", template.path()));
return result;
}

protected List<Tag> extraExceptionTags(RequestTemplate template,
Kuvaldis marked this conversation as resolved.
Show resolved Hide resolved
Options options,
Exception exception) {
final List<Tag> result = new ArrayList<>();
result.add(Tag.of("uri", template.path()));
result.add(Tag.of("exception_name", exception.getClass().getSimpleName()));
return result;
}
}
87 changes: 65 additions & 22 deletions micrometer/src/main/java/feign/micrometer/MeteredDecoder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 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
Expand All @@ -16,14 +16,17 @@

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import feign.FeignException;
import feign.Request;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.*;

/**
* Warp feign {@link Decoder} with metrics.
Expand All @@ -46,41 +49,81 @@ public MeteredDecoder(Decoder decoder, MeterRegistry meterRegistry, MetricName m

@Override
public Object decode(Response response, Type type)
throws IOException, DecodeException, FeignException {
final RequestTemplate template = response.request().requestTemplate();
throws IOException, FeignException {
final Optional<MeteredBody> body = Optional.ofNullable(response.body())
.map(MeteredBody::new);

Response meteredResponse = body.map(b -> response.toBuilder().body(b).build())
.orElse(response);

Object decoded;

final Timer.Sample sample = Timer.start(meterRegistry);
try {
decoded = meterRegistry
.timer(metricName.name(),
metricName.tag(template.methodMetadata(), template.feignTarget()))
.recordCallable(() -> decoder.decode(meteredResponse, type));
decoded = decoder.decode(meteredResponse, type);
final Timer timer = createSuccessTimer(response, type);
sample.stop(timer);
} catch (IOException | RuntimeException e) {
meterRegistry.counter(
metricName.name("error_count"),
metricName.tag(template.methodMetadata(), template.feignTarget())
.and(Tag.of("exception_name", e.getClass().getSimpleName())))
.count();
sample.stop(createExceptionTimer(response, type, e));
createExceptionCounter(response, type, e).count();
throw e;
} catch (Exception e) {
meterRegistry.counter(
metricName.name("error_count"),
metricName.tag(template.methodMetadata(), template.feignTarget())
.and(Tag.of("exception_name", e.getClass().getSimpleName())))
.count();
sample.stop(createExceptionTimer(response, type, e));
createExceptionCounter(response, type, e).count();
throw new IOException(e);
}

body.ifPresent(b -> meterRegistry.summary(
metricName.name("response_size"),
metricName.tag(template.methodMetadata(), template.feignTarget())).record(b.count()));
body.ifPresent(b -> createSummary(response, type).record(b.count()));

return decoded;
}

private Timer createSuccessTimer(Response response, Type type) {
final List<Tag> successTags = extraSuccessTags(response, type);
final Tag[] tags = successTags.toArray(new Tag[] {});
final RequestTemplate template = response.request().requestTemplate();
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.timer(metricName.name(), allTags);
}

private Timer createExceptionTimer(Response response, Type type, Exception exception) {
final List<Tag> exceptionTags = extraExceptionTags(response, type, exception);
final Tag[] tags = exceptionTags.toArray(new Tag[] {});
final RequestTemplate template = response.request().requestTemplate();
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.timer(metricName.name("exception"), allTags);
}

private Counter createExceptionCounter(Response response, Type type, Exception exception) {
final List<Tag> exceptionTags = extraExceptionTags(response, type, exception);
final Tag[] tags = exceptionTags.toArray(new Tag[] {});
final RequestTemplate template = response.request().requestTemplate();
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.counter(metricName.name("error_count"), allTags);
}

private DistributionSummary createSummary(Response response, Type type) {
final List<Tag> successTags = extraSummaryTags(response, type);
final Tag[] tags = successTags.toArray(new Tag[] {});
final RequestTemplate template = response.request().requestTemplate();
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.summary(metricName.name("response_size"), allTags);
}

protected List<Tag> extraSuccessTags(Response response, Type type) {
return Collections.emptyList();
}

protected List<Tag> extraExceptionTags(Response response, Type type, Exception exception) {
final RequestTemplate template = response.request().requestTemplate();
final List<Tag> result = new ArrayList<>();
result.add(Tag.of("uri", template.path()));
result.add(Tag.of("exception_name", exception.getClass().getSimpleName()));
return result;
}

protected List<Tag> extraSummaryTags(Response response, Type type) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you are using this for some internal metrics (I would be curious to know what you are going =] )

I would prefer to see code to live in the MetricName interface... that would give a single place for all your tag customization needs.

We would have a few methods, one called requestExtraTags other one responseExtraTags, another like exceptionExtraTags

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted tags resolution to a separate extendable class. However, seems like depending on where we create tags (i.e. client, encoder/decoder, method handler) we have really different parameters to build tags upon. I added defaultTags method, which might be helpful for customization in general.

return Collections.emptyList();
}

}
40 changes: 31 additions & 9 deletions micrometer/src/main/java/feign/micrometer/MeteredEncoder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 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
Expand All @@ -14,10 +14,14 @@
package feign.micrometer;


import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.*;
import java.lang.reflect.Type;

/**
Expand All @@ -42,17 +46,35 @@ public MeteredEncoder(Encoder encoder, MeterRegistry meterRegistry, MetricName m
@Override
public void encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
meterRegistry.timer(
metricName.name(),
metricName.tag(template.methodMetadata(), template.feignTarget()))
createTimer(object, bodyType, template)
.record(() -> encoder.encode(object, bodyType, template));

if (template.body() != null) {
meterRegistry.summary(
metricName.name("request_size"),
metricName.tag(template.methodMetadata(), template.feignTarget()))
.record(template.body().length);
createSummary(object, bodyType, template).record(template.body().length);
}
}

private Timer createTimer(Object object, Type bodyType, RequestTemplate template) {
final List<Tag> successTags = extraTimerTags(object, bodyType, template);
final Tag[] tags = successTags.toArray(new Tag[] {});
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.timer(metricName.name(), allTags);
}

private DistributionSummary createSummary(Object object,
Type bodyType,
RequestTemplate template) {
final List<Tag> successTags = extraSummaryTags(object, bodyType, template);
final Tag[] tags = successTags.toArray(new Tag[] {});
final Tags allTags = metricName.tag(template.methodMetadata(), template.feignTarget(), tags);
return meterRegistry.summary(metricName.name("response_size"), allTags);
}

protected List<Tag> extraTimerTags(Object object, Type bodyType, RequestTemplate template) {
return Collections.emptyList();
}

protected List<Tag> extraSummaryTags(Object object, Type bodyType, RequestTemplate template) {
return Collections.emptyList();
}
}