Skip to content

Commit

Permalink
add metric annotation instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Duncan-tree-zhou committed May 20, 2024
1 parent 8b4e6d2 commit f7dd6b9
Show file tree
Hide file tree
Showing 11 changed files with 835 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ out/

# Others #
##########
instrumentation/opentelemetry-instrumentation-annotations-1.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/instrumentationannotations/annotations/
/logs/*
/bin
/out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Comparing source compatibility of against
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW INTERFACE: java.lang.annotation.Annotation
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String[] attributes()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String[] additionalAttributes()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String description()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String returnValueAttribute()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.util.concurrent.TimeUnit unit()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String unit()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String value()
+++ NEW ANNOTATION: java.lang.annotation.Target
+++ NEW ELEMENT: value=java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.CONSTRUCTOR (+)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
* <p>The name should follow the instrument naming rule: <a
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-naming-rule</a>
*
* <p>The default name is {@code method.invocations.total}.
* <p>The default name is {@code method.invocation.count}.
*/
String value() default "method.invocations.total";
String value() default "method.invocation.count";

/**
* Description of the instrument.
Expand All @@ -59,7 +59,7 @@
* <p>Unit strings should follow the instrument unit rules: <a
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-unit">https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-unit</a>
*/
String unit() default "1";
String unit() default "{invocation}";

/**
* List of key-value pairs to supply additional attributes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
* <p>If not specified and the code is compiled using the `{@code -parameters}` argument to
* `javac`, the parameter name will be used instead. If the parameter name is not available, e.g.,
* because the code was not compiled with that flag, the attribute will be ignored.
*
* <p>Warning: be careful to fill it because it might cause an explosion of the cardinality on
* your metric
*/
String value() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
* This annotation creates a {@link io.opentelemetry.api.metrics.LongHistogram Histogram} instrument
Expand Down Expand Up @@ -44,7 +43,7 @@
*
* <p>The default name is {@code method.invocations.duration}.
*/
String value() default "method.invocations.duration";
String value() default "method.invocation.duration";

/**
* Description for the instrument.
Expand All @@ -59,7 +58,7 @@
*
* <p>Default is millis seconds.
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
String unit() default "ms";

/**
* List of key-value pairs to supply additional attributes.
Expand All @@ -74,7 +73,7 @@
* })
* </pre>
*/
String[] attributes() default {};
String[] additionalAttributes() default {};

/**
* Attribute name for the return value.
Expand All @@ -83,6 +82,9 @@
* will be called on the return value to convert it to a String.
*
* <p>By default, the instrument will not have an attribute with the return value.
*
* <p>Warning: be careful to fill it because it might cause an explosion of the cardinality on
* your metric
*/
String returnValueAttribute() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import static java.util.logging.Level.FINE;

import application.io.opentelemetry.instrumentation.annotations.WithSpan;
import application.io.opentelemetry.instrumentation.annotations.Counted;
import application.io.opentelemetry.instrumentation.annotations.MetricAttribute;
import application.io.opentelemetry.instrumentation.annotations.Timed;
import application.io.opentelemetry.instrumentation.annotations.WithSpan;
import com.google.common.base.Stopwatch;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.Attributes;
Expand Down Expand Up @@ -66,9 +66,9 @@ public static void recordHistogramWithAttributes(
Timed timedAnnotation = methodRequest.method().getAnnotation(Timed.class);
AttributesBuilder attributesBuilder = Attributes.builder();
extractMetricAttributes(methodRequest, attributesBuilder);
extractAdditionAttributes(timedAnnotation, attributesBuilder);
getHistogram(timedAnnotation)
.record(stopwatch.stop().elapsed().toMillis(), attributesBuilder.build());
extractAdditionAttributes(timedAnnotation.additionalAttributes(), attributesBuilder);
double duration = ((double) stopwatch.stop().elapsed().toMillis()) / 1000;
getHistogram(timedAnnotation).record(duration, attributesBuilder.build());
}

private static void extractMetricAttributes(
Expand All @@ -90,26 +90,39 @@ private static void extractMetricAttributes(
}
}

public static void recordHistogram(Method method, Stopwatch stopwatch) {
Timed timedAnnotation = method.getAnnotation(Timed.class);
AttributesBuilder attributesBuilder = Attributes.builder();
extractAdditionAttributes(timedAnnotation.additionalAttributes(), attributesBuilder);
double duration = ((double) stopwatch.stop().elapsed().toMillis()) / 1000;
getHistogram(timedAnnotation).record(duration, attributesBuilder.build());
}

private static void extractAdditionAttributes(
Timed timedAnnotation, AttributesBuilder attributesBuilder) {
int length = timedAnnotation.attributes().length;
String[] attributes, AttributesBuilder attributesBuilder) {
int length = attributes.length;
for (int i = 0; i < length / 2; i++) {
attributesBuilder.put(
timedAnnotation.attributes()[i],
i + 1 > length ? "" : timedAnnotation.attributes()[i + 1]);
attributesBuilder.put(attributes[i], i + 1 > length ? "" : attributes[i + 1]);
}
}

