Add test for hibernate-reactive (#9225)

This commit is contained in:
Lauri Tulmin 2023-08-17 21:01:11 +03:00 committed by GitHub
parent 2f8db1b443
commit 42e2f738cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 566 additions and 5 deletions

View File

@ -25,7 +25,7 @@ public class HibernateInstrumentationModule extends InstrumentationModule {
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed(
// not present before 6.0
"org.hibernate.query.Query");
"org.hibernate.query.spi.SqmQuery");
}
@Override

View File

@ -15,10 +15,67 @@ dependencies {
library("io.vertx:vertx-sql-client:4.0.0")
compileOnly("io.vertx:vertx-codegen:4.0.0")
// for hibernateReactive2Test
testInstrumentation(project(":instrumentation:hibernate:hibernate-6.0:javaagent"))
testLibrary("io.vertx:vertx-pg-client:4.0.0")
testLibrary("io.vertx:vertx-codegen:4.0.0")
}
tasks.withType<Test>().configureEach {
usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
val latestDepTest = findProperty("testLatestDeps") as Boolean
testing {
suites {
val hibernateReactive1Test by registering(JvmTestSuite::class) {
dependencies {
implementation("org.testcontainers:testcontainers")
if (latestDepTest) {
implementation("org.hibernate.reactive:hibernate-reactive-core:1.+")
implementation("io.vertx:vertx-pg-client:+")
} else {
implementation("org.hibernate.reactive:hibernate-reactive-core:1.0.0.Final")
implementation("io.vertx:vertx-pg-client:4.1.5")
}
}
}
val hibernateReactive2Test by registering(JvmTestSuite::class) {
dependencies {
implementation("org.testcontainers:testcontainers")
if (latestDepTest) {
implementation("org.hibernate.reactive:hibernate-reactive-core:2.+")
implementation("io.vertx:vertx-pg-client:+")
} else {
implementation("org.hibernate.reactive:hibernate-reactive-core:2.0.0.Final")
implementation("io.vertx:vertx-pg-client:4.4.2")
}
}
}
}
}
tasks {
withType<Test>().configureEach {
usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
}
named("compileHibernateReactive2TestJava", JavaCompile::class).configure {
options.release.set(11)
}
val testJavaVersion =
gradle.startParameter.projectProperties.get("testJavaVersion")?.let(JavaVersion::toVersion)
?: JavaVersion.current()
if (testJavaVersion.isJava8) {
named("hibernateReactive2Test", Test::class).configure {
enabled = false
}
if (latestDepTest) {
named("hibernateReactive1Test", Test::class).configure {
enabled = false
}
}
}
check {
dependsOn(testing.suites)
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.hibernate.reactive.mutiny.Mutiny;
import org.hibernate.reactive.stage.Stage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
class HibernateReactiveTest {
private static final Logger logger = LoggerFactory.getLogger(HibernateReactiveTest.class);
private static final String USER_DB = "SA";
private static final String PW_DB = "password123";
private static final String DB = "tempdb";
@RegisterExtension
protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
private static GenericContainer<?> container;
private static int port;
private static EntityManagerFactory entityManagerFactory;
private static Mutiny.SessionFactory mutinySessionFactory;
private static Stage.SessionFactory stageSessionFactory;
@BeforeAll
static void setUp() {
container =
new GenericContainer<>("postgres:9.6.8")
.withEnv("POSTGRES_USER", USER_DB)
.withEnv("POSTGRES_PASSWORD", PW_DB)
.withEnv("POSTGRES_DB", DB)
.withExposedPorts(5432)
.withLogConsumer(new Slf4jLogConsumer(logger))
.withStartupTimeout(Duration.ofMinutes(2));
container.start();
port = container.getMappedPort(5432);
System.setProperty("db.port", String.valueOf(port));
entityManagerFactory = Persistence.createEntityManagerFactory("test-pu");
Value value = new Value("name");
value.setId(1L);
mutinySessionFactory = entityManagerFactory.unwrap(Mutiny.SessionFactory.class);
stageSessionFactory = entityManagerFactory.unwrap(Stage.SessionFactory.class);
mutinySessionFactory
.withTransaction((session, tx) -> session.merge(value))
.await()
.atMost(Duration.ofSeconds(30));
}
@AfterAll
static void cleanUp() {
if (entityManagerFactory != null) {
entityManagerFactory.close();
}
if (mutinySessionFactory != null) {
mutinySessionFactory.close();
}
if (stageSessionFactory != null) {
stageSessionFactory.close();
}
container.stop();
}
@Test
void testMutiny() {
testing.runWithSpan(
"parent",
() -> {
mutinySessionFactory
.withSession(
session ->
session
.find(Value.class, 1L)
.invoke(
value -> {
testing.runWithSpan("callback", () -> {});
}))
.await()
.atMost(Duration.ofSeconds(30));
});
assertTrace();
}
@Test
void testStage() throws Exception {
testing
.runWithSpan(
"parent",
() ->
stageSessionFactory
.withSession(
session ->
session
.find(Value.class, 1L)
.thenAccept(
value -> {
testing.runWithSpan("callback", () -> {});
}))
.toCompletableFuture())
.get(30, TimeUnit.SECONDS);
assertTrace();
}
private static void assertTrace() {
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
span ->
span.hasName("SELECT tempdb.Value")
.hasKind(SpanKind.CLIENT)
.hasParent(trace.getSpan(0))
.hasAttributesSatisfyingExactly(
equalTo(DB_NAME, DB),
equalTo(DB_USER, USER_DB),
equalTo(
DB_STATEMENT,
"select value0_.id as id1_0_0_, value0_.name as name2_0_0_ from Value value0_ where value0_.id=$?"),
equalTo(DB_OPERATION, "SELECT"),
equalTo(DB_SQL_TABLE, "Value"),
equalTo(NET_PEER_NAME, "localhost"),
equalTo(NET_PEER_PORT, port)),
span ->
span.hasName("callback")
.hasKind(SpanKind.INTERNAL)
.hasParent(trace.getSpan(0))));
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table
public class Value {
private Long id;
private String name;
public Value() {}
public Value(String name) {
this.name = name;
}
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String title) {
name = title;
}
}

View File

@ -0,0 +1,20 @@
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="test-pu">
<provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>
<class>io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v1_0.Value</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://localhost:${db.port}/tempdb"/>
<property name="jakarta.persistence.jdbc.user" value="SA"/>
<property name="jakarta.persistence.jdbc.password" value="password123"/>
<property name="hibernate.connection.pool_size" value="10"/>
<property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>

View File

@ -0,0 +1,160 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME;
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.hibernate.reactive.mutiny.Mutiny;
import org.hibernate.reactive.stage.Stage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
class HibernateReactiveTest {
private static final Logger logger = LoggerFactory.getLogger(HibernateReactiveTest.class);
private static final String USER_DB = "SA";
private static final String PW_DB = "password123";
private static final String DB = "tempdb";
@RegisterExtension
protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
private static GenericContainer<?> container;
private static int port;
private static EntityManagerFactory entityManagerFactory;
private static Mutiny.SessionFactory mutinySessionFactory;
private static Stage.SessionFactory stageSessionFactory;
@BeforeAll
static void setUp() {
container =
new GenericContainer<>("postgres:9.6.8")
.withEnv("POSTGRES_USER", USER_DB)
.withEnv("POSTGRES_PASSWORD", PW_DB)
.withEnv("POSTGRES_DB", DB)
.withExposedPorts(5432)
.withLogConsumer(new Slf4jLogConsumer(logger))
.withStartupTimeout(Duration.ofMinutes(2));
container.start();
port = container.getMappedPort(5432);
System.setProperty("db.port", String.valueOf(port));
entityManagerFactory = Persistence.createEntityManagerFactory("test-pu");
Value value = new Value("name");
value.setId(1L);
mutinySessionFactory = entityManagerFactory.unwrap(Mutiny.SessionFactory.class);
stageSessionFactory = entityManagerFactory.unwrap(Stage.SessionFactory.class);
mutinySessionFactory
.withTransaction((session, tx) -> session.merge(value))
.await()
.atMost(Duration.ofSeconds(30));
}
@AfterAll
static void cleanUp() {
if (entityManagerFactory != null) {
entityManagerFactory.close();
}
if (mutinySessionFactory != null) {
mutinySessionFactory.close();
}
if (stageSessionFactory != null) {
stageSessionFactory.close();
}
container.stop();
}
@Test
void testMutiny() {
testing.runWithSpan(
"parent",
() -> {
mutinySessionFactory
.withSession(
session ->
session
.find(Value.class, 1L)
.invoke(
value -> {
testing.runWithSpan("callback", () -> {});
}))
.await()
.atMost(Duration.ofSeconds(30));
});
assertTrace();
}
@Test
void testStage() throws Exception {
testing
.runWithSpan(
"parent",
() ->
stageSessionFactory
.withSession(
session ->
session
.find(Value.class, 1L)
.thenAccept(
value -> {
testing.runWithSpan("callback", () -> {});
}))
.toCompletableFuture())
.get(30, TimeUnit.SECONDS);
assertTrace();
}
private static void assertTrace() {
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
span ->
span.hasName("SELECT tempdb.Value")
.hasKind(SpanKind.CLIENT)
.hasParent(trace.getSpan(0))
.hasAttributesSatisfyingExactly(
equalTo(DB_NAME, DB),
equalTo(DB_USER, USER_DB),
equalTo(
DB_STATEMENT,
"select v1_0.id,v1_0.name from Value v1_0 where v1_0.id=$?"),
equalTo(DB_OPERATION, "SELECT"),
equalTo(DB_SQL_TABLE, "Value"),
equalTo(NET_PEER_NAME, "localhost"),
equalTo(NET_PEER_PORT, port)),
span ->
span.hasName("callback")
.hasKind(SpanKind.INTERNAL)
.hasParent(trace.getSpan(0))));
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table
public class Value {
private Long id;
private String name;
public Value() {}
public Value(String name) {
this.name = name;
}
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String title) {
name = title;
}
}

View File

@ -0,0 +1,20 @@
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="test-pu">
<provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>
<class>io.opentelemetry.javaagent.instrumentation.hibernate.reactive.v2_0.Value</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://localhost:${db.port}/tempdb"/>
<property name="jakarta.persistence.jdbc.user" value="SA"/>
<property name="jakarta.persistence.jdbc.password" value="password123"/>
<property name="hibernate.connection.pool_size" value="10"/>
<property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>

View File

@ -5,36 +5,53 @@
package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.vertx.core.Future;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.SqlConnectOptions;
import io.vertx.sqlclient.SqlConnection;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class PoolInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("io.vertx.sqlclient.Pool");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("io.vertx.sqlclient.Pool");
return implementsInterface(named("io.vertx.sqlclient.Pool"));
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("pool")
.and(isStatic())
.and(takesArguments(3))
.and(takesArgument(1, named("io.vertx.sqlclient.SqlConnectOptions")))
.and(returns(named("io.vertx.sqlclient.Pool"))),
PoolInstrumentation.class.getName() + "$PoolAdvice");
transformer.applyAdviceToMethod(
named("getConnection").and(takesNoArguments()).and(returns(named("io.vertx.core.Future"))),
PoolInstrumentation.class.getName() + "$GetConnectionAdvice");
}
@SuppressWarnings("unused")
@ -53,12 +70,33 @@ public class PoolInstrumentation implements TypeInstrumentation {
}
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) {
public static void onExit(
@Advice.Return Pool pool,
@Advice.Argument(1) SqlConnectOptions sqlConnectOptions,
@Advice.Local("otelCallDepth") CallDepth callDepth) {
if (callDepth.decrementAndGet() > 0) {
return;
}
VirtualField<Pool, SqlConnectOptions> virtualField =
VirtualField.find(Pool.class, SqlConnectOptions.class);
virtualField.set(pool, sqlConnectOptions);
setSqlConnectOptions(null);
}
}
@SuppressWarnings("unused")
public static class GetConnectionAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.This Pool pool, @Advice.Return(readOnly = false) Future<SqlConnection> future) {
// copy connect options stored on pool to new connection
VirtualField<Pool, SqlConnectOptions> virtualField =
VirtualField.find(Pool.class, SqlConnectOptions.class);
SqlConnectOptions sqlConnectOptions = virtualField.get(pool);
future = VertxSqlClientSingletons.attachConnectOptions(future, sqlConnectOptions);
}
}
}

