Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
634f70e
Initial plan
Copilot Oct 24, 2026
c6d9ddd
Add separate database response status extraction API
Copilot Oct 24, 2026
28a8812
Expose database response codes in scoped getters and span tests
Copilot Oct 24, 2026
8e51559
Remove temporary validation logs from database status changes
Copilot Oct 24, 2026
6890aef
Exclude client-only database statuses while preserving error classifi…
Copilot Oct 24, 2026
65e3de5
Fix database failure status extraction gaps
Copilot Oct 25, 2026
f428daa
Preserve OpenSearch error type on failed responses
Copilot Oct 25, 2026
c0e60f5
Separate database status extraction from classification
Copilot Oct 25, 2026
002d1b8
Decouple database response status extraction
Copilot Oct 25, 2026
6e15239
Clarify OpenSearch status precedence test
Copilot Oct 25, 2026
009edff
Exclude PostgreSQL client-local response codes
Copilot Oct 25, 2026
8e883d9
Emit spymemcached completion statuses
Copilot Oct 26, 2026
52fdc98
Emit successful REST response statuses
Copilot Oct 26, 2026
e0127cb
Prefer available SQL vendor response codes
Copilot Oct 26, 2026
6425827
Preserve Lettuce server error statuses
Copilot Oct 26, 2026
5b3f5d9
Fix Vert.x MySQL response status extraction
Copilot Oct 26, 2026
52f339e
Prefer SQL vendor response codes
Copilot Oct 26, 2026
76abcaa
Handle generic SQL response status fallbacks
Copilot Oct 26, 2026
3fa0a27
Include client-provided database statuses
Copilot Oct 26, 2026
a79ef06
Capture available database response codes
Copilot Oct 27, 2026
bc5b6bd
Capture all available MongoDB status codes
Copilot Oct 27, 2026
2d2b47c
Fix database response status review findings
Copilot Oct 27, 2026
832c1ec
Rename shared database status values
Copilot Oct 27, 2026
783b644
Inline ClickHouse error code extraction
Copilot Oct 27, 2026
c400836
Reconcile spymemcached assertions after rebase
Copilot Oct 27, 2026
6b42199
Expect Lettuce auth setup status codes
Copilot Oct 29, 2026
f7584c3
Condition database response status attributes
Copilot Oct 29, 2026
bf89796
Capture JDBC warning response status codes
Copilot Oct 30, 2026
e358633
Handle JDBC warning read failures safely
Copilot Oct 30, 2026
08054f7
Keep extracted response codes on database metrics
Copilot Oct 30, 2026
9b5f59c
Add real MySQL JDBC warning lifecycle tests
Copilot Oct 30, 2026
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 @@ -11,6 +11,7 @@
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_TEXT;
import static io.opentelemetry.semconv.DbAttributes.DB_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;

