Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 1 | /* |
Carl Mastrangelo | 3bfd630 | 2017-05-31 13:29:01 -0700 | [diff] [blame] | 2 | * Copyright 2017, gRPC Authors All rights reserved. |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 3 | * |
Carl Mastrangelo | 3bfd630 | 2017-05-31 13:29:01 -0700 | [diff] [blame] | 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 |
Carl Mastrangelo | 166108a | 2017-06-01 14:28:37 -0700 | [diff] [blame] | 7 | * |
Carl Mastrangelo | 3bfd630 | 2017-05-31 13:29:01 -0700 | [diff] [blame] | 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
Carl Mastrangelo | 166108a | 2017-06-01 14:28:37 -0700 | [diff] [blame] | 9 | * |
Carl Mastrangelo | 3bfd630 | 2017-05-31 13:29:01 -0700 | [diff] [blame] | 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. |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 15 | */ |
| 16 | |
| 17 | package io.grpc.internal; |
| 18 | |
Yang Song | 4a0cf0b | 2017-06-06 18:16:55 -0700 | [diff] [blame^] | 19 | import static com.google.instrumentation.stats.ContextUtils.STATS_CONTEXT_KEY; |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 20 | import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| 21 | import static org.junit.Assert.assertEquals; |
| 22 | import static org.junit.Assert.assertFalse; |
| 23 | import static org.junit.Assert.assertNotNull; |
| 24 | import static org.junit.Assert.assertNotSame; |
| 25 | import static org.junit.Assert.assertNull; |
| 26 | import static org.junit.Assert.assertSame; |
| 27 | import static org.junit.Assert.assertTrue; |
| 28 | import static org.junit.Assert.fail; |
| 29 | import static org.mockito.Matchers.any; |
| 30 | import static org.mockito.Matchers.anyString; |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 31 | import static org.mockito.Matchers.argThat; |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 32 | import static org.mockito.Matchers.eq; |
| 33 | import static org.mockito.Matchers.isNull; |
| 34 | import static org.mockito.Matchers.same; |
| 35 | import static org.mockito.Mockito.never; |
| 36 | import static org.mockito.Mockito.spy; |
| 37 | import static org.mockito.Mockito.verify; |
| 38 | import static org.mockito.Mockito.verifyNoMoreInteractions; |
| 39 | import static org.mockito.Mockito.verifyZeroInteractions; |
| 40 | import static org.mockito.Mockito.when; |
| 41 | |
| 42 | import com.google.instrumentation.stats.RpcConstants; |
| 43 | import com.google.instrumentation.stats.StatsContext; |
| 44 | import com.google.instrumentation.stats.TagValue; |
| 45 | import com.google.instrumentation.trace.Annotation; |
| 46 | import com.google.instrumentation.trace.AttributeValue; |
| 47 | import com.google.instrumentation.trace.BinaryPropagationHandler; |
| 48 | import com.google.instrumentation.trace.ContextUtils; |
| 49 | import com.google.instrumentation.trace.EndSpanOptions; |
| 50 | import com.google.instrumentation.trace.Link; |
| 51 | import com.google.instrumentation.trace.NetworkEvent; |
| 52 | import com.google.instrumentation.trace.Span; |
| 53 | import com.google.instrumentation.trace.SpanContext; |
| 54 | import com.google.instrumentation.trace.SpanFactory; |
| 55 | import com.google.instrumentation.trace.SpanId; |
| 56 | import com.google.instrumentation.trace.StartSpanOptions; |
| 57 | import com.google.instrumentation.trace.TraceId; |
| 58 | import com.google.instrumentation.trace.TraceOptions; |
| 59 | import com.google.instrumentation.trace.Tracer; |
| 60 | import io.grpc.CallOptions; |
| 61 | import io.grpc.Channel; |
| 62 | import io.grpc.ClientCall; |
| 63 | import io.grpc.ClientInterceptor; |
| 64 | import io.grpc.ClientInterceptors; |
| 65 | import io.grpc.ClientStreamTracer; |
| 66 | import io.grpc.Context; |
| 67 | import io.grpc.Metadata; |
| 68 | import io.grpc.MethodDescriptor; |
| 69 | import io.grpc.ServerCall; |
| 70 | import io.grpc.ServerCallHandler; |
| 71 | import io.grpc.ServerServiceDefinition; |
| 72 | import io.grpc.ServerStreamTracer; |
| 73 | import io.grpc.Status; |
| 74 | import io.grpc.internal.testing.StatsTestUtils; |
| 75 | import io.grpc.internal.testing.StatsTestUtils.FakeStatsContextFactory; |
| 76 | import io.grpc.testing.GrpcServerRule; |
| 77 | import java.io.ByteArrayInputStream; |
| 78 | import java.io.InputStream; |
| 79 | import java.text.ParseException; |
| 80 | import java.util.Map; |
| 81 | import java.util.Random; |
| 82 | import java.util.concurrent.atomic.AtomicReference; |
| 83 | import javax.annotation.Nullable; |
| 84 | import org.junit.After; |
| 85 | import org.junit.Before; |
| 86 | import org.junit.Rule; |
| 87 | import org.junit.Test; |
| 88 | import org.junit.runner.RunWith; |
| 89 | import org.junit.runners.JUnit4; |
| 90 | import org.mockito.ArgumentCaptor; |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 91 | import org.mockito.ArgumentMatcher; |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 92 | import org.mockito.Captor; |
| 93 | import org.mockito.Mock; |
| 94 | import org.mockito.MockitoAnnotations; |
| 95 | |
| 96 | /** |
| 97 | * Test for {@link CensusStatsModule} and {@link CensusTracingModule}. |
| 98 | */ |
| 99 | @RunWith(JUnit4.class) |
| 100 | public class CensusModulesTest { |
| 101 | private static final CallOptions.Key<String> CUSTOM_OPTION = |
| 102 | CallOptions.Key.of("option1", "default"); |
| 103 | private static final CallOptions CALL_OPTIONS = |
| 104 | CallOptions.DEFAULT.withOption(CUSTOM_OPTION, "customvalue"); |
| 105 | |
| 106 | private static class StringInputStream extends InputStream { |
| 107 | final String string; |
| 108 | |
| 109 | StringInputStream(String string) { |
| 110 | this.string = string; |
| 111 | } |
| 112 | |
| 113 | @Override |
| 114 | public int read() { |
| 115 | // InProcessTransport doesn't actually read bytes from the InputStream. The InputStream is |
| 116 | // passed to the InProcess server and consumed by MARSHALLER.parse(). |
| 117 | throw new UnsupportedOperationException("Should not be called"); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | private static final MethodDescriptor.Marshaller<String> MARSHALLER = |
| 122 | new MethodDescriptor.Marshaller<String>() { |
| 123 | @Override |
| 124 | public InputStream stream(String value) { |
| 125 | return new StringInputStream(value); |
| 126 | } |
| 127 | |
| 128 | @Override |
| 129 | public String parse(InputStream stream) { |
| 130 | return ((StringInputStream) stream).string; |
| 131 | } |
| 132 | }; |
| 133 | |
| 134 | private final MethodDescriptor<String, String> method = MethodDescriptor.create( |
| 135 | MethodDescriptor.MethodType.UNKNOWN, "package1.service2/method3", |
| 136 | MARSHALLER, MARSHALLER); |
| 137 | private final FakeClock fakeClock = new FakeClock(); |
| 138 | private final FakeStatsContextFactory statsCtxFactory = new FakeStatsContextFactory(); |
| 139 | private final Random random = new Random(0); |
| 140 | private final SpanContext fakeClientSpanContext = |
| 141 | SpanContext.create( |
| 142 | TraceId.generateRandomId(random), SpanId.generateRandomId(random), |
| 143 | TraceOptions.builder().build()); |
| 144 | private final SpanContext fakeClientParentSpanContext = |
| 145 | SpanContext.create( |
| 146 | TraceId.generateRandomId(random), SpanId.generateRandomId(random), |
| 147 | TraceOptions.builder().build()); |
| 148 | private final SpanContext fakeServerSpanContext = |
| 149 | SpanContext.create( |
| 150 | TraceId.generateRandomId(random), SpanId.generateRandomId(random), |
| 151 | TraceOptions.builder().build()); |
| 152 | private final SpanContext fakeServerParentSpanContext = |
| 153 | SpanContext.create( |
| 154 | TraceId.generateRandomId(random), SpanId.generateRandomId(random), |
| 155 | TraceOptions.builder().build()); |
| 156 | private final Span fakeClientSpan = new FakeSpan(fakeClientSpanContext); |
| 157 | private final Span fakeServerSpan = new FakeSpan(fakeServerSpanContext); |
| 158 | private final Span fakeClientParentSpan = new FakeSpan(fakeClientParentSpanContext); |
| 159 | private final Span fakeServerParentSpan = new FakeSpan(fakeServerParentSpanContext); |
| 160 | private final Span spyClientSpan = spy(fakeClientSpan); |
| 161 | private final Span spyServerSpan = spy(fakeServerSpan); |
| 162 | private final byte[] binarySpanContext = new byte[]{3, 1, 5}; |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 163 | private final ArgumentMatcher<StartSpanOptions> startSpanOptionsMatcher = |
| 164 | new ArgumentMatcher<StartSpanOptions>() { |
| 165 | @Override |
| 166 | public boolean matches(Object argument) { |
| 167 | return Boolean.TRUE.equals(((StartSpanOptions) argument).getRecordEvents()); |
| 168 | } |
| 169 | }; |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 170 | |
| 171 | @Rule |
| 172 | public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor(); |
| 173 | |
| 174 | @Mock |
| 175 | private AccessibleSpanFactory mockSpanFactory; |
| 176 | @Mock |
| 177 | private BinaryPropagationHandler mockTracingPropagationHandler; |
| 178 | @Mock |
| 179 | private ClientCall.Listener<String> mockClientCallListener; |
| 180 | @Mock |
| 181 | private ServerCall.Listener<String> mockServerCallListener; |
| 182 | @Captor |
| 183 | private ArgumentCaptor<CallOptions> callOptionsCaptor; |
| 184 | @Captor |
| 185 | private ArgumentCaptor<ClientCall.Listener<String>> clientCallListenerCaptor; |
| 186 | @Captor |
| 187 | private ArgumentCaptor<Status> statusCaptor; |
| 188 | |
| 189 | private Tracer tracer; |
| 190 | private CensusStatsModule censusStats; |
| 191 | private CensusTracingModule censusTracing; |
| 192 | |
| 193 | @Before |
| 194 | @SuppressWarnings("unchecked") |
| 195 | public void setUp() throws Exception { |
| 196 | MockitoAnnotations.initMocks(this); |
| 197 | when(mockSpanFactory.startSpan(any(Span.class), anyString(), any(StartSpanOptions.class))) |
| 198 | .thenReturn(spyClientSpan); |
| 199 | when( |
| 200 | mockSpanFactory.startSpanWithRemoteParent( |
| 201 | any(SpanContext.class), anyString(), any(StartSpanOptions.class))) |
| 202 | .thenReturn(spyServerSpan); |
| 203 | when(mockTracingPropagationHandler.toBinaryValue(any(SpanContext.class))) |
| 204 | .thenReturn(binarySpanContext); |
| 205 | when(mockTracingPropagationHandler.fromBinaryValue(any(byte[].class))) |
| 206 | .thenReturn(fakeServerParentSpanContext); |
| 207 | tracer = new Tracer(mockSpanFactory) {}; |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 208 | censusStats = new CensusStatsModule(statsCtxFactory, fakeClock.getStopwatchSupplier(), true); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 209 | censusTracing = new CensusTracingModule(tracer, mockTracingPropagationHandler); |
| 210 | } |
| 211 | |
| 212 | @After |
| 213 | public void wrapUp() { |
| 214 | assertNull(statsCtxFactory.pollRecord()); |
| 215 | } |
| 216 | |
| 217 | @Test |
| 218 | public void clientInterceptorNoCustomTag() { |
| 219 | testClientInterceptors(false); |
| 220 | } |
| 221 | |
| 222 | @Test |
| 223 | public void clientInterceptorCustomTag() { |
| 224 | testClientInterceptors(true); |
| 225 | } |
| 226 | |
| 227 | // Test that Census ClientInterceptors uses the StatsContext and Span out of the current Context |
| 228 | // to create the ClientCallTracer, and that it intercepts ClientCall.Listener.onClose() to call |
| 229 | // ClientCallTracer.callEnded(). |
| 230 | private void testClientInterceptors(boolean nonDefaultContext) { |
| 231 | grpcServerRule.getServiceRegistry().addService( |
| 232 | ServerServiceDefinition.builder("package1.service2").addMethod( |
| 233 | method, new ServerCallHandler<String, String>() { |
| 234 | @Override |
| 235 | public ServerCall.Listener<String> startCall( |
| 236 | ServerCall<String, String> call, Metadata headers) { |
| 237 | call.sendHeaders(new Metadata()); |
| 238 | call.sendMessage("Hello"); |
| 239 | call.close( |
| 240 | Status.PERMISSION_DENIED.withDescription("No you don't"), new Metadata()); |
| 241 | return mockServerCallListener; |
| 242 | } |
| 243 | }).build()); |
| 244 | |
| 245 | final AtomicReference<CallOptions> capturedCallOptions = new AtomicReference<CallOptions>(); |
| 246 | ClientInterceptor callOptionsCaptureInterceptor = new ClientInterceptor() { |
| 247 | @Override |
| 248 | public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( |
| 249 | MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { |
| 250 | capturedCallOptions.set(callOptions); |
| 251 | return next.newCall(method, callOptions); |
| 252 | } |
| 253 | }; |
| 254 | Channel interceptedChannel = |
| 255 | ClientInterceptors.intercept( |
| 256 | grpcServerRule.getChannel(), callOptionsCaptureInterceptor, |
| 257 | censusStats.getClientInterceptor(), censusTracing.getClientInterceptor()); |
| 258 | ClientCall<String, String> call; |
| 259 | if (nonDefaultContext) { |
| 260 | Context ctx = |
| 261 | Context.ROOT.withValues( |
Yang Song | 4a0cf0b | 2017-06-06 18:16:55 -0700 | [diff] [blame^] | 262 | STATS_CONTEXT_KEY, |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 263 | statsCtxFactory.getDefault().with( |
| 264 | StatsTestUtils.EXTRA_TAG, TagValue.create("extra value")), |
| 265 | ContextUtils.CONTEXT_SPAN_KEY, |
| 266 | fakeClientParentSpan); |
| 267 | Context origCtx = ctx.attach(); |
| 268 | try { |
| 269 | call = interceptedChannel.newCall(method, CALL_OPTIONS); |
| 270 | } finally { |
| 271 | ctx.detach(origCtx); |
| 272 | } |
| 273 | } else { |
Yang Song | 4a0cf0b | 2017-06-06 18:16:55 -0700 | [diff] [blame^] | 274 | assertNull(STATS_CONTEXT_KEY.get()); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 275 | assertNull(ContextUtils.CONTEXT_SPAN_KEY.get()); |
| 276 | call = interceptedChannel.newCall(method, CALL_OPTIONS); |
| 277 | } |
| 278 | |
| 279 | // The interceptor adds tracer factory to CallOptions |
| 280 | assertEquals("customvalue", capturedCallOptions.get().getOption(CUSTOM_OPTION)); |
| 281 | assertEquals(2, capturedCallOptions.get().getStreamTracerFactories().size()); |
| 282 | assertTrue( |
| 283 | capturedCallOptions.get().getStreamTracerFactories().get(0) |
| 284 | instanceof CensusTracingModule.ClientCallTracer); |
| 285 | assertTrue( |
| 286 | capturedCallOptions.get().getStreamTracerFactories().get(1) |
| 287 | instanceof CensusStatsModule.ClientCallTracer); |
| 288 | |
| 289 | // Make the call |
| 290 | Metadata headers = new Metadata(); |
| 291 | call.start(mockClientCallListener, headers); |
| 292 | assertNull(statsCtxFactory.pollRecord()); |
| 293 | if (nonDefaultContext) { |
| 294 | verify(mockSpanFactory).startSpan( |
| 295 | same(fakeClientParentSpan), eq("Sent.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 296 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 297 | } else { |
| 298 | verify(mockSpanFactory).startSpan( |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 299 | isNull(Span.class), eq("Sent.package1.service2.method3"), |
| 300 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 301 | } |
| 302 | verify(spyClientSpan, never()).end(any(EndSpanOptions.class)); |
| 303 | |
| 304 | // End the call |
| 305 | call.halfClose(); |
| 306 | call.request(1); |
| 307 | |
| 308 | verify(mockClientCallListener).onClose(statusCaptor.capture(), any(Metadata.class)); |
| 309 | Status status = statusCaptor.getValue(); |
| 310 | assertEquals(Status.Code.PERMISSION_DENIED, status.getCode()); |
| 311 | assertEquals("No you don't", status.getDescription()); |
| 312 | |
| 313 | // The intercepting listener calls callEnded() on ClientCallTracer, which records to Census. |
| 314 | StatsTestUtils.MetricsRecord record = statsCtxFactory.pollRecord(); |
| 315 | assertNotNull(record); |
| 316 | TagValue methodTag = record.tags.get(RpcConstants.RPC_CLIENT_METHOD); |
| 317 | assertEquals(method.getFullMethodName(), methodTag.toString()); |
| 318 | TagValue statusTag = record.tags.get(RpcConstants.RPC_STATUS); |
| 319 | assertEquals(Status.Code.PERMISSION_DENIED.toString(), statusTag.toString()); |
| 320 | if (nonDefaultContext) { |
| 321 | TagValue extraTag = record.tags.get(StatsTestUtils.EXTRA_TAG); |
| 322 | assertEquals("extra value", extraTag.toString()); |
| 323 | } else { |
| 324 | assertNull(record.tags.get(StatsTestUtils.EXTRA_TAG)); |
| 325 | } |
| 326 | verify(spyClientSpan).end( |
| 327 | EndSpanOptions.builder() |
| 328 | .setStatus( |
| 329 | com.google.instrumentation.trace.Status.PERMISSION_DENIED |
| 330 | .withDescription("No you don't")) |
| 331 | .build()); |
| 332 | verify(spyClientSpan, never()).end(); |
| 333 | } |
| 334 | |
| 335 | @Test |
| 336 | public void clientBasicStatsDefaultContext() { |
| 337 | CensusStatsModule.ClientCallTracer callTracer = |
| 338 | censusStats.newClientCallTracer(statsCtxFactory.getDefault(), method.getFullMethodName()); |
| 339 | Metadata headers = new Metadata(); |
| 340 | ClientStreamTracer tracer = callTracer.newClientStreamTracer(headers); |
| 341 | |
| 342 | fakeClock.forwardTime(30, MILLISECONDS); |
| 343 | tracer.outboundHeaders(); |
| 344 | |
| 345 | fakeClock.forwardTime(100, MILLISECONDS); |
| 346 | tracer.outboundWireSize(1028); |
| 347 | tracer.outboundUncompressedSize(1128); |
| 348 | |
| 349 | fakeClock.forwardTime(16, MILLISECONDS); |
| 350 | tracer.inboundWireSize(33); |
| 351 | tracer.inboundUncompressedSize(67); |
| 352 | tracer.outboundWireSize(99); |
| 353 | tracer.outboundUncompressedSize(865); |
| 354 | |
| 355 | fakeClock.forwardTime(24, MILLISECONDS); |
| 356 | tracer.inboundWireSize(154); |
| 357 | tracer.inboundUncompressedSize(552); |
| 358 | tracer.streamClosed(Status.OK); |
| 359 | callTracer.callEnded(Status.OK); |
| 360 | |
| 361 | StatsTestUtils.MetricsRecord record = statsCtxFactory.pollRecord(); |
| 362 | assertNotNull(record); |
| 363 | assertNoServerContent(record); |
| 364 | TagValue methodTag = record.tags.get(RpcConstants.RPC_CLIENT_METHOD); |
| 365 | assertEquals(method.getFullMethodName(), methodTag.toString()); |
| 366 | TagValue statusTag = record.tags.get(RpcConstants.RPC_STATUS); |
| 367 | assertEquals(Status.Code.OK.toString(), statusTag.toString()); |
| 368 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_ERROR_COUNT)); |
| 369 | assertEquals(1028 + 99, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_REQUEST_BYTES)); |
| 370 | assertEquals(1128 + 865, |
| 371 | record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES)); |
| 372 | assertEquals(33 + 154, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_RESPONSE_BYTES)); |
| 373 | assertEquals(67 + 552, |
| 374 | record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES)); |
| 375 | assertEquals(30 + 100 + 16 + 24, |
| 376 | record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_ROUNDTRIP_LATENCY)); |
| 377 | } |
| 378 | |
| 379 | @Test |
| 380 | public void clientBasicTracingDefaultSpan() { |
| 381 | CensusTracingModule.ClientCallTracer callTracer = |
| 382 | censusTracing.newClientCallTracer(null, method.getFullMethodName()); |
| 383 | Metadata headers = new Metadata(); |
| 384 | ClientStreamTracer tracer = callTracer.newClientStreamTracer(headers); |
| 385 | verify(mockSpanFactory).startSpan( |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 386 | isNull(Span.class), eq("Sent.package1.service2.method3"), argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 387 | verify(spyClientSpan, never()).end(any(EndSpanOptions.class)); |
| 388 | |
| 389 | tracer.streamClosed(Status.OK); |
| 390 | callTracer.callEnded(Status.OK); |
| 391 | |
| 392 | verify(spyClientSpan).end( |
| 393 | EndSpanOptions.builder().setStatus(com.google.instrumentation.trace.Status.OK).build()); |
| 394 | verifyNoMoreInteractions(mockSpanFactory); |
| 395 | } |
| 396 | |
| 397 | @Test |
| 398 | public void clientStreamNeverCreatedStillRecordStats() { |
| 399 | CensusStatsModule.ClientCallTracer callTracer = |
| 400 | censusStats.newClientCallTracer( |
| 401 | statsCtxFactory.getDefault(), method.getFullMethodName()); |
| 402 | |
| 403 | fakeClock.forwardTime(3000, MILLISECONDS); |
| 404 | callTracer.callEnded(Status.DEADLINE_EXCEEDED.withDescription("3 seconds")); |
| 405 | |
| 406 | StatsTestUtils.MetricsRecord record = statsCtxFactory.pollRecord(); |
| 407 | assertNotNull(record); |
| 408 | assertNoServerContent(record); |
| 409 | TagValue methodTag = record.tags.get(RpcConstants.RPC_CLIENT_METHOD); |
| 410 | assertEquals(method.getFullMethodName(), methodTag.toString()); |
| 411 | TagValue statusTag = record.tags.get(RpcConstants.RPC_STATUS); |
| 412 | assertEquals(Status.Code.DEADLINE_EXCEEDED.toString(), statusTag.toString()); |
| 413 | assertEquals(1, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_ERROR_COUNT)); |
| 414 | assertEquals(0, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_REQUEST_BYTES)); |
| 415 | assertEquals(0, |
| 416 | record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES)); |
| 417 | assertEquals(0, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_RESPONSE_BYTES)); |
| 418 | assertEquals(0, |
| 419 | record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES)); |
| 420 | assertEquals(3000, record.getMetricAsLongOrFail(RpcConstants.RPC_CLIENT_ROUNDTRIP_LATENCY)); |
| 421 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_SERVER_ELAPSED_TIME)); |
| 422 | } |
| 423 | |
| 424 | @Test |
| 425 | public void clientStreamNeverCreatedStillRecordTracing() { |
| 426 | CensusTracingModule.ClientCallTracer callTracer = |
| 427 | censusTracing.newClientCallTracer(fakeClientParentSpan, method.getFullMethodName()); |
| 428 | verify(mockSpanFactory).startSpan( |
| 429 | same(fakeClientParentSpan), eq("Sent.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 430 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 431 | |
| 432 | callTracer.callEnded(Status.DEADLINE_EXCEEDED.withDescription("3 seconds")); |
| 433 | verify(spyClientSpan).end( |
| 434 | EndSpanOptions.builder() |
| 435 | .setStatus( |
| 436 | com.google.instrumentation.trace.Status.DEADLINE_EXCEEDED |
| 437 | .withDescription("3 seconds")) |
| 438 | .build()); |
| 439 | verify(spyClientSpan, never()).end(); |
| 440 | } |
| 441 | |
| 442 | @Test |
| 443 | public void statsHeadersPropagateTags() { |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 444 | subtestStatsHeadersPropagateTags(true); |
| 445 | } |
| 446 | |
| 447 | @Test |
| 448 | public void statsHeadersNotPropagateTags() { |
| 449 | subtestStatsHeadersPropagateTags(false); |
| 450 | } |
| 451 | |
| 452 | private void subtestStatsHeadersPropagateTags(boolean propagate) { |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 453 | // EXTRA_TAG is propagated by the FakeStatsContextFactory. Note that not all tags are |
| 454 | // propagated. The StatsContextFactory decides which tags are to propagated. gRPC facilitates |
| 455 | // the propagation by putting them in the headers. |
| 456 | StatsContext clientCtx = statsCtxFactory.getDefault().with( |
| 457 | StatsTestUtils.EXTRA_TAG, TagValue.create("extra-tag-value-897")); |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 458 | CensusStatsModule census = |
| 459 | new CensusStatsModule(statsCtxFactory, fakeClock.getStopwatchSupplier(), propagate); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 460 | Metadata headers = new Metadata(); |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 461 | CensusStatsModule.ClientCallTracer callTracer = |
| 462 | census.newClientCallTracer(clientCtx, method.getFullMethodName()); |
| 463 | // This propagates clientCtx to headers if propagates==true |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 464 | callTracer.newClientStreamTracer(headers); |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 465 | if (propagate) { |
| 466 | assertTrue(headers.containsKey(census.statsHeader)); |
| 467 | } else { |
| 468 | assertFalse(headers.containsKey(census.statsHeader)); |
| 469 | return; |
| 470 | } |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 471 | |
| 472 | ServerStreamTracer serverTracer = |
Kun Zhang | be74e97 | 2017-04-26 10:50:55 -0700 | [diff] [blame] | 473 | census.getServerTracerFactory().newServerStreamTracer( |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 474 | method.getFullMethodName(), headers); |
| 475 | // Server tracer deserializes clientCtx from the headers, so that it records stats with the |
| 476 | // propagated tags. |
| 477 | Context serverContext = serverTracer.filterContext(Context.ROOT); |
| 478 | // It also put clientCtx in the Context seen by the call handler |
Yang Song | 4a0cf0b | 2017-06-06 18:16:55 -0700 | [diff] [blame^] | 479 | assertEquals(clientCtx, STATS_CONTEXT_KEY.get(serverContext)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 480 | |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 481 | // Verifies that the server tracer records the status with the propagated tag |
| 482 | serverTracer.streamClosed(Status.OK); |
| 483 | |
| 484 | StatsTestUtils.MetricsRecord serverRecord = statsCtxFactory.pollRecord(); |
| 485 | assertNotNull(serverRecord); |
| 486 | assertNoClientContent(serverRecord); |
| 487 | TagValue serverMethodTag = serverRecord.tags.get(RpcConstants.RPC_SERVER_METHOD); |
| 488 | assertEquals(method.getFullMethodName(), serverMethodTag.toString()); |
| 489 | TagValue serverStatusTag = serverRecord.tags.get(RpcConstants.RPC_STATUS); |
| 490 | assertEquals(Status.Code.OK.toString(), serverStatusTag.toString()); |
| 491 | assertNull(serverRecord.getMetric(RpcConstants.RPC_SERVER_ERROR_COUNT)); |
| 492 | TagValue serverPropagatedTag = serverRecord.tags.get(StatsTestUtils.EXTRA_TAG); |
| 493 | assertEquals("extra-tag-value-897", serverPropagatedTag.toString()); |
| 494 | |
| 495 | // Verifies that the client tracer factory uses clientCtx, which includes the custom tags, to |
| 496 | // record stats. |
| 497 | callTracer.callEnded(Status.OK); |
| 498 | |
| 499 | StatsTestUtils.MetricsRecord clientRecord = statsCtxFactory.pollRecord(); |
| 500 | assertNotNull(clientRecord); |
| 501 | assertNoServerContent(clientRecord); |
| 502 | TagValue clientMethodTag = clientRecord.tags.get(RpcConstants.RPC_CLIENT_METHOD); |
| 503 | assertEquals(method.getFullMethodName(), clientMethodTag.toString()); |
| 504 | TagValue clientStatusTag = clientRecord.tags.get(RpcConstants.RPC_STATUS); |
| 505 | assertEquals(Status.Code.OK.toString(), clientStatusTag.toString()); |
| 506 | assertNull(clientRecord.getMetric(RpcConstants.RPC_CLIENT_ERROR_COUNT)); |
| 507 | TagValue clientPropagatedTag = clientRecord.tags.get(StatsTestUtils.EXTRA_TAG); |
| 508 | assertEquals("extra-tag-value-897", clientPropagatedTag.toString()); |
| 509 | } |
| 510 | |
| 511 | @Test |
| 512 | public void statsHeadersNotPropagateDefaultContext() { |
| 513 | CensusStatsModule.ClientCallTracer callTracer = |
| 514 | censusStats.newClientCallTracer(statsCtxFactory.getDefault(), method.getFullMethodName()); |
| 515 | Metadata headers = new Metadata(); |
| 516 | callTracer.newClientStreamTracer(headers); |
| 517 | assertFalse(headers.containsKey(censusStats.statsHeader)); |
| 518 | } |
| 519 | |
| 520 | @Test |
| 521 | public void statsHeaderMalformed() { |
| 522 | // Construct a malformed header and make sure parsing it will throw |
| 523 | byte[] statsHeaderValue = new byte[]{1}; |
| 524 | Metadata.Key<byte[]> arbitraryStatsHeader = |
| 525 | Metadata.Key.of("grpc-tags-bin", Metadata.BINARY_BYTE_MARSHALLER); |
| 526 | try { |
| 527 | statsCtxFactory.deserialize(new ByteArrayInputStream(statsHeaderValue)); |
| 528 | fail("Should have thrown"); |
| 529 | } catch (Exception e) { |
| 530 | // Expected |
| 531 | } |
| 532 | |
| 533 | // But the header key will return a default context for it |
| 534 | Metadata headers = new Metadata(); |
| 535 | assertNull(headers.get(censusStats.statsHeader)); |
| 536 | headers.put(arbitraryStatsHeader, statsHeaderValue); |
| 537 | assertSame(statsCtxFactory.getDefault(), headers.get(censusStats.statsHeader)); |
| 538 | } |
| 539 | |
| 540 | @Test |
| 541 | public void traceHeadersPropagateSpanContext() throws Exception { |
| 542 | CensusTracingModule.ClientCallTracer callTracer = |
| 543 | censusTracing.newClientCallTracer(fakeClientParentSpan, method.getFullMethodName()); |
| 544 | Metadata headers = new Metadata(); |
| 545 | callTracer.newClientStreamTracer(headers); |
| 546 | |
| 547 | verify(mockTracingPropagationHandler).toBinaryValue(same(fakeClientSpanContext)); |
| 548 | verifyNoMoreInteractions(mockTracingPropagationHandler); |
| 549 | verify(mockSpanFactory).startSpan( |
| 550 | same(fakeClientParentSpan), eq("Sent.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 551 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 552 | verifyNoMoreInteractions(mockSpanFactory); |
| 553 | assertTrue(headers.containsKey(censusTracing.tracingHeader)); |
| 554 | |
| 555 | ServerStreamTracer serverTracer = |
| 556 | censusTracing.getServerTracerFactory().newServerStreamTracer( |
| 557 | method.getFullMethodName(), headers); |
| 558 | verify(mockTracingPropagationHandler).fromBinaryValue(same(binarySpanContext)); |
| 559 | verify(mockSpanFactory).startSpanWithRemoteParent( |
| 560 | same(fakeServerParentSpanContext), eq("Recv.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 561 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 562 | |
| 563 | Context filteredContext = serverTracer.filterContext(Context.ROOT); |
| 564 | assertSame(spyServerSpan, ContextUtils.CONTEXT_SPAN_KEY.get(filteredContext)); |
| 565 | } |
| 566 | |
| 567 | @Test |
| 568 | public void traceHeaderMalformed() throws Exception { |
| 569 | // As comparison, normal header parsing |
| 570 | Metadata headers = new Metadata(); |
| 571 | headers.put(censusTracing.tracingHeader, fakeClientSpanContext); |
| 572 | // mockTracingPropagationHandler was stubbed to always return fakeServerParentSpanContext |
| 573 | assertSame(fakeServerParentSpanContext, headers.get(censusTracing.tracingHeader)); |
| 574 | |
| 575 | // Make BinaryPropagationHandler always throw when parsing the header |
| 576 | when(mockTracingPropagationHandler.fromBinaryValue(any(byte[].class))) |
| 577 | .thenThrow(new ParseException("Malformed header", 0)); |
| 578 | |
| 579 | headers = new Metadata(); |
| 580 | assertNull(headers.get(censusTracing.tracingHeader)); |
| 581 | headers.put(censusTracing.tracingHeader, fakeClientSpanContext); |
| 582 | assertSame(SpanContext.INVALID, headers.get(censusTracing.tracingHeader)); |
| 583 | assertNotSame(fakeServerParentSpanContext, SpanContext.INVALID); |
| 584 | |
| 585 | // A null Span is used as the parent in this case |
| 586 | censusTracing.getServerTracerFactory().newServerStreamTracer( |
| 587 | method.getFullMethodName(), headers); |
| 588 | verify(mockSpanFactory).startSpanWithRemoteParent( |
| 589 | isNull(SpanContext.class), eq("Recv.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 590 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 591 | } |
| 592 | |
| 593 | @Test |
| 594 | public void serverBasicStatsNoHeaders() { |
| 595 | ServerStreamTracer.Factory tracerFactory = censusStats.getServerTracerFactory(); |
| 596 | ServerStreamTracer tracer = |
| 597 | tracerFactory.newServerStreamTracer(method.getFullMethodName(), new Metadata()); |
| 598 | |
| 599 | Context filteredContext = tracer.filterContext(Context.ROOT); |
Yang Song | 4a0cf0b | 2017-06-06 18:16:55 -0700 | [diff] [blame^] | 600 | assertNull(STATS_CONTEXT_KEY.get(filteredContext)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 601 | |
| 602 | tracer.inboundWireSize(34); |
| 603 | tracer.inboundUncompressedSize(67); |
| 604 | |
| 605 | fakeClock.forwardTime(100, MILLISECONDS); |
| 606 | tracer.outboundWireSize(1028); |
| 607 | tracer.outboundUncompressedSize(1128); |
| 608 | |
| 609 | fakeClock.forwardTime(16, MILLISECONDS); |
| 610 | tracer.inboundWireSize(154); |
| 611 | tracer.inboundUncompressedSize(552); |
| 612 | tracer.outboundWireSize(99); |
| 613 | tracer.outboundUncompressedSize(865); |
| 614 | |
| 615 | fakeClock.forwardTime(24, MILLISECONDS); |
| 616 | |
| 617 | tracer.streamClosed(Status.CANCELLED); |
| 618 | |
| 619 | StatsTestUtils.MetricsRecord record = statsCtxFactory.pollRecord(); |
| 620 | assertNotNull(record); |
| 621 | assertNoClientContent(record); |
| 622 | TagValue methodTag = record.tags.get(RpcConstants.RPC_SERVER_METHOD); |
| 623 | assertEquals(method.getFullMethodName(), methodTag.toString()); |
| 624 | TagValue statusTag = record.tags.get(RpcConstants.RPC_STATUS); |
| 625 | assertEquals(Status.Code.CANCELLED.toString(), statusTag.toString()); |
| 626 | assertEquals(1, record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_ERROR_COUNT)); |
| 627 | assertEquals(1028 + 99, record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_RESPONSE_BYTES)); |
| 628 | assertEquals(1128 + 865, |
| 629 | record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES)); |
| 630 | assertEquals(34 + 154, record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_REQUEST_BYTES)); |
| 631 | assertEquals(67 + 552, |
| 632 | record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES)); |
| 633 | assertEquals(100 + 16 + 24, |
| 634 | record.getMetricAsLongOrFail(RpcConstants.RPC_SERVER_SERVER_LATENCY)); |
| 635 | } |
| 636 | |
| 637 | @Test |
| 638 | public void serverBasicTracingNoHeaders() { |
| 639 | ServerStreamTracer.Factory tracerFactory = censusTracing.getServerTracerFactory(); |
| 640 | ServerStreamTracer tracer = |
| 641 | tracerFactory.newServerStreamTracer(method.getFullMethodName(), new Metadata()); |
| 642 | verifyZeroInteractions(mockTracingPropagationHandler); |
| 643 | verify(mockSpanFactory).startSpanWithRemoteParent( |
| 644 | isNull(SpanContext.class), eq("Recv.package1.service2.method3"), |
Bogdan Drutu | 530b714 | 2017-05-23 15:10:36 -0700 | [diff] [blame] | 645 | argThat(startSpanOptionsMatcher)); |
Kun Zhang | 49bde54 | 2017-04-25 13:53:29 -0700 | [diff] [blame] | 646 | |
| 647 | Context filteredContext = tracer.filterContext(Context.ROOT); |
| 648 | assertSame(spyServerSpan, ContextUtils.CONTEXT_SPAN_KEY.get(filteredContext)); |
| 649 | |
| 650 | verify(spyServerSpan, never()).end(any(EndSpanOptions.class)); |
| 651 | tracer.streamClosed(Status.CANCELLED); |
| 652 | |
| 653 | verify(spyServerSpan).end( |
| 654 | EndSpanOptions.builder() |
| 655 | .setStatus(com.google.instrumentation.trace.Status.CANCELLED).build()); |
| 656 | verify(spyServerSpan, never()).end(); |
| 657 | } |
| 658 | |
| 659 | @Test |
| 660 | public void convertToTracingStatus() { |
| 661 | // Without description |
| 662 | for (Status.Code grpcCode : Status.Code.values()) { |
| 663 | Status grpcStatus = Status.fromCode(grpcCode); |
| 664 | com.google.instrumentation.trace.Status tracingStatus = |
| 665 | CensusTracingModule.convertStatus(grpcStatus); |
| 666 | assertEquals(grpcCode.toString(), tracingStatus.getCanonicalCode().toString()); |
| 667 | assertNull(tracingStatus.getDescription()); |
| 668 | } |
| 669 | |
| 670 | // With description |
| 671 | for (Status.Code grpcCode : Status.Code.values()) { |
| 672 | Status grpcStatus = Status.fromCode(grpcCode).withDescription("This is my description"); |
| 673 | com.google.instrumentation.trace.Status tracingStatus = |
| 674 | CensusTracingModule.convertStatus(grpcStatus); |
| 675 | assertEquals(grpcCode.toString(), tracingStatus.getCanonicalCode().toString()); |
| 676 | assertEquals(grpcStatus.getDescription(), tracingStatus.getDescription()); |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | private static void assertNoServerContent(StatsTestUtils.MetricsRecord record) { |
| 681 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_ERROR_COUNT)); |
| 682 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_REQUEST_BYTES)); |
| 683 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_RESPONSE_BYTES)); |
| 684 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_SERVER_ELAPSED_TIME)); |
| 685 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_SERVER_LATENCY)); |
| 686 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES)); |
| 687 | assertNull(record.getMetric(RpcConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES)); |
| 688 | } |
| 689 | |
| 690 | private static void assertNoClientContent(StatsTestUtils.MetricsRecord record) { |
| 691 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_ERROR_COUNT)); |
| 692 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_REQUEST_BYTES)); |
| 693 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_RESPONSE_BYTES)); |
| 694 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_ROUNDTRIP_LATENCY)); |
| 695 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_SERVER_ELAPSED_TIME)); |
| 696 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES)); |
| 697 | assertNull(record.getMetric(RpcConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES)); |
| 698 | } |
| 699 | |
| 700 | // Promote the visibility of SpanFactory's methods to allow mocking |
| 701 | private abstract static class AccessibleSpanFactory extends SpanFactory { |
| 702 | @Override |
| 703 | public abstract Span startSpan(@Nullable Span parent, String name, StartSpanOptions options); |
| 704 | |
| 705 | @Override |
| 706 | public abstract Span startSpanWithRemoteParent( |
| 707 | @Nullable SpanContext remoteParent, String name, StartSpanOptions options); |
| 708 | } |
| 709 | |
| 710 | private static class FakeSpan extends Span { |
| 711 | FakeSpan(SpanContext ctx) { |
| 712 | super(ctx, null); |
| 713 | } |
| 714 | |
| 715 | @Override |
| 716 | public void addAttributes(Map<String, AttributeValue> attributes) { |
| 717 | } |
| 718 | |
| 719 | @Override |
| 720 | public void addAnnotation(String description, Map<String, AttributeValue> attributes) { |
| 721 | } |
| 722 | |
| 723 | @Override |
| 724 | public void addAnnotation(Annotation annotation) { |
| 725 | } |
| 726 | |
| 727 | @Override |
| 728 | public void addNetworkEvent(NetworkEvent networkEvent) { |
| 729 | } |
| 730 | |
| 731 | @Override |
| 732 | public void addLink(Link link) { |
| 733 | } |
| 734 | |
| 735 | @Override |
| 736 | public void end(EndSpanOptions options) { |
| 737 | } |
| 738 | } |
| 739 | } |