Skip to content

Commit b464369

Browse files
authored
Improve spring data reactive instrumentation (#9561)
1 parent 5afe4d2 commit b464369

File tree

8 files changed

+271
-3
lines changed

8 files changed

+271
-3
lines changed

instrumentation/spring/spring-data/spring-data-1.8/javaagent/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ muzzle {
2626
dependencies {
2727
library("org.springframework.data:spring-data-commons:1.8.0.RELEASE")
2828
compileOnly("org.springframework:spring-aop:1.2")
29+
compileOnly(project(":instrumentation-annotations-support"))
2930

3031
testInstrumentation(project(":instrumentation:jdbc:javaagent"))
3132

instrumentation/spring/spring-data/spring-data-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/spring/data/v1_8/SpringDataInstrumentationModule.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.google.auto.service.AutoService;
1515
import io.opentelemetry.context.Context;
1616
import io.opentelemetry.context.Scope;
17+
import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
1718
import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod;
1819
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
1920
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
@@ -109,8 +110,8 @@ public Object invoke(MethodInvocation methodInvocation) throws Throwable {
109110
Context context = instrumenter().start(parentContext, classAndMethod);
110111
try (Scope ignored = context.makeCurrent()) {
111112
Object result = methodInvocation.proceed();
112-
instrumenter().end(context, classAndMethod, null, null);
113-
return result;
113+
return AsyncOperationEndSupport.create(instrumenter(), Void.class, method.getReturnType())
114+
.asyncEnd(context, classAndMethod, result, null);
114115
} catch (Throwable t) {
115116
instrumenter().end(context, classAndMethod, null, t);
116117
throw t;

instrumentation/spring/spring-data/spring-data-3.0/testing/build.gradle.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ plugins {
44

55
dependencies {
66
testInstrumentation(project(":instrumentation:jdbc:javaagent"))
7+
testInstrumentation(project(":instrumentation:r2dbc-1.0:javaagent"))
8+
testInstrumentation(project(":instrumentation:reactor:reactor-3.1:javaagent"))
79
testInstrumentation(project(":instrumentation:spring:spring-core-2.0:javaagent"))
810
testInstrumentation(project(":instrumentation:spring:spring-data:spring-data-1.8:javaagent"))
911

@@ -12,9 +14,12 @@ dependencies {
1214
testLibrary("org.hibernate.orm:hibernate-core:6.0.0.Final")
1315
testLibrary("org.springframework.data:spring-data-commons:3.0.0")
1416
testLibrary("org.springframework.data:spring-data-jpa:3.0.0")
17+
testLibrary("org.springframework.data:spring-data-r2dbc:3.0.0")
1518
testLibrary("org.springframework:spring-test:6.0.0")
1619

1720
testImplementation("org.hsqldb:hsqldb:2.0.0")
21+
testImplementation("com.h2database:h2:1.4.197")
22+
testImplementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
1823

1924
latestDepTestLibrary("org.hibernate.orm:hibernate-core:6.2.+")
2025
}
@@ -23,10 +28,27 @@ otelJava {
2328
minJavaVersionSupported.set(JavaVersion.VERSION_17)
2429
}
2530

31+
testing {
32+
suites {
33+
val reactiveTest by registering(JvmTestSuite::class) {
34+
dependencies {
35+
implementation("org.springframework.data:spring-data-r2dbc:3.0.0")
36+
implementation("org.testcontainers:testcontainers")
37+
implementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
38+
implementation("com.h2database:h2:1.4.197")
39+
}
40+
}
41+
}
42+
}
43+
2644
tasks {
2745
test {
2846
jvmArgs("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED")
2947
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
3048
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
3149
}
50+
51+
check {
52+
dependsOn(testing.suites)
53+
}
3254
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
9+
import static io.opentelemetry.semconv.SemanticAttributes.DB_CONNECTION_STRING;
10+
import static io.opentelemetry.semconv.SemanticAttributes.DB_NAME;
11+
import static io.opentelemetry.semconv.SemanticAttributes.DB_OPERATION;
12+
import static io.opentelemetry.semconv.SemanticAttributes.DB_SQL_TABLE;
13+
import static io.opentelemetry.semconv.SemanticAttributes.DB_STATEMENT;
14+
import static io.opentelemetry.semconv.SemanticAttributes.DB_SYSTEM;
15+
import static io.opentelemetry.semconv.SemanticAttributes.DB_USER;
16+
import static io.opentelemetry.semconv.SemanticAttributes.NET_PEER_NAME;
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import io.opentelemetry.api.trace.SpanKind;
20+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
21+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
22+
import io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository.CustomerRepository;
23+
import io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository.PersistenceConfig;
24+
import io.opentelemetry.semconv.SemanticAttributes;
25+
import java.time.Duration;
26+
import org.junit.jupiter.api.AfterAll;
27+
import org.junit.jupiter.api.BeforeAll;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.RegisterExtension;
30+
import org.springframework.context.ConfigurableApplicationContext;
31+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
32+
33+
@SuppressWarnings("deprecation") // until old http semconv are dropped in 2.0
34+
class ReactiveSpringDataTest {
35+
36+
@RegisterExtension
37+
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
38+
39+
private static ConfigurableApplicationContext applicationContext;
40+
private static CustomerRepository customerRepository;
41+
42+
@BeforeAll
43+
static void setUp() {
44+
applicationContext = new AnnotationConfigApplicationContext(PersistenceConfig.class);
45+
customerRepository = applicationContext.getBean(CustomerRepository.class);
46+
}
47+
48+
@AfterAll
49+
static void cleanUp() {
50+
applicationContext.close();
51+
}
52+
53+
@Test
54+
void testFindAll() {
55+
long count =
56+
testing
57+
.runWithSpan("parent", () -> customerRepository.findAll())
58+
.count()
59+
.block(Duration.ofSeconds(30));
60+
assertThat(count).isEqualTo(1);
61+
62+
testing.waitAndAssertTraces(
63+
trace ->
64+
trace.hasSpansSatisfyingExactly(
65+
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
66+
span ->
67+
span.hasName("CustomerRepository.findAll")
68+
.hasKind(SpanKind.INTERNAL)
69+
.hasAttributesSatisfyingExactly(
70+
equalTo(
71+
SemanticAttributes.CODE_NAMESPACE,
72+
CustomerRepository.class.getName()),
73+
equalTo(SemanticAttributes.CODE_FUNCTION, "findAll")),
74+
span ->
75+
span.hasName("SELECT db.CUSTOMER")
76+
.hasKind(SpanKind.CLIENT)
77+
.hasParent(trace.getSpan(1))
78+
// assert that this span ends before its parent span
79+
.satisfies(
80+
spanData ->
81+
assertThat(spanData.getEndEpochNanos())
82+
.isLessThanOrEqualTo(trace.getSpan(1).getEndEpochNanos()))
83+
.hasAttributesSatisfyingExactly(
84+
equalTo(DB_SYSTEM, "h2"),
85+
equalTo(DB_NAME, "db"),
86+
equalTo(DB_USER, "sa"),
87+
equalTo(DB_STATEMENT, "SELECT CUSTOMER.* FROM CUSTOMER"),
88+
equalTo(DB_OPERATION, "SELECT"),
89+
equalTo(DB_SQL_TABLE, "CUSTOMER"),
90+
equalTo(DB_CONNECTION_STRING, "h2:mem://localhost"),
91+
equalTo(NET_PEER_NAME, "localhost"))));
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository;
7+
8+
import java.util.Objects;
9+
import javax.annotation.Nullable;
10+
import org.springframework.data.annotation.Id;
11+
12+
public class Customer {
13+
14+
@Id private Long id;
15+
16+
private String firstName;
17+
private String lastName;
18+
19+
protected Customer() {}
20+
21+
public Customer(String firstName, String lastName) {
22+
this.firstName = firstName;
23+
this.lastName = lastName;
24+
}
25+
26+
public Long getId() {
27+
return id;
28+
}
29+
30+
public void setId(Long id) {
31+
this.id = id;
32+
}
33+
34+
public String getFirstName() {
35+
return firstName;
36+
}
37+
38+
public void setFirstName(String firstName) {
39+
this.firstName = firstName;
40+
}
41+
42+
public String getLastName() {
43+
return lastName;
44+
}
45+
46+
public void setLastName(String lastName) {
47+
this.lastName = lastName;
48+
}
49+
50+
@Override
51+
public String toString() {
52+
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
53+
}
54+
55+
@Override
56+
public boolean equals(@Nullable Object obj) {
57+
if (obj == this) {
58+
return true;
59+
}
60+
if (!(obj instanceof Customer)) {
61+
return false;
62+
}
63+
Customer other = (Customer) obj;
64+
return Objects.equals(id, other.id)
65+
&& Objects.equals(firstName, other.firstName)
66+
&& Objects.equals(lastName, other.lastName);
67+
}
68+
69+
@Override
70+
public int hashCode() {
71+
return Objects.hash(id, firstName, lastName);
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository;
7+
8+
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
9+
10+
public interface CustomerRepository extends ReactiveCrudRepository<Customer, Long> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository;
7+
8+
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
9+
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
10+
import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
11+
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
12+
import static io.r2dbc.spi.ConnectionFactoryOptions.PROTOCOL;
13+
import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
14+
15+
import io.r2dbc.spi.ConnectionFactories;
16+
import io.r2dbc.spi.ConnectionFactory;
17+
import io.r2dbc.spi.ConnectionFactoryOptions;
18+
import io.r2dbc.spi.Option;
19+
import java.nio.charset.StandardCharsets;
20+
import org.springframework.context.annotation.Bean;
21+
import org.springframework.core.io.ByteArrayResource;
22+
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
23+
import org.springframework.data.r2dbc.dialect.H2Dialect;
24+
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
25+
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
26+
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
27+
import org.springframework.r2dbc.core.DatabaseClient;
28+
29+
@EnableR2dbcRepositories(
30+
basePackages = "io.opentelemetry.javaagent.instrumentation.spring.data.v3_0.repository")
31+
public class PersistenceConfig {
32+
33+
@Bean
34+
ConnectionFactory connectionFactory() {
35+
return ConnectionFactories.find(
36+
ConnectionFactoryOptions.builder()
37+
.option(DRIVER, "h2")
38+
.option(PROTOCOL, "mem")
39+
.option(HOST, "localhost")
40+
.option(USER, "sa")
41+
.option(PASSWORD, "")
42+
.option(DATABASE, "db")
43+
.option(Option.valueOf("DB_CLOSE_DELAY"), "-1")
44+
.build());
45+
}
46+
47+
@Bean
48+
ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
49+
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
50+
initializer.setConnectionFactory(connectionFactory);
51+
initializer.setDatabasePopulator(
52+
new ResourceDatabasePopulator(
53+
new ByteArrayResource(
54+
("CREATE TABLE customer (id INT PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL);"
55+
+ "INSERT INTO customer (id, firstname, lastname) VALUES ('1', 'First', 'Last');")
56+
.getBytes(StandardCharsets.UTF_8))));
57+
58+
return initializer;
59+
}
60+
61+
@Bean
62+
public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) {
63+
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
64+
65+
return new R2dbcEntityTemplate(databaseClient, H2Dialect.INSTANCE);
66+
}
67+
}

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ignore/AdditionalLibraryIgnoredTypesConfigurer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ public void configure(IgnoredTypesBuilder builder) {
154154
.allowClass("org.springframework.core.task.")
155155
.allowClass("org.springframework.core.DecoratingClassLoader")
156156
.allowClass("org.springframework.core.OverridingClassLoader")
157-
.allowClass("org.springframework.core.ReactiveAdapterRegistry$EmptyCompletableFuture");
157+
.allowClass("org.springframework.core.ReactiveAdapterRegistry$EmptyCompletableFuture")
158+
.allowClass("org.springframework.core.io.buffer.DataBufferUtils$");
158159

159160
builder
160161
.ignoreClass("org.springframework.instrument.")

0 commit comments

Comments
 (0)