diff --git a/tritium-lib/src/main/java/com/palantir/tritium/Tritium.java b/tritium-lib/src/main/java/com/palantir/tritium/Tritium.java index c7affffd7..807867e10 100644 --- a/tritium-lib/src/main/java/com/palantir/tritium/Tritium.java +++ b/tritium-lib/src/main/java/com/palantir/tritium/Tritium.java @@ -43,5 +43,4 @@ public static T instrument(Class serviceInterface, U delegat .withPerformanceTraceLogging() .build(); } - } diff --git a/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java b/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java index 788f6cbcd..cbce80704 100644 --- a/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java +++ b/tritium-lib/src/main/java/com/palantir/tritium/proxy/Instrumentation.java @@ -118,14 +118,43 @@ private Builder(Class interfaceClass, U delegate) { this.delegate = checkNotNull(delegate, "delegate"); } - public Builder withMetrics(MetricRegistry metricRegistry) { + /** + * Supply additional metrics name prefix to be used across interfaces that use MetricGroup annotations. + * + * Example: + * + * Given a prefix com.business.service and instrumented interfaces below, a single metric + * "com.business.service.fastcall" will share recordings across classes. Functionality assumes a shared + * MetricsRegistry. + * + * interface WidgetService { + * @MetricGroup("fastcall") + * getWidgets(); + * } + * + * interface UserService { + * @MetricGroup("fastcall") + * getUsers(); + * } + * + * @param metricRegistry - MetricsRegistry used for this application + * @param globalPrefix - Metrics name prefix to be used + * @return - InstrumentationBuilder + */ + public Builder withMetrics(MetricRegistry metricRegistry, String globalPrefix) { checkNotNull(metricRegistry, "metricRegistry"); this.handlers.add(new MetricsInvocationEventHandler( metricRegistry, - MetricRegistry.name(interfaceClass.getName()))); + delegate.getClass(), + MetricRegistry.name(interfaceClass.getName()), + globalPrefix)); return this; } + public Builder withMetrics(MetricRegistry metricRegistry) { + return withMetrics(metricRegistry, null); + } + public Builder withPerformanceTraceLogging() { return withLogging( getPerformanceLoggerForInterface(interfaceClass), diff --git a/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java b/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java index 5ba7c50ce..d50c3a08e 100644 --- a/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java +++ b/tritium-lib/src/test/java/com/palantir/tritium/proxy/InstrumentationTest.java @@ -37,6 +37,7 @@ import com.palantir.tritium.api.event.InvocationContext; import com.palantir.tritium.api.event.InvocationEventHandler; import com.palantir.tritium.event.log.LoggingInvocationEventHandler; +import com.palantir.tritium.event.metrics.annotations.MetricGroup; import com.palantir.tritium.metrics.MetricRegistries; import com.palantir.tritium.test.TestImplementation; import com.palantir.tritium.test.TestInterface; @@ -58,6 +59,17 @@ @RunWith(MockitoJUnitRunner.class) public class InstrumentationTest { + @MetricGroup("DEFAULT") + interface AnnotatedInterface { + @MetricGroup("ONE") + void method(); + + @MetricGroup("ONE") + void otherMethod(); + + void defaultMethod(); + } + private static final String EXPECTED_METRIC_NAME = TestInterface.class.getName() + ".test"; // Exceed the HotSpot JIT thresholds @@ -116,6 +128,29 @@ public void testBuilder() { Slf4jReporter.forRegistry(metricRegistry).withLoggingLevel(LoggingLevel.INFO).build().report(); } + @Test + public void testMetricGroupBuilder() { + AnnotatedInterface delegate = mock(AnnotatedInterface.class); + String globalPrefix = "com.business.service"; + + MetricRegistry metricRegistry = MetricRegistries.createWithHdrHistogramReservoirs(); + + AnnotatedInterface instrumentedService = Instrumentation.builder(AnnotatedInterface.class, delegate) + .withMetrics(metricRegistry, globalPrefix) + .withPerformanceTraceLogging() + .build(); + //call + instrumentedService.method(); + instrumentedService.otherMethod(); + instrumentedService.defaultMethod(); + + assertThat(metricRegistry.timer(AnnotatedInterface.class.getName() + ".ONE").getCount()).isEqualTo(2L); + assertThat(metricRegistry.timer(globalPrefix + ".ONE").getCount()).isEqualTo(2L); + assertThat(metricRegistry.timer(AnnotatedInterface.class.getName() + ".DEFAULT").getCount()).isEqualTo(1L); + assertThat(metricRegistry.timer(globalPrefix + ".DEFAULT").getCount()).isEqualTo(1L); + assertThat(metricRegistry.timer(AnnotatedInterface.class.getName() + ".method").getCount()).isEqualTo(1L); + } + private void executeManyTimes(TestInterface instrumentedService, int invocations) { Stopwatch timer = Stopwatch.createStarted(); for (int i = 0; i < invocations; i++) { diff --git a/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandler.java b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandler.java index 468fac175..d59cd8be5 100644 --- a/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandler.java +++ b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandler.java @@ -19,12 +19,17 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.palantir.tritium.api.event.InvocationContext; import com.palantir.tritium.api.event.InvocationEventHandler; import com.palantir.tritium.api.functions.BooleanSupplier; import com.palantir.tritium.event.AbstractInvocationEventHandler; import com.palantir.tritium.event.DefaultInvocationContext; +import com.palantir.tritium.event.metrics.annotations.AnnotationHelper; +import com.palantir.tritium.event.metrics.annotations.MetricGroup; import java.lang.reflect.Method; +import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -42,16 +47,55 @@ public final class MetricsInvocationEventHandler extends AbstractInvocationEvent private final MetricRegistry metricRegistry; private final String serviceName; + //consider creating annotation handlers as separate objects + private final Map metricGroups; + @Nullable private final String globalGroupPrefix; + public MetricsInvocationEventHandler(MetricRegistry metricRegistry, String serviceName) { super(getEnabledSupplier(serviceName)); this.metricRegistry = checkNotNull(metricRegistry, "metricRegistry"); this.serviceName = checkNotNull(serviceName, "serviceName"); + this.metricGroups = ImmutableMap.of(); + this.globalGroupPrefix = null; + } + + public MetricsInvocationEventHandler( + MetricRegistry metricRegistry, Class serviceClass, String serviceName, @Nullable String globalGroupPrefix) { + super(getEnabledSupplier(serviceName)); + this.metricRegistry = checkNotNull(metricRegistry, "metricRegistry"); + this.serviceName = checkNotNull(serviceName, "serviceName"); + this.metricGroups = createMethodGroupMapping(checkNotNull(serviceClass)); + this.globalGroupPrefix = Strings.emptyToNull(globalGroupPrefix); + } + + public MetricsInvocationEventHandler( + MetricRegistry metricRegistry, Class serviceClass, @Nullable String globalGroupPrefix) { + this(metricRegistry, serviceClass, checkNotNull(serviceClass.getName()), globalGroupPrefix); } private static String failuresMetricName() { return "failures"; } + private static Map createMethodGroupMapping(Class serviceClass) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + + MetricGroup classGroup = AnnotationHelper.getSuperTypeAnnotation(serviceClass, MetricGroup.class); + + for (Method method : serviceClass.getMethods()) { + AnnotationHelper.MethodSignature sig = AnnotationHelper.MethodSignature.of(method); + MetricGroup methodGroup = AnnotationHelper.getMethodAnnotation(MetricGroup.class, serviceClass, sig); + + if (methodGroup != null) { + builder.put(sig, methodGroup.value()); + } else if (classGroup != null) { + builder.put(sig, classGroup.value()); + } + } + + return builder.build(); + } + static BooleanSupplier getEnabledSupplier(final String serviceName) { return getSystemPropertySupplier(serviceName); } @@ -67,8 +111,20 @@ public void onSuccess(@Nullable InvocationContext context, @Nullable Object resu logger.debug("Encountered null metric context likely due to exception in preInvocation"); return; } + long nanos = System.nanoTime() - context.getStartTimeNanos(); metricRegistry.timer(getBaseMetricName(context)) - .update(System.nanoTime() - context.getStartTimeNanos(), TimeUnit.NANOSECONDS); + .update(nanos, TimeUnit.NANOSECONDS); + + String metricName = metricGroups.get(AnnotationHelper.MethodSignature.of(context.getMethod())); + if (metricName != null) { + metricRegistry.timer(MetricRegistry.name(serviceName, metricName)) + .update(nanos, TimeUnit.NANOSECONDS); + + if (globalGroupPrefix != null) { + metricRegistry.timer(MetricRegistry.name(globalGroupPrefix, metricName)) + .update(nanos, TimeUnit.NANOSECONDS); + } + } } @Override @@ -83,6 +139,19 @@ public void onFailure(@Nullable InvocationContext context, @Nonnull Throwable ca String failuresMetricName = MetricRegistry.name(getBaseMetricName(context), failuresMetricName()); metricRegistry.meter(failuresMetricName).mark(); metricRegistry.meter(MetricRegistry.name(failuresMetricName, cause.getClass().getName())).mark(); + + long nanos = System.nanoTime() - context.getStartTimeNanos(); + String metricName = metricGroups.get(AnnotationHelper.MethodSignature.of(context.getMethod())); + + if (metricName != null) { + metricRegistry.timer(MetricRegistry.name(serviceName, metricName, failuresMetricName())) + .update(nanos, TimeUnit.NANOSECONDS); + + if (globalGroupPrefix != null) { + metricRegistry.timer(MetricRegistry.name(globalGroupPrefix, metricName, failuresMetricName())) + .update(nanos, TimeUnit.NANOSECONDS); + } + } } private String getBaseMetricName(InvocationContext context) { diff --git a/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/AnnotationHelper.java b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/AnnotationHelper.java new file mode 100644 index 000000000..ba0255e6d --- /dev/null +++ b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/AnnotationHelper.java @@ -0,0 +1,164 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.tritium.event.metrics.annotations; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Set; + +public final class AnnotationHelper { + + private AnnotationHelper() { + throw new UnsupportedOperationException(); + } + + /** + * Annotation as implemented on passed in type or parent of that type, works for both super classes and interfaces. + * + * @param clazz - Class type to scan for annotations + * @param annotation - Annotation type to scan for + * @return - First matching annotation found in depth first search, or null if not found + */ + public static T getSuperTypeAnnotation(Class clazz, Class annotation) { + if (clazz.isAnnotationPresent(annotation)) { + return clazz.getAnnotation(annotation); + } + + for (Class ifaces : getParentClasses(clazz)) { + T superAnnotation = getSuperTypeAnnotation(ifaces, annotation); + if (superAnnotation != null) { + return superAnnotation; + } + } + + return null; + } + + /** + * Depth first search up the Type hierarchy to find a matching annotation, Types which do not implement the + * specified method signature are ignored. + * + * @param annotation - Annotation type to scan for + * @param clazz - Class type to scan for matching annotations + * @param methodSignature - Method to search annotation for + * @return - First found matching annotation or null + */ + public static T getMethodAnnotation( + Class annotation, Class clazz, MethodSignature methodSignature) { + + Method method; + try { + method = clazz.getMethod(methodSignature.getMethodName(), methodSignature.getParameterTypes()); + } catch (NoSuchMethodException e) { + return null; + } + + if (method.isAnnotationPresent(annotation)) { + return method.getAnnotation(annotation); + } + + for (Class iface : getParentClasses(clazz)) { + T foundAnnotation = getMethodAnnotation(annotation, iface, methodSignature); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + + return null; + } + + @VisibleForTesting + static Set> getParentClasses(Class clazz) { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + builder.add(clazz.getInterfaces()); + Class superclass = clazz.getSuperclass(); + while (superclass != null) { + builder.add(superclass.getInterfaces()); + builder.add(superclass); + superclass = superclass.getSuperclass(); + } + return builder.build(); + } + + public static final class MethodSignature { + private final String methodName; + private final Class[] parameterTypes; + + private static final Class[] NO_ARGS = new Class[0]; + + private MethodSignature(String methodName, Class... parameterTypes) { + this.methodName = checkNotNull(methodName); + this.parameterTypes = (parameterTypes == null || parameterTypes.length == 0) + ? NO_ARGS : parameterTypes.clone(); + } + + public String getMethodName() { + return methodName; + } + + public Class[] getParameterTypes() { + return parameterTypes; + } + + @Override + public String toString() { + return "MethodSignature{" + + "methodName='" + methodName + '\'' + + ", parameterTypes=" + Arrays.toString(parameterTypes) + + '}'; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + MethodSignature that = (MethodSignature) other; + + if (getMethodName() != null ? !getMethodName().equals(that.getMethodName()) + : that.getMethodName() != null) { + return false; + } + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(getParameterTypes(), that.getParameterTypes()); + } + + @Override + public int hashCode() { + int result = getMethodName() != null ? getMethodName().hashCode() : 0; + result = 31 * result + Arrays.hashCode(getParameterTypes()); + return result; + } + + public static MethodSignature of(Method method) { + return MethodSignature.of(method.getName(), method.getParameterTypes()); + } + + public static MethodSignature of(String methodName, Class... parameterTypes) { + return new MethodSignature(methodName, parameterTypes); + } + } +} diff --git a/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/MetricGroup.java b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/MetricGroup.java new file mode 100644 index 000000000..b03339f5d --- /dev/null +++ b/tritium-metrics/src/main/java/com/palantir/tritium/event/metrics/annotations/MetricGroup.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.tritium.event.metrics.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Instrumentation instruction to tritium to group calls into a common metric name. + * Setting Type (class) level applies a default metric group to all Methods that are annotated + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MetricGroup { + /** + * String identifier grouped metrics. + * @return + */ + String value(); +} diff --git a/tritium-metrics/src/test/java/com/palantir/tritium/annotations/AnnotationHelperTest.java b/tritium-metrics/src/test/java/com/palantir/tritium/annotations/AnnotationHelperTest.java new file mode 100644 index 000000000..7b884b5ec --- /dev/null +++ b/tritium-metrics/src/test/java/com/palantir/tritium/annotations/AnnotationHelperTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.tritium.annotations; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import com.palantir.tritium.event.metrics.annotations.AnnotationHelper; +import com.palantir.tritium.event.metrics.annotations.MetricGroup; +import org.junit.Test; + +public class AnnotationHelperTest { + + @MetricGroup("DEFAULT") + public interface TestSuperInterface { + + @MetricGroup("ONE") + void method(); + + @MetricGroup("OVERLOAD") + void method(String arg); + + @MetricGroup("TWO") + void hasParams(String arg); + + @MetricGroup("VARGS") + void vargMethod(String... vargs); + + void otherMethod(); + } + + public interface TestOverrideInterface extends TestSuperInterface { + @MetricGroup("OVERRIDE") + void method(); + } + + @Test + public void testParentInterfaceAnnotations() throws NoSuchMethodException { + TestSuperInterface impl = mock(TestSuperInterface.class); + + //discovery annotation on parent class + MetricGroup cls = AnnotationHelper.getSuperTypeAnnotation(impl.getClass(), MetricGroup.class); + assertThat(cls.value()).isEqualTo("DEFAULT"); + + //find annotation by class method + MetricGroup met = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of(impl.getClass().getMethod("method"))); + assertThat(met.value()).isEqualTo("ONE"); + + //find annotation by string descriptor + MetricGroup descriptor = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of("method")); + assertThat(descriptor.value()).isEqualTo("ONE"); + + //validate overloaded methods + MetricGroup overload = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of("method", String.class)); + assertThat(overload.value()).isEqualTo("OVERLOAD"); + + //return null if annotation does not exist + MetricGroup noAnnotation = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of(impl.getClass().getMethod("otherMethod"))); + assertThat(noAnnotation).isNull(); + + //validate method matching with parameters + MetricGroup clsParams = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of(impl.getClass().getMethod("hasParams", String.class))); + assertThat(clsParams.value()).isEqualTo("TWO"); + + //validate signature matching with parameters + MetricGroup sigParams = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of("hasParams", String.class)); + assertThat(sigParams.value()).isEqualTo("TWO"); + + //return null if method does not exist + MetricGroup noMethod = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of("noMethod")); + assertThat(noMethod).isNull(); + } + + @Test + public void testMethodSignatureEquality() throws NoSuchMethodException { + assertThat(AnnotationHelper.MethodSignature.of( + TestSuperInterface.class.getMethod("method"))) + .isEqualTo(AnnotationHelper.MethodSignature.of("method")); + + assertThat(AnnotationHelper.MethodSignature.of( + TestSuperInterface.class.getMethod("hasParams", String.class))) + .isEqualTo(AnnotationHelper.MethodSignature.of("hasParams", String.class)); + } + + @Test + public void testVargVariants() throws NoSuchMethodException { + TestSuperInterface impl = mock(TestSuperInterface.class); + AnnotationHelper.MethodSignature vargSig = AnnotationHelper.MethodSignature.of("vargMethod", String[].class); + + //validate signature matching with vargs + MetricGroup vargParams = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + vargSig); + + assertThat(vargParams.value()).isEqualTo("VARGS"); + + assertThat(vargSig).isEqualTo( + AnnotationHelper.MethodSignature.of(impl.getClass().getMethod("vargMethod", String[].class))); + } + + @Test + public void testOverrideInterface() { + TestOverrideInterface impl = mock(TestOverrideInterface.class); + + //validate signature matching with vargs + MetricGroup override = AnnotationHelper.getMethodAnnotation( + MetricGroup.class, + impl.getClass(), + AnnotationHelper.MethodSignature.of("method")); + + assertThat(override.value()).isEqualTo("OVERRIDE"); + } +} diff --git a/tritium-metrics/src/test/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandlerTest.java b/tritium-metrics/src/test/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandlerTest.java index 21189bdb6..e767626e6 100644 --- a/tritium-metrics/src/test/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandlerTest.java +++ b/tritium-metrics/src/test/java/com/palantir/tritium/event/metrics/MetricsInvocationEventHandlerTest.java @@ -22,10 +22,33 @@ import com.codahale.metrics.MetricRegistry; import com.palantir.tritium.api.event.InvocationContext; +import com.palantir.tritium.event.DefaultInvocationContext; +import com.palantir.tritium.event.metrics.annotations.MetricGroup; import org.junit.Test; public class MetricsInvocationEventHandlerTest { + @MetricGroup("DEFAULT") + public interface AnnotatedTestInterface { + + @MetricGroup("ONE") + String methodA(); + + @MetricGroup("ONE") + void methodB(); + + @MetricGroup("TWO") + void methodC(); + + //Should match the default + void methodD(); + } + + @MetricGroup("DEFAULT") + public interface AnnotatedOtherInterface { + void methodE(); + } + @Test public void testFailure() throws Exception { MetricRegistry metricRegistry = new MetricRegistry(); @@ -69,4 +92,53 @@ public void testSystemPropertySupplier_Handler_Enabled() throws Exception { assertThat(MetricsInvocationEventHandler.getEnabledSupplier("test").asBoolean()).isTrue(); } + @Test + public void testMetricGroupAnnotations() throws Exception { + AnnotatedTestInterface obj = mock(AnnotatedTestInterface.class); + when(obj.methodA()).thenReturn("ok"); + + AnnotatedOtherInterface other = mock(AnnotatedOtherInterface.class); + + MetricRegistry metricRegistry = new MetricRegistry(); + String globalPrefix = "com.business.myservice"; + + MetricsInvocationEventHandler handler = + new MetricsInvocationEventHandler(metricRegistry, obj.getClass(), globalPrefix); + + MetricsInvocationEventHandler otherHandler = + new MetricsInvocationEventHandler(metricRegistry, other.getClass(), globalPrefix); + + //AnnotatedTestInterface + callVoidMethod(handler, obj, "methodA", true); + callVoidMethod(handler, obj, "methodB", true); + callVoidMethod(handler, obj, "methodC", true); + callVoidMethod(handler, obj, "methodD", true); + callVoidMethod(handler, obj, "methodA", false); + + assertThat(metricRegistry.timer(obj.getClass().getName() + ".ONE").getCount()).isEqualTo(2L); + assertThat(metricRegistry.timer(obj.getClass().getName() + ".TWO").getCount()).isEqualTo(1L); + assertThat(metricRegistry.timer(obj.getClass().getName() + ".DEFAULT").getCount()).isEqualTo(1L); + assertThat(metricRegistry.timer(obj.getClass().getName() + ".ONE.failures").getCount()).isEqualTo(1L); + + //AnnotatedOtherInterface + callVoidMethod(otherHandler, other, "methodE", true); + assertThat(metricRegistry.timer(other.getClass().getName() + ".DEFAULT").getCount()).isEqualTo(1L); + + //GlobalPrefix Tests + assertThat(metricRegistry.timer(globalPrefix + ".DEFAULT").getCount()).isEqualTo(2L); + assertThat(metricRegistry.timer(globalPrefix + ".ONE").getCount()).isEqualTo(2L); + } + + private void callVoidMethod( + MetricsInvocationEventHandler handler, Object obj, String methodName, boolean success) throws Exception { + + InvocationContext context = DefaultInvocationContext.of(obj, obj.getClass().getMethod(methodName), null); + if (success) { + handler.onSuccess(context, null); + } else { + handler.onFailure(context, new RuntimeException("test failure")); + } + + } + }