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..e56c3ae9b96
--- /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() < AMAZONAWS_COM.length()) {
+ 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";