Skip to content

Commit fbad8bb

Browse files
committed
use declarative config
1 parent 6997acc commit fbad8bb

File tree

14 files changed

+303
-58
lines changed

14 files changed

+303
-58
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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ 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=SERVER];javax.naming.directory.InitialDirContext[search=CLIENT]"
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]"
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
10+
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
11+
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
12+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
13+
14+
import io.opentelemetry.api.trace.Span;
15+
import io.opentelemetry.api.trace.SpanKind;
16+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
17+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
18+
import java.util.concurrent.Callable;
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
import javax.naming.NoInitialContextException;
24+
import javax.naming.directory.InitialDirContext;
25+
import javax.naming.ldap.InitialLdapContext;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.RegisterExtension;
28+
29+
@SuppressWarnings("deprecation") // using deprecated semconv
30+
class MethodTest {
31+
32+
@RegisterExtension
33+
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
34+
35+
@SuppressWarnings("deprecation") // using deprecated semconv
36+
@Test
37+
void methodTraced() {
38+
assertThat(new ConfigTracedCallable().call()).isEqualTo("Hello!");
39+
testing.waitAndAssertTraces(
40+
trace ->
41+
trace.hasSpansSatisfyingExactly(
42+
span ->
43+
span.hasName("ConfigTracedCallable.call")
44+
.hasKind(SpanKind.INTERNAL)
45+
.hasAttributesSatisfyingExactly(
46+
equalTo(CODE_NAMESPACE, ConfigTracedCallable.class.getName()),
47+
equalTo(CODE_FUNCTION, "call"))));
48+
}
49+
50+
@Test
51+
void bootLoaderMethodTraced() throws Exception {
52+
InitialLdapContext context = new InitialLdapContext();
53+
AtomicReference<Throwable> throwableReference = new AtomicReference<>();
54+
assertThatThrownBy(
55+
() -> {
56+
try {
57+
context.search("foo", null);
58+
} catch (Throwable throwable) {
59+
throwableReference.set(throwable);
60+
throw throwable;
61+
}
62+
})
63+
.isInstanceOf(NoInitialContextException.class);
64+
65+
testing.waitAndAssertTraces(
66+
trace ->
67+
trace.hasSpansSatisfyingExactly(
68+
span ->
69+
span.hasName("InitialDirContext.search")
70+
.hasKind(SpanKind.CLIENT)
71+
.hasException(throwableReference.get())
72+
.hasAttributesSatisfyingExactly(
73+
equalTo(CODE_NAMESPACE, InitialDirContext.class.getName()),
74+
equalTo(CODE_FUNCTION, "search"))));
75+
}
76+
77+
static class ConfigTracedCallable implements Callable<String> {
78+
79+
@Override
80+
public String call() {
81+
return "Hello!";
82+
}
83+
}
84+
85+
@Test
86+
void methodTracedWithAsyncStop() throws Exception {
87+
ConfigTracedCompletableFuture traced = new ConfigTracedCompletableFuture();
88+
CompletableFuture<String> future = traced.getResult();
89+
90+
// span is ended when CompletableFuture is completed
91+
// verify that span has not been ended yet
92+
assertThat(traced.span).isNotNull().satisfies(span -> assertThat(span.isRecording()).isTrue());
93+
94+
traced.countDownLatch.countDown();
95+
assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("Hello!");
96+
97+
testing.waitAndAssertTraces(
98+
trace ->
99+
trace.hasSpansSatisfyingExactly(
100+
span ->
101+
span.hasName("ConfigTracedCompletableFuture.getResult")
102+
.hasKind(SpanKind.SERVER)
103+
.hasAttributesSatisfyingExactly(
104+
equalTo(CODE_NAMESPACE, ConfigTracedCompletableFuture.class.getName()),
105+
equalTo(CODE_FUNCTION, "getResult"))));
106+
}
107+
108+
static class ConfigTracedCompletableFuture {
109+
final CountDownLatch countDownLatch = new CountDownLatch(1);
110+
Span span;
111+
112+
CompletableFuture<String> getResult() {
113+
CompletableFuture<String> completableFuture = new CompletableFuture<>();
114+
span = Span.current();
115+
new Thread(
116+
() -> {
117+
try {
118+
countDownLatch.await();
119+
} catch (InterruptedException exception) {
120+
// ignore
121+
}
122+
completableFuture.complete("Hello!");
123+
})
124+
.start();
125+
return completableFuture;
126+
}
127+
}
128+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
file_format: "0.4"
2+
instrumentation/development:
3+
java:
4+
methods:
5+
include:
6+
- class: io.opentelemetry.javaagent.instrumentation.methods.MethodTest$ConfigTracedCallable
7+
methods:
8+
- name: call
9+
- class: io.opentelemetry.javaagent.instrumentation.methods.MethodTest$ConfigTracedCompletableFuture
10+
methods:
11+
- name: getResult
12+
span_kind: SERVER
13+
- class: javax.naming.directory.InitialDirContext
14+
methods:
15+
- name: search
16+
span_kind: CLIENT

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

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
2222
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
2323
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
24-
import java.util.Locale;
2524
import java.util.Set;
26-
import java.util.logging.Level;
27-
import java.util.logging.Logger;
28-
import java.util.stream.Collectors;
2925
import net.bytebuddy.asm.Advice;
3026
import net.bytebuddy.description.type.TypeDescription;
3127
import net.bytebuddy.implementation.bytecode.assign.Assigner;
@@ -37,48 +33,12 @@ public class MethodInstrumentation implements TypeInstrumentation {
3733
private final Set<String> serverMethodNames;
3834
private final Set<String> clientMethodNames;
3935

40-
private static final Logger logger = Logger.getLogger(MethodInstrumentation.class.getName());
41-
42-
public MethodInstrumentation(String className, Set<String> methodNames) {
36+
public MethodInstrumentation(String className, Set<String> internalMethodNames,
37+
Set<String> serverMethodNames, Set<String> clientMethodNames) {
4338
this.className = className;
44-
this.internalMethodNames = filterMethodNames(className, methodNames, SpanKind.INTERNAL, true);
45-
this.serverMethodNames = filterMethodNames(className, methodNames, SpanKind.SERVER, false);
46-
this.clientMethodNames = filterMethodNames(className, methodNames, SpanKind.CLIENT, 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-
});
39+
this.internalMethodNames = internalMethodNames;
40+
this.serverMethodNames = serverMethodNames;
41+
this.clientMethodNames = clientMethodNames;
8242
}
8343

8444
@Override

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
import static java.util.Collections.emptyList;
99

1010
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
1112
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
1213
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
1314
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1415
import io.opentelemetry.javaagent.tooling.config.MethodsConfigurationParser;
1516
import java.util.Arrays;
17+
import java.util.Collections;
1618
import java.util.List;
1719
import java.util.Map;
1820
import java.util.Set;
@@ -28,15 +30,24 @@ public class MethodInstrumentationModule extends InstrumentationModule {
2830
public MethodInstrumentationModule() {
2931
super("methods");
3032

33+
DeclarativeConfigProperties methods =
34+
AgentInstrumentationConfig.get().getDeclarativeConfig("methods");
35+
typeInstrumentations =
36+
methods != null ? MethodsConfig.parseDeclarativeConfig(methods) : parseConfigProperties();
37+
}
38+
39+
private static List<TypeInstrumentation> parseConfigProperties() {
3140
Map<String, Set<String>> classMethodsToTrace =
3241
MethodsConfigurationParser.parse(
3342
AgentInstrumentationConfig.get().getString(TRACE_METHODS_CONFIG));
3443

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());
44+
return classMethodsToTrace.entrySet().stream()
45+
.filter(e -> !e.getValue().isEmpty())
46+
.map(
47+
e ->
48+
new MethodInstrumentation(
49+
e.getKey(), e.getValue(), Collections.emptySet(), Collections.emptySet()))
50+
.collect(Collectors.toList());
4051
}
4152

4253
// the default configuration has empty "otel.instrumentation.methods.include", and so doesn't
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.opentelemetry.javaagent.instrumentation.methods;
2+
3+
import static java.util.Collections.emptyList;
4+
5+
import com.google.common.base.Strings;
6+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
7+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
8+
import java.util.HashSet;
9+
import java.util.List;
10+
import java.util.Set;
11+
import java.util.logging.Level;
12+
import java.util.logging.Logger;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
public class MethodsConfig {
17+
18+
private static final Logger logger = Logger.getLogger(MethodsConfig.class.getName());
19+
20+
private MethodsConfig() {}
21+
22+
static List<TypeInstrumentation> parseDeclarativeConfig(DeclarativeConfigProperties methods) {
23+
return methods.getStructuredList("include", emptyList()).stream()
24+
.flatMap(MethodsConfig::parseMethodInstrumentation)
25+
.collect(Collectors.toList());
26+
}
27+
28+
private static Stream<MethodInstrumentation> parseMethodInstrumentation(
29+
DeclarativeConfigProperties config) {
30+
String clazz = config.getString("class");
31+
if (Strings.isNullOrEmpty(clazz)) {
32+
logger.log(Level.WARNING, "Invalid methods configuration - class name missing: {0}", config);
33+
return Stream.empty();
34+
}
35+
Set<String> internal = new HashSet<>();
36+
Set<String> server = new HashSet<>();
37+
Set<String> client = new HashSet<>();
38+
39+
List<DeclarativeConfigProperties> methods = config.getStructuredList("methods", emptyList());
40+
for (DeclarativeConfigProperties method : methods) {
41+
String methodName = method.getString("name");
42+
if (Strings.isNullOrEmpty(methodName)) {
43+
logger.log(
44+
Level.WARNING, "Invalid methods configuration - method name missing: {0}", method);
45+
continue;
46+
}
47+
String spanKind = method.getString("span_kind", "internal");
48+
if ("internal".equalsIgnoreCase(spanKind)) {
49+
internal.add(methodName);
50+
} else if ("server".equalsIgnoreCase(spanKind)) {
51+
server.add(methodName);
52+
} else if ("client".equalsIgnoreCase(spanKind)) {
53+
client.add(methodName);
54+
} else {
55+
logger.log(Level.WARNING, "Invalid methods configuration - unknown span_kind: {0}", method);
56+
}
57+
}
58+
59+
if (internal.isEmpty() && server.isEmpty() && client.isEmpty()) {
60+
logger.log(Level.WARNING, "Invalid methods configuration - no methods defined: {0}", config);
61+
return Stream.empty();
62+
}
63+
64+
return Stream.of(new MethodInstrumentation(clazz, internal, server, client));
65+
}
66+
}

0 commit comments

Comments
 (0)