package io.dropwizard.metrics.jersey2;
import static io.dropwizard.metrics.MetricRegistry.name;
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 io.dropwizard.metrics.annotation.ExceptionMetered;
import io.dropwizard.metrics.annotation.Metered;
import io.dropwizard.metrics.annotation.Timed;
import io.dropwizard.metrics.Meter;
import io.dropwizard.metrics.MetricName;
import io.dropwizard.metrics.MetricRegistry;
import io.dropwizard.metrics.Timer;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.core.Configuration;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* An application event listener that listens for Jersey application initialization to
* be finished, then creates a map of resource method that have metrics annotations.
* <p/>
* 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 final MetricRegistry metrics;
private ConcurrentMap<Method, Timer> timers = new ConcurrentHashMap<>();
private ConcurrentMap<Method, Meter> meters = new ConcurrentHashMap<>();
private ConcurrentMap<Method, ExceptionMeterMetric> exceptionMeters = new ConcurrentHashMap<>();
/**
* Construct an application event listener using the given metrics registry.
* <p/>
* <p/>
* 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 = metrics;
}
/**
* 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<? extends Throwable> cause;
public ExceptionMeterMetric(final MetricRegistry registry,
final ResourceMethod method,
final ExceptionMetered exceptionMetered) {
final MetricName name = chooseName(exceptionMetered.name(),
exceptionMetered.absolute(), method, ExceptionMetered.DEFAULT_NAME_SUFFIX);
this.meter = registry.meter(name);
this.cause = exceptionMetered.cause();
}
}
private static class TimerRequestEventListener implements RequestEventListener {
private final ConcurrentMap<Method, Timer> timers;
private Timer.Context context = null;
public TimerRequestEventListener(final ConcurrentMap<Method, Timer> timers) {
this.timers = timers;
}
@Override
public void onEvent(RequestEvent event) {
if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) {
final Timer timer = this.timers.get(event.getUriInfo()
.getMatchedResourceMethod().getInvocable().getDefinitionMethod());
if (timer != null) {
this.context = timer.time();
}
} else if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_FINISHED) {
if (this.context != null) {
this.context.close();
}
}
}
}
private static class MeterRequestEventListener implements RequestEventListener {
private final ConcurrentMap<Method, Meter> meters;
public MeterRequestEventListener(final ConcurrentMap<Method, Meter> 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<Method, ExceptionMeterMetric> exceptionMeters;
public ExceptionMeterRequestEventListener(final ConcurrentMap<Method, ExceptionMeterMetric> 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 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);
for (final ResourceMethod method : resource.getAllMethods()) {
registerTimedAnnotations(method, classLevelTimed);
registerMeteredAnnotations(method, classLevelMetered);
registerExceptionMeteredAnnotations(method, classLevelExceptionMetered);
}
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);
for (final ResourceMethod method : childResource.getAllMethods()) {
registerTimedAnnotations(method, classLevelTimedChild);
registerMeteredAnnotations(method, classLevelMeteredChild);
registerExceptionMeteredAnnotations(method, classLevelExceptionMeteredChild);
}
}
}
}
@Override
public RequestEventListener onRequest(final RequestEvent event) {
final RequestEventListener listener = new ChainedRequestEventListener(
new TimerRequestEventListener(timers),
new MeterRequestEventListener(meters),
new ExceptionMeterRequestEventListener(exceptionMeters));
return listener;
}
private <T extends Annotation> T getClassLevelAnnotation(final Resource resource, final Class<T> 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) {
timers.putIfAbsent(definitionMethod, timerMetric(this.metrics, method, classLevelTimed));
return;
}
final Timed annotation = definitionMethod.getAnnotation(Timed.class);
if (annotation != null) {
timers.putIfAbsent(definitionMethod, timerMetric(this.metrics, method, annotation));
}
}
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 static Timer timerMetric(final MetricRegistry registry,
final ResourceMethod method,
final Timed timed) {
final MetricName name = chooseName(timed.name(), timed.absolute(), method);
return registry.timer(name);
}
private static Meter meterMetric(final MetricRegistry registry,
final ResourceMethod method,
final Metered metered) {
final MetricName name = chooseName(metered.name(), metered.absolute(), method);
return registry.meter(name);
}
protected static MetricName chooseName(final String explicitName, final boolean absolute, final ResourceMethod method, final String... suffixes) {
if (explicitName != null && !explicitName.isEmpty()) {
if (absolute) {
return MetricName.build(explicitName);
}
return name(method.getInvocable().getDefinitionMethod().getDeclaringClass(), explicitName);
}
Method definitionMethod = method.getInvocable().getDefinitionMethod();
return MetricName.join(name(definitionMethod.getDeclaringClass(), definitionMethod.getName()), MetricName.build(suffixes));
}
}