Skip to content

Commit 26827d9

Browse files
committed
test(o11y): add showcase test for golden signals metrics
Added `ITOtelGoldenMetrics` alongside `ITOtelTracing` to ensure that Golden Signals are correctly collected with accurate dimensional attributes (e.g., service name, status code, error type, port, addressing) across both the gRPC and HTTP/JSON runtime transports.
1 parent f6b2b66 commit 26827d9

File tree

1 file changed

+377
-0
lines changed

1 file changed

+377
-0
lines changed
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.showcase.v1beta1.it;
32+
33+
import static com.google.common.truth.Truth.assertThat;
34+
import static org.junit.Assert.assertThrows;
35+
36+
import com.google.api.client.http.HttpTransport;
37+
import com.google.api.gax.core.NoCredentialsProvider;
38+
import com.google.api.gax.rpc.StatusCode;
39+
import com.google.api.gax.rpc.TransportChannelProvider;
40+
import com.google.api.gax.rpc.UnavailableException;
41+
import com.google.api.gax.tracing.GoldenSignalsMetricsTracerFactory;
42+
import com.google.api.gax.tracing.ObservabilityAttributes;
43+
import com.google.common.collect.ImmutableList;
44+
import com.google.rpc.Status;
45+
import com.google.showcase.v1beta1.EchoClient;
46+
import com.google.showcase.v1beta1.EchoRequest;
47+
import com.google.showcase.v1beta1.EchoSettings;
48+
import com.google.showcase.v1beta1.it.util.TestClientInitializer;
49+
import com.google.showcase.v1beta1.stub.EchoStubSettings;
50+
import io.grpc.CallOptions;
51+
import io.grpc.Channel;
52+
import io.grpc.ClientCall;
53+
import io.grpc.ClientInterceptor;
54+
import io.grpc.MethodDescriptor;
55+
import io.grpc.Metadata;
56+
import io.opentelemetry.api.common.AttributeKey;
57+
import io.opentelemetry.sdk.OpenTelemetrySdk;
58+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
59+
import io.opentelemetry.sdk.metrics.data.MetricData;
60+
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
61+
import java.io.ByteArrayInputStream;
62+
import java.io.InputStream;
63+
import java.util.Collection;
64+
import org.junit.jupiter.api.AfterEach;
65+
import org.junit.jupiter.api.BeforeEach;
66+
import org.junit.jupiter.api.Test;
67+
68+
class ITOtelGoldenMetrics {
69+
private static final String SHOWCASE_SERVER_ADDRESS = "localhost";
70+
private static final long SHOWCASE_SERVER_PORT = 7469;
71+
private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase";
72+
73+
private InMemoryMetricReader metricReader;
74+
private OpenTelemetrySdk openTelemetrySdk;
75+
76+
@BeforeEach
77+
void setup() {
78+
metricReader = InMemoryMetricReader.create();
79+
80+
SdkMeterProvider meterProvider =
81+
SdkMeterProvider.builder().registerMetricReader(metricReader).build();
82+
83+
openTelemetrySdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build();
84+
}
85+
86+
@AfterEach
87+
void tearDown() {
88+
if (openTelemetrySdk != null) {
89+
openTelemetrySdk.close();
90+
}
91+
}
92+
93+
@Test
94+
void testMetrics_successfulEcho_grpc() throws Exception {
95+
GoldenSignalsMetricsTracerFactory tracerFactory =
96+
new GoldenSignalsMetricsTracerFactory(openTelemetrySdk);
97+
98+
try (EchoClient client =
99+
TestClientInitializer.createGrpcEchoClientOpentelemetry(tracerFactory)) {
100+
101+
client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
102+
103+
Collection<MetricData> metrics = metricReader.collectAllMetrics();
104+
assertThat(metrics).isNotEmpty();
105+
106+
MetricData durationMetric =
107+
metrics.stream()
108+
.filter(m -> m.getName().equals("gcp.client.request.duration"))
109+
.findFirst()
110+
.orElseThrow(() -> new AssertionError("Duration metric not found"));
111+
112+
assertThat(durationMetric.getInstrumentationScopeInfo().getName())
113+
.isEqualTo(SHOWCASE_ARTIFACT);
114+
assertThat(durationMetric.getInstrumentationScopeInfo().getVersion())
115+
.isEqualTo(com.google.api.gax.core.GaxProperties.getLibraryVersion(EchoClient.class));
116+
117+
io.opentelemetry.api.common.Attributes attributes =
118+
durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
119+
120+
assertThat(
121+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
122+
.isEqualTo(SHOWCASE_SERVER_ADDRESS);
123+
assertThat(
124+
attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
125+
.isEqualTo(SHOWCASE_SERVER_PORT);
126+
assertThat(
127+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE)))
128+
.isEqualTo("grpc");
129+
assertThat(
130+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
131+
.isEqualTo("showcase");
132+
assertThat(
133+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
134+
.isEqualTo("google.showcase.v1beta1.Echo/Echo");
135+
assertThat(
136+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
137+
.isEqualTo("OK");
138+
}
139+
}
140+
141+
@Test
142+
void testMetrics_failedEcho_grpc_recordsErrorType() throws Exception {
143+
GoldenSignalsMetricsTracerFactory tracerFactory =
144+
new GoldenSignalsMetricsTracerFactory(openTelemetrySdk);
145+
146+
ClientInterceptor interceptor =
147+
new ClientInterceptor() {
148+
@Override
149+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
150+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
151+
return new ClientCall<ReqT, RespT>() {
152+
@Override
153+
public void start(Listener<RespT> responseListener, Metadata headers) {
154+
responseListener.onClose(io.grpc.Status.UNAVAILABLE, new Metadata());
155+
}
156+
157+
@Override
158+
public void request(int numMessages) {}
159+
160+
@Override
161+
public void cancel(String message, Throwable cause) {}
162+
163+
@Override
164+
public void halfClose() {}
165+
166+
@Override
167+
public void sendMessage(ReqT message) {}
168+
};
169+
}
170+
};
171+
172+
TransportChannelProvider transportChannelProvider =
173+
EchoSettings.defaultGrpcTransportProviderBuilder()
174+
.setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext)
175+
.setInterceptorProvider(() -> ImmutableList.of(interceptor))
176+
.build();
177+
178+
try (EchoClient client =
179+
TestClientInitializer.createGrpcEchoClientOpentelemetry(
180+
tracerFactory, transportChannelProvider)) {
181+
182+
assertThrows(
183+
UnavailableException.class,
184+
() -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
185+
186+
Collection<MetricData> metrics = metricReader.collectAllMetrics();
187+
assertThat(metrics).isNotEmpty();
188+
189+
MetricData durationMetric =
190+
metrics.stream()
191+
.filter(m -> m.getName().equals("gcp.client.request.duration"))
192+
.findFirst()
193+
.orElseThrow(() -> new AssertionError("Duration metric not found"));
194+
195+
io.opentelemetry.api.common.Attributes attributes =
196+
durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
197+
198+
assertThat(
199+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
200+
.isEqualTo("UNAVAILABLE");
201+
assertThat(
202+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
203+
.isEqualTo("UNAVAILABLE");
204+
}
205+
}
206+
207+
@Test
208+
void testMetrics_successfulEcho_httpjson() throws Exception {
209+
GoldenSignalsMetricsTracerFactory tracerFactory =
210+
new GoldenSignalsMetricsTracerFactory(openTelemetrySdk);
211+
212+
try (EchoClient client =
213+
TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracerFactory)) {
214+
215+
client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
216+
217+
Collection<MetricData> metrics = metricReader.collectAllMetrics();
218+
assertThat(metrics).isNotEmpty();
219+
220+
MetricData durationMetric =
221+
metrics.stream()
222+
.filter(m -> m.getName().equals("gcp.client.request.duration"))
223+
.findFirst()
224+
.orElseThrow(() -> new AssertionError("Duration metric not found"));
225+
226+
assertThat(durationMetric.getInstrumentationScopeInfo().getName())
227+
.isEqualTo(SHOWCASE_ARTIFACT);
228+
assertThat(durationMetric.getInstrumentationScopeInfo().getVersion())
229+
.isEqualTo(com.google.api.gax.core.GaxProperties.getLibraryVersion(EchoClient.class));
230+
231+
io.opentelemetry.api.common.Attributes attributes =
232+
durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
233+
234+
assertThat(
235+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
236+
.isEqualTo(SHOWCASE_SERVER_ADDRESS);
237+
assertThat(
238+
attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
239+
.isEqualTo(SHOWCASE_SERVER_PORT);
240+
assertThat(
241+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE)))
242+
.isEqualTo("http");
243+
assertThat(
244+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
245+
.isEqualTo("showcase");
246+
assertThat(
247+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
248+
.isEqualTo("OK");
249+
assertThat(
250+
attributes.get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
251+
.isEqualTo(200L);
252+
assertThat(
253+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.URL_TEMPLATE_ATTRIBUTE)))
254+
.isEqualTo("v1beta1/echo:echo");
255+
assertThat(
256+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
257+
.isNull();
258+
}
259+
}
260+
261+
@Test
262+
void testMetrics_failedEcho_httpjson_recordsErrorType() throws Exception {
263+
GoldenSignalsMetricsTracerFactory tracerFactory =
264+
new GoldenSignalsMetricsTracerFactory(openTelemetrySdk);
265+
266+
HttpTransport mockTransport =
267+
new HttpTransport() {
268+
@Override
269+
protected com.google.api.client.http.LowLevelHttpRequest buildRequest(
270+
String method, String url) {
271+
return new com.google.api.client.http.LowLevelHttpRequest() {
272+
@Override
273+
public void addHeader(String name, String value) {}
274+
275+
@Override
276+
public com.google.api.client.http.LowLevelHttpResponse execute() {
277+
return new com.google.api.client.http.LowLevelHttpResponse() {
278+
@Override
279+
public InputStream getContent() {
280+
return new ByteArrayInputStream("{}".getBytes());
281+
}
282+
283+
@Override
284+
public String getContentEncoding() {
285+
return null;
286+
}
287+
288+
@Override
289+
public long getContentLength() {
290+
return 2;
291+
}
292+
293+
@Override
294+
public String getContentType() {
295+
return "application/json";
296+
}
297+
298+
@Override
299+
public String getStatusLine() {
300+
return "HTTP/1.1 503 Service Unavailable";
301+
}
302+
303+
@Override
304+
public int getStatusCode() {
305+
return 503;
306+
}
307+
308+
@Override
309+
public String getReasonPhrase() {
310+
return "Service Unavailable";
311+
}
312+
313+
@Override
314+
public int getHeaderCount() {
315+
return 0;
316+
}
317+
318+
@Override
319+
public String getHeaderName(int index) {
320+
return null;
321+
}
322+
323+
@Override
324+
public String getHeaderValue(int index) {
325+
return null;
326+
}
327+
};
328+
}
329+
};
330+
}
331+
};
332+
333+
EchoSettings httpJsonEchoSettings =
334+
EchoSettings.newHttpJsonBuilder()
335+
.setCredentialsProvider(NoCredentialsProvider.create())
336+
.setTransportChannelProvider(
337+
EchoSettings.defaultHttpJsonTransportProviderBuilder()
338+
.setHttpTransport(mockTransport)
339+
.setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
340+
.build())
341+
.build();
342+
343+
EchoStubSettings echoStubSettings =
344+
(EchoStubSettings)
345+
httpJsonEchoSettings.getStubSettings().toBuilder()
346+
.setTracerFactory(tracerFactory)
347+
.build();
348+
349+
try (EchoClient client = EchoClient.create(echoStubSettings.createStub())) {
350+
assertThrows(
351+
UnavailableException.class,
352+
() -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
353+
354+
Collection<MetricData> metrics = metricReader.collectAllMetrics();
355+
assertThat(metrics).isNotEmpty();
356+
357+
MetricData durationMetric =
358+
metrics.stream()
359+
.filter(m -> m.getName().equals("gcp.client.request.duration"))
360+
.findFirst()
361+
.orElseThrow(() -> new AssertionError("Duration metric not found"));
362+
363+
io.opentelemetry.api.common.Attributes attributes =
364+
durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
365+
366+
assertThat(
367+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
368+
.isEqualTo("UNAVAILABLE");
369+
assertThat(
370+
attributes.get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
371+
.isEqualTo(503L);
372+
assertThat(
373+
attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
374+
.isEqualTo("503");
375+
}
376+
}
377+
}

0 commit comments

Comments
 (0)