Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support including Module and Action in each JDBC session with Oracle JDBC #3183

Open
wants to merge 46 commits into
base: 4.10.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b45b532
Investigation for setting connection client info
radovanradic Oct 15, 2024
e0b6f4a
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Oct 16, 2024
19e58f0
Use Connectable interface to pass connection client tracing info
radovanradic Oct 16, 2024
b8ed459
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Oct 16, 2024
92902bd
Remove unneeded changes
radovanradic Oct 16, 2024
afb396d
Use connectable for Oracle Book repo
radovanradic Oct 16, 2024
a481771
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Oct 16, 2024
27bdc07
Documentation, comments and code cleanup.
radovanradic Oct 17, 2024
8bb94e8
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Oct 17, 2024
7874bdf
Rename ConnectionClientTracingInfo to ConnectionTracingInfo
radovanradic Oct 17, 2024
41fd257
Changes per CR comments, still not there.
radovanradic Oct 21, 2024
8e70480
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Oct 21, 2024
71e66de
Change as suggested in PR comment
radovanradic Oct 21, 2024
7a16299
Properly clear connection client info
radovanradic Oct 21, 2024
2785d8d
Introduce new OracleConnectionClientInfo annotation for connection cl…
radovanradic Oct 21, 2024
304d577
Fixed javadoc
radovanradic Oct 21, 2024
7e90fa5
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Nov 1, 2024
b3b0468
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Nov 15, 2024
b8060d3
Resolve module/class name using InvocationContext.getTarget()
radovanradic Nov 15, 2024
3320b38
Introduce ConnectionCustomizer for more flexibility
radovanradic Nov 15, 2024
d84dad1
More changes as suggested in pull request comments.
radovanradic Nov 17, 2024
1bb490a
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Nov 17, 2024
4e98412
Add @Experimental to ConnectionClientInfoDetails
radovanradic Nov 17, 2024
91b3cd6
Applied more suggestions from pull request comments.
radovanradic Nov 18, 2024
fdff622
Updated comments
radovanradic Nov 18, 2024
d56a78b
Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
radovanradic Nov 18, 2024
c25d742
Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
radovanradic Nov 18, 2024
f6b7f80
Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
radovanradic Nov 18, 2024
1f9e9e9
Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc
radovanradic Nov 18, 2024
c68b657
Renamed classes as suggested and also property to enable Oracle clien…
radovanradic Nov 18, 2024
06901f3
Refactoring according to suggestions in PR comments.
radovanradic Nov 19, 2024
5adc61e
Cleanup
radovanradic Nov 19, 2024
f66881f
Cache module name by the class name
radovanradic Nov 19, 2024
5c97b15
Remove enabled attribute and create repeatable annotation
radovanradic Nov 19, 2024
da4e8a3
Fixes for Sonar
radovanradic Nov 19, 2024
913d9a8
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Nov 20, 2024
886b103
Update @since attributes
radovanradic Nov 20, 2024
60458ad
Rename classes to shorter names
radovanradic Nov 23, 2024
e0099b8
Rename connection client info annotations
radovanradic Nov 25, 2024
e5e0c1b
Connection interceptor
dstepanov Nov 29, 2024
7566f76
Adjust code per Denis's suggestion for callbacks
radovanradic Nov 29, 2024
a5d01a8
Merge branch '4.10.x' into jdbc-clientinfo
radovanradic Nov 29, 2024
9130104
Revert changes in ConnectableInterceptor
radovanradic Nov 29, 2024
8528515
Revert new line
radovanradic Nov 29, 2024
c325e1b
Reverted some changes and refactored code to resolve Sonar warning
radovanradic Nov 29, 2024
dcfbcfa
Updated docs
radovanradic Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@

