-
Notifications
You must be signed in to change notification settings - Fork 40
feat: Add W3C Trace Context support for distributed tracing #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,146 @@ | ||
| package com.databricks.jdbc.dbclient.impl.common; | ||
|
|
||
| /** Utility class to support request tracing */ | ||
| import java.security.SecureRandom; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** Utility class to support W3C Trace Context request tracing */ | ||
| public final class TracingUtil { | ||
|
|
||
| public static final String TRACE_HEADER = "traceparent"; | ||
| public static final String TRACE_STATE_HEADER = "tracestate"; | ||
|
|
||
| private static final String SEED_CHARACTERS = "0123456789abcdef"; | ||
| private static final int SEED_CHARACTERS_LENGTH = SEED_CHARACTERS.length(); | ||
| private static final SecureRandom SECURE_RANDOM = new SecureRandom(); | ||
|
|
||
| // W3C Trace Context format: version-trace-id-parent-id-trace-flags | ||
| private static final Pattern TRACEPARENT_PATTERN = | ||
| Pattern.compile("^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$"); | ||
|
|
||
| private static final String VERSION = "00"; | ||
| private static final String DEFAULT_FLAGS = "01"; // sampled | ||
|
|
||
| private TracingUtil() { | ||
| // Utility class | ||
| } | ||
|
|
||
| /** | ||
| * Validates a W3C traceparent header format. | ||
| * | ||
| * @param traceparent The traceparent header to validate | ||
| * @return true if valid, false otherwise | ||
| */ | ||
| public static boolean isValidTraceparent(String traceparent) { | ||
| if (traceparent == null || traceparent.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| Matcher matcher = TRACEPARENT_PATTERN.matcher(traceparent.toLowerCase()); | ||
| if (!matcher.matches()) { | ||
| return false; | ||
| } | ||
|
|
||
| // Only support version 00 for now | ||
| String version = matcher.group(1); | ||
| return VERSION.equals(version); | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the trace ID from a valid traceparent header. | ||
| * | ||
| * @param traceparent The traceparent header | ||
| * @return The trace ID or null if invalid | ||
| */ | ||
| public static String extractTraceId(String traceparent) { | ||
| if (!isValidTraceparent(traceparent)) { | ||
| return null; | ||
| } | ||
|
|
||
| public static String getTraceHeader() { | ||
| // Construct the string with the specified format | ||
| return String.format("00-%s-%s-01", randomSegment(32), randomSegment(16)); | ||
| Matcher matcher = TRACEPARENT_PATTERN.matcher(traceparent.toLowerCase()); | ||
| if (matcher.matches()) { | ||
| return matcher.group(2); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the trace flags from a valid traceparent header. | ||
| * | ||
| * @param traceparent The traceparent header | ||
| * @return The trace flags or null if invalid | ||
| */ | ||
| public static String extractTraceFlags(String traceparent) { | ||
| if (!isValidTraceparent(traceparent)) { | ||
| return null; | ||
| } | ||
|
|
||
| Matcher matcher = TRACEPARENT_PATTERN.matcher(traceparent.toLowerCase()); | ||
| if (matcher.matches()) { | ||
| return matcher.group(4); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Generates a new trace ID. | ||
| * | ||
| * @return A 32-character hex trace ID | ||
| */ | ||
| public static String generateTraceId() { | ||
| return randomSegment(32); | ||
| } | ||
|
|
||
| /** | ||
| * Generates a new span ID. | ||
| * | ||
| * @return A 16-character hex span ID | ||
| */ | ||
| public static String generateSpanId() { | ||
| return randomSegment(16); | ||
| } | ||
|
|
||
| /** | ||
| * Builds a W3C compliant traceparent header. | ||
| * | ||
| * @param traceId The trace ID (32 hex characters) | ||
| * @param spanId The span ID (16 hex characters) | ||
| * @param traceFlags The trace flags (2 hex characters) | ||
| * @return The formatted traceparent header | ||
| */ | ||
| public static String buildTraceparent(String traceId, String spanId, String traceFlags) { | ||
| return String.format("%s-%s-%s-%s", VERSION, traceId, spanId, traceFlags); | ||
| } | ||
|
|
||
| /** | ||
| * Generates a complete traceparent header with new IDs. | ||
| * | ||
| * @return A new traceparent header | ||
| */ | ||
| public static String generateTraceparent() { | ||
| return buildTraceparent(generateTraceId(), generateSpanId(), DEFAULT_FLAGS); | ||
| } | ||
|
|
||
| /** | ||
| * Generates a traceparent header with the given trace ID and flags, and a new span ID. | ||
| * | ||
| * @param traceId The trace ID to use | ||
| * @param traceFlags The trace flags to use | ||
| * @return A traceparent header with new span ID | ||
| */ | ||
| public static String generateTraceparentWithTraceId(String traceId, String traceFlags) { | ||
| return buildTraceparent(traceId, generateSpanId(), traceFlags); | ||
| } | ||
|
|
||
| private static String randomSegment(int length) { | ||
| StringBuilder result = new StringBuilder(); | ||
| for (int i = 0; i < length; i++) { | ||
| result.append( | ||
| SEED_CHARACTERS.charAt((int) Math.floor(Math.random() * SEED_CHARACTERS_LENGTH))); | ||
| StringBuilder result = new StringBuilder(length); | ||
| byte[] bytes = new byte[length / 2]; | ||
| SECURE_RANDOM.nextBytes(bytes); | ||
|
|
||
| for (byte b : bytes) { | ||
| result.append(SEED_CHARACTERS.charAt((b >> 4) & 0xF)); | ||
| result.append(SEED_CHARACTERS.charAt(b & 0xF)); | ||
| } | ||
|
|
||
| return result.toString(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import com.databricks.jdbc.common.util.UserAgentManager; | ||
| import com.databricks.jdbc.dbclient.IDatabricksHttpClient; | ||
| import com.databricks.jdbc.dbclient.impl.common.ConfiguratorUtils; | ||
| import com.databricks.jdbc.dbclient.impl.common.TracingUtil; | ||
| import com.databricks.jdbc.exception.DatabricksDriverException; | ||
| import com.databricks.jdbc.exception.DatabricksHttpException; | ||
| import com.databricks.jdbc.exception.DatabricksRetryHandlerException; | ||
|
|
@@ -51,8 +52,10 @@ public class DatabricksHttpClient implements IDatabricksHttpClient, Closeable { | |
| private final CloseableHttpClient httpClient; | ||
| private IdleConnectionEvictor idleConnectionEvictor; | ||
| private CloseableHttpAsyncClient asyncClient; | ||
| private final IDatabricksConnectionContext connectionContext; | ||
|
|
||
| DatabricksHttpClient(IDatabricksConnectionContext connectionContext, HttpClientType type) { | ||
| this.connectionContext = connectionContext; | ||
| connectionManager = initializeConnectionManager(connectionContext); | ||
| httpClient = makeClosableHttpClient(connectionContext, type); | ||
| idleConnectionEvictor = | ||
|
|
@@ -65,7 +68,9 @@ public class DatabricksHttpClient implements IDatabricksHttpClient, Closeable { | |
| @VisibleForTesting | ||
| DatabricksHttpClient( | ||
| CloseableHttpClient testCloseableHttpClient, | ||
| PoolingHttpClientConnectionManager testConnectionManager) { | ||
| PoolingHttpClientConnectionManager testConnectionManager, | ||
| IDatabricksConnectionContext connectionContext) { | ||
| this.connectionContext = connectionContext; | ||
| httpClient = testCloseableHttpClient; | ||
| connectionManager = testConnectionManager; | ||
| } | ||
|
|
@@ -88,6 +93,22 @@ public CloseableHttpResponse execute(HttpUriRequest request, boolean supportGzip | |
| if (!isNullOrEmpty(userAgentString) && !request.containsHeader("User-Agent")) { | ||
| request.setHeader("User-Agent", userAgentString); | ||
| } | ||
|
|
||
| // Add W3C Trace Context headers if request tracing is enabled | ||
| if (connectionContext.isRequestTracingEnabled() && connectionContext.getTraceId() != null) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:empty check |
||
| String traceHeader = | ||
| TracingUtil.generateTraceparentWithTraceId( | ||
| connectionContext.getTraceId(), connectionContext.getTraceFlags()); | ||
| request.setHeader(TracingUtil.TRACE_HEADER, traceHeader); | ||
| LOGGER.debug("Added trace header: {}", traceHeader); | ||
|
|
||
| // Add tracestate if present | ||
| String traceState = connectionContext.getTraceState(); | ||
| if (traceState != null && !traceState.isEmpty()) { | ||
| request.setHeader(TracingUtil.TRACE_STATE_HEADER, traceState); | ||
| } | ||
| } | ||
|
|
||
| return httpClient.execute(request); | ||
| } catch (IOException e) { | ||
| throwHttpException(e, request); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -450,10 +450,18 @@ private boolean useCloudFetchForResult(StatementType statementType) { | |
|
|
||
| private Map<String, String> getHeaders(String method) { | ||
| Map<String, String> headers = new HashMap<>(JSON_HTTP_HEADERS); | ||
| if (connectionContext.isRequestTracingEnabled()) { | ||
| String traceHeader = TracingUtil.getTraceHeader(); | ||
| if (connectionContext.isRequestTracingEnabled() && connectionContext.getTraceId() != null) { | ||
| String traceHeader = | ||
| TracingUtil.generateTraceparentWithTraceId( | ||
| connectionContext.getTraceId(), connectionContext.getTraceFlags()); | ||
| LOGGER.debug("Tracing header for method {}: [{}]", method, traceHeader); | ||
| headers.put(TracingUtil.TRACE_HEADER, traceHeader); | ||
|
|
||
| // Add tracestate if present | ||
| String traceState = connectionContext.getTraceState(); | ||
| if (traceState != null && !traceState.isEmpty()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can use !Strings.isNullOrEmpty |
||
| headers.put(TracingUtil.TRACE_STATE_HEADER, traceState); | ||
| } | ||
| } | ||
|
|
||
| // Overriding with URL defined headers | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,11 +110,18 @@ public void flush() throws TTransportException { | |
| // Overriding with URL defined headers | ||
| this.connectionContext.getCustomHeaders().forEach(request::setHeader); | ||
|
|
||
| if (connectionContext.isRequestTracingEnabled()) { | ||
| String traceHeader = TracingUtil.getTraceHeader(); | ||
| if (connectionContext.isRequestTracingEnabled() && connectionContext.getTraceId() != null) { | ||
| String traceHeader = | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we check for empty also? |
||
| TracingUtil.generateTraceparentWithTraceId( | ||
| connectionContext.getTraceId(), connectionContext.getTraceFlags()); | ||
| LOGGER.debug("Thrift tracing header: " + traceHeader); | ||
|
|
||
| request.addHeader(TracingUtil.TRACE_HEADER, traceHeader); | ||
|
|
||
| // Add tracestate if present | ||
| String traceState = connectionContext.getTraceState(); | ||
| if (traceState != null && !traceState.isEmpty()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can use Strings.isNullOrEmpty |
||
| request.addHeader(TracingUtil.TRACE_STATE_HEADER, traceState); | ||
| } | ||
| } | ||
|
|
||
| // Set the request entity | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nullOrEmpty else-case, why are we checking again?