/* * Copyright 2022 The gRPC Authors * * 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 * * http://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 io.grpc.gcp.observability; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import io.grpc.ManagedChannelBuilder; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.StaticTestingClassLoader; import io.grpc.gcp.observability.interceptors.ConfigFilterHelper; import io.grpc.gcp.observability.interceptors.ConfigFilterHelper.FilterParams; import io.grpc.gcp.observability.interceptors.InternalLoggingChannelInterceptor; import io.grpc.gcp.observability.interceptors.InternalLoggingServerInterceptor; import io.grpc.gcp.observability.interceptors.LogHelper; import io.grpc.gcp.observability.logging.GcpLogSink; import io.grpc.gcp.observability.logging.Sink; import io.grpc.gcp.observability.logging.TraceLoggingHelper; import io.grpc.observabilitylog.v1.GrpcLogRecord; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.protobuf.SimpleServiceGrpc; import io.opencensus.trace.SpanContext; import java.io.IOException; import java.util.Collections; import java.util.regex.Pattern; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @RunWith(JUnit4.class) public class LoggingTest { @ClassRule public static final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); private static final String PROJECT_ID = "PROJECT"; private static final ImmutableMap CUSTOM_TAGS = ImmutableMap.of( "KEY1", "Value1", "KEY2", "VALUE2"); private final StaticTestingClassLoader classLoader = new StaticTestingClassLoader(getClass().getClassLoader(), Pattern.compile("io\\.grpc\\..*")); /** * Cloud logging test using GlobalInterceptors. * *

Ignoring test, because it calls external Cloud Logging APIs. * To test cloud logging setup locally, * 1. Set up Cloud auth credentials * 2. Assign permissions to service account to write logs to project specified by * variable PROJECT_ID * 3. Comment @Ignore annotation * 4. This test is expected to pass when ran with above setup. This has been verified manually. *

*/ @Ignore @Test public void clientServer_interceptorCalled_logAlways() throws Exception { Class runnable = classLoader.loadClass(LoggingTest.StaticTestingClassEndtoEndLogging.class.getName()); ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); } @Test public void clientServer_interceptorCalled_logNever() throws Exception { Class runnable = classLoader.loadClass(LoggingTest.StaticTestingClassLogNever.class.getName()); ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); } @Test public void clientServer_interceptorCalled_logEvents_usingMockSink() throws Exception { Class runnable = classLoader.loadClass(StaticTestingClassLogEventsUsingMockSink.class.getName()); ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); } // UsedReflectively public static final class StaticTestingClassEndtoEndLogging implements Runnable { @Override public void run() { ObservabilityConfig config = mock(ObservabilityConfig.class); when(config.getCustomTags()).thenReturn(CUSTOM_TAGS); Sink sink = new GcpLogSink( PROJECT_ID, config, Collections.emptySet(), mock(TraceLoggingHelper.class)); LogHelper spyLogHelper = spy(new LogHelper(sink)); ConfigFilterHelper mockFilterHelper = mock(ConfigFilterHelper.class); InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); InternalLoggingServerInterceptor.Factory serverInterceptorFactory = new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); when(config.isEnableCloudLogging()).thenReturn(true); FilterParams logAlwaysFilterParams = FilterParams.create(true, 1024, 10); when(mockFilterHelper.logRpcMethod(anyString(), eq(true))) .thenReturn(logAlwaysFilterParams); when(mockFilterHelper.logRpcMethod(anyString(), eq(false))) .thenReturn(logAlwaysFilterParams); try (GcpObservability unused = GcpObservability.grpcInit( sink, config, channelInterceptorFactory, serverInterceptorFactory)) { Server server = ServerBuilder.forPort(0) .addService(new ObservabilityTestHelper.SimpleServiceImpl()) .build() .start(); int port = cleanupRule.register(server).getPort(); SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc.newBlockingStub( cleanupRule.register( ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) .isEqualTo("Hello buddy"); assertThat(Mockito.mockingDetails(spyLogHelper).getInvocations().size()).isGreaterThan(11); } catch (IOException e) { throw new AssertionError("Exception while testing logging", e); } } } public static final class StaticTestingClassLogNever implements Runnable { @Override public void run() { Sink mockSink = mock(GcpLogSink.class); ObservabilityConfig config = mock(ObservabilityConfig.class); LogHelper spyLogHelper = spy(new LogHelper(mockSink)); ConfigFilterHelper mockFilterHelper = mock(ConfigFilterHelper.class); InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); InternalLoggingServerInterceptor.Factory serverInterceptorFactory = new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); when(config.isEnableCloudLogging()).thenReturn(true); FilterParams logNeverFilterParams = FilterParams.create(false, 0, 0); when(mockFilterHelper.logRpcMethod(anyString(), eq(true))) .thenReturn(logNeverFilterParams); when(mockFilterHelper.logRpcMethod(anyString(), eq(false))) .thenReturn(logNeverFilterParams); try (GcpObservability unused = GcpObservability.grpcInit( mockSink, config, channelInterceptorFactory, serverInterceptorFactory)) { Server server = ServerBuilder.forPort(0) .addService(new ObservabilityTestHelper.SimpleServiceImpl()) .build() .start(); int port = cleanupRule.register(server).getPort(); SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc.newBlockingStub( cleanupRule.register( ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) .isEqualTo("Hello buddy"); verifyNoInteractions(spyLogHelper); verifyNoInteractions(mockSink); } catch (IOException e) { throw new AssertionError("Exception while testing logging event filter", e); } } } public static final class StaticTestingClassLogEventsUsingMockSink implements Runnable { @Override public void run() { Sink mockSink = mock(GcpLogSink.class); ObservabilityConfig config = mock(ObservabilityConfig.class); LogHelper spyLogHelper = spy(new LogHelper(mockSink)); ConfigFilterHelper mockFilterHelper2 = mock(ConfigFilterHelper.class); InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper2); InternalLoggingServerInterceptor.Factory serverInterceptorFactory = new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper2); when(config.isEnableCloudLogging()).thenReturn(true); FilterParams logAlwaysFilterParams = FilterParams.create(true, 0, 0); when(mockFilterHelper2.logRpcMethod(anyString(), eq(true))) .thenReturn(logAlwaysFilterParams); when(mockFilterHelper2.logRpcMethod(anyString(), eq(false))) .thenReturn(logAlwaysFilterParams); try (GcpObservability observability = GcpObservability.grpcInit( mockSink, config, channelInterceptorFactory, serverInterceptorFactory)) { Server server = ServerBuilder.forPort(0) .addService(new ObservabilityTestHelper.SimpleServiceImpl()) .build() .start(); int port = cleanupRule.register(server).getPort(); SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc.newBlockingStub( cleanupRule.register( ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) .isEqualTo("Hello buddy"); // Total number of calls should have been 14 (6 from client and 6 from server) // Since cancel is not invoked, it will be 12. // Request message(Total count:2 (1 from client and 1 from server) and Response // message(count:2) // events are not in the event_types list, i.e 14 - 2(cancel) - 2(req_msg) - 2(resp_msg) // = 8 assertThat(Mockito.mockingDetails(mockSink).getInvocations().size()).isEqualTo(12); ArgumentCaptor captor = ArgumentCaptor.forClass(GrpcLogRecord.class); verify(mockSink, times(12)).write(captor.capture(), AdditionalMatchers.or(ArgumentMatchers.isNull(), ArgumentMatchers.any(SpanContext.class))); for (GrpcLogRecord record : captor.getAllValues()) { assertThat(record.getType()).isInstanceOf(GrpcLogRecord.EventType.class); assertThat(record.getLogger()).isInstanceOf(GrpcLogRecord.EventLogger.class); } } catch (IOException e) { throw new AssertionError("Exception while testing logging using mock sink", e); } } } }