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:
+ *
+ *
+ * - {@code Connection selected} with the following attributes:
+ *
+ * - {@code id}
+ *
- The id of the connection in the local connection pool
+ *
+ *
+ * - {@code Attempt started} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
+ *
+ * - {@code Attempt cancelled} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
- {@code attempt request count}
+ *
- The number of requests sent in this attempt. This will only be set for client
+ * streaming and bidi RPCs.
+ *
- {@code attempt response count}
+ *
- The number of responses received in this attempt. This will only be set for server
+ * streaming and bidi RPCs.
+ *
+ *
+ * - {@code Attempt failed, scheduling next attempt} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
- {@code status}
+ *
- The status code of the failed attempt
+ *
- {@code delay}
+ *
- The number of milliseconds to wait before trying again
+ *
- {@code attempt request count}
+ *
- The number of requests sent in this attempt. This will only be set for client
+ * streaming and bidi RPCs.
+ *
- {@code attempt response count}
+ *
- The number of responses received in this attempt. This will only be set for server
+ * streaming and bidi RPCs.
+ *
+ *
+ * - {@code Attempts exhausted} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
- {@code status}
+ *
- The status code of the failed attempt
+ *
- {@code attempt request count}
+ *
- The number of requests sent in this attempt. This will only be set for client
+ * streaming and bidi RPCs.
+ *
- {@code attempt response count}
+ *
- The number of responses received in this attempt. This will only be set for server
+ * streaming and bidi RPCs.
+ *
+ *
+ * - {@code Attempt failed, error not retryable} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
- {@code status}
+ *
- The status code of the failed attempt
+ *
- {@code attempt request count}
+ *
- The number of requests sent in this attempt. This will only be set for client
+ * streaming and bidi RPCs.
+ *
- {@code attempt response count}
+ *
- The number of responses received in this attempt. This will only be set for server
+ * streaming and bidi RPCs.
+ *
+ *
+ * - {@code Attempt succeeded} with the following attributes:
+ *
+ * - {@code attempt}
+ *
- Zero based sequential attempt number
+ *
- {@code attempt request count}
+ *
- The number of requests sent in this attempt. This will only be set for client
+ * streaming and bidi RPCs.
+ *
- {@code attempt response count}
+ *
- The number of responses received in this attempt. This will only be set for server
+ * streaming and bidi RPCs.
+ *
+ *
+ *
+ *
+ * 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