diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 7ff7d2a70f22..3d274eeea72c 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -94,6 +94,7 @@ These are the supported libraries and frameworks: | [Logback](http://logback.qos.ch/) | 1.0+ | [opentelemetry-logback-appender-1.0](../instrumentation/logback/logback-appender-1.0/library),
[opentelemetry-logback-mdc-1.0](../instrumentation/logback/logback-mdc-1.0/library) | none | | [Micrometer](https://micrometer.io/) | 1.5+ | [opentelemetry-micrometer-1.5](../instrumentation/micrometer/micrometer-1.5/library) | none | | [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | [opentelemetry-mongo-3.1](../instrumentation/mongo/mongo-3.1/library) | [Database Client Spans] | +| [MyBatis](https://mybatis.org/mybatis-3/) | 3.2+ | N/A | none | | [Netty](https://github.com/netty/netty) | 3.8+ | [opentelemetry-netty-4.1](../instrumentation/netty/netty-4.1/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | | [OkHttp](https://github.com/square/okhttp/) | 2.2+ | [opentelemetry-okhttp-3.0](../instrumentation/okhttp/okhttp-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics] | | [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/) | 11.2+ | [opentelemetry-oracle-ucp-11.2](../instrumentation/oracle-ucp-11.2/library) | [Database Pool Metrics] | diff --git a/instrumentation/mybatis-3.2/javaagent/build.gradle.kts b/instrumentation/mybatis-3.2/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c7c8a098ea77 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.mybatis") + module.set("mybatis") + versions.set("[3.2.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("org.mybatis:mybatis:3.2.0") + + testImplementation("com.h2database:h2:1.4.191") +} + +tasks.withType().configureEach { + jvmArgs("-Dotel.instrumentation.mybatis.enabled=true") + + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java new file mode 100644 index 000000000000..741005717f60 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MapperMethodInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.mybatis.v3_2.MyBatisSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.ibatis.binding.MapperMethod.SqlCommand; + +public class MapperMethodInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.ibatis.binding.MapperMethod"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("execute"), MapperMethodInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + @SuppressWarnings("unused") + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void getMapperInfo( + @Advice.FieldValue("command") SqlCommand command, + @Advice.Local("otelRequest") ClassAndMethod request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (command == null) { + return; + } + request = SqlCommandUtil.getClassAndMethod(command); + if (request == null) { + return; + } + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + context = instrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") ClassAndMethod request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope != null) { + scope.close(); + instrumenter().end(context, request, null, throwable); + } + } + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java new file mode 100644 index 000000000000..55d4910ef232 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisInstrumentationModule.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class MyBatisInstrumentationModule extends InstrumentationModule { + + public MyBatisInstrumentationModule() { + super("mybatis", "mybatis-3.2"); + } + + @Override + public List typeInstrumentations() { + return asList(new MapperMethodInstrumentation(), new SqlCommandInstrumentation()); + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return false; + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java new file mode 100644 index 000000000000..b24eac8cce1e --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisSingletons.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; + +public final class MyBatisSingletons { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.mybatis-3.2"; + private static final Instrumenter INSTRUMENTER; + + static { + CodeAttributesGetter codeAttributesGetter = + ClassAndMethod.codeAttributesGetter(); + + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + CodeSpanNameExtractor.create(codeAttributesGetter)) + .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter)) + .buildInstrumenter(SpanKindExtractor.alwaysInternal()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private MyBatisSingletons() {} +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java new file mode 100644 index 000000000000..c137bd294082 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandInstrumentation.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.ibatis.binding.MapperMethod.SqlCommand; + +public class SqlCommandInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.ibatis.binding.MapperMethod$SqlCommand"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor().and(takesArgument(1, Class.class)).and(takesArgument(2, Method.class)), + SqlCommandInstrumentation.class.getName() + "$ConstructorAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This SqlCommand command, + @Advice.Argument(1) Class mapperInterface, + @Advice.Argument(2) Method method) { + SqlCommandUtil.setClassAndMethod(command, mapperInterface, method); + } + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java new file mode 100644 index 000000000000..f67692f47229 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/SqlCommandUtil.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import java.lang.reflect.Method; +import org.apache.ibatis.binding.MapperMethod.SqlCommand; + +public final class SqlCommandUtil { + private static final VirtualField field = + VirtualField.find(SqlCommand.class, ClassAndMethod.class); + + public static void setClassAndMethod(SqlCommand command, Class clazz, Method method) { + if (clazz == null || method == null || method.getName() == null) { + return; + } + field.set(command, ClassAndMethod.create(clazz, method.getName())); + } + + public static ClassAndMethod getClassAndMethod(SqlCommand command) { + return field.get(command); + } + + private SqlCommandUtil() {} +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java new file mode 100644 index 000000000000..42b81e0b1ee0 --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/MyBatisTest.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.SemanticAttributes; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.h2.jdbcx.JdbcDataSource; +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; + +class MyBatisTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static SqlSession sqlSession; + + @BeforeAll + static void setUp() { + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + Configuration configuration = new Configuration(); + configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), dataSource)); + configuration.addMapper(TestMapper.class); + sqlSession = new SqlSessionFactoryBuilder().build(configuration).openSession(); + } + + @AfterAll + static void cleanUp() { + if (sqlSession != null) { + sqlSession.close(); + } + } + + @Test + void testSelect() { + TestMapper testMapper = sqlSession.getMapper(TestMapper.class); + testMapper.select(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasKind(SpanKind.INTERNAL) + .hasName("TestMapper.select") + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.CODE_NAMESPACE, TestMapper.class.getName()), + equalTo(SemanticAttributes.CODE_FUNCTION, "select")))); + } +} diff --git a/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java new file mode 100644 index 000000000000..1d5e858a91ce --- /dev/null +++ b/instrumentation/mybatis-3.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/mybatis/v3_2/TestMapper.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.mybatis.v3_2; + +import org.apache.ibatis.annotations.Select; + +public interface TestMapper { + + @Select("SELECT 1") + int select(); +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ae4b4afe767a..785e057fe3d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -384,6 +384,7 @@ include(":instrumentation:mongo:mongo-3.7:javaagent") include(":instrumentation:mongo:mongo-4.0:javaagent") include(":instrumentation:mongo:mongo-async-3.3:javaagent") include(":instrumentation:mongo:mongo-common:testing") +include(":instrumentation:mybatis-3.2:javaagent") include(":instrumentation:netty:netty-3.8:javaagent") include(":instrumentation:netty:netty-4.0:javaagent") include(":instrumentation:netty:netty-4.1:javaagent")