Skip to content

Commit 4251217

Browse files
zeitlingerlauritotelbot[bot]
authored
Span kind for method instrumentation / Declarative configuration tooling (#14014)
Co-authored-by: Lauri Tulmin <[email protected]> Co-authored-by: otelbot <[email protected]>
1 parent 7689228 commit 4251217

File tree

29 files changed

+585
-48
lines changed

29 files changed

+585
-48
lines changed

instrumentation-api-incubator/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ group = "io.opentelemetry.instrumentation"
1313
dependencies {
1414
api("io.opentelemetry.semconv:opentelemetry-semconv")
1515
api(project(":instrumentation-api"))
16-
implementation("io.opentelemetry:opentelemetry-api-incubator")
16+
api("io.opentelemetry:opentelemetry-api-incubator")
1717

1818
compileOnly("com.google.auto.value:auto-value-annotations")
1919
annotationProcessor("com.google.auto.value:auto-value")

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/InstrumentationConfig.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import static java.util.Collections.emptyList;
99

10+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
1011
import java.time.Duration;
1112
import java.util.List;
1213
import java.util.Map;
@@ -107,4 +108,18 @@ default List<String> getList(String name) {
107108
* {@code key=value,anotherKey=anotherValue}. The returned map is unmodifiable.
108109
*/
109110
Map<String, String> getMap(String name, Map<String, String> defaultValue);
111+
112+
/**
113+
* Returns a {@link DeclarativeConfigProperties} for the given instrumentation name, or {@code
114+
* null} if no declarative configuration is available for that instrumentation.
115+
*
116+
* <p>Declarative configuration is used to configure instrumentation properties in a declarative
117+
* way, such as through YAML or JSON files.
118+
*
119+
* @param instrumentationName the name of the instrumentation
120+
* @return the declarative configuration properties for the given instrumentation name, or {@code
121+
* null} if not available
122+
*/
123+
@Nullable
124+
DeclarativeConfigProperties getDeclarativeConfig(String instrumentationName);
110125
}

instrumentation/methods/javaagent/build.gradle.kts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,30 @@ dependencies {
1313
compileOnly(project(":instrumentation-annotations-support"))
1414
}
1515

16-
tasks.withType<Test>().configureEach {
16+
tasks.test {
1717
jvmArgs(
1818
"-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]"
1919
)
2020
}
21+
22+
testing {
23+
suites {
24+
val declarativeConfigTest by registering(JvmTestSuite::class) {
25+
targets {
26+
all {
27+
testTask.configure {
28+
jvmArgs(
29+
"-Dotel.experimental.config.file=$projectDir/src/declarativeConfigTest/resources/declarative-config.yaml"
30+
)
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}
37+
38+
tasks {
39+
check {
40+
dependsOn(testing.suites)
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 static io.opentelemetry.instrumentation.testing.junit.code.SemconvCodeStabilityUtil.codeFunctionAssertions;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
10+
11+
import io.opentelemetry.api.trace.SpanKind;
12+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
13+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
14+
import java.util.concurrent.Callable;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
@SuppressWarnings("deprecation") // using deprecated semconv
19+
class MethodTest {
20+
21+
@RegisterExtension
22+
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
23+
24+
@SuppressWarnings("deprecation") // using deprecated semconv
25+
@Test
26+
void methodTraced() {
27+
assertThat(new ConfigTracedCallable().call()).isEqualTo("Hello!");
28+
testing.waitAndAssertTraces(
29+
trace ->
30+
trace.hasSpansSatisfyingExactly(
31+
span ->
32+
span.hasName("ConfigTracedCallable.call")
33+
.hasKind(SpanKind.SERVER)
34+
.hasAttributesSatisfyingExactly(
35+
codeFunctionAssertions(ConfigTracedCallable.class, "call"))));
36+
}
37+
38+
static class ConfigTracedCallable implements Callable<String> {
39+
40+
@Override
41+
public String call() {
42+
return "Hello!";
43+
}
44+
}
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
file_format: "0.4"
2+
tracer_provider:
3+
processors:
4+
- simple:
5+
exporter:
6+
test:
7+
- simple:
8+
exporter:
9+
console:
10+
11+
logger_provider:
12+
processors:
13+
- simple:
14+
exporter:
15+
test:
16+
17+
meter_provider:
18+
readers:
19+
- periodic:
20+
# Set really long interval. We'll call forceFlush when we need the metrics
21+
# instead of collecting them periodically.
22+
interval: 1000000
23+
exporter:
24+
test:
25+
26+
instrumentation/development:
27+
java:
28+
methods:
29+
include:
30+
- class: io.opentelemetry.javaagent.instrumentation.methods.MethodTest$ConfigTracedCallable
31+
methods:
32+
- name: call
33+
span_kind: SERVER
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: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,41 @@
1010
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
1111
import static io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons.getBootstrapLoader;
1212
import static io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons.instrumenter;
13+
import static net.bytebuddy.matcher.ElementMatchers.any;
1314
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
1415
import static net.bytebuddy.matcher.ElementMatchers.named;
1516
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
17+
import static net.bytebuddy.matcher.ElementMatchers.none;
1618

19+
import io.opentelemetry.api.trace.SpanKind;
1720
import io.opentelemetry.context.Context;
1821
import io.opentelemetry.context.Scope;
1922
import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
2023
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
2124
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
2225
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
23-
import java.util.Set;
26+
import java.util.Collection;
27+
import java.util.Map;
2428
import net.bytebuddy.asm.Advice;
29+
import net.bytebuddy.description.enumeration.EnumerationDescription;
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 Map<SpanKind, Collection<String>> methodNames;
3237

33-
public MethodInstrumentation(String className, Set<String> methodNames) {
38+
public MethodInstrumentation(String className, Map<SpanKind, Collection<String>> methodNames) {
3439
this.className = className;
3540
this.methodNames = methodNames;
3641
}
3742

3843
@Override
3944
public ElementMatcher<ClassLoader> classLoaderOptimization() {
45+
if (className == null) {
46+
return any();
47+
}
4048
ElementMatcher<ClassLoader> delegate = hasClassesNamed(className);
4149
return target -> {
4250
// hasClassesNamed does not support null class loader, so we provide a custom loader that
@@ -50,37 +58,51 @@ public ElementMatcher<ClassLoader> classLoaderOptimization() {
5058

5159
@Override
5260
public ElementMatcher<TypeDescription> typeMatcher() {
53-
return hasSuperType(named(className));
61+
return className == null ? none() : hasSuperType(named(className));
5462
}
5563

5664
@Override
5765
public void transform(TypeTransformer transformer) {
58-
transformer.applyAdviceToMethod(
59-
namedOneOf(methodNames.toArray(new String[0])).and(isMethod()),
60-
mapping ->
61-
mapping.bind(
62-
MethodReturnType.class,
63-
(instrumentedType, instrumentedMethod, assigner, argumentHandler, sort) ->
64-
Advice.OffsetMapping.Target.ForStackManipulation.of(
65-
instrumentedMethod.getReturnType().asErasure())),
66-
MethodInstrumentation.class.getName() + "$MethodAdvice");
66+
for (Map.Entry<SpanKind, Collection<String>> entry : methodNames.entrySet()) {
67+
SpanKind spanKind = entry.getKey();
68+
Collection<String> names = entry.getValue();
69+
transformer.applyAdviceToMethod(
70+
namedOneOf(names.toArray(new String[0])).and(isMethod()),
71+
mapping ->
72+
mapping
73+
.bind(
74+
MethodReturnType.class,
75+
(instrumentedType, instrumentedMethod, assigner, argumentHandler, sort) ->
76+
Advice.OffsetMapping.Target.ForStackManipulation.of(
77+
instrumentedMethod.getReturnType().asErasure()))
78+
.bind(
79+
MethodSpanKind.class,
80+
new EnumerationDescription.ForLoadedEnumeration(spanKind)),
81+
MethodInstrumentation.class.getName() + "$MethodAdvice");
82+
}
6783
}
6884

6985
// custom annotation that represents the return type of the method
7086
@interface MethodReturnType {}
7187

88+
// custom annotation that represents the SpanKind of the method
89+
@interface MethodSpanKind {}
90+
7291
@SuppressWarnings("unused")
7392
public static class MethodAdvice {
7493

7594
@Advice.OnMethodEnter(suppress = Throwable.class)
7695
public static void onEnter(
96+
@MethodSpanKind SpanKind spanKind,
7797
@Advice.Origin("#t") Class<?> declaringClass,
7898
@Advice.Origin("#m") String methodName,
79-
@Advice.Local("otelMethod") ClassAndMethod classAndMethod,
99+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
80100
@Advice.Local("otelContext") Context context,
81101
@Advice.Local("otelScope") Scope scope) {
82102
Context parentContext = currentContext();
83-
classAndMethod = ClassAndMethod.create(declaringClass, methodName);
103+
classAndMethod =
104+
MethodAndType.create(ClassAndMethod.create(declaringClass, methodName), spanKind);
105+
84106
if (!instrumenter().shouldStart(parentContext, classAndMethod)) {
85107
return;
86108
}
@@ -92,7 +114,7 @@ public static void onEnter(
92114
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
93115
public static void stopSpan(
94116
@MethodReturnType Class<?> methodReturnType,
95-
@Advice.Local("otelMethod") ClassAndMethod classAndMethod,
117+
@Advice.Local("otelMethod") MethodAndType classAndMethod,
96118
@Advice.Local("otelContext") Context context,
97119
@Advice.Local("otelScope") Scope scope,
98120
@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: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
package io.opentelemetry.javaagent.instrumentation.methods;
77

88
import static java.util.Collections.emptyList;
9+
import static java.util.Collections.singletonList;
10+
import static java.util.Collections.singletonMap;
911

1012
import com.google.auto.service.AutoService;
13+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
14+
import io.opentelemetry.api.trace.SpanKind;
1115
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
1216
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
1317
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1418
import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser;
15-
import java.util.Arrays;
1619
import java.util.List;
1720
import java.util.Map;
1821
import java.util.Set;
@@ -27,27 +30,35 @@ public class MethodInstrumentationModule extends InstrumentationModule {
2730

2831
public MethodInstrumentationModule() {
2932
super("methods");
33+
typeInstrumentations = createInstrumentations();
34+
}
35+
36+
private static List<TypeInstrumentation> createInstrumentations() {
37+
DeclarativeConfigProperties methods =
38+
AgentInstrumentationConfig.get().getDeclarativeConfig("methods");
39+
List<TypeInstrumentation> list =
40+
methods != null ? MethodsConfig.parseDeclarativeConfig(methods) : parseConfigProperties();
41+
// ensure that there is at least one instrumentation so that muzzle reference collection could
42+
// work
43+
if (list.isEmpty()) {
44+
return singletonList(
45+
new MethodInstrumentation(null, singletonMap(SpanKind.INTERNAL, emptyList())));
46+
}
47+
return list;
48+
}
3049

50+
private static List<TypeInstrumentation> parseConfigProperties() {
3151
Map<String, Set<String>> classMethodsToTrace =
3252
MethodsConfigurationParser.parse(
3353
AgentInstrumentationConfig.get().getString(TRACE_METHODS_CONFIG));
3454

35-
typeInstrumentations =
36-
classMethodsToTrace.entrySet().stream()
37-
.filter(e -> !e.getValue().isEmpty())
38-
.map(e -> new MethodInstrumentation(e.getKey(), e.getValue()))
39-
.collect(Collectors.toList());
40-
}
41-
42-
// the default configuration has empty "otel.instrumentation.methods.include", and so doesn't
43-
// generate any TypeInstrumentation for muzzle to analyze
44-
@Override
45-
public List<String> getAdditionalHelperClassNames() {
46-
return typeInstrumentations.isEmpty()
47-
? emptyList()
48-
: Arrays.asList(
49-
"io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons",
50-
"io.opentelemetry.javaagent.instrumentation.methods.MethodSingletons$BootstrapLoader");
55+
return classMethodsToTrace.entrySet().stream()
56+
.filter(e -> !e.getValue().isEmpty())
57+
.map(
58+
e ->
59+
new MethodInstrumentation(
60+
e.getKey(), singletonMap(SpanKind.INTERNAL, e.getValue())))
61+
.collect(Collectors.toList());
5162
}
5263

5364
@Override

0 commit comments

Comments
 (0)