Skip to content

Commit b6c429e

Browse files
committed
test(gax): add automated integration test for actionable errors logging
1 parent 965761a commit b6c429e

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.showcase.v1beta1.it.logging;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import ch.qos.logback.classic.Level;
23+
import ch.qos.logback.classic.spi.ILoggingEvent;
24+
import com.google.api.client.http.LowLevelHttpRequest;
25+
import com.google.api.client.http.LowLevelHttpResponse;
26+
import com.google.api.client.testing.http.MockHttpTransport;
27+
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
28+
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
29+
import com.google.api.gax.core.NoCredentialsProvider;
30+
import com.google.api.gax.rpc.ApiException;
31+
import com.google.api.gax.tracing.LoggingTracerFactory;
32+
import com.google.protobuf.Any;
33+
import com.google.rpc.ErrorInfo;
34+
import com.google.rpc.Status;
35+
import com.google.showcase.v1beta1.EchoClient;
36+
import com.google.showcase.v1beta1.EchoRequest;
37+
import com.google.showcase.v1beta1.EchoSettings;
38+
import com.google.showcase.v1beta1.it.util.TestClientInitializer;
39+
import java.io.IOException;
40+
import java.util.List;
41+
import java.util.concurrent.TimeUnit;
42+
import org.junit.jupiter.api.AfterAll;
43+
import org.junit.jupiter.api.BeforeAll;
44+
import org.junit.jupiter.api.Test;
45+
import org.slf4j.LoggerFactory;
46+
import org.slf4j.event.KeyValuePair;
47+
48+
public class ITActionableErrorsLogging {
49+
50+
private static EchoClient grpcClient;
51+
private static EchoClient httpjsonClient;
52+
53+
@BeforeAll
54+
static void createClients() throws Exception {
55+
try {
56+
java.lang.reflect.Method m =
57+
com.google.api.gax.logging.LoggingUtils.class.getDeclaredMethod(
58+
"setLoggingEnabled", boolean.class);
59+
m.setAccessible(true);
60+
m.invoke(null, true);
61+
} catch (Exception e) {
62+
throw new RuntimeException(e);
63+
}
64+
65+
grpcClient =
66+
TestClientInitializer.createGrpcEchoClientOpentelemetry(new LoggingTracerFactory());
67+
httpjsonClient =
68+
TestClientInitializer.createHttpJsonEchoClientOpentelemetry(new LoggingTracerFactory());
69+
}
70+
71+
@AfterAll
72+
static void destroyClients() throws InterruptedException {
73+
grpcClient.close();
74+
httpjsonClient.close();
75+
76+
grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
77+
httpjsonClient.awaitTermination(
78+
TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
79+
try {
80+
java.lang.reflect.Method m =
81+
com.google.api.gax.logging.LoggingUtils.class.getDeclaredMethod(
82+
"setLoggingEnabled", boolean.class);
83+
m.setAccessible(true);
84+
m.invoke(null, false);
85+
} catch (Exception e) {
86+
throw new RuntimeException(e);
87+
}
88+
}
89+
90+
private TestAppender setupTestLogger() {
91+
TestAppender testAppender = new TestAppender();
92+
testAppender.start();
93+
org.slf4j.Logger logger = LoggerFactory.getLogger("com.google.api.gax.tracing.LoggingTracer");
94+
((ch.qos.logback.classic.Logger) logger).setLevel(Level.DEBUG);
95+
((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
96+
return testAppender;
97+
}
98+
99+
private EchoRequest buildErrorRequest() {
100+
ErrorInfo errorInfo =
101+
ErrorInfo.newBuilder()
102+
.setReason("TEST_REASON")
103+
.setDomain("test.googleapis.com")
104+
.putMetadata("test_metadata", "test_value")
105+
.build();
106+
Status status =
107+
Status.newBuilder()
108+
.setCode(3) // INVALID_ARGUMENT
109+
.setMessage("This is a test error")
110+
.addDetails(Any.pack(errorInfo))
111+
.build();
112+
return EchoRequest.newBuilder().setError(status).build();
113+
}
114+
115+
@Test
116+
void testGrpc_actionableErrorLogged() {
117+
TestAppender testAppender = setupTestLogger();
118+
119+
EchoRequest request = buildErrorRequest();
120+
121+
ApiException exception = assertThrows(ApiException.class, () -> grpcClient.echo(request));
122+
123+
assertThat(testAppender.events.size()).isAtLeast(1);
124+
ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
125+
126+
assertThat(loggingEvent.getLevel()).isEqualTo(Level.DEBUG);
127+
assertThat(loggingEvent.getMessage()).contains("This is a test error");
128+
129+
List<KeyValuePair> kvps = loggingEvent.getKeyValuePairs();
130+
assertThat(kvps).contains(new KeyValuePair("rpc.system.name", "grpc"));
131+
assertThat(kvps).contains(new KeyValuePair("rpc.method", "google.showcase.v1beta1.Echo/Echo"));
132+
assertThat(kvps).contains(new KeyValuePair("rpc.response.status_code", "INVALID_ARGUMENT"));
133+
assertThat(kvps).contains(new KeyValuePair("error.type", "TEST_REASON"));
134+
assertThat(kvps).contains(new KeyValuePair("gcp.errors.domain", "test.googleapis.com"));
135+
assertThat(kvps).contains(new KeyValuePair("gcp.errors.metadata.test_metadata", "test_value"));
136+
137+
testAppender.stop();
138+
}
139+
140+
@Test
141+
void testHttpJson_actionableErrorLogged() throws Exception {
142+
TestAppender testAppender = setupTestLogger();
143+
144+
// The gapic-showcase server currently returns text/plain for failEchoWithDetails instead of
145+
// JSON.
146+
// Additionally, sending an ErrorInfo in a request over REST fails serialization.
147+
// To test HTTP JSON actionable errors logic, we use a MockHttpTransport that simulates the
148+
// correct JSON format.
149+
MockHttpTransport mockTransport =
150+
new MockHttpTransport() {
151+
@Override
152+
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
153+
return new MockLowLevelHttpRequest() {
154+
@Override
155+
public LowLevelHttpResponse execute() throws IOException {
156+
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
157+
response.setStatusCode(409); // ABORTED
158+
response.setContentType("application/json");
159+
String jsonError =
160+
"{\n"
161+
+ " \"error\": {\n"
162+
+ " \"code\": 409,\n"
163+
+ " \"message\": \"This is a mock JSON error generated by the server\",\n"
164+
+ " \"status\": \"ABORTED\",\n"
165+
+ " \"details\": [\n"
166+
+ " {\n"
167+
+ " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\",\n"
168+
+ " \"reason\": \"mock_error_reason\",\n"
169+
+ " \"domain\": \"mock.googleapis.com\",\n"
170+
+ " \"metadata\": {\"mock_key\": \"mock_value\"}\n"
171+
+ " }\n"
172+
+ " ]\n"
173+
+ " }\n"
174+
+ "}";
175+
response.setContent(jsonError);
176+
return response;
177+
}
178+
};
179+
}
180+
};
181+
182+
EchoSettings httpJsonEchoSettings =
183+
EchoSettings.newHttpJsonBuilder()
184+
.setCredentialsProvider(NoCredentialsProvider.create())
185+
.setTransportChannelProvider(
186+
EchoSettings.defaultHttpJsonTransportProviderBuilder()
187+
.setHttpTransport(mockTransport)
188+
.setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
189+
.build())
190+
.build();
191+
192+
com.google.showcase.v1beta1.stub.EchoStubSettings echoStubSettings =
193+
(com.google.showcase.v1beta1.stub.EchoStubSettings)
194+
httpJsonEchoSettings.getStubSettings().toBuilder()
195+
.setTracerFactory(new LoggingTracerFactory())
196+
.build();
197+
com.google.showcase.v1beta1.stub.EchoStub stub = echoStubSettings.createStub();
198+
EchoClient mockHttpJsonClient = EchoClient.create(stub);
199+
200+
EchoRequest request = EchoRequest.newBuilder().build();
201+
202+
ApiException exception =
203+
assertThrows(ApiException.class, () -> mockHttpJsonClient.echo(request));
204+
205+
assertThat(testAppender.events.size()).isAtLeast(1);
206+
ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
207+
208+
assertThat(loggingEvent.getLevel()).isEqualTo(Level.DEBUG);
209+
assertThat(loggingEvent.getMessage())
210+
.contains("This is a mock JSON error generated by the server");
211+
212+
List<KeyValuePair> kvps = loggingEvent.getKeyValuePairs();
213+
assertThat(kvps).contains(new KeyValuePair("rpc.system.name", "http"));
214+
assertThat(kvps).contains(new KeyValuePair("http.request.method", "POST"));
215+
assertThat(kvps).contains(new KeyValuePair("url.template", "v1beta1/echo:echo"));
216+
assertThat(kvps).contains(new KeyValuePair("rpc.response.status_code", "ABORTED"));
217+
assertThat(kvps).contains(new KeyValuePair("error.type", "mock_error_reason"));
218+
assertThat(kvps).contains(new KeyValuePair("gcp.errors.domain", "mock.googleapis.com"));
219+
assertThat(kvps).contains(new KeyValuePair("gcp.errors.metadata.mock_key", "mock_value"));
220+
221+
mockHttpJsonClient.close();
222+
testAppender.stop();
223+
}
224+
}

0 commit comments

Comments
 (0)