From afcfb6a391d4fe998b3447f587f5ac21688c2e93 Mon Sep 17 00:00:00 2001 From: Pedram Safaei Date: Tue, 22 Sep 2026 09:45:57 +0000 Subject: [PATCH 1/2] Tag database client spans with the identity encoded in RDS endpoint hostnames Database client spans against Amazon RDS only carried peer.hostname, for example orders-db.c9akciq32bzq.us-east-1.rds.amazonaws.com. That name deterministically encodes the DB instance (or Aurora cluster, custom endpoint or RDS Proxy) identifier and the Region, but nothing exposed them as queryable tags. Correlating a slow query with the aws.rds.* integration metrics for the same instance meant parsing the hostname by hand. DatabaseClientDecorator now parses the hostname whenever it matches an RDS endpoint and tags the span with: - aws.rds.identifier: the leading label (instance, cluster, custom endpoint or proxy name) - aws.rds.endpoint_type: instance, cluster, cluster-ro, cluster-custom or proxy - aws.region: the Region encoded in the hostname - dbinstanceidentifier when the endpoint is an instance endpoint, and dbclusteridentifier when it is a cluster or cluster reader endpoint. These match the dimension tags on the aws.rds.* metrics so APM spans and the RDS integration (and Database Monitoring) join on the same key. The instance or cluster identifier is only claimed when the endpoint type proves it. A cluster endpoint does not name the writer instance behind it, and a custom endpoint or proxy name is not a cluster identifier, so those types get aws.rds.identifier only. Both the standard (...rds.amazonaws.com) and China (..rds..amazonaws.com.cn) shapes are handled. Parsing is cached per hostname; hostnames not ending in ".amazonaws.com" or ".amazonaws.com.cn" (an O(1) suffix check) are rejected before the cache so they neither evict RDS entries nor pay for the parse, and are otherwise left untouched. So every existing database client integration (JDBC, R2DBC, Vert.x, ...) picks this up with no behaviour change outside RDS. --- .../decorator/AwsRdsEndpoint.java | 229 ++++++++++++++++++ .../decorator/DatabaseClientDecorator.java | 27 +++ .../DatabaseClientDecoratorTest.groovy | 44 ++++ .../decorator/AwsRdsEndpointTest.java | 159 ++++++++++++ .../api/InstrumentationTags.java | 9 + 5 files changed, 468 insertions(+) create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpointTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java new file mode 100644 index 00000000000..b0d8028945e --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java @@ -0,0 +1,229 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import datadog.trace.api.cache.DDCache; +import datadog.trace.api.cache.DDCaches; +import java.util.Locale; + +/** + * Identity encoded in an Amazon RDS endpoint hostname. + * + *

RDS hands out DNS names of the form {@code ...rds.amazonaws.com} (or + * {@code ..rds..amazonaws.com.cn} in the China partition). The leading + * label is the DB instance identifier, the Aurora cluster identifier, a custom endpoint name or an + * RDS Proxy name depending on the {@code } label prefix. The Region is always present. + * + *

Only the endpoint type is recoverable from the name. A cluster endpoint always names a + * cluster, but the writer instance behind it is not part of the hostname, so this class never + * claims an instance identifier for a cluster endpoint. + */ +public final class AwsRdsEndpoint { + + public enum Type { + /** {@code ...rds.amazonaws.com}. */ + INSTANCE("instance"), + /** {@code .cluster-..rds.amazonaws.com}. */ + CLUSTER("cluster"), + /** {@code .cluster-ro-..rds.amazonaws.com}. */ + CLUSTER_READER("cluster-ro"), + /** {@code .cluster-custom-..rds.amazonaws.com}. */ + CLUSTER_CUSTOM("cluster-custom"), + /** {@code .proxy-..rds.amazonaws.com}. */ + PROXY("proxy"); + + private final String tagValue; + + Type(String tagValue) { + this.tagValue = tagValue; + } + + public String tagValue() { + return tagValue; + } + } + + private static final AwsRdsEndpoint NOT_RDS = new AwsRdsEndpoint(null, null, null); + + // Applications talk to a handful of databases; the cache only needs to absorb the per-span + // parse of the same few hostnames. + private static final DDCache CACHE = DDCaches.newFixedSizeCache(16); + + private final String identifier; + private final Type type; + private final String region; + + private AwsRdsEndpoint(String identifier, Type type, String region) { + this.identifier = identifier; + this.type = type; + this.region = region; + } + + /** + * Parses an RDS endpoint hostname. + * + * @return the identity encoded in the hostname, or {@code null} when the hostname is not an RDS + * endpoint. + */ + public static AwsRdsEndpoint parse(final CharSequence hostname) { + if (hostname == null || hostname.length() == 0) { + return null; + } + String hostnameString = hostname.toString(); + // Every RDS endpoint, in any partition, ends in ".amazonaws.com" or ".amazonaws.com.cn". + // Reject everything else before touching the cache so the handful of real RDS hostnames are + // never evicted by the unbounded stream of non-AWS database hosts, and so those hosts never + // pay for the label split. + if (!isPlausibleRdsHostname(hostnameString)) { + return null; + } + AwsRdsEndpoint endpoint = CACHE.computeIfAbsent(hostnameString, AwsRdsEndpoint::doParse); + return endpoint == NOT_RDS ? null : endpoint; + } + + private static final String AMAZONAWS_COM = ".amazonaws.com"; + private static final String AMAZONAWS_COM_CN = ".amazonaws.com.cn"; + + /** + * O(1) check that the hostname ends with {@code .amazonaws.com} or {@code .amazonaws.com.cn}, + * ignoring case and tolerating the same trailing {@code .} and {@code :port} that {@link + * #doParse} accepts, so the gate and the parser never disagree on what is RDS-shaped. Anchoring + * on the suffix (rather than scanning for the substring) also rejects hostnames that merely + * contain {@code .amazonaws.com} followed by other labels. + */ + static boolean isPlausibleRdsHostname(final String hostname) { + int end = hostname.length(); + if (end > 0 && hostname.charAt(end - 1) == '.') { + end--; + } + end = stripPort(hostname, end); + return endsWithIgnoreCase(hostname, end, AMAZONAWS_COM) + || endsWithIgnoreCase(hostname, end, AMAZONAWS_COM_CN); + } + + /** Returns the end index with a trailing {@code :port} removed. Bounded to 5 digits. */ + private static int stripPort(final String hostname, final int end) { + int i = end; + int digits = 0; + while (i > 0 && digits < 5 && isAsciiDigit(hostname.charAt(i - 1))) { + i--; + digits++; + } + return digits > 0 && i > 0 && hostname.charAt(i - 1) == ':' ? i - 1 : end; + } + + private static boolean isAsciiDigit(final char c) { + return c >= '0' && c <= '9'; + } + + private static boolean endsWithIgnoreCase( + final String hostname, final int end, final String suffix) { + final int length = suffix.length(); + return end >= length && hostname.regionMatches(true, end - length, suffix, 0, length); + } + + /** The DB instance identifier, cluster identifier, custom endpoint name or proxy name. */ + public String identifier() { + return identifier; + } + + public Type type() { + return type; + } + + /** The AWS Region the endpoint lives in, for example {@code us-east-1}. */ + public String region() { + return region; + } + + private static AwsRdsEndpoint doParse(final String rawHostname) { + String hostname = rawHostname.toLowerCase(Locale.ROOT); + int end = hostname.length(); + // Strip a trailing dot from a fully qualified name and a :port suffix if one leaked in. + if (end > 0 && hostname.charAt(end - 1) == '.') { + end--; + } + int colon = hostname.indexOf(':'); + if (colon >= 0) { + end = Math.min(end, colon); + } + String[] labels = hostname.substring(0, end).split("\\.", -1); + + // ...rds.amazonaws.com + if (labels.length == 6 + && "rds".equals(labels[3]) + && "amazonaws".equals(labels[4]) + && "com".equals(labels[5])) { + return build(labels[0], labels[1], labels[2]); + } + // ..rds..amazonaws.com.cn + if (labels.length == 7 + && "rds".equals(labels[2]) + && "amazonaws".equals(labels[4]) + && "com".equals(labels[5]) + && "cn".equals(labels[6])) { + return build(labels[0], labels[1], labels[3]); + } + return NOT_RDS; + } + + private static AwsRdsEndpoint build( + final String identifier, final String hash, final String region) { + if (identifier.isEmpty() || !isRegion(region)) { + return NOT_RDS; + } + Type type; + String suffix; + if (hash.startsWith("cluster-ro-")) { + type = Type.CLUSTER_READER; + suffix = hash.substring("cluster-ro-".length()); + } else if (hash.startsWith("cluster-custom-")) { + type = Type.CLUSTER_CUSTOM; + suffix = hash.substring("cluster-custom-".length()); + } else if (hash.startsWith("cluster-")) { + type = Type.CLUSTER; + suffix = hash.substring("cluster-".length()); + } else if (hash.startsWith("proxy-")) { + type = Type.PROXY; + suffix = hash.substring("proxy-".length()); + } else { + type = Type.INSTANCE; + suffix = hash; + } + if (!isHash(suffix)) { + return NOT_RDS; + } + return new AwsRdsEndpoint(identifier, type, region); + } + + /** The account-scoped hash RDS appends to every endpoint: lowercase alphanumerics only. */ + private static boolean isHash(final String value) { + if (value.length() < 8 || value.length() > 16) { + return false; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) { + return false; + } + } + return true; + } + + /** An AWS Region code such as us-east-1, us-gov-west-1, cn-north-1 or eu-isoe-west-1. */ + private static boolean isRegion(final String value) { + int length = value.length(); + if (length < 8 || length > 24) { + return false; + } + int dashes = 0; + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + if (c == '-') { + dashes++; + } else if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) { + return false; + } + } + // Every Region ends in a digit and has at least two dashes (e.g. us-east-1). + return dashes >= 2 && value.charAt(0) != '-' && Character.isDigit(value.charAt(length - 1)); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java index 79d08f4139a..c32cdaaa8fb 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecorator.java @@ -16,6 +16,7 @@ import datadog.trace.api.naming.SpanNaming; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.util.function.BiConsumer; @@ -74,6 +75,7 @@ public void onConnection(final AgentSpan span, final CONNECTION connection) { CharSequence hostName = dbHostname(connection); if (hostName != null) { span.setTag(Tags.PEER_HOSTNAME, hostName); + onRdsEndpoint(span, AwsRdsEndpoint.parse(hostName)); if (Config.get().isDbClientSplitByHost()) { span.setServiceName(hostName.toString(), DB_CLIENT_SPLIT_BY_HOST); @@ -82,6 +84,31 @@ public void onConnection(final AgentSpan span, final CONNECTION connection) { } } + /** + * Tags the identity an Amazon RDS endpoint hostname carries: the identifier, what kind of + * endpoint it is and its Region. The DB instance or cluster identifier is only claimed when the + * endpoint type proves it, since a cluster endpoint does not name the instance behind it. + */ + protected void onRdsEndpoint(final AgentSpan span, final AwsRdsEndpoint endpoint) { + if (endpoint == null) { + return; + } + span.setTag(InstrumentationTags.AWS_RDS_IDENTIFIER, endpoint.identifier()); + span.setTag(InstrumentationTags.AWS_RDS_ENDPOINT_TYPE, endpoint.type().tagValue()); + span.setTag(InstrumentationTags.AWS_REGION, endpoint.region()); + switch (endpoint.type()) { + case INSTANCE: + span.setTag(InstrumentationTags.RDS_DB_INSTANCE_IDENTIFIER, endpoint.identifier()); + break; + case CLUSTER: + case CLUSTER_READER: + span.setTag(InstrumentationTags.RDS_DB_CLUSTER_IDENTIFIER, endpoint.identifier()); + break; + default: + break; + } + } + protected void onInstance(final AgentSpan span, final String dbInstance) { if (dbInstance != null) { span.setTag(Tags.DB_INSTANCE, dbInstance); diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy index 93852ccc88c..782aa2c8346 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/DatabaseClientDecoratorTest.groovy @@ -4,6 +4,7 @@ import datadog.trace.api.DDTags import datadog.trace.api.TagMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags import datadog.trace.bootstrap.instrumentation.api.Tags import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_HOST @@ -88,6 +89,49 @@ class DatabaseClientDecoratorTest extends ClientDecoratorTest { true | true | true | [user: "test-user", instance: "test-instance"] } + def "test onConnection tags RDS endpoint identity for #hostname"() { + setup: + def decorator = newDecorator() + + when: + decorator.onConnection(span, [user: "test-user", hostname: hostname]) + + then: + 1 * span.setTag(Tags.DB_USER, "test-user") + 1 * span.setTag(Tags.PEER_HOSTNAME, hostname) + 1 * span.setTag(InstrumentationTags.AWS_RDS_IDENTIFIER, identifier) + 1 * span.setTag(InstrumentationTags.AWS_RDS_ENDPOINT_TYPE, endpointType) + 1 * span.setTag(InstrumentationTags.AWS_REGION, "us-east-1") + if (instanceIdentifier) { + 1 * span.setTag(InstrumentationTags.RDS_DB_INSTANCE_IDENTIFIER, instanceIdentifier) + } + if (clusterIdentifier) { + 1 * span.setTag(InstrumentationTags.RDS_DB_CLUSTER_IDENTIFIER, clusterIdentifier) + } + 0 * _ + + where: + hostname | identifier | endpointType | instanceIdentifier | clusterIdentifier + "orders.c9akciq32bzq.us-east-1.rds.amazonaws.com" | "orders" | "instance" | "orders" | null + "orders.cluster-c9akciq32bzq.us-east-1.rds.amazonaws.com" | "orders" | "cluster" | null | "orders" + "orders.cluster-ro-c9akciq32bzq.us-east-1.rds.amazonaws.com" | "orders" | "cluster-ro" | null | "orders" + "reports.cluster-custom-c9akciq32bzq.us-east-1.rds.amazonaws.com" | "reports" | "cluster-custom" | null | null + "orders.proxy-c9akciq32bzq.us-east-1.rds.amazonaws.com" | "orders" | "proxy" | null | null + } + + def "test onConnection leaves non-RDS hostname untagged"() { + setup: + def decorator = newDecorator() + + when: + decorator.onConnection(span, [user: "test-user", hostname: "db.internal.example.com"]) + + then: + 1 * span.setTag(Tags.DB_USER, "test-user") + 1 * span.setTag(Tags.PEER_HOSTNAME, "db.internal.example.com") + 0 * _ + } + def "test onStatement"() { setup: def decorator = newDecorator() diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpointTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpointTest.java new file mode 100644 index 00000000000..945ec58a4e8 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpointTest.java @@ -0,0 +1,159 @@ +package datadog.trace.bootstrap.instrumentation.decorator; + +import static datadog.trace.bootstrap.instrumentation.decorator.AwsRdsEndpoint.Type.CLUSTER; +import static datadog.trace.bootstrap.instrumentation.decorator.AwsRdsEndpoint.Type.CLUSTER_CUSTOM; +import static datadog.trace.bootstrap.instrumentation.decorator.AwsRdsEndpoint.Type.CLUSTER_READER; +import static datadog.trace.bootstrap.instrumentation.decorator.AwsRdsEndpoint.Type.INSTANCE; +import static datadog.trace.bootstrap.instrumentation.decorator.AwsRdsEndpoint.Type.PROXY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class AwsRdsEndpointTest { + + private static final String RDS = "orders-db.c9akciq32bzq.us-east-1.rds.amazonaws.com"; + + static Stream rdsEndpoints() { + return Stream.of( + Arguments.of(RDS, "orders-db", INSTANCE, "us-east-1"), + Arguments.of( + "Orders-DB.C9AKCIQ32BZQ.US-EAST-1.RDS.AMAZONAWS.COM", + "orders-db", + INSTANCE, + "us-east-1"), + Arguments.of(RDS + ".", "orders-db", INSTANCE, "us-east-1"), + Arguments.of(RDS + ":5432", "orders-db", INSTANCE, "us-east-1"), + Arguments.of( + "orders-db.c9akciq32bzq.us-gov-west-1.rds.amazonaws.com", + "orders-db", + INSTANCE, + "us-gov-west-1"), + Arguments.of( + "orders-db.c9akciq32bzq.eu-isoe-west-1.rds.amazonaws.com", + "orders-db", + INSTANCE, + "eu-isoe-west-1"), + Arguments.of( + "orders-db.c9akciq32bzq.rds.cn-north-1.amazonaws.com.cn", + "orders-db", + INSTANCE, + "cn-north-1"), + Arguments.of( + "orders-aurora.cluster-c9akciq32bzq.us-west-2.rds.amazonaws.com", + "orders-aurora", + CLUSTER, + "us-west-2"), + Arguments.of( + "orders-aurora.cluster-ro-c9akciq32bzq.us-west-2.rds.amazonaws.com", + "orders-aurora", + CLUSTER_READER, + "us-west-2"), + Arguments.of( + "reporting.cluster-custom-c9akciq32bzq.us-west-2.rds.amazonaws.com", + "reporting", + CLUSTER_CUSTOM, + "us-west-2"), + Arguments.of( + "orders-proxy.proxy-c9akciq32bzq.ap-southeast-2.rds.amazonaws.com", + "orders-proxy", + PROXY, + "ap-southeast-2")); + } + + @ParameterizedTest(name = "parses {0}") + @MethodSource("rdsEndpoints") + void parsesRdsEndpoint( + String hostname, String identifier, AwsRdsEndpoint.Type type, String region) { + AwsRdsEndpoint endpoint = AwsRdsEndpoint.parse(hostname); + assertNotNull(endpoint); + assertEquals(identifier, endpoint.identifier()); + assertEquals(type, endpoint.type()); + assertEquals(region, endpoint.region()); + } + + @ParameterizedTest(name = "rejects {0}") + @NullAndEmptySource + @ValueSource( + strings = { + "localhost", + "db.internal.example.com", + "orders-db.c9akciq32bzq.us-east-1.amazonaws.com", + "orders-db.rds.amazonaws.com", + "c9akciq32bzq.us-east-1.rds.amazonaws.com", + "orders-db.c9akciq32bzq.us-east-1.rds.amazonaws.com.evil.example", + "orders-db.not_a_hash!.us-east-1.rds.amazonaws.com", + "orders-db.c9akciq32bzq.useast1.rds.amazonaws.com", + "orders-db.c9akciq32bzq.us-east.rds.amazonaws.com", + ".c9akciq32bzq.us-east-1.rds.amazonaws.com", + "orders-db.cluster-.us-east-1.rds.amazonaws.com", + "s3.us-east-1.amazonaws.com", + "dynamodb.us-east-1.amazonaws.com", + }) + void rejectsNonRdsHostname(String hostname) { + assertNull(AwsRdsEndpoint.parse(hostname)); + } + + @Test + void cachesParsedEndpointsPerHostname() { + assertSame(AwsRdsEndpoint.parse(RDS), AwsRdsEndpoint.parse(RDS)); + } + + @ParameterizedTest(name = "gate accepts {0}") + @ValueSource( + strings = { + RDS, + RDS + ".", + RDS + ":5432", + RDS + ":5432.", + "orders-db.c9akciq32bzq.rds.cn-north-1.amazonaws.com.cn", + "orders-db.c9akciq32bzq.rds.cn-north-1.amazonaws.com.cn.", + "orders-db.c9akciq32bzq.rds.cn-north-1.amazonaws.com.cn:3306", + "ORDERS-DB.C9AKCIQ32BZQ.US-EAST-1.RDS.AMAZONAWS.COM", + "orders-db.c9akciq32bzq.us-east-1.rds.AmazonAWS.com", + // shaped like an AWS host, so it may reach the cache; doParse rejects it structurally + "s3.us-east-1.amazonaws.com", + }) + void preCacheGateAccepts(String hostname) { + assertTrue(AwsRdsEndpoint.isPlausibleRdsHostname(hostname)); + } + + @ParameterizedTest(name = "gate rejects {0}") + @ValueSource( + strings = { + "db-01.internal", + "localhost", + "orders-db.amazonaws.co", + "a.b", + "", + // ".amazonaws.com" present but not the suffix: rejected before the cache + "orders-db.c9akciq32bzq.us-east-1.rds.amazonaws.com.evil.example", + "orders-db.c9akciq32bzq.us-east-1.rds.amazonaws.com.cn.evil.example", + // port must be all digits and at most five of them + RDS + ":54x2", + RDS + ":543210", + }) + void preCacheGateRejects(String hostname) { + assertFalse(AwsRdsEndpoint.isPlausibleRdsHostname(hostname)); + } + + @Test + void nonRdsHostnamesDoNotEvictCachedRdsEndpoints() { + AwsRdsEndpoint first = AwsRdsEndpoint.parse(RDS); + for (int i = 0; i < 100; i++) { + assertNull(AwsRdsEndpoint.parse("db-" + i + ".internal")); + assertNull(AwsRdsEndpoint.parse("db-" + i + ".rds.amazonaws.com.evil.example")); + } + assertSame(first, AwsRdsEndpoint.parse(RDS)); + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index 0c1054e7776..23cfe89cc24 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -37,6 +37,15 @@ public class InstrumentationTags { public static final String AWS_REQUEST_ID = "aws.requestId"; public static final String AWS_STORAGE_CLASS = "aws.storage.class"; + // Identity encoded in an Amazon RDS endpoint hostname. dbinstanceidentifier and + // dbclusteridentifier match the dimension tags on the aws.rds.* metrics so database client + // spans and Database Monitoring join on the same keys. + public static final String AWS_REGION = "aws.region"; + public static final String AWS_RDS_IDENTIFIER = "aws.rds.identifier"; + public static final String AWS_RDS_ENDPOINT_TYPE = "aws.rds.endpoint_type"; + public static final String RDS_DB_INSTANCE_IDENTIFIER = "dbinstanceidentifier"; + public static final String RDS_DB_CLUSTER_IDENTIFIER = "dbclusteridentifier"; + // These are temporary keys used for span pointer hash calculation public static final String S3_ETAG = "s3.eTag"; public static final String DYNAMO_PRIMARY_KEY_1 = "dynamodb.primary_key_1"; From 5c85113bc42b724b3d33d47c846653b2959bfc82 Mon Sep 17 00:00:00 2001 From: Syntax Error <34566793+pedramsafaei@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:11:23 -0700 Subject: [PATCH 2/2] Update dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java Co-authored-by: Stuart McCulloch --- .../bootstrap/instrumentation/decorator/AwsRdsEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java index b0d8028945e..e56c3ae9b96 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/AwsRdsEndpoint.java @@ -64,7 +64,7 @@ private AwsRdsEndpoint(String identifier, Type type, String region) { * endpoint. */ public static AwsRdsEndpoint parse(final CharSequence hostname) { - if (hostname == null || hostname.length() == 0) { + if (hostname == null || hostname.length() < AMAZONAWS_COM.length()) { return null; } String hostnameString = hostname.toString();