Skip to content

Commit

Permalink
Support including Module and Action in each JDBC session with Oracle …
Browse files Browse the repository at this point in the history
…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
3 people authored Dec 5, 2024
1 parent 130c65c commit 4a09882
Show file tree
Hide file tree
Showing 15 changed files with 547 additions and 36 deletions.
1 change: 1 addition & 0 deletions data-connection-jdbc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
api projects.micronautDataConnection
implementation mnSql.micronaut.jdbc
implementation mn.micronaut.aop
implementation mn.micronaut.runtime

testAnnotationProcessor mn.micronaut.inject.java

Expand Down
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);
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package io.micronaut.data.connection;


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

Expand All @@ -28,7 +30,7 @@
* @author Denis Stepanov
* @since 4.0.0
*/
public interface ConnectionDefinition {
public interface ConnectionDefinition extends AnnotationMetadataProvider {

/**
* Use the default propagation value.
Expand Down Expand Up @@ -117,6 +119,15 @@ enum Propagation {
@NonNull
ConnectionDefinition withName(String name);

/**
* Connection definition with new annotation metadata.
*
* @param annotationMetadata The new annotation metadata
* @return A new connection definition with specified annotation metadata
*/
@NonNull
ConnectionDefinition withAnnotationMetadata(AnnotationMetadata annotationMetadata);

/**
* Create a new {@link ConnectionDefinition} for the given behaviour.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micronaut.data.connection;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
Expand All @@ -25,10 +26,11 @@
/**
* 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 annotationMetadata The annotation metadata
* @author Denis Stepanov
* @since 4.0.0
*/
Expand All @@ -37,19 +39,25 @@ public record DefaultConnectionDefinition(
@Nullable String name,
Propagation propagationBehavior,
@Nullable Duration timeout,
Boolean readOnlyValue
Boolean readOnlyValue,
@NonNull AnnotationMetadata annotationMetadata
) implements ConnectionDefinition {

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

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

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

public DefaultConnectionDefinition(String name, Propagation propagationBehavior, Duration timeout,
Boolean readOnlyValue) {
this(name, propagationBehavior, timeout, readOnlyValue, AnnotationMetadata.EMPTY_METADATA);
}

@Override
Expand All @@ -76,13 +84,22 @@ public String getName() {

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

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

@Override
public ConnectionDefinition withAnnotationMetadata(AnnotationMetadata newAnnotationMetadata) {
return new DefaultConnectionDefinition(name, propagationBehavior, timeout, readOnlyValue, newAnnotationMetadata);
}

@Override
public @NonNull AnnotationMetadata getAnnotationMetadata() {
return annotationMetadata;
}
}

Loading

0 comments on commit 4a09882

Please sign in to comment.