diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a3b871ae38..358e72dadf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -228,6 +228,10 @@ jobs: - name: Install Maven modules run: | mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip + - name: Install logging module # this module is not part of root project. + run: | + mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip + working-directory: java-sdk-logging - name: Java Linter working-directory: java-showcase run: | diff --git a/.github/workflows/java_sdk_logging.yaml b/.github/workflows/java_sdk_logging.yaml index 24a82cf9de..b7f8159e64 100644 --- a/.github/workflows/java_sdk_logging.yaml +++ b/.github/workflows/java_sdk_logging.yaml @@ -31,13 +31,28 @@ jobs: with: java-version: 8 distribution: temurin + - run: echo "JAVA8_HOME=${JAVA_HOME}" >> $GITHUB_ENV + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin - name: Install parent module run: | mvn install -B -ntp -pl gapic-generator-java-pom-parent - - name: Unit Tests + - name: Install logging module + run: | + mvn install -B -ntp -Dcheckstyle.skip -Dfmt.skip working-directory: java-sdk-logging + - name: Unit Tests in Java 8 run: | - mvn test -B -ntp -Dcheckstyle.skip -Dfmt.skip + set -x + export JAVA_HOME=$JAVA_HOME + export PATH=${JAVA_HOME}/bin:$PATH + mvn verify -B -ntp \ + -Dcheckstyle.skip \ + -Dfmt.skip \ + -Djvm="${JAVA8_HOME}/bin/java" + working-directory: java-sdk-logging module-lint: runs-on: ubuntu-latest steps: @@ -59,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin - name: Install parent module run: | @@ -74,7 +89,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin - name: Install parent module run: | diff --git a/java-sdk-logging/logback-extension/pom.xml b/java-sdk-logging/logback-extension/pom.xml index 64b1a8958b..e4da6edfe7 100644 --- a/java-sdk-logging/logback-extension/pom.xml +++ b/java-sdk-logging/logback-extension/pom.xml @@ -14,6 +14,8 @@ UTF-8 1.2.13 + 7.3 diff --git a/java-sdk-logging/logback-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProvider.java b/java-sdk-logging/logback-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProvider.java new file mode 100644 index 0000000000..8455af10d2 --- /dev/null +++ b/java-sdk-logging/logback-extension/src/main/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProvider.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.cloud.sdk.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.Map; +import net.logstash.logback.composite.loggingevent.MdcJsonProvider; + +public class SDKLoggingMdcJsonProvider extends MdcJsonProvider { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException { + Map mdcProperties = event.getMDCPropertyMap(); + if (mdcProperties == null || mdcProperties.isEmpty()) { + return; + } + + boolean hasWrittenStart = false; + for (Map.Entry entry : mdcProperties.entrySet()) { + String fieldName = entry.getKey(); + String entryValueString = entry.getValue(); + // an entry will be skipped if one of the scenario happens: + // 1. key or value is null + if (fieldName == null || entryValueString == null) { + continue; + } + // 2. includeMdcKeyNames is not empty and the key is not in the list + if (!getIncludeMdcKeyNames().isEmpty() && !getIncludeMdcKeyNames().contains(fieldName)) { + continue; + } + // 3. excludeMdcKeyNames is not empty and the key is in the list + if (!getExcludeMdcKeyNames().isEmpty() && getExcludeMdcKeyNames().contains(fieldName)) { + continue; + } + + fieldName = getMdcKeyFieldNames().getOrDefault(entry.getKey(), fieldName); + if (!hasWrittenStart && getFieldName() != null) { + generator.writeObjectFieldStart(getFieldName()); + hasWrittenStart = true; + } + generator.writeFieldName(fieldName); + + try { + generator.writeTree(convertToTreeNode(entryValueString)); + } catch (JsonProcessingException e) { + // in case of conversion exception, just use String + generator.writeObject(entryValueString); + } + } + if (hasWrittenStart) { + generator.writeEndObject(); + } + } + + private JsonNode convertToTreeNode(String jsonString) throws JsonProcessingException { + return objectMapper.readTree(jsonString); + } +} diff --git a/java-sdk-logging/logback-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProviderTest.java b/java-sdk-logging/logback-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProviderTest.java new file mode 100644 index 0000000000..35ebbb829c --- /dev/null +++ b/java-sdk-logging/logback-extension/src/test/java/com/google/cloud/sdk/logging/SDKLoggingMdcJsonProviderTest.java @@ -0,0 +1,252 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.cloud.sdk.logging; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +class SDKLoggingMdcJsonProviderTest { + + private final SDKLoggingMdcJsonProvider provider = new SDKLoggingMdcJsonProvider(); + private final JsonGenerator generator = mock(JsonGenerator.class); + private final ILoggingEvent event = mock(ILoggingEvent.class); + private Map mdc; + + @BeforeEach + void init() { + mdc = new HashMap<>(); + when(event.getMDCPropertyMap()).thenReturn(mdc); + } + + @AfterEach + void post() { + mdc.clear(); + } + + @Test + void testWriteNullMdcMap() throws IOException { + when(event.getMDCPropertyMap()).thenReturn(null); + provider.writeTo(generator, event); + verify(generator, never()).writeFieldName(anyString()); + verify(generator, never()).writeTree(any(JsonNode.class)); + } + + @Test + void testWriteEmptyMdcMap() throws IOException { + provider.writeTo(generator, event); + verify(generator, never()).writeFieldName(anyString()); + verify(generator, never()).writeTree(any(JsonNode.class)); + } + + @Test + void testWriteValidJsonStringToJsonTree() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + + provider.setFieldName("log-name"); + provider.writeTo(generator, event); + InOrder inOrder = inOrder(generator); + inOrder.verify(generator).writeObjectFieldStart("log-name"); + inOrder.verify(generator).writeFieldName("json1"); + inOrder.verify(generator).writeTree(any(JsonNode.class)); + inOrder.verify(generator).writeEndObject(); + } + + @Test + void testWriteValidJsonStringAndNullKeyToJsonTree() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + mdc.put(null, "example value"); + + provider.setFieldName("log-name"); + provider.writeTo(generator, event); + verify(generator, times(1)).writeObjectFieldStart("log-name"); + verify(generator, times(1)).writeFieldName("json1"); + verify(generator, times(1)).writeTree(any(JsonNode.class)); + verify(generator, never()).writeObject(anyString()); + verify(generator, times(1)).writeEndObject(); + } + + @Test + void testWriteValidJsonStringAndValidPairToJsonTree() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + mdc.put("example key", "example value"); + + provider.writeTo(generator, event); + verify(generator, times(1)).writeFieldName("json1"); + verify(generator, times(1)).writeFieldName("example key"); + verify(generator, times(1)).writeTree(any(JsonNode.class)); + verify(generator, times(1)).writeObject(anyString()); + } + + @Test + void testWriteToJsonTreeIncludedKey() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + mdc.put("example key", "example value"); + provider.setIncludeMdcKeyNames(Collections.singletonList("json1")); + provider.writeTo(generator, event); + verify(generator, times(1)).writeFieldName("json1"); + verify(generator, never()).writeFieldName("example key"); + verify(generator, times(1)).writeTree(any(JsonNode.class)); + verify(generator, never()).writeObject(anyString()); + } + + @Test + void testWriteToJsonTreeReplacedKey() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + provider.addMdcKeyFieldName("json1=new_json"); + provider.writeTo(generator, event); + verify(generator, times(1)).writeFieldName("new_json"); + verify(generator, times(1)).writeTree(any(JsonNode.class)); + verify(generator, never()).writeObject(anyString()); + } + + @Test + void testWriteToJsonTreeExcludedKey() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\"\n" + + " }\n" + + "}"); + mdc.put("example key", "example value"); + List excluded = new ArrayList<>(); + excluded.add("json1"); + excluded.add("example key"); + provider.setExcludeMdcKeyNames(excluded); + provider.writeTo(generator, event); + verify(generator, never()).writeFieldName(anyString()); + verify(generator, never()).writeTree(any(JsonNode.class)); + verify(generator, never()).writeObject(anyString()); + } + + @Test + void testWriteInvalidJsonStringToString() throws IOException { + mdc.put( + "json1", + "{\n" + + " \"@version\": \"1\",\n" + + " \"textPayload\": \"Received response\",\n" + + " \"response.payload\": {\n" + + " \"name\": \"example\",\n" + + " \"state\": \"ACTIVE\",\n" // the last semicolon is redundant. + + " }\n" + + "}"); + provider.writeTo(generator, event); + verify(generator).writeFieldName("json1"); + verify(generator).writeObject(anyString()); + // should not write tree node because the json string is invalid. + verify(generator, never()).writeTree(any(JsonNode.class)); + } + + @Test + void testWriteNullValueDoesNotThrowsException() throws IOException { + mdc.put("json1", null); + provider.writeTo(generator, event); + verify(generator, never()).writeObject(anyString()); + verify(generator, never()).writeTree(any(JsonNode.class)); + } + + @Test + void testWriteNullKeyDoesNotThrowsException() throws IOException { + mdc.put(null, "example value"); + provider.writeTo(generator, event); + verify(generator, never()).writeObject(anyString()); + verify(generator, never()).writeTree(any(JsonNode.class)); + } +} diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index 36d1ab34f9..b77ceff9e6 100644 --- a/java-showcase/gapic-showcase/pom.xml +++ b/java-showcase/gapic-showcase/pom.xml @@ -258,6 +258,8 @@ **/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/TestMdcAppender.java @@ -291,6 +293,12 @@ ${slf4j1-logback.version} test + + com.google.cloud + logback-extension + 0.1.0-SNAPSHOT + test + @@ -347,6 +355,8 @@ **/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/TestMdcAppender.java diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITLogging1x.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITLogging1x.java index d0dec4b80d..2aaf68e36f 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITLogging1x.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITLogging1x.java @@ -20,6 +20,9 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.api.gax.grpc.GrpcLoggingInterceptor; import com.google.api.gax.httpjson.HttpJsonLoggingInterceptor; import com.google.common.collect.ImmutableMap; @@ -27,6 +30,8 @@ import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; @@ -49,16 +54,16 @@ public class ITLogging1x { private static final String ENDPOINT = "http://localhost:7469"; private static final String SENDING_REQUEST_MESSAGE = "Sending request"; private static final String RECEIVING_RESPONSE_MESSAGE = "Received response"; + private final ObjectMapper objectMapper = new ObjectMapper(); - private static Logger logger = LoggerFactory.getLogger(ITLogging1x.class); + private static final Logger CLASS_LOGGER = LoggerFactory.getLogger(ITLogging1x.class); - private TestAppender setupTestLogger(Class clazz, Level level) { - TestAppender testAppender = new TestAppender(); - testAppender.start(); + private > void setupTestLogger( + T appender, Class clazz, Level level) { + appender.start(); Logger logger = LoggerFactory.getLogger(clazz); ((ch.qos.logback.classic.Logger) logger).setLevel(level); - ((ch.qos.logback.classic.Logger) logger).addAppender(testAppender); - return testAppender; + ((ch.qos.logback.classic.Logger) logger).addAppender(appender); } @BeforeAll @@ -81,13 +86,14 @@ static void destroyClients() throws InterruptedException { @Test void test() { - assertThat(logger.isInfoEnabled()).isTrue(); - assertThat(logger.isDebugEnabled()).isTrue(); + assertThat(CLASS_LOGGER.isInfoEnabled()).isTrue(); + assertThat(CLASS_LOGGER.isDebugEnabled()).isTrue(); } @Test void testGrpc_receiveContent_logDebug() { - TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptor.class, Level.DEBUG); + TestAppender testAppender = new TestAppender(); + setupTestLogger(testAppender, GrpcLoggingInterceptor.class, Level.DEBUG); assertThat(echoGrpc(ECHO_STRING)).isEqualTo(ECHO_STRING); assertThat(testAppender.events.size()).isEqualTo(2); @@ -121,9 +127,27 @@ void testGrpc_receiveContent_logDebug() { testAppender.stop(); } + @Test + void testGrpc_receiveContent_logDebug_structured_log() throws IOException { + TestMdcAppender testAppender = new TestMdcAppender(); + setupTestLogger(testAppender, GrpcLoggingInterceptor.class, Level.DEBUG); + assertThat(echoGrpc(ECHO_STRING)).isEqualTo(ECHO_STRING); + List byteLists = testAppender.getByteLists(); + assertThat(byteLists.size()).isEqualTo(2); + JsonNode request = objectMapper.readTree(byteLists.get(0)); + assertThat(request.get("message").asText()).isEqualTo("Sending request"); + assertThat(request.get("request.payload").get("content").asText()).isEqualTo("echo?"); + JsonNode response = objectMapper.readTree(byteLists.get(1)); + assertThat(response.get("message").asText()).isEqualTo("Received response"); + assertThat(response.get("response.payload").get("content").asText()).isEqualTo("echo?"); + + testAppender.stop(); + } + @Test void testGrpc_receiveContent_logInfo() { - TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptor.class, Level.INFO); + TestAppender testAppender = new TestAppender(); + setupTestLogger(testAppender, GrpcLoggingInterceptor.class, Level.INFO); assertThat(echoGrpc(ECHO_STRING)).isEqualTo(ECHO_STRING); assertThat(testAppender.events.size()).isEqualTo(2); @@ -149,9 +173,30 @@ void testGrpc_receiveContent_logInfo() { testAppender.stop(); } + @Test + void testGrpc_receiveContent_logInfo_structured_log() throws IOException { + TestMdcAppender testAppender = new TestMdcAppender(); + setupTestLogger(testAppender, GrpcLoggingInterceptor.class, Level.INFO); + assertThat(echoGrpc(ECHO_STRING)).isEqualTo(ECHO_STRING); + List byteLists = testAppender.getByteLists(); + assertThat(byteLists.size()).isEqualTo(2); + JsonNode request = objectMapper.readTree(byteLists.get(0)); + assertThat(request.get("message").asText()).isEqualTo("Sending request"); + assertThat(request.get("serviceName").asText()).isEqualTo(SERVICE_NAME); + assertThat(request.get("rpcName").asText()).isEqualTo(RPC_NAME); + JsonNode response = objectMapper.readTree(byteLists.get(1)); + assertThat(response.get("message").asText()).isEqualTo("Received response"); + assertThat(response.get("serviceName").asText()).isEqualTo(SERVICE_NAME); + assertThat(response.get("rpcName").asText()).isEqualTo(RPC_NAME); + assertThat(response.get("response.status").asText()).isEqualTo("OK"); + + testAppender.stop(); + } + @Test void testHttpJson_receiveContent_logDebug() { - TestAppender testAppender = setupTestLogger(HttpJsonLoggingInterceptor.class, Level.DEBUG); + TestAppender testAppender = new TestAppender(); + setupTestLogger(testAppender, HttpJsonLoggingInterceptor.class, Level.DEBUG); assertThat(echoHttpJson(ECHO_STRING)).isEqualTo(ECHO_STRING); assertThat(testAppender.events.size()).isEqualTo(2); // logging event for request @@ -179,9 +224,28 @@ void testHttpJson_receiveContent_logDebug() { testAppender.stop(); } + @Test + void testHttpJson_receiveContent_logDebug_structured_log() throws IOException { + TestMdcAppender testAppender = new TestMdcAppender(); + setupTestLogger(testAppender, HttpJsonLoggingInterceptor.class, Level.DEBUG); + assertThat(echoHttpJson(ECHO_STRING)).isEqualTo(ECHO_STRING); + List byteLists = testAppender.getByteLists(); + assertThat(byteLists.size()).isEqualTo(2); + JsonNode request = objectMapper.readTree(byteLists.get(0)); + assertThat(request.get("request.url").asText()).isEqualTo(ENDPOINT); + assertThat(request.get("request.payload").get("content").asText()).isEqualTo("echo?"); + JsonNode response = objectMapper.readTree(byteLists.get(1)); + assertThat(response.get("rpcName").asText()).isEqualTo(RPC_NAME); + assertThat(response.get("response.payload").get("content").asText()).isEqualTo("echo?"); + assertThat(response.get("response.status").asText()).isEqualTo("200"); + + testAppender.stop(); + } + @Test void testHttpJson_receiveContent_logInfo() { - TestAppender testAppender = setupTestLogger(HttpJsonLoggingInterceptor.class, Level.INFO); + TestAppender testAppender = new TestAppender(); + setupTestLogger(testAppender, HttpJsonLoggingInterceptor.class, Level.INFO); assertThat(echoHttpJson(ECHO_STRING)).isEqualTo(ECHO_STRING); assertThat(testAppender.events.size()).isEqualTo(2); // logging event for request @@ -205,6 +269,24 @@ void testHttpJson_receiveContent_logInfo() { testAppender.stop(); } + @Test + void testHttpJson_receiveContent_logInfo_structured_log() throws IOException { + TestMdcAppender testAppender = new TestMdcAppender(); + setupTestLogger(testAppender, HttpJsonLoggingInterceptor.class, Level.INFO); + assertThat(echoHttpJson(ECHO_STRING)).isEqualTo(ECHO_STRING); + List byteLists = testAppender.getByteLists(); + assertThat(byteLists.size()).isEqualTo(2); + JsonNode request = objectMapper.readTree(byteLists.get(0)); + assertThat(request.get("rpcName").asText()).isEqualTo(RPC_NAME); + assertThat(request.get("rpcName").asText()).isEqualTo(RPC_NAME); + JsonNode response = objectMapper.readTree(byteLists.get(1)); + assertThat(response.get("message").asText()).isEqualTo("Received response"); + assertThat(response.get("rpcName").asText()).isEqualTo(RPC_NAME); + assertThat(response.get("response.status").asText()).isEqualTo("200"); + + testAppender.stop(); + } + private String echoGrpc(String value) { EchoResponse response = grpcClient.echo(EchoRequest.newBuilder().setContent(value).build()); return response.getContent(); diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/TestMdcAppender.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/TestMdcAppender.java new file mode 100644 index 0000000000..460b801129 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/TestMdcAppender.java @@ -0,0 +1,42 @@ +package com.google.showcase.v1beta1.it.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.google.cloud.sdk.logging.SDKLoggingMdcJsonProvider; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.logstash.logback.encoder.LogstashEncoder; + +public class TestMdcAppender extends AppenderBase { + + private final LogstashEncoder encoder; + private final List byteLists; + + public TestMdcAppender() { + encoder = new LogstashEncoder(); + encoder.addProvider(new SDKLoggingMdcJsonProvider()); + byteLists = new ArrayList<>(); + } + + @Override + public void start() { + encoder.start(); + super.start(); + } + + @Override + public void stop() { + encoder.stop(); + super.stop(); + } + + @Override + protected void append(ILoggingEvent eventObject) { + byteLists.add(encoder.encode(eventObject)); + } + + public List getByteLists() { + return Collections.unmodifiableList(byteLists); + } +}