Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] add client side logging #3403

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
59b5d73
poc: add log in grpc interceptor.
zhumin8 Nov 19, 2024
d34ec88
try out basics with httpjson interceptor.
zhumin8 Nov 20, 2024
b6417ae
implement conditional logging, use JUL when no binding. log formattin…
zhumin8 Nov 25, 2024
0c169ad
temp manual testing with showcase.
zhumin8 Nov 25, 2024
9ced175
simplify logic for conditional logger.
zhumin8 Nov 25, 2024
681f7d5
implement logging for basic case.
zhumin8 Nov 26, 2024
5c01ef2
add a JsonContextMapHandler to format in JUL case.
zhumin8 Nov 27, 2024
6605ef3
fix response payload for streaming. Add if guards to bypass log when …
zhumin8 Nov 27, 2024
ebbae15
when logging disabled, assign NOPLogger directly.
zhumin8 Nov 27, 2024
543298d
some temp settings for testing with showcase.
zhumin8 Dec 2, 2024
aac66f2
remove JUL wrapper for now. refactor getLogger(). Add tests for Loggi…
zhumin8 Dec 5, 2024
487614c
minor clean up.:
zhumin8 Dec 5, 2024
cc3465b
fix test after adding interceptor for logging for http.
zhumin8 Dec 5, 2024
211a3d1
cleanup and fix response header.
zhumin8 Dec 6, 2024
2970210
lint
zhumin8 Dec 6, 2024
32f8b7c
lint: license.
zhumin8 Dec 6, 2024
245c14d
refactor GrpcLoggingInterceptor for readability, intro LogData.
zhumin8 Dec 6, 2024
8358497
minor cleanups.
zhumin8 Dec 6, 2024
97087b2
GrpcLoggingInterceptor fix for thread safe, separate out debug loggin…
zhumin8 Dec 6, 2024
727a3e9
minor changes + add showcase it test.(need to run with env var, setup…
zhumin8 Dec 6, 2024
23bc111
refactor HttpJsonLoggingInterceptor. remove public modifier from logg…
zhumin8 Dec 6, 2024
bbe0700
add GrpcLoggingInterceptorTest and test interceptor structure.
zhumin8 Dec 8, 2024
3d7c7f9
fix status code '0'.
zhumin8 Dec 10, 2024
b870d81
merge logs in interceptors, record log data. remove request id.
zhumin8 Dec 10, 2024
56a2870
replace txt message to json message with all fields duplicated.
zhumin8 Dec 10, 2024
83eedf0
add try catch to logging methods
zhumin8 Dec 10, 2024
d85c848
add showcase tests. expose logging interceptors as public to setup te…
zhumin8 Dec 10, 2024
1919809
add internal api annotation.
zhumin8 Dec 10, 2024
613b6c8
lint
zhumin8 Dec 10, 2024
1169eaa
fix typo
zhumin8 Dec 11, 2024
a6b5433
remove unused test class.
zhumin8 Dec 11, 2024
fb0966e
try to exclude test from showcase-native.
zhumin8 Dec 11, 2024
77939fe
fix logback version.
zhumin8 Dec 11, 2024
04ef774
revert accidental change.
zhumin8 Dec 11, 2024
439e071
fix typo in ci.
zhumin8 Dec 11, 2024
673c9fe
cleanup gax pom. add tests.
zhumin8 Dec 11, 2024
c2b607b
add test.
zhumin8 Dec 11, 2024
3df39fa
Merge branch 'main' into client-log
zhumin8 Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ jobs:
-P enable-integration-tests \
--batch-mode \
--no-transfer-progress
# The `envVarLoggingTest` profile runs tests that require an environment variable
- name: Showcase integration tests - Logging
run: |
mvn verify -P '!showcase,enable-integration-tests,envVarLoggingTest' \
--batch-mode \
--no-transfer-progress
# Set the Env Var for this step only
env:
GOOGLE_SDK_JAVA_LOGGING: true

showcase-native:
runs-on: ubuntu-22.04
Expand Down
1 change: 1 addition & 0 deletions gapic-generator-java-pom-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<j2objc-annotations.version>3.0.0</j2objc-annotations.version>
<threetenbp.version>1.7.0</threetenbp.version>
<junit.version>5.11.3</junit.version>
<slf4j.version>2.0.16</slf4j.version>
</properties>

<developers>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2024 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.api.gax.grpc;

import com.google.api.core.InternalApi;
import com.google.api.gax.logging.LogData;
import com.google.api.gax.logging.LoggingUtils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.event.Level;

@InternalApi
public class GrpcLoggingInterceptor implements ClientInterceptor {

private static final Logger logger = LoggingUtils.getLogger(GrpcLoggingInterceptor.class);
private static final Gson gson = new Gson();

ClientCall.Listener<?> currentListener; // expose for test setup

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {

return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, callOptions)) {
LogData.Builder logDataBuilder = LogData.builder();

@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
logRequestInfo(method, logDataBuilder, logger);
recordRequestHeaders(headers, logDataBuilder);
SimpleForwardingClientCallListener<RespT> responseLoggingListener =
new SimpleForwardingClientCallListener<RespT>(responseListener) {
@Override
public void onHeaders(Metadata headers) {
recordResponseHeaders(headers, logDataBuilder);
super.onHeaders(headers);
}

@Override
public void onMessage(RespT message) {
recordResponsePayload(message, logDataBuilder);
super.onMessage(message);
}

@Override
public void onClose(Status status, Metadata trailers) {
try {
logResponse(status.getCode().toString(), logDataBuilder);
} finally {
logDataBuilder = null; // release resource
}
super.onClose(status, trailers);
}
};
currentListener = responseLoggingListener;
super.start(responseLoggingListener, headers);
}

@Override
public void sendMessage(ReqT message) {
logRequestDetails(message, logDataBuilder);
super.sendMessage(message);
}
};
}

