From 8b5286c78d098d976318d0cfc67b072a521336bd Mon Sep 17 00:00:00 2001 From: Jochen Schalanda Date: Sun, 18 Dec 2022 00:12:32 +0100 Subject: [PATCH] Add support for Jersey 3.1.x (#3041) --- metrics-jersey31/pom.xml | 116 ++++ ...ntedResourceMethodApplicationListener.java | 518 ++++++++++++++++++ .../metrics/jersey31/MetricsFeature.java | 97 ++++ .../CustomReservoirImplementationTest.java | 44 ++ .../SingletonFilterMetricsJerseyTest.java | 162 ++++++ ...icsExceptionMeteredPerClassJerseyTest.java | 98 ++++ .../jersey31/SingletonMetricsJerseyTest.java | 156 ++++++ ...letonMetricsMeteredPerClassJerseyTest.java | 66 +++ ...ricsResponseMeteredPerClassJerseyTest.java | 139 +++++ ...ngletonMetricsTimedPerClassJerseyTest.java | 66 +++ .../metrics/jersey31/TestClock.java | 13 + .../jersey31/exception/TestException.java | 9 + .../exception/mapper/TestExceptionMapper.java | 14 + .../InstrumentedFilteredResource.java | 61 +++ .../resources/InstrumentedResource.java | 69 +++ ...entedResourceExceptionMeteredPerClass.java | 32 ++ .../InstrumentedResourceMeteredPerClass.java | 25 + ...mentedResourceResponseMeteredPerClass.java | 58 ++ .../InstrumentedResourceTimedPerClass.java | 25 + .../resources/InstrumentedSubResource.java | 19 + ...edSubResourceExceptionMeteredPerClass.java | 24 + ...nstrumentedSubResourceMeteredPerClass.java | 17 + ...tedSubResourceResponseMeteredPerClass.java | 18 + .../InstrumentedSubResourceTimedPerClass.java | 17 + .../jersey31/resources/TestRequestFilter.java | 21 + pom.xml | 1 + 26 files changed, 1885 insertions(+) create mode 100644 metrics-jersey31/pom.xml create mode 100644 metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/InstrumentedResourceMethodApplicationListener.java create mode 100644 metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/MetricsFeature.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/CustomReservoirImplementationTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonFilterMetricsJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsExceptionMeteredPerClassJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsMeteredPerClassJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsResponseMeteredPerClassJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsTimedPerClassJerseyTest.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/TestClock.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/TestException.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/mapper/TestExceptionMapper.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedFilteredResource.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResource.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceExceptionMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceResponseMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceTimedPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResource.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceExceptionMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceResponseMeteredPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceTimedPerClass.java create mode 100644 metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/TestRequestFilter.java diff --git a/metrics-jersey31/pom.xml b/metrics-jersey31/pom.xml new file mode 100644 index 0000000000..4b161aa09f --- /dev/null +++ b/metrics-jersey31/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.14-SNAPSHOT + + + metrics-jersey31 + Metrics Integration for Jersey 3.1.x + bundle + + A set of class providing Metrics integration for Jersey 3.1.x, the reference JAX-RS + implementation. + + + + com.codahale.metrics.jersey31 + 3.1.0 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + org.glassfish.jersey + jersey-bom + ${jersey.version} + pom + import + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + jakarta.inject + jakarta.inject-api + 2.0.1 + + + + + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-annotation + + + org.glassfish.jersey.core + jersey-server + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + + + junit + junit + 4.13.1 + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + org.glassfish.jersey.inject + jersey-hk2 + test + + + org.glassfish.jersey.core + jersey-client + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + test + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + diff --git a/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/InstrumentedResourceMethodApplicationListener.java b/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/InstrumentedResourceMethodApplicationListener.java new file mode 100644 index 0000000000..4f14f116f6 --- /dev/null +++ b/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/InstrumentedResourceMethodApplicationListener.java @@ -0,0 +1,518 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Clock; +import com.codahale.metrics.ExponentiallyDecayingReservoir; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Reservoir; +import com.codahale.metrics.Timer; +import com.codahale.metrics.annotation.ExceptionMetered; +import com.codahale.metrics.annotation.Metered; +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.ext.Provider; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.model.ModelProcessor; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.model.ResourceModel; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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; + +/** + * An application event listener that listens for Jersey application initialization to + * be finished, then creates a map of resource method that have metrics annotations. + *

+ * Finally, it listens for method start events, and returns a {@link RequestEventListener} + * that updates the relevant metric for suitably annotated methods when it gets the + * request events indicating that the method is about to be invoked, or just got done + * being invoked. + */ +@Provider +public class InstrumentedResourceMethodApplicationListener implements ApplicationEventListener, ModelProcessor { + + private static final String[] REQUEST_FILTERING = {"request", "filtering"}; + private static final String[] RESPONSE_FILTERING = {"response", "filtering"}; + private static final String TOTAL = "total"; + + private final MetricRegistry metrics; + private final ConcurrentMap timers = new ConcurrentHashMap<>(); + private final ConcurrentMap meters = new ConcurrentHashMap<>(); + private final ConcurrentMap exceptionMeters = new ConcurrentHashMap<>(); + private final ConcurrentMap responseMeters = new ConcurrentHashMap<>(); + + private final Clock clock; + private final boolean trackFilters; + private final Supplier reservoirSupplier; + + /** + * Construct an application event listener using the given metrics registry. + *

+ * When using this constructor, the {@link InstrumentedResourceMethodApplicationListener} + * should be added to a Jersey {@code ResourceConfig} as a singleton. + * + * @param metrics a {@link MetricRegistry} + */ + public InstrumentedResourceMethodApplicationListener(final MetricRegistry metrics) { + this(metrics, Clock.defaultClock(), false); + } + + /** + * Constructs a custom application listener. + * + * @param metrics the metrics registry where the metrics will be stored + * @param clock the {@link Clock} to track time (used mostly in testing) in timers + * @param trackFilters whether the processing time for request and response filters should be tracked + */ + public InstrumentedResourceMethodApplicationListener(final MetricRegistry metrics, final Clock clock, + final boolean trackFilters) { + this(metrics, clock, trackFilters, ExponentiallyDecayingReservoir::new); + } + + /** + * Constructs a custom application listener. + * + * @param metrics the metrics registry where the metrics will be stored + * @param clock the {@link Clock} to track time (used mostly in testing) in timers + * @param trackFilters whether the processing time for request and response filters should be tracked + * @param reservoirSupplier Supplier for creating the {@link Reservoir} for {@link Timer timers}. + */ + public InstrumentedResourceMethodApplicationListener(final MetricRegistry metrics, final Clock clock, + final boolean trackFilters, + final Supplier reservoirSupplier) { + this.metrics = metrics; + this.clock = clock; + this.trackFilters = trackFilters; + this.reservoirSupplier = reservoirSupplier; + } + + /** + * A private class to maintain the metric for a method annotated with the + * {@link ExceptionMetered} annotation, which needs to maintain both a meter + * and a cause for which the meter should be updated. + */ + private static class ExceptionMeterMetric { + public final Meter meter; + public final Class cause; + + public ExceptionMeterMetric(final MetricRegistry registry, + final ResourceMethod method, + final ExceptionMetered exceptionMetered) { + final String name = chooseName(exceptionMetered.name(), + exceptionMetered.absolute(), method, ExceptionMetered.DEFAULT_NAME_SUFFIX); + this.meter = registry.meter(name); + this.cause = exceptionMetered.cause(); + } + } + + /** + * A private class to maintain the metrics for a method annotated with the + * {@link ResponseMetered} annotation, which needs to maintain meters for + * different response codes + */ + private static class ResponseMeterMetric { + public final List meters; + + 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( + 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 + )); + } + } + + private static class TimerRequestEventListener implements RequestEventListener { + + private final ConcurrentMap timers; + private final Clock clock; + private final long start; + private Timer.Context resourceMethodStartContext; + private Timer.Context requestMatchedContext; + private Timer.Context responseFiltersStartContext; + + public TimerRequestEventListener(final ConcurrentMap timers, final Clock clock) { + this.timers = timers; + this.clock = clock; + start = clock.getTick(); + } + + @Override + public void onEvent(RequestEvent event) { + switch (event.getType()) { + case RESOURCE_METHOD_START: + resourceMethodStartContext = context(event); + break; + case REQUEST_MATCHED: + requestMatchedContext = context(event); + break; + case RESP_FILTERS_START: + responseFiltersStartContext = context(event); + break; + case RESOURCE_METHOD_FINISHED: + if (resourceMethodStartContext != null) { + resourceMethodStartContext.close(); + } + break; + case REQUEST_FILTERED: + if (requestMatchedContext != null) { + requestMatchedContext.close(); + } + break; + case RESP_FILTERS_FINISHED: + if (responseFiltersStartContext != null) { + responseFiltersStartContext.close(); + } + break; + case FINISHED: + if (requestMatchedContext != null && responseFiltersStartContext != null) { + final Timer timer = timer(event); + if (timer != null) { + timer.update(clock.getTick() - start, TimeUnit.NANOSECONDS); + } + } + break; + default: + break; + } + } + + private Timer timer(RequestEvent event) { + final ResourceMethod resourceMethod = event.getUriInfo().getMatchedResourceMethod(); + if (resourceMethod == null) { + return null; + } + return timers.get(new EventTypeAndMethod(event.getType(), resourceMethod.getInvocable().getDefinitionMethod())); + } + + private Timer.Context context(RequestEvent event) { + final Timer timer = timer(event); + return timer != null ? timer.time() : null; + } + } + + private static class MeterRequestEventListener implements RequestEventListener { + private final ConcurrentMap meters; + + public MeterRequestEventListener(final ConcurrentMap meters) { + this.meters = meters; + } + + @Override + public void onEvent(RequestEvent event) { + if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) { + final Meter meter = this.meters.get(event.getUriInfo().getMatchedResourceMethod().getInvocable().getDefinitionMethod()); + if (meter != null) { + meter.mark(); + } + } + } + } + + private static class ExceptionMeterRequestEventListener implements RequestEventListener { + private final ConcurrentMap exceptionMeters; + + public ExceptionMeterRequestEventListener(final ConcurrentMap exceptionMeters) { + this.exceptionMeters = exceptionMeters; + } + + @Override + public void onEvent(RequestEvent event) { + if (event.getType() == RequestEvent.Type.ON_EXCEPTION) { + final ResourceMethod method = event.getUriInfo().getMatchedResourceMethod(); + final ExceptionMeterMetric metric = (method != null) ? + this.exceptionMeters.get(method.getInvocable().getDefinitionMethod()) : null; + + if (metric != null) { + if (metric.cause.isAssignableFrom(event.getException().getClass()) || + (event.getException().getCause() != null && + metric.cause.isAssignableFrom(event.getException().getCause().getClass()))) { + metric.meter.mark(); + } + } + } + } + } + + private static class ResponseMeterRequestEventListener implements RequestEventListener { + private final ConcurrentMap responseMeters; + + public ResponseMeterRequestEventListener(final ConcurrentMap responseMeters) { + this.responseMeters = responseMeters; + } + + @Override + public void onEvent(RequestEvent event) { + if (event.getType() == RequestEvent.Type.FINISHED) { + final ResourceMethod method = event.getUriInfo().getMatchedResourceMethod(); + final ResponseMeterMetric metric = (method != null) ? + this.responseMeters.get(method.getInvocable().getDefinitionMethod()) : null; + + if (metric != null) { + ContainerResponse containerResponse = event.getContainerResponse(); + if (containerResponse == null) { + if (event.getException() != null) { + metric.meters.get(4).mark(); + } + } else { + final int responseStatus = containerResponse.getStatus() / 100; + if (responseStatus >= 1 && responseStatus <= 5) { + metric.meters.get(responseStatus - 1).mark(); + } + } + } + } + } + } + + private static class ChainedRequestEventListener implements RequestEventListener { + private final RequestEventListener[] listeners; + + private ChainedRequestEventListener(final RequestEventListener... listeners) { + this.listeners = listeners; + } + + @Override + public void onEvent(final RequestEvent event) { + for (RequestEventListener listener : listeners) { + listener.onEvent(event); + } + } + } + + @Override + public void onEvent(ApplicationEvent event) { + if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) { + registerMetricsForModel(event.getResourceModel()); + } + } + + @Override + public ResourceModel processResourceModel(ResourceModel resourceModel, Configuration configuration) { + return resourceModel; + } + + @Override + public ResourceModel processSubResource(ResourceModel subResourceModel, Configuration configuration) { + registerMetricsForModel(subResourceModel); + return subResourceModel; + } + + private void registerMetricsForModel(ResourceModel resourceModel) { + for (final Resource resource : resourceModel.getResources()) { + + final Timed classLevelTimed = getClassLevelAnnotation(resource, Timed.class); + final Metered classLevelMetered = getClassLevelAnnotation(resource, Metered.class); + final ExceptionMetered classLevelExceptionMetered = getClassLevelAnnotation(resource, ExceptionMetered.class); + final ResponseMetered classLevelResponseMetered = getClassLevelAnnotation(resource, ResponseMetered.class); + + for (final ResourceMethod method : resource.getAllMethods()) { + registerTimedAnnotations(method, classLevelTimed); + registerMeteredAnnotations(method, classLevelMetered); + registerExceptionMeteredAnnotations(method, classLevelExceptionMetered); + registerResponseMeteredAnnotations(method, classLevelResponseMetered); + } + + for (final Resource childResource : resource.getChildResources()) { + + final Timed classLevelTimedChild = getClassLevelAnnotation(childResource, Timed.class); + final Metered classLevelMeteredChild = getClassLevelAnnotation(childResource, Metered.class); + final ExceptionMetered classLevelExceptionMeteredChild = getClassLevelAnnotation(childResource, ExceptionMetered.class); + final ResponseMetered classLevelResponseMeteredChild = getClassLevelAnnotation(childResource, ResponseMetered.class); + + for (final ResourceMethod method : childResource.getAllMethods()) { + registerTimedAnnotations(method, classLevelTimedChild); + registerMeteredAnnotations(method, classLevelMeteredChild); + registerExceptionMeteredAnnotations(method, classLevelExceptionMeteredChild); + registerResponseMeteredAnnotations(method, classLevelResponseMeteredChild); + } + } + } + } + + @Override + public RequestEventListener onRequest(final RequestEvent event) { + final RequestEventListener listener = new ChainedRequestEventListener( + new TimerRequestEventListener(timers, clock), + new MeterRequestEventListener(meters), + new ExceptionMeterRequestEventListener(exceptionMeters), + new ResponseMeterRequestEventListener(responseMeters)); + + return listener; + } + + private T getClassLevelAnnotation(final Resource resource, final Class annotationClazz) { + T annotation = null; + + for (final Class clazz : resource.getHandlerClasses()) { + annotation = clazz.getAnnotation(annotationClazz); + + if (annotation != null) { + break; + } + } + return annotation; + } + + private void registerTimedAnnotations(final ResourceMethod method, final Timed classLevelTimed) { + final Method definitionMethod = method.getInvocable().getDefinitionMethod(); + if (classLevelTimed != null) { + registerTimers(method, definitionMethod, classLevelTimed); + return; + } + + final Timed annotation = definitionMethod.getAnnotation(Timed.class); + if (annotation != null) { + registerTimers(method, definitionMethod, annotation); + } + } + + private void registerTimers(ResourceMethod method, Method definitionMethod, Timed annotation) { + timers.putIfAbsent(EventTypeAndMethod.requestMethodStart(definitionMethod), timerMetric(metrics, method, annotation)); + if (trackFilters) { + timers.putIfAbsent(EventTypeAndMethod.requestMatched(definitionMethod), timerMetric(metrics, method, annotation, REQUEST_FILTERING)); + timers.putIfAbsent(EventTypeAndMethod.respFiltersStart(definitionMethod), timerMetric(metrics, method, annotation, RESPONSE_FILTERING)); + timers.putIfAbsent(EventTypeAndMethod.finished(definitionMethod), timerMetric(metrics, method, annotation, TOTAL)); + } + } + + private void registerMeteredAnnotations(final ResourceMethod method, final Metered classLevelMetered) { + final Method definitionMethod = method.getInvocable().getDefinitionMethod(); + + if (classLevelMetered != null) { + meters.putIfAbsent(definitionMethod, meterMetric(metrics, method, classLevelMetered)); + return; + } + final Metered annotation = definitionMethod.getAnnotation(Metered.class); + + if (annotation != null) { + meters.putIfAbsent(definitionMethod, meterMetric(metrics, method, annotation)); + } + } + + private void registerExceptionMeteredAnnotations(final ResourceMethod method, final ExceptionMetered classLevelExceptionMetered) { + final Method definitionMethod = method.getInvocable().getDefinitionMethod(); + + if (classLevelExceptionMetered != null) { + exceptionMeters.putIfAbsent(definitionMethod, new ExceptionMeterMetric(metrics, method, classLevelExceptionMetered)); + return; + } + final ExceptionMetered annotation = definitionMethod.getAnnotation(ExceptionMetered.class); + + if (annotation != null) { + exceptionMeters.putIfAbsent(definitionMethod, new ExceptionMeterMetric(metrics, method, annotation)); + } + } + + private void registerResponseMeteredAnnotations(final ResourceMethod method, final ResponseMetered classLevelResponseMetered) { + final Method definitionMethod = method.getInvocable().getDefinitionMethod(); + + if (classLevelResponseMetered != null) { + responseMeters.putIfAbsent(definitionMethod, new ResponseMeterMetric(metrics, method, classLevelResponseMetered)); + return; + } + final ResponseMetered annotation = definitionMethod.getAnnotation(ResponseMetered.class); + + if (annotation != null) { + responseMeters.putIfAbsent(definitionMethod, new ResponseMeterMetric(metrics, method, annotation)); + } + } + + private Timer timerMetric(final MetricRegistry registry, + final ResourceMethod method, + final Timed timed, + final String... suffixes) { + final String name = chooseName(timed.name(), timed.absolute(), method, suffixes); + return registry.timer(name, () -> new Timer(reservoirSupplier.get(), clock)); + } + + private Meter meterMetric(final MetricRegistry registry, + final ResourceMethod method, + final Metered metered) { + final String name = chooseName(metered.name(), metered.absolute(), method); + return registry.meter(name, () -> new Meter(clock)); + } + + protected static String chooseName(final String explicitName, final boolean absolute, final ResourceMethod method, + final String... suffixes) { + final Method definitionMethod = method.getInvocable().getDefinitionMethod(); + final String metricName; + if (explicitName != null && !explicitName.isEmpty()) { + metricName = absolute ? explicitName : name(definitionMethod.getDeclaringClass(), explicitName); + } else { + metricName = name(definitionMethod.getDeclaringClass(), definitionMethod.getName()); + } + return name(metricName, suffixes); + } + + private static class EventTypeAndMethod { + + private final RequestEvent.Type type; + private final Method method; + + private EventTypeAndMethod(RequestEvent.Type type, Method method) { + this.type = type; + this.method = method; + } + + static EventTypeAndMethod requestMethodStart(Method method) { + return new EventTypeAndMethod(RequestEvent.Type.RESOURCE_METHOD_START, method); + } + + static EventTypeAndMethod requestMatched(Method method) { + return new EventTypeAndMethod(RequestEvent.Type.REQUEST_MATCHED, method); + } + + static EventTypeAndMethod respFiltersStart(Method method) { + return new EventTypeAndMethod(RequestEvent.Type.RESP_FILTERS_START, method); + } + + static EventTypeAndMethod finished(Method method) { + return new EventTypeAndMethod(RequestEvent.Type.FINISHED, method); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EventTypeAndMethod that = (EventTypeAndMethod) o; + + if (type != that.type) { + return false; + } + return method.equals(that.method); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + method.hashCode(); + return result; + } + } +} diff --git a/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/MetricsFeature.java b/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/MetricsFeature.java new file mode 100644 index 0000000000..87ae86eeed --- /dev/null +++ b/metrics-jersey31/src/main/java/io/dropwizard/metrics/jersey31/MetricsFeature.java @@ -0,0 +1,97 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Clock; +import com.codahale.metrics.ExponentiallyDecayingReservoir; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Reservoir; +import com.codahale.metrics.SharedMetricRegistries; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; + +import java.util.function.Supplier; + +/** + * A {@link Feature} which registers a {@link InstrumentedResourceMethodApplicationListener} + * for recording request events. + */ +public class MetricsFeature implements Feature { + + private final MetricRegistry registry; + private final Clock clock; + private final boolean trackFilters; + private final Supplier reservoirSupplier; + + /* + * @param registry the metrics registry where the metrics will be stored + */ + public MetricsFeature(MetricRegistry registry) { + this(registry, Clock.defaultClock()); + } + + /* + * @param registry the metrics registry where the metrics will be stored + * @param reservoirSupplier Supplier for creating the {@link Reservoir} for {@link Timer timers}. + */ + public MetricsFeature(MetricRegistry registry, Supplier reservoirSupplier) { + this(registry, Clock.defaultClock(), false, reservoirSupplier); + } + + /* + * @param registry the metrics registry where the metrics will be stored + * @param clock the {@link Clock} to track time (used mostly in testing) in timers + */ + public MetricsFeature(MetricRegistry registry, Clock clock) { + this(registry, clock, false); + } + + /* + * @param registry the metrics registry where the metrics will be stored + * @param clock the {@link Clock} to track time (used mostly in testing) in timers + * @param trackFilters whether the processing time for request and response filters should be tracked + */ + public MetricsFeature(MetricRegistry registry, Clock clock, boolean trackFilters) { + this(registry, clock, trackFilters, ExponentiallyDecayingReservoir::new); + } + + /* + * @param registry the metrics registry where the metrics will be stored + * @param clock the {@link Clock} to track time (used mostly in testing) in timers + * @param trackFilters whether the processing time for request and response filters should be tracked + * @param reservoirSupplier Supplier for creating the {@link Reservoir} for {@link Timer timers}. + */ + public MetricsFeature(MetricRegistry registry, Clock clock, boolean trackFilters, Supplier reservoirSupplier) { + this.registry = registry; + this.clock = clock; + this.trackFilters = trackFilters; + this.reservoirSupplier = reservoirSupplier; + } + + public MetricsFeature(String registryName) { + this(SharedMetricRegistries.getOrCreate(registryName)); + } + + /** + * A call-back method called when the feature is to be enabled in a given + * runtime configuration scope. + *

+ * The responsibility of the feature is to properly update the supplied runtime configuration context + * and return {@code true} if the feature was successfully enabled or {@code false} otherwise. + *

+ * Note that under some circumstances the feature may decide not to enable itself, which + * is indicated by returning {@code false}. In such case the configuration context does + * not add the feature to the collection of enabled features and a subsequent call to + * {@link jakarta.ws.rs.core.Configuration#isEnabled(jakarta.ws.rs.core.Feature)} or + * {@link jakarta.ws.rs.core.Configuration#isEnabled(Class)} method + * would return {@code false}. + *

+ * + * @param context configurable context in which the feature should be enabled. + * @return {@code true} if the feature was successfully enabled, {@code false} + * otherwise. + */ + @Override + public boolean configure(FeatureContext context) { + context.register(new InstrumentedResourceMethodApplicationListener(registry, clock, trackFilters, reservoirSupplier)); + return true; + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/CustomReservoirImplementationTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/CustomReservoirImplementationTest.java new file mode 100644 index 0000000000..60190d0564 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/CustomReservoirImplementationTest.java @@ -0,0 +1,44 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.codahale.metrics.UniformReservoir; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResourceTimedPerClass; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; + +public class CustomReservoirImplementationTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + return new ResourceConfig() + .register(new MetricsFeature(this.registry, UniformReservoir::new)) + .register(InstrumentedResourceTimedPerClass.class); + } + + @Test + public void timerHistogramIsUsingCustomReservoirImplementation() { + assertThat(target("timedPerClass").request().get(String.class)).isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedResourceTimedPerClass.class, "timedPerClass")); + assertThat(timer) + .extracting("histogram") + .extracting("reservoir") + .isInstanceOf(UniformReservoir.class); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonFilterMetricsJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonFilterMetricsJerseyTest.java new file mode 100644 index 0000000000..e4bfc10837 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonFilterMetricsJerseyTest.java @@ -0,0 +1,162 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import io.dropwizard.metrics.jersey31.resources.InstrumentedFilteredResource; +import io.dropwizard.metrics.jersey31.resources.TestRequestFilter; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Before; +import org.junit.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link ResourceConfig} with filter tracking + */ +public class SingletonFilterMetricsJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + private TestClock testClock; + + @Override + protected Application configure() { + registry = new MetricRegistry(); + testClock = new TestClock(); + ResourceConfig config = new ResourceConfig(); + config = config.register(new MetricsFeature(this.registry, testClock, true)); + config = config.register(new TestRequestFilter(testClock)); + config = config.register(new InstrumentedFilteredResource(testClock)); + return config; + } + + @Before + public void resetClock() { + testClock.tick = 0; + } + + @Test + public void timedMethodsAreTimed() { + assertThat(target("timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "timed")); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(1); + } + + @Test + public void explicitNamesAreTimed() { + assertThat(target("named") + .request() + .get(String.class)) + .isEqualTo("fancy"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "fancyName")); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(1); + } + + @Test + public void absoluteNamesAreTimed() { + assertThat(target("absolute") + .request() + .get(String.class)) + .isEqualTo("absolute"); + + final Timer timer = registry.timer("absolutelyFancy"); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(1); + } + + @Test + public void requestFiltersOfTimedMethodsAreTimed() { + assertThat(target("timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "timed", "request", "filtering")); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(4); + } + + @Test + public void responseFiltersOfTimedMethodsAreTimed() { + assertThat(target("timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "timed", "response", "filtering")); + + assertThat(timer.getCount()).isEqualTo(1); + } + + @Test + public void totalTimeOfTimedMethodsIsTimed() { + assertThat(target("timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "timed", "total")); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(5); + } + + @Test + public void requestFiltersOfNamedMethodsAreTimed() { + assertThat(target("named") + .request() + .get(String.class)) + .isEqualTo("fancy"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.class, "fancyName", "request", "filtering")); + + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(4); + } + + @Test + public void requestFiltersOfAbsoluteMethodsAreTimed() { + assertThat(target("absolute") + .request() + .get(String.class)) + .isEqualTo("absolute"); + + final Timer timer = registry.timer(name("absolutelyFancy", "request", "filtering")); + assertThat(timer.getCount()).isEqualTo(1); + assertThat(timer.getSnapshot().getValues()[0]).isEqualTo(4); + } + + @Test + public void subResourcesFromLocatorsRegisterMetrics() { + assertThat(target("subresource/timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedFilteredResource.InstrumentedFilteredSubResource.class, + "timed")); + assertThat(timer.getCount()).isEqualTo(1); + + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsExceptionMeteredPerClassJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsExceptionMeteredPerClassJerseyTest.java new file mode 100644 index 0000000000..d84c835297 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsExceptionMeteredPerClassJerseyTest.java @@ -0,0 +1,98 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResourceExceptionMeteredPerClass; +import io.dropwizard.metrics.jersey31.resources.InstrumentedSubResourceExceptionMeteredPerClass; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link ResourceConfig} + */ +public class SingletonMetricsExceptionMeteredPerClassJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + ResourceConfig config = new ResourceConfig(); + + config = config.register(new MetricsFeature(this.registry)); + config = config.register(InstrumentedResourceExceptionMeteredPerClass.class); + + return config; + } + + @Test + public void exceptionMeteredMethodsAreExceptionMetered() { + final Meter meter = registry.meter(name(InstrumentedResourceExceptionMeteredPerClass.class, + "exceptionMetered", + "exceptions")); + + assertThat(target("exception-metered") + .request() + .get(String.class)) + .isEqualTo("fuh"); + + assertThat(meter.getCount()).isZero(); + + try { + target("exception-metered") + .queryParam("splode", true) + .request() + .get(String.class); + + failBecauseExceptionWasNotThrown(ProcessingException.class); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + } + + assertThat(meter.getCount()).isEqualTo(1); + } + + @Test + public void subresourcesFromLocatorsRegisterMetrics() { + final Meter meter = registry.meter(name(InstrumentedSubResourceExceptionMeteredPerClass.class, + "exceptionMetered", + "exceptions")); + + assertThat(target("subresource/exception-metered") + .request() + .get(String.class)) + .isEqualTo("fuh"); + + assertThat(meter.getCount()).isZero(); + + try { + target("subresource/exception-metered") + .queryParam("splode", true) + .request() + .get(String.class); + + failBecauseExceptionWasNotThrown(ProcessingException.class); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + } + + assertThat(meter.getCount()).isEqualTo(1); + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsJerseyTest.java new file mode 100644 index 0000000000..11c0d4ce9a --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsJerseyTest.java @@ -0,0 +1,156 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResource; +import io.dropwizard.metrics.jersey31.resources.InstrumentedSubResource; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link org.glassfish.jersey.server.ResourceConfig} + */ +public class SingletonMetricsJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + ResourceConfig config = new ResourceConfig(); + config = config.register(new MetricsFeature(this.registry)); + config = config.register(InstrumentedResource.class); + + return config; + } + + @Test + public void timedMethodsAreTimed() { + assertThat(target("timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedResource.class, "timed")); + + assertThat(timer.getCount()).isEqualTo(1); + } + + @Test + public void meteredMethodsAreMetered() { + assertThat(target("metered") + .request() + .get(String.class)) + .isEqualTo("woo"); + + final Meter meter = registry.meter(name(InstrumentedResource.class, "metered")); + assertThat(meter.getCount()).isEqualTo(1); + } + + @Test + public void exceptionMeteredMethodsAreExceptionMetered() { + final Meter meter = registry.meter(name(InstrumentedResource.class, + "exceptionMetered", + "exceptions")); + + assertThat(target("exception-metered") + .request() + .get(String.class)) + .isEqualTo("fuh"); + + assertThat(meter.getCount()).isZero(); + + try { + target("exception-metered") + .queryParam("splode", true) + .request() + .get(String.class); + + failBecauseExceptionWasNotThrown(ProcessingException.class); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + } + + assertThat(meter.getCount()).isEqualTo(1); + } + + @Test + public void responseMeteredMethodsAreMetered() { + final Meter meter2xx = registry.meter(name(InstrumentedResource.class, + "response2xxMetered", + "2xx-responses")); + final Meter meter4xx = registry.meter(name(InstrumentedResource.class, + "response4xxMetered", + "4xx-responses")); + final Meter meter5xx = registry.meter(name(InstrumentedResource.class, + "response5xxMetered", + "5xx-responses")); + + assertThat(meter2xx.getCount()).isZero(); + assertThat(target("response-2xx-metered") + .request() + .get().getStatus()) + .isEqualTo(200); + + assertThat(meter4xx.getCount()).isZero(); + assertThat(target("response-4xx-metered") + .request() + .get().getStatus()) + .isEqualTo(400); + + assertThat(meter5xx.getCount()).isZero(); + assertThat(target("response-5xx-metered") + .request() + .get().getStatus()) + .isEqualTo(500); + + assertThat(meter2xx.getCount()).isEqualTo(1); + assertThat(meter4xx.getCount()).isEqualTo(1); + assertThat(meter5xx.getCount()).isEqualTo(1); + } + + @Test + public void testResourceNotFound() { + final Response response = target().path("not-found").request().get(); + assertThat(response.getStatus()).isEqualTo(404); + + try { + target().path("not-found").request().get(ClientResponse.class); + failBecauseExceptionWasNotThrown(NotFoundException.class); + } catch (NotFoundException e) { + assertThat(e.getMessage()).isEqualTo("HTTP 404 Not Found"); + } + } + + @Test + public void subresourcesFromLocatorsRegisterMetrics() { + assertThat(target("subresource/timed") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedSubResource.class, "timed")); + assertThat(timer.getCount()).isEqualTo(1); + + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsMeteredPerClassJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsMeteredPerClassJerseyTest.java new file mode 100644 index 0000000000..d3e89de233 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsMeteredPerClassJerseyTest.java @@ -0,0 +1,66 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResourceMeteredPerClass; +import io.dropwizard.metrics.jersey31.resources.InstrumentedSubResourceMeteredPerClass; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link ResourceConfig} + */ +public class SingletonMetricsMeteredPerClassJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + ResourceConfig config = new ResourceConfig(); + + config = config.register(new MetricsFeature(this.registry)); + config = config.register(InstrumentedResourceMeteredPerClass.class); + + return config; + } + + @Test + public void meteredPerClassMethodsAreMetered() { + assertThat(target("meteredPerClass") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Meter meter = registry.meter(name(InstrumentedResourceMeteredPerClass.class, "meteredPerClass")); + + assertThat(meter.getCount()).isEqualTo(1); + } + + @Test + public void subresourcesFromLocatorsRegisterMetrics() { + assertThat(target("subresource/meteredPerClass") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Meter meter = registry.meter(name(InstrumentedSubResourceMeteredPerClass.class, "meteredPerClass")); + assertThat(meter.getCount()).isEqualTo(1); + + } + + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsResponseMeteredPerClassJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsResponseMeteredPerClassJerseyTest.java new file mode 100644 index 0000000000..7836bb9ffd --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsResponseMeteredPerClassJerseyTest.java @@ -0,0 +1,139 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.metrics.jersey31.exception.mapper.TestExceptionMapper; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResourceResponseMeteredPerClass; +import io.dropwizard.metrics.jersey31.resources.InstrumentedSubResourceResponseMeteredPerClass; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link ResourceConfig} + */ +public class SingletonMetricsResponseMeteredPerClassJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + + ResourceConfig config = new ResourceConfig(); + + config = config.register(new MetricsFeature(this.registry)); + config = config.register(InstrumentedResourceResponseMeteredPerClass.class); + config = config.register(new TestExceptionMapper()); + + return config; + } + + @Test + public void responseMetered2xxPerClassMethodsAreMetered() { + assertThat(target("responseMetered2xxPerClass") + .request() + .get().getStatus()) + .isEqualTo(200); + + final Meter meter2xx = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMetered2xxPerClass", + "2xx-responses")); + + assertThat(meter2xx.getCount()).isEqualTo(1); + } + + @Test + public void responseMetered4xxPerClassMethodsAreMetered() { + assertThat(target("responseMetered4xxPerClass") + .request() + .get().getStatus()) + .isEqualTo(400); + assertThat(target("responseMeteredBadRequestPerClass") + .request() + .get().getStatus()) + .isEqualTo(400); + + final Meter meter4xx = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMetered4xxPerClass", + "4xx-responses")); + final Meter meterException4xx = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMeteredBadRequestPerClass", + "4xx-responses")); + + assertThat(meter4xx.getCount()).isEqualTo(1); + assertThat(meterException4xx.getCount()).isEqualTo(1); + } + + @Test + public void responseMetered5xxPerClassMethodsAreMetered() { + assertThat(target("responseMetered5xxPerClass") + .request() + .get().getStatus()) + .isEqualTo(500); + + final Meter meter5xx = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMetered5xxPerClass", + "5xx-responses")); + + assertThat(meter5xx.getCount()).isEqualTo(1); + } + + @Test + public void responseMeteredMappedExceptionPerClassMethodsAreMetered() { + assertThat(target("responseMeteredTestExceptionPerClass") + .request() + .get().getStatus()) + .isEqualTo(500); + + final Meter meterTestException = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMeteredTestExceptionPerClass", + "5xx-responses")); + + assertThat(meterTestException.getCount()).isEqualTo(1); + } + + @Test + public void responseMeteredUnmappedExceptionPerClassMethodsAreMetered() { + try { + target("responseMeteredRuntimeExceptionPerClass") + .request() + .get(); + fail("expected RuntimeException"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(RuntimeException.class); + } + + final Meter meterException5xx = registry.meter(name(InstrumentedResourceResponseMeteredPerClass.class, + "responseMeteredRuntimeExceptionPerClass", + "5xx-responses")); + + assertThat(meterException5xx.getCount()).isEqualTo(1); + } + + @Test + public void subresourcesFromLocatorsRegisterMetrics() { + 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); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsTimedPerClassJerseyTest.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsTimedPerClassJerseyTest.java new file mode 100644 index 0000000000..a1e39ee674 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/SingletonMetricsTimedPerClassJerseyTest.java @@ -0,0 +1,66 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import io.dropwizard.metrics.jersey31.resources.InstrumentedResourceTimedPerClass; +import io.dropwizard.metrics.jersey31.resources.InstrumentedSubResourceTimedPerClass; +import jakarta.ws.rs.core.Application; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests registering {@link InstrumentedResourceMethodApplicationListener} as a singleton + * in a Jersey {@link ResourceConfig} + */ +public class SingletonMetricsTimedPerClassJerseyTest extends JerseyTest { + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private MetricRegistry registry; + + @Override + protected Application configure() { + this.registry = new MetricRegistry(); + + ResourceConfig config = new ResourceConfig(); + + config = config.register(new MetricsFeature(this.registry)); + config = config.register(InstrumentedResourceTimedPerClass.class); + + return config; + } + + @Test + public void timedPerClassMethodsAreTimed() { + assertThat(target("timedPerClass") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedResourceTimedPerClass.class, "timedPerClass")); + + assertThat(timer.getCount()).isEqualTo(1); + } + + @Test + public void subresourcesFromLocatorsRegisterMetrics() { + assertThat(target("subresource/timedPerClass") + .request() + .get(String.class)) + .isEqualTo("yay"); + + final Timer timer = registry.timer(name(InstrumentedSubResourceTimedPerClass.class, "timedPerClass")); + assertThat(timer.getCount()).isEqualTo(1); + + } + + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/TestClock.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/TestClock.java new file mode 100644 index 0000000000..b9d34e5cf9 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/TestClock.java @@ -0,0 +1,13 @@ +package io.dropwizard.metrics.jersey31; + +import com.codahale.metrics.Clock; + +public class TestClock extends Clock { + + public long tick; + + @Override + public long getTick() { + return tick; + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/TestException.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/TestException.java new file mode 100644 index 0000000000..1bf2427dcd --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/TestException.java @@ -0,0 +1,9 @@ +package io.dropwizard.metrics.jersey31.exception; + +public class TestException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public TestException(String message) { + super(message); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/mapper/TestExceptionMapper.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/mapper/TestExceptionMapper.java new file mode 100644 index 0000000000..a3ececef14 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/exception/mapper/TestExceptionMapper.java @@ -0,0 +1,14 @@ +package io.dropwizard.metrics.jersey31.exception.mapper; + +import io.dropwizard.metrics.jersey31.exception.TestException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class TestExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(TestException exception) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedFilteredResource.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedFilteredResource.java new file mode 100644 index 0000000000..6146c5f27a --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedFilteredResource.java @@ -0,0 +1,61 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.metrics.jersey31.TestClock; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedFilteredResource { + + private final TestClock testClock; + + public InstrumentedFilteredResource(TestClock testClock) { + this.testClock = testClock; + } + + @GET + @Timed + @Path("/timed") + public String timed() { + testClock.tick++; + return "yay"; + } + + @GET + @Timed(name = "fancyName") + @Path("/named") + public String named() { + testClock.tick++; + return "fancy"; + } + + @GET + @Timed(name = "absolutelyFancy", absolute = true) + @Path("/absolute") + public String absolute() { + testClock.tick++; + return "absolute"; + } + + @Path("/subresource") + public InstrumentedFilteredSubResource locateSubResource() { + return new InstrumentedFilteredSubResource(); + } + + @Produces(MediaType.TEXT_PLAIN) + public class InstrumentedFilteredSubResource { + + @GET + @Timed + @Path("/timed") + public String timed() { + testClock.tick += 2; + return "yay"; + } + + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResource.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResource.java new file mode 100644 index 0000000000..7d346777d1 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResource.java @@ -0,0 +1,69 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.ExceptionMetered; +import com.codahale.metrics.annotation.Metered; +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.io.IOException; + +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedResource { + @GET + @Timed + @Path("/timed") + public String timed() { + return "yay"; + } + + @GET + @Metered + @Path("/metered") + public String metered() { + return "woo"; + } + + @GET + @ExceptionMetered(cause = IOException.class) + @Path("/exception-metered") + public String exceptionMetered(@QueryParam("splode") @DefaultValue("false") boolean splode) throws IOException { + if (splode) { + throw new IOException("AUGH"); + } + return "fuh"; + } + + @GET + @ResponseMetered + @Path("/response-2xx-metered") + public Response response2xxMetered() { + return Response.ok().build(); + } + + @GET + @ResponseMetered + @Path("/response-4xx-metered") + public Response response4xxMetered() { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + @GET + @ResponseMetered + @Path("/response-5xx-metered") + public Response response5xxMetered() { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + @Path("/subresource") + public InstrumentedSubResource locateSubResource() { + return new InstrumentedSubResource(); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceExceptionMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceExceptionMeteredPerClass.java new file mode 100644 index 0000000000..449c777bb4 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceExceptionMeteredPerClass.java @@ -0,0 +1,32 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.ExceptionMetered; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import java.io.IOException; + +@ExceptionMetered(cause = IOException.class) +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedResourceExceptionMeteredPerClass { + + @GET + @Path("/exception-metered") + public String exceptionMetered(@QueryParam("splode") @DefaultValue("false") boolean splode) throws IOException { + if (splode) { + throw new IOException("AUGH"); + } + return "fuh"; + } + + @Path("/subresource") + public InstrumentedSubResourceExceptionMeteredPerClass locateSubResource() { + return new InstrumentedSubResourceExceptionMeteredPerClass(); + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceMeteredPerClass.java new file mode 100644 index 0000000000..f9f6804dc1 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceMeteredPerClass.java @@ -0,0 +1,25 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Metered; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Metered +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedResourceMeteredPerClass { + + @GET + @Path("/meteredPerClass") + public String meteredPerClass() { + return "yay"; + } + + @Path("/subresource") + public InstrumentedSubResourceMeteredPerClass locateSubResource() { + return new InstrumentedSubResourceMeteredPerClass(); + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceResponseMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceResponseMeteredPerClass.java new file mode 100644 index 0000000000..8c10ba1842 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceResponseMeteredPerClass.java @@ -0,0 +1,58 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.ResponseMetered; +import io.dropwizard.metrics.jersey31.exception.TestException; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@ResponseMetered +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedResourceResponseMeteredPerClass { + + @GET + @Path("/responseMetered2xxPerClass") + public Response responseMetered2xxPerClass() { + return Response.ok().build(); + } + + @GET + @Path("/responseMetered4xxPerClass") + public Response responseMetered4xxPerClass() { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + @GET + @Path("/responseMetered5xxPerClass") + public Response responseMetered5xxPerClass() { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + @GET + @Path("/responseMeteredBadRequestPerClass") + public String responseMeteredBadRequestPerClass() { + throw new BadRequestException(); + } + + @GET + @Path("/responseMeteredRuntimeExceptionPerClass") + public String responseMeteredRuntimeExceptionPerClass() { + throw new RuntimeException(); + } + + @GET + @Path("/responseMeteredTestExceptionPerClass") + public String responseMeteredTestExceptionPerClass() { + throw new TestException("test"); + } + + @Path("/subresource") + public InstrumentedSubResourceResponseMeteredPerClass locateSubResource() { + return new InstrumentedSubResourceResponseMeteredPerClass(); + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceTimedPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceTimedPerClass.java new file mode 100644 index 0000000000..fb45f288e8 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedResourceTimedPerClass.java @@ -0,0 +1,25 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Timed; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Timed +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedResourceTimedPerClass { + + @GET + @Path("/timedPerClass") + public String timedPerClass() { + return "yay"; + } + + @Path("/subresource") + public InstrumentedSubResourceTimedPerClass locateSubResource() { + return new InstrumentedSubResourceTimedPerClass(); + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResource.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResource.java new file mode 100644 index 0000000000..36c8e6a773 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResource.java @@ -0,0 +1,19 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Timed; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedSubResource { + + @GET + @Timed + @Path("/timed") + public String timed() { + return "yay"; + } + +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceExceptionMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceExceptionMeteredPerClass.java new file mode 100644 index 0000000000..e983ade805 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceExceptionMeteredPerClass.java @@ -0,0 +1,24 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.ExceptionMetered; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import java.io.IOException; + +@ExceptionMetered(cause = IOException.class) +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedSubResourceExceptionMeteredPerClass { + @GET + @Path("/exception-metered") + public String exceptionMetered(@QueryParam("splode") @DefaultValue("false") boolean splode) throws IOException { + if (splode) { + throw new IOException("AUGH"); + } + return "fuh"; + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceMeteredPerClass.java new file mode 100644 index 0000000000..5282641bb3 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceMeteredPerClass.java @@ -0,0 +1,17 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Metered; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Metered +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedSubResourceMeteredPerClass { + @GET + @Path("/meteredPerClass") + public String meteredPerClass() { + return "yay"; + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceResponseMeteredPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceResponseMeteredPerClass.java new file mode 100644 index 0000000000..31bbf6a85d --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceResponseMeteredPerClass.java @@ -0,0 +1,18 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.ResponseMetered; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@ResponseMetered +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedSubResourceResponseMeteredPerClass { + @GET + @Path("/responseMeteredPerClass") + public Response responseMeteredPerClass() { + return Response.status(Response.Status.OK).build(); + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceTimedPerClass.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceTimedPerClass.java new file mode 100644 index 0000000000..0c115f2136 --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/InstrumentedSubResourceTimedPerClass.java @@ -0,0 +1,17 @@ +package io.dropwizard.metrics.jersey31.resources; + +import com.codahale.metrics.annotation.Timed; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Timed +@Produces(MediaType.TEXT_PLAIN) +public class InstrumentedSubResourceTimedPerClass { + @GET + @Path("/timedPerClass") + public String timedPerClass() { + return "yay"; + } +} diff --git a/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/TestRequestFilter.java b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/TestRequestFilter.java new file mode 100644 index 0000000000..9ceb8ec49a --- /dev/null +++ b/metrics-jersey31/src/test/java/io/dropwizard/metrics/jersey31/resources/TestRequestFilter.java @@ -0,0 +1,21 @@ +package io.dropwizard.metrics.jersey31.resources; + +import io.dropwizard.metrics.jersey31.TestClock; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; + +import java.io.IOException; + +public class TestRequestFilter implements ContainerRequestFilter { + + private final TestClock testClock; + + public TestRequestFilter(TestClock testClock) { + this.testClock = testClock; + } + + @Override + public void filter(ContainerRequestContext containerRequestContext) throws IOException { + testClock.tick += 4; + } +} diff --git a/pom.xml b/pom.xml index be8b7cc2ec..0f5ccd890f 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ metrics-jdbi3 metrics-jersey2 metrics-jersey3 + metrics-jersey31 metrics-jetty9 metrics-jetty10 metrics-jetty11