/******************************************************************************* * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. *******************************************************************************/ package fr.gouv.vitam.common.metrics; import java.lang.reflect.Method; import javax.ws.rs.core.MediaType; import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.server.model.ResourceMethod; import org.glassfish.jersey.server.monitoring.ApplicationEvent; import org.glassfish.jersey.server.monitoring.RequestEvent; import org.glassfish.jersey.server.monitoring.RequestEventListener; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener; import fr.gouv.vitam.common.ParametersChecker; import fr.gouv.vitam.common.VitamConfiguration; import jersey.repackaged.com.google.common.collect.ImmutableMap; /** * A fork of the {@link InstrumentedResourceMethodApplicationListener} * <p> * This class enables the automatic generation of Metrics/Jersey annotations such as : @Timed, @Metered * and @ExceptionMetered on any API end-points of the resources inside the application. * </p> * Metric names are automatically generated under the form: * <p> * URI:HTTP_METHOD:METRIC_TYPE * </p> * Example: * <p> * /application/test:GET:Timer * </p> * <p> * WARNING This class doesn't support nested Jersey resource * </p> */ public final class VitamInstrumentedResourceMethodApplicationListener extends InstrumentedResourceMethodApplicationListener { private final MetricRegistry metrics; private ImmutableMap<Method, Timer> timers = ImmutableMap.of(); private ImmutableMap<Method, Meter> meters = ImmutableMap.of(); private ImmutableMap<Method, Meter> exceptionMeters = ImmutableMap.of(); private static final String METRIC_NAME_DELIMITER = ":"; private static final String METRIC_METER_NAME = "meter"; private static final String METRIC_TIMER_NAME = "timer"; private static final String METRIC_EXCEPTION_METER_NAME = "exceptionMeter"; private static final String METRIC_NAME_CONFIGURATION_PARAMETERS = "Metric name configuration parameters"; /**************************************************************************************************** * * THE CODE STARTING HERE DIFFERS FROM {@link InstrumentedResourceMethodApplicationListener} * * ***************************************************************************************************/ /** * Construct an application event listener using the given metrics registry. * <p> * When using this constructor, the {@link VitamInstrumentedResourceMethodApplicationListener} should be added to a * Jersey {@code ResourceConfig} as a singleton. * </p> * * @param metrics a {@link MetricRegistry} */ public VitamInstrumentedResourceMethodApplicationListener(MetricRegistry metrics) { super(metrics); ParametersChecker.checkParameter("MetricRegistry", metrics); this.metrics = metrics; } /** * Appends to a given {@code String} the meter metric name and a delimiter character. * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_METER_NAME} * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_NAME_DELIMITER} * * @param name * @return String */ public static final String metricMeterName(final String name) { ParametersChecker.checkParameterNullOnly(METRIC_NAME_CONFIGURATION_PARAMETERS, name); return name + METRIC_NAME_DELIMITER + METRIC_METER_NAME; } /** * Appends to a given {@code String} the meter metric name and a delimiter character. * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_TIMER_NAME} * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_NAME_DELIMITER} * * @param name * @return String */ public static final String metricTimerName(final String name) { ParametersChecker.checkParameterNullOnly(METRIC_NAME_CONFIGURATION_PARAMETERS, name); return name + METRIC_NAME_DELIMITER + METRIC_TIMER_NAME; } /** * Appends to a given {@code String} the meter metric name and a delimiter character. * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_EXCEPTION_METER_NAME} * {@see VitamInstrumentedResourceMethodApplicationListener#METRIC_NAME_DELIMITER} * * @param name * @return String */ public static final String metricExceptionMeterName(final String name) { ParametersChecker.checkParameterNullOnly(METRIC_NAME_CONFIGURATION_PARAMETERS, name); return name + METRIC_NAME_DELIMITER + METRIC_EXCEPTION_METER_NAME; } /** * Concat two strings together making sure at least one slash character '/' exists between them. * * @param first * @param second * @return String */ final private String concatURI(String first, String second) { final StringBuilder stringBuilder = new StringBuilder(); if (first.length() > 0 && first.charAt(first.length() - 1) != '/' && second.length() > 0 && second.charAt(0) != '/') { return stringBuilder.append(first).append('/').append(second).toString(); } else { return stringBuilder.append(first).append(second).toString(); } } final private String getConsumedTypesAsString(final ResourceMethod method) { final StringBuilder stringBuilder = new StringBuilder(); if (!method.getConsumedTypes().isEmpty()) { for (final MediaType type : method.getConsumedTypes()) { stringBuilder.append(type.toString()).append(','); } return stringBuilder.deleteCharAt(stringBuilder.length() - 1).toString(); } else { return "*"; } } final private String getProducedTypesAsString(final ResourceMethod method) { final StringBuilder stringBuilder = new StringBuilder(); if (!method.getProducedTypes().isEmpty()) { for (final MediaType type : method.getProducedTypes()) { stringBuilder.append(type.toString()); stringBuilder.append(','); } return stringBuilder.deleteCharAt(stringBuilder.length() - 1).toString(); } else { return "*"; } } /** * Creates a generic name for the API end-point of the type: * <p> * URI:HTTP_METHOD:CONSUME_MEDIA_TYPES:PRODUCE_MEDIA_TYPE * </p> * * @param method {@link ResourceMethod} * @param URI {@link String} the end-point URI * @return String */ final private String metricGenericName(final ResourceMethod method, final String uri) { return uri + METRIC_NAME_DELIMITER + method.getHttpMethod() + METRIC_NAME_DELIMITER + getConsumedTypesAsString(method) + METRIC_NAME_DELIMITER + getProducedTypesAsString(method); } /** * Register a new TimerMetric on a given registry with a given name. * <p> * Appends the metric type "Timer" to the name. * </p> * * @param registry {@link MetricRegistry} * @param name {@link String} * @return {@link Timer} */ final private Timer timerMetric(String name) { return metrics.timer(metricTimerName(name)); } /** * Register a new MeterMetric on a given registry with a given name. * <p> * Appends the metric type "Meter" to the name. * </p> * * @param registry {@link MetricRegistry} * @param name {@link String} * @return {@link Meter} */ final private Meter meterMetric(String name) { return metrics.meter(metricMeterName(name)); } /** * Register a new MeterMetric on a given registry with a given name. * <p> * Appends the metric type "ExceptionMeter" to the name. * </p> * * @param registry {@link MetricRegistry} * @param name {@link String} * @return {@link Meter} */ final private Meter exceptionMeterMetric(String name) { return metrics.meter(metricExceptionMeterName(name)); } /** * This ExceptionMeterRequestEventListener differs from the original one because the * {@link ExceptionMeterRequestEventListener#onEvent(RequestEvent)} method is no longer checking if the raised * Exception should be caught, instead every exception that occurs marks the meter. */ private static class ExceptionMeterRequestEventListener implements RequestEventListener { private final ImmutableMap<Method, Meter> exceptionMeters; public ExceptionMeterRequestEventListener(final ImmutableMap<Method, Meter> exceptionMeters) { this.exceptionMeters = exceptionMeters; } @Override public void onEvent(RequestEvent event) { if (event.getType() == RequestEvent.Type.ON_EXCEPTION) { final ResourceMethod method = event.getUriInfo().getMatchedResourceMethod(); final Meter meter = method != null ? exceptionMeters.get(method.getInvocable().getDefinitionMethod()) : null; if (meter != null) { meter.mark(); } } } } /** * <p> * Registers a meter, timer and exceptionMeter for a given Jersey end-point. * </p> * This method excludes extended Jersey methods. * * @param timerBuilder * @param meterBuilder * @param exceptionMeterBuilder * @param method * @param path * @param rootPath */ private void registerMetricsForMethod( final ImmutableMap.Builder<Method, Timer> timerBuilder, final ImmutableMap.Builder<Method, Meter> meterBuilder, final ImmutableMap.Builder<Method, Meter> exceptionMeterBuilder, final ResourceMethod method, final String path, final String rootPath) { final Method definitionMethod = method.getInvocable().getDefinitionMethod(); final String metricName; // Note : an extended method is a method not present in the original API, but created by Jersey for technical // purposes (ex: mediatype transformation, ...) if (!method.isExtended() && method.getHttpMethod() != null) { // TODO P2 /admin/v1/... and .../status URI are removed here but should be removed with regex in Kibana the // day it is possible if (rootPath != null && ("/admin/v1".equals(rootPath) || "/status".equals(path) || VitamConfiguration.TENANTS_URL.equals(path))) { return; } else if (rootPath == null) { metricName = metricGenericName(method, path); } else { metricName = metricGenericName(method, concatURI(rootPath, path)); } meterBuilder.put(definitionMethod, meterMetric(metricName)); timerBuilder.put(definitionMethod, timerMetric(metricName)); exceptionMeterBuilder.put(definitionMethod, exceptionMeterMetric(metricName)); } } /** * This function is called every time the application registers an event. * <p> * If the event is of type {@code ApplicationEvent.Type.INITIALIZATION_APP_FINISHED} the function will parse the * different methods of each {@see Resource} and automatically create metrics. * </p> */ @Override public void onEvent(ApplicationEvent event) { if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) { final ImmutableMap.Builder<Method, Timer> timerBuilder = ImmutableMap.<Method, Timer>builder(); final ImmutableMap.Builder<Method, Meter> meterBuilder = ImmutableMap.<Method, Meter>builder(); final ImmutableMap.Builder<Method, Meter> exceptionMeterBuilder = ImmutableMap.<Method, Meter>builder(); /* * TODO P1: This class does not handle nested resources for the moment. This feature should be implemented */ for (final Resource resource : event.getResourceModel().getResources()) { /* TODO P1: Remove the application.wadl resources with a better option */ if ("application.wadl".equals(resource.getPath())) { continue; } for (final ResourceMethod method : resource.getResourceMethods()) { registerMetricsForMethod( timerBuilder, meterBuilder, exceptionMeterBuilder, method, resource.getPath(), null); } for (final Resource childResource : resource.getChildResources()) { for (final ResourceMethod method : childResource.getResourceMethods()) { registerMetricsForMethod( timerBuilder, meterBuilder, exceptionMeterBuilder, method, childResource.getPath(), resource.getPath()); } } } timers = timerBuilder.build(); meters = meterBuilder.build(); exceptionMeters = exceptionMeterBuilder.build(); } } /**************************************************************************************************** * * THE CODE ENDING HERE DIFFERS FROM {@link InstrumentedResourceMethodApplicationListener} * * ***************************************************************************************************/ private static class TimerRequestEventListener implements RequestEventListener { private final ImmutableMap<Method, Timer> timers; private Timer.Context context = null; public TimerRequestEventListener(final ImmutableMap<Method, Timer> timers) { this.timers = timers; } @Override public void onEvent(RequestEvent event) { if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) { final Timer timer = timers.get(event.getUriInfo() .getMatchedResourceMethod().getInvocable().getDefinitionMethod()); if (timer != null) { context = timer.time(); } } else if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_FINISHED && context != null) { context.close(); } } } private static class MeterRequestEventListener implements RequestEventListener { private final ImmutableMap<Method, Meter> meters; public MeterRequestEventListener(final ImmutableMap<Method, Meter> meters) { this.meters = meters; } @Override public void onEvent(RequestEvent event) { if (event.getType() == RequestEvent.Type.RESOURCE_METHOD_START) { final Meter meter = meters.get(event.getUriInfo() .getMatchedResourceMethod().getInvocable().getDefinitionMethod()); if (meter != null) { 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 (final RequestEventListener listener : listeners) { listener.onEvent(event); } } } @Override public RequestEventListener onRequest(final RequestEvent event) { final RequestEventListener listener = new ChainedRequestEventListener( new TimerRequestEventListener(timers), new MeterRequestEventListener(meters), new ExceptionMeterRequestEventListener(exceptionMeters)); return listener; } }