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 39 commits into
base: 4.10.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 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
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
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,195 @@
/*
* 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.StringUtils;
import io.micronaut.data.connection.ConnectionDefinition;
import io.micronaut.data.connection.ConnectionStatus;
import io.micronaut.data.connection.annotation.ConnClientInfoAttr;
import io.micronaut.data.connection.annotation.ConnClientInfo;
import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource;
import io.micronaut.data.connection.support.AbstractConnectionOperations;
import io.micronaut.data.connection.support.ConnectionListener;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@EachBean(DataSource.class) was not creating this bean until added @Context. Not sure if this can cause some issues, I would prefer if it could work without @Context but not sure what is missing.

@Internal
final class OracleClientInfoConnectionListener implements ConnectionListener<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(OracleClientInfoConnectionListener.class);

private static final Map<Class<?>, String> MODULE_CLASS_MAP = new ConcurrentHashMap<>(100);

@Nullable
private final String applicationName;

OracleClientInfoConnectionListener(@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.addConnectionListener(this);
}
} catch (SQLException e) {
LOG.error("Failed to get connection for oracle connection listener", e);
}
}

@Override
public void afterOpen(@NonNull ConnectionStatus<Connection> connectionStatus) {
ConnectionDefinition connectionDefinition = connectionStatus.getDefinition();
// Set client info for connection if Oracle connection after connection is opened
Map<String, String> connectionClientInfo = getConnectionClientInfo(connectionDefinition);
if (connectionClientInfo != null && !connectionClientInfo.isEmpty()) {
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);
}
}
}

@Override
public void beforeClose(@NonNull ConnectionStatus<Connection> connectionStatus) {
// Clear client info for connection if it was Oracle connection and client info was set previously
ConnectionDefinition connectionDefinition = connectionStatus.getDefinition();
Map<String, String> connectionClientInfo = getConnectionClientInfo(connectionDefinition);
if (connectionClientInfo != null && !connectionClientInfo.isEmpty()) {
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);
}
}
}

@Override
public String getName() {
return "Oracle Connection Client Info Customizer";
}

/**
* 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 ConnClientInfoAttr} annotation.
*
* @param connectionDefinition The connection definition
* @return The connection client info or null if not configured to be used
*/
private @Nullable Map<String, String> getConnectionClientInfo(@NonNull ConnectionDefinition connectionDefinition) {
AnnotationMetadata annotationMetadata = connectionDefinition.getAnnotationMetadata();
AnnotationValue<ConnClientInfo> annotation = annotationMetadata.getAnnotation(ConnClientInfo.class);
if (annotation == null) {
return null;
}
List<AnnotationValue<ConnClientInfoAttr>> clientInfoAttributes = annotation.getAnnotations(VALUE_MEMBER);
Map<String, String> additionalClientInfoAttributes = new LinkedHashMap<>(clientInfoAttributes.size());
for (AnnotationValue<ConnClientInfoAttr> clientInfoAttribute : clientInfoAttributes) {
String name = clientInfoAttribute.getRequiredValue(NAME_MEMBER, String.class);
String value = clientInfoAttribute.getRequiredValue(VALUE_MEMBER, String.class);
additionalClientInfoAttributes.put(name, value);
}
if (StringUtils.isNotEmpty(applicationName)) {
additionalClientInfoAttributes.putIfAbsent(ORACLE_CLIENT_ID, applicationName);
}
if (annotationMetadata instanceof MethodInvocationContext methodInvocationContext) {
additionalClientInfoAttributes.putIfAbsent(ORACLE_MODULE,
MODULE_CLASS_MAP.computeIfAbsent(methodInvocationContext.getTarget().getClass(),
clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, ""))
);
additionalClientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInvocationContext.getName());
}
return additionalClientInfoAttributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.micronaut.data.connection;


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

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

/**
* Use the default propagation value.
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,20 @@ 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);
}

@Override
Expand All @@ -76,13 +79,17 @@ 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 @NonNull AnnotationMetadata getAnnotationMetadata() {
return annotationMetadata;
}
}

Loading
Loading