public static void recordHistogram(Method method, Stopwatch stopwatch) {
Timed timedAnnotation = method.getAnnotation(Timed.class);
public static void recordCountWithAttributes(MethodRequest methodRequest) {
Counted countedAnnotation = methodRequest.method().getAnnotation(Counted.class);
AttributesBuilder attributesBuilder = Attributes.builder();
extractAdditionAttributes(timedAnnotation, attributesBuilder);
getHistogram(timedAnnotation)
.record(stopwatch.stop().elapsed().toMillis(), attributesBuilder.build());
extractMetricAttributes(methodRequest, attributesBuilder);
extractAdditionAttributes(countedAnnotation.additionalAttributes(), attributesBuilder);
getCounter(methodRequest.method()).add(1, attributesBuilder.build());
}

private static LongCounter getCounter(MethodRequest methodRequest) {
Counted countedAnnotation = methodRequest.method().getAnnotation(Counted.class);
public static void recordCount(Method method) {
Counted countedAnnotation = method.getAnnotation(Counted.class);
AttributesBuilder attributesBuilder = Attributes.builder();
extractAdditionAttributes(countedAnnotation.additionalAttributes(), attributesBuilder);
getCounter(method).add(1, attributesBuilder.build());
}

private static LongCounter getCounter(Method method) {
Counted countedAnnotation = method.getAnnotation(Counted.class);
if (!COUNTERS.containsKey(countedAnnotation.value())) {
synchronized (countedAnnotation.value()) {
if (!COUNTERS.containsKey(countedAnnotation.value())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static io.opentelemetry.javaagent.instrumentation.instrumentationannotations.KotlinCoroutineUtil.isKotlinSuspendMethod;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.hasParameters;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.whereAny;

import com.google.common.base.Stopwatch;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.lang.reflect.Method;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.annotation.AnnotationSource;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;

public class CountedInstrumentation implements TypeInstrumentation {

private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
private final ElementMatcher.Junction<MethodDescription> annotatedParametersMatcher;
// this matcher matches all methods that should be excluded from transformation
private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;

CountedInstrumentation() {
annotatedMethodMatcher =
isAnnotatedWith(named("application.io.opentelemetry.instrumentation.annotations.Counted"));
annotatedParametersMatcher =
hasParameters(
whereAny(
isAnnotatedWith(
named(
"application.io.opentelemetry.instrumentation.annotations.MetricAttribute"))));
// exclude all kotlin suspend methods, these are handle in kotlinx-coroutines instrumentation
excludedMethodsMatcher =
AnnotationExcludedMethods.configureExcludedMethods().or(isKotlinSuspendMethod());
}

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return declaresMethod(annotatedMethodMatcher);
}

@Override
public void transform(TypeTransformer transformer) {
ElementMatcher.Junction<MethodDescription> countedMethods =
annotatedMethodMatcher.and(not(excludedMethodsMatcher));

ElementMatcher.Junction<MethodDescription> timedMethodsWithParameters =
countedMethods.and(annotatedParametersMatcher);

ElementMatcher.Junction<MethodDescription> timedMethodsWithoutParameters =
countedMethods.and(not(annotatedParametersMatcher));

transformer.applyAdviceToMethod(
timedMethodsWithoutParameters, CountedInstrumentation.class.getName() + "$CountedAdvice");

// Only apply advice for tracing parameters as attributes if any of the parameters are annotated
// with @MetricsAttribute to avoid unnecessarily copying the arguments into an array.
transformer.applyAdviceToMethod(
timedMethodsWithParameters,
CountedInstrumentation.class.getName() + "$CountedAttributesAdvice");
}

@SuppressWarnings("unused")
public static class CountedAttributesAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Origin Method originMethod,
@Advice.Local("otelMethod") Method method,
@Advice.AllArguments(typing = Assigner.Typing.DYNAMIC) Object[] args,
@Advice.Local("otelRequest") MethodRequest request,
@Advice.Local("stopwatch") Stopwatch stopwatch) {

// Every usage of @Advice.Origin Method is replaced with a call to Class.getMethod, copy it
// to local variable so that there would be only one call to Class.getMethod.
method = originMethod;
request = new MethodRequest(method, args);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Local("otelMethod") Method method,
@Advice.Local("otelRequest") MethodRequest request,
@Advice.Local("stopwatch") Stopwatch stopwatch,
@Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
@Advice.Thrown Throwable throwable) {
AnnotationSingletons.recordCountWithAttributes(request);
}
}

@SuppressWarnings("unused")
public static class CountedAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Origin Method originMethod,
@Advice.Local("otelMethod") Method method,
@Advice.Local("stopwatch") Stopwatch stopwatch) {
// Every usage of @Advice.Origin Method is replaced with a call to Class.getMethod, copy it
// to local variable so that there would be only one call to Class.getMethod.
method = originMethod;
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Local("otelMethod") Method method,
@Advice.Local("stopwatch") Stopwatch stopwatch,
@Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
@Advice.Thrown Throwable throwable) {
AnnotationSingletons.recordCount(method);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Arrays.asList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class CountedInstrumentationsModule extends InstrumentationModule {

public CountedInstrumentationsModule() {
super("opentelemetry-instrumentation-annotations", "counted");
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("application.io.opentelemetry.instrumentation.annotations.Counted");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new CountedInstrumentation());
}

@Override
public int order() {
// Run first to ensure other automatic instrumentation is added after and therefore is executed
// earlier in the instrumented method and create the span to attach attributes to.
return -1000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.instrumentationannotations;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Arrays.asList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class TimedInstrumentationsModule extends InstrumentationModule {

public TimedInstrumentationsModule() {
super("opentelemetry-instrumentation-annotations", "timed");
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("application.io.opentelemetry.instrumentation.annotations.Timed");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new TimedInstrumentation());
}

@Override
public int order() {
// Run first to ensure other automatic instrumentation is added after and therefore is executed
// earlier in the instrumented method and create the span to attach attributes to.
return -1000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.opentelemetry.test.annotation;

import io.opentelemetry.javaagent.instrumentation.instrumentationannotations.annotations.Counted;

public class CountedExample {

@Counted
public String defaultExample() {
return "defualt example";
}

@Counted("another.name")
public String exampleWithAnotherName() {
return "example with another name.";
}

}
Loading

0 comments on commit f7dd6b9

Please sign in to comment.