diff --git a/sdk-platform-java/gax-java/gax/pom.xml b/sdk-platform-java/gax-java/gax/pom.xml index 2feb9c9aba81..4d92bdfd5255 100644 --- a/sdk-platform-java/gax-java/gax/pom.xml +++ b/sdk-platform-java/gax-java/gax/pom.xml @@ -115,6 +115,7 @@ com/google/api/gax/rpc/testing/** com/google/api/gax/rpc/mtls/** com/google/api/gax/util/** + com/google/api/gax/logging/** **/native-image.properties diff --git a/sdk-platform-java/java-showcase/gapic-showcase/pom.xml b/sdk-platform-java/java-showcase/gapic-showcase/pom.xml index bc8d2cc812b6..85f2b44a6996 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/pom.xml +++ b/sdk-platform-java/java-showcase/gapic-showcase/pom.xml @@ -74,6 +74,16 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + + + + **/ITActionableErrorsLogging.java + + + @@ -274,6 +284,7 @@ **/com/google/showcase/v1beta1/it/*.java **/com/google/showcase/v1beta1/it/logging/ITLoggingDisabled.java **/com/google/showcase/v1beta1/it/logging/ITLogging1x.java + **/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java @@ -318,6 +329,7 @@ **/com/google/showcase/v1beta1/it/*.java **/com/google/showcase/v1beta1/it/logging/ITLoggingDisabled.java **/com/google/showcase/v1beta1/it/logging/ITLogging.java + **/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java @@ -363,6 +375,7 @@ **/com/google/showcase/v1beta1/it/*.java **/com/google/showcase/v1beta1/it/logging/ITLogging1x.java **/com/google/showcase/v1beta1/it/logging/ITLogging.java + **/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java @@ -388,6 +401,44 @@ + + envVarTest + + + org.slf4j + slf4j-api + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + **/ITActionableErrorsLogging.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + **/ITActionableErrorsLogging.java + + + ${project.basedir}/src/test/slf4j-test-provider + + + + + + diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java new file mode 100644 index 000000000000..910ed08057e7 --- /dev/null +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java @@ -0,0 +1,194 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.showcase.v1beta1.it.logging; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.logging.TestLogger; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.tracing.LoggingTracerFactory; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +public class ITActionableErrorsLogging { + + private static EchoClient grpcClient; + private static EchoClient httpjsonClient; + private TestLogger testLogger; + + @BeforeAll + static void createClients() throws Exception { + grpcClient = + TestClientInitializer.createGrpcEchoClientOpentelemetry(new LoggingTracerFactory()); + httpjsonClient = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(new LoggingTracerFactory()); + } + + @AfterAll + static void destroyClients() throws InterruptedException { + grpcClient.close(); + httpjsonClient.close(); + + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + httpjsonClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + @BeforeEach + void setupTestLogger() { + testLogger = (TestLogger) LoggerFactory.getLogger("com.google.api.gax.tracing.LoggingTracer"); + testLogger.getMessageList().clear(); + testLogger.getKeyValuePairsMap().clear(); + } + + private EchoRequest buildErrorRequest() { + ErrorInfo errorInfo = + ErrorInfo.newBuilder() + .setReason("TEST_REASON") + .setDomain("test.googleapis.com") + .putMetadata("test_metadata", "test_value") + .build(); + Status status = + Status.newBuilder() + .setCode(3) // INVALID_ARGUMENT + .setMessage("This is a test error") + .addDetails(Any.pack(errorInfo)) + .build(); + return EchoRequest.newBuilder().setError(status).build(); + } + + @Test + void testGrpc_actionableErrorLogged() { + EchoRequest request = buildErrorRequest(); + + assertThrows(ApiException.class, () -> grpcClient.echo(request)); + + assertThat(testLogger.getMessageList().size()).isAtLeast(1); + String loggedMessage = testLogger.getMessageList().get(testLogger.getMessageList().size() - 1); + + assertThat(loggedMessage).contains("This is a test error"); + + Map kvps = testLogger.getKeyValuePairsMap(); + assertThat(kvps).containsEntry("rpc.system.name", "grpc"); + assertThat(kvps).containsEntry("rpc.method", "google.showcase.v1beta1.Echo/Echo"); + assertThat(kvps).containsEntry("rpc.response.status_code", "INVALID_ARGUMENT"); + assertThat(kvps).containsEntry("error.type", "TEST_REASON"); + assertThat(kvps).containsEntry("gcp.errors.domain", "test.googleapis.com"); + assertThat(kvps).containsEntry("gcp.errors.metadata.test_metadata", "test_value"); + } + + @Test + void testHttpJson_actionableErrorLogged() throws Exception { + // The gapic-showcase server currently returns text/plain for failEchoWithDetails instead of + // JSON. + // Additionally, sending an ErrorInfo in a request over REST fails serialization. + // To test HTTP JSON actionable errors logic, we use a MockHttpTransport that simulates the + // correct JSON format. + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(409); // ABORTED + response.setContentType("application/json"); + String jsonError = + "{\n" + + " \"error\": {\n" + + " \"code\": 409,\n" + + " \"message\": \"This is a mock JSON error generated by the server\",\n" + + " \"status\": \"ABORTED\",\n" + + " \"details\": [\n" + + " {\n" + + " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\",\n" + + " \"reason\": \"mock_error_reason\",\n" + + " \"domain\": \"mock.googleapis.com\",\n" + + " \"metadata\": {\"mock_key\": \"mock_value\"}\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + response.setContent(jsonError); + return response; + } + }; + } + }; + + EchoSettings httpJsonEchoSettings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(mockTransport) + .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT) + .build()) + .build(); + + com.google.showcase.v1beta1.stub.EchoStubSettings echoStubSettings = + (com.google.showcase.v1beta1.stub.EchoStubSettings) + httpJsonEchoSettings.getStubSettings().toBuilder() + .setTracerFactory(new LoggingTracerFactory()) + .build(); + com.google.showcase.v1beta1.stub.EchoStub stub = echoStubSettings.createStub(); + EchoClient mockHttpJsonClient = EchoClient.create(stub); + + EchoRequest request = EchoRequest.newBuilder().build(); + + assertThrows(ApiException.class, () -> mockHttpJsonClient.echo(request)); + + assertThat(testLogger.getMessageList().size()).isAtLeast(1); + String loggedMessage = testLogger.getMessageList().get(testLogger.getMessageList().size() - 1); + + assertThat(loggedMessage).contains("This is a mock JSON error generated by the server"); + + Map kvps = testLogger.getKeyValuePairsMap(); + assertThat(kvps).containsEntry("rpc.system.name", "http"); + assertThat(kvps).containsEntry("http.request.method", "POST"); + assertThat(kvps).containsEntry("url.template", "v1beta1/echo:echo"); + assertThat(kvps).containsEntry("rpc.response.status_code", "ABORTED"); + assertThat(kvps).containsEntry("error.type", "mock_error_reason"); + assertThat(kvps).containsEntry("gcp.errors.domain", "mock.googleapis.com"); + assertThat(kvps).containsEntry("gcp.errors.metadata.mock_key", "mock_value"); + + mockHttpJsonClient.close(); + mockHttpJsonClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } +} diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/slf4j-test-provider/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/sdk-platform-java/java-showcase/gapic-showcase/src/test/slf4j-test-provider/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 000000000000..b97d560115b5 --- /dev/null +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/slf4j-test-provider/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +com.google.api.gax.logging.TestServiceProvider diff --git a/sdk-platform-java/java-showcase/pom.xml b/sdk-platform-java/java-showcase/pom.xml index a241e7677e03..41efc38e5311 100644 --- a/sdk-platform-java/java-showcase/pom.xml +++ b/sdk-platform-java/java-showcase/pom.xml @@ -35,6 +35,11 @@ pom import + + org.slf4j + slf4j-api + 2.0.16 + com.google.api.grpc proto-gapic-showcase-v1beta1