Expand Down Expand Up @@ -122,6 +123,8 @@ static <REQUEST, RESPONSE> void onEndCommon(
REQUEST request,
@Nullable RESPONSE response,
@Nullable Throwable error) {
attributes.put(
DB_RESPONSE_STATUS_CODE, getter.getDbResponseStatusCode(request, response, error));
Comment thread
trask marked this conversation as resolved.
Resolved
Comment thread
trask marked this conversation as resolved.
Resolved
String errorType = getter.getErrorType(request, response, error);
// fall back to exception class name
if (errorType == null && error != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ default Map<String, String> getDbQueryParameters(REQUEST request) {
return emptyMap();
}

/**
* Returns an available response status code supplied by the database or client library.
*
* <p>Return {@code null} when no response status code is available, including errors represented
* only by an exception class name or a fallback error type.
*/
@Nullable
default String getDbResponseStatusCode(
REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) {
return null;
}

/**
* Returns a description of a class of error the operation ended with.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) {
}

Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build();

duration.record((endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static io.opentelemetry.semconv.DbAttributes.DB_NAMESPACE;
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY;
import static io.opentelemetry.semconv.DbAttributes.DB_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
Expand Down Expand Up @@ -39,6 +40,7 @@ static void applyClientDurationAdvice(DoubleHistogramBuilder builder) {
DB_NAMESPACE,
DB_OPERATION_NAME,
DB_QUERY_SUMMARY,
DB_RESPONSE_STATUS_CODE,
Comment thread
trask marked this conversation as resolved.
Resolved
ERROR_TYPE,
NETWORK_PEER_ADDRESS,
NETWORK_PEER_PORT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_TEXT;
import static io.opentelemetry.semconv.DbAttributes.DB_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
Expand Down Expand Up @@ -188,4 +189,69 @@ void shouldUseExceptionClassWhenErrorTypeIsUnavailable() {
assertThat(attributes.build())
.containsOnly(entry(ERROR_TYPE, IllegalStateException.class.getName()));
}

@ParameterizedTest
@ValueSource(strings = {"42601", "custom"})
void shouldExtractResponseStatusIndependentlyOfErrorType(String errorType) {
AttributesExtractor<Map<String, String>, Void> extractor =
DbClientAttributesExtractor.create(
new TestAttributesGetter() {
@Override
public String getDbResponseStatusCode(
Map<String, String> request, Void response, Throwable error) {
return "42601";
}

@Override
public String getErrorType(
Map<String, String> request, Void response, Throwable error) {
return errorType;
}
});
AttributesBuilder attributes = Attributes.builder();

extractor.onEnd(attributes, Context.root(), emptyMap(), null, null);

assertThat(attributes.build())
.containsOnly(entry(DB_RESPONSE_STATUS_CODE, "42601"), entry(ERROR_TYPE, errorType));
}

@Test
void shouldPreserveExceptionFallbackWithResponseStatus() {
AttributesExtractor<Map<String, String>, Void> extractor =
DbClientAttributesExtractor.create(
new TestAttributesGetter() {
@Override
public String getDbResponseStatusCode(
Map<String, String> request, Void response, Throwable error) {
return "42601";
}
});
AttributesBuilder attributes = Attributes.builder();

extractor.onEnd(attributes, Context.root(), emptyMap(), null, new IllegalStateException());

assertThat(attributes.build())
.containsOnly(
entry(DB_RESPONSE_STATUS_CODE, "42601"),
entry(ERROR_TYPE, IllegalStateException.class.getName()));
}

@Test
void shouldNotUseFallbackErrorTypeAsResponseStatus() {
AttributesExtractor<Map<String, String>, Void> extractor =
DbClientAttributesExtractor.create(
new TestAttributesGetter() {
@Override
public String getErrorType(
Map<String, String> request, Void response, Throwable error) {
return "_OTHER";
}
});
AttributesBuilder attributes = Attributes.builder();

extractor.onEnd(attributes, Context.root(), emptyMap(), null, null);

assertThat(attributes.build()).containsOnly(entry(ERROR_TYPE, "_OTHER"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.opentelemetry.semconv.DbAttributes.DB_NAMESPACE;
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY;
import static io.opentelemetry.semconv.DbAttributes.DB_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
Expand Down Expand Up @@ -61,6 +62,7 @@ void collectsMetrics() {
Attributes responseAttributes =
Attributes.builder()
.put(ERROR_TYPE, "400")
.put(DB_RESPONSE_STATUS_CODE, "400")
.put(NETWORK_PEER_ADDRESS, "1.2.3.4")
.put(NETWORK_PEER_PORT, 8080)
.build();
Expand Down Expand Up @@ -94,7 +96,7 @@ void collectsMetrics() {
point ->
point
.hasSum(0.15 /* seconds */)
.hasAttributesSatisfying(
.hasAttributesSatisfyingExactly(
equalTo(DB_SYSTEM_NAME, "myDb"),
equalTo(DB_NAMESPACE, "potatoes"),
equalTo(DB_OPERATION_NAME, "SELECT"),
Expand All @@ -104,6 +106,7 @@ void collectsMetrics() {
SERVER_ADDRESS,
"db1.example:5432,db2.example:5432"),
equalTo(ERROR_TYPE, "400"),
equalTo(DB_RESPONSE_STATUS_CODE, "400"),
equalTo(NETWORK_PEER_ADDRESS, "1.2.3.4"),
equalTo(NETWORK_PEER_PORT, 8080))
.hasExemplarsSatisfying(
Expand All @@ -114,6 +117,81 @@ void collectsMetrics() {
.hasBucketBoundaries(DURATION_BUCKETS))));
}