// Helper methods for logging
// some duplications with http equivalent to avoid exposing as public method for now
<ReqT, RespT> void logRequestInfo(
MethodDescriptor<ReqT, RespT> method, LogData.Builder logDataBuilder, Logger logger) {
try {
if (logger.isInfoEnabled()) {
logDataBuilder.serviceName(method.getServiceName()).rpcName(method.getFullMethodName());

if (!logger.isDebugEnabled()) {
LoggingUtils.logWithMDC(
logger, Level.INFO, logDataBuilder.build().toMapRequest(), "Sending gRPC request");
}
}
} catch (Exception e) {
logger.error("Error logging request info (and headers)", e);
}
}

private void recordRequestHeaders(Metadata headers, LogData.Builder logDataBuilder) {
try {
if (logger.isDebugEnabled()) {
JsonObject requestHeaders = mapHeadersToJsonObject(headers);
logDataBuilder.requestHeaders(gson.toJson(requestHeaders));
}
} catch (Exception e) {
logger.error("Error recording request headers", e);
}
}

void recordResponseHeaders(Metadata headers, LogData.Builder logDataBuilder) {
try {
if (logger.isDebugEnabled()) {
JsonObject responseHeaders = mapHeadersToJsonObject(headers);
logDataBuilder.responseHeaders(gson.toJson(responseHeaders));
}
} catch (Exception e) {
logger.error("Error recording response headers", e);
}
}

<RespT> void recordResponsePayload(RespT message, LogData.Builder logDataBuilder) {
try {
if (logger.isDebugEnabled()) {
logDataBuilder.responsePayload(gson.toJsonTree(message));
}
} catch (Exception e) {
logger.error("Error recording response payload", e);
}
}

void logResponse(String statusCode, LogData.Builder logDataBuilder) {
try {

if (logger.isInfoEnabled()) {
logDataBuilder.responseStatus(statusCode);
}
if (logger.isInfoEnabled() && !logger.isDebugEnabled()) {
Map<String, String> responseData = logDataBuilder.build().toMapResponse();
LoggingUtils.logWithMDC(logger, Level.INFO, responseData, "Received Grpc response");
}
if (logger.isDebugEnabled()) {
Map<String, String> responsedDetailsMap = logDataBuilder.build().toMapResponse();
LoggingUtils.logWithMDC(logger, Level.DEBUG, responsedDetailsMap, "Received Grpc response");
}
} catch (Exception e) {
logger.error("Error logging request response", e);
}
}

<RespT> void logRequestDetails(RespT message, LogData.Builder logDataBuilder) {
try {
if (logger.isDebugEnabled()) {
logDataBuilder.requestPayload(gson.toJson(message));
Map<String, String> requestDetailsMap = logDataBuilder.build().toMapRequest();
LoggingUtils.logWithMDC(
logger, Level.DEBUG, requestDetailsMap, "Sending gRPC request: request payload");
}
} catch (Exception e) {
logger.error("Error logging request details", e);
}
}

