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);
+ }
+}