1 /* 2 * Copyright 2022 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.gcp.observability; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.mockito.ArgumentMatchers.anyString; 21 import static org.mockito.ArgumentMatchers.eq; 22 import static org.mockito.Mockito.mock; 23 import static org.mockito.Mockito.spy; 24 import static org.mockito.Mockito.times; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.verifyNoInteractions; 27 import static org.mockito.Mockito.when; 28 29 import com.google.common.collect.ImmutableMap; 30 import io.grpc.ManagedChannelBuilder; 31 import io.grpc.Server; 32 import io.grpc.ServerBuilder; 33 import io.grpc.StaticTestingClassLoader; 34 import io.grpc.gcp.observability.interceptors.ConfigFilterHelper; 35 import io.grpc.gcp.observability.interceptors.ConfigFilterHelper.FilterParams; 36 import io.grpc.gcp.observability.interceptors.InternalLoggingChannelInterceptor; 37 import io.grpc.gcp.observability.interceptors.InternalLoggingServerInterceptor; 38 import io.grpc.gcp.observability.interceptors.LogHelper; 39 import io.grpc.gcp.observability.logging.GcpLogSink; 40 import io.grpc.gcp.observability.logging.Sink; 41 import io.grpc.gcp.observability.logging.TraceLoggingHelper; 42 import io.grpc.observabilitylog.v1.GrpcLogRecord; 43 import io.grpc.testing.GrpcCleanupRule; 44 import io.grpc.testing.protobuf.SimpleServiceGrpc; 45 import io.opencensus.trace.SpanContext; 46 import java.io.IOException; 47 import java.util.Collections; 48 import java.util.regex.Pattern; 49 import org.junit.ClassRule; 50 import org.junit.Ignore; 51 import org.junit.Test; 52 import org.junit.runner.RunWith; 53 import org.junit.runners.JUnit4; 54 import org.mockito.AdditionalMatchers; 55 import org.mockito.ArgumentCaptor; 56 import org.mockito.ArgumentMatchers; 57 import org.mockito.Mockito; 58 59 @RunWith(JUnit4.class) 60 public class LoggingTest { 61 62 @ClassRule 63 public static final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); 64 65 private static final String PROJECT_ID = "PROJECT"; 66 private static final ImmutableMap<String, String> CUSTOM_TAGS = ImmutableMap.of( 67 "KEY1", "Value1", 68 "KEY2", "VALUE2"); 69 70 private final StaticTestingClassLoader classLoader = 71 new StaticTestingClassLoader(getClass().getClassLoader(), Pattern.compile("io\\.grpc\\..*")); 72 73 /** 74 * Cloud logging test using GlobalInterceptors. 75 * 76 * <p> Ignoring test, because it calls external Cloud Logging APIs. 77 * To test cloud logging setup locally, 78 * 1. Set up Cloud auth credentials 79 * 2. Assign permissions to service account to write logs to project specified by 80 * variable PROJECT_ID 81 * 3. Comment @Ignore annotation 82 * 4. This test is expected to pass when ran with above setup. This has been verified manually. 83 * </p> 84 */ 85 @Ignore 86 @Test clientServer_interceptorCalled_logAlways()87 public void clientServer_interceptorCalled_logAlways() throws Exception { 88 Class<?> runnable = 89 classLoader.loadClass(LoggingTest.StaticTestingClassEndtoEndLogging.class.getName()); 90 ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); 91 } 92 93 @Test clientServer_interceptorCalled_logNever()94 public void clientServer_interceptorCalled_logNever() throws Exception { 95 Class<?> runnable = 96 classLoader.loadClass(LoggingTest.StaticTestingClassLogNever.class.getName()); 97 ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); 98 } 99 100 @Test clientServer_interceptorCalled_logEvents_usingMockSink()101 public void clientServer_interceptorCalled_logEvents_usingMockSink() throws Exception { 102 Class<?> runnable = 103 classLoader.loadClass(StaticTestingClassLogEventsUsingMockSink.class.getName()); 104 ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); 105 } 106 107 // UsedReflectively 108 public static final class StaticTestingClassEndtoEndLogging implements Runnable { 109 110 @Override run()111 public void run() { 112 ObservabilityConfig config = mock(ObservabilityConfig.class); 113 when(config.getCustomTags()).thenReturn(CUSTOM_TAGS); 114 Sink sink = 115 new GcpLogSink( 116 PROJECT_ID, config, Collections.emptySet(), 117 mock(TraceLoggingHelper.class)); 118 LogHelper spyLogHelper = spy(new LogHelper(sink)); 119 ConfigFilterHelper mockFilterHelper = mock(ConfigFilterHelper.class); 120 InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = 121 new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); 122 InternalLoggingServerInterceptor.Factory serverInterceptorFactory = 123 new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); 124 125 when(config.isEnableCloudLogging()).thenReturn(true); 126 FilterParams logAlwaysFilterParams = FilterParams.create(true, 1024, 10); 127 when(mockFilterHelper.logRpcMethod(anyString(), eq(true))) 128 .thenReturn(logAlwaysFilterParams); 129 when(mockFilterHelper.logRpcMethod(anyString(), eq(false))) 130 .thenReturn(logAlwaysFilterParams); 131 132 try (GcpObservability unused = 133 GcpObservability.grpcInit( 134 sink, config, channelInterceptorFactory, serverInterceptorFactory)) { 135 Server server = 136 ServerBuilder.forPort(0) 137 .addService(new ObservabilityTestHelper.SimpleServiceImpl()) 138 .build() 139 .start(); 140 int port = cleanupRule.register(server).getPort(); 141 SimpleServiceGrpc.SimpleServiceBlockingStub stub = 142 SimpleServiceGrpc.newBlockingStub( 143 cleanupRule.register( 144 ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); 145 assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) 146 .isEqualTo("Hello buddy"); 147 assertThat(Mockito.mockingDetails(spyLogHelper).getInvocations().size()).isGreaterThan(11); 148 } catch (IOException e) { 149 throw new AssertionError("Exception while testing logging", e); 150 } 151 } 152 } 153 154 public static final class StaticTestingClassLogNever implements Runnable { 155 156 @Override run()157 public void run() { 158 Sink mockSink = mock(GcpLogSink.class); 159 ObservabilityConfig config = mock(ObservabilityConfig.class); 160 LogHelper spyLogHelper = spy(new LogHelper(mockSink)); 161 ConfigFilterHelper mockFilterHelper = mock(ConfigFilterHelper.class); 162 InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = 163 new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); 164 InternalLoggingServerInterceptor.Factory serverInterceptorFactory = 165 new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper); 166 167 when(config.isEnableCloudLogging()).thenReturn(true); 168 FilterParams logNeverFilterParams = FilterParams.create(false, 0, 0); 169 when(mockFilterHelper.logRpcMethod(anyString(), eq(true))) 170 .thenReturn(logNeverFilterParams); 171 when(mockFilterHelper.logRpcMethod(anyString(), eq(false))) 172 .thenReturn(logNeverFilterParams); 173 174 try (GcpObservability unused = 175 GcpObservability.grpcInit( 176 mockSink, config, channelInterceptorFactory, serverInterceptorFactory)) { 177 Server server = 178 ServerBuilder.forPort(0) 179 .addService(new ObservabilityTestHelper.SimpleServiceImpl()) 180 .build() 181 .start(); 182 int port = cleanupRule.register(server).getPort(); 183 SimpleServiceGrpc.SimpleServiceBlockingStub stub = 184 SimpleServiceGrpc.newBlockingStub( 185 cleanupRule.register( 186 ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); 187 assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) 188 .isEqualTo("Hello buddy"); 189 verifyNoInteractions(spyLogHelper); 190 verifyNoInteractions(mockSink); 191 } catch (IOException e) { 192 throw new AssertionError("Exception while testing logging event filter", e); 193 } 194 } 195 } 196 197 public static final class StaticTestingClassLogEventsUsingMockSink implements Runnable { 198 199 @Override run()200 public void run() { 201 Sink mockSink = mock(GcpLogSink.class); 202 ObservabilityConfig config = mock(ObservabilityConfig.class); 203 LogHelper spyLogHelper = spy(new LogHelper(mockSink)); 204 ConfigFilterHelper mockFilterHelper2 = mock(ConfigFilterHelper.class); 205 InternalLoggingChannelInterceptor.Factory channelInterceptorFactory = 206 new InternalLoggingChannelInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper2); 207 InternalLoggingServerInterceptor.Factory serverInterceptorFactory = 208 new InternalLoggingServerInterceptor.FactoryImpl(spyLogHelper, mockFilterHelper2); 209 210 when(config.isEnableCloudLogging()).thenReturn(true); 211 FilterParams logAlwaysFilterParams = FilterParams.create(true, 0, 0); 212 when(mockFilterHelper2.logRpcMethod(anyString(), eq(true))) 213 .thenReturn(logAlwaysFilterParams); 214 when(mockFilterHelper2.logRpcMethod(anyString(), eq(false))) 215 .thenReturn(logAlwaysFilterParams); 216 217 try (GcpObservability observability = 218 GcpObservability.grpcInit( 219 mockSink, config, channelInterceptorFactory, serverInterceptorFactory)) { 220 Server server = 221 ServerBuilder.forPort(0) 222 .addService(new ObservabilityTestHelper.SimpleServiceImpl()) 223 .build() 224 .start(); 225 int port = cleanupRule.register(server).getPort(); 226 SimpleServiceGrpc.SimpleServiceBlockingStub stub = 227 SimpleServiceGrpc.newBlockingStub( 228 cleanupRule.register( 229 ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build())); 230 assertThat(ObservabilityTestHelper.makeUnaryRpcViaClientStub("buddy", stub)) 231 .isEqualTo("Hello buddy"); 232 // Total number of calls should have been 14 (6 from client and 6 from server) 233 // Since cancel is not invoked, it will be 12. 234 // Request message(Total count:2 (1 from client and 1 from server) and Response 235 // message(count:2) 236 // events are not in the event_types list, i.e 14 - 2(cancel) - 2(req_msg) - 2(resp_msg) 237 // = 8 238 assertThat(Mockito.mockingDetails(mockSink).getInvocations().size()).isEqualTo(12); 239 ArgumentCaptor<GrpcLogRecord> captor = ArgumentCaptor.forClass(GrpcLogRecord.class); 240 verify(mockSink, times(12)).write(captor.capture(), 241 AdditionalMatchers.or(ArgumentMatchers.isNull(), 242 ArgumentMatchers.any(SpanContext.class))); 243 for (GrpcLogRecord record : captor.getAllValues()) { 244 assertThat(record.getType()).isInstanceOf(GrpcLogRecord.EventType.class); 245 assertThat(record.getLogger()).isInstanceOf(GrpcLogRecord.EventLogger.class); 246 } 247 } catch (IOException e) { 248 throw new AssertionError("Exception while testing logging using mock sink", e); 249 } 250 } 251 } 252 } 253