-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support including Module and Action in each JDBC session with Oracle …
…JDBC (#3183) * Investigation for setting connection client info * Use Connectable interface to pass connection client tracing info * Remove unneeded changes * Use connectable for Oracle Book repo * Documentation, comments and code cleanup. * Rename ConnectionClientTracingInfo to ConnectionTracingInfo * Changes per CR comments, still not there. * Change as suggested in PR comment * Properly clear connection client info * Introduce new OracleConnectionClientInfo annotation for connection client info tracing * Fixed javadoc * Resolve module/class name using InvocationContext.getTarget() * Introduce ConnectionCustomizer for more flexibility * More changes as suggested in pull request comments. * Add @experimental to ConnectionClientInfoDetails * Applied more suggestions from pull request comments. * Updated comments * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com> * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com> * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com> * Update src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com> * Renamed classes as suggested and also property to enable Oracle client info connection listener * Refactoring according to suggestions in PR comments. * Cleanup * Cache module name by the class name * Remove enabled attribute and create repeatable annotation * Fixes for Sonar * Update @SInCE attributes * Rename classes to shorter names * Rename connection client info annotations * Connection interceptor * Adjust code per Denis's suggestion for callbacks * Revert changes in ConnectableInterceptor * Revert new line * Reverted some changes and refactored code to resolve Sonar warning * Updated docs --------- Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com> Co-authored-by: Denis Stepanov <denis.s.stepanov@oracle.com>
- Loading branch information
1 parent
130c65c
commit 4a09882
Showing
15 changed files
with
547 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
...dbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoCondition.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
* 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.jdbc.oracle; | ||
|
||
import io.micronaut.context.BeanResolutionContext; | ||
import io.micronaut.context.Qualifier; | ||
import io.micronaut.context.condition.Condition; | ||
import io.micronaut.context.condition.ConditionContext; | ||
import io.micronaut.core.annotation.Internal; | ||
import io.micronaut.core.naming.Named; | ||
import io.micronaut.inject.BeanDefinition; | ||
|
||
/** | ||
* A condition that determines whether to customize Oracle client information based on configuration properties. | ||
* | ||
* This condition checks if the data source dialect is set to Oracle and if the 'customize-oracle-client-info' property is enabled. | ||
* | ||
* @author radovanradic | ||
* @since 4.11 | ||
*/ | ||
@Internal | ||
final class OracleClientInfoCondition implements Condition { | ||
|
||
static final String DATASOURCES = "datasources"; | ||
private static final Character DOT = '.'; | ||
private static final String DIALECT = "dialect"; | ||
private static final String ORACLE_CLIENT_INFO_ENABLED = "enable-oracle-client-info"; | ||
private static final String ORACLE_DIALECT = "ORACLE"; | ||
|
||
@Override | ||
public boolean matches(ConditionContext context) { | ||
BeanResolutionContext beanResolutionContext = context.getBeanResolutionContext(); | ||
String dataSourceName; | ||
if (beanResolutionContext == null) { | ||
return true; | ||
} else { | ||
Qualifier<?> currentQualifier = beanResolutionContext.getCurrentQualifier(); | ||
if (currentQualifier == null && context.getComponent() instanceof BeanDefinition<?> definition) { | ||
currentQualifier = definition.getDeclaredQualifier(); | ||
} | ||
if (currentQualifier instanceof Named named) { | ||
dataSourceName = named.getName(); | ||
} else { | ||
dataSourceName = "default"; | ||
} | ||
} | ||
|
||
String dialectProperty = DATASOURCES + DOT + dataSourceName + DOT + DIALECT; | ||
String dialect = context.getProperty(dialectProperty, String.class).orElse(null); | ||
if (!ORACLE_DIALECT.equalsIgnoreCase(dialect)) { | ||
return false; | ||
} | ||
|
||
String property = DATASOURCES + DOT + dataSourceName + DOT + ORACLE_CLIENT_INFO_ENABLED; | ||
return context.getProperty(property, Boolean.class, false); | ||
} | ||
} |
201 changes: 201 additions & 0 deletions
201
...n/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* | ||
* 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.jdbc.oracle; | ||
|
||
import io.micronaut.aop.MethodInvocationContext; | ||
import io.micronaut.context.annotation.Context; | ||
import io.micronaut.context.annotation.EachBean; | ||
import io.micronaut.context.annotation.Parameter; | ||
import io.micronaut.context.annotation.Requires; | ||
import io.micronaut.core.annotation.AnnotationMetadata; | ||
import io.micronaut.core.annotation.AnnotationValue; | ||
import io.micronaut.core.annotation.Internal; | ||
import io.micronaut.core.annotation.NonNull; | ||
import io.micronaut.core.annotation.Nullable; | ||
import io.micronaut.core.util.CollectionUtils; | ||
import io.micronaut.core.util.StringUtils; | ||
import io.micronaut.data.connection.ConnectionDefinition; | ||
import io.micronaut.data.connection.ConnectionStatus; | ||
import io.micronaut.data.connection.annotation.ClientInfo; | ||
import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource; | ||
import io.micronaut.data.connection.support.AbstractConnectionOperations; | ||
import io.micronaut.data.connection.support.ConnectionCustomizer; | ||
import io.micronaut.runtime.ApplicationConfiguration; | ||
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.Collections; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.function.Function; | ||
|
||
/** | ||
* A customizer for Oracle database connections that sets client information after opening and clears before closing. | ||
* | ||
* This customizer checks if the connection is an Oracle database connection and then sets the client information | ||
* (client ID, module, and action) after opening the connection. It also clears these properties before closing the connection. | ||
* | ||
* @author radovanradic | ||
* @since 4.11 | ||
*/ | ||
@EachBean(DataSource.class) | ||
@Requires(condition = OracleClientInfoCondition.class) | ||
@Context | ||
@Internal | ||
final class OracleClientInfoConnectionCustomizer implements ConnectionCustomizer<Connection> { | ||
|
||
private static final String NAME_MEMBER = "name"; | ||
private static final String VALUE_MEMBER = "value"; | ||
private static final String INTERCEPTED_SUFFIX = "$Intercepted"; | ||
|
||
/** | ||
* Constant for the Oracle connection client info client ID property name. | ||
*/ | ||
private static final String ORACLE_CLIENT_ID = "OCSID.CLIENTID"; | ||
/** | ||
* Constant for the Oracle connection client info module property name. | ||
*/ | ||
private static final String ORACLE_MODULE = "OCSID.MODULE"; | ||
/** | ||
* Constant for the Oracle connection client info action property name. | ||
*/ | ||
private static final String ORACLE_ACTION = "OCSID.ACTION"; | ||
/** | ||
* Constant for the Oracle connection database product name. | ||
*/ | ||
private static final String ORACLE_CONNECTION_DATABASE_PRODUCT_NAME = "Oracle"; | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(OracleClientInfoConnectionCustomizer.class); | ||
|
||
private static final Map<Class<?>, String> MODULE_CLASS_MAP = new ConcurrentHashMap<>(100); | ||
|
||
@Nullable | ||
private final String applicationName; | ||
|
||
OracleClientInfoConnectionCustomizer(@NonNull DataSource dataSource, | ||
@NonNull @Parameter AbstractConnectionOperations<Connection> connectionOperations, | ||
@Nullable ApplicationConfiguration applicationConfiguration) { | ||
this.applicationName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(null) : null; | ||
try { | ||
Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection(); | ||
if (isOracleConnection(connection)) { | ||
connectionOperations.addConnectionCustomizer(this); | ||
} | ||
} catch (SQLException e) { | ||
LOG.error("Failed to get connection for oracle connection listener", e); | ||
} | ||
} | ||
|
||
@Override | ||
public <R> Function<ConnectionStatus<Connection>, R> intercept(Function<ConnectionStatus<Connection>, R> operation) { | ||
return connectionStatus -> { | ||
ConnectionDefinition connectionDefinition = connectionStatus.getDefinition(); | ||
// Set client info for connection if Oracle before issue JDBC call | ||
Map<String, String> connectionClientInfo = getConnectionClientInfo(connectionDefinition); | ||
applyClientInfo(connectionStatus, connectionClientInfo); | ||
try { | ||
return operation.apply(connectionStatus); | ||
} finally { | ||
// Clear client info for connection if it was Oracle connection and client info was set previously | ||
clearClientInfo(connectionStatus, connectionClientInfo); | ||
} | ||
|
||
}; | ||
} | ||
|
||
private void applyClientInfo(@NonNull ConnectionStatus<Connection> connectionStatus, @NonNull Map<String, String> connectionClientInfo) { | ||
if (CollectionUtils.isNotEmpty(connectionClientInfo)) { | ||
Connection connection = connectionStatus.getConnection(); | ||
LOG.trace("Setting connection tracing info to the Oracle connection"); | ||
try { | ||
for (Map.Entry<String, String> additionalInfo : connectionClientInfo.entrySet()) { | ||
String name = additionalInfo.getKey(); | ||
String value = additionalInfo.getValue(); | ||
connection.setClientInfo(name, value); | ||
} | ||
} catch (SQLClientInfoException e) { | ||
LOG.debug("Failed to set connection tracing info", e); | ||
} | ||
} | ||
} | ||
|
||
private void clearClientInfo(@NonNull ConnectionStatus<Connection> connectionStatus, @NonNull Map<String, String> connectionClientInfo) { | ||
if (CollectionUtils.isNotEmpty(connectionClientInfo)) { | ||
try { | ||
Connection connection = connectionStatus.getConnection(); | ||
for (String key : connectionClientInfo.keySet()) { | ||
connection.setClientInfo(key, null); | ||
} | ||
} catch (SQLClientInfoException e) { | ||
LOG.debug("Failed to clear connection tracing info", e); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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(); | ||
return StringUtils.isNotEmpty(databaseProductName) && databaseProductName.equalsIgnoreCase(ORACLE_CONNECTION_DATABASE_PRODUCT_NAME); | ||
} catch (SQLException e) { | ||
LOG.debug("Failed to get database product name from the connection", e); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Gets connection client info from the {@link ClientInfo} annotation. | ||
* | ||
* @param connectionDefinition The connection definition info | ||
* @return The connection client info or null if not configured to be used | ||
*/ | ||
private @NonNull Map<String, String> getConnectionClientInfo(@NonNull ConnectionDefinition connectionDefinition) { | ||
AnnotationMetadata annotationMetadata = connectionDefinition.getAnnotationMetadata(); | ||
AnnotationValue<ClientInfo> annotation = annotationMetadata.getAnnotation(ClientInfo.class); | ||
List<AnnotationValue<ClientInfo.Attribute>> clientInfoValues = annotation != null ? annotation.getAnnotations(VALUE_MEMBER) : Collections.emptyList(); | ||
Map<String, String> clientInfoAttributes = new LinkedHashMap<>(clientInfoValues.size()); | ||
if (CollectionUtils.isNotEmpty(clientInfoValues)) { | ||
for (AnnotationValue<ClientInfo.Attribute> clientInfoValue : clientInfoValues) { | ||
String name = clientInfoValue.getRequiredValue(NAME_MEMBER, String.class); | ||
String value = clientInfoValue.getRequiredValue(VALUE_MEMBER, String.class); | ||
clientInfoAttributes.put(name, value); | ||
} | ||
} | ||
// Fallback defaults if not provided in the annotation | ||
if (StringUtils.isNotEmpty(applicationName)) { | ||
clientInfoAttributes.putIfAbsent(ORACLE_CLIENT_ID, applicationName); | ||
} | ||
if (annotationMetadata instanceof MethodInvocationContext<?, ?> methodInvocationContext) { | ||
clientInfoAttributes.putIfAbsent(ORACLE_MODULE, | ||
MODULE_CLASS_MAP.computeIfAbsent(methodInvocationContext.getTarget().getClass(), | ||
clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, "")) | ||
); | ||
clientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInvocationContext.getName()); | ||
} | ||
return clientInfoAttributes; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.