Skip to content

Commit

Permalink
Metric grouping via @MetricGroup annotation (#33)
Browse files Browse the repository at this point in the history
* Initial add of metric group annotation
* Add class level default to handling
* Utility to scan class for annotations on parent classes and interfaces
* Fix parent annotation tracking inside Handler and tests
* Test variations for AnnotationHelper
* Document annotation helper
* Implement global prefix across instrumented interfaces
  • Loading branch information
pdunn authored and schlosna committed Oct 3, 2017
1 parent 7fb2132 commit 5bd714b
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ public static <T, U extends T> T instrument(Class<T> serviceInterface, U delegat
.withPerformanceTraceLogging()
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,43 @@ private Builder(Class<T> interfaceClass, U delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}

public Builder<T, U> 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<T, U> 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<T, U> withMetrics(MetricRegistry metricRegistry) {
return withMetrics(metricRegistry, null);
}

public Builder<T, U> withPerformanceTraceLogging() {
return withLogging(
getPerformanceLoggerForInterface(interfaceClass),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<AnnotationHelper.MethodSignature, String> 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<AnnotationHelper.MethodSignature, String> createMethodGroupMapping(Class<?> serviceClass) {
ImmutableMap.Builder<AnnotationHelper.MethodSignature, String> 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);
}
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 extends Annotation> T getSuperTypeAnnotation(Class<?> clazz, Class<T> 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 extends Annotation> T getMethodAnnotation(
Class<T> 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<Class<?>> getParentClasses(Class<?> clazz) {
ImmutableSet.Builder<Class<?>> 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);
}
}
}
Loading

0 comments on commit 5bd714b

Please sign in to comment.