diff --git a/build.gradle b/build.gradle index 85b556836..91a5e4de4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,9 @@ apply plugin: 'io.codearte.nexus-staging' project.version = "1.37.1-SNAPSHOT" // {x-version-update:gax:current} ext { + // When upgrading grpc, make sure to upgrade opencensusVersion to be consistent with grpc. grpcVersion = '1.17.1' + opencensusVersion = '0.17.0' commonProtosVersion = '1.12.0' authVersion = '0.12.0' // Project names not used for release @@ -108,6 +110,7 @@ subprojects { ext { grpcVersion = grpcVersion + opencensusVersion = opencensusVersion commonProtosVersion = commonProtosVersion // Shortcuts for libraries we are using @@ -127,6 +130,7 @@ subprojects { authCredentials: "com.google.auth:google-auth-library-credentials:${authVersion}", commonProtos: "com.google.api.grpc:proto-google-common-protos:${commonProtosVersion}", apiCommon: "com.google.api:api-common:1.7.0", + opencensusApi: "io.opencensus:opencensus-api:${opencensusVersion}", // Testing junit: 'junit:junit:4.12', diff --git a/gax/build.gradle b/gax/build.gradle index e3a8305ea..fb7cd9f84 100644 --- a/gax/build.gradle +++ b/gax/build.gradle @@ -19,7 +19,8 @@ dependencies { libraries.jsr305, libraries.threetenbp, libraries.auth, - libraries.apiCommon + libraries.apiCommon, + libraries.opencensusApi compileOnly libraries.autovalue diff --git a/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java new file mode 100644 index 000000000..b58746dc6 --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -0,0 +1,375 @@ +/* + * Copyright 2019 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.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.ApiException; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import io.opencensus.trace.AttributeValue; +import io.opencensus.trace.EndSpanOptions; +import io.opencensus.trace.Span; +import io.opencensus.trace.Status; +import io.opencensus.trace.Status.CanonicalCode; +import io.opencensus.trace.Tracer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nonnull; +import org.threeten.bp.Duration; + +/** + * Implementation of {@link ApiTracer} that uses OpenCensus. + * + *

This implementation wraps an OpenCensus {@link Span} for every tracer and annotates that + * {@link Span} with various events throughout the lifecycle of the logical operation. + * + *

Each span will be named {@code ClientName.MethodName} and will have the following attributes: + * + *

+ *
{@code attempt count} + *
The Number of attempts sent before the logical operation completed + *
{@code status} + *
The status code of the last attempt + *
{@code total response count} + *
The number of messages received across all of the attempts. This will only be set for + * server streaming and bidi RPCs. + *
{@code total request count} + *
The number of messages sent across all of the attempts. This will only be set for client + * streaming and bidi RPCs. + *
{@code batch count} + *
For batch requests, the number of elements in the request. + *
{@code batch size} + *
For batch requests, the byte size of the request. + *
+ * + *

The spans will contain the following annotations: + * + *

+ * + *

