Skip to content

Commit a49d06b

Browse files
committed
impl(o11y): introduce http.response.status_code
1 parent dea24db commit a49d06b

File tree

6 files changed

+361
-9
lines changed

6 files changed

+361
-9
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
@@ -88,6 +88,9 @@ public class ObservabilityAttributes {
8888
/** Size of the response body in bytes. */
8989
public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
9090

91+
/** The HTTP status code of the request (e.g., 200, 404). */
92+
public static final String HTTP_RESPONSE_STATUS_ATTRIBUTE = "http.response.status_code";
93+
9194
/** The resend count of the request. Only used in HTTP transport. */
9295
public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count";
9396

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

Lines changed: 45 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,40 @@ 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+
}
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+
static void populateStatusAttributes(
87+
Map<String, Object> attributes,
88+
@Nullable Throwable error,
89+
ApiTracerContext.Transport transport) {
90+
if (transport == ApiTracerContext.Transport.GRPC) {
91+
attributes.put(
92+
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
93+
} else if (transport == ApiTracerContext.Transport.HTTP) {
94+
attributes.put(
95+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, extractStatus(error, transport));
96+
}
97+
}
98+
5999
static Attributes toOtelAttributes(Map<String, Object> attributes) {
60100
AttributesBuilder attributesBuilder = Attributes.builder();
61101
if (attributes == null) {
@@ -69,6 +109,8 @@ static Attributes toOtelAttributes(Map<String, Object> attributes) {
69109
attributesBuilder.put(k, (Long) v);
70110
} else if (v instanceof Integer) {
71111
attributesBuilder.put(k, (long) (Integer) v);
112+
} else if (v instanceof Long) {
113+
attributesBuilder.put(k, (Long) v);
72114
}
73115
});
74116
return attributesBuilder.build();

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@
3636
import io.opentelemetry.api.trace.SpanBuilder;
3737
import io.opentelemetry.api.trace.SpanKind;
3838
import io.opentelemetry.api.trace.Tracer;
39+
40+
import java.time.Duration;
3941
import java.util.HashMap;
4042
import java.util.Map;
43+
import java.util.concurrent.CancellationException;
4144

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

134137
@Override
135138
public void attemptSucceeded() {
136-
endAttempt();
139+
endAttempt(null);
137140
}
138141

139142
@Override
@@ -180,26 +183,34 @@ private long extractContentLength(java.util.Map<String, Object> headers) {
180183

181184
@Override
182185
public void attemptCancelled() {
183-
endAttempt();
186+
endAttempt(new CancellationException());
184187
}
185188

186189
@Override
187190
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
188-
endAttempt();
191+
endAttempt(error);
189192
}
190193

191194
@Override
192195
public void attemptFailedRetriesExhausted(Throwable error) {
193-
endAttempt();
196+
endAttempt(error);
194197
}
195198

196199
@Override
197200
public void attemptPermanentFailure(Throwable error) {
198-
endAttempt();
201+
endAttempt(error);
199202
}
200203

201-
private void endAttempt() {
204+
private void endAttempt(Throwable error) {
202205
if (attemptSpan != null) {
206+
Map<String, Object> endAttributes = new HashMap<>();
207+
ObservabilityUtils.populateStatusAttributes(
208+
endAttributes, error, this.apiTracerContext.transport());
209+
210+
if (!endAttributes.isEmpty()) {
211+
attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(endAttributes));
212+
}
213+
203214
attemptSpan.end();
204215
attemptSpan = null;
205216
}

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());

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,52 @@ void testAttemptStarted_includesLanguageAttribute() {
8787
SpanTracer.DEFAULT_LANGUAGE);
8888
}
8989