@Test
void responseStatusCodeIsRecordedWhenAvailable() {
InMemoryMetricReader metricReader = InMemoryMetricReader.create();
SdkMeterProvider meterProvider =
SdkMeterProvider.builder().registerMetricReader(metricReader).build();
cleanup.deferCleanup(meterProvider);

OperationListener listener = DbClientMetrics.get().create(meterProvider.get("test"));

record(
listener,
"failedWithCode",
Attributes.builder().put(ERROR_TYPE, "400").put(DB_RESPONSE_STATUS_CODE, "400").build());
record(listener, "failedWithoutCode", Attributes.of(ERROR_TYPE, "io.example.DbException"));
record(listener, "successfulWithCode", Attributes.of(DB_RESPONSE_STATUS_CODE, "200"));
record(listener, "successfulWithoutCode", Attributes.empty());

assertThat(metricReader.collectAllMetrics())
.satisfiesExactly(
metric -> {
assertThat(metric).hasName("db.client.operation.duration");
assertThat(metric.getHistogramData().getPoints())
.satisfiesExactlyInAnyOrder(
point ->
assertThat(point.getAttributes().asMap())
.isEqualTo(
Attributes.builder()
.put(DB_SYSTEM_NAME, "myDb")
.put(DB_OPERATION_NAME, "failedWithCode")
.put(ERROR_TYPE, "400")
.put(DB_RESPONSE_STATUS_CODE, "400")
.build()
.asMap()),
point ->
assertThat(point.getAttributes().asMap())
.isEqualTo(
Attributes.builder()
.put(DB_SYSTEM_NAME, "myDb")
.put(DB_OPERATION_NAME, "failedWithoutCode")
.put(ERROR_TYPE, "io.example.DbException")
.build()
.asMap()),
point ->
assertThat(point.getAttributes().asMap())
.isEqualTo(
Attributes.builder()
.put(DB_SYSTEM_NAME, "myDb")
.put(DB_OPERATION_NAME, "successfulWithCode")
.put(DB_RESPONSE_STATUS_CODE, "200")
.build()
.asMap()),
point ->
assertThat(point.getAttributes().asMap())
.isEqualTo(
Attributes.builder()
.put(DB_SYSTEM_NAME, "myDb")
.put(DB_OPERATION_NAME, "successfulWithoutCode")
.build()
.asMap()));
});
}

private static void record(
OperationListener listener, String operationName, Attributes responseAttributes) {
Context context =
listener.onStart(
Context.root(),
Attributes.builder()
.put(DB_SYSTEM_NAME, "myDb")
.put(DB_OPERATION_NAME, operationName)
.build(),
nanos(100));
listener.onEnd(context, responseAttributes, nanos(250));
}

private static long nanos(int millis) {
return MILLISECONDS.toNanos(millis);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY;
import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_TEXT;
import static io.opentelemetry.semconv.DbAttributes.DB_RESPONSE_STATUS_CODE;
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT;
import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_QUERY_PARAMETER;
Expand All @@ -38,6 +40,31 @@

class SqlClientAttributesExtractorTest {

@Test
void shouldExtractResponseStatusAndErrorType() {
AttributesExtractor<Map<String, Object>, Void> extractor =
SqlClientAttributesExtractor.create(
new TestAttributesGetter() {
@Override
public String getDbResponseStatusCode(
Map<String, Object> request, Void response, Throwable error) {
return "42601";
}

@Override
public String getErrorType(
Map<String, Object> request, Void response, Throwable error) {
return "42601";
}
});
AttributesBuilder attributes = Attributes.builder();

extractor.onEnd(attributes, Context.root(), emptyMap(), null, new IllegalStateException());

assertThat(attributes.build())
.containsOnly(entry(DB_RESPONSE_STATUS_CODE, "42601"), entry(ERROR_TYPE, "42601"));
}

@Test
void shouldRetainNetworkPeerWithoutProtocolAttributes() {
AttributesExtractor<Map<String, Object>, Void> extractor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,26 @@ public Long getDbOperationBatchSize(ExecutionAttributes executionAttributes) {
return extractBatchSize(operation, executionAttributes);
}

@Override
@Nullable
public String getDbResponseStatusCode(
ExecutionAttributes executionAttributes,
@Nullable Response response,
@Nullable Throwable error) {
return getServiceErrorCode(error);
}

@Override
@Nullable
public String getErrorType(
ExecutionAttributes executionAttributes,
@Nullable Response response,
@Nullable Throwable error) {
return getServiceErrorCode(error);
}

@Nullable
private static String getServiceErrorCode(@Nullable Throwable error) {
if (!(error instanceof AwsServiceException)) {
return null;
}
Expand Down
Loading
Loading