This class is thread compatible. It expects callers to follow grpc's threading model: there is + * only one thread that invokes the operation* and attempt* methods. Please see {@link + * com.google.api.gax.rpc.ApiStreamObserver} for more information. + */ +@BetaApi("Surface for tracing is not yet stable") +public class OpencensusTracer implements ApiTracer { + private final Tracer tracer; + private final Span span; + + private volatile long currentAttemptId; + private AtomicLong attemptSentMessages = new AtomicLong(0); + private long attemptReceivedMessages = 0; + private AtomicLong totalSentMessages = new AtomicLong(0); + private long totalReceivedMessages = 0; + + OpencensusTracer(@Nonnull Tracer tracer, @Nonnull Span span) { + this.tracer = Preconditions.checkNotNull(tracer, "tracer can't be null"); + this.span = Preconditions.checkNotNull(span, "span can't be null"); + } + + /** {@inheritDoc} */ + @Override + public Scope inScope() { + final io.opencensus.common.Scope scope = tracer.withSpan(span); + + return new Scope() { + @Override + public void close() { + scope.close(); + } + }; + } + + /** {@inheritDoc} */ + @Override + public void operationSucceeded() { + Map attributes = baseOperationAttributes(); + + span.putAttributes(attributes); + span.end(); + } + + /** {@inheritDoc} */ + @Override + public void operationCancelled() { + Map attributes = baseOperationAttributes(); + span.putAttributes(attributes); + span.end( + EndSpanOptions.builder() + .setStatus(Status.CANCELLED.withDescription("Cancelled by caller")) + .build()); + } + + /** {@inheritDoc} */ + @Override + public void operationFailed(Throwable error) { + Map attributes = baseOperationAttributes(); + + span.putAttributes(attributes); + span.end(EndSpanOptions.builder().setStatus(convertErrorToStatus(error)).build()); + } + + /** {@inheritDoc} */ + @Override + public void connectionSelected(int id) { + span.addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(id))); + } + + /** {@inheritDoc} */ + @Override + public void attemptStarted(int attemptNumber) { + currentAttemptId = attemptNumber; + attemptSentMessages.set(0); + attemptReceivedMessages = 0; + + HashMap attributes = new HashMap<>(); + populateAttemptNumber(attributes); + + span.addAnnotation("Attempt started", attributes); + } + + /** {@inheritDoc} */ + @Override + public void attemptSucceeded() { + Map attributes = baseAttemptAttributes(); + + span.addAnnotation("Attempt succeeded", attributes); + } + + @Override + public void attemptCancelled() { + Map attributes = baseAttemptAttributes(); + + span.addAnnotation("Attempt cancelled", attributes); + } + + /** {@inheritDoc} */ + @Override + public void attemptFailed(Throwable error, Duration delay) { + Map attributes = baseAttemptAttributes(); + attributes.put("delay ms", AttributeValue.longAttributeValue(delay.toMillis())); + populateError(attributes, error); + + String msg = error != null ? "Attempt failed" : "Operation incomplete"; + span.addAnnotation(msg + ", scheduling next attempt", attributes); + } + + /** {@inheritDoc} */ + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + Map attributes = baseAttemptAttributes(); + populateError(attributes, error); + + span.addAnnotation("Attempts exhausted", attributes); + } + + /** {@inheritDoc} */ + @Override + public void attemptPermanentFailure(Throwable error) { + Map attributes = baseAttemptAttributes(); + populateError(attributes, error); + + span.addAnnotation("Attempt failed, error not retryable", attributes); + } + + /** {@inheritDoc} */ + @Override + public void responseReceived() { + attemptReceivedMessages++; + totalReceivedMessages++; + } + + /** {@inheritDoc} */ + @Override + public void requestSent() { + attemptSentMessages.incrementAndGet(); + totalSentMessages.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public void batchRequestSent(long elementCount, long requestSize) { + span.putAttribute("batch count", AttributeValue.longAttributeValue(elementCount)); + span.putAttribute("batch size", AttributeValue.longAttributeValue(requestSize)); + } + + private Map baseOperationAttributes() { + HashMap attributes = new HashMap<>(); + + attributes.put("attempt count", AttributeValue.longAttributeValue(currentAttemptId + 1)); + + long localTotalSentMessages = totalSentMessages.get(); + if (localTotalSentMessages > 0) { + attributes.put( + "total request count", AttributeValue.longAttributeValue(localTotalSentMessages)); + } + if (totalReceivedMessages > 0) { + attributes.put( + "total response count", AttributeValue.longAttributeValue(totalReceivedMessages)); + } + + return attributes; + } + + private Map baseAttemptAttributes() { + HashMap attributes = new HashMap<>(); + + populateAttemptNumber(attributes); + + long localAttemptSentMessages = attemptSentMessages.get(); + if (localAttemptSentMessages > 0) { + attributes.put( + "attempt request count", AttributeValue.longAttributeValue(localAttemptSentMessages)); + } + if (attemptReceivedMessages > 0) { + attributes.put( + "attempt response count", AttributeValue.longAttributeValue(attemptReceivedMessages)); + } + + return attributes; + } + + private void populateAttemptNumber(Map attributes) { + attributes.put("attempt", AttributeValue.longAttributeValue(currentAttemptId)); + } + + private void populateError(Map attributes, Throwable error) { + if (error == null) { + attributes.put("status", null); + return; + } + + Status status = convertErrorToStatus(error); + + attributes.put( + "status", AttributeValue.stringAttributeValue(status.getCanonicalCode().toString())); + } + + @InternalApi("Visible for testing") + static Status convertErrorToStatus(Throwable error) { + if (!(error instanceof ApiException)) { + return Status.UNKNOWN.withDescription(error.getMessage()); + } + + ApiException apiException = (ApiException) error; + + Status.CanonicalCode code; + try { + code = Status.CanonicalCode.valueOf(apiException.getStatusCode().getCode().name()); + } catch (IllegalArgumentException e) { + code = CanonicalCode.UNKNOWN; + } + + return code.toStatus().withDescription(error.getMessage()); + } +} diff --git a/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracerFactory.java b/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracerFactory.java new file mode 100644 index 000000000..55eab5b8d --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracerFactory.java @@ -0,0 +1,117 @@ +/* + * Copyright 2019 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.tracing; + +import com.google.api.core.InternalApi; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import io.opencensus.trace.Span; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracing; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A {@link ApiTracerFactory} to build instances of {@link OpencensusTracer}. + * + *

