diff --git a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationAutoConfiguration.java b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationAutoConfiguration.java index 1220acc..2547188 100644 --- a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationAutoConfiguration.java +++ b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,11 +126,13 @@ public static DataSourceObservationBeanPostProcessor dataSourceObservationBeanPo ObjectProvider parameterTransformer, ObjectProvider queryTransformer, ObjectProvider resultSetProxyLogicFactory, + ObjectProvider generatedKeysProxyLogicFactory, ObjectProvider dataSourceProxyConnectionIdManagerProvider, ObjectProvider proxyDataSourceBuilderCustomizers) { return new DataSourceObservationBeanPostProcessor(jdbcProperties, dataSourceNameResolvers, listeners, methodExecutionListeners, parameterTransformer, queryTransformer, resultSetProxyLogicFactory, - dataSourceProxyConnectionIdManagerProvider, proxyDataSourceBuilderCustomizers); + generatedKeysProxyLogicFactory, dataSourceProxyConnectionIdManagerProvider, + proxyDataSourceBuilderCustomizers); } @Configuration(proxyBeanMethods = false) diff --git a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessor.java b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessor.java index 5bbb107..40c7116 100644 --- a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessor.java +++ b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,8 @@ public class DataSourceObservationBeanPostProcessor implements BeanPostProcessor private final ObjectProvider resultSetProxyLogicFactoryProvider; + private final ObjectProvider generatedKeysProxyLogicFactoryProvider; + private final ObjectProvider dataSourceProxyConnectionIdManagerProviderProvider; private DataSourceProxyBuilderConfigurer dataSourceProxyBuilderConfigurer; @@ -64,6 +66,7 @@ public DataSourceObservationBeanPostProcessor(ObjectProvider jdb ObjectProvider parameterTransformerProvider, ObjectProvider queryTransformerProvider, ObjectProvider resultSetProxyLogicFactoryProvider, + ObjectProvider generatedKeysProxyLogicFactoryProvider, ObjectProvider dataSourceProxyConnectionIdManagerProviderProvider, ObjectProvider proxyDataSourceBuilderCustomizers) { this.jdbcPropertiesProvider = jdbcPropertiesProvider; @@ -73,6 +76,7 @@ public DataSourceObservationBeanPostProcessor(ObjectProvider jdb this.parameterTransformerProvider = parameterTransformerProvider; this.queryTransformerProvider = queryTransformerProvider; this.resultSetProxyLogicFactoryProvider = resultSetProxyLogicFactoryProvider; + this.generatedKeysProxyLogicFactoryProvider = generatedKeysProxyLogicFactoryProvider; this.dataSourceProxyConnectionIdManagerProviderProvider = dataSourceProxyConnectionIdManagerProviderProvider; this.proxyDataSourceBuilderCustomizers = proxyDataSourceBuilderCustomizers; } @@ -100,6 +104,7 @@ private DataSourceProxyBuilderConfigurer getConfigurer() { this.methodExecutionListenersProvider.orderedStream().toList(), this.parameterTransformerProvider.getIfAvailable(), this.queryTransformerProvider.getIfAvailable(), this.resultSetProxyLogicFactoryProvider.getIfAvailable(), + this.generatedKeysProxyLogicFactoryProvider.getIfAvailable(), this.dataSourceProxyConnectionIdManagerProviderProvider.getIfAvailable()); } return this.dataSourceProxyBuilderConfigurer; diff --git a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceProxyBuilderConfigurer.java b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceProxyBuilderConfigurer.java index 537415a..f48b3fd 100644 --- a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceProxyBuilderConfigurer.java +++ b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/DataSourceProxyBuilderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,12 +53,19 @@ public class DataSourceProxyBuilderConfigurer { private final List methodExecutionListeners; + @Nullable private final ParameterTransformer parameterTransformer; + @Nullable private final QueryTransformer queryTransformer; + @Nullable private final ResultSetProxyLogicFactory resultSetProxyLogicFactory; + @Nullable + private final ResultSetProxyLogicFactory generatedKeysProxyLogicFactory; + + @Nullable private final DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider; private final JdbcProperties jdbcProperties; @@ -67,6 +74,7 @@ public DataSourceProxyBuilderConfigurer(JdbcProperties jdbcProperties, List methodExecutionListeners, @Nullable ParameterTransformer parameterTransformer, @Nullable QueryTransformer queryTransformer, @Nullable ResultSetProxyLogicFactory resultSetProxyLogicFactory, + @Nullable ResultSetProxyLogicFactory generatedKeysProxyLogicFactory, @Nullable DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider) { this.jdbcProperties = jdbcProperties; this.listeners = listeners; @@ -74,6 +82,7 @@ public DataSourceProxyBuilderConfigurer(JdbcProperties jdbcProperties, List l.forEach(proxyDataSourceBuilder::listener)); ifAvailable(this.methodExecutionListeners, m -> m.forEach(proxyDataSourceBuilder::methodListener)); ifAvailable(this.parameterTransformer, proxyDataSourceBuilder::parameterTransformer); diff --git a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/JdbcProperties.java b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/JdbcProperties.java index 626de03..fea110a 100644 --- a/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/JdbcProperties.java +++ b/datasource-micrometer-spring-boot/src/main/java/net/ttddyy/observation/boot/autoconfigure/JdbcProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ public class JdbcProperties { /** * Which types of tracing we would like to include. */ - private Set includes = Set.of(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); + private Set includes = Set.of(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH, TraceType.KEYS); /** * List of DataSource bean names that will not be decorated. @@ -308,7 +308,12 @@ public enum TraceType { /** * Related to ResultSets. */ - FETCH(JdbcObservationDocumentation.RESULT_SET); + FETCH(JdbcObservationDocumentation.RESULT_SET), + + /** + * Related to generated keys. + */ + KEYS(JdbcObservationDocumentation.GENERATED_KEYS); final JdbcObservationDocumentation supportedDocumentation; diff --git a/datasource-micrometer-spring-boot/src/test/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessorTests.java b/datasource-micrometer-spring-boot/src/test/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessorTests.java index 7b81f74..b36b806 100644 --- a/datasource-micrometer-spring-boot/src/test/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessorTests.java +++ b/datasource-micrometer-spring-boot/src/test/java/net/ttddyy/observation/boot/autoconfigure/DataSourceObservationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,6 +60,8 @@ class DataSourceObservationBeanPostProcessorTests { private ObjectProvider resultSetProxyLogicFactoryProvider; + private ObjectProvider generatedKeysProxyLogicFactoryProvider; + private ObjectProvider dataSourceProxyConnectionIdManagerProviderProvider; private ObjectProvider proxyDataSourceBuilderCustomizers; @@ -76,14 +78,15 @@ void beforeEach() { this.parameterTransformerProvider = mock(ObjectProvider.class); this.queryTransformerProvider = mock(ObjectProvider.class); this.resultSetProxyLogicFactoryProvider = mock(ObjectProvider.class); + this.generatedKeysProxyLogicFactoryProvider = mock(ObjectProvider.class); this.dataSourceProxyConnectionIdManagerProviderProvider = mock(ObjectProvider.class); this.proxyDataSourceBuilderCustomizers = mock(ObjectProvider.class); this.processor = new DataSourceObservationBeanPostProcessor(this.jdbcPropertiesProvider, this.dataSourceNameResolverProvider, this.listenersProvider, this.methodExecutionListenersProvider, this.parameterTransformerProvider, this.queryTransformerProvider, - this.resultSetProxyLogicFactoryProvider, this.dataSourceProxyConnectionIdManagerProviderProvider, - this.proxyDataSourceBuilderCustomizers); + this.resultSetProxyLogicFactoryProvider, this.generatedKeysProxyLogicFactoryProvider, + this.dataSourceProxyConnectionIdManagerProviderProvider, this.proxyDataSourceBuilderCustomizers); } @Test diff --git a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ConnectionAttributesManager.java b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ConnectionAttributesManager.java index a528568..f4c5a9a 100644 --- a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ConnectionAttributesManager.java +++ b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ConnectionAttributesManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,9 +69,19 @@ class ConnectionAttributes { class ResultSetAttributesManager { - Map byResultSet = new ConcurrentHashMap<>(); + private final Map byResultSet = new ConcurrentHashMap<>(); - Map statements = new ConcurrentHashMap<>(); + private final Map statements = new ConcurrentHashMap<>(); + + private final Set generatedKeys = new HashSet<>(); + + boolean isGeneratedKeys(ResultSet resultSet) { + return this.generatedKeys.contains(resultSet); + } + + void addGeneratedKeys(ResultSet resultSet) { + this.generatedKeys.add(resultSet); + } ResultSetAttributes add(ResultSet resultSet, @Nullable Statement statement, ResultSetAttributes attributes) { this.byResultSet.put(resultSet, attributes); @@ -88,6 +98,7 @@ ResultSetAttributes getByResultSet(ResultSet resultSet) { @Nullable ResultSetAttributes removeByResultSet(ResultSet resultSet) { + this.generatedKeys.remove(resultSet); this.statements.remove(resultSet); return this.byResultSet.remove(resultSet); } @@ -102,13 +113,14 @@ Set removeByStatement(Statement statement) { iter.remove(); } } - + this.generatedKeys.removeAll(resultSets); return resultSets.stream().map(this.byResultSet::remove).filter(Objects::nonNull) .collect(Collectors.toSet()); } Set removeAll() { Set attributes = new HashSet<>(this.byResultSet.values()); + this.generatedKeys.clear(); this.byResultSet.clear(); this.statements.clear(); return attributes; diff --git a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/DataSourceObservationListener.java b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/DataSourceObservationListener.java index 28f4a5c..9865152 100644 --- a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/DataSourceObservationListener.java +++ b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/DataSourceObservationListener.java @@ -42,6 +42,7 @@ import net.ttddyy.dsproxy.listener.MethodExecutionContext; import net.ttddyy.dsproxy.listener.MethodExecutionListener; import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.proxy.ProxyJdbcObject; import net.ttddyy.observation.tracing.ConnectionAttributesManager.ConnectionAttributes; import net.ttddyy.observation.tracing.ConnectionAttributesManager.ResultSetAttributes; import net.ttddyy.observation.tracing.JdbcObservationDocumentation.JdbcEvents; @@ -55,6 +56,14 @@ public class DataSourceObservationListener implements QueryExecutionListener, Me private static final InternalLogger logger = InternalLoggerFactory.getInstance(DataSourceObservationListener.class); + private static final Set METHODS_TO_IGNORE = new HashSet<>(Arrays.asList( + // java.lang.Object methods + "toString", "equals", "hashCode", + // java.sql.Wrapper methods + "unwrap", "isWrapperFor", + // datasource-proxy methods + "getTarget", "getProxyConfig", "getDataSourceName")); + private final Supplier observationRegistrySupplier; private ConnectionAttributesManager connectionAttributesManager = new DefaultConnectionAttributesManager(); @@ -68,6 +77,9 @@ public class DataSourceObservationListener implements QueryExecutionListener, Me private ResultSetObservationConvention resultSetObservationConvention = new ResultSetObservationConvention() { }; + private GeneratedKeysObservationConvention generatedKeysObservationConvention = new GeneratedKeysObservationConvention() { + }; + private QueryParametersSpanTagProvider queryParametersSpanTagProvider = new DefaultQueryParametersSpanTagProvider(); /** @@ -193,6 +205,9 @@ else if ("executeBatch".equals(methodName)) { @Override public void beforeMethod(MethodExecutionContext executionContext) { String methodName = executionContext.getMethod().getName(); + if (METHODS_TO_IGNORE.contains(methodName)) { + return; + } Object target = executionContext.getTarget(); if (target instanceof DataSource && "getConnection".equals(methodName)) { handleGetConnectionBefore(executionContext); @@ -202,6 +217,9 @@ public void beforeMethod(MethodExecutionContext executionContext) { @Override public void afterMethod(MethodExecutionContext executionContext) { String methodName = executionContext.getMethod().getName(); + if (METHODS_TO_IGNORE.contains(methodName)) { + return; + } Object target = executionContext.getTarget(); if (target instanceof DataSource && "getConnection".equals(methodName)) { handleGetConnectionAfter(executionContext); @@ -221,6 +239,18 @@ else if (target instanceof Statement) { if ("close".equals(methodName)) { handleStatementClose(executionContext); } + else if ("getGeneratedKeys".equals(methodName) && executionContext.getResult() instanceof ResultSet) { + String connectionId = executionContext.getConnectionInfo().getConnectionId(); + ConnectionAttributes connectionAttributes = this.connectionAttributesManager.get(connectionId); + if (connectionAttributes != null) { + ResultSet resultSet = (ResultSet) executionContext.getResult(); + // result could be a proxy, unwrap it. + if (resultSet instanceof ProxyJdbcObject) { + resultSet = (ResultSet) ((ProxyJdbcObject) resultSet).getTarget(); + } + connectionAttributes.resultSetAttributesManager.addGeneratedKeys(resultSet); + } + } } else if (target instanceof ResultSet) { handleResultSet(executionContext); @@ -345,7 +375,8 @@ private void handleResultSet(MethodExecutionContext executionContext) { ResultSetAttributes resultSetAttributes = connectionAttributes.resultSetAttributesManager .getByResultSet(resultSet); if (resultSetAttributes == null) { - resultSetAttributes = createResultSetAttributesAndStartObservation(executionContext); + boolean isGeneratedKey = connectionAttributes.resultSetAttributesManager.isGeneratedKeys(resultSet); + resultSetAttributes = createResultSetAttributesAndStartObservation(executionContext, isGeneratedKey); Statement statement = null; try { @@ -361,7 +392,8 @@ private void handleResultSet(MethodExecutionContext executionContext) { connectionAttributes.resultSetAttributesManager.add(resultSet, statement, resultSetAttributes); } - ResultSetOperation operation = new ResultSetOperation(executionContext.getMethod(), executionContext.getResult()); + ResultSetOperation operation = new ResultSetOperation(executionContext.getMethod(), + executionContext.getResult()); resultSetAttributes.context.addOperation(operation); String methodName = executionContext.getMethod().getName(); @@ -377,12 +409,21 @@ else if ("next".equals(methodName)) { } } - private ResultSetAttributes createResultSetAttributesAndStartObservation(MethodExecutionContext executionContext) { + private ResultSetAttributes createResultSetAttributesAndStartObservation(MethodExecutionContext executionContext, + boolean isGeneratedKeys) { // new ResultSet observation ResultSetContext resultSetContext = new ResultSetContext(); populateFromConnectionAttributes(resultSetContext, executionContext.getConnectionInfo().getConnectionId()); - Observation observation = createAndStartObservation(JdbcObservationDocumentation.RESULT_SET, resultSetContext, - this.resultSetObservationConvention); + + Observation observation; + if (isGeneratedKeys) { + observation = createAndStartObservation(JdbcObservationDocumentation.GENERATED_KEYS, resultSetContext, + this.generatedKeysObservationConvention); + } + else { + observation = createAndStartObservation(JdbcObservationDocumentation.RESULT_SET, resultSetContext, + this.resultSetObservationConvention); + } if (logger.isDebugEnabled()) { logger.debug("Created a new result-set observation [" + observation + "]"); @@ -458,6 +499,11 @@ public void setResultSetObservationConvention(ResultSetObservationConvention res this.resultSetObservationConvention = resultSetObservationConvention; } + public void setGeneratedKeysObservationConvention( + GeneratedKeysObservationConvention generatedKeysObservationConvention) { + this.generatedKeysObservationConvention = generatedKeysObservationConvention; + } + public void setQueryParametersSpanTagProvider(QueryParametersSpanTagProvider queryParametersSpanTagProvider) { this.queryParametersSpanTagProvider = queryParametersSpanTagProvider; } diff --git a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/GeneratedKeysObservationConvention.java b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/GeneratedKeysObservationConvention.java new file mode 100644 index 0000000..5006d69 --- /dev/null +++ b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/GeneratedKeysObservationConvention.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.ttddyy.observation.tracing; + +import java.util.stream.Collectors; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.ObservationConvention; +import net.ttddyy.observation.tracing.JdbcObservationDocumentation.GeneratedKeysHighCardinalityKeyNames; + +/** + * A {@link ObservationConvention} for generated keys. + * + * @author Tadaya Tsuyukubo + * @since 1.1 + */ +public interface GeneratedKeysObservationConvention extends ResultSetObservationConvention { + + @Override + default KeyValues getHighCardinalityKeyValues(ResultSetContext context) { + String keys = context.getOperations().stream().filter(ResultSetOperation::isDataRetrievalOperation) + .map(ResultSetOperation::getResult).map(Object::toString).collect(Collectors.joining(",")); + return ResultSetObservationConvention.super.getHighCardinalityKeyValues(context) + .and(KeyValue.of(GeneratedKeysHighCardinalityKeyNames.KEYS.asString(), keys)); + } + +} diff --git a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/JdbcObservationDocumentation.java b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/JdbcObservationDocumentation.java index 5e11e7d..af5a7cb 100644 --- a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/JdbcObservationDocumentation.java +++ b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/JdbcObservationDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,6 +105,31 @@ public KeyName[] getHighCardinalityKeyNames() { return ResultSetHighCardinalityKeyNames.values(); } + @Override + public String getPrefix() { + return "jdbc"; + } + }, + + /** + * Span created when generated keys are returned. + */ + GENERATED_KEYS { + @Override + public String getName() { + return "jdbc.generated-keys"; + } + + @Override + public String getContextualName() { + return "generated-keys"; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return GeneratedKeysHighCardinalityKeyNames.values(); + } + @Override public String getPrefix() { return "jdbc"; @@ -161,6 +186,20 @@ public String asString() { } + public enum GeneratedKeysHighCardinalityKeyNames implements KeyName { + + /** + * Generated keys. + */ + KEYS { + @Override + public String asString() { + return "jdbc.generated-keys"; + } + } + + } + public enum ConnectionKeyNames implements KeyName { /** diff --git a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ResultSetOperation.java b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ResultSetOperation.java index 293ee59..fd9f866 100644 --- a/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ResultSetOperation.java +++ b/datasource-micrometer/src/main/java/net/ttddyy/observation/tracing/ResultSetOperation.java @@ -18,6 +18,9 @@ import java.lang.reflect.Method; import java.sql.ResultSet; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; /** * Represent an operation(method call) performed on the proxy {@link ResultSet}. @@ -36,6 +39,19 @@ public ResultSetOperation(Method method, Object result) { this.result = result; } + private static final Set NON_DATA_RETRIEVAL_METHODS = new HashSet<>(); + static { + // start with "get" but not data retrieval + NON_DATA_RETRIEVAL_METHODS + .addAll(Arrays.asList("getConcurrency", "getCursorName", "getMetaData", "getFetchDirection", + "getFetchSize", "getHoldability", "getRow", "getStatement", "getType", "getWarnings")); + } + + public static boolean isDataRetrievalOperation(ResultSetOperation op) { + String methodName = op.getMethod().getName(); + return methodName.startsWith("get") && !NON_DATA_RETRIEVAL_METHODS.contains(methodName); + } + public Method getMethod() { return this.method; } diff --git a/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/DataSourceListenerIncludeTypesIntegrationTests.java b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/DataSourceListenerIncludeTypesIntegrationTests.java index 8331907..d840a75 100644 --- a/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/DataSourceListenerIncludeTypesIntegrationTests.java +++ b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/DataSourceListenerIncludeTypesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; import java.util.HashSet; @@ -30,15 +31,20 @@ import javax.sql.DataSource; import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; import io.micrometer.tracing.test.simple.SimpleSpan; import io.micrometer.tracing.test.simple.SimpleTracer; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.h2.jdbcx.JdbcDataSource; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import static org.assertj.core.api.Assertions.assertThat; @@ -52,10 +58,7 @@ class DataSourceListenerIncludeTypesIntegrationTests { @BeforeAll static void setUpDataSource() throws Exception { dataSource = createDataSource(); - executeQuery(dataSource, "CREATE TABLE emp(id INT, name VARCHAR(20))"); - executeQuery(dataSource, "INSERT INTO emp VALUES (10, 'Foo')"); - executeQuery(dataSource, "INSERT INTO emp VALUES (20, 'Bar')"); - executeQuery(dataSource, "INSERT INTO emp VALUES (30, 'Baz')"); + executeQuery(dataSource, "CREATE TABLE emp(id IDENTITY NOT NULL PRIMARY KEY, name VARCHAR(20))"); } private static JdbcDataSource createDataSource() { @@ -79,62 +82,216 @@ static void shutdownDataBase() throws Exception { executeQuery(dataSource, "SHUTDOWN"); } - @ParameterizedTest - @MethodSource - void supportedTypes(List supportedTypeLists) throws Exception { - Set supportedTypes = new HashSet<>(supportedTypeLists); - Set expectedSpanNames = supportedTypes.stream().map(JdbcObservationDocumentation::getContextualName) - .collect(Collectors.toSet()); - - ObservationRegistry registry = ObservationRegistry.create(); - SimpleTracer tracer = new SimpleTracer(); - registry.observationConfig().observationHandler(new ConnectionTracingObservationHandler(tracer)); - registry.observationConfig().observationHandler(new QueryTracingObservationHandler(tracer)); - registry.observationConfig().observationHandler(new ResultSetTracingObservationHandler(tracer)); - DataSourceObservationListener listener = new DataSourceObservationListener(registry); - listener.setSupportedTypes(supportedTypes); - - ProxyDataSourceBuilder builder = ProxyDataSourceBuilder.create(dataSource).listener(listener).name("proxy-ds") - .methodListener(listener); - builder.proxyResultSet(); // enable ResultSet observation - builder.name("proxy"); // translates to the service name - DataSource proxyDataSource = builder.build(); - - String result = doLogic(proxyDataSource); - assertThat(result).isEqualTo("Bar"); - assertThat(tracer.getSpans()).extracting(SimpleSpan::getName) - .containsExactlyInAnyOrderElementsOf(expectedSpanNames); - - // make sure, observation scope is closed. - assertThat(registry.getCurrentObservation()).isNull(); + static abstract class Base { + + protected SimpleTracer tracer; + + protected ObservationRegistry registry; + + @BeforeEach + void clear() throws Exception { + executeQuery(dataSource, "DELETE FROM emp"); + executeQuery(dataSource, "ALTER TABLE emp ALTER COLUMN id RESTART WITH 10"); + + this.tracer = new SimpleTracer(); + this.registry = createObservationRegistry(this.tracer); + } + + protected ObservationRegistry createObservationRegistry(Tracer tracer) { + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new ConnectionTracingObservationHandler(tracer)); + registry.observationConfig().observationHandler(new QueryTracingObservationHandler(tracer)); + registry.observationConfig().observationHandler(new ResultSetTracingObservationHandler(tracer)); + return registry; + } + + protected DataSource createProxyDataSource(List supportedTypeLists, + ObservationRegistry registry) { + DataSourceObservationListener listener = new DataSourceObservationListener(registry); + listener.setSupportedTypes(new HashSet<>(supportedTypeLists)); + + ProxyDataSourceBuilder builder = ProxyDataSourceBuilder.create(dataSource).listener(listener) + .name("proxy-ds").methodListener(listener); + builder.proxyResultSet(); // enable ResultSet observation + builder.proxyGeneratedKeys(); // enable Generated Keys observation + builder.name("proxy"); // translates to the service name + return builder.build(); + } + + protected void verifySpanNames(List supportedTypeLists) { + Set expectedSpanNames = supportedTypeLists.stream() + .map(JdbcObservationDocumentation::getContextualName).collect(Collectors.toSet()); + assertThat(this.tracer.getSpans()).extracting(SimpleSpan::getName) + .containsExactlyInAnyOrderElementsOf(expectedSpanNames); + } + + protected int countTable() throws SQLException { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement statement = connection.prepareStatement("SELECT count(*) FROM emp")) { + try (ResultSet resultSet = statement.executeQuery()) { + resultSet.next(); + return resultSet.getInt(1); + } + } + } + } + } - private String doLogic(DataSource ds) throws Exception { - try (Connection conn = ds.getConnection()) { - try (PreparedStatement stmt = conn.prepareStatement("SELECT name FROM emp WHERE id = ?")) { - stmt.setInt(1, 20); - try (ResultSet rs = stmt.executeQuery()) { - rs.next(); - return rs.getString(1); + @Nested + class InsertWithGeneratedKeys extends Base { + + @ParameterizedTest + @ArgumentsSource(InsertWithGeneratedKeysDataProvider.class) + void supportedTypes(List supportedTypeLists, + List expectedTypes) throws Exception { + DataSource proxyDataSource = createProxyDataSource(supportedTypeLists, this.registry); + String result = doLogic(proxyDataSource); + assertThat(result).isEqualTo("10"); + verifySpanNames(expectedTypes); + // make sure, observation scope is closed. + assertThat(this.registry.getCurrentObservation()).isNull(); + } + + private String doLogic(DataSource ds) throws Exception { + try (Connection conn = ds.getConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO emp (name) VALUES (?)", + Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, "FOO"); + stmt.executeUpdate(); + try (ResultSet rs = stmt.getGeneratedKeys()) { + rs.next(); + return rs.getString(1); + } } } } + + } + + static class InsertWithGeneratedKeysDataProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + // @formatter:off + return Stream.of( + // input list, expected list + Arguments.of(Arrays.asList(JdbcObservationDocumentation.values()), + Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.QUERY, JdbcObservationDocumentation.GENERATED_KEYS)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION), Arrays.asList(JdbcObservationDocumentation.CONNECTION)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.QUERY), Arrays.asList(JdbcObservationDocumentation.QUERY)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.RESULT_SET), Arrays.asList()), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.GENERATED_KEYS), Arrays.asList(JdbcObservationDocumentation.GENERATED_KEYS)), + Arguments.of(Arrays.asList(), Arrays.asList()) + ); + // @formatter:on + } + } - static Stream supportedTypes() { - // @formatter:off - return Stream.of( - Arguments.of(Arrays.asList(JdbcObservationDocumentation.values())), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.QUERY)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.RESULT_SET)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.QUERY)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.QUERY, JdbcObservationDocumentation.RESULT_SET)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.RESULT_SET)), - Arguments.of(Arrays.asList(JdbcObservationDocumentation.QUERY, JdbcObservationDocumentation.RESULT_SET)), - Arguments.of(Arrays.asList()) - ); - // @formatter:on + @Nested + class Insert extends Base { + + @ParameterizedTest + @ArgumentsSource(InsertDataProvider.class) + void supportedTypes(List supportedTypeLists, + List expectedTypes) throws Exception { + DataSource proxyDataSource = createProxyDataSource(supportedTypeLists, this.registry); + doLogic(proxyDataSource); + + int count = countTable(); + assertThat(count).isEqualTo(1); + verifySpanNames(expectedTypes); + // make sure, observation scope is closed. + assertThat(this.registry.getCurrentObservation()).isNull(); + } + + private void doLogic(DataSource ds) throws Exception { + try (Connection conn = ds.getConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO emp (id, name) VALUES (?, ?)")) { + stmt.setInt(1, 99); + stmt.setString(2, "FOO"); + stmt.executeUpdate(); + } + } + } + + } + + static class InsertDataProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + // @formatter:off + return Stream.of( + // input list, expected list + Arguments.of(Arrays.asList(JdbcObservationDocumentation.values()), + Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.QUERY)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION), Arrays.asList(JdbcObservationDocumentation.CONNECTION)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.QUERY), Arrays.asList(JdbcObservationDocumentation.QUERY)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.RESULT_SET), Arrays.asList()), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.GENERATED_KEYS), Arrays.asList()), + Arguments.of(Arrays.asList(), Arrays.asList()) + ); + // @formatter:on + } + + } + + @Nested + class Select extends Base { + + @BeforeEach + void prepareData() throws Exception { + // Base class clears data + executeQuery(dataSource, "INSERT INTO emp (id, name) VALUES (20, 'Bar')"); + } + + @ParameterizedTest + @ArgumentsSource(SelectDataProvider.class) + void supportedTypes(List supportedTypeLists, + List expectedTypes) throws Exception { + DataSource proxyDataSource = createProxyDataSource(supportedTypeLists, this.registry); + String result = doLogic(proxyDataSource); + assertThat(result).isEqualTo("Bar"); + + verifySpanNames(expectedTypes); + // make sure, observation scope is closed. + assertThat(this.registry.getCurrentObservation()).isNull(); + } + + private String doLogic(DataSource ds) throws Exception { + try (Connection conn = ds.getConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT name FROM emp WHERE id = ?")) { + stmt.setInt(1, 20); + try (ResultSet rs = stmt.executeQuery()) { + rs.next(); + return rs.getString(1); + } + } + } + } + + } + + static class SelectDataProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + // @formatter:off + return Stream.of( + // input list, expected list + Arguments.of(Arrays.asList(JdbcObservationDocumentation.values()), + Arrays.asList(JdbcObservationDocumentation.CONNECTION, JdbcObservationDocumentation.QUERY, JdbcObservationDocumentation.RESULT_SET)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.CONNECTION), Arrays.asList(JdbcObservationDocumentation.CONNECTION)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.QUERY), Arrays.asList(JdbcObservationDocumentation.QUERY)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.RESULT_SET), Arrays.asList(JdbcObservationDocumentation.RESULT_SET)), + Arguments.of(Arrays.asList(JdbcObservationDocumentation.GENERATED_KEYS), Arrays.asList()), + Arguments.of(Arrays.asList(), Arrays.asList()) + ); + // @formatter:on + } + } } diff --git a/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/HikariJdbcObservationFilterTests.java b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/HikariJdbcObservationFilterTests.java index 7fb27cc..ebbf440 100644 --- a/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/HikariJdbcObservationFilterTests.java +++ b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/HikariJdbcObservationFilterTests.java @@ -38,6 +38,7 @@ class HikariJdbcObservationFilterTests { private static final String DRIVER = "my-driver"; + private static final String POOL = "my-pool"; @Test diff --git a/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/ResultSetAttributesManagerTests.java b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/ResultSetAttributesManagerTests.java new file mode 100644 index 0000000..16f4736 --- /dev/null +++ b/datasource-micrometer/src/test/java/net/ttddyy/observation/tracing/ResultSetAttributesManagerTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.ttddyy.observation.tracing; + +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Set; + +import net.ttddyy.observation.tracing.ConnectionAttributesManager.ResultSetAttributes; +import net.ttddyy.observation.tracing.ConnectionAttributesManager.ResultSetAttributesManager; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Tadaya Tsuyukubo + */ +class ResultSetAttributesManagerTests { + + ResultSetAttributesManager manager = new ResultSetAttributesManager(); + + @Test + void removeByResultSet() { + ResultSet resultSet1 = mock(ResultSet.class); + ResultSet resultSet2 = mock(ResultSet.class); + Statement statement = mock(Statement.class); + ResultSetAttributes attributes1 = new ResultSetAttributes(); + ResultSetAttributes attributes2 = new ResultSetAttributes(); + + this.manager.add(resultSet1, statement, attributes1); + assertThat(this.manager.getByResultSet(resultSet1)).isSameAs(attributes1); + assertThat(this.manager.getByResultSet(resultSet2)).isNull(); + + this.manager.add(resultSet2, statement, attributes2); + assertThat(this.manager.getByResultSet(resultSet1)).isSameAs(attributes1); + assertThat(this.manager.getByResultSet(resultSet2)).isSameAs(attributes2); + + assertThat(this.manager.removeByResultSet(resultSet1)).isSameAs(attributes1); + assertThat(this.manager.getByResultSet(resultSet1)).isNull(); + assertThat(this.manager.getByResultSet(resultSet2)).isSameAs(attributes2); + } + + @Test + void removeByStatement() { + ResultSet resultSet1 = mock(ResultSet.class); + ResultSet resultSet2 = mock(ResultSet.class); + Statement statement = mock(Statement.class); + ResultSetAttributes attributes1 = new ResultSetAttributes(); + ResultSetAttributes attributes2 = new ResultSetAttributes(); + + this.manager.add(resultSet1, statement, attributes1); + this.manager.add(resultSet2, statement, attributes2); + assertThat(this.manager.getByResultSet(resultSet1)).isSameAs(attributes1); + assertThat(this.manager.getByResultSet(resultSet2)).isSameAs(attributes2); + + Set removed = this.manager.removeByStatement(statement); + assertThat(removed).containsExactlyInAnyOrder(attributes1, attributes2); + + assertThat(this.manager.getByResultSet(resultSet1)).isNull(); + assertThat(this.manager.getByResultSet(resultSet2)).isNull(); + } + + @Test + void isGeneratedKeys() { + ResultSet generatedKeys = mock(ResultSet.class); + ResultSet resultSet = mock(ResultSet.class); + + this.manager.addGeneratedKeys(generatedKeys); + assertThat(this.manager.isGeneratedKeys(generatedKeys)).isTrue(); + assertThat(this.manager.isGeneratedKeys(resultSet)).isFalse(); + + this.manager.removeAll(); + assertThat(this.manager.isGeneratedKeys(generatedKeys)).isFalse(); + } + + @Test + void generatedKeysRemoval() { + ResultSet generatedKeys = mock(ResultSet.class); + Statement statement = mock(Statement.class); + ResultSetAttributes attributes = new ResultSetAttributes(); + + this.manager.addGeneratedKeys(generatedKeys); + this.manager.add(generatedKeys, statement, attributes); + assertThat(this.manager.isGeneratedKeys(generatedKeys)).isTrue(); + + this.manager.removeByResultSet(generatedKeys); + assertThat(this.manager.isGeneratedKeys(generatedKeys)).isFalse(); + } + +} diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/src/main/asciidoc/getting-started.adoc index b7e4eb7..56188b7 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/src/main/asciidoc/getting-started.adoc @@ -132,7 +132,7 @@ The auto-configuration class automatically sets up the observation on your `Data [[getting-started-migration-from-spring-cloud-sleuth]] == Migration from Spring Cloud Sleuth -Datasource Micrometer deliberatively provides similar property names to ease the migration from {spring-cloud-sleuth}[Spring Cloud Sleuth]. +Datasource Micrometer deliberately provides similar property names to ease the migration from {spring-cloud-sleuth}[Spring Cloud Sleuth]. Most of the JDBC related properties from `spring.sleuth.jdbc` and `spring.sleuth.jdbc.datasource-proxy` map to the `jdbc` and `jdbc.datasource-proxy` properties. Please reference the list of application properties in https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/appendix.html#common-application-properties[Spring Cloud Sleuth] and xref:appendix.adoc#appendix-common-application-properties[Datasource Micrometer]. diff --git a/docs/src/main/asciidoc/howto.adoc b/docs/src/main/asciidoc/howto.adoc index 660a2e0..a0d7417 100644 --- a/docs/src/main/asciidoc/howto.adoc +++ b/docs/src/main/asciidoc/howto.adoc @@ -36,6 +36,8 @@ There are 3 tracing observation handlers that react to the observations from `Da * `QueryTracingObservationHandler` * `ResultSetTracingObservationHandler` +NOTE: generated-keys are also handled by `ResultSetTracingObservationHandler`. + [source,java,indent=0] ---- ObservationRegistry registry = ... @@ -59,6 +61,19 @@ builder.proxyResultSet(); // enable ResultSet proxy creation DataSource instrumented = builder.build(); ---- +[[how-to-instrument-generated-keys]] +=== How to Instrument Generated Keys + +By default, {datasource-proxy}[datasource-proxy] does not create a proxy for the generated-keys. +You need to explicitly enable the generated-keys proxy creation. + +[source,java,indent=0] +---- +ProxyDataSourceBuilder builder = ProxyDataSourceBuilder.create(dataSource).listener(listener).methodListener(listener); +builder.proxyGeneratedKeys(); // enable Generated-Keys proxy creation +DataSource instrumented = builder.build(); +---- + [[how-to-include-bind-parameter-values]] === How to Include Bind Parameter Values @@ -84,7 +99,8 @@ Set the `jdbc.datasource-proxy.enabled` property to `false`. === How to Choose What To Observe Specify `jdbc.includes` property. -By default, the property is set to include(observe) all(`CONNECTION`, `QUERY`, `FETCH`) types. +By default, the property is set to include(observe) all(`CONNECTION`, `QUERY`, +`KEYS`, `FETCH`) types. [[how-to-include-bind-parameter-values-in-boot]] === How to Include the Bind Parameter Values in Spans diff --git a/docs/src/main/asciidoc/using.adoc b/docs/src/main/asciidoc/using.adoc index 6ad087d..62e6321 100644 --- a/docs/src/main/asciidoc/using.adoc +++ b/docs/src/main/asciidoc/using.adoc @@ -8,10 +8,11 @@ This section goes into more detail about how you should use {project-full-name}. [[using-types-of-observation]] == Types of Observations -The {project-full-name} creates Connection, Query, and ResultSet observations. +The {project-full-name} creates Connection, Query, Generated Keys(from `v1.1`), and ResultSet observations. The Connection observation represents the database connection operations. It is the base observation, as any database access requires a connection. The Query observation provides query execution details, such as execution time, SQL query, bind parameters, etc. +The Generated Keys observation records generated keys when auto-generated keys feature is used for the insert statements. The ResultSet observation shows how the operations fetched the data from the query result, including the number of retrieved rows. To configure these observations, see <>. +