View File

@ -42,6 +42,7 @@ public class SqlClientBaseInstrumentation implements TypeInstrumentation {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.This SqlClientBase<?> sqlClientBase) {
// copy connection options from ThreadLocal to VirtualField
// this virtual field is also set in VertxSqlClientSingletons.attachConnectOptions
VirtualField<SqlClientBase<?>, SqlConnectOptions> virtualField =
VirtualField.find(SqlClientBase.class, SqlConnectOptions.class);
virtualField.set(sqlClientBase, getSqlConnectOptions());

View File

@ -16,8 +16,12 @@ import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtr
import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.network.ServerAttributesExtractor;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
import io.vertx.core.Future;
import io.vertx.sqlclient.SqlConnectOptions;
import io.vertx.sqlclient.SqlConnection;
import io.vertx.sqlclient.impl.SqlClientBase;
import java.util.Map;
public final class VertxSqlClientSingletons {
@ -74,5 +78,20 @@ public final class VertxSqlClientSingletons {
return otelParentContext.makeCurrent();
}
// this virtual field is also used in SqlClientBase instrumentation
private static final VirtualField<SqlClientBase<?>, SqlConnectOptions> connectOptionsField =
VirtualField.find(SqlClientBase.class, SqlConnectOptions.class);
public static Future<SqlConnection> attachConnectOptions(
Future<SqlConnection> future, SqlConnectOptions connectOptions) {
return future.map(
sqlConnection -> {
if (sqlConnection instanceof SqlClientBase) {
connectOptionsField.set((SqlClientBase<?>) sqlConnection, connectOptions);
}
return sqlConnection;
});
}
private VertxSqlClientSingletons() {}
}