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

Add response code meters for ResponseMetered annotation #3043

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/source/manual/jersey.rst
Expand Up @@ -56,7 +56,7 @@ application's ``ResourceConfig`` as a singleton provider for this to work.
}

@GET
@ResponseMetered
@ResponseMetered(level = ResponseMeteredLevel.ALL)
@Path("/response-metered")
public Response responseMetered(@QueryParam("invalid") @DefaultValue("false") boolean invalid) {
if (invalid) {
Expand All @@ -74,6 +74,6 @@ If the annotation is placed on the class, it will apply to all its resource meth

* ``@Timed`` adds a timer and measures time spent in that method.
* ``@Metered`` adds a meter and measures the rate at which the resource method is accessed.
* ``@ResponseMetered`` adds a meter and measures rate for each class of response codes (1xx/2xx/3xx/4xx/5xx).
* ``@ResponseMetered`` adds meters and measures rate for response codes based on the selected level.
* ``@ExceptionMetered`` adds a meter and measures how often the specified exception occurs when processing the resource.
If the ``cause`` is not specified, the default is ``Exception.class``.
Expand Up @@ -12,14 +12,15 @@
* <p>
* Given a method like this:
* <pre><code>
* {@literal @}ResponseMetered(name = "fancyName")
* {@literal @}ResponseMetered(name = "fancyName", level = ResponseMeteredLevel.ALL)
* public String fancyName(String name) {
* return "Sir Captain " + name;
* }
* </code></pre>
* <p>
* A meter for the defining class with the name {@code fancyName} will be created for 1xx/2xx/3xx/4xx/5xx responses
* and each time the {@code #fancyName(String)} method is invoked, the appropriate response meter will be marked.
* Meters for the defining class with the name {@code fancyName} will be created for response codes
* based on the ResponseMeteredLevel selected. Each time the {@code #fancyName(String)} method is invoked,
* the appropriate response meter will be marked.
*/
@Inherited
@Documented
Expand All @@ -36,4 +37,9 @@
* relative to the annotated class. When annotating a class, this must be {@code false}.
*/
boolean absolute() default false;

/**
* @return the ResponseMeteredLevel which decides which response code meters are marked.
*/
ResponseMeteredLevel level() default ResponseMeteredLevel.COARSE;
}
@@ -0,0 +1,23 @@
package com.codahale.metrics.annotation;

/**
* ResponseMeteredLevel is a parameter for the ResponseMetered annotation. The constants of this enumerated type
* decide what meters are included when a class or method is annotated with the ResponseMetered annotation.
*
*/
public enum ResponseMeteredLevel {
/**
* Will include meters for 1xx/2xx/3xx/4xx/5xx responses
*/
COARSE,

/**
* Will include meters for every response code
*/
DETAILED,

/**
* Will include meters for every response code in addition to top level 1xx/2xx/3xx/4xx/5xx responses
*/
ALL;
}
Expand Up @@ -9,6 +9,7 @@
import com.codahale.metrics.annotation.ExceptionMetered;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.ResponseMeteredLevel;
import com.codahale.metrics.annotation.Timed;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.model.ModelProcessor;
Expand All @@ -27,12 +28,18 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static com.codahale.metrics.MetricRegistry.name;
import static com.codahale.metrics.annotation.ResponseMeteredLevel.COARSE;
import static com.codahale.metrics.annotation.ResponseMeteredLevel.DETAILED;
import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL;

/**
* An application event listener that listens for Jersey application initialization to
Expand Down Expand Up @@ -126,19 +133,48 @@ public ExceptionMeterMetric(final MetricRegistry registry,
* different response codes
*/
private static class ResponseMeterMetric {
private static final Set<ResponseMeteredLevel> COARSE_METER_LEVELS = EnumSet.of(COARSE, ALL);
private static final Set<ResponseMeteredLevel> DETAILED_METER_LEVELS = EnumSet.of(DETAILED, ALL);
public final List<Meter> meters;
private final Map<Integer, Meter> responseCodeMeters;
private final MetricRegistry metricRegistry;
private final String metricName;
private final ResponseMeteredLevel level;

public ResponseMeterMetric(final MetricRegistry registry,
final ResourceMethod method,
final ResponseMetered responseMetered) {
final String metricName = chooseName(responseMetered.name(), responseMetered.absolute(), method);
this.meters = Collections.unmodifiableList(Arrays.asList(
this.metricName = chooseName(responseMetered.name(), responseMetered.absolute(), method);
this.level = responseMetered.level();
this.meters = COARSE_METER_LEVELS.contains(level) ?
Collections.unmodifiableList(Arrays.asList(
registry.meter(name(metricName, "1xx-responses")), // 1xx
registry.meter(name(metricName, "2xx-responses")), // 2xx
registry.meter(name(metricName, "3xx-responses")), // 3xx
registry.meter(name(metricName, "4xx-responses")), // 4xx
registry.meter(name(metricName, "5xx-responses")) // 5xx
));
)) : Collections.emptyList();
this.responseCodeMeters = DETAILED_METER_LEVELS.contains(level) ? new ConcurrentHashMap<>() : Collections.emptyMap();
this.metricRegistry = registry;
}

public void mark(int statusCode) {
if (DETAILED_METER_LEVELS.contains(level)) {
getResponseCodeMeter(statusCode).mark();
}

if (COARSE_METER_LEVELS.contains(level)) {
final int responseStatus = statusCode / 100;
if (responseStatus >= 1 && responseStatus <= 5) {
meters.get(responseStatus - 1).mark();
}
}
}

public Meter getResponseCodeMeter(int statusCode) {
dennyac marked this conversation as resolved.
Show resolved Hide resolved
return responseCodeMeters
.computeIfAbsent(statusCode, sc -> metricRegistry
.meter(name(metricName, String.format("%d-responses", sc))));
}
}

Expand Down Expand Up @@ -270,15 +306,10 @@ public void onEvent(RequestEvent event) {

if (metric != null) {
ContainerResponse containerResponse = event.getContainerResponse();
if (containerResponse == null) {
if (event.getException() != null) {
metric.meters.get(4).mark();
}
if (containerResponse == null && event.getException() != null) {
metric.mark(500);
} else {
final int responseStatus = containerResponse.getStatus() / 100;
if (responseStatus >= 1 && responseStatus <= 5) {
metric.meters.get(responseStatus - 1).mark();
}
metric.mark(containerResponse.getStatus());
}
}
}
Expand Down
Expand Up @@ -95,38 +95,73 @@ public void exceptionMeteredMethodsAreExceptionMetered() {
}

@Test
public void responseMeteredMethodsAreMetered() {
public void responseMeteredMethodsAreMeteredWithCoarseLevel() {
final Meter meter2xx = registry.meter(name(InstrumentedResource.class,
"response2xxMetered",
"responseMeteredCoarse",
"2xx-responses"));
final Meter meter4xx = registry.meter(name(InstrumentedResource.class,
"response4xxMetered",
"4xx-responses"));
final Meter meter5xx = registry.meter(name(InstrumentedResource.class,
"response5xxMetered",
"5xx-responses"));
final Meter meter200 = registry.meter(name(InstrumentedResource.class,
"responseMeteredCoarse",
"200-responses"));

assertThat(meter2xx.getCount()).isZero();
assertThat(target("response-2xx-metered")
assertThat(meter200.getCount()).isZero();
assertThat(target("response-metered-coarse")
.request()
.get().getStatus())
.isEqualTo(200);

assertThat(meter4xx.getCount()).isZero();
assertThat(target("response-4xx-metered")
assertThat(meter2xx.getCount()).isOne();
assertThat(meter200.getCount()).isZero();
}

@Test
public void responseMeteredMethodsAreMeteredWithDetailedLevel() {
final Meter meter2xx = registry.meter(name(InstrumentedResource.class,
"responseMeteredDetailed",
"2xx-responses"));
final Meter meter200 = registry.meter(name(InstrumentedResource.class,
"responseMeteredDetailed",
"200-responses"));
final Meter meter201 = registry.meter(name(InstrumentedResource.class,
"responseMeteredDetailed",
"201-responses"));

assertThat(meter2xx.getCount()).isZero();
assertThat(meter200.getCount()).isZero();
assertThat(meter201.getCount()).isZero();
assertThat(target("response-metered-detailed")
.request()
.get().getStatus())
.isEqualTo(200);
assertThat(target("response-metered-detailed")
.queryParam("status_code", 201)
.request()
.get().getStatus())
.isEqualTo(400);
.isEqualTo(201);

assertThat(meter5xx.getCount()).isZero();
assertThat(target("response-5xx-metered")
assertThat(meter2xx.getCount()).isZero();
assertThat(meter200.getCount()).isOne();
assertThat(meter201.getCount()).isOne();
}

@Test
public void responseMeteredMethodsAreMeteredWithAllLevel() {
final Meter meter2xx = registry.meter(name(InstrumentedResource.class,
"responseMeteredAll",
"2xx-responses"));
final Meter meter200 = registry.meter(name(InstrumentedResource.class,
"responseMeteredAll",
"200-responses"));

assertThat(meter2xx.getCount()).isZero();
assertThat(meter200.getCount()).isZero();
assertThat(target("response-metered-all")
.request()
.get().getStatus())
.isEqualTo(500);
.isEqualTo(200);

assertThat(meter2xx.getCount()).isEqualTo(1);
assertThat(meter4xx.getCount()).isEqualTo(1);
assertThat(meter5xx.getCount()).isEqualTo(1);
assertThat(meter2xx.getCount()).isOne();
assertThat(meter200.getCount()).isOne();
}

@Test
Expand Down
Expand Up @@ -126,14 +126,21 @@ public void responseMeteredUnmappedExceptionPerClassMethodsAreMetered() {

@Test
public void subresourcesFromLocatorsRegisterMetrics() {
final Meter meter2xx = registry.meter(name(InstrumentedSubResourceResponseMeteredPerClass.class,
"responseMeteredPerClass",
"2xx-responses"));
final Meter meter201 = registry.meter(name(InstrumentedSubResourceResponseMeteredPerClass.class,
dennyac marked this conversation as resolved.
Show resolved Hide resolved
"responseMeteredPerClass",
"200-responses"));

assertThat(meter2xx.getCount()).isZero();
assertThat(meter2xx.getCount()).isZero();
dennyac marked this conversation as resolved.
Show resolved Hide resolved
assertThat(target("subresource/responseMeteredPerClass")
.request()
.get().getStatus())
.isEqualTo(200);

final Meter meter = registry.meter(name(InstrumentedSubResourceResponseMeteredPerClass.class,
"responseMeteredPerClass",
"2xx-responses"));
assertThat(meter.getCount()).isEqualTo(1);
assertThat(meter2xx.getCount()).isOne();
assertThat(meter2xx.getCount()).isOne();
dennyac marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Up @@ -14,6 +14,10 @@
import javax.ws.rs.core.Response;
import java.io.IOException;

import static com.codahale.metrics.annotation.ResponseMeteredLevel.COARSE;
import static com.codahale.metrics.annotation.ResponseMeteredLevel.DETAILED;
import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL;

@Path("/")
@Produces(MediaType.TEXT_PLAIN)
public class InstrumentedResource {
Expand Down Expand Up @@ -42,24 +46,24 @@ public String exceptionMetered(@QueryParam("splode") @DefaultValue("false") bool
}

@GET
@ResponseMetered
@Path("/response-2xx-metered")
public Response response2xxMetered() {
return Response.ok().build();
@ResponseMetered(level = DETAILED)
@Path("/response-metered-detailed")
public Response responseMeteredDetailed(@QueryParam("status_code") @DefaultValue("200") int statusCode) {
return Response.status(Response.Status.fromStatusCode(statusCode)).build();
}

@GET
@ResponseMetered
@Path("/response-4xx-metered")
public Response response4xxMetered() {
return Response.status(Response.Status.BAD_REQUEST).build();
@ResponseMetered(level = COARSE)
@Path("/response-metered-coarse")
public Response responseMeteredCoarse(@QueryParam("status_code") @DefaultValue("200") int statusCode) {
return Response.status(Response.Status.fromStatusCode(statusCode)).build();
}

@GET
@ResponseMetered
@Path("/response-5xx-metered")
public Response response5xxMetered() {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
@ResponseMetered(level = ALL)
@Path("/response-metered-all")
public Response responseMeteredAll(@QueryParam("status_code") @DefaultValue("200") int statusCode) {
return Response.status(Response.Status.fromStatusCode(statusCode)).build();
}

@Path("/subresource")
Expand Down
@@ -1,13 +1,17 @@
package com.codahale.metrics.jersey2.resources;

import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.ResponseMeteredLevel;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@ResponseMetered
import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL;

@ResponseMetered(level = ALL)
@Produces(MediaType.TEXT_PLAIN)
public class InstrumentedSubResourceResponseMeteredPerClass {
@GET
Expand Down