Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -117,7 +118,9 @@ public static String inject(
final String parentService = config.getServiceName();
final String env = config.getEnv();
final String version = config.getVersion();
final int commentSize = capacity(traceParent, parentService, dbService, env, version);
final Map<String, String> customFields = SQLCommenterContext.getCopyOfContextMap();
final int commentSize =
capacity(traceParent, parentService, dbService, env, version, customFields);
StringBuilder sb = new StringBuilder(sql.length() + commentSize);
boolean commentAdded = false;
String peerService = peerServiceObj != null ? peerServiceObj.toString() : null;
Expand All @@ -137,7 +140,8 @@ public static String inject(
peerService,
env,
version,
traceParent);
traceParent,
customFields);
sb.append(CLOSE_COMMENT);
} else {
sb.append(OPEN_COMMENT);
Expand All @@ -152,7 +156,8 @@ public static String inject(
peerService,
env,
version,
traceParent);
traceParent,
customFields);

sb.append(CLOSE_COMMENT);
sb.append(SPACE);
Expand Down Expand Up @@ -226,7 +231,8 @@ protected static boolean toComment(
final String peerService,
final String env,
final String version,
final String traceparent) {
final String traceparent,
final Map<String, String> customFields) {
int emptySize = sb.length();

append(sb, PARENT_SERVICE, parentService, false);
Expand All @@ -241,6 +247,14 @@ protected static boolean toComment(
if (injectTrace) {
append(sb, TRACEPARENT, traceparent, sb.length() > emptySize);
}

// Add custom fields from SQLCommenterContext
if (customFields != null) {
for (Map.Entry<String, String> entry : customFields.entrySet()) {
append(sb, encode(entry.getKey()), entry.getValue(), sb.length() > emptySize);
}
}

return sb.length() > emptySize;
}

Expand Down Expand Up @@ -268,7 +282,8 @@ private static int capacity(
final String parentService,
final String dbService,
final String env,
final String version) {
final String version,
final Map<String, String> customFields) {
int len = INITIAL_CAPACITY;
if (null != traceparent) {
len += traceparent.length();
Expand All @@ -285,6 +300,18 @@ private static int capacity(
if (null != version) {
len += version.length();
}

// Add capacity for custom fields
if (customFields != null) {
for (Map.Entry<String, String> entry : customFields.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
len += entry.getKey().length();
len += entry.getValue().length();
len += 4; // equals, comma, and two quotes
}
}
}

return len;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package datadog.trace.instrumentation.jdbc;

import java.util.HashMap;
import java.util.Map;

/**
* Thread-local context system for custom SQL comment fields. This class provides functionality
* similar to SLF4J's MDC but specifically for SQL comment injection. External users can add custom
* key-value pairs that will be included in SQL comments.
*/
public class SQLCommenterContext {

private static final ThreadLocal<Map<String, String>> contextHolder =
new ThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue() {
return new HashMap<>();
}
};

/**
* Gets a copy of the current context map.
*
* @return a copy of the current context map, or null if no context is set
*/
public static Map<String, String> getCopyOfContextMap() {
Map<String, String> contextMap = contextHolder.get();
return contextMap.isEmpty() ? null : new HashMap<>(contextMap);
}

/**
* Sets the context map for the current thread.
*
* @param contextMap the new context map to set
*/
public static void setContextMap(Map<String, String> contextMap) {
if (contextMap == null) {
contextHolder.remove();
} else {
contextHolder.set(new HashMap<>(contextMap));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ public Map<String, String> contextStore() {

@Override
public String[] helperClassNames() {
return new String[] {packageName + ".JDBCDecorator", packageName + ".SQLCommenter"};
return new String[] {
packageName + ".JDBCDecorator",
packageName + ".SQLCommenter",
packageName + ".SQLCommenterContext"
};
}

// prepend mode will prepend the SQL comment to the raw sql query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer
import datadog.trace.bootstrap.instrumentation.api.Tags

import datadog.trace.instrumentation.jdbc.SQLCommenter
import datadog.trace.instrumentation.jdbc.SQLCommenterContext

import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace

Expand Down Expand Up @@ -145,4 +146,185 @@ class SQLCommenterTest extends AgentTestRunner {
"SELECT * FROM foo" | "SqlCommenter" | "Test" | "my-service" | "mysql" | "h" | "n" | "TestVersion" | true | true | "00-00000000000000007fffffffffffffff-000000024cb016ea-00" | "testPeer" | "SELECT * FROM foo /*ddps='SqlCommenter',dddbs='my-service',ddh='h',dddb='n',ddprs='testPeer',dde='Test',ddpv='TestVersion',traceparent='00-00000000000000007fffffffffffffff-000000024cb016ea-00'*/"
"SELECT * FROM foo" | "SqlCommenter" | "Test" | "my-service" | "postgres" | "h" | "n" | "TestVersion" | true | true | "00-00000000000000007fffffffffffffff-000000024cb016ea-00" | "testPeer" | "SELECT * FROM foo /*ddps='SqlCommenter',dddbs='my-service',ddh='h',dddb='n',ddprs='testPeer',dde='Test',ddpv='TestVersion',traceparent='00-00000000000000007fffffffffffffff-000000024cb016ea-00'*/"
}

def "test SQL comment injection with custom fields"() {
setup:
injectSysConfig("dd.service", "SqlCommenter")
injectSysConfig("dd.env", "Test")
injectSysConfig("dd.version", "TestVersion")

when:
String sqlWithComment = ""

// Set up custom fields in context
SQLCommenterContext.put("tenant_id", "abc123")
SQLCommenterContext.put("request_id", "req-456")
SQLCommenterContext.put("user_id", 789)

sqlWithComment = SQLCommenter.inject(
"SELECT * FROM foo",
"my-service",
"mysql",
"h",
"n",
"00-00000000000000007fffffffffffffff-000000024cb016ea-00",
true,
true
)

SQLCommenterContext.clear()

then:
// Should contain all standard fields plus custom fields
sqlWithComment.contains("ddps='SqlCommenter'")
sqlWithComment.contains("dddbs='my-service'")
sqlWithComment.contains("ddh='h'")
sqlWithComment.contains("dddb='n'")
sqlWithComment.contains("dde='Test'")
sqlWithComment.contains("ddpv='TestVersion'")
sqlWithComment.contains("traceparent='00-00000000000000007fffffffffffffff-000000024cb016ea-00'")
sqlWithComment.contains("tenant_id='abc123'")
sqlWithComment.contains("request_id='req-456'")
sqlWithComment.contains("user_id='789'")
sqlWithComment.startsWith("SELECT * FROM foo /*")
sqlWithComment.endsWith("*/")
}

def "test SQL comment injection with custom fields using withSQLCommentFields"() {
setup:
injectSysConfig("dd.service", "SqlCommenter")
injectSysConfig("dd.env", "Test")
injectSysConfig("dd.version", "TestVersion")

when:
String sqlWithComment = SQLCommenterContext.withSQLCommentFields(
["tenant_id": "abc123", "request_id": "req-456"], {
return SQLCommenter.inject(
"SELECT * FROM foo",
"my-service",
"mysql",
"h",
"n",
"00-00000000000000007fffffffffffffff-000000024cb016ea-00",
true,
true
)
}
)

then:
// Should contain all standard fields plus custom fields
sqlWithComment.contains("ddps='SqlCommenter'")
sqlWithComment.contains("dddbs='my-service'")
sqlWithComment.contains("tenant_id='abc123'")
sqlWithComment.contains("request_id='req-456'")
sqlWithComment.startsWith("SELECT * FROM foo /*")
sqlWithComment.endsWith("*/")

// Context should be cleared after the block
SQLCommenterContext.isEmpty()
}

def "test SQL comment injection with empty custom fields"() {
setup:
injectSysConfig("dd.service", "SqlCommenter")
injectSysConfig("dd.env", "Test")
injectSysConfig("dd.version", "TestVersion")

when:
String sqlWithComment = SQLCommenter.inject(
"SELECT * FROM foo",
"my-service",
"mysql",
"h",
"n",
"00-00000000000000007fffffffffffffff-000000024cb016ea-00",
true,
true
)

then:
// Should contain only standard fields
sqlWithComment.contains("ddps='SqlCommenter'")
sqlWithComment.contains("dddbs='my-service'")
sqlWithComment.contains("ddh='h'")
sqlWithComment.contains("dddb='n'")
sqlWithComment.contains("dde='Test'")
sqlWithComment.contains("ddpv='TestVersion'")
sqlWithComment.contains("traceparent='00-00000000000000007fffffffffffffff-000000024cb016ea-00'")
sqlWithComment.startsWith("SELECT * FROM foo /*")
sqlWithComment.endsWith("*/")
}

def "test SQL comment injection with custom fields containing special characters"() {
setup:
injectSysConfig("dd.service", "SqlCommenter")
injectSysConfig("dd.env", "Test")
injectSysConfig("dd.version", "TestVersion")

when:
String sqlWithComment = ""

// Set up custom fields with special characters that need URL encoding
SQLCommenterContext.put("special_key", "value with spaces")
SQLCommenterContext.put("unicode_key", "value_with_émojis_🎉")
SQLCommenterContext.put("symbols_key", "value&with=symbols")

sqlWithComment = SQLCommenter.inject(
"SELECT * FROM foo",
"my-service",
"mysql",
"h",
"n",
null,
false,
true
)

SQLCommenterContext.clear()

then:
// Should contain URL-encoded custom fields (URLEncoder uses + for spaces)
sqlWithComment.contains("special_key='value+with+spaces'")
sqlWithComment.contains("unicode_key=") // Should be URL encoded
sqlWithComment.contains("symbols_key='value%26with%3Dsymbols'")
sqlWithComment.startsWith("SELECT * FROM foo /*")
sqlWithComment.endsWith("*/")
}

def "test SQL comment injection with null and empty custom field values"() {
setup:
injectSysConfig("dd.service", "SqlCommenter")
injectSysConfig("dd.env", "Test")
injectSysConfig("dd.version", "TestVersion")

when:
String sqlWithComment = ""

// Set up custom fields with null and empty values
SQLCommenterContext.put("null_key", null)
SQLCommenterContext.put("empty_key", "")
SQLCommenterContext.put("valid_key", "valid_value")

sqlWithComment = SQLCommenter.inject(
"SELECT * FROM foo",
"my-service",
"mysql",
"h",
"n",
null,
false,
true
)

SQLCommenterContext.clear()

then:
// Should not contain null or empty fields, but should contain valid field
!sqlWithComment.contains("null_key")
!sqlWithComment.contains("empty_key")
sqlWithComment.contains("valid_key='valid_value'")
sqlWithComment.startsWith("SELECT * FROM foo /*")
sqlWithComment.endsWith("*/")
}
}
Loading