Skip to content

Commit 212e794

Browse files
committed
feat(observability): Add HTTP/gRPC response status attributes to tracing spans
1 parent 92f300a commit 212e794

File tree

6 files changed

+317
-13
lines changed

6 files changed

+317
-13
lines changed

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public class ObservabilityAttributes {
8585
/** The url template of the request (e.g. /v1/{name}:access). */
8686
public static final String URL_TEMPLATE_ATTRIBUTE = "url.template";
8787

88+
/** The HTTP status code of the request (e.g., 200, 404). */
89+
public static final String HTTP_RESPONSE_STATUS_ATTRIBUTE = "http.response.status_code";
90+
8891
/** The resend count of the request. Only used in HTTP transport. */
8992
public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count";
9093

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,20 @@
3939

4040
class ObservabilityUtils {
4141

42-
/** Function to extract the status of the error as a string */
42+
/** Function to extract the status of the error as a string (defaults to gRPC canonical codes). */
4343
static String extractStatus(@Nullable Throwable error) {
44-
final String statusString;
44+
return (String) extractStatus(error, ApiTracerContext.Transport.GRPC);
45+
}
46+
47+
static Object extractStatus(@Nullable Throwable error, ApiTracerContext.Transport transport) {
48+
if (transport == ApiTracerContext.Transport.HTTP) {
49+
return extractHttpStatus(error);
50+
}
51+
return extractGrpcStatus(error);
52+
}
4553

54+
private static String extractGrpcStatus(@Nullable Throwable error) {
55+
final String statusString;
4656
if (error == null) {
4757
return StatusCode.Code.OK.toString();
4858
} else if (error instanceof CancellationException) {
@@ -52,10 +62,41 @@ static String extractStatus(@Nullable Throwable error) {
5262
} else {
5363
statusString = StatusCode.Code.UNKNOWN.toString();
5464
}
55-
5665
return statusString;
5766
}
5867

68+
private static Long extractHttpStatus(@Nullable Throwable error) {
69+
if (error == null) {
70+
return 200L;
71+
} else if (error instanceof ApiException) {
72+
Object transportCode = ((ApiException) error).getStatusCode().getTransportCode();
73+
if (transportCode instanceof Integer) {
74+
return ((Integer) transportCode).longValue();
75+
} else {
76+
return (long) ((ApiException) error).getStatusCode().getCode().getHttpStatusCode();
77+
}
78+
} else {
79+
StatusCode.Code code = StatusCode.Code.UNKNOWN;
80+
if (error instanceof CancellationException) {
81+
code = StatusCode.Code.CANCELLED;
82+
}
83+
return (long) code.getHttpStatusCode();
84+
}
85+
}
86+
87+
static void populateStatusAttributes(
88+
Map<String, Object> attributes,
89+
@Nullable Throwable error,
90+
ApiTracerContext.Transport transport) {
91+
if (transport == ApiTracerContext.Transport.GRPC) {
92+
attributes.put(
93+
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
94+
} else if (transport == ApiTracerContext.Transport.HTTP) {
95+
attributes.put(
96+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
97+
}
98+
}
99+
59100
static Attributes toOtelAttributes(Map<String, Object> attributes) {
60101
AttributesBuilder attributesBuilder = Attributes.builder();
61102
if (attributes == null) {
@@ -69,6 +110,8 @@ static Attributes toOtelAttributes(Map<String, Object> attributes) {
69110
attributesBuilder.put(k, (Long) v);
70111
} else if (v instanceof Integer) {
71112
attributesBuilder.put(k, (long) (Integer) v);
113+
} else if (v instanceof Long) {
114+
attributesBuilder.put(k, (Long) v);
72115
}
73116
});
74117
return attributesBuilder.build();

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.opentelemetry.api.trace.Tracer;
3939
import java.util.HashMap;
4040
import java.util.Map;
41+
import java.util.concurrent.CancellationException;
4142

4243
/** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */
4344
@BetaApi
@@ -131,31 +132,44 @@ public void attemptStarted(Object request, int attemptNumber) {
131132

132133
@Override
133134
public void attemptSucceeded() {
134-
endAttempt();
135+
endAttempt(null);
135136
}
136137

137138
@Override
138139
public void attemptCancelled() {
139-
endAttempt();
140+
endAttempt(new CancellationException());
140141
}
141142

142143
@Override
143-
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
144-
endAttempt();
144+
public void attemptFailedRetriesExhausted(Throwable error) {
145+
endAttempt(error);
145146
}
146147

147148
@Override
148-
public void attemptFailedRetriesExhausted(Throwable error) {
149-
endAttempt();
149+
public void attemptPermanentFailure(Throwable error) {
150+
endAttempt(error);
150151
}
151152

152153
@Override
153-
public void attemptPermanentFailure(Throwable error) {
154-
endAttempt();
154+
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
155+
endAttempt(error);
155156
}
156157

157-
private void endAttempt() {
158+
@Override
159+
public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) {
160+
endAttempt(error);
161+
}
162+
163+
private void endAttempt(Throwable error) {
158164
if (attemptSpan != null) {
165+
Map<String, Object> endAttributes = new HashMap<>();
166+
ObservabilityUtils.populateStatusAttributes(
167+
endAttributes, error, this.apiTracerContext.transport());
168+
169+
if (!endAttributes.isEmpty()) {
170+
attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(endAttributes));
171+
}
172+
159173
attemptSpan.end();
160174
attemptSpan = null;
161175
}

sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,106 @@ void testToOtelAttributes_shouldMapIntAttributes() {
114114
.isEqualTo((long) attribute2Value);
115115
}
116116

117+
@Test
118+
void testPopulateStatusAttributes_grpc_success() {
119+
Map<String, Object> attributes = new java.util.HashMap<>();
120+
ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.GRPC);
121+
assertThat(attributes)
122+
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "OK");
123+
}
124+
125+
@Test
126+
void testPopulateStatusAttributes_grpc_apiException() {
127+
Map<String, Object> attributes = new java.util.HashMap<>();
128+
ApiException error =
129+
new ApiException("fake_error", null, new FakeStatusCode(StatusCode.Code.NOT_FOUND), false);
130+
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC);
131+
assertThat(attributes)
132+
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "NOT_FOUND");
133+
}
134+
135+
@Test
136+
void testPopulateStatusAttributes_grpc_cancellationException() {
137+
Map<String, Object> attributes = new java.util.HashMap<>();
138+
Throwable error = new java.util.concurrent.CancellationException();
139+
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC);
140+
assertThat(attributes)
141+
.containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "CANCELLED");
142+
}
143+
144+
@Test
145+
void testPopulateStatusAttributes_http_success() {
146+
Map<String, Object> attributes = new java.util.HashMap<>();
147+
ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.HTTP);
148+
assertThat(attributes)
149+
.containsEntry(
150+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
151+
(long) StatusCode.Code.OK.getHttpStatusCode());
152+
}
153+
154+
@Test
155+
void testPopulateStatusAttributes_http_apiExceptionWithIntegerTransportCode() {
156+
Map<String, Object> attributes = new java.util.HashMap<>();
157+
ApiException error =
158+
new ApiException(
159+
"fake_error",
160+
null,
161+
new com.google.api.gax.rpc.StatusCode() {
162+
@Override
163+
public Code getCode() {
164+
return Code.NOT_FOUND;
165+
}
166+
167+
@Override
168+
public Object getTransportCode() {
169+
return StatusCode.Code.NOT_FOUND.getHttpStatusCode();
170+
}
171+
},
172+
false);
173+
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
174+
assertThat(attributes)
175+
.containsEntry(
176+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
177+
(long) StatusCode.Code.NOT_FOUND.getHttpStatusCode());
178+
}
179+
180+
@Test
181+
void testPopulateStatusAttributes_http_apiExceptionWithNonIntegerTransportCode() {
182+
Map<String, Object> attributes = new java.util.HashMap<>();
183+
ApiException error =
184+
new ApiException(
185+
"fake_error",
186+
null,
187+
new com.google.api.gax.rpc.StatusCode() {
188+
@Override
189+
public Code getCode() {
190+
return Code.NOT_FOUND;
191+
}
192+
193+
@Override
194+
public Object getTransportCode() {
195+
return "Not Found";
196+
}
197+
},
198+
false);
199+
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
200+
assertThat(attributes)
201+
.containsEntry(
202+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
203+
(long) StatusCode.Code.NOT_FOUND.getHttpStatusCode());
204+
}
205+
206+
@Test
207+
void testPopulateStatusAttributes_http_cancellationException() {
208+
Map<String, Object> attributes = new java.util.HashMap<>();
209+
Throwable error = new java.util.concurrent.CancellationException();
210+
ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP);
211+
assertThat(attributes)
212+
.containsEntry(
213+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE,
214+
(long) StatusCode.Code.CANCELLED.getHttpStatusCode());
215+
}
216+
117217
@Test
118218
void testToOtelAttributes_shouldReturnEmptyAttributes_nullInput() {
119219
assertThat(ObservabilityUtils.toOtelAttributes(null)).isEqualTo(Attributes.empty());

0 commit comments

Comments
 (0)