From d80ad6a37e416180ff7249278770c9c28e57c930 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 2 Apr 2026 08:25:34 -0700 Subject: [PATCH 01/12] Add JDBC DbInfo semconv bridge --- .../DataSourceDbAttributesExtractor.java | 16 +-- .../jdbc/internal/JdbcAttributesGetter.java | 38 ++++-- .../jdbc/internal/dbinfo/DbInfo.java | 114 +++++++++++++++--- .../OpenTelemetryDataSourceTest.java | 14 +-- .../internal/JdbcAttributesGetterTest.java | 65 ++++++++++ .../internal/OpenTelemetryConnectionTest.java | 15 +-- 6 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetterTest.java diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java index 9ef16f2abce0..47a58c016dbb 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DataSourceDbAttributesExtractor.java @@ -43,18 +43,14 @@ public void onEnd( return; } if (emitStableDatabaseSemconv()) { - attributes.put(DB_NAMESPACE, getName(dbInfo)); - attributes.put(DB_SYSTEM_NAME, SemconvStability.stableDbSystemName(dbInfo.getSystem())); + attributes.put(DB_NAMESPACE, dbInfo.getDbNamespace()); + attributes.put(DB_SYSTEM_NAME, SemconvStability.stableDbSystemName(dbInfo.getDbSystemName())); } if (emitOldDatabaseSemconv()) { - attributes.put(DB_USER, dbInfo.getUser()); - attributes.put(DB_NAME, getName(dbInfo)); - attributes.put(DB_CONNECTION_STRING, dbInfo.getShortUrl()); - attributes.put(DB_SYSTEM, dbInfo.getSystem()); + attributes.put(DB_USER, dbInfo.getDbUser()); + attributes.put(DB_NAME, dbInfo.getDbName()); + attributes.put(DB_CONNECTION_STRING, dbInfo.getDbConnectionString()); + attributes.put(DB_SYSTEM, dbInfo.getDbSystem()); } } - - private static String getName(DbInfo dbInfo) { - return dbInfo.getName(); - } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java index 9836c1dc4dd3..e225180e2edf 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java @@ -25,7 +25,7 @@ public final class JdbcAttributesGetter implements SqlClientAttributesGetter { // Databases where double quotes are exclusively identifiers and cannot be string literals. - private static final Set DOUBLE_QUOTES_FOR_IDENTIFIERS_SYSTEMS = + private static final Set DOUBLE_QUOTES_FOR_IDENTIFIERS_SYSTEM_NAMES = new HashSet<>( asList( // "A string constant in SQL is an arbitrary sequence of characters @@ -35,11 +35,11 @@ public final class JdbcAttributesGetter implements SqlClientAttributesGetter ::= [...] // ::= ... // https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/sql-notation-conventions - "hanadb", + "sap.hana", // "String literals must be enclosed in single quotes. // Double quotes are not supported." // https://clickhouse.com/docs/en/sql-reference/syntax#string @@ -59,36 +59,48 @@ public final class JdbcAttributesGetter implements SqlClientAttributesGetter identifierDialectDbSystemNames() { + return Stream.of(POSTGRESQL, ORACLE_DB, IBM_DB2, DERBY, HSQLDB, SAP_HANA, CLICKHOUSE, POLARDB); + } + + private static Stream stringLiteralDialectDbSystemNames() { + return Stream.of( + // "A string is a sequence of bytes or characters, enclosed within either single quote + // (') or double quote (") characters." + // https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + MYSQL, + // "When SET QUOTED_IDENTIFIER is OFF, ... Literals can be delimited by either single or + // double quotation marks." + // https://learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql + MICROSOFT_SQL_SERVER); + } + + @ParameterizedTest + @MethodSource("identifierDialectDbSystemNames") + void getSqlDialectTreatsDoubleQuotesAsIdentifiers(String dbSystemName) { + DbRequest request = + DbRequest.create(DbInfo.builder().dbSystemName(dbSystemName).build(), "SELECT 1", false); + + assertThat(ATTRIBUTES_GETTER.getSqlDialect(request)).isEqualTo(DOUBLE_QUOTES_ARE_IDENTIFIERS); + } + + @ParameterizedTest + @MethodSource("stringLiteralDialectDbSystemNames") + void getSqlDialectTreatsDoubleQuotesAsStringLiteralsByDefault(String dbSystemName) { + DbRequest request = + DbRequest.create(DbInfo.builder().dbSystemName(dbSystemName).build(), "SELECT 1", false); + + assertThat(ATTRIBUTES_GETTER.getSqlDialect(request)) + .isEqualTo(DOUBLE_QUOTES_ARE_STRING_LITERALS); + } +} diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java index daf4e40f5d42..6112c6449189 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java @@ -101,13 +101,14 @@ void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { private static DbInfo getDbInfo() { return DbInfo.builder() - .system("my_system") - .subtype("my_sub_type") - .shortUrl("my_connection_string") - .user("my_user") - .name("my_name") - .host("my_host") - .port(1234) + .dbSystemName("my_system") + .dbSystem("my_system") + .dbConnectionString("my_connection_string") + .dbUser("my_user") + .dbName("my_name") + .dbNamespace("my_name") + .serverAddress("my_host") + .serverPort(1234) .build(); } From 26e7e2558717c899b36dc388a5008881edb3d741 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 2 Apr 2026 08:26:13 -0700 Subject: [PATCH 02/12] Add JDBC parser framework scaffolding --- .../jdbc/internal/parser/JdbcUrlParser.java | 45 +++ .../jdbc/internal/parser/ParseContext.java | 331 ++++++++++++++++++ .../jdbc/internal/parser/UrlParsingUtils.java | 312 +++++++++++++++++ 3 files changed, 688 insertions(+) create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JdbcUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/UrlParsingUtils.java diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JdbcUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JdbcUrlParser.java new file mode 100644 index 000000000000..577def35dcb4 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JdbcUrlParser.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Interface for JDBC URL parsers. + * + *

IMPORTANT: Implementations should expect that JDBC URLs passed to the {@link + * #parse(String, ParseContext)} method have been lowercased by the caller. URL parameter names + * should be checked using lowercase keys (e.g., "user", "servername", "databasename"). + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface JdbcUrlParser { + + /** + * Parse the JDBC URL and populate the context with extracted information. + * + *

Implementations are responsible for the full parsing lifecycle: + * + *

    + *
  1. Set driver-specific defaults (system name, default host/port/user) + *
  2. Call {@link ParseContext#applyDataSourceProperties()} or {@link + * ParseContext#applyUserProperty()} at the appropriate point to match driver-specific + * precedence semantics. Use {@code applyUserProperty()} for drivers whose DataSource does + * not support the standard serverName/portNumber/databaseName properties. + *
  3. Parse the URL structure + *
+ * + *

The placement of {@link ParseContext#applyDataSourceProperties()} controls precedence: + * + *

    + *
  • Before URL parsing: URL values take precedence (e.g., PostgreSQL, MySQL, Oracle) + *
  • After URL parsing: DataSource properties take precedence (e.g., Microsoft SQL Server) + *
+ * + * @param jdbcUrl the JDBC URL to parse (without the "jdbc:" prefix) + * @param ctx the parse context containing type and optional DataSource properties + */ + void parse(String jdbcUrl, ParseContext ctx); +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java new file mode 100644 index 000000000000..ab607049e2ba --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java @@ -0,0 +1,331 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.buildShortUrl; + +import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.HostPort; +import io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.UrlParams; +import java.util.Map; +import java.util.Properties; +import javax.annotation.Nullable; + +/** + * Mutable context for building up connection info during JDBC URL parsing. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ParseContext { + + private final String type; + @Nullable private String system; + @Nullable private String oldSemconvSystem; + @Nullable private String subtype; + @Nullable private String host; + @Nullable private Integer port; + @Nullable private String user; + @Nullable private String databaseName; + @Nullable private String namespace; + @Deprecated @Nullable private String dbName; + @Nullable private final Properties props; + + private ParseContext(String type, @Nullable Properties props) { + this.type = type; + this.props = props; + } + + /** Create a context with the JDBC type and optional properties. */ + public static ParseContext of(String type, @Nullable Properties props) { + return new ParseContext(type, props); + } + + /** The JDBC type (e.g., "mysql", "postgresql"). */ + public String type() { + return type; + } + + /** The database system identifier (stable/new value, e.g., "postgresql", "h2database"). */ + @Nullable + public String system() { + return system; + } + + /** + * Set the database system identifier (stable/new value). For systems where old and new values + * differ, also call {@link #oldSemconvSystem(String)}. + */ + public void system(String system) { + this.system = system; + } + + /** The old semconv database system identifier (e.g., "mssql", "h2"). */ + @Deprecated // to be removed in 3.0 + @Nullable + public String oldSemconvSystem() { + return oldSemconvSystem; + } + + /** Set the old semconv database system identifier (only required when different from system). */ + @Deprecated // to be removed in 3.0 + public void oldSemconvSystem(@Nullable String oldSemconvSystem) { + this.oldSemconvSystem = oldSemconvSystem; + } + + /** The optional subtype (e.g., "tcp", "aurora"). */ + @Nullable + public String subtype() { + return subtype; + } + + /** Set the subtype value. */ + public void subtype(@Nullable String subtype) { + this.subtype = subtype; + } + + /** The host value accumulated so far. */ + @Nullable + public String host() { + return host; + } + + /** Set the host value. */ + public void host(@Nullable String host) { + this.host = host; + } + + /** The port value accumulated so far. */ + @Nullable + public Integer port() { + return port; + } + + /** Set the port value. */ + public void port(@Nullable Integer port) { + this.port = port; + } + + /** The user value accumulated so far. */ + @Deprecated // to be removed in 3.0 + @Nullable + public String user() { + return user; + } + + /** Set the user value. */ + @Deprecated // to be removed in 3.0 + public void user(@Nullable String user) { + this.user = user; + } + + /** The database name value accumulated so far. */ + @Nullable + public String databaseName() { + return databaseName; + } + + /** Set the database name value. */ + public void databaseName(@Nullable String databaseName) { + this.databaseName = databaseName; + } + + /** The namespace value accumulated so far. */ + @Nullable + public String namespace() { + return namespace; + } + + /** Set the namespace value. */ + public void namespace(@Nullable String namespace) { + this.namespace = namespace; + } + + /** + * Override for the dbName field in the resulting DbInfo. When set, this value takes precedence + * over the databaseName-derived value. Used by SQL Server parsers to preserve old behavior where + * dbName is the instance name when both instance and database are present. + */ + @Deprecated // to be removed in 3.0 + @Nullable + public String dbName() { + return dbName; + } + + @Deprecated // to be removed in 3.0 + public void dbName(@Nullable String dbName) { + this.dbName = dbName; + } + + /** DataSource connection properties. */ + @Nullable + public Properties props() { + return props; + } + + /** + * Apply common parameters from URL parameters to the context. + * + *

Extracts the same properties as {@link #applyDataSourceProperties()} but using lowercase + * keys (servername, portnumber, databasename, user) as URL params are typically lowercased. + * + * @param jdbcUrl the JDBC URL containing parameters + * @param startDelimiter the delimiter marking the start of parameters (";" or "?") + * @param splitSeparator the separator between individual parameters (";" or "&") + */ + public void applyCommonParams(String jdbcUrl, String startDelimiter, String splitSeparator) { + Map params = + UrlParsingUtils.extractParams(jdbcUrl, startDelimiter, splitSeparator); + + if (params.isEmpty()) { + return; + } + if (params.containsKey("servername")) { + this.host = params.get("servername"); + } + Integer port = UrlParsingUtils.parsePort(params.get("portnumber")); + if (port != null) { + this.port = port; + } + String databaseName = params.get("databasename"); + if (databaseName != null && !databaseName.isEmpty()) { + this.databaseName = databaseName; + } + if (params.containsKey("user")) { + this.user = params.get("user"); + } + } + + /** + * Apply common DataSource properties to this context. These properties are defined by the JDBC + * specification (JSR 221, Section 9.4.1). + * + *

Extracts serverName, portNumber, databaseName, and user from the properties if present. + */ + public void applyDataSourceProperties() { + if (props == null) { + return; + } + + String serverName = props.getProperty("serverName"); + if (serverName != null && !serverName.isEmpty()) { + this.host = serverName; + } + + Integer parsedPort = UrlParsingUtils.parsePort(props.getProperty("portNumber")); + if (parsedPort != null) { + this.port = parsedPort; + } + + String databaseName = props.getProperty("databaseName"); + if (databaseName != null && !databaseName.isEmpty()) { + this.databaseName = databaseName; + } + + String propsUser = props.getProperty("user"); + if (propsUser != null && !propsUser.isEmpty()) { + this.user = propsUser; + } + } + + /** + * Apply only the user property from DataSource properties. Use this for drivers that don't + * support the standard serverName/portNumber/databaseName DataSource properties (e.g., SAP HANA, + * H2, HSQLDB, Derby). + * + *

TODO: Currently delegates to {@link #applyDataSourceProperties()} to avoid a behavioral + * change in this refactoring. In the future, this will be changed to only apply the user + * property. + */ + public void applyUserProperty() { + // TODO: change to only apply user property + applyDataSourceProperties(); + } + + /** + * Parse a URL-style JDBC connection string that uses semicolons for properties. Updates this + * context with extracted values (user, host, port, path). + * + *

Database path acts as fallback and does not override an existing database name. + * + * @param jdbcUrl the JDBC URL to parse + */ + public void parseUrl(String jdbcUrl) { + // Split off semicolon-delimited parameters + String[] split = jdbcUrl.split(";", 2); + String urlPart = split[0]; + if (split.length > 1) { + UrlParams params = UrlParams.fromSemicolon(split[1]); + if (params.get("user") != null) { + this.user = params.get("user"); + } + } + + int hostIndex = urlPart.indexOf("://"); + if (hostIndex <= 0) { + return; + } + + // Parse URL host/port + String serverName = urlPart.substring(hostIndex + 3); + if (serverName.isEmpty()) { + return; + } + + // Extract database path from URL + int pathLoc = serverName.indexOf("/"); + if (pathLoc > 0) { + String databaseName = serverName.substring(pathLoc + 1); + if (!databaseName.isEmpty()) { + this.databaseName = databaseName; + } + serverName = serverName.substring(0, pathLoc); + } + + // Handle IPv6 addresses and extract host:port + HostPort hostPort = UrlParsingUtils.extractHostPort(serverName); + if (hostPort.port() != null) { + this.port = hostPort.port(); + } + if (!hostPort.host().isEmpty()) { + this.host = hostPort.host(); + } + } + + /** + * Build the final DbInfo from the accumulated context values. + * + * @return the complete DbInfo + */ + public DbInfo toDbInfo() { + // oldSemconvSystem falls back to system when not explicitly set (i.e., when both are the same) + String oldSystem = oldSemconvSystem != null ? oldSemconvSystem : system; + DbInfo.Builder builder = DbInfo.builder().dbSystemName(system).dbSystem(oldSystem); + if (host != null) { + builder.serverAddress(host); + } + if (port != null) { + builder.serverPort(port); + } + if (user != null) { + builder.dbUser(user); + } + if (namespace != null) { + builder.dbNamespace(namespace); + } else if (databaseName != null) { + builder.dbNamespace(databaseName); + } + if (dbName != null) { + builder.dbName(dbName); + } else if (databaseName != null) { + builder.dbName(databaseName); + } else if (namespace != null) { + builder.dbName(namespace); + } + builder.dbConnectionString(buildShortUrl(type, subtype, host, port)); + return builder.build(); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/UrlParsingUtils.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/UrlParsingUtils.java new file mode 100644 index 000000000000..e58c20889214 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/UrlParsingUtils.java @@ -0,0 +1,312 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static java.util.Collections.emptyMap; +import static java.util.logging.Level.FINE; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * Utility methods for parsing JDBC URLs. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class UrlParsingUtils { + + private static final Logger logger = Logger.getLogger(UrlParsingUtils.class.getName()); + + // Source: Regular Expressions Cookbook 2nd edition - 8.17. + // Matches Standard, Mixed or Compressed notation in a wider body of text + public static final Pattern IPV6_PATTERN = + Pattern.compile( + // Non Compressed + "(?:(?:(?:[A-F0-9]{1,4}:){6}" + // Compressed with at most 6 colons + + "|(?=(?:[A-F0-9]{0,4}:){0,6}" + // and 4 bytes and anchored + + "(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?![:.\\w]))" + // and at most 1 double colon + + "(([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:)" + // Compressed with 7 colons and 5 numbers + + "|::(?:[A-F0-9]{1,4}:){5})" + // 255.255.255. + + "(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}" + // 255 + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + // Standard + + "|(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}" + // Compressed with at most 7 colons and anchored + + "|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}(?![:.\\w]))" + // and at most 1 double colon + + "(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)" + // Compressed with 8 colons + + "|(?:[A-F0-9]{1,4}:){7}:|:(:[A-F0-9]{1,4}){7})(?![:.\\w])", + Pattern.CASE_INSENSITIVE); + + private UrlParsingUtils() {} + + /** + * Split a query string into key-value pairs. + * + * @param query the query string + * @param separator the separator between pairs (e.g., "&" or ";") + * @return a map of key-value pairs + */ + // Source: https://stackoverflow.com/a/13592567 + public static Map splitQuery(String query, String separator) { + if (query == null || query.isEmpty()) { + return emptyMap(); + } + Map queryPairs = new LinkedHashMap<>(); + String[] pairs = query.split(separator); + for (String pair : pairs) { + try { + int idx = pair.indexOf("="); + String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; + if (!queryPairs.containsKey(key)) { + String value = + idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") + : null; + queryPairs.put(key, value); + } + } catch (UnsupportedEncodingException e) { + // Ignore. + } + } + return queryPairs; + } + + /** + * Parse an integer value, returning null if parsing fails. + * + * @param value the string value to parse + * @return the parsed integer, or null if parsing fails + */ + public static Integer parsePort(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + logger.log(FINE, e.getMessage(), e); + return null; + } + } + + /** + * Build the short URL for db.connection_string attribute. + * + * @param type the JDBC type (e.g., "postgresql", "mysql") + * @param subtype optional subtype (e.g., "thin" for Oracle, "aurora" for MySQL) + * @param host the host name + * @param port the port number + * @return the short URL in format "type:[subtype:]//host:port" or "type:" if no host + */ + public static String buildShortUrl( + String type, @Nullable String subtype, @Nullable String host, @Nullable Integer port) { + StringBuilder url = new StringBuilder(type); + url.append(':'); + if (subtype != null) { + url.append(subtype); + url.append(':'); + } + if (host != null) { + url.append("//"); + url.append(host); + if (port != null) { + url.append(':'); + url.append(port); + } + } + return url.toString(); + } + + /** + * Extract parameters from a JDBC URL. + * + * @param jdbcUrl the JDBC URL + * @param startDelimiter the delimiter marking the start of parameters (";" or "?") + * @param splitSeparator the separator between parameters (";" or "&") + * @return a map of parameter key-value pairs + */ + public static Map extractParams( + String jdbcUrl, String startDelimiter, String splitSeparator) { + int paramLoc = jdbcUrl.indexOf(startDelimiter); + if (paramLoc < 0) { + return emptyMap(); + } + return splitQuery(jdbcUrl.substring(paramLoc + 1), splitSeparator); + } + + /** + * Extract semicolon-delimited URL parameters from a JDBC URL. + * + * @param jdbcUrl the JDBC URL containing parameters after semicolon + * @return a map of parameter key-value pairs + */ + public static Map extractSemicolonParams(String jdbcUrl) { + return extractParams(jdbcUrl, ";", ";"); + } + + /** + * Extract query-style URL parameters from a JDBC URL. + * + * @param jdbcUrl the JDBC URL containing parameters after "?" + * @param separator the parameter separator (typically "&") + * @return a map of parameter key-value pairs + */ + public static Map extractQueryParams(String jdbcUrl, String separator) { + return extractParams(jdbcUrl, "?", separator); + } + + /** + * Extract subtype from a JDBC URL of the form "type:subtype://...". + * + *

For example, "mysql:aurora://host:port/db" returns "aurora", "oceanbase:oracle://host/db" + * returns "oracle". + * + * @param jdbcUrl the JDBC URL + * @return the subtype, or null if no subtype is present + */ + @Nullable + public static String extractSubtype(String jdbcUrl) { + int protoLoc = jdbcUrl.indexOf("://"); + int typeEndLoc = jdbcUrl.indexOf(':'); + if (protoLoc > 0 && typeEndLoc > 0 && typeEndLoc < protoLoc) { + return jdbcUrl.substring(typeEndLoc + 1, protoLoc); + } + return null; + } + + /** + * Find the index of the first occurrence of any of the specified characters. + * + * @param str the string to search + * @param chars the characters to search for + * @return the index of the first occurrence, or -1 if none found + */ + public static int indexOfAny(String str, char... chars) { + int minIndex = -1; + for (char c : chars) { + int idx = str.indexOf(c); + if (idx >= 0 && (minIndex < 0 || idx < minIndex)) { + minIndex = idx; + } + } + return minIndex; + } + + /** + * Result of parsing a server string into host and port components. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ + public static final class HostPort { + private final String host; + @Nullable private final Integer port; + @Nullable private final String ipv6Address; + + private HostPort(String host, @Nullable Integer port, @Nullable String ipv6Address) { + this.host = host; + this.port = port; + this.ipv6Address = ipv6Address; + } + + /** The host, with IPv6 addresses bracketed. */ + public String host() { + return host; + } + + /** The port, or null if not specified. */ + @Nullable + public Integer port() { + return port; + } + + /** The raw IPv6 address match (without brackets), or null if not IPv6. */ + @Nullable + public String ipv6Address() { + return ipv6Address; + } + } + + /** + * Extract host and port from a server string, handling IPv6 addresses. Supports formats: host, + * host:port, [ipv6], [ipv6]:port, ipv6 (auto-bracketed). + * + * @param serverName the server string to parse + * @return the extracted host and port + */ + public static HostPort extractHostPort(String serverName) { + Matcher ipv6Matcher = IPV6_PATTERN.matcher(serverName); + boolean isIpv6 = ipv6Matcher.find(); + String ipv6Address = isIpv6 ? ipv6Matcher.group(0) : null; + + int portLoc = -1; + if (isIpv6) { + if (serverName.startsWith("[")) { + portLoc = serverName.indexOf("]:") + 1; + } else { + serverName = "[" + serverName + "]"; + } + } else { + portLoc = serverName.indexOf(":"); + } + + Integer port = null; + if (portLoc > 0) { + port = parsePort(serverName.substring(portLoc + 1)); + serverName = serverName.substring(0, portLoc); + } + + return new HostPort(serverName, port, ipv6Address); + } + + /** + * Lightweight wrapper for URL parameters that provides cleaner access patterns. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ + public static final class UrlParams { + private static final UrlParams EMPTY = new UrlParams(emptyMap()); + + private final Map params; + + private UrlParams(Map params) { + this.params = params; + } + + /** Parse semicolon-delimited parameters (e.g., "user=foo;password=bar"). */ + public static UrlParams fromSemicolon(@Nullable String paramString) { + if (paramString == null || paramString.isEmpty()) { + return EMPTY; + } + return new UrlParams(splitQuery(paramString, ";")); + } + + /** Get parameter value, or null if not present. */ + @Nullable + public String get(String key) { + return params.get(key); + } + + /** Get parameter value, or default if not present. */ + public String getOrDefault(String key, String defaultValue) { + String value = params.get(key); + return value != null ? value : defaultValue; + } + } +} From 7ccd833cb33db8018d1a718d77baaf8641325907 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 2 Apr 2026 08:26:54 -0700 Subject: [PATCH 03/12] Add simple JDBC parser implementations --- .../internal/parser/ClickhouseUrlParser.java | 46 +++++ .../jdbc/internal/parser/DerbyUrlParser.java | 167 +++++++++++++++ .../internal/parser/GenericUrlParser.java | 83 ++++++++ .../jdbc/internal/parser/H2UrlParser.java | 87 ++++++++ .../jdbc/internal/parser/HsqlUrlParser.java | 106 ++++++++++ .../internal/parser/LindormUrlParser.java | 49 +++++ .../jdbc/internal/parser/MysqlUrlParser.java | 193 ++++++++++++++++++ .../internal/parser/OceanbaseUrlParser.java | 57 ++++++ .../internal/parser/PolardbUrlParser.java | 34 +++ .../internal/parser/PostgresqlUrlParser.java | 87 ++++++++ .../jdbc/internal/parser/SapUrlParser.java | 46 +++++ 11 files changed, 955 insertions(+) create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ClickhouseUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DerbyUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/GenericUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/H2UrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/HsqlUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/LindormUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MysqlUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OceanbaseUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PolardbUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PostgresqlUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/SapUrlParser.java diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ClickhouseUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ClickhouseUrlParser.java new file mode 100644 index 000000000000..86ed5bfc8483 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ClickhouseUrlParser.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for ClickHouse JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • clickhouse:http://host:8123/db + *
  • clickhouse:https://host:8443/db + *
  • clickhouse://host:9000/db + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ClickhouseUrlParser implements JdbcUrlParser { + + private static final String SYSTEM = "clickhouse"; + + public static final ClickhouseUrlParser INSTANCE = new ClickhouseUrlParser(); + + private ClickhouseUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(SYSTEM); + + ctx.applyUserProperty(); + + String clickhouseUrl = jdbcUrl.substring("clickhouse:".length()); + + // Extract protocol (http or https) as subtype from URLs like "http://..." or "https://..." + int protoLoc = clickhouseUrl.indexOf("://"); + if (protoLoc > 0) { + ctx.subtype(clickhouseUrl.substring(0, protoLoc)); + } + + GenericUrlParser.INSTANCE.parse(clickhouseUrl, ctx); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DerbyUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DerbyUrlParser.java new file mode 100644 index 000000000000..722778302b43 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DerbyUrlParser.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.parsePort; +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.splitQuery; + +import java.util.Map; + +/** + * Parser for Apache Derby JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • derby:mydb (directory, default) + *
  • derby:directory:/path/to/db + *
  • derby:memory:testdb (in-memory) + *
  • derby:classpath:db (from classpath) + *
  • derby:jar:(path/to/db.jar)db + *
  • derby://host:1527/db (network) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class DerbyUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String DERBY = "derby"; + + private static final String DEFAULT_USER = "APP"; + private static final int DEFAULT_PORT = 1527; + private static final String[] SIMPLE_MODES = {"memory", "classpath", "jar"}; + + public static final DerbyUrlParser INSTANCE = new DerbyUrlParser(); + + private DerbyUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(DERBY); + ctx.user(DEFAULT_USER); + + ctx.applyUserProperty(); + + // Parse URL + String derbyUrl = jdbcUrl.substring("derby:".length()); + String[] split = derbyUrl.split(";", 2); + + String details = split[0]; + + // Parse URL path first — Derby gives subname (path) priority over ;databaseName= attribute. + // See https://db.apache.org/derby/docs/10.8/ref/rrefattrib17246.html + parseDetails(details, ctx); + + if (split.length > 1) { + // Apply semicolon-delimited params; databaseName is fallback only (path takes priority) + applyParamsAsFallback(split[1], ctx); + } + } + + /** + * Apply semicolon-delimited URL parameters, with databaseName as fallback only. + * + *

Derby gives the subname (path) priority over the databaseName attribute, so we only set + * databaseName from params when no path was present. + */ + private static void applyParamsAsFallback(String paramString, ParseContext ctx) { + Map params = splitQuery(paramString, ";"); + if (params.containsKey("user")) { + ctx.user(params.get("user")); + } + // databaseName attribute is fallback only — subname (path) takes priority + if (ctx.databaseName() == null) { + String databaseName = params.get("databasename"); + if (databaseName != null && !databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + } + } + + private static void parseDetails(String details, ParseContext ctx) { + // Handle network mode (starts with //) + if (details.startsWith("//")) { + parseNetworkMode(details, ctx); + ctx.subtype("network"); + return; + } + + // Local modes: host/port don't apply — clear any values set by DataSource properties + ctx.host(null); + ctx.port(null); + + // Handle directory mode specially (uses parseDirectoryName) + if (details.startsWith("directory:")) { + String databaseName = parseDirectoryName(details.substring("directory:".length())); + if (databaseName != null && !databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + ctx.subtype("directory"); + return; + } + + // Handle simple prefix modes: memory, classpath, jar + for (String mode : SIMPLE_MODES) { + String prefix = mode + ":"; + if (details.startsWith(prefix)) { + String databaseName = details.substring(prefix.length()); + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + ctx.subtype(mode); + return; + } + } + + // Default to directory + if (!details.isEmpty()) { + ctx.databaseName(details); + } + ctx.subtype("directory"); + } + + private static String parseDirectoryName(String urlInstance) { + if (!urlInstance.isEmpty()) { + int dbNameStartLocation = urlInstance.lastIndexOf('/'); + if (dbNameStartLocation != -1) { + return urlInstance.substring(dbNameStartLocation + 1); + } + return urlInstance; + } + return null; + } + + private static void parseNetworkMode(String details, ParseContext ctx) { + String url = details.substring("//".length()); + + int instanceLoc = url.indexOf("/"); + if (instanceLoc >= 0) { + String databaseName = url.substring(instanceLoc + 1); + int protoLoc = databaseName.indexOf(":"); + if (protoLoc >= 0) { + databaseName = databaseName.substring(protoLoc + 1); + } + // Path takes priority over ;databaseName= param + ctx.databaseName(databaseName); + url = url.substring(0, instanceLoc); + } + + int portLoc = url.indexOf(":"); + if (portLoc > 0) { + ctx.host(url.substring(0, portLoc)); + Integer port = parsePort(url.substring(portLoc + 1)); + if (port != null) { + ctx.port(port); + } + } else { + ctx.host(url); + ctx.port(DEFAULT_PORT); + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/GenericUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/GenericUrlParser.java new file mode 100644 index 000000000000..23f504ff986e --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/GenericUrlParser.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static java.util.logging.Level.FINE; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Logger; + +/** + * Parses standard URL-like JDBC connection strings using Java's URI parser. + * + *

Used by database-specific parsers to handle standard URL formats like: {@code + * type://host:port/db?user=username} + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class GenericUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String OTHER_SQL = "other_sql"; + + private static final Logger logger = Logger.getLogger(GenericUrlParser.class.getName()); + + public static final GenericUrlParser INSTANCE = new GenericUrlParser(); + + private GenericUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + if (ctx.system() == null) { + ctx.system(OTHER_SQL); + } + + URI uri; + try { + uri = new URI(jdbcUrl); + } catch (URISyntaxException e) { + logger.log(FINE, e.getMessage(), e); + return; + } + + // 1. User from URI userInfo + String uriUser = uri.getUserInfo(); + if (uriUser != null) { + int colonIndex = uriUser.indexOf(':'); + if (colonIndex != -1) { + uriUser = uriUser.substring(0, colonIndex); + } + ctx.user(uriUser); + } + + // 2. Host and port from URI + if (uri.getHost() != null) { + ctx.host(uri.getHost()); + } + if (uri.getPort() > 0) { + ctx.port(uri.getPort()); + } + + // 3. Database from path + String databaseName = uri.getPath(); + if (databaseName == null) { + databaseName = ""; + } + if (databaseName.startsWith("/")) { + databaseName = databaseName.substring(1); + } + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + + // 4. Query params (highest precedence) + // URL is lowercased by JdbcConnectionUrlParser, so check lowercase param names + ctx.applyCommonParams(jdbcUrl, "?", "&"); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/H2UrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/H2UrlParser.java new file mode 100644 index 000000000000..2e8c87d548f1 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/H2UrlParser.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for H2 Database JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • h2:mem:testdb (in-memory) + *
  • h2:file:/path/to/db (file-based) + *
  • h2:zip:/path/to/db.zip!/db (zip archive) + *
  • h2:tcp://host:8082/db (network) + *
  • h2:ssl://host:8082/db (secure network) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class H2UrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String H2DATABASE = "h2database"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String H2 = "h2"; + + private static final int DEFAULT_PORT = 8082; + private static final String[] LOCAL_MODES = {"mem", "file", "zip"}; + private static final String[] NETWORK_MODES = {"tcp", "ssl"}; + + public static final H2UrlParser INSTANCE = new H2UrlParser(); + + private H2UrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(H2DATABASE); + ctx.oldSemconvSystem(H2); + + ctx.applyUserProperty(); + + String h2Url = jdbcUrl.substring("h2:".length()); + + for (String mode : LOCAL_MODES) { + String prefix = mode + ":"; + if (h2Url.startsWith(prefix)) { + parseLocalMode(h2Url.substring(prefix.length()), mode, ctx); + return; + } + } + + for (String mode : NETWORK_MODES) { + if (h2Url.startsWith(mode + ":")) { + parseNetworkMode(mode, jdbcUrl, ctx); + return; + } + } + + // Default to file + parseLocalMode(h2Url, "file", ctx); + } + + private static void parseLocalMode(String remainder, String subtype, ParseContext ctx) { + // Local modes have no network host/port — clear any values set by DataSource properties + ctx.host(null); + ctx.port(null); + + int propLoc = remainder.indexOf(";"); + String databaseName = propLoc >= 0 ? remainder.substring(0, propLoc) : remainder; + + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + ctx.subtype(subtype); + } + + private static void parseNetworkMode(String subtype, String jdbcUrl, ParseContext ctx) { + ctx.port(DEFAULT_PORT); + ctx.subtype(subtype); + ctx.parseUrl(jdbcUrl); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/HsqlUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/HsqlUrlParser.java new file mode 100644 index 000000000000..675777afe407 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/HsqlUrlParser.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.indexOfAny; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parser for HyperSQL (HSQLDB) JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • hsqldb:mem:testdb (in-memory) + *
  • hsqldb:file:/path/to/db (file-based) + *
  • hsqldb:res:/path/to/db (resource) + *
  • hsqldb:hsql://host:9001/db (network) + *
  • hsqldb:hsqls://host:9001/db (secure network) + *
  • hsqldb:http://host:80/db (HTTP) + *
  • hsqldb:https://host:443/db (HTTPS) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class HsqlUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String HSQLDB = "hsqldb"; + + private static final String DEFAULT_USER = "SA"; + private static final int DEFAULT_PORT = 9001; + private static final String[] LOCAL_MODES = {"mem", "file", "res"}; + private static final Map NETWORK_MODES = buildNetworkModes(); + + private static Map buildNetworkModes() { + Map modes = new HashMap<>(4); + modes.put("hsql", DEFAULT_PORT); + modes.put("hsqls", DEFAULT_PORT); + modes.put("http", 80); + modes.put("https", 443); + return modes; + } + + public static final HsqlUrlParser INSTANCE = new HsqlUrlParser(); + + private HsqlUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(HSQLDB); + ctx.user(DEFAULT_USER); + + ctx.applyUserProperty(); + + String hsqlUrl = jdbcUrl.substring("hsqldb:".length()); + + // Strip parameters (semicolon or query string) + int paramIndex = indexOfAny(hsqlUrl, ';', '?'); + if (paramIndex >= 0) { + hsqlUrl = hsqlUrl.substring(0, paramIndex); + } + + for (String mode : LOCAL_MODES) { + String prefix = mode + ":"; + if (hsqlUrl.startsWith(prefix)) { + parseLocalMode(hsqlUrl.substring(prefix.length()), mode, ctx); + return; + } + } + + for (Map.Entry entry : NETWORK_MODES.entrySet()) { + if (hsqlUrl.startsWith(entry.getKey() + ":")) { + parseNetworkMode(entry.getKey(), jdbcUrl, entry.getValue(), ctx); + return; + } + } + + // Default to mem + parseLocalMode(hsqlUrl, "mem", ctx); + } + + private static void parseLocalMode(String databaseName, String subtype, ParseContext ctx) { + // Local modes have no network host/port — clear any values set by DataSource properties + ctx.host(null); + ctx.port(null); + + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + ctx.subtype(subtype); + } + + private static void parseNetworkMode( + String subtype, String jdbcUrl, int defaultPort, ParseContext ctx) { + ctx.port(defaultPort); + ctx.subtype(subtype); + ctx.parseUrl(jdbcUrl); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/LindormUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/LindormUrlParser.java new file mode 100644 index 000000000000..82aff05581b4 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/LindormUrlParser.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for Alibaba Lindorm JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • jdbc:lindorm:table:url=http//server_name:30060/test + *
  • jdbc:lindorm:tsdb:url=http://server_name:8242/test + *
  • jdbc:lindorm:search:url=http://server_name:30070/test + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class LindormUrlParser implements JdbcUrlParser { + + private static final String SYSTEM = "lindorm"; + + public static final LindormUrlParser INSTANCE = new LindormUrlParser(); + + private LindormUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(SYSTEM); + + ctx.applyUserProperty(); + + String lindormUrl = jdbcUrl.substring("lindorm:".length()); + + int urlIndex = lindormUrl.indexOf(":url="); + if (urlIndex < 0) { + return; + } + + // Extract subtype (table, tsdb, search) before :url= + ctx.subtype(lindormUrl.substring(0, urlIndex)); + + String realUrl = lindormUrl.substring(urlIndex + 5); + GenericUrlParser.INSTANCE.parse(realUrl, ctx); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MysqlUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MysqlUrlParser.java new file mode 100644 index 000000000000..f19277581dee --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MysqlUrlParser.java @@ -0,0 +1,193 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.extractSubtype; +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.parsePort; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for MySQL and MariaDB JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • mysql://host:3306/db + *
  • mysql://host/db?user=root + *
  • mysql:aurora://host:3306/db + *
  • mysql:host:3306/db (non-standard format) + *
  • mariadb:replication://host1,host2/db + *
  • mariadb:sequential:address=(host=host1)(port=3306)(user=root)/db + *
  • mysql://[::1]:3306/db (IPv6) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class MysqlUrlParser implements JdbcUrlParser { + + // copied from DbAttributes.DbSystemNameValues + private static final String MYSQL = "mysql"; + // copied from DbAttributes.DbSystemNameValues + private static final String MARIADB = "mariadb"; + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String OTHER_SQL = "other_sql"; + + private static final Map TYPE_TO_SYSTEM = buildTypeToSystem(); + + private static Map buildTypeToSystem() { + Map map = new HashMap<>(2); + map.put("mysql", MYSQL); + map.put("mariadb", MARIADB); + return map; + } + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 3306; + + public static final MysqlUrlParser INSTANCE = new MysqlUrlParser(); + + private MysqlUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + String system = TYPE_TO_SYSTEM.get(ctx.type()); + if (system == null) { + // not possible: JdbcConnectionUrlParser only maps "mysql" and "mariadb" to this parser + system = OTHER_SQL; + } + ctx.system(system); + ctx.host(DEFAULT_HOST); + ctx.port(DEFAULT_PORT); + + ctx.applyUserProperty(); + + // Parse URL (overwrites defaults and props) + String subtype = extractSubtype(jdbcUrl); + int protoLoc = jdbcUrl.indexOf("://"); + + if (subtype != null) { + // Has subprotocol (e.g., mysql:aurora://...) + ctx.subtype(subtype); + parseMariaSubProtocol(jdbcUrl.substring(protoLoc + 3), ctx); + } else if (protoLoc > 0) { + // Standard URL format - delegate to GenericUrlParser + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + } else { + // Non-standard format: type/host:port/db?params + parseNonStandardUrl(jdbcUrl, ctx); + } + } + + private static void parseNonStandardUrl(String jdbcUrl, ParseContext ctx) { + int typeEndLoc = jdbcUrl.indexOf(':'); + int portLoc = jdbcUrl.indexOf(":", typeEndLoc + 1); + int dbLoc = jdbcUrl.indexOf("/", typeEndLoc); + int paramLoc = jdbcUrl.indexOf("?", dbLoc); + + // Extract database name + String databaseName; + if (paramLoc > 0) { + databaseName = jdbcUrl.substring(dbLoc + 1, paramLoc); + } else { + databaseName = jdbcUrl.substring(dbLoc + 1); + } + ctx.databaseName(databaseName); + + // Host and port from URL + int hostEndLoc; + if (portLoc > 0) { + hostEndLoc = portLoc; + Integer parsedPort = parsePort(jdbcUrl.substring(portLoc + 1, dbLoc)); + if (parsedPort != null) { + ctx.port(parsedPort); + } + } else { + hostEndLoc = dbLoc; + } + ctx.host(jdbcUrl.substring(typeEndLoc + 1, hostEndLoc)); + + // Apply query params (highest precedence) + ctx.applyCommonParams(jdbcUrl, "?", "&"); + } + + private static void parseMariaSubProtocol(String jdbcUrl, ParseContext ctx) { + int hostEndLoc; + int clusterSepLoc = jdbcUrl.indexOf(","); + int ipv6End = jdbcUrl.startsWith("[") ? jdbcUrl.indexOf("]") : -1; + int portLoc = jdbcUrl.indexOf(":", Math.max(0, ipv6End)); + portLoc = clusterSepLoc != -1 && clusterSepLoc < portLoc ? -1 : portLoc; + int dbLoc = jdbcUrl.indexOf("/", Math.max(portLoc, clusterSepLoc)); + + int paramLoc = dbLoc != -1 ? jdbcUrl.indexOf("?", dbLoc) : -1; + + if (paramLoc > 0) { + ctx.databaseName(jdbcUrl.substring(dbLoc + 1, paramLoc)); + } else if (dbLoc != -1) { + ctx.databaseName(jdbcUrl.substring(dbLoc + 1)); + } + + if (jdbcUrl.startsWith("address=")) { + // Apply query params first so address fields can override them + ctx.applyCommonParams(jdbcUrl, "?", "&"); + parseMariaAddress(jdbcUrl, ctx); + return; + } + + int effectiveDbLoc = dbLoc != -1 ? dbLoc : jdbcUrl.length(); + if (portLoc > 0) { + hostEndLoc = portLoc; + int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : effectiveDbLoc; + Integer parsedPort = parsePort(jdbcUrl.substring(portLoc + 1, portEndLoc)); + if (parsedPort != null) { + ctx.port(parsedPort); + } + } else { + hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : effectiveDbLoc; + } + + if (ipv6End > 0) { + ctx.host(jdbcUrl.substring(1, ipv6End)); + } else { + ctx.host(jdbcUrl.substring(0, hostEndLoc)); + } + + // Apply query params (highest precedence) + ctx.applyCommonParams(jdbcUrl, "?", "&"); + } + + private static final Pattern HOST_PATTERN = + Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private static final Pattern PORT_PATTERN = + Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private static final Pattern USER_PATTERN = + Pattern.compile("\\(\\s*user\\s*=\\s*([^ )]+)\\s*\\)"); + + private static void parseMariaAddress(String jdbcUrl, ParseContext ctx) { + int addressEnd = jdbcUrl.indexOf(",address="); + String addressUrl = addressEnd > 0 ? jdbcUrl.substring(0, addressEnd) : jdbcUrl; + + Matcher hostMatcher = HOST_PATTERN.matcher(addressUrl); + if (hostMatcher.find()) { + ctx.host(hostMatcher.group(1)); + } + + Matcher portMatcher = PORT_PATTERN.matcher(addressUrl); + if (portMatcher.find()) { + ctx.port(Integer.parseInt(portMatcher.group(1))); + } + + Matcher userMatcher = USER_PATTERN.matcher(addressUrl); + if (userMatcher.find()) { + ctx.user(userMatcher.group(1)); + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OceanbaseUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OceanbaseUrlParser.java new file mode 100644 index 000000000000..cb7aa0156186 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OceanbaseUrlParser.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.extractSubtype; + +/** + * Parser for OceanBase JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • jdbc:oceanbase://host:port/dbname + *
  • jdbc:oceanbase:oracle://host:port/dbname + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class OceanbaseUrlParser implements JdbcUrlParser { + + private static final String SYSTEM = "oceanbase"; + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String ORACLE_DB = "oracle.db"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String ORACLE = "oracle"; + + public static final OceanbaseUrlParser INSTANCE = new OceanbaseUrlParser(); + + private OceanbaseUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(SYSTEM); + + ctx.applyUserProperty(); + + String subtype = extractSubtype(jdbcUrl); + + if (subtype != null) { + // Has subtype (e.g., oceanbase:oracle://...) + if (subtype.equals(ORACLE)) { + // Override system for Oracle mode + ctx.system(ORACLE_DB); + ctx.oldSemconvSystem(ORACLE); + } + ctx.subtype(subtype); + ctx.parseUrl(jdbcUrl); + } else { + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PolardbUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PolardbUrlParser.java new file mode 100644 index 000000000000..b43a7c4b796e --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PolardbUrlParser.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for Alibaba PolarDB JDBC URLs. + * + *

Sample URL: jdbc:polardb://server_name:1901/dbname + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class PolardbUrlParser implements JdbcUrlParser { + + private static final String SYSTEM = "polardb"; + private static final int DEFAULT_PORT = 1521; + + public static final PolardbUrlParser INSTANCE = new PolardbUrlParser(); + + private PolardbUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(SYSTEM); + ctx.port(DEFAULT_PORT); + + ctx.applyUserProperty(); + + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PostgresqlUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PostgresqlUrlParser.java new file mode 100644 index 000000000000..935c256b2dd2 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/PostgresqlUrlParser.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.splitQuery; + +import java.util.Map; +import java.util.Properties; + +/** + * Parses PostgreSQL JDBC connection strings. + * + *

Sample URLs: + * + *

    + *
  • postgresql://host:5432/db + *
  • postgresql://host/db?user=postgres + *
  • postgresql://user@host/db + *
  • pgsql://host:5432/db + *
  • postgresql://host/db?currentSchema=myschema + *
+ * + *

PostgreSQL defaults to localhost:5432 when host/port are not specified. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class PostgresqlUrlParser implements JdbcUrlParser { + + // copied from DbAttributes.DbSystemNameValues + private static final String POSTGRESQL = "postgresql"; + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 5432; + + public static final PostgresqlUrlParser INSTANCE = new PostgresqlUrlParser(); + + private PostgresqlUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(POSTGRESQL); + ctx.host(DEFAULT_HOST); + ctx.port(DEFAULT_PORT); + + ctx.applyUserProperty(); + + // Delegate to generic parser for standard URL parsing + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + + String schema = extractCurrentSchema(jdbcUrl); + if (schema == null) { + schema = extractCurrentSchema(ctx.props()); + } + if (schema == null && ctx.user() != null) { + // Fall back to user as schema if no currentSchema param + schema = ctx.user(); + } + + // Format namespace as database|schema (only when schema is available) + String database = ctx.databaseName(); + if (database != null && schema != null) { + ctx.namespace(database + "|" + schema); + } + } + + private static String extractCurrentSchema(String jdbcUrl) { + int queryIndex = jdbcUrl.indexOf('?'); + if (queryIndex < 0) { + return null; + } + Map params = splitQuery(jdbcUrl.substring(queryIndex + 1), "&"); + return params.get("currentschema"); + } + + private static String extractCurrentSchema(Properties props) { + if (props == null) { + return null; + } + String currentSchema = props.getProperty("currentSchema"); + return currentSchema == null || currentSchema.isEmpty() ? null : currentSchema; + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/SapUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/SapUrlParser.java new file mode 100644 index 000000000000..2d9f4520745a --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/SapUrlParser.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for SAP HANA JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • sap://host:30015 + *
  • sap://host:30015/db?user=system + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class SapUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String SAP_HANA = "sap.hana"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String HANADB = "hanadb"; + + private static final String DEFAULT_HOST = "localhost"; + + public static final SapUrlParser INSTANCE = new SapUrlParser(); + + private SapUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(SAP_HANA); + ctx.oldSemconvSystem(HANADB); + ctx.host(DEFAULT_HOST); + + // SAP HANA driver doesn't support serverName/portNumber/databaseName DataSource properties + ctx.applyUserProperty(); + + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + } +} From 97c5aff9a4fb7414a4647ae979eb091fd52fbb7d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 2 Apr 2026 08:27:32 -0700 Subject: [PATCH 04/12] Add complex JDBC parser implementations --- .../internal/parser/DataDirectUrlParser.java | 112 +++++++++++ .../jdbc/internal/parser/Db2UrlParser.java | 58 ++++++ .../parser/InformixDirectUrlParser.java | 57 ++++++ .../parser/InformixSqliUrlParser.java | 63 ++++++ .../jdbc/internal/parser/JtdsUrlParser.java | 89 +++++++++ .../jdbc/internal/parser/MssqlUrlParser.java | 173 +++++++++++++++++ .../jdbc/internal/parser/OracleUrlParser.java | 183 ++++++++++++++++++ 7 files changed, 735 insertions(+) create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DataDirectUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/Db2UrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixDirectUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixSqliUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MssqlUrlParser.java create mode 100644 instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OracleUrlParser.java diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DataDirectUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DataDirectUrlParser.java new file mode 100644 index 000000000000..cf968e27d45d --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/DataDirectUrlParser.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parser for DataDirect and TIBCO Software JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • datadirect:sqlserver://host:1433;databaseName=db + *
  • datadirect:oracle://host:1521;SID=orcl + *
  • datadirect:mysql://host:3306/db + *
  • datadirect:postgresql://host:5432/db + *
  • datadirect:db2://host:50000/db + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class DataDirectUrlParser implements JdbcUrlParser { + + // copied from DbAttributes.DbSystemNameValues + private static final String MICROSOFT_SQL_SERVER = "microsoft.sql_server"; + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String ORACLE_DB = "oracle.db"; + // copied from DbAttributes.DbSystemNameValues + private static final String MYSQL = "mysql"; + // copied from DbAttributes.DbSystemNameValues + private static final String POSTGRESQL = "postgresql"; + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String IBM_DB2 = "ibm.db2"; + + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String MSSQL = "mssql"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String ORACLE = "oracle"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String DB2 = "db2"; + + // DataDirect subtypes mapped to stable db.system.name values + private static final String SUBTYPE_SQLSERVER = "sqlserver"; + private static final String SUBTYPE_ORACLE = "oracle"; + private static final String SUBTYPE_MYSQL = "mysql"; + private static final String SUBTYPE_POSTGRESQL = "postgresql"; + private static final String SUBTYPE_DB2 = "db2"; + + private static final Map SUBTYPE_TO_SYSTEM = buildSubtypeToSystem(); + + private static Map buildSubtypeToSystem() { + Map map = new HashMap<>(5); + map.put(SUBTYPE_SQLSERVER, MICROSOFT_SQL_SERVER); + map.put(SUBTYPE_ORACLE, ORACLE_DB); + map.put(SUBTYPE_MYSQL, MYSQL); + map.put(SUBTYPE_POSTGRESQL, POSTGRESQL); + map.put(SUBTYPE_DB2, IBM_DB2); + return map; + } + + private static final Map SUBTYPE_TO_OLD_SYSTEM = buildSubtypeToOldSystem(); + + private static Map buildSubtypeToOldSystem() { + Map map = new HashMap<>(3); + map.put(SUBTYPE_SQLSERVER, MSSQL); + map.put(SUBTYPE_ORACLE, ORACLE); + map.put(SUBTYPE_DB2, DB2); + return map; + } + + public static final DataDirectUrlParser INSTANCE = new DataDirectUrlParser(); + + private DataDirectUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.applyDataSourceProperties(); + + int typeEndIndex = jdbcUrl.indexOf(':'); + int subtypeEndIndex = jdbcUrl.indexOf(':', typeEndIndex + 1); + + if (subtypeEndIndex == -1) { + return; + } + + String subtype = jdbcUrl.substring(typeEndIndex + 1, subtypeEndIndex); + + // Determine the actual database system based on the subtype + String system = SUBTYPE_TO_SYSTEM.get(subtype); + if (system != null) { + ctx.system(system); + String oldSystem = SUBTYPE_TO_OLD_SYSTEM.get(subtype); + if (oldSystem != null) { + ctx.oldSemconvSystem(oldSystem); + } + } + + ctx.subtype(subtype); + + ctx.parseUrl(jdbcUrl); + + // DataDirect/Tibco uses DatabaseName in semicolon-delimited params + // URL is lowercased by JdbcConnectionUrlParser, so check lowercase param name + ctx.applyCommonParams(jdbcUrl, ";", ";"); + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/Db2UrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/Db2UrlParser.java new file mode 100644 index 000000000000..5ba2a77daaca --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/Db2UrlParser.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for IBM DB2 and AS400 JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • db2://host:50000/db + *
  • as400://host:50000/db + *
  • db2://host:50000/db:user=dbuser;password=pass; + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class Db2UrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String IBM_DB2 = "ibm.db2"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String DB2 = "db2"; + + private static final int DEFAULT_PORT = 50000; + + public static final Db2UrlParser INSTANCE = new Db2UrlParser(); + + private Db2UrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(IBM_DB2); + ctx.oldSemconvSystem(DB2); + ctx.port(DEFAULT_PORT); + + ctx.applyDataSourceProperties(); + + // DB2/AS400 uses colon to separate URL from params, semicolons within params + if (jdbcUrl.contains("=")) { + int paramLoc = jdbcUrl.lastIndexOf(":"); + if (paramLoc >= 0) { + // Prepend semicolon delimiter so extractParams can find and parse params + ctx.applyCommonParams(";" + jdbcUrl.substring(paramLoc + 1), ";", ";"); + ctx.parseUrl(jdbcUrl.substring(0, paramLoc)); + } else { + ctx.parseUrl(jdbcUrl); + } + } else { + ctx.parseUrl(jdbcUrl); + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixDirectUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixDirectUrlParser.java new file mode 100644 index 000000000000..07145d0b47e6 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixDirectUrlParser.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.indexOfAny; + +/** + * Parser for IBM Informix Direct JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • informix-direct://dbname + *
  • informix-direct://dbname:INFORMIXSERVER=server + *
  • informix-direct://dbname;user=informix;password=pass + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class InformixDirectUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String IBM_INFORMIX = "ibm.informix"; + private static final String OLD_SYSTEM = "informix-direct"; + + public static final InformixDirectUrlParser INSTANCE = new InformixDirectUrlParser(); + + private InformixDirectUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(IBM_INFORMIX); + ctx.oldSemconvSystem(OLD_SYSTEM); + + ctx.applyUserProperty(); + + // Extract user/db from semicolon-delimited URL params + ctx.applyCommonParams(jdbcUrl, ";", ";"); + + // For direct connections, extract just the database name (no host/port) + // URL format: informix-direct://dbname:... or informix-direct://dbname;... + int protoIndex = jdbcUrl.indexOf("://"); + if (protoIndex >= 0) { + String remainder = jdbcUrl.substring(protoIndex + 3); + int endIndex = indexOfAny(remainder, ':', ';'); + String databaseName = endIndex >= 0 ? remainder.substring(0, endIndex) : remainder; + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixSqliUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixSqliUrlParser.java new file mode 100644 index 000000000000..70e6eb4828c5 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/InformixSqliUrlParser.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +/** + * Parser for IBM Informix SQLI JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • informix-sqli://host:9088/db:INFORMIXSERVER=server + *
  • informix-sqli://host/db + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class InformixSqliUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String IBM_INFORMIX = "ibm.informix"; + private static final String OLD_SYSTEM = "informix-sqli"; + + private static final int DEFAULT_PORT = 9088; + + public static final InformixSqliUrlParser INSTANCE = new InformixSqliUrlParser(); + + private InformixSqliUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(IBM_INFORMIX); + ctx.oldSemconvSystem(OLD_SYSTEM); + ctx.port(DEFAULT_PORT); + + ctx.applyDataSourceProperties(); + + // Parse URL for host/port (parseUrl also extracts path as databaseName, but Informix uses + // colon-separated params in the path, so we override databaseName below) + ctx.parseUrl(jdbcUrl); + + // Override database name: Informix paths use format /db:INFORMIXSERVER=server + // Strip the colon-delimited params that parseUrl would have included + int hostIndex = jdbcUrl.indexOf("://"); + if (hostIndex != -1) { + int dbNameStartIndex = jdbcUrl.indexOf('/', hostIndex + 3); + if (dbNameStartIndex != -1) { + int dbNameEndIndex = jdbcUrl.indexOf(':', dbNameStartIndex); + if (dbNameEndIndex == -1) { + dbNameEndIndex = jdbcUrl.length(); + } + String databaseName = jdbcUrl.substring(dbNameStartIndex + 1, dbNameEndIndex); + if (!databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + } + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java new file mode 100644 index 000000000000..eb5c4e3959ed --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.splitQuery; + +import java.util.Map; + +/** + * Parser for jTDS SQL Server JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • jtds:sqlserver://host:1433/db + *
  • jtds:sqlserver://host/db;instance=SQLEXPRESS + *
  • jtds:sqlserver://host;databaseName=db + *
+ * + *

See http://jtds.sourceforge.net/faq.html#urlFormat + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class JtdsUrlParser implements JdbcUrlParser { + + public static final JtdsUrlParser INSTANCE = new JtdsUrlParser(); + + // copied from DbAttributes.DbSystemNameValues + private static final String MICROSOFT_SQL_SERVER = "microsoft.sql_server"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String MSSQL = "mssql"; + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 1433; + + private JtdsUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(MICROSOFT_SQL_SERVER); + ctx.oldSemconvSystem(MSSQL); + ctx.host(DEFAULT_HOST); + ctx.port(DEFAULT_PORT); + + ctx.subtype("sqlserver"); + + // Use ParseContext.parseUrl() to handle URL structure parsing (user, host, port, path) + // Note: parseUrl() sets URL path to ctx.name, but for jTDS the path is the database + ctx.parseUrl(jdbcUrl); + + // Handle jTDS/SQL Server-specific parameters + // For jTDS, URL path (already in ctx.name from parseUrl) represents database name + // Extract instance and database parameters + String[] split = jdbcUrl.split(";", 2); + String instanceName = null; + if (split.length > 1) { + Map urlParams = splitQuery(split[1], ";"); + + // Extract instance name from parameters + if (urlParams.containsKey("instance")) { + instanceName = urlParams.get("instance"); + } + + // If no path, use databasename param as fallback for database + if (ctx.databaseName() == null && urlParams.containsKey("databasename")) { + ctx.databaseName(urlParams.get("databasename")); + } + } + + // DataSource properties applied last — SQL Server driver gives DataSource precedence over URL + ctx.applyDataSourceProperties(); + + // Set namespace with instance formatting (mirrors MssqlUrlParser behavior) + if (instanceName != null && !instanceName.isEmpty()) { + // Preserve old behavior: dbName is the instance name (not the database name) + ctx.dbName(instanceName); + if (ctx.databaseName() != null && !ctx.databaseName().isEmpty()) { + ctx.namespace(instanceName + "|" + ctx.databaseName()); + } else { + ctx.namespace(instanceName); + } + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MssqlUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MssqlUrlParser.java new file mode 100644 index 000000000000..2c4718d7e00a --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/MssqlUrlParser.java @@ -0,0 +1,173 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.HostPort; + +/** + * Parser for Microsoft SQL Server JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • sqlserver://host:1433;databaseName=db + *
  • sqlserver://host\instance + *
  • sqlserver://host:1433/db + *
  • microsoft:sqlserver://host:1433 + *
  • sqlserver://[::1]:1433 (IPv6) + *
  • sqlserver://[::1]\instance (IPv6 with instance) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class MssqlUrlParser implements JdbcUrlParser { + + // copied from DbAttributes.DbSystemNameValues + private static final String MICROSOFT_SQL_SERVER = "microsoft.sql_server"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String MSSQL = "mssql"; + + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 1433; + + public static final MssqlUrlParser INSTANCE = new MssqlUrlParser(); + + private MssqlUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(MICROSOFT_SQL_SERVER); + ctx.oldSemconvSystem(MSSQL); + ctx.host(DEFAULT_HOST); + ctx.port(DEFAULT_PORT); + + // Extract subtype from URL like microsoft:sqlserver://... + String subtype = UrlParsingUtils.extractSubtype(jdbcUrl); + if (subtype != null) { + ctx.subtype(subtype); + } + + // Layer 3: URL params (SQL Server-specific: servername) + ctx.applyCommonParams(jdbcUrl, ";", ";"); + + // Layer 4: Parse URL structure (host:port/path) + String instanceName = parseUrlWithInstance(jdbcUrl, ctx); + + // DataSource properties applied last — SQL Server driver gives DataSource precedence over URL + ctx.applyDataSourceProperties(); + + // Namespace depends on the effective databaseName, so derive it after DataSource overrides. + setNamespace(ctx, instanceName); + } + + /** + * Parse URL with SQL Server-specific backslash instance handling. + * + *

Uses the current ctx.host() value (from params/DataSource properties) as a fallback when the + * URL doesn't contain a host part. + * + * @param jdbcUrl the JDBC URL + * @param ctx the parse context to update + */ + private static String parseUrlWithInstance(String jdbcUrl, ParseContext ctx) { + // Capture current host as fallback (could be from defaults, DataSource properties, or URL + // params) + String fallbackHost = ctx.host() != null ? ctx.host() : ""; + // Split off semicolon-delimited parameters + String urlPart = jdbcUrl.split(";", 2)[0]; + + int hostIndex = urlPart.indexOf("://"); + String serverName; + + if (hostIndex <= 0) { + // No URL host - use fallback + if (fallbackHost.isEmpty()) { + return null; + } + serverName = fallbackHost; + } else { + // URL host takes precedence over fallback + String urlServerName = urlPart.substring(hostIndex + 3); + serverName = urlServerName.isEmpty() ? fallbackHost : urlServerName; + if (serverName.isEmpty()) { + return null; + } + } + + // Extract database path from URL (before handling instance) + // Fallback only: ;databaseName= param (from applyCommonParams) takes precedence over URL path. + // The MSSQL JDBC URL grammar does not include a path component for database name — it uses + // ;databaseName= as a semicolon-delimited property instead. + // See https://learn.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + int pathLoc = serverName.indexOf("/"); + if (pathLoc > 0) { + String databaseName = serverName.substring(pathLoc + 1); + if (ctx.databaseName() == null && !databaseName.isEmpty()) { + ctx.databaseName(databaseName); + } + serverName = serverName.substring(0, pathLoc); + } + + String instanceName = null; + + // Handle IPv6 addresses and extract host:port + HostPort hostPort = UrlParsingUtils.extractHostPort(serverName); + serverName = hostPort.host(); + if (hostPort.port() != null) { + ctx.port(hostPort.port()); + } + + // SQL Server-specific: Extract backslash-separated instance name (host\instance) + int instanceLoc = serverName.indexOf("\\"); + if (instanceLoc > 0) { + if (hostPort.ipv6Address() != null) { + int closingBracket = serverName.lastIndexOf(']'); + int instanceEnd = closingBracket > instanceLoc ? closingBracket : serverName.length(); + instanceName = serverName.substring(instanceLoc + 1, instanceEnd); + serverName = "[" + hostPort.ipv6Address() + "]"; + } else { + instanceName = serverName.substring(instanceLoc + 1); + serverName = serverName.substring(0, instanceLoc); + } + } + + if (!serverName.isEmpty()) { + ctx.host(serverName); + } + + return instanceName; + } + + /** + * Sets the namespace after parsing completes. + * + *

For SQL Server, namespace format is: + * + *

    + *
  • {@code instance|database} when both named instance and database are present + *
  • {@code instance} when only instance is present (with or without explicit DatabaseName + * parameter) + *
+ */ + @SuppressWarnings("deprecation") // dbName is deprecated, to be removed in 3.0 + private static void setNamespace(ParseContext ctx, String instanceName) { + String database = ctx.databaseName(); + + // When we have an instance name + if (instanceName != null && !instanceName.isEmpty()) { + // Preserve old behavior: dbName is the instance name (not the database name) + ctx.dbName(instanceName); + // If there's a non-empty database that's different from instance: format as instance|database + if (database != null && !database.isEmpty() && !database.equals(instanceName)) { + ctx.namespace(instanceName + "|" + database); + } else { + ctx.namespace(instanceName); + } + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OracleUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OracleUrlParser.java new file mode 100644 index 000000000000..f8306368ed48 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/OracleUrlParser.java @@ -0,0 +1,183 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal.parser; + +import static io.opentelemetry.instrumentation.jdbc.internal.parser.UrlParsingUtils.parsePort; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for Oracle JDBC URLs. + * + *

Sample URLs: + * + *

    + *
  • oracle:thin:@host:1521:orcl + *
  • oracle:thin:@host:1521/service + *
  • oracle:thin:user/pass@host/service + *
  • oracle:thin:@//host:1521/service + *
  • oracle:thin:@ldap://host:389/cn=OracleContext + *
  • oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=host)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=service))) + *
+ * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@SuppressWarnings("deprecation") // supporting old semconv until 3.0 +public final class OracleUrlParser implements JdbcUrlParser { + + // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues + private static final String ORACLE_DB = "oracle.db"; + // copied from DbIncubatingAttributes.DbSystemIncubatingValues + private static final String ORACLE = "oracle"; + + private static final int DEFAULT_PORT = 1521; + + // LIMITATION: Simple regex matching across entire DESCRIPTION may extract values from + // non-primary ADDRESS blocks when the first block has incomplete specifications. + private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("@\\s*\\(\\s*description"); + private static final Pattern HOST_PATTERN = + Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); + private static final Pattern PORT_PATTERN = + Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); + private static final Pattern SERVICE_NAME_PATTERN = + Pattern.compile("\\(\\s*service_name\\s*=\\s*([^ )]+)\\s*\\)"); + + public static final OracleUrlParser INSTANCE = new OracleUrlParser(); + + private OracleUrlParser() {} + + @Override + public void parse(String jdbcUrl, ParseContext ctx) { + ctx.system(ORACLE_DB); + ctx.oldSemconvSystem(ORACLE); + ctx.port(DEFAULT_PORT); + + ctx.applyDataSourceProperties(); + + // Parse the URL + // URL format: oracle:: + int subtypeStart = "oracle:".length(); + int typeEndIndex = jdbcUrl.indexOf(":", subtypeStart); + String subtype = jdbcUrl.substring(subtypeStart, typeEndIndex); + String remainder = jdbcUrl.substring(typeEndIndex + 1); + + if (remainder.contains("@")) { + parseAtFormat(remainder, ctx); + } else { + parseConnectInfo(remainder, ctx); + } + + ctx.subtype(subtype); + } + + private static void parseAtFormat(String jdbcUrl, ParseContext ctx) { + if (DESCRIPTION_PATTERN.matcher(jdbcUrl).find()) { + parseDescriptionFormat(jdbcUrl, ctx); + return; + } + + String[] atSplit = jdbcUrl.split("@", 2); + + // Check for user info before @ + int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + ctx.user(atSplit[0].substring(0, userInfoLoc)); + } + + String connectInfo = atSplit[1]; + int hostStart; + if (connectInfo.startsWith("//")) { + hostStart = "//".length(); + } else if (connectInfo.startsWith("ldap://")) { + hostStart = "ldap://".length(); + } else { + hostStart = 0; + } + + parseConnectInfo(connectInfo.substring(hostStart), ctx); + } + + /** + * Parse Oracle DESCRIPTION format URLs. + * + *

LIMITATION: When multiple ADDRESS blocks are present in the DESCRIPTION, this parser + * uses simple regex matching that may extract values from non-primary address blocks. For + * example, if the first ADDRESS block omits the port but a second ADDRESS block includes it, this + * parser will incorrectly use the port from the second block. This is a known limitation that + * affects multi-address configurations with incomplete first address specifications. + */ + private static void parseDescriptionFormat(String jdbcUrl, ParseContext ctx) { + String[] atSplit = jdbcUrl.split("@", 2); + + int userInfoLoc = atSplit[0].indexOf("/"); + if (userInfoLoc > 0) { + ctx.user(atSplit[0].substring(0, userInfoLoc)); + } + + Matcher hostMatcher = HOST_PATTERN.matcher(atSplit[1]); + if (hostMatcher.find()) { + ctx.host(hostMatcher.group(1)); + } + + Matcher portMatcher = PORT_PATTERN.matcher(atSplit[1]); + if (portMatcher.find()) { + ctx.port(Integer.parseInt(portMatcher.group(1))); + } + + Matcher instanceMatcher = SERVICE_NAME_PATTERN.matcher(atSplit[1]); + if (instanceMatcher.find()) { + ctx.databaseName(instanceMatcher.group(1)); + } + } + + private static void parseConnectInfo(String jdbcUrl, ParseContext ctx) { + int hostEnd = jdbcUrl.indexOf(":"); + int instanceLoc = jdbcUrl.indexOf("/"); + + // Case: no colon - just host, host/instance, or instance + if (hostEnd <= 0) { + if (instanceLoc > 0) { + // host/instance (no port - keep default) + ctx.host(jdbcUrl.substring(0, instanceLoc)); + ctx.databaseName(jdbcUrl.substring(instanceLoc + 1)); + } else if (!jdbcUrl.isEmpty()) { + // Just instance name - keep default host/port + ctx.databaseName(jdbcUrl); + } + return; + } + + // From here: hostEnd > 0, so we have host:something + ctx.host(jdbcUrl.substring(0, hostEnd)); + + int afterHostEnd = jdbcUrl.indexOf(":", hostEnd + 1); + if (afterHostEnd > 0) { + // host:port:instance + ctx.port(parsePort(jdbcUrl.substring(hostEnd + 1, afterHostEnd))); + ctx.databaseName(jdbcUrl.substring(afterHostEnd + 1)); + return; + } + + if (instanceLoc > 0) { + // host:port/instance + ctx.port(parsePort(jdbcUrl.substring(hostEnd + 1, instanceLoc))); + ctx.databaseName(jdbcUrl.substring(instanceLoc + 1)); + return; + } + + // host:portOrInstance - could be port or instance name + String portOrInstance = jdbcUrl.substring(hostEnd + 1); + Integer parsedPort = parsePort(portOrInstance); + if (parsedPort != null) { + ctx.port(parsedPort); + } else { + // It's an instance name, not a port - keep default port + ctx.databaseName(portOrInstance); + } + } +} From edbb6c98e7d8173c58bce5933fd7e76773bdf52d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 2 Apr 2026 08:30:00 -0700 Subject: [PATCH 05/12] Switch JDBC URL parsing to parser framework --- .../internal/JdbcConnectionUrlParser.java | 1247 ++--------------- .../internal/JdbcConnectionUrlParserTest.java | 396 ++++-- 2 files changed, 398 insertions(+), 1245 deletions(-) diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java index 9c0f93efa6fe..5cd47a2a1a9d 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java @@ -6,1221 +6,160 @@ package io.opentelemetry.instrumentation.jdbc.internal; import static io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo.DEFAULT; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyMap; import static java.util.logging.Level.FINE; -import static java.util.regex.Pattern.CASE_INSENSITIVE; -import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.util.Collections; +import io.opentelemetry.instrumentation.jdbc.internal.parser.ClickhouseUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.DataDirectUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.Db2UrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.DerbyUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.GenericUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.H2UrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.HsqlUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.InformixDirectUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.InformixSqliUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.JdbcUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.JtdsUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.LindormUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.MssqlUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.MysqlUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.OceanbaseUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.OracleUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.ParseContext; +import io.opentelemetry.instrumentation.jdbc.internal.parser.PolardbUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.PostgresqlUrlParser; +import io.opentelemetry.instrumentation.jdbc.internal.parser.SapUrlParser; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** - * Structured as an enum instead of a class hierarchy to allow iterating through the parsers - * automatically without having to maintain a separate list of parsers. + * Parses JDBC connection URLs to extract database connection information. * *

This class is internal and is hence not for public use. Its APIs are unstable and can change * at any time. */ -public enum JdbcConnectionUrlParser { - GENERIC_URL_LIKE() { - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - try { - // Attempt generic parsing - URI uri = new URI(jdbcUrl); +public final class JdbcConnectionUrlParser { - populateStandardProperties(builder, splitQuery(uri.getQuery(), "&")); - - String user = uri.getUserInfo(); - if (user != null) { - int colonIndex = user.indexOf(':'); - if (colonIndex != -1) { - user = user.substring(0, colonIndex); - } - if (!user.isEmpty()) { - builder.user(user); - } - } - - String path = uri.getPath(); - if (path == null) { - path = ""; - } - if (path.startsWith("/")) { - path = path.substring(1); - } - if (!path.isEmpty()) { - builder.name(path); - } - if (uri.getHost() != null) { - builder.host(uri.getHost()); - } - if (uri.getPort() > 0) { - builder.port(uri.getPort()); - } - } catch (Exception e) { - logger.log(FINE, e.getMessage(), e); - } - return builder; - } - }, - - // see http://jtds.sourceforge.net/faq.html#urlFormat - JTDS_URL_LIKE() { - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String serverName = ""; - - int hostIndex = jdbcUrl.indexOf("jtds:sqlserver://"); - if (hostIndex < 0) { - return builder; - } - - String[] split = jdbcUrl.split(";", 2); - - String urlServerName = split[0].substring(hostIndex + 17); - if (!urlServerName.isEmpty()) { - serverName = urlServerName; - } - - int databaseLoc = serverName.indexOf("/"); - if (databaseLoc > 1) { - builder.name(serverName.substring(databaseLoc + 1)); - serverName = serverName.substring(0, databaseLoc); - } - - if (split.length > 1) { - Map props = splitQuery(split[1], ";"); - populateStandardProperties(builder, props); - if (props.containsKey("instance")) { - builder.name(props.get("instance")); - } - } - - int portLoc = serverName.indexOf(":"); - if (portLoc > 1) { - builder.port(Integer.parseInt(serverName.substring(portLoc + 1))); - serverName = serverName.substring(0, portLoc); - } - - if (!serverName.isEmpty()) { - builder.host(serverName); - } - - return builder; - } - }, - - MODIFIED_URL_LIKE() { - // Source: Regular Expressions Cookbook 2nd edition - 8.17. - // Matches Standard, Mixed or Compressed notation in a wider body of text - private final Pattern ipv6 = - Pattern.compile( - // Non Compressed - "(?:(?:(?:[A-F0-9]{1,4}:){6}" - // Compressed with at most 6 colons - + "|(?=(?:[A-F0-9]{0,4}:){0,6}" - // and 4 bytes and anchored - + "(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?![:.\\w]))" - // and at most 1 double colon - + "(([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:)" - // Compressed with 7 colons and 5 numbers - + "|::(?:[A-F0-9]{1,4}:){5})" - // 255.255.255. - + "(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}" - // 255 - + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" - // Standard - + "|(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}" - // Compressed with at most 7 colons and anchored - + "|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}(?![:.\\w]))" - // and at most 1 double colon - + "(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)" - // Compressed with 8 colons - + "|(?:[A-F0-9]{1,4}:){7}:|:(:[A-F0-9]{1,4}){7})(?![:.\\w])", - CASE_INSENSITIVE); - - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String serverName = ""; - Integer port = null; - String name = null; - - int hostIndex = jdbcUrl.indexOf("://"); - - if (hostIndex <= 0) { - return builder; - } - - String type = jdbcUrl.substring(0, hostIndex); - - String[] split; - if (type.equals("db2") || type.equals("as400")) { - if (jdbcUrl.contains("=")) { - int paramLoc = jdbcUrl.lastIndexOf(":"); - split = new String[] {jdbcUrl.substring(0, paramLoc), jdbcUrl.substring(paramLoc + 1)}; - } else { - split = new String[] {jdbcUrl}; - } - } else { - split = jdbcUrl.split(";", 2); - } - - if (split.length > 1) { - Map props = splitQuery(split[1], ";"); - populateStandardProperties(builder, props); - if (props.containsKey("servername")) { - serverName = props.get("servername"); - } - } - - String urlServerName = split[0].substring(hostIndex + 3); - if (!urlServerName.isEmpty()) { - serverName = urlServerName; - } - - int instanceLoc = serverName.indexOf("/"); - if (instanceLoc > 1) { - name = serverName.substring(instanceLoc + 1); - serverName = serverName.substring(0, instanceLoc); - } - - Matcher ipv6Matcher = ipv6.matcher(serverName); - boolean isIpv6 = ipv6Matcher.find(); - - int portLoc = -1; - if (isIpv6) { - if (serverName.startsWith("[")) { - portLoc = serverName.indexOf("]:") + 1; - } else { - serverName = "[" + serverName + "]"; - } - } else { - portLoc = serverName.indexOf(":"); - } - - if (portLoc > 1) { - port = Integer.parseInt(serverName.substring(portLoc + 1)); - serverName = serverName.substring(0, portLoc); - } - - instanceLoc = serverName.indexOf("\\"); - if (instanceLoc > 1) { - if (isIpv6) { - name = serverName.substring(instanceLoc + 1, serverName.lastIndexOf(']')); - serverName = "[" + ipv6Matcher.group(0) + "]"; - } else { - name = serverName.substring(instanceLoc + 1); - serverName = serverName.substring(0, instanceLoc); - } - } - - if (name != null) { - builder.name(name); - } - - if (!serverName.isEmpty()) { - builder.host(serverName); - } - - if (port != null) { - builder.port(port); - } - - return builder; - } - }, - - POSTGRES("postgresql") { - private static final String DEFAULT_HOST = "localhost"; - private static final int DEFAULT_PORT = 5432; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); - } - }, - - MYSQL("mysql", "mariadb") { - private static final String DEFAULT_HOST = "localhost"; - private static final int DEFAULT_PORT = 3306; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - - int protoLoc = jdbcUrl.indexOf("://"); - int typeEndLoc = jdbcUrl.indexOf(':'); - if (typeEndLoc < protoLoc) { - return MARIA_SUBPROTO - .doParse(jdbcUrl.substring(protoLoc + 3), builder) - .subtype(jdbcUrl.substring(typeEndLoc + 1, protoLoc)); - } - if (protoLoc > 0) { - return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); - } - - int hostEndLoc; - int portLoc = jdbcUrl.indexOf(":", typeEndLoc + 1); - int dbLoc = jdbcUrl.indexOf("/", typeEndLoc); - int paramLoc = jdbcUrl.indexOf("?", dbLoc); - - if (paramLoc > 0) { - populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); - builder.name(jdbcUrl.substring(dbLoc + 1, paramLoc)); - } else { - builder.name(jdbcUrl.substring(dbLoc + 1)); - } - - if (portLoc > 0) { - hostEndLoc = portLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, dbLoc))); - } catch (NumberFormatException e) { - logger.log(FINE, e.getMessage(), e); - } - } else { - hostEndLoc = dbLoc; - } - - builder.host(jdbcUrl.substring(typeEndLoc + 1, hostEndLoc)); - - return builder; - } - }, - - MARIA_SUBPROTO() { - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - int hostEndLoc; - int clusterSepLoc = jdbcUrl.indexOf(","); - int ipv6End = jdbcUrl.startsWith("[") ? jdbcUrl.indexOf("]") : -1; - int portLoc = jdbcUrl.indexOf(":", Math.max(0, ipv6End)); - portLoc = clusterSepLoc != -1 && clusterSepLoc < portLoc ? -1 : portLoc; - int dbLoc = jdbcUrl.indexOf("/", Math.max(portLoc, clusterSepLoc)); - - int paramLoc = dbLoc != -1 ? jdbcUrl.indexOf("?", dbLoc) : -1; - - if (paramLoc > 0) { - populateStandardProperties(builder, splitQuery(jdbcUrl.substring(paramLoc + 1), "&")); - builder.name(jdbcUrl.substring(dbLoc + 1, paramLoc)); - } else if (dbLoc != -1) { - builder.name(jdbcUrl.substring(dbLoc + 1)); - } - - if (jdbcUrl.startsWith("address=")) { - return MARIA_ADDRESS.doParse(jdbcUrl, builder); - } - - dbLoc = dbLoc != -1 ? dbLoc : jdbcUrl.length(); - if (portLoc > 0) { - hostEndLoc = portLoc; - int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, portEndLoc))); - } catch (NumberFormatException e) { - logger.log(FINE, e.getMessage(), e); - } - } else { - hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; - } - - if (ipv6End > 0) { - builder.host(jdbcUrl.substring(1, ipv6End)); - } else { - builder.host(jdbcUrl.substring(0, hostEndLoc)); - } - return builder; - } - }, - - MARIA_ADDRESS() { - private final Pattern hostPattern = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); - private final Pattern portPattern = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); - private final Pattern userPattern = Pattern.compile("\\(\\s*user\\s*=\\s*([^ )]+)\\s*\\)"); - - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - int addressEnd = jdbcUrl.indexOf(",address="); - if (addressEnd > 0) { - jdbcUrl = jdbcUrl.substring(0, addressEnd); - } - Matcher hostMatcher = hostPattern.matcher(jdbcUrl); - if (hostMatcher.find()) { - builder.host(hostMatcher.group(1)); - } - - Matcher portMatcher = portPattern.matcher(jdbcUrl); - if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); - } - - Matcher userMatcher = userPattern.matcher(jdbcUrl); - if (userMatcher.find()) { - builder.user(userMatcher.group(1)); - } - - return builder; - } - }, - - SAP("sap") { - private static final String DEFAULT_HOST = "localhost"; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); - } - }, - - MSSQLSERVER("jtds", "microsoft", "sqlserver") { - private static final String DEFAULT_HOST = "localhost"; - private static final int DEFAULT_PORT = 1433; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - - int protoLoc = jdbcUrl.indexOf("://"); - int typeEndLoc = jdbcUrl.indexOf(':'); - if (protoLoc > typeEndLoc) { - String subtype = jdbcUrl.substring(typeEndLoc + 1, protoLoc); - builder.subtype(subtype); - } - - if (jdbcUrl.startsWith("jtds:")) { - return JTDS_URL_LIKE.doParse(jdbcUrl, builder); - } - - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); - } - }, - - DB2("db2", "as400") { - private static final int DEFAULT_PORT = 50000; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); - } - }, - - ORACLE("oracle") { - private static final int DEFAULT_PORT = 1521; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - int typeEndIndex = jdbcUrl.indexOf(":", "oracle:".length()); - String subtype = jdbcUrl.substring("oracle:".length(), typeEndIndex); - jdbcUrl = jdbcUrl.substring(typeEndIndex + 1); - - builder.subtype(subtype); - DbInfo dbInfo = builder.build(); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - - if (jdbcUrl.contains("@")) { - return ORACLE_AT.doParse(jdbcUrl, builder); - } else { - return ORACLE_CONNECT_INFO.doParse(jdbcUrl, builder); - } - } - }, - - ORACLE_CONNECT_INFO() { - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - - String host; - Integer port; - String instance; - - int hostEnd = jdbcUrl.indexOf(":"); - int instanceLoc = jdbcUrl.indexOf("/"); - if (hostEnd > 0) { - host = jdbcUrl.substring(0, hostEnd); - int afterHostEnd = jdbcUrl.indexOf(":", hostEnd + 1); - if (afterHostEnd > 0) { - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, afterHostEnd)); - instance = jdbcUrl.substring(afterHostEnd + 1); - } else { - if (instanceLoc > 0) { - instance = jdbcUrl.substring(instanceLoc + 1); - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, instanceLoc)); - } else { - String portOrInstance = jdbcUrl.substring(hostEnd + 1); - Integer parsedPort = null; - try { - parsedPort = Integer.parseInt(portOrInstance); - } catch (NumberFormatException e) { - logger.log(FINE, e.getMessage(), e); - } - if (parsedPort == null) { - port = null; - instance = portOrInstance; - } else { - port = parsedPort; - instance = null; - } - } - } - } else { - if (instanceLoc > 0) { - host = jdbcUrl.substring(0, instanceLoc); - port = null; - instance = jdbcUrl.substring(instanceLoc + 1); - } else { - if (jdbcUrl.isEmpty()) { - return builder; - } else { - host = null; - port = null; - instance = jdbcUrl; - } - } - } - if (host != null) { - builder.host(host); - } - if (port != null) { - builder.port(port); - } - return builder.name(instance); - } - }, - - ORACLE_AT() { - private final Pattern descriptionPattern = Pattern.compile("@\\s*\\(\\s*description"); - - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - if (descriptionPattern.matcher(jdbcUrl).find()) { - return ORACLE_AT_DESCRIPTION.doParse(jdbcUrl, builder); - } - String user; - - String[] atSplit = jdbcUrl.split("@", 2); - - int userInfoLoc = atSplit[0].indexOf("/"); - if (userInfoLoc > 0) { - user = atSplit[0].substring(0, userInfoLoc); - } else { - user = null; - } - - String connectInfo = atSplit[1]; - int hostStart; - if (connectInfo.startsWith("//")) { - hostStart = "//".length(); - } else if (connectInfo.startsWith("ldap://")) { - hostStart = "ldap://".length(); - } else { - hostStart = 0; - } - if (user != null) { - builder.user(user); - } - return ORACLE_CONNECT_INFO.doParse(connectInfo.substring(hostStart), builder); - } - }, - - /** - * This parser can locate incorrect data if multiple addresses are defined but not everything is - * defined in the first block. It would locate data from subsequent address blocks. - */ - ORACLE_AT_DESCRIPTION() { - private final Pattern hostPattern = Pattern.compile("\\(\\s*host\\s*=\\s*([^ )]+)\\s*\\)"); - private final Pattern portPattern = Pattern.compile("\\(\\s*port\\s*=\\s*([\\d]+)\\s*\\)"); - private final Pattern instancePattern = - Pattern.compile("\\(\\s*service_name\\s*=\\s*([^ )]+)\\s*\\)"); - - @Override - @CanIgnoreReturnValue - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String[] atSplit = jdbcUrl.split("@", 2); - - int userInfoLoc = atSplit[0].indexOf("/"); - if (userInfoLoc > 0) { - builder.user(atSplit[0].substring(0, userInfoLoc)); - } - - Matcher hostMatcher = hostPattern.matcher(atSplit[1]); - if (hostMatcher.find()) { - builder.host(hostMatcher.group(1)); - } - - Matcher portMatcher = portPattern.matcher(atSplit[1]); - if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); - } - - Matcher instanceMatcher = instancePattern.matcher(atSplit[1]); - if (instanceMatcher.find()) { - builder.name(instanceMatcher.group(1)); - } - - return builder; - } - }, - - H2("h2") { - private static final int DEFAULT_PORT = 8082; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String instance; - - String h2Url = jdbcUrl.substring("h2:".length()); - if (h2Url.startsWith("mem:")) { - builder.subtype("mem").host(null).port(null); - int propLoc = h2Url.indexOf(";"); - if (propLoc >= 0) { - instance = h2Url.substring("mem:".length(), propLoc); - } else { - instance = h2Url.substring("mem:".length()); - } - } else if (h2Url.startsWith("file:")) { - builder.subtype("file").host(null).port(null); - int propLoc = h2Url.indexOf(";"); - if (propLoc >= 0) { - instance = h2Url.substring("file:".length(), propLoc); - } else { - instance = h2Url.substring("file:".length()); - } - } else if (h2Url.startsWith("zip:")) { - builder.subtype("zip").host(null).port(null); - int propLoc = h2Url.indexOf(";"); - if (propLoc >= 0) { - instance = h2Url.substring("zip:".length(), propLoc); - } else { - instance = h2Url.substring("zip:".length()); - } - } else if (h2Url.startsWith("tcp:")) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("tcp"); - } else if (h2Url.startsWith("ssl:")) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("ssl"); - } else { - builder.subtype("file").host(null).port(null); - int propLoc = h2Url.indexOf(";"); - if (propLoc >= 0) { - instance = h2Url.substring(0, propLoc); - } else { - instance = h2Url; - } - } - if (!instance.isEmpty()) { - builder.name(instance); - } - return builder; - } - }, - - HSQL("hsqldb") { - private static final String DEFAULT_USER = "SA"; - private static final int DEFAULT_PORT = 9001; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String instance = null; - DbInfo dbInfo = builder.build(); - if (dbInfo.getUser() == null) { - builder.user(DEFAULT_USER); - } - String hsqlUrl = jdbcUrl.substring("hsqldb:".length()); - int proIndex = hsqlUrl.indexOf(";"); - if (proIndex >= 0) { - hsqlUrl = hsqlUrl.substring(0, proIndex); - } else { - int varIndex = hsqlUrl.indexOf("?"); - if (varIndex >= 0) { - hsqlUrl = hsqlUrl.substring(0, varIndex); - } - } - if (hsqlUrl.startsWith("mem:")) { - builder.subtype("mem").host(null).port(null); - instance = hsqlUrl.substring("mem:".length()); - } else if (hsqlUrl.startsWith("file:")) { - builder.subtype("file").host(null).port(null); - instance = hsqlUrl.substring("file:".length()); - } else if (hsqlUrl.startsWith("res:")) { - builder.subtype("res").host(null).port(null); - instance = hsqlUrl.substring("res:".length()); - } else if (hsqlUrl.startsWith("hsql:")) { - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("hsql"); - } else if (hsqlUrl.startsWith("hsqls:")) { - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("hsqls"); - } else if (hsqlUrl.startsWith("http:")) { - if (dbInfo.getPort() == null) { - builder.port(80); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("http"); - } else if (hsqlUrl.startsWith("https:")) { - if (dbInfo.getPort() == null) { - builder.port(443); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder).subtype("https"); - } else { - builder.subtype("mem").host(null).port(null); - instance = hsqlUrl; - } - return builder.name(instance); - } - }, + private static final Logger logger = Logger.getLogger(JdbcConnectionUrlParser.class.getName()); - DERBY("derby") { - private static final String DEFAULT_USER = "APP"; - private static final int DEFAULT_PORT = 1527; + private static final Map TYPE_PARSERS = new HashMap<>(); - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String instance = null; - String host = null; + static { + // PostgreSQL + TYPE_PARSERS.put("postgresql", PostgresqlUrlParser.INSTANCE); - DbInfo dbInfo = builder.build(); - if (dbInfo.getUser() == null) { - builder.user(DEFAULT_USER); - } + // MySQL and MariaDB + TYPE_PARSERS.put("mysql", MysqlUrlParser.INSTANCE); + TYPE_PARSERS.put("mariadb", MysqlUrlParser.INSTANCE); - String derbyUrl = jdbcUrl.substring("derby:".length()); - String[] split = derbyUrl.split(";", 2); + // Microsoft SQL Server + TYPE_PARSERS.put("jtds", JtdsUrlParser.INSTANCE); + TYPE_PARSERS.put("microsoft", MssqlUrlParser.INSTANCE); + TYPE_PARSERS.put("sqlserver", MssqlUrlParser.INSTANCE); - if (split.length > 1) { - populateStandardProperties(builder, splitQuery(split[1], ";")); - } + // Oracle + TYPE_PARSERS.put("oracle", OracleUrlParser.INSTANCE); - String details = split[0]; - if (details.startsWith("memory:")) { - builder.subtype("memory").host(null).port(null); - String urlInstance = details.substring("memory:".length()); - if (!urlInstance.isEmpty()) { - instance = urlInstance; - } - } else if (details.startsWith("directory:")) { - builder.subtype("directory").host(null).port(null); - String urlInstance = details.substring("directory:".length()); - if (!urlInstance.isEmpty()) { - int dbNameStartLocation = urlInstance.lastIndexOf('/'); - if (dbNameStartLocation != -1) { - instance = urlInstance.substring(dbNameStartLocation + 1); - } else { - instance = urlInstance; - } - } - } else if (details.startsWith("classpath:")) { - builder.subtype("classpath").host(null).port(null); - String urlInstance = details.substring("classpath:".length()); - if (!urlInstance.isEmpty()) { - instance = urlInstance; - } - } else if (details.startsWith("jar:")) { - builder.subtype("jar").host(null).port(null); - String urlInstance = details.substring("jar:".length()); - if (!urlInstance.isEmpty()) { - instance = urlInstance; - } - } else if (details.startsWith("//")) { - builder.subtype("network"); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - String url = details.substring("//".length()); - int instanceLoc = url.indexOf("/"); - if (instanceLoc >= 0) { - instance = url.substring(instanceLoc + 1); - int protoLoc = instance.indexOf(":"); - if (protoLoc >= 0) { - instance = instance.substring(protoLoc + 1); - } - url = url.substring(0, instanceLoc); - } - int portLoc = url.indexOf(":"); - if (portLoc > 0) { - host = url.substring(0, portLoc); - builder.port(Integer.parseInt(url.substring(portLoc + 1))); - } else { - host = url; - } - } else { - builder.subtype("directory").host(null).port(null); - if (!details.isEmpty()) { - instance = details; - } - } + // DB2 and AS400 + TYPE_PARSERS.put("db2", Db2UrlParser.INSTANCE); + TYPE_PARSERS.put("as400", Db2UrlParser.INSTANCE); - if (host != null) { - builder.host(host); - } - if (instance != null) { - builder.name(instance); - } - return builder; - } - }, + // H2 + TYPE_PARSERS.put("h2", H2UrlParser.INSTANCE); - DATADIRECT("datadirect", "tibcosoftware") { - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - int typeEndIndex = jdbcUrl.indexOf(':'); - int subtypeEndIndex = jdbcUrl.indexOf(':', typeEndIndex + 1); + // HyperSQL (HSQLDB) + TYPE_PARSERS.put("hsqldb", HsqlUrlParser.INSTANCE); - if (subtypeEndIndex == -1) { - return builder; - } - - String subtype = jdbcUrl.substring(typeEndIndex + 1, subtypeEndIndex); - builder.subtype(subtype); - - if (subtype.equals("sqlserver")) { - builder.system(DbSystemValues.MSSQL); - } else if (subtype.equals("oracle")) { - builder.system(DbSystemValues.ORACLE); - } else if (subtype.equals("mysql")) { - builder.system(DbSystemValues.MYSQL); - } else if (subtype.equals("postgresql")) { - builder.system(DbSystemValues.POSTGRESQL); - } else if (subtype.equals("db2")) { - builder.system(DbSystemValues.DB2); - } + // Apache Derby + TYPE_PARSERS.put("derby", DerbyUrlParser.INSTANCE); - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); - } - }, - INFORMIX_SQLI("informix-sqli") { - private static final int DEFAULT_PORT = 9088; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - builder = MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); + // SAP HANA + TYPE_PARSERS.put("sap", SapUrlParser.INSTANCE); - DbInfo dbInfo = builder.build(); - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } + // DataDirect and TIBCO + TYPE_PARSERS.put("datadirect", DataDirectUrlParser.INSTANCE); + TYPE_PARSERS.put("tibcosoftware", DataDirectUrlParser.INSTANCE); - int hostIndex = jdbcUrl.indexOf("://"); - if (hostIndex == -1) { - return builder; - } + // Informix + TYPE_PARSERS.put("informix-sqli", InformixSqliUrlParser.INSTANCE); + TYPE_PARSERS.put("informix-direct", InformixDirectUrlParser.INSTANCE); - int dbNameStartIndex = jdbcUrl.indexOf('/', hostIndex + 3); - if (dbNameStartIndex == -1) { - return builder; - } - int dbNameEndIndex = jdbcUrl.indexOf(':', dbNameStartIndex); - if (dbNameEndIndex == -1) { - dbNameEndIndex = jdbcUrl.length(); - } - String name = jdbcUrl.substring(dbNameStartIndex + 1, dbNameEndIndex); - if (name.isEmpty()) { - builder.name(null); - } else { - builder.name(name); - } + // ClickHouse + TYPE_PARSERS.put("clickhouse", ClickhouseUrlParser.INSTANCE); - return builder; - } - }, + // OceanBase + TYPE_PARSERS.put("oceanbase", OceanbaseUrlParser.INSTANCE); - INFORMIX_DIRECT("informix-direct") { - private final Pattern pattern = Pattern.compile("://(.*?)(:|;|$)"); + // Lindorm + TYPE_PARSERS.put("lindorm", LindormUrlParser.INSTANCE); - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - builder = MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); - builder.host(null); - builder.port(null); + // PolarDB + TYPE_PARSERS.put("polardb", PolardbUrlParser.INSTANCE); + } - Matcher matcher = pattern.matcher(jdbcUrl); - if (matcher.find()) { - String name = matcher.group(1); - if (!name.isEmpty()) { - builder.name(name); - } - } + private JdbcConnectionUrlParser() {} - return builder; - } - }, - /** - * Driver - * configuration doc mentions that besides clickhouse ch could also - * be used but ClickHouse Connection implementation always returns full prefix - * jdbc:clickhouse: - */ - CLICKHOUSE("clickhouse") { - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String clickhouseUrl = jdbcUrl.substring("clickhouse:".length()); - int typeEndIndex = clickhouseUrl.indexOf("://"); - if (typeEndIndex > 0) { - builder.subtype(clickhouseUrl.substring(0, typeEndIndex)); - } - return GENERIC_URL_LIKE.doParse(clickhouseUrl, builder); - } - }, - /** - * Sample urls: - * - *

    - *
  • jdbc:oceanbase://host:port/dbname - *
  • jdbc:oceanbase:oracle://host:port/dbname - *
- */ - OCEANBASE("oceanbase") { - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - int protoLoc = jdbcUrl.indexOf("://"); - int typeEndLoc = jdbcUrl.indexOf(':'); - if (protoLoc > typeEndLoc) { - String subtype = jdbcUrl.substring(typeEndLoc + 1, protoLoc); - builder.subtype(subtype); - if (subtype.equals(DbSystemValues.ORACLE)) { - builder.system(DbSystemValues.ORACLE); - } - return MODIFIED_URL_LIKE.doParse(jdbcUrl, builder); - } else { - return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); - } - } - }, /** - * Driver - * configuration doc - * - *

Sample urls: + * Parse a JDBC connection URL and extract database connection information. * - *

    - *
  • jdbc:lindorm:table:url=http//server_name:30060/test - *
  • jdbc:lindorm:tsdb:url=http://server_name:8242/test - *
  • jabc:lindorm:search:url=http://server_name:30070/test - *
+ * @param connectionUrl the JDBC connection URL + * @param props optional connection properties + * @return the parsed DbInfo, or DbInfo.DEFAULT if parsing fails */ - LINDORM("lindorm") { - private static final String DEFAULT_HOST = "localhost"; - private static final int DEFAULT_PORT = 30060; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - String lindormUrl = jdbcUrl.substring("lindorm:".length()); - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - - int urlIndex = lindormUrl.indexOf(":url="); - if (urlIndex < 0) { - return builder; - } - builder.subtype(lindormUrl.substring(0, urlIndex)); - String realUrl = lindormUrl.substring(urlIndex + 5); - return GENERIC_URL_LIKE.doParse(realUrl, builder); - } - }, - /** Sample url: jdbc:polardb://server_name:1901/dbname */ - POLARDB("polardb") { - private static final int DEFAULT_PORT = 1521; - private static final String DEFAULT_HOST = "localhost"; - - @Override - DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder) { - DbInfo dbInfo = builder.build(); - if (dbInfo.getHost() == null) { - builder.host(DEFAULT_HOST); - } - if (dbInfo.getPort() == null) { - builder.port(DEFAULT_PORT); - } - return GENERIC_URL_LIKE.doParse(jdbcUrl, builder); - } - }; - - private static final Logger logger = Logger.getLogger(JdbcConnectionUrlParser.class.getName()); - - private static final Map typeParsers = new HashMap<>(); - - static { - for (JdbcConnectionUrlParser parser : JdbcConnectionUrlParser.values()) { - for (String key : parser.typeKeys) { - typeParsers.put(key, parser); - } - } - } - - // Wrapped in unmodifiableList - @SuppressWarnings("ImmutableEnumChecker") - private final List typeKeys; - - JdbcConnectionUrlParser(String... typeKeys) { - this.typeKeys = Collections.unmodifiableList(asList(typeKeys)); - } - - abstract DbInfo.Builder doParse(String jdbcUrl, DbInfo.Builder builder); - public static DbInfo parse(String connectionUrl, Properties props) { if (connectionUrl == null) { return DEFAULT; } + // Make this easier and ignore case. connectionUrl = connectionUrl.toLowerCase(Locale.ROOT); - String jdbcUrl; - if (connectionUrl.startsWith("jdbc:tracing:")) { - // see https://github.com/opentracing-contrib/java-jdbc - jdbcUrl = connectionUrl.substring("jdbc:tracing:".length()); - } else if (connectionUrl.startsWith("jdbc:")) { - jdbcUrl = connectionUrl.substring("jdbc:".length()); - } else if (connectionUrl.startsWith("jdbc-secretsmanager:tracing:")) { - jdbcUrl = connectionUrl.substring("jdbc-secretsmanager:tracing:".length()); - } else if (connectionUrl.startsWith("jdbc-secretsmanager:")) { - jdbcUrl = connectionUrl.substring("jdbc-secretsmanager:".length()); - } else { + String jdbcUrl = stripJdbcPrefix(connectionUrl); + if (jdbcUrl == null) { return DEFAULT; } int typeLoc = jdbcUrl.indexOf(':'); - if (typeLoc < 1) { // Invalid format: `jdbc:` or `jdbc::` return DEFAULT; } String type = jdbcUrl.substring(0, typeLoc); - String system = toDbSystem(type); - DbInfo.Builder parsedProps = DEFAULT.toBuilder().system(system); - populateStandardProperties(parsedProps, props); try { - if (typeParsers.containsKey(type)) { - // Delegate to specific parser - return withUrl(typeParsers.get(type).doParse(jdbcUrl, parsedProps), type); - } - return withUrl(GENERIC_URL_LIKE.doParse(jdbcUrl, parsedProps), type); - } catch (RuntimeException e) { - logger.log(FINE, "Error parsing URL", e); - return parsedProps.build(); - } - } - - private static DbInfo withUrl(DbInfo.Builder builder, String type) { - DbInfo info = builder.build(); - StringBuilder url = new StringBuilder(); - url.append(type); - url.append(':'); - String subtype = info.getSubtype(); - if (subtype != null) { - url.append(subtype); - url.append(':'); - } - String host = info.getHost(); - if (host != null) { - url.append("//"); - url.append(host); - Integer port = info.getPort(); - if (port != null) { - url.append(':'); - url.append(port); - } - } - return builder.shortUrl(url.toString()).build(); - } - - // Source: https://stackoverflow.com/a/13592567 - private static Map splitQuery(String query, String separator) { - if (query == null || query.isEmpty()) { - return emptyMap(); - } - Map queryPairs = new LinkedHashMap<>(); - String[] pairs = query.split(separator); - for (String pair : pairs) { - try { - int idx = pair.indexOf("="); - String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; - if (!queryPairs.containsKey(key)) { - String value = - idx > 0 && pair.length() > idx + 1 - ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") - : null; - queryPairs.put(key, value); - } - } catch (UnsupportedEncodingException e) { - // Ignore. - } - } - return queryPairs; - } - - private static void populateStandardProperties(DbInfo.Builder builder, Map props) { - if (props != null && !props.isEmpty()) { - String user = (String) props.get("user"); - if (user != null && !user.isEmpty()) { - builder.user(user); - } - - if (props.containsKey("databasename")) { - builder.name((String) props.get("databasename")); - } - if (props.containsKey("databaseName")) { - builder.name((String) props.get("databaseName")); - } + JdbcUrlParser parser = TYPE_PARSERS.get(type); - if (props.containsKey("servername")) { - builder.host((String) props.get("servername")); - } - if (props.containsKey("serverName")) { - builder.host((String) props.get("serverName")); - } - - if (props.containsKey("portnumber")) { - String portNumber = (String) props.get("portnumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (NumberFormatException e) { - if (logger.isLoggable(FINE)) { - logger.log(FINE, "Error parsing portnumber property: " + portNumber, e); - } - } + ParseContext ctx = ParseContext.of(type, props); + if (parser == null) { + // Unknown JDBC type: apply all standard DataSource properties and use generic parser + ctx.applyDataSourceProperties(); + GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); + } else { + parser.parse(jdbcUrl, ctx); } - if (props.containsKey("portNumber")) { - String portNumber = (String) props.get("portNumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (NumberFormatException e) { - if (logger.isLoggable(FINE)) { - logger.log(FINE, "Error parsing portNumber property: " + portNumber, e); - } - } - } + return ctx.toDbInfo(); + } catch (RuntimeException e) { + logger.log(FINE, "Error parsing URL", e); + return DEFAULT; } } - // see - // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/database-spans.md - private static String toDbSystem(String type) { - switch (type) { - case "as400": // IBM AS400 Database - case "db2": // IBM Db2 - return DbSystemValues.DB2; - case "derby": // Apache Derby - return DbSystemValues.DERBY; - case "h2": // H2 Database - return DbSystemValues.H2; - case "hsqldb": // Hyper SQL Database - return "hsqldb"; - case "informix-sqli": // IBM Informix - return DbSystemValues.INFORMIX_SQLI; - case "informix-direct": - return DbSystemValues.INFORMIX_DIRECT; - case "mariadb": // MariaDB - return DbSystemValues.MARIADB; - case "mysql": // MySQL - return DbSystemValues.MYSQL; - case "oracle": // Oracle Database - return DbSystemValues.ORACLE; - case "postgresql": // PostgreSQL - return DbSystemValues.POSTGRESQL; - case "jtds": // jTDS - the pure Java JDBC 3.0 driver for Microsoft SQL Server - case "microsoft": - case "sqlserver": // Microsoft SQL Server - return DbSystemValues.MSSQL; - case "sap": // SAP Hana - return DbSystemValues.HANADB; - case "clickhouse": // ClickHouse - return DbSystemValues.CLICKHOUSE; - case "oceanbase": // Oceanbase - return DbSystemValues.OCEANBASE; - case "polardb": // PolarDB - return DbSystemValues.POLARDB; - case "lindorm": // Lindorm - return DbSystemValues.LINDORM; - default: - return DbSystemValues.OTHER_SQL; // Unknown DBMS + private static String stripJdbcPrefix(String connectionUrl) { + if (connectionUrl.startsWith("jdbc:tracing:")) { + // see https://github.com/opentracing-contrib/java-jdbc + return connectionUrl.substring("jdbc:tracing:".length()); + } else if (connectionUrl.startsWith("jdbc:")) { + return connectionUrl.substring("jdbc:".length()); + } else if (connectionUrl.startsWith("jdbc-secretsmanager:tracing:")) { + return connectionUrl.substring("jdbc-secretsmanager:tracing:".length()); + } else if (connectionUrl.startsWith("jdbc-secretsmanager:")) { + return connectionUrl.substring("jdbc-secretsmanager:".length()); } - } - - // copied from DbIncubatingAttributes - private static final class DbSystemValues { - static final String OTHER_SQL = "other_sql"; - static final String MSSQL = "mssql"; - static final String MYSQL = "mysql"; - static final String ORACLE = "oracle"; - static final String DB2 = "db2"; - static final String INFORMIX_SQLI = "informix-sqli"; - static final String INFORMIX_DIRECT = "informix-direct"; - static final String POSTGRESQL = "postgresql"; - static final String HANADB = "hanadb"; - static final String DERBY = "derby"; - static final String MARIADB = "mariadb"; - static final String H2 = "h2"; - static final String CLICKHOUSE = "clickhouse"; - static final String OCEANBASE = "oceanbase"; - static final String POLARDB = "polardb"; - static final String LINDORM = "lindorm"; - - private DbSystemValues() {} + return null; } } diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java index e3b2d027d765..a30584f9c067 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java @@ -8,15 +8,10 @@ import static io.opentelemetry.instrumentation.jdbc.internal.JdbcConnectionUrlParser.parse; import static io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo.DEFAULT; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.CLICKHOUSE; -import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.DB2; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.DERBY; -import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.H2; -import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.HANADB; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.HSQLDB; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.MARIADB; -import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.MSSQL; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.MYSQL; -import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.ORACLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.POSTGRESQL; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -49,6 +44,13 @@ private static Properties stdProps() { return prop; } + private static Properties postgresProps(String user, String currentSchema) { + Properties prop = new Properties(); + prop.setProperty("user", user); + prop.setProperty("currentSchema", currentSchema); + return prop; + } + @ParameterizedTest @ValueSource(strings = {"", "jdbc:", "jdbc::", "bogus:string"}) void testInvalidUrlReturnsDefault(String url) { @@ -231,6 +233,7 @@ private static Stream postgresArguments() { .setUser("stdUserName") .setHost("stdServerName") .setPort(9999) + .setNamespace("stdDatabaseName|stdUserName") .setName("stdDatabaseName") .build(), arg("jdbc:postgresql://pg.host") @@ -245,6 +248,7 @@ private static Stream postgresArguments() { .setUser("pguser") .setHost("pg.host") .setPort(11) + .setNamespace("pgdb|pguser") .setName("pgdb") .build(), arg("jdbc:postgresql://pg.host:11/pgdb?user=pguser&password=PW") @@ -254,6 +258,56 @@ private static Stream postgresArguments() { .setUser("pguser") .setHost("pg.host") .setPort(11) + .setNamespace("pgdb|pguser") + .setName("pgdb") + .build(), + // currentSchema param takes precedence over user for namespace + arg("jdbc:postgresql://pg.host:11/pgdb?user=pguser¤tSchema=myschema") + .setShortUrl("postgresql://pg.host:11") + .setSystem("postgresql") + .setUser("pguser") + .setHost("pg.host") + .setPort(11) + .setNamespace("pgdb|myschema") + .setName("pgdb") + .build(), + // currentSchema without user + arg("jdbc:postgresql://pg.host/pgdb?currentSchema=myschema") + .setShortUrl("postgresql://pg.host:5432") + .setSystem("postgresql") + .setHost("pg.host") + .setPort(5432) + .setNamespace("pgdb|myschema") + .setName("pgdb") + .build(), + // currentSchema from connection properties is used when the URL does not specify it + arg("jdbc:postgresql://pg.host/pgdb") + .setProperties(postgresProps("pguser", "propertyschema")) + .setShortUrl("postgresql://pg.host:5432") + .setSystem("postgresql") + .setUser("pguser") + .setHost("pg.host") + .setPort(5432) + .setNamespace("pgdb|propertyschema") + .setName("pgdb") + .build(), + // currentSchema URL param takes precedence over currentSchema property + arg("jdbc:postgresql://pg.host/pgdb?currentSchema=urlschema") + .setProperties(postgresProps("pguser", "propertyschema")) + .setShortUrl("postgresql://pg.host:5432") + .setSystem("postgresql") + .setUser("pguser") + .setHost("pg.host") + .setPort(5432) + .setNamespace("pgdb|urlschema") + .setName("pgdb") + .build(), + // database only, no schema or user — namespace falls back to database name + arg("jdbc:postgresql://pg.host/pgdb") + .setShortUrl("postgresql://pg.host:5432") + .setSystem("postgresql") + .setHost("pg.host") + .setPort(5432) .setName("pgdb") .build()); } @@ -397,47 +451,63 @@ private static Stream sqlServerArguments() { // https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url arg("jdbc:microsoft:sqlserver://;") .setShortUrl("microsoft:sqlserver://localhost:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("localhost") .setPort(1433) .build(), arg("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9") .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") .setPort(1433) .build(), arg("jdbc:sqlserver://;serverName=2001:0db8:85a3:0000:0000:8a2e:0370:7334") .setShortUrl("sqlserver://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") .setPort(1433) .build(), arg("jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") .setPort(43) .build(), arg("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance") .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") .setPort(1433) .setName("ssinstance") .build(), arg("jdbc:sqlserver://;serverName=[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\ssinstance]:43") .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:43") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") .setPort(43) .setName("ssinstance") .build(), + arg("jdbc:sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]\\ssinstance;databaseName=ssdb") + .setShortUrl("sqlserver://[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]:1433") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setHost("[3ffe:8311:eeee:f70f:0:5eae:10.203.31.9]") + .setPort(1433) + .setNamespace("ssinstance|ssdb") + .setName("ssinstance") + .build(), arg("jdbc:microsoft:sqlserver://;") .setProperties(stdProps()) .setShortUrl("microsoft:sqlserver://stdServerName:9999") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setUser("stdUserName") .setHost("stdServerName") @@ -446,40 +516,67 @@ private static Stream sqlServerArguments() { .build(), arg("jdbc:sqlserver://ss.host\\ssinstance:44;databaseName=ssdb;user=ssuser;password=pw") .setShortUrl("sqlserver://ss.host:44") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setUser("ssuser") .setHost("ss.host") .setPort(44) + .setNamespace("ssinstance|ssdb") .setName("ssinstance") .build(), arg("jdbc:sqlserver://;serverName=ss.host\\ssinstance:44;DatabaseName=;") .setShortUrl("sqlserver://ss.host:44") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("ss.host") .setPort(44) .setName("ssinstance") .build(), arg("jdbc:sqlserver://ss.host;serverName=althost;DatabaseName=ssdb;") .setShortUrl("sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("ss.host") .setPort(1433) .setName("ssdb") .build(), arg("jdbc:microsoft:sqlserver://ss.host:44;DatabaseName=ssdb;user=ssuser;password=pw;user=ssuser2;") .setShortUrl("microsoft:sqlserver://ss.host:44") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setUser("ssuser") .setHost("ss.host") .setPort(44) .setName("ssdb") .build(), + arg("jdbc:sqlserver://ss.host:44/urldb;user=ssuser") + .setProperties(stdProps()) + .setShortUrl("sqlserver://stdServerName:9999") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setName("stdDatabaseName") + .build(), + arg("jdbc:sqlserver://ss.host\\ssinstance:44;databaseName=urldb;user=ssuser") + .setProperties(stdProps()) + .setShortUrl("sqlserver://stdServerName:9999") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setNamespace("ssinstance|stdDatabaseName") + .setName("ssinstance") + .build(), // http://jtds.sourceforge.net/faq.html#urlFormat arg("jdbc:jtds:sqlserver://ss.host/ssdb") .setShortUrl("jtds:sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("ss.host") .setPort(1433) @@ -487,7 +584,8 @@ private static Stream sqlServerArguments() { .build(), arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb") .setShortUrl("jtds:sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("ss.host") .setPort(1433) @@ -495,7 +593,8 @@ private static Stream sqlServerArguments() { .build(), arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb;user=ssuser") .setShortUrl("jtds:sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setUser("ssuser") .setHost("ss.host") @@ -504,27 +603,64 @@ private static Stream sqlServerArguments() { .build(), arg("jdbc:jtds:sqlserver://ss.host/ssdb;instance=ssinstance") .setShortUrl("jtds:sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("ss.host") .setPort(1433) + .setNamespace("ssinstance|ssdb") .setName("ssinstance") .build(), arg("jdbc:jtds:sqlserver://ss.host:1444/ssdb;instance=ssinstance") .setShortUrl("jtds:sqlserver://ss.host:1444") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("ss.host") .setPort(1444) + .setNamespace("ssinstance|ssdb") .setName("ssinstance") .build(), arg("jdbc:jtds:sqlserver://ss.host:1433/ssdb;instance=ssinstance;user=ssuser") .setShortUrl("jtds:sqlserver://ss.host:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setUser("ssuser") .setHost("ss.host") .setPort(1433) + .setNamespace("ssinstance|ssdb") + .setName("ssinstance") + .build(), + // instance without database — namespace is just the instance name + arg("jdbc:jtds:sqlserver://ss.host;instance=ssinstance") + .setShortUrl("jtds:sqlserver://ss.host:1433") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setHost("ss.host") + .setPort(1433) + .setNamespace("ssinstance") + .setName("ssinstance") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1444/urldb") + .setProperties(stdProps()) + .setShortUrl("jtds:sqlserver://stdServerName:9999") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setName("stdDatabaseName") + .build(), + arg("jdbc:jtds:sqlserver://ss.host:1444/urldb;instance=ssinstance") + .setProperties(stdProps()) + .setShortUrl("jtds:sqlserver://stdServerName:9999") + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setNamespace("ssinstance|stdDatabaseName") .setName("ssinstance") .build()); } @@ -541,7 +677,8 @@ private static Stream oracleArguments() { // https://docs.oracle.com/cd/B28359_01/java.111/b31224/jdbcthin.htm arg("jdbc:oracle:thin:orcluser/PW@localhost:55:orclsn") .setShortUrl("oracle:thin://localhost:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setUser("orcluser") .setHost("localhost") @@ -550,7 +687,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:orcluser/PW@//orcl.host:55/orclsn") .setShortUrl("oracle:thin://orcl.host:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setUser("orcluser") .setHost("orcl.host") @@ -559,7 +697,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:orcluser/PW@127.0.0.1:orclsn") .setShortUrl("oracle:thin://127.0.0.1:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setUser("orcluser") .setHost("127.0.0.1") @@ -568,7 +707,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:orcluser/PW@//orcl.host/orclsn") .setShortUrl("oracle:thin://orcl.host:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setUser("orcluser") .setHost("orcl.host") @@ -577,7 +717,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:@//orcl.host:55/orclsn") .setShortUrl("oracle:thin://orcl.host:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("orcl.host") .setPort(55) @@ -585,7 +726,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:@ldap://orcl.host:55/some,cn=OracleContext,dc=com") .setShortUrl("oracle:thin://orcl.host:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("orcl.host") .setPort(55) @@ -593,7 +735,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:127.0.0.1:orclsn") .setShortUrl("oracle:thin://127.0.0.1:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("127.0.0.1") .setPort(1521) // Default Oracle port assumed as not specified in the URL @@ -602,7 +745,8 @@ private static Stream oracleArguments() { arg("jdbc:oracle:thin:orcl.host:orclsn") .setProperties(stdProps()) .setShortUrl("oracle:thin://orcl.host:9999") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setUser("stdUserName") .setHost("orcl.host") @@ -612,7 +756,8 @@ private static Stream oracleArguments() { arg("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=666))" + "(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orclsn)))") .setShortUrl("oracle:thin://127.0.0.1:666") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("127.0.0.1") .setPort(666) @@ -620,7 +765,8 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:thin:@ ( description = (connect_timeout=90)(retry_count=20)(retry_delay=3) (transport_connect_timeout=3000) (address_list = (load_balance = on) (failover = on) (address = (protocol = tcp)(host = orcl.host1 )(port = 1521 )) (address = (protocol = tcp)(host = orcl.host2)(port = 1521)) (address = (protocol = tcp)(host = orcl.host3)(port = 1521)) (address = (protocol = tcp)(host = orcl.host4)(port = 1521)) ) (connect_data = (server = dedicated) (service_name = orclsn)))") .setShortUrl("oracle:thin://orcl.host1:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("orcl.host1") .setPort(1521) @@ -630,7 +776,8 @@ private static Stream oracleArguments() { // https://docs.oracle.com/cd/B28359_01/java.111/b31224/instclnt.htm arg("jdbc:oracle:drivertype:orcluser/PW@orcl.host:55/orclsn") .setShortUrl("oracle:drivertype://orcl.host:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("drivertype") .setUser("orcluser") .setHost("orcl.host") @@ -639,14 +786,16 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:oci8:@") .setShortUrl("oracle:oci8:") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oci8") .setPort(1521) .build(), arg("jdbc:oracle:oci8:@") .setProperties(stdProps()) .setShortUrl("oracle:oci8://stdServerName:9999") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oci8") .setUser("stdUserName") .setHost("stdServerName") @@ -655,14 +804,16 @@ private static Stream oracleArguments() { .build(), arg("jdbc:oracle:oci8:@orclsn") .setShortUrl("oracle:oci8:") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oci8") .setPort(1521) .setName("orclsn") .build(), arg("jdbc:oracle:oci:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=orcl.host)(PORT=55))(CONNECT_DATA=(SERVICE_NAME=orclsn)))") .setShortUrl("oracle:oci://orcl.host:55") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oci") .setHost("orcl.host") .setPort(55) @@ -682,14 +833,16 @@ private static Stream db2Arguments() { // https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.5.0/com.ibm.db2.luw.apdv.java.doc/src/tpc/imjcc_r0052342.html arg("jdbc:db2://db2.host") .setShortUrl("db2://db2.host:50000") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setHost("db2.host") .setPort(50000) .build(), arg("jdbc:db2://db2.host") .setProperties(stdProps()) .setShortUrl("db2://db2.host:9999") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setUser("stdUserName") .setHost("db2.host") .setPort(9999) @@ -697,7 +850,8 @@ private static Stream db2Arguments() { .build(), arg("jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;") .setShortUrl("db2://db2.host:77") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setUser("db2user") .setHost("db2.host") .setPort(77) @@ -706,7 +860,8 @@ private static Stream db2Arguments() { arg("jdbc:db2://db2.host:77/db2db:user=db2user;password=PW;") .setProperties(stdProps()) .setShortUrl("db2://db2.host:77") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setUser("db2user") .setHost("db2.host") .setPort(77) @@ -714,7 +869,8 @@ private static Stream db2Arguments() { .build(), arg("jdbc:as400://ashost:66/asdb:user=asuser;password=PW;") .setShortUrl("as400://ashost:66") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setUser("asuser") .setHost("ashost") .setPort(66) @@ -733,13 +889,15 @@ private static Stream sapArguments() { // https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/2.0.03/en-US/ff15928cf5594d78b841fbbe649f04b4.html arg("jdbc:sap://sap.host") .setShortUrl("sap://sap.host") - .setSystem(HANADB) + .setSystem("sap.hana") + .setOldSystem("hanadb") .setHost("sap.host") .build(), arg("jdbc:sap://sap.host") .setProperties(stdProps()) .setShortUrl("sap://sap.host:9999") - .setSystem(HANADB) + .setSystem("sap.hana") + .setOldSystem("hanadb") .setUser("stdUserName") .setHost("sap.host") .setPort(9999) @@ -747,7 +905,8 @@ private static Stream sapArguments() { .build(), arg("jdbc:sap://sap.host:88/?databaseName=sapdb&user=sapuser&password=PW") .setShortUrl("sap://sap.host:88") - .setSystem(HANADB) + .setSystem("sap.hana") + .setOldSystem("hanadb") .setUser("sapuser") .setHost("sap.host") .setPort(88) @@ -765,7 +924,8 @@ private static Stream informixArguments() { return args( // https://www.ibm.com/support/pages/how-configure-informix-jdbc-connection-string-connect-group arg("jdbc:informix-sqli://infxhost:99/infxdb:INFORMIXSERVER=infxsn;user=infxuser;password=PW") - .setSystem("informix-sqli") + .setSystem("ibm.informix") + .setOldSystem("informix-sqli") .setUser("infxuser") .setShortUrl("informix-sqli://infxhost:99") .setHost("infxhost") @@ -773,50 +933,58 @@ private static Stream informixArguments() { .setName("infxdb") .build(), arg("jdbc:informix-sqli://localhost:9088/stores_demo:INFORMIXSERVER=informix") - .setSystem("informix-sqli") + .setSystem("ibm.informix") + .setOldSystem("informix-sqli") .setShortUrl("informix-sqli://localhost:9088") .setHost("localhost") .setPort(9088) .setName("stores_demo") .build(), arg("jdbc:informix-sqli://infxhost:99") - .setSystem("informix-sqli") + .setSystem("ibm.informix") + .setOldSystem("informix-sqli") .setShortUrl("informix-sqli://infxhost:99") .setHost("infxhost") .setPort(99) .build(), arg("jdbc:informix-sqli://infxhost/") - .setSystem("informix-sqli") + .setSystem("ibm.informix") + .setOldSystem("informix-sqli") .setShortUrl("informix-sqli://infxhost:9088") .setHost("infxhost") .setPort(9088) .build(), arg("jdbc:informix-sqli:") - .setSystem("informix-sqli") + .setSystem("ibm.informix") + .setOldSystem("informix-sqli") .setShortUrl("informix-sqli:") .setPort(9088) .build(), // https://www.ibm.com/docs/en/informix-servers/12.10?topic=method-format-database-urls arg("jdbc:informix-direct://infxdb:999;user=infxuser;password=PW") - .setSystem("informix-direct") + .setSystem("ibm.informix") + .setOldSystem("informix-direct") .setShortUrl("informix-direct:") .setUser("infxuser") .setName("infxdb") .build(), arg("jdbc:informix-direct://infxdb;user=infxuser;password=PW") - .setSystem("informix-direct") + .setSystem("ibm.informix") + .setOldSystem("informix-direct") .setShortUrl("informix-direct:") .setUser("infxuser") .setName("infxdb") .build(), arg("jdbc:informix-direct://infxdb") - .setSystem("informix-direct") + .setSystem("ibm.informix") + .setOldSystem("informix-direct") .setShortUrl("informix-direct:") .setName("infxdb") .build(), arg("jdbc:informix-direct:") - .setSystem("informix-direct") + .setSystem("ibm.informix") + .setOldSystem("informix-direct") .setShortUrl("informix-direct:") .build()); } @@ -830,24 +998,32 @@ void testInformixParsing(ParseTestArgument argument) { private static Stream h2Arguments() { return args( // http://www.h2database.com/html/features.html#database_url - arg("jdbc:h2:mem:").setShortUrl("h2:mem:").setSystem(H2).setSubtype("mem").build(), + arg("jdbc:h2:mem:") + .setShortUrl("h2:mem:") + .setSystem("h2database") + .setOldSystem("h2") + .setSubtype("mem") + .build(), arg("jdbc:h2:mem:") .setProperties(stdProps()) .setShortUrl("h2:mem:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("mem") .setUser("stdUserName") .setName("stdDatabaseName") .build(), arg("jdbc:h2:mem:h2db") .setShortUrl("h2:mem:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("mem") .setName("h2db") .build(), arg("jdbc:h2:tcp://h2.host:111/path/h2db;user=h2user;password=PW") .setShortUrl("h2:tcp://h2.host:111") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("tcp") .setUser("h2user") .setHost("h2.host") @@ -856,7 +1032,8 @@ private static Stream h2Arguments() { .build(), arg("jdbc:h2:ssl://h2.host:111/path/h2db;user=h2user;password=PW") .setShortUrl("h2:ssl://h2.host:111") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("ssl") .setUser("h2user") .setHost("h2.host") @@ -865,31 +1042,36 @@ private static Stream h2Arguments() { .build(), arg("jdbc:h2:/data/h2file") .setShortUrl("h2:file:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("file") .setName("/data/h2file") .build(), arg("jdbc:h2:file:~/h2file;USER=h2user;PASSWORD=PW") .setShortUrl("h2:file:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("file") .setName("~/h2file") .build(), arg("jdbc:h2:file:/data/h2file") .setShortUrl("h2:file:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("file") .setName("/data/h2file") .build(), arg("jdbc:h2:file:C:/data/h2file") .setShortUrl("h2:file:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("file") .setName("c:/data/h2file") .build(), arg("jdbc:h2:zip:~/db.zip!/h2zip") .setShortUrl("h2:zip:") - .setSystem(H2) + .setSystem("h2database") + .setOldSystem("h2") .setSubtype("zip") .setName("~/db.zip!/h2zip") .build()); @@ -1170,7 +1352,8 @@ private static Stream dataDirectArguments() { // https://docs.progress.com/bundle/datadirect-connect-jdbc-51/page/URL-Formats-DataDirect-Connect-for-JDBC-Drivers.html arg("jdbc:datadirect:sqlserver://server_name:1433;DatabaseName=dbname") .setShortUrl("datadirect:sqlserver://server_name:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("server_name") .setPort(1433) @@ -1178,7 +1361,8 @@ private static Stream dataDirectArguments() { .build(), arg("jdbc:datadirect:oracle://server_name:1521;ServiceName=your_servicename") .setShortUrl("datadirect:oracle://server_name:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oracle") .setHost("server_name") .setPort(1521) @@ -1200,7 +1384,8 @@ private static Stream dataDirectArguments() { .build(), arg("jdbc:datadirect:db2://server_name:50000;DatabaseName=dbname") .setShortUrl("datadirect:db2://server_name:50000") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setSubtype("db2") .setHost("server_name") .setPort(50000) @@ -1220,7 +1405,8 @@ private static Stream tibcoArguments() { // https://community.jaspersoft.com/documentation/tibco-jasperreports-server-administrator-guide/v601/working-data-sources arg("jdbc:tibcosoftware:sqlserver://server_name:1433;DatabaseName=dbname") .setShortUrl("tibcosoftware:sqlserver://server_name:1433") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setSubtype("sqlserver") .setHost("server_name") .setPort(1433) @@ -1228,7 +1414,8 @@ private static Stream tibcoArguments() { .build(), arg("jdbc:tibcosoftware:oracle://server_name:1521;ServiceName=your_servicename") .setShortUrl("tibcosoftware:oracle://server_name:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oracle") .setHost("server_name") .setPort(1521) @@ -1250,7 +1437,8 @@ private static Stream tibcoArguments() { .build(), arg("jdbc:tibcosoftware:db2://server_name:50000;DatabaseName=dbname") .setShortUrl("tibcosoftware:db2://server_name:50000") - .setSystem(DB2) + .setSystem("ibm.db2") + .setOldSystem("db2") .setSubtype("db2") .setHost("server_name") .setPort(50000) @@ -1282,7 +1470,8 @@ private static Stream secretsManagerArguments() { .build(), arg("jdbc-secretsmanager:oracle:thin:@example.com:50000/ORCL") .setShortUrl("oracle:thin://example.com:50000") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("example.com") .setPort(50000) @@ -1290,7 +1479,8 @@ private static Stream secretsManagerArguments() { .build(), arg("jdbc-secretsmanager:sqlserver://example.com:50000") .setShortUrl("sqlserver://example.com:50000") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("example.com") .setPort(50000) .build()); @@ -1320,7 +1510,8 @@ private static Stream openTracingArguments() { .build(), arg("jdbc:tracing:oracle:thin:@example.com:50000/ORCL") .setShortUrl("oracle:thin://example.com:50000") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("thin") .setHost("example.com") .setPort(50000) @@ -1328,7 +1519,8 @@ private static Stream openTracingArguments() { .build(), arg("jdbc:tracing:sqlserver://example.com:50000") .setShortUrl("sqlserver://example.com:50000") - .setSystem(MSSQL) + .setSystem("microsoft.sql_server") + .setOldSystem("mssql") .setHost("example.com") .setPort(50000) .build()); @@ -1352,7 +1544,8 @@ private static Stream oceanbaseArguments() { .build(), arg("jdbc:oceanbase:oracle://host:1521") .setShortUrl("oceanbase:oracle://host:1521") - .setSystem(ORACLE) + .setSystem("oracle.db") + .setOldSystem("oracle") .setSubtype("oracle") .setHost("host") .setPort(1521) @@ -1425,12 +1618,13 @@ void testPolardbParsing(ParseTestArgument argument) { private static void testVerifySystemSubtypeParsingOfUrl(ParseTestArgument argument) { DbInfo info = parse(argument.url, argument.properties); DbInfo expected = argument.dbInfo; - assertThat(info.getShortUrl()).isEqualTo(expected.getShortUrl()); - assertThat(info.getSystem()).isEqualTo(expected.getSystem()); - assertThat(info.getHost()).isEqualTo(expected.getHost()); - assertThat(info.getPort()).isEqualTo(expected.getPort()); - assertThat(info.getUser()).isEqualTo(expected.getUser()); - assertThat(info.getName()).isEqualTo(expected.getName()); + assertThat(info.getDbConnectionString()).isEqualTo(expected.getDbConnectionString()); + assertThat(info.getDbSystemName()).isEqualTo(expected.getDbSystemName()); + assertThat(info.getServerAddress()).isEqualTo(expected.getServerAddress()); + assertThat(info.getServerPort()).isEqualTo(expected.getServerPort()); + assertThat(info.getDbUser()).isEqualTo(expected.getDbUser()); + assertThat(info.getDbNamespace()).isEqualTo(expected.getDbNamespace()); + assertThat(info.getDbName()).isEqualTo(expected.getDbName()); assertThat(info).isEqualTo(expected); } @@ -1442,21 +1636,27 @@ static class ParseTestArgument { ParseTestArgument(ParseTestArgumentBuilder builder) { this.url = builder.url; this.properties = builder.properties; + + String oldSystem = builder.oldSystem != null ? builder.oldSystem : builder.system; + String namespace = builder.namespace != null ? builder.namespace : builder.name; + String oldDbName = builder.name != null ? builder.name : namespace; + this.dbInfo = DbInfo.builder() - .shortUrl(builder.shortUrl) - .system(builder.system) - .subtype(builder.subtype) - .user(builder.user) - .name(builder.name) - .host(builder.host) - .port(builder.port) + .dbConnectionString(builder.shortUrl) + .dbSystemName(builder.system) + .dbSystem(oldSystem) + .dbUser(builder.user) + .dbNamespace(namespace) + .dbName(oldDbName) + .serverAddress(builder.host) + .serverPort(builder.port) .build(); } @Override public String toString() { - return dbInfo.getSystem() + ":" + dbInfo.getSubtype() + " parsing of " + url; + return dbInfo.getDbSystemName() + " parsing of " + url; } } @@ -1465,10 +1665,11 @@ static class ParseTestArgumentBuilder { Properties properties; String shortUrl; String system; - String subtype; + String oldSystem; String user; String host; Integer port; + String namespace; String name; ParseTestArgumentBuilder(String url) { @@ -1490,8 +1691,16 @@ ParseTestArgumentBuilder setSystem(String system) { return this; } + ParseTestArgumentBuilder setOldSystem(String oldSystem) { + this.oldSystem = oldSystem; + return this; + } + + /** + * @deprecated Subtype tracking removed; retained as no-op for diff clarity. + */ + @Deprecated ParseTestArgumentBuilder setSubtype(String subtype) { - this.subtype = subtype; return this; } @@ -1510,6 +1719,11 @@ ParseTestArgumentBuilder setPort(Integer port) { return this; } + ParseTestArgumentBuilder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + ParseTestArgumentBuilder setName(String name) { this.name = name; return this; From 01182577e10d9f0beba2da0d7515bd7e4b4c2c7b Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 1 Apr 2026 19:06:14 -0700 Subject: [PATCH 06/12] Adjust DbClientSpanNameExtractor semconv test --- .../api/incubator/semconv/db/DbClientSpanNameExtractorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java index 228a1e53f5d6..f90a5f60ea49 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractorTest.java @@ -173,7 +173,6 @@ void shouldUseQuerySummaryWhenAvailable() { // given DbRequest dbRequest = new DbRequest(); - // Needs to be lenient because not called during this test under old semconv mode if (emitStableDatabaseSemconv()) { when(dbAttributesGetter.getDbQuerySummary(dbRequest)).thenReturn("SELECT users"); } From ad96d688373b1bc8ae372b96a212524d2577f034 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 1 Apr 2026 19:07:17 -0700 Subject: [PATCH 07/12] Update Kafka Connect base test plumbing --- .../kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java index 90d6b8463951..fe80feed24fb 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java +++ b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/KafkaConnectSinkTaskBaseTest.java @@ -275,6 +275,9 @@ private void setupKafkaConnect() { .withEnv("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "1") .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10ms") .withEnv("OTEL_METRIC_EXPORT_INTERVAL", "1000") + .withEnv( + "OTEL_SEMCONV_STABILITY_OPT_IN", + System.getProperty("otel.semconv-stability.opt-in")) .withEnv("CONNECT_BOOTSTRAP_SERVERS", getInternalKafkaBoostrapServers()) .withEnv("CONNECT_REST_ADVERTISED_HOST_NAME", KAFKA_CONNECT_NETWORK_ALIAS) .withEnv("CONNECT_PLUGIN_PATH", PLUGIN_PATH_CONTAINER) From f69c3b9d5fbafb2ccc013bf15e1ea47c3a0247be Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 1 Apr 2026 19:11:13 -0700 Subject: [PATCH 08/12] Update Kafka Connect Postgres assertions --- .../PostgresKafkaConnectSinkTaskTest.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java index 147a9b04bb1b..0c8147e52f9f 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java +++ b/instrumentation/kafka/kafka-connect-2.6/testing/src/test/java/io/opentelemetry/instrumentation/kafkaconnect/v2_6/PostgresKafkaConnectSinkTaskTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.kafkaconnect.v2_6; import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_BATCH_MESSAGE_COUNT; @@ -18,6 +19,7 @@ import static io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME; import static io.restassured.RestAssured.given; import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import io.opentelemetry.api.trace.Span; @@ -160,8 +162,14 @@ void testSingleMessage() throws Exception { trace -> { // kafka connect consumer trace, linked to producer span via a span link Consumer selectAssertion = - span -> - span.hasName("SELECT test").hasKind(SpanKind.CLIENT).hasParent(trace.getSpan(0)); + span -> { + if (emitStableDatabaseSemconv()) { + span.satisfies(spanData -> assertThat(spanData.getName()).startsWith("SELECT")); + } else { + span.hasName("SELECT " + DATABASE_NAME); + } + span.hasKind(SpanKind.CLIENT).hasParent(trace.getSpan(0)); + }; trace.hasSpansSatisfyingExactly( span -> @@ -182,7 +190,10 @@ void testSingleMessage() throws Exception { selectAssertion, selectAssertion, span -> - span.hasName("INSERT test." + DB_TABLE_PERSON) + span.hasName( + emitStableDatabaseSemconv() + ? "INSERT \"" + DB_TABLE_PERSON + "\"" + : "INSERT " + DATABASE_NAME + "." + DB_TABLE_PERSON) .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0))); }, @@ -285,8 +296,14 @@ void testMultiTopic() throws Exception { trace -> { // kafka connect consumer trace, linked to producer span via a span link Consumer selectAssertion = - span -> - span.hasName("SELECT test").hasKind(SpanKind.CLIENT).hasParent(trace.getSpan(0)); + span -> { + if (emitStableDatabaseSemconv()) { + span.satisfies(spanData -> assertThat(spanData.getName()).startsWith("SELECT")); + } else { + span.hasName("SELECT " + DATABASE_NAME); + } + span.hasKind(SpanKind.CLIENT).hasParent(trace.getSpan(0)); + }; trace.hasSpansSatisfyingExactly( span -> @@ -309,7 +326,10 @@ void testMultiTopic() throws Exception { selectAssertion, selectAssertion, span -> - span.hasName("INSERT test." + DB_TABLE_PERSON) + span.hasName( + emitStableDatabaseSemconv() + ? "BATCH INSERT \"" + DB_TABLE_PERSON + "\"" + : "INSERT " + DATABASE_NAME + "." + DB_TABLE_PERSON) .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0))); }, From 9061146fdf5a277de425fc5c0f4c1e00425aa827 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 1 Apr 2026 19:15:14 -0700 Subject: [PATCH 09/12] Add Kafka Connect stable semconv test task --- .../kafka-connect-2.6/testing/build.gradle.kts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts b/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts index 2989e94f72cc..98a179147a18 100644 --- a/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts +++ b/instrumentation/kafka/kafka-connect-2.6/testing/build.gradle.kts @@ -37,3 +37,16 @@ tasks.withType().configureEach { systemProperty("io.opentelemetry.smoketest.agent.shadowJar.path", agentShadowJar.get().archiveFile.get().toString()) systemProperty("collectMetadata", findProperty("collectMetadata")) } + +tasks { + val testStableSemconv by registering(Test::class) { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + jvmArgs("-Dotel.semconv-stability.opt-in=database") + systemProperty("metadataConfig", "otel.semconv-stability.opt-in=database") + } + + check { + dependsOn(testStableSemconv) + } +} From db7842a054db56642ba2cdaf94a1e9dde2e779ab Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 3 Apr 2026 11:54:16 -0700 Subject: [PATCH 10/12] Update instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../instrumentation/jdbc/internal/parser/ParseContext.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java index ab607049e2ba..a88fe3550304 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/ParseContext.java @@ -249,7 +249,8 @@ public void applyUserProperty() { * Parse a URL-style JDBC connection string that uses semicolons for properties. Updates this * context with extracted values (user, host, port, path). * - *

Database path acts as fallback and does not override an existing database name. + *

If the URL contains a database path, it is applied to this context and overrides any + * previously set database name. * * @param jdbcUrl the JDBC URL to parse */ From 3fcba475593784afb63750d0b8e373ddf3d0974d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 3 Apr 2026 11:54:33 -0700 Subject: [PATCH 11/12] Update instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../instrumentation/jdbc/internal/parser/JtdsUrlParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java index eb5c4e3959ed..cebb3aa71bb9 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/parser/JtdsUrlParser.java @@ -50,11 +50,11 @@ public void parse(String jdbcUrl, ParseContext ctx) { ctx.subtype("sqlserver"); // Use ParseContext.parseUrl() to handle URL structure parsing (user, host, port, path) - // Note: parseUrl() sets URL path to ctx.name, but for jTDS the path is the database + // Note: for jTDS, parseUrl() maps the URL path to the database name ctx.parseUrl(jdbcUrl); // Handle jTDS/SQL Server-specific parameters - // For jTDS, URL path (already in ctx.name from parseUrl) represents database name + // For jTDS, the URL path parsed above represents the database name // Extract instance and database parameters String[] split = jdbcUrl.split(";", 2); String instanceName = null; From ccf487119c0dc7ddf4eeac931f64d9eb1562e600 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 3 Apr 2026 13:18:34 -0700 Subject: [PATCH 12/12] review --- .../jdbc/internal/JdbcConnectionUrlParser.java | 11 +++++------ .../internal/JdbcConnectionUrlParserTest.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java index 5cd47a2a1a9d..7785bc476a40 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParser.java @@ -107,7 +107,8 @@ private JdbcConnectionUrlParser() {} * * @param connectionUrl the JDBC connection URL * @param props optional connection properties - * @return the parsed DbInfo, or DbInfo.DEFAULT if parsing fails + * @return the parsed DbInfo, or DbInfo.DEFAULT for null/invalid non-JDBC inputs; parser failures + * return the best-effort result accumulated before the failure */ public static DbInfo parse(String connectionUrl, Properties props) { if (connectionUrl == null) { @@ -129,13 +130,11 @@ public static DbInfo parse(String connectionUrl, Properties props) { } String type = jdbcUrl.substring(0, typeLoc); + JdbcUrlParser parser = TYPE_PARSERS.get(type); + ParseContext ctx = ParseContext.of(type, props); try { - JdbcUrlParser parser = TYPE_PARSERS.get(type); - - ParseContext ctx = ParseContext.of(type, props); if (parser == null) { - // Unknown JDBC type: apply all standard DataSource properties and use generic parser ctx.applyDataSourceProperties(); GenericUrlParser.INSTANCE.parse(jdbcUrl, ctx); } else { @@ -145,7 +144,7 @@ public static DbInfo parse(String connectionUrl, Properties props) { return ctx.toDbInfo(); } catch (RuntimeException e) { logger.log(FINE, "Error parsing URL", e); - return DEFAULT; + return ctx.toDbInfo(); } } diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java index a30584f9c067..2503c7c4b9a8 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcConnectionUrlParserTest.java @@ -62,6 +62,23 @@ void testNullUrlReturnsDefault() { assertThat(JdbcConnectionUrlParser.parse(null, null)).isEqualTo(DEFAULT); } + @Test + void testParserExceptionReturnsBestEffortInfo() { + // Intentionally malformed Oracle URL: missing subtype/connect info, which triggers the + // Oracle parser's substring-based failure path after it has already applied defaults/props. + testVerifySystemSubtypeParsingOfUrl( + arg("jdbc:oracle:") + .setProperties(stdProps()) + .setShortUrl("oracle://stdServerName:9999") + .setSystem("oracle.db") + .setOldSystem("oracle") + .setUser("stdUserName") + .setHost("stdServerName") + .setPort(9999) + .setName("stdDatabaseName") + .build()); + } + private static Stream mySqlArguments() { return args( // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html