This class wraps the {@link Tracer} provided by Opencensus in {@code Tracing.getTracer()}. It + * will be used to create new spans and wrap them in {@link OpencensusTracer} defined in gax. + * + *

This class is thread safe. + */ +@InternalApi("For google-cloud-java client use only") +public final class OpencensusTracerFactory implements ApiTracerFactory { + @Nonnull private final Tracer internalTracer; + @Nullable private final String clientNameOverride; + + /** + * Instantiates a new instance capturing the {@link io.opencensus.trace.Tracer} in {@code + * Tracing.getTracer}. + */ + public OpencensusTracerFactory() { + this(null); + } + + /** + * Instantiates a new instance capturing the {@link io.opencensus.trace.Tracer} in {@code + * Tracing.getTracer}. It will also override the service name of the grpc stub with a custom + * client name. This is useful disambiguate spans created outer manual written wrappers and around + * generated gapic spans. + * + * @param clientNameOverride the client name that will override all of the spans' client name. + */ + public OpencensusTracerFactory(@Nullable String clientNameOverride) { + this(Tracing.getTracer(), clientNameOverride); + } + + /** + * Instantiates a new instance with an explicit {@link io.opencensus.trace.Tracer}. It will also + * override the service name of the grpc stub with a custom client name. This is useful + * disambiguate spans created outer manual written wrappers and around generated gapic spans. + * + * @param internalTracer the Opencensus tracer to wrap. + * @param clientNameOverride the client name that will override all of the spans' client name. + */ + @InternalApi("Visible for testing") + OpencensusTracerFactory(Tracer internalTracer, @Nullable String clientNameOverride) { + this.internalTracer = + Preconditions.checkNotNull(internalTracer, "internalTracer can't be null"); + this.clientNameOverride = clientNameOverride; + } + + /** {@inheritDoc } */ + @Override + public ApiTracer newTracer(SpanName spanName) { + if (clientNameOverride != null) { + spanName = spanName.withClientName(clientNameOverride); + } + Span span = internalTracer.spanBuilder(spanName.toString()).setRecordEvents(true).startSpan(); + + return new OpencensusTracer(internalTracer, span); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OpencensusTracerFactory that = (OpencensusTracerFactory) o; + return Objects.equal(internalTracer, that.internalTracer) + && Objects.equal(clientNameOverride, that.clientNameOverride); + } + + @Override + public int hashCode() { + return Objects.hashCode(internalTracer, clientNameOverride); + } +} diff --git a/gax/src/main/java/com/google/api/gax/tracing/SpanName.java b/gax/src/main/java/com/google/api/gax/tracing/SpanName.java index 80c3ed5a6..d4fb0eb5f 100644 --- a/gax/src/main/java/com/google/api/gax/tracing/SpanName.java +++ b/gax/src/main/java/com/google/api/gax/tracing/SpanName.java @@ -65,4 +65,9 @@ public SpanName withClientName(String clientName) { public SpanName withMethodName(String methodName) { return of(getClientName(), methodName); } + + @Override + public String toString() { + return getClientName() + "." + getMethodName(); + } } diff --git a/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerFactoryTest.java b/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerFactoryTest.java new file mode 100644 index 000000000..24a3a1c36 --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerFactoryTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2019 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.tracing; + +import com.google.common.truth.Truth; +import io.opencensus.trace.BlankSpan; +import io.opencensus.trace.Sampler; +import io.opencensus.trace.Span; +import io.opencensus.trace.SpanBuilder; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.Tracer; +import java.util.List; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class OpencensusTracerFactoryTest { + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + private FakeTracer internalTracer; + + private OpencensusTracerFactory factory; + + @Before + public void setUp() { + internalTracer = new FakeTracer(); + } + + @Test + public void testSpanNamePassthrough() { + OpencensusTracerFactory factory = new OpencensusTracerFactory(internalTracer, null); + + factory.newTracer(SpanName.of("FakeClient", "FakeMethod")); + + Truth.assertThat(internalTracer.lastSpanName).isEqualTo("FakeClient.FakeMethod"); + } + + @Test + public void testSpanNameOverride() { + OpencensusTracerFactory factory = + new OpencensusTracerFactory(internalTracer, "OverridenClient"); + + factory.newTracer(SpanName.of("FakeClient", "FakeMethod")); + + Truth.assertThat(internalTracer.lastSpanName).isEqualTo("OverridenClient.FakeMethod"); + } + + private static class FakeTracer extends Tracer { + String lastSpanName; + + @Override + public SpanBuilder spanBuilderWithExplicitParent(String s, @Nullable Span span) { + lastSpanName = s; + return new FakeSpanBuilder(); + } + + @Override + public SpanBuilder spanBuilderWithRemoteParent(String s, @Nullable SpanContext spanContext) { + lastSpanName = s; + return new FakeSpanBuilder(); + } + } + + private static class FakeSpanBuilder extends SpanBuilder { + @Override + public SpanBuilder setSampler(Sampler sampler) { + return this; + } + + @Override + public SpanBuilder setParentLinks(List list) { + return this; + } + + @Override + public SpanBuilder setRecordEvents(boolean b) { + return this; + } + + @Override + public Span startSpan() { + return BlankSpan.INSTANCE; + } + } +} diff --git a/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerTest.java b/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerTest.java new file mode 100644 index 000000000..f3b24ca11 --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/tracing/OpencensusTracerTest.java @@ -0,0 +1,355 @@ +/* + * Copyright 2019 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.tracing; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.DeadlineExceededException; +import com.google.api.gax.rpc.NotFoundException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.common.collect.ImmutableMap; +import io.opencensus.trace.AttributeValue; +import io.opencensus.trace.EndSpanOptions; +import io.opencensus.trace.Span; +import io.opencensus.trace.Status; +import io.opencensus.trace.Tracer; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; +import org.threeten.bp.Duration; + +@RunWith(JUnit4.class) +public class OpencensusTracerTest { + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + @Mock private Tracer internalTracer; + @Mock private Span span; + @Captor private ArgumentCaptor> attributeCaptor; + + private OpencensusTracer tracer; + + @Before + public void setUp() { + tracer = new OpencensusTracer(internalTracer, span); + } + + @Test + public void testUnarySuccessExample() { + tracer.attemptStarted(0); + tracer.connectionSelected(1); + ApiException error0 = + new DeadlineExceededException( + "deadline exceeded", null, new FakeStatusCode(Code.DEADLINE_EXCEEDED), true); + tracer.attemptFailed(error0, Duration.ofMillis(5)); + + tracer.attemptStarted(1); + tracer.connectionSelected(2); + tracer.attemptSucceeded(); + tracer.operationSucceeded(); + + // Attempt 0 + verify(span) + .addAnnotation( + "Attempt started", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(0))); + + verify(span) + .addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(1))); + + verify(span) + .addAnnotation( + "Attempt failed, scheduling next attempt", + ImmutableMap.of( + "attempt", AttributeValue.longAttributeValue(0), + "delay ms", AttributeValue.longAttributeValue(5), + "status", AttributeValue.stringAttributeValue("DEADLINE_EXCEEDED"))); + + // Attempt 1 + verify(span) + .addAnnotation( + "Attempt started", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(1))); + + verify(span) + .addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(2))); + + verify(span) + .addAnnotation( + "Attempt succeeded", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(1))); + + verify(span) + .putAttributes(ImmutableMap.of("attempt count", AttributeValue.longAttributeValue(2))); + verify(span).end(); + + verifyNoMoreInteractions(span); + } + + @Test + public void testBatchExample() { + tracer.batchRequestSent(100, 1000); + tracer.attemptStarted(0); + tracer.connectionSelected(1); + tracer.attemptSucceeded(); + tracer.operationSucceeded(); + + verify(span).putAttribute("batch count", AttributeValue.longAttributeValue(100)); + verify(span).putAttribute("batch size", AttributeValue.longAttributeValue(1000)); + } + + @Test + public void testRetriesExhaustedExample() { + tracer.attemptStarted(0); + tracer.connectionSelected(1); + ApiException error0 = + new DeadlineExceededException( + "deadline exceeded", null, new FakeStatusCode(Code.DEADLINE_EXCEEDED), false); + tracer.attemptFailedRetriesExhausted(error0); + tracer.operationFailed(error0); + + verify(span) + .addAnnotation( + "Attempt started", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(0))); + + verify(span) + .addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(1))); + + verify(span) + .addAnnotation( + "Attempts exhausted", + ImmutableMap.of( + "attempt", AttributeValue.longAttributeValue(0), + "status", AttributeValue.stringAttributeValue("DEADLINE_EXCEEDED"))); + + verify(span) + .putAttributes(ImmutableMap.of("attempt count", AttributeValue.longAttributeValue(1))); + + verify(span) + .end( + EndSpanOptions.builder() + .setStatus(Status.DEADLINE_EXCEEDED.withDescription("deadline exceeded")) + .build()); + + verifyNoMoreInteractions(span); + } + + @Test + public void testCancellationExample() { + tracer.attemptStarted(0); + tracer.connectionSelected(1); + tracer.attemptCancelled(); + tracer.operationCancelled(); + + verify(span) + .addAnnotation( + "Attempt started", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(0))); + + verify(span) + .addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(1))); + + verify(span) + .addAnnotation( + "Attempt cancelled", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(0))); + + verify(span) + .putAttributes(ImmutableMap.of("attempt count", AttributeValue.longAttributeValue(1))); + + verify(span) + .end( + EndSpanOptions.builder() + .setStatus(Status.CANCELLED.withDescription("Cancelled by caller")) + .build()); + verifyNoMoreInteractions(span); + } + + @Test + public void testFailureExample() { + tracer.attemptStarted(0); + tracer.connectionSelected(1); + ApiException error0 = + new NotFoundException("not found", null, new FakeStatusCode(Code.NOT_FOUND), false); + tracer.attemptPermanentFailure(error0); + tracer.operationFailed(error0); + + verify(span) + .addAnnotation( + "Attempt started", ImmutableMap.of("attempt", AttributeValue.longAttributeValue(0))); + + verify(span) + .addAnnotation( + "Connection selected", ImmutableMap.of("id", AttributeValue.longAttributeValue(1))); + + verify(span) + .addAnnotation( + "Attempt failed, error not retryable", + ImmutableMap.of( + "attempt", AttributeValue.longAttributeValue(0), + "status", AttributeValue.stringAttributeValue("NOT_FOUND"))); + + verify(span) + .putAttributes(ImmutableMap.of("attempt count", AttributeValue.longAttributeValue(1))); + + verify(span) + .end( + EndSpanOptions.builder() + .setStatus(Status.NOT_FOUND.withDescription("not found")) + .build()); + verifyNoMoreInteractions(span); + } + + @Test + public void testResponseCount() { + // Initial attempt got 2 messages, then failed + tracer.attemptStarted(0); + tracer.responseReceived(); + tracer.responseReceived(); + tracer.attemptFailed(new RuntimeException(), Duration.ofMillis(1)); + + // Next attempt got 1 message, then successfully finished the attempt and the logical operation. + tracer.attemptStarted(1); + tracer.responseReceived(); + tracer.attemptSucceeded(); + tracer.operationSucceeded(); + + verify(span) + .addAnnotation(eq("Attempt failed, scheduling next attempt"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt response count", AttributeValue.longAttributeValue(2)); + + verify(span).addAnnotation(eq("Attempt succeeded"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt response count", AttributeValue.longAttributeValue(1)); + + verify(span).putAttributes(attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("total response count", AttributeValue.longAttributeValue(3)); + } + + @Test + public void testRequestCount() { + // Initial attempt sent 2 messages, then failed + tracer.attemptStarted(0); + tracer.requestSent(); + tracer.requestSent(); + tracer.attemptFailed(new RuntimeException(), Duration.ofMillis(1)); + + // Next attempt sent 1 message, then successfully finished the attempt and the logical operation. + tracer.attemptStarted(1); + tracer.requestSent(); + tracer.attemptSucceeded(); + tracer.operationSucceeded(); + + verify(span) + .addAnnotation(eq("Attempt failed, scheduling next attempt"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt request count", AttributeValue.longAttributeValue(2)); + + verify(span).addAnnotation(eq("Attempt succeeded"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt request count", AttributeValue.longAttributeValue(1)); + + verify(span).putAttributes(attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("total request count", AttributeValue.longAttributeValue(3)); + } + + @Test + public void testAttemptNumber() { + tracer.attemptStarted(0); + tracer.attemptFailed(new RuntimeException(), Duration.ofMillis(1)); + tracer.attemptStarted(1); + tracer.attemptSucceeded(); + tracer.operationSucceeded(); + + verify(span) + .addAnnotation(eq("Attempt failed, scheduling next attempt"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt", AttributeValue.longAttributeValue(0)); + + verify(span).addAnnotation(eq("Attempt succeeded"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt", AttributeValue.longAttributeValue(1)); + + verify(span).putAttributes(attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("attempt count", AttributeValue.longAttributeValue(2)); + } + + @Test + public void testStatusCode() { + tracer.attemptStarted(0); + tracer.attemptFailed( + new DeadlineExceededException( + "deadline exceeded", null, new FakeStatusCode(Code.DEADLINE_EXCEEDED), true), + Duration.ofMillis(1)); + + tracer.attemptStarted(1); + ApiException permanentError = + new NotFoundException("not found", null, new FakeStatusCode(Code.NOT_FOUND), false); + tracer.attemptPermanentFailure(permanentError); + tracer.operationFailed(permanentError); + + verify(span) + .addAnnotation(eq("Attempt failed, scheduling next attempt"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("status", AttributeValue.stringAttributeValue("DEADLINE_EXCEEDED")); + + verify(span) + .addAnnotation(eq("Attempt failed, error not retryable"), attributeCaptor.capture()); + assertThat(attributeCaptor.getValue()) + .containsEntry("status", AttributeValue.stringAttributeValue("NOT_FOUND")); + } + + @Test + public void testErrorConversion() { + for (Code code : Code.values()) { + ApiException error = new ApiException("fake message", null, new FakeStatusCode(code), false); + Status opencensusStatus = OpencensusTracer.convertErrorToStatus(error); + assertThat(opencensusStatus.getDescription()).isEqualTo("fake message"); + assertThat(opencensusStatus.getCanonicalCode().toString()).isEqualTo(code.toString()); + } + } +}