Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 @@ -29,9 +29,11 @@
*/
package com.google.api.gax.tracing;

import static com.google.api.gax.tracing.ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE;
import static com.google.api.gax.tracing.ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE;

import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.tracing.ApiTracerContext.Transport;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
Expand All @@ -49,12 +51,14 @@ class GoldenSignalsMetricsTracer implements ApiTracer {
private final Stopwatch clientRequestTimer;
private final GoldenSignalsMetricsRecorder metricsRecorder;
private final Map<String, Object> attributes;
private final ApiTracerContext.Transport transport;

GoldenSignalsMetricsTracer(
GoldenSignalsMetricsRecorder metricsRecorder, ApiTracerContext apiTracerContext) {
this.clientRequestTimer = Stopwatch.createStarted();
this.metricsRecorder = metricsRecorder;
this.attributes = apiTracerContext.getMetricsAttributes();
this.transport = apiTracerContext.transport();
}

@VisibleForTesting
Expand All @@ -65,6 +69,7 @@ class GoldenSignalsMetricsTracer implements ApiTracer {
this.clientRequestTimer = Stopwatch.createStarted(ticker);
this.metricsRecorder = metricsRecorder;
this.attributes = new HashMap<>(apiTracerContext.getMetricsAttributes());
this.transport = apiTracerContext.transport();
}

/**
Expand All @@ -88,7 +93,13 @@ public void operationCancelled() {

@Override
public void operationFailed(Throwable error) {
attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
if (transport == Transport.HTTP) {
attributes.put(
HTTP_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error, transport));
} else {
attributes.put(
RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error, transport));
}
metricsRecorder.recordOperationLatency(
clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void operationFailed(Throwable error) {
if (operationFinished.getAndSet(true)) {
throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE);
}
attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
attributes.put(STATUS_ATTRIBUTE, (String) ObservabilityUtils.extractStatus(error));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid type casting. Either use different methods for extracting grpc and http statuses, or return a StatusCode.Code and get different status with different getters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. In this case we were already defaulting to the gRPC status code, so I'm using extractStatus(...).toString()

metricsRecorder.recordOperationLatency(
operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
metricsRecorder.recordOperationCount(1, attributes);
Expand Down Expand Up @@ -172,7 +172,7 @@ public void attemptCancelled() {
*/
@Override
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
attributes.put(STATUS_ATTRIBUTE, (String) ObservabilityUtils.extractStatus(error));
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
metricsRecorder.recordAttemptCount(1, attributes);
}
Expand All @@ -196,7 +196,7 @@ public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
*/
@Override
public void attemptFailedRetriesExhausted(Throwable error) {
attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
attributes.put(STATUS_ATTRIBUTE, (String) ObservabilityUtils.extractStatus(error));
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
metricsRecorder.recordAttemptCount(1, attributes);
}
Expand All @@ -210,7 +210,7 @@ public void attemptFailedRetriesExhausted(Throwable error) {
*/
@Override
public void attemptPermanentFailure(Throwable error) {
attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error));
attributes.put(STATUS_ATTRIBUTE, (String) ObservabilityUtils.extractStatus(error));
metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes);
metricsRecorder.recordAttemptCount(1, attributes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ public class ObservabilityAttributes {
/** Size of the response body in bytes. */
public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";

/** The HTTP status code of the request (e.g., 200, 404). */
public static final String HTTP_RESPONSE_STATUS_ATTRIBUTE = "http.response.status_code";

/** The resend count of the request. Only used in HTTP transport. */
public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@

final class ObservabilityUtils {

private ObservabilityUtils() {}
/** Function to extract the status of the error as a string (defaults to gRPC canonical codes). */
static String extractStatus(@Nullable Throwable error) {
return (String) extractStatus(error, ApiTracerContext.Transport.GRPC);
}

static Object extractStatus(@Nullable Throwable error, ApiTracerContext.Transport transport) {
if (transport == ApiTracerContext.Transport.HTTP) {
return extractHttpStatus(error);
}
return extractGrpcStatus(error);
}

/** Constant for redacted values. */
private static final String REDACTED_VALUE = "REDACTED";
Expand Down Expand Up @@ -150,15 +160,8 @@ private static String redactSensitiveQueryValues(final String rawQuery) {
return Joiner.on('&').join(redactedParams);
}

/**
* Function to extract the status of the error as a string.
*
* @param error the thrown throwable error
* @return the extracted status string
*/
static String extractStatus(@Nullable final Throwable error) {
private static String extractGrpcStatus(@Nullable Throwable error) {
final String statusString;

if (error == null) {
return StatusCode.Code.OK.toString();
} else if (error instanceof CancellationException) {
Expand All @@ -168,10 +171,45 @@ static String extractStatus(@Nullable final Throwable error) {
} else {
statusString = StatusCode.Code.UNKNOWN.toString();
}

return statusString;
}

private static Long extractHttpStatus(@Nullable Throwable error) {
if (error == null) {
return 200L;
} else if (error instanceof ApiException) {
Object transportCode = ((ApiException) error).getStatusCode().getTransportCode();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a getHttpStatusCode method. Can we use it directly?

For example, let extractStatus return StatusCode.code, and use toString for gRPC status and getHttpStatusCode for http status in the tracer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG, done.

// HttpJsonStatusCode.getTransportCode() returns an Integer (HTTP status code).
// GrpcStatusCode returns a Status.Code enum, and FakeStatusCode (in tests)
// returns
// a StatusCode.Code enum. If it's not an Integer, we fall back to the mapped
// HTTP status code of the canonical code.
if (transportCode instanceof Integer) {
return ((Integer) transportCode).longValue();
} else {
return (long) ((ApiException) error).getStatusCode().getCode().getHttpStatusCode();
}
}
StatusCode.Code code = StatusCode.Code.UNKNOWN;
if (error instanceof CancellationException) {
code = StatusCode.Code.CANCELLED;
}
return (long) code.getHttpStatusCode();
}

static void populateStatusAttributes(
Map<String, Object> attributes,
@Nullable Throwable error,
ApiTracerContext.Transport transport) {
if (transport == ApiTracerContext.Transport.GRPC) {
attributes.put(
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
} else if (transport == ApiTracerContext.Transport.HTTP) {
attributes.put(
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
}
}

/** Function to extract the ErrorInfo payload from the error, if available */
@Nullable
static ErrorInfo extractErrorInfo(@Nullable Throwable error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.opentelemetry.api.trace.Tracer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;

/** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */
@BetaApi
Expand Down Expand Up @@ -133,7 +134,7 @@ public void attemptStarted(Object request, int attemptNumber) {

@Override
public void attemptSucceeded() {
endAttempt();
endAttempt(null);
}

@Override
Expand Down Expand Up @@ -180,26 +181,34 @@ private long extractContentLength(java.util.Map<String, Object> headers) {

@Override
public void attemptCancelled() {
endAttempt();
endAttempt(new CancellationException());
}

@Override
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
endAttempt();
endAttempt(error);
}

@Override
public void attemptFailedRetriesExhausted(Throwable error) {
endAttempt();
endAttempt(error);
}

@Override
public void attemptPermanentFailure(Throwable error) {
endAttempt();
endAttempt(error);
}

private void endAttempt() {
private void endAttempt(Throwable error) {
if (attemptSpan != null) {
Map<String, Object> endAttributes = new HashMap<>();
ObservabilityUtils.populateStatusAttributes(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we plan to reuse populateStatusAttributes in GoldenSignalsMetricsTracer? Otherwise it seems a little redundant to have another layer. We can extract status and set it to the Span directly here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

populateStatusAttributes does the trick here too. Updated

endAttributes, error, this.apiTracerContext.transport());

if (!endAttributes.isEmpty()) {
attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(endAttributes));
}

attemptSpan.end();
attemptSpan = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,26 @@ void testExtractStatus_errorConversion_apiExceptions() {
ApiException error =
new ApiException(
"fake_error", null, new FakeStatusCode(StatusCode.Code.INVALID_ARGUMENT), false);
String errorCode = ObservabilityUtils.extractStatus(error);
String errorCode =
(String) ObservabilityUtils.extractStatus(error, ApiTracerContext.Transport.GRPC);
assertThat(errorCode).isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString());
}

@Test
void testExtractStatus_errorConversion_noError() {
// test "OK", which corresponds to a "null" error.
String successCode = ObservabilityUtils.extractStatus(null);
String successCode =
(String) ObservabilityUtils.extractStatus(null, ApiTracerContext.Transport.GRPC);
assertThat(successCode).isEqualTo(StatusCode.Code.OK.toString());
}

@Test
void testExtractStatus_errorConversion_unknownException() {
// test "UNKNOWN"
Throwable unknownException = new RuntimeException();
String errorCode2 = ObservabilityUtils.extractStatus(unknownException);
String errorCode2 =
(String)
ObservabilityUtils.extractStatus(unknownException, ApiTracerContext.Transport.GRPC);
assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN.toString());
}

Expand Down Expand Up @@ -114,6 +118,106 @@ void testToOtelAttributes_shouldMapIntAttributes() {
.isEqualTo((long) attribute2Value);
}

@Test
void testPopulateStatusAttributes_grpc_success() {
Map<String, Object> attributes = new java.util.HashMap<>();
ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.GRPC);
assertThat(attributes)
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "OK");
}

@Test
void testPopulateStatusAttributes_grpc_apiException() {
Map<String, Object> attributes = new java.util.HashMap<>();
ApiException error =
new ApiException("fake_error", null, new FakeStatusCode(StatusCode.Code.NOT_FOUND), false);
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC);
assertThat(attributes)
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "NOT_FOUND");
}

@Test
void testPopulateStatusAttributes_grpc_cancellationException() {
Map<String, Object> attributes = new java.util.HashMap<>();
Throwable error = new java.util.concurrent.CancellationException();
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC);
assertThat(attributes)
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "CANCELLED");
}

@Test
void testPopulateStatusAttributes_http_success() {
Map<String, Object> attributes = new java.util.HashMap<>();
ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.HTTP);
assertThat(attributes)
.containsEntry(
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
(long) StatusCode.Code.OK.getHttpStatusCode());
}

@Test
void testPopulateStatusAttributes_http_apiExceptionWithIntegerTransportCode() {
Map<String, Object> attributes = new java.util.HashMap<>();
ApiException error =
new ApiException(
"fake_error",
null,
new com.google.api.gax.rpc.StatusCode() {
@Override
public Code getCode() {
return Code.NOT_FOUND;
}

@Override
public Object getTransportCode() {
return StatusCode.Code.NOT_FOUND.getHttpStatusCode();
}
},
false);
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
assertThat(attributes)
.containsEntry(
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
(long) StatusCode.Code.NOT_FOUND.getHttpStatusCode());
}

@Test
void testPopulateStatusAttributes_http_apiExceptionWithNonIntegerTransportCode() {
Map<String, Object> attributes = new java.util.HashMap<>();
ApiException error =
new ApiException(
"fake_error",
null,
new com.google.api.gax.rpc.StatusCode() {
@Override
public Code getCode() {
return Code.NOT_FOUND;
}

@Override
public Object getTransportCode() {
return "Not Found";
}
},
false);
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
assertThat(attributes)
.containsEntry(
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
(long) StatusCode.Code.NOT_FOUND.getHttpStatusCode());
}

@Test
void testPopulateStatusAttributes_http_cancellationException() {
Map<String, Object> attributes = new java.util.HashMap<>();
Throwable error = new java.util.concurrent.CancellationException();
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
assertThat(attributes)
.containsEntry(
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
(long) StatusCode.Code.CANCELLED.getHttpStatusCode());
}

@Test
void testToOtelAttributes_shouldReturnEmptyAttributes_nullInput() {
assertThat(ObservabilityUtils.toOtelAttributes(null)).isEqualTo(Attributes.empty());
Expand Down
Loading
Loading