Skip to content

Commit 9bd77a5

Browse files
committed
add span kind support for method instrumentation
1 parent c904a8f commit 9bd77a5

File tree

7 files changed

+202
-19
lines changed

7 files changed

+202
-19
lines changed

instrumentation/methods/javaagent/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ dependencies {
1515

1616
tasks.withType<Test>().configureEach {
1717
jvmArgs(
18-
"-Dotel.instrumentation.methods.include=io.opentelemetry.javaagent.instrumentation.methods.MethodTest\$ConfigTracedCallable[call];io.opentelemetry.javaagent.instrumentation.methods.MethodTest\$ConfigTracedCompletableFuture[getResult];javax.naming.directory.InitialDirContext[search]"
18+
"-Dotel.instrumentation.methods.include=io.opentelemetry.javaagent.instrumentation.methods.MethodTest\$ConfigTracedCallable[call];io.opentelemetry.javaagent.instrumentation.methods.MethodTest\$ConfigTracedCompletableFuture[getResult=SERVER];javax.naming.directory.InitialDirContext[search=CLIENT]"
1919
)
2020
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.methods;
7+
8+
import io.opentelemetry.api.trace.SpanKind;
9+
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
10+
11+
public class MethodAndType {
12+
private final ClassAndMethod classAndMethod;
13+
private final SpanKind spanKind;
14+
15+
private MethodAndType(ClassAndMethod classAndMethod, SpanKind spanKind) {
16+
this.classAndMethod = classAndMethod;
17+
this.spanKind = spanKind;
18+
}
19+
20+
public static MethodAndType create(ClassAndMethod classAndMethod, SpanKind spanKind) {
21+
return new MethodAndType(classAndMethod, spanKind);
22+
}
23+
24+
public ClassAndMethod getClassAndMethod() {
25+
return classAndMethod;
26+
}
27+
28+
public SpanKind getSpanKind() {
29+
return spanKind;
30+
}
31+
}

instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentation.java

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,71 @@
1414
import static net.bytebuddy.matcher.ElementMatchers.named;
1515
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
1616

17+
import io.opentelemetry.api.trace.SpanKind;
1718
import io.opentelemetry.context.Context;
1819
import io.opentelemetry.context.Scope;
1920
import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
2021
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
2122
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
2223
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
24+
import java.util.Locale;
2325
import java.util.Set;
26+
import java.util.logging.Level;
27+
import java.util.logging.Logger;
28+
import java.util.stream.Collectors;
2429
import net.bytebuddy.asm.Advice;
2530
import net.bytebuddy.description.type.TypeDescription;
2631
import net.bytebuddy.implementation.bytecode.assign.Assigner;
2732
import net.bytebuddy.matcher.ElementMatcher;
2833

2934
public class MethodInstrumentation implements TypeInstrumentation {
3035
private final String className;
31-
private final Set<String> methodNames;
36+
private final Set<String> internalMethodNames;
37+
private final Set<String> serverMethodNames;
38+
private final Set<String> clientMethodNames;
39+
40+
private static final Logger logger = Logger.getLogger(MethodInstrumentation.class.getName());
3241

3342
public MethodInstrumentation(String className, Set<String> methodNames) {
3443
this.className = className;
35-
this.methodNames = methodNames;
44+
this.internalMethodNames = filterMethodNames(className, methodNames, SpanKind.INTERNAL, true);
45+
this.serverMethodNames = filterMethodNames(className, methodNames, SpanKind.CLIENT, false);
46+
this.clientMethodNames = filterMethodNames(className, methodNames, SpanKind.SERVER, false);
47+
}
48+
49+
private static Set<String> filterMethodNames(
50+
String className, Set<String> methodNames, SpanKind kind, boolean isDefault) {
51+
String suffix = "=" + kind.name();
52+
Set<String> methods =
53+
methodNames.stream()
54+
.filter(
55+
methodName ->
56+
methodName.toUpperCase(Locale.ROOT).endsWith(suffix)
57+
|| (!methodName.contains("=") && isDefault))
58+
.map(
59+
methodName ->
60+
methodName.contains("=")
61+
? methodName.substring(0, methodName.indexOf('='))
62+
: methodName)
63+
.collect(Collectors.toSet());
64+
logMethods(className, methods, kind);
65+
return methods;
66+
}
67+
68+
private static void logMethods(String className, Set<String> methodNames, SpanKind spanKind) {
69+
if (!logger.isLoggable(Level.FINE) || methodNames.isEmpty()) {
70+
return;
71+
}
72+
logger.log(
73+
Level.FINE,
74+
"Tracing class {0} with methods {1} using span kind {2}",
75+
new Object[] {
76+
className,
77+
methodNames.stream()
78+
.map(name -> className + "." + name)
79+
.collect(Collectors.joining(", ")),
80+
spanKind.name()
81+
});
3682
}
3783

3884
@Override
@@ -55,6 +101,13 @@ public ElementMatcher<TypeDescription> typeMatcher() {
55101

56102
@Override
57103
public void transform(TypeTransformer transformer) {
104+
applyMethodTransformation(transformer, internalMethodNames, "$InternalMethodAdvice");
105+
applyMethodTransformation(transformer, clientMethodNames, "$ClientMethodAdvice");
106+
applyMethodTransformation(transformer, serverMethodNames, "$ServerMethodAdvice");
107+
}
108+
109+
private static void applyMethodTransformation(
110+
TypeTransformer transformer, Set<String> methodNames, String methodAdvice) {
58111
transformer.applyAdviceToMethod(
59112
namedOneOf(methodNames.toArray(new String[0])).and(isMethod()),
60113
mapping ->
@@ -63,24 +116,109 @@ public void transform(TypeTransformer transformer) {
63116
(instrumentedType, instrumentedMethod, assigner, argumentHandler, sort) ->
64117
Advice.OffsetMapping.Target.ForStackManipulation.of(
65118
instrumentedMethod.getReturnType().asErasure())),
66-
MethodInstrumentation.class.getName() + "$MethodAdvice");
119+
MethodInstrumentation.class.getName() + methodAdvice);
67120
}
68121

69122
// custom annotation that represents the return type of the method
70123
@interface MethodReturnType {}
71124

72125
@SuppressWarnings("unused")
73-
public static class MethodAdvice {
126+
public static class InternalMethodAdvice {
74127

75128
@Advice.OnMethodEnter(suppress = Throwable.class)
76129
public static void onEnter(
77130
@Advice.Origin("#t") Class<?> declaringClass,
78131
@Advice.Origin("#m") String methodName,
79-
@Advice.Local("otelMethod") ClassAndMethod classAndMethod,
132+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
80133
@Advice.Local("otelContext") Context context,
81134
@Advice.Local("otelScope") Scope scope) {
82135
Context parentContext = currentContext();
83-
classAndMethod = ClassAndMethod.create(declaringClass, methodName);
136+
classAndMethod =
137+
MethodAndType.create(
138+
ClassAndMethod.create(declaringClass, methodName), SpanKind.INTERNAL);
139+
140+
if (!instrumenter().shouldStart(parentContext, classAndMethod)) {
141+
return;
142+
}
143+
144+
context = instrumenter().start(parentContext, classAndMethod);
145+
scope = context.makeCurrent();
146+
}
147+
148+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
149+
public static void stopSpan(
150+
@MethodReturnType Class<?> methodReturnType,
151+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
152+
@Advice.Local("otelContext") Context context,
153+
@Advice.Local("otelScope") Scope scope,
154+
@Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
155+
@Advice.Thrown Throwable throwable) {
156+
if (scope == null) {
157+
return;
158+
}
159+
scope.close();
160+
161+
returnValue =
162+
AsyncOperationEndSupport.create(instrumenter(), Void.class, methodReturnType)
163+
.asyncEnd(context, classAndMethod, returnValue, throwable);
164+
}
165+
}
166+
167+
@SuppressWarnings("unused")
168+
public static class ClientMethodAdvice {
169+
170+
@Advice.OnMethodEnter(suppress = Throwable.class)
171+
public static void onEnter(
172+
@Advice.Origin("#t") Class<?> declaringClass,
173+
@Advice.Origin("#m") String methodName,
174+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
175+
@Advice.Local("otelContext") Context context,
176+
@Advice.Local("otelScope") Scope scope) {
177+
Context parentContext = currentContext();
178+
classAndMethod =
179+
MethodAndType.create(ClassAndMethod.create(declaringClass, methodName), SpanKind.CLIENT);
180+
181+
if (!instrumenter().shouldStart(parentContext, classAndMethod)) {
182+
return;
183+
}
184+
185+
context = instrumenter().start(parentContext, classAndMethod);
186+
scope = context.makeCurrent();
187+
}
188+
189+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
190+
public static void stopSpan(
191+
@MethodReturnType Class<?> methodReturnType,
192+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
193+
@Advice.Local("otelContext") Context context,
194+
@Advice.Local("otelScope") Scope scope,
195+
@Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
196+
@Advice.Thrown Throwable throwable) {
197+
if (scope == null) {
198+
return;
199+
}
200+
scope.close();
201+
202+
returnValue =
203+
AsyncOperationEndSupport.create(instrumenter(), Void.class, methodReturnType)
204+
.asyncEnd(context, classAndMethod, returnValue, throwable);
205+
}
206+
}
207+
208+
@SuppressWarnings("unused")
209+
public static class ServerMethodAdvice {
210+
211+
@Advice.OnMethodEnter(suppress = Throwable.class)
212+
public static void onEnter(
213+
@Advice.Origin("#t") Class<?> declaringClass,
214+
@Advice.Origin("#m") String methodName,
215+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
216+
@Advice.Local("otelContext") Context context,
217+
@Advice.Local("otelScope") Scope scope) {
218+
Context parentContext = currentContext();
219+
classAndMethod =
220+
MethodAndType.create(ClassAndMethod.create(declaringClass, methodName), SpanKind.SERVER);
221+
84222
if (!instrumenter().shouldStart(parentContext, classAndMethod)) {
85223
return;
86224
}
@@ -92,7 +230,7 @@ public static void onEnter(
92230
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
93231
public static void stopSpan(
94232
@MethodReturnType Class<?> methodReturnType,
95-
@Advice.Local("otelMethod") ClassAndMethod classAndMethod,
233+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
96234
@Advice.Local("otelContext") Context context,
97235
@Advice.Local("otelScope") Scope scope,
98236
@Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,

instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodInstrumentationModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public List<String> getAdditionalHelperClassNames() {
4646
return typeInstrumentations.isEmpty()
4747
? emptyList()
4848
: Arrays.asList(
49+
"io.opentelemetry.javaagent.instrumentation.methods.MethodAndType",
4950
"io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons",
51+
"io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons$1",
5052
"io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons$BootstrapLoader");
5153
}
5254

instrumentation/methods/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/methods/MethodSingletons.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,41 @@
99
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor;
1010
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter;
1111
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor;
12-
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
1312
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
14-
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
13+
import javax.annotation.Nullable;
1514

1615
public final class MethodSingletons {
1716
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.methods";
1817

19-
private static final Instrumenter<ClassAndMethod, Void> INSTRUMENTER;
18+
private static final Instrumenter<MethodAndType, Void> INSTRUMENTER;
2019
private static final ClassLoader bootstrapLoader = new BootstrapLoader();
2120

2221
static {
23-
CodeAttributesGetter<ClassAndMethod> codeAttributesGetter =
24-
ClassAndMethod.codeAttributesGetter();
22+
CodeAttributesGetter<MethodAndType> codeAttributesGetter =
23+
new CodeAttributesGetter<MethodAndType>() {
24+
@Nullable
25+
@Override
26+
public Class<?> getCodeClass(MethodAndType methodAndType) {
27+
return methodAndType.getClassAndMethod().declaringClass();
28+
}
29+
30+
@Nullable
31+
@Override
32+
public String getMethodName(MethodAndType methodAndType) {
33+
return methodAndType.getClassAndMethod().methodName();
34+
}
35+
};
2536

2637
INSTRUMENTER =
27-
Instrumenter.<ClassAndMethod, Void>builder(
38+
Instrumenter.<MethodAndType, Void>builder(
2839
GlobalOpenTelemetry.get(),
2940
INSTRUMENTATION_NAME,
3041
CodeSpanNameExtractor.create(codeAttributesGetter))
3142
.addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter))
32-
.buildInstrumenter(SpanKindExtractor.alwaysInternal());
43+
.buildInstrumenter(MethodAndType::getSpanKind);
3344
}
3445

35-
public static Instrumenter<ClassAndMethod, Void> instrumenter() {
46+
public static Instrumenter<MethodAndType, Void> instrumenter() {
3647
return INSTRUMENTER;
3748
}
3849

instrumentation/methods/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/methods/MethodTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ void bootLoaderMethodTraced() throws Exception {
6767
trace.hasSpansSatisfyingExactly(
6868
span ->
6969
span.hasName("InitialDirContext.search")
70-
.hasKind(SpanKind.INTERNAL)
70+
.hasKind(SpanKind.CLIENT)
7171
.hasException(throwableReference.get())
7272
.hasAttributesSatisfyingExactly(
7373
equalTo(CODE_NAMESPACE, InitialDirContext.class.getName()),
@@ -99,7 +99,7 @@ void methodTracedWithAsyncStop() throws Exception {
9999
trace.hasSpansSatisfyingExactly(
100100
span ->
101101
span.hasName("ConfigTracedCompletableFuture.getResult")
102-
.hasKind(SpanKind.INTERNAL)
102+
.hasKind(SpanKind.SERVER)
103103
.hasAttributesSatisfyingExactly(
104104
equalTo(CODE_NAMESPACE, ConfigTracedCompletableFuture.class.getName()),
105105
equalTo(CODE_FUNCTION, "getResult"))));

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/config/MethodsConfigurationParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public final class MethodsConfigurationParser {
1919
private static final Logger logger = Logger.getLogger(MethodsConfigurationParser.class.getName());
2020

2121
private static final String PACKAGE_CLASS_NAME_REGEX = "[\\w.$]+";
22-
private static final String METHOD_LIST_REGEX = "(?:\\s*\\w+\\s*,)*+(?:\\s*\\w+)?\\s*";
22+
private static final String METHOD_LIST_REGEX =
23+
"(?:\\s*\\w+(=\\w+)?\\s*,)*+(?:\\s*\\w+(=\\w+)?)?\\s*";
2324
private static final String CONFIG_FORMAT =
2425
PACKAGE_CLASS_NAME_REGEX + "(?:\\[" + METHOD_LIST_REGEX + "])?";
2526

0 commit comments

Comments
 (0)