private static JsonObject mapHeadersToJsonObject(Metadata headers) {
JsonObject jsonHeaders = new JsonObject();
headers
.keys()
.forEach(
key -> {
Metadata.Key<String> metadataKey =
Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
String headerValue = headers.get(metadataKey);
jsonHeaders.addProperty(key, headerValue);
});
return jsonHeaders;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ private ManagedChannel createSingleChannel() throws IOException {
builder =
builder
.intercept(new GrpcChannelUUIDInterceptor())
.intercept(new GrpcLoggingInterceptor())
.intercept(headerInterceptor)
.intercept(metadataHandlerInterceptor)
.userAgent(headerInterceptor.getUserAgentHeader())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2024 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.api.gax.grpc;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import ch.qos.logback.classic.Level;
import com.google.api.gax.grpc.testing.FakeMethodDescriptor;
import com.google.api.gax.logging.LogData;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptors;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class GrpcLoggingInterceptorTest {
@Mock private Channel channel;

@Mock private ClientCall<String, Integer> call;

private static final MethodDescriptor<String, Integer> method = FakeMethodDescriptor.create();

private static final Logger LOGGER = LoggerFactory.getLogger(GrpcLoggingInterceptorTest.class);
/** Sets up mocks. */
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
when(channel.newCall(Mockito.<MethodDescriptor<String, Integer>>any(), any(CallOptions.class)))
.thenReturn(call);
}

@Test
void testInterceptor_basic() {
GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor();
Channel intercepted = ClientInterceptors.intercept(channel, interceptor);
@SuppressWarnings("unchecked")
ClientCall.Listener<Integer> listener = mock(ClientCall.Listener.class);
ClientCall<String, Integer> interceptedCall = intercepted.newCall(method, CallOptions.DEFAULT);
// Simulate starting the call
interceptedCall.start(listener, new Metadata());
// Verify that the underlying call's start() method is invoked
verify(call).start(any(ClientCall.Listener.class), any(Metadata.class));

// Simulate sending a message
String requestMessage = "test request";
interceptedCall.sendMessage(requestMessage);
// Verify that the underlying call's sendMessage() method is invoked
verify(call).sendMessage(requestMessage);
}

@Test
void testInterceptor_responseListener() {
GrpcLoggingInterceptor interceptor = spy(new GrpcLoggingInterceptor());
Channel intercepted = ClientInterceptors.intercept(channel, interceptor);
@SuppressWarnings("unchecked")
ClientCall.Listener<Integer> listener = mock(ClientCall.Listener.class);
ClientCall<String, Integer> interceptedCall = intercepted.newCall(method, CallOptions.DEFAULT);
interceptedCall.start(listener, new Metadata());

// Simulate respond interceptor calls
Metadata responseHeaders = new Metadata();
responseHeaders.put(
Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER), "header-value");
interceptor.currentListener.onHeaders(responseHeaders);

interceptor.currentListener.onMessage(null);

Status status = Status.OK;
interceptor.currentListener.onClose(status, new Metadata());

// --- Verify that the response listener's methods were called ---
verify(interceptor).recordResponseHeaders(eq(responseHeaders), any(LogData.Builder.class));
verify(interceptor).recordResponsePayload(any(), any(LogData.Builder.class));
verify(interceptor).logResponse(eq(status.getCode().toString()), any(LogData.Builder.class));
}

@Test
void testLogRequestInfo() {

TestAppender testAppender = setupTestLogger(GrpcLoggingInterceptorTest.class);
GrpcLoggingInterceptor interceptor = new GrpcLoggingInterceptor();
interceptor.logRequestInfo(method, LogData.builder(), LOGGER);

Assertions.assertEquals(1, testAppender.events.size());
assertEquals(Level.INFO, testAppender.events.get(0).getLevel());
assertEquals(
"{\"serviceName\":\"FakeClient\",\"message\":\"Sending gRPC request\",\"rpcName\":\"FakeClient/fake-method\"}",
testAppender.events.get(0).getMessage());
testAppender.stop();
}

private TestAppender setupTestLogger(Class<?> clazz) {
TestAppender testAppender = new TestAppender();
testAppender.start();
Logger logger = LoggerFactory.getLogger(clazz);
((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
return testAppender;
}
}
Loading
Loading