import io.micronaut.context.annotation.EachBean;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.connection.exceptions.ConnectionException;
import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
import io.micronaut.data.connection.jdbc.exceptions.CannotGetJdbcConnectionException;
import io.micronaut.data.connection.ConnectionDefinition;
import io.micronaut.data.connection.ConnectionStatus;
import io.micronaut.data.connection.ConnectionSynchronization;
import io.micronaut.data.connection.support.AbstractConnectionOperations;
import io.micronaut.data.connection.support.ConnectionTracingInfo;
import io.micronaut.data.connection.support.JdbcConnectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -44,6 +47,10 @@
@EachBean(DataSource.class)
public final class DefaultDataSourceConnectionOperations extends AbstractConnectionOperations<Connection> {

private static final String ORACLE_TRACE_CLIENTID = "OCSID.CLIENTID";
private static final String ORACLE_TRACE_MODULE = "OCSID.MODULE";
private static final String ORACLE_TRACE_ACTION = "OCSID.ACTION";

private static final Logger LOG = LoggerFactory.getLogger(DefaultDataSourceConnectionOperations.class);
private final DataSource dataSource;

Expand All @@ -62,7 +69,8 @@ protected Connection openConnection(ConnectionDefinition definition) {

@Override
protected void setupConnection(ConnectionStatus<Connection> connectionStatus) {
connectionStatus.getDefinition().isReadOnly().ifPresent(readOnly -> {
ConnectionDefinition connectionDefinition = connectionStatus.getDefinition();
connectionDefinition.isReadOnly().ifPresent(readOnly -> {
List<Runnable> onCompleteCallbacks = new ArrayList<>(1);
JdbcConnectionUtils.applyReadOnly(LOG, connectionStatus.getConnection(), readOnly, onCompleteCallbacks);
if (!onCompleteCallbacks.isEmpty()) {
Expand All @@ -76,6 +84,26 @@ public void executionComplete() {
});
}
});
ConnectionTracingInfo connectionTracingInfo = connectionDefinition.connectionTracingInfo();
if (connectionTracingInfo == null) {
return;
}
Connection conn = connectionStatus.getConnection();
if (!isOracleConnection(conn)) {
LOG.debug("Connection tracing info is supported only for Oracle database connections.");
return;
}

LOG.trace("Setting connection tracing info to the Oracle connection");
try {
if (connectionTracingInfo.appName() != null) {
conn.setClientInfo(ORACLE_TRACE_CLIENTID, connectionTracingInfo.appName());
}
conn.setClientInfo(ORACLE_TRACE_MODULE, connectionTracingInfo.module());
conn.setClientInfo(ORACLE_TRACE_ACTION, connectionTracingInfo.action());
} catch (SQLClientInfoException e) {
LOG.debug("Failed to set connection tracing info", e);
}
}

@Override
Expand All @@ -87,4 +115,19 @@ protected void closeConnection(ConnectionStatus<Connection> connectionStatus) {
}
}

/**
* Checks whether current connection is Oracle database connection.
*
* @param connection The connection
* @return true if current connection is Oracle database connection
*/
private boolean isOracleConnection(Connection connection) {
try {
String databaseProductName = connection.getMetaData().getDatabaseProductName();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be evaluated once in a condition on startup somewhere in the ConnectionCusotmizer mentioned above

return StringUtils.isNotEmpty(databaseProductName) && databaseProductName.toUpperCase().contains("ORACLE");
radovanradic marked this conversation as resolved.
Show resolved Hide resolved
} catch (SQLException e) {
LOG.debug("Failed to get database product name from the connection", e);
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.connection.support.ConnectionTracingInfo;

import java.time.Duration;
import java.util.Optional;
Expand Down Expand Up @@ -101,6 +102,14 @@ enum Propagation {
@Nullable
String getName();

/**
* Returns the connection tracing information associated with this connection definition.
* If no connection tracing information has been set, this method will return null.
*
* @return An instance of {@link ConnectionTracingInfo} representing the connection tracing information, or null if not set.
*/
@Nullable ConnectionTracingInfo connectionTracingInfo();

/**
* Connection definition with specific propagation.
* @param propagation The new propagation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.connection.support.ConnectionTracingInfo;

import java.time.Duration;
import java.util.Optional;

/**
* Default implementation of the {@link ConnectionDefinition} interface.
*
* @param name The connection name
* @param propagationBehavior The propagation behaviour
* @param timeout The timeout
* @param readOnlyValue The read only
* @param name The connection name
* @param propagationBehavior The propagation behaviour
* @param timeout The timeout
* @param readOnlyValue The read only
* @param connectionTracingInfo The connection client tracing info, can be null
* @author Denis Stepanov
* @since 4.0.0
*/
Expand All @@ -37,19 +39,21 @@ public record DefaultConnectionDefinition(
@Nullable String name,
Propagation propagationBehavior,
@Nullable Duration timeout,
Boolean readOnlyValue
Boolean readOnlyValue,

@Nullable ConnectionTracingInfo connectionTracingInfo
) implements ConnectionDefinition {

DefaultConnectionDefinition(String name) {
this(name, PROPAGATION_DEFAULT, null, null);
this(name, PROPAGATION_DEFAULT, null, null, null);
}

public DefaultConnectionDefinition(Propagation propagationBehaviour) {
this(null, propagationBehaviour, null, null);
this(null, propagationBehaviour, null, null, null);
}

public DefaultConnectionDefinition(String name, boolean readOnly) {
this(name, PROPAGATION_DEFAULT, null, readOnly);
this(name, PROPAGATION_DEFAULT, null, readOnly, null);
}

@Override
Expand All @@ -76,12 +80,12 @@ public String getName() {

@Override
public ConnectionDefinition withPropagation(Propagation propagation) {
return new DefaultConnectionDefinition(name, propagation, timeout, readOnlyValue);
return new DefaultConnectionDefinition(name, propagation, timeout, readOnlyValue, connectionTracingInfo);
}

@Override
public ConnectionDefinition withName(String name) {
return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue);
return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue, connectionTracingInfo);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,29 @@
*/
boolean readOnly() default false;

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is ConnectableInterceptor that is creating custom ConnectionDefinition I thought we could use this annotation for this purpose so users can customize module and action that will be traced. Don't see other way than use this annotation for the repository that needs to be traced. Without it, we cannot obtain details about class/method being executed when connection is established.

* If true, then when connection is established {@link java.sql.Connection#setClientInfo(String, String)} will be called
* if it is connected to the Oracle database. It will issue calls to set MODULE, ACTION and CLIENT_IDENTIFIER.
*
* @return whether connection should trace/set client info
*/
boolean traceClientInfo() default false;

/**
* The module name for tracing if {@link #traceClientInfo()} is true.
* If not provided, then it will fall back to the name of the class currently being intercepted in {@link io.micronaut.data.connection.interceptor.ConnectableInterceptor}.
* Currently supported only for Oracle database connections.
*
* @return the custom module name for tracing
*/
String tracingModule() default "";

/**
* The action name for tracing if {@link #traceClientInfo()} is true.
* If not provided, then it will fall back to the name of the method currently being intercepted in {@link io.micronaut.data.connection.interceptor.ConnectableInterceptor}.
* Currently supported only for Oracle database connections.
*
* @return the custom module name for tracing
*/
String tracingAction() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.aop.InterceptorBean;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
Expand All @@ -33,6 +34,7 @@
import io.micronaut.data.connection.async.AsyncConnectionOperations;
import io.micronaut.data.connection.reactive.ReactiveStreamsConnectionOperations;
import io.micronaut.data.connection.reactive.ReactorConnectionOperations;
import io.micronaut.data.connection.support.ConnectionTracingInfo;
import io.micronaut.inject.ExecutableMethod;
import jakarta.inject.Singleton;
import reactor.core.publisher.Flux;
Expand All @@ -54,6 +56,10 @@
@InterceptorBean(Connectable.class)
public final class ConnectableInterceptor implements MethodInterceptor<Object, Object> {

private static final String TRACE_CLIENT_INFO_MEMBER = "traceClientInfo";
private static final String TRACING_MODULE_MEMBER = "tracingModule";
private static final String TRACING_ACTION_MEMBER = "tracingAction";

private final Map<TenantExecutableMethod, ConnectionInvocation> connectionInvocationMap = new ConcurrentHashMap<>(30);

@NonNull
Expand All @@ -63,6 +69,9 @@ public final class ConnectableInterceptor implements MethodInterceptor<Object, O

private final ConversionService conversionService;

@Nullable
private final String appName;

/**
* Default constructor.
*
Expand All @@ -72,9 +81,11 @@ public final class ConnectableInterceptor implements MethodInterceptor<Object, O
*/
ConnectableInterceptor(@NonNull ConnectionOperationsRegistry connectionOperationsRegistry,
@Nullable ConnectionDataSourceTenantResolver tenantResolver,
@Nullable @Value("${micronaut.application.name}") String appName,
radovanradic marked this conversation as resolved.
Show resolved Hide resolved
ConversionService conversionService) {
this.connectionOperationsRegistry = connectionOperationsRegistry;
this.tenantResolver = tenantResolver;
this.appName = appName;
this.conversionService = conversionService;
}

Expand All @@ -97,7 +108,7 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
final ConnectionInvocation connectionInvocation = connectionInvocationMap
.computeIfAbsent(new TenantExecutableMethod(tenantDataSourceName, executableMethod), ignore -> {
final String dataSource = tenantDataSourceName == null ? executableMethod.stringValue(Connectable.class).orElse(null) : tenantDataSourceName;
final ConnectionDefinition connectionDefinition = getConnectionDefinition(executableMethod);
final ConnectionDefinition connectionDefinition = getConnectionDefinition(executableMethod, appName);

switch (interceptedMethod.resultType()) {
case PUBLISHER -> {
Expand Down Expand Up @@ -150,20 +161,48 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
}

@NonNull
public static ConnectionDefinition getConnectionDefinition(ExecutableMethod<Object, Object> executableMethod) {
public static ConnectionDefinition getConnectionDefinition(ExecutableMethod<Object, Object> executableMethod, String appName) {
AnnotationValue<Connectable> annotation = executableMethod.getAnnotation(Connectable.class);
if (annotation == null) {
throw new IllegalStateException("No declared @Connectable annotation present");
}

ConnectionTracingInfo connectionTracingInfo = getConnectionClientTracingInfo(annotation, executableMethod, appName);
return new DefaultConnectionDefinition(
executableMethod.getDeclaringType().getSimpleName() + "." + executableMethod.getMethodName(),
annotation.enumValue("propagation", ConnectionDefinition.Propagation.class).orElse(ConnectionDefinition.PROPAGATION_DEFAULT),
annotation.longValue("timeout").stream().mapToObj(Duration::ofSeconds).findFirst().orElse(null),
annotation.booleanValue("readOnly").orElse(null)
annotation.booleanValue("readOnly").orElse(null),
connectionTracingInfo
);
}

/**
* Gets connection tracing info from the {@link Connectable} annotation.
*
* @param annotation The {@link Connectable} annotation value
* @param executableMethod The method being executed
* @param appName The micronaut application name, null if not set
* @return The connection tracing info or null if not configured to be used
*/
private static ConnectionTracingInfo getConnectionClientTracingInfo(AnnotationValue<Connectable> annotation,
ExecutableMethod<Object, Object> executableMethod,
String appName) {
boolean traceClientInfo = annotation.booleanValue(TRACE_CLIENT_INFO_MEMBER).orElse(false);
if (!traceClientInfo) {
return null;
}
String module = annotation.stringValue(TRACING_MODULE_MEMBER).orElse(null);
String action = annotation.stringValue(TRACING_ACTION_MEMBER).orElse(null);
if (module == null) {
module = executableMethod.getDeclaringType().getName();
radovanradic marked this conversation as resolved.
Show resolved Hide resolved
}
if (action == null) {
action = executableMethod.getMethodName();
}
return new ConnectionTracingInfo(appName, module, action);
}

/**
* Cached invocation associating a method with a definition a connection manager.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2024 original 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 io.micronaut.data.connection.support;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;

/**
* The connection tracing information that can be used to set to {@link java.sql.Connection#setClientInfo(String, String)}.
* Currently used only for Oracle database connections.
*
* @param appName The app name corresponding to the micronaut.application.name config value and can be null
* @param module The module
* @param action The action
*/
public record ConnectionTracingInfo(@Nullable String appName, @NonNull String module, @NonNull String action) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add @Experimental for now

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package io.micronaut.data.jdbc.oraclexe;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.annotation.Expandable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.TypeDef;
import io.micronaut.data.annotation.sql.Procedure;
import io.micronaut.data.connection.annotation.Connectable;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.query.builder.sql.Dialect;
Expand All @@ -31,6 +33,7 @@
import java.util.List;

@JdbcRepository(dialect = Dialect.ORACLE)
@Connectable(traceClientInfo = true, tracingModule = "BOOKS")
public abstract class OracleXEBookRepository extends BookRepository {
public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) {
super(authorRepository);
Expand All @@ -45,6 +48,7 @@ public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) {

@Override
@Query(value = "select * from book b where b.title = ANY (:arg0)", nativeQuery = true)
@Connectable(traceClientInfo = false)
public abstract List<Book> listNativeBooksWithTitleAnyArray(@Expandable @TypeDef(type = DataType.STRING) @Nullable String[] arg0);

@Procedure
Expand All @@ -53,7 +57,11 @@ public OracleXEBookRepository(OracleXEAuthorRepository authorRepository) {
@Procedure("add1")
public abstract int add1Aliased(int input);

// public abstract Book updateReturning(Book book);
@Override
@Connectable(traceClientInfo = true, tracingAction = "INSERT")
public abstract @NonNull Book save(@NonNull Book book);

// public abstract Book updateReturning(Book book);
//
// public abstract String updateReturningTitle(Book book);
//
Expand Down
Loading
Loading