90+
@Test
91+
void testAttemptSucceeded_grpc() {
92+
ApiTracerContext context =
93+
ApiTracerContext.newBuilder()
94+
.setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty())
95+
.setTransport(ApiTracerContext.Transport.GRPC)
96+
.build();
97+
spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME);
98+
99+
spanTracer.attemptStarted(new Object(), 1);
100+
spanTracer.attemptSucceeded();
101+
102+
ArgumentCaptor<Attributes> attrsCaptor = ArgumentCaptor.forClass(Attributes.class);
103+
verify(span).setAllAttributes(attrsCaptor.capture());
104+
verify(span).end();
105+
106+
assertThat(attrsCaptor.getValue().asMap())
107+
.containsEntry(
108+
io.opentelemetry.api.common.AttributeKey.stringKey(
109+
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE),
110+
"OK");
111+
}
112+
113+
@Test
114+
void testAttemptSucceeded_http() {
115+
ApiTracerContext context =
116+
ApiTracerContext.newBuilder()
117+
.setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty())
118+
.setTransport(ApiTracerContext.Transport.HTTP)
119+
.build();
120+
spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME);
121+
122+
spanTracer.attemptStarted(new Object(), 1);
123+
spanTracer.attemptSucceeded();
124+
125+
ArgumentCaptor<Attributes> attrsCaptor = ArgumentCaptor.forClass(Attributes.class);
126+
verify(span).setAllAttributes(attrsCaptor.capture());
127+
verify(span).end();
128+
129+
assertThat(attrsCaptor.getValue().asMap())
130+
.containsEntry(
131+
io.opentelemetry.api.common.AttributeKey.longKey(
132+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE),
133+
200L);
134+
}
135+
90136
@Test
91137
void testResponseHeadersReceived_setsContentLengthAttribute() {
92138
spanTracer.attemptStarted(new Object(), 1);
@@ -170,6 +216,46 @@ void testAttemptStarted_noRetryAttributes_grpc() {
170216
ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE));
171217
}
172218

219+
@Test
220+
void testAttemptFailed_grpc() {
221+
ApiTracerContext context =
222+
ApiTracerContext.newBuilder()
223+
.setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty())
224+
.setTransport(ApiTracerContext.Transport.GRPC)
225+
.build();
226+
spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME);
227+
228+
com.google.api.gax.rpc.ApiException exception =
229+
new com.google.api.gax.rpc.ApiException(
230+
"error",
231+
null,
232+
new com.google.api.gax.rpc.StatusCode() {
233+
@Override
234+
public Code getCode() {
235+
return Code.NOT_FOUND;
236+
}
237+
238+
@Override
239+
public Object getTransportCode() {
240+
return null;
241+
}
242+
},
243+
false);
244+
245+
spanTracer.attemptStarted(new Object(), 1);
246+
spanTracer.attemptFailedRetriesExhausted(exception);
247+
248+
ArgumentCaptor<Attributes> attrsCaptor = ArgumentCaptor.forClass(Attributes.class);
249+
verify(span).setAllAttributes(attrsCaptor.capture());
250+
verify(span).end();
251+
252+
assertThat(attrsCaptor.getValue().asMap())
253+
.containsEntry(
254+
io.opentelemetry.api.common.AttributeKey.stringKey(
255+
ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE),
256+
"NOT_FOUND");
257+
}
258+
173259
@Test
174260
void testAttemptStarted_retryAttributes_grpc() {
175261
ApiTracerContext grpcContext =
@@ -196,6 +282,46 @@ void testAttemptStarted_retryAttributes_grpc() {
196282
ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE));
197283
}
198284

285+
@Test
286+
void testAttemptFailed_http() {
287+
ApiTracerContext context =
288+
ApiTracerContext.newBuilder()
289+
.setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty())
290+
.setTransport(ApiTracerContext.Transport.HTTP)
291+
.build();
292+
spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME);
293+
294+
com.google.api.gax.rpc.ApiException exception =
295+
new com.google.api.gax.rpc.ApiException(
296+
"error",
297+
null,
298+
new com.google.api.gax.rpc.StatusCode() {
299+
@Override
300+
public Code getCode() {
301+
return Code.NOT_FOUND;
302+
}
303+
304+
@Override
305+
public Object getTransportCode() {
306+
return 404;
307+
}
308+
},
309+
false);
310+
311+
spanTracer.attemptStarted(new Object(), 1);
312+
spanTracer.attemptFailedRetriesExhausted(exception);
313+
314+
ArgumentCaptor<Attributes> attrsCaptor = ArgumentCaptor.forClass(Attributes.class);
315+
verify(span).setAllAttributes(attrsCaptor.capture());
316+
verify(span).end();
317+
318+
assertThat(attrsCaptor.getValue().asMap())
319+
.containsEntry(
320+
io.opentelemetry.api.common.AttributeKey.longKey(
321+
ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE),
322+
404L);
323+
}
324+
199325
@Test
200326
void testAttemptStarted_noRetryAttributes_http() {
201327
ApiTracerContext httpContext =

0 commit comments

Comments
 (0)