Repository: googleapis/google-http-java-client
Pull request #837 (2019-10-11 17:52:27)

revert: "feat: remove opencensus (#834)"
#621 

chingor13: We can make it an optional dependency before the opentelemetry conversion. So rather than ripping it out and then reimplementing, we can make it defensive in a single PR.

Patch:
diff --git a/google-http-client/pom.xml b/google-http-client/pom.xml
index 9dcf51e0..9b548025 100644
--- a/google-http-client/pom.xml
+++ b/google-http-client/pom.xml
@@ -16,6 +16,18 @@
   </description>
 
   <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-dependency-plugin</artifactId>
+          <version>3.1.1</version>
+          <configuration>
+            <ignoredUnusedDeclaredDependencies>io.opencensus:opencensus-impl</ignoredUnusedDeclaredDependencies>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -97,6 +109,10 @@
       <groupId>org.apache.httpcomponents</groupId>
       <artifactId>httpcore</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
@@ -105,6 +121,14 @@
       <groupId>com.google.j2objc</groupId>
       <artifactId>j2objc-annotations</artifactId>
     </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-contrib-http-util</artifactId>
+    </dependency>
 
     <dependency>
       <groupId>com.google.guava</groupId>
@@ -126,5 +150,15 @@
       <artifactId>mockito-all</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.opencensus</groupId>
+      <artifactId>opencensus-testing</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
index 7bd95957..f6cd29ef 100644
--- a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
+++ b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java
@@ -23,6 +23,11 @@ import com.google.api.client.util.Sleeper;
 import com.google.api.client.util.StreamingContent;
 import com.google.api.client.util.StringUtils;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.http.util.HttpTraceAttributeConstants;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Properties;
@@ -192,6 +197,9 @@ public final class HttpRequest {
   /** Sleeper. */
   private Sleeper sleeper = Sleeper.DEFAULT;
 
+  /** OpenCensus tracing component. */
+  private final Tracer tracer = OpenCensusUtils.getTracer();
+
   /**
    * Determines whether {@link HttpResponse#getContent()} of this request should return raw input
    * stream or not.
@@ -835,7 +843,13 @@ public final class HttpRequest {
     Preconditions.checkNotNull(requestMethod);
     Preconditions.checkNotNull(url);
 
+    Span span =
+        tracer
+            .spanBuilder(OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE)
+            .setRecordEvents(OpenCensusUtils.isRecordEvent())
+            .startSpan();
     do {
+      span.addAnnotation("retry #" + (numRetries - retriesRemaining));
       // Cleanup any unneeded response from a previous iteration
       if (response != null) {
         response.ignore();
@@ -850,6 +864,10 @@ public final class HttpRequest {
       }
       // build low-level HTTP request
       String urlString = url.build();
+      addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_METHOD, requestMethod);
+      addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_HOST, url.getHost());
+      addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_PATH, url.getRawPath());
+      addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_URL, urlString);
 
       LowLevelHttpRequest lowLevelHttpRequest = transport.buildRequest(requestMethod, urlString);
       Logger logger = HttpTransport.LOGGER;
@@ -879,11 +897,14 @@ public final class HttpRequest {
       if (!suppressUserAgentSuffix) {
         if (originalUserAgent == null) {
           headers.setUserAgent(USER_AGENT_SUFFIX);
+          addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, USER_AGENT_SUFFIX);
         } else {
           String newUserAgent = originalUserAgent + " " + USER_AGENT_SUFFIX;
           headers.setUserAgent(newUserAgent);
+          addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, newUserAgent);
         }
       }
+      OpenCensusUtils.propagateTracingContext(span, headers);
 
       // headers
       HttpHeaders.serializeHeaders(headers, logbuf, curlbuf, logger, lowLevelHttpRequest);
@@ -967,8 +988,15 @@ public final class HttpRequest {
       lowLevelHttpRequest.setTimeout(connectTimeout, readTimeout);
       lowLevelHttpRequest.setWriteTimeout(writeTimeout);
 
+      // switch tracing scope to current span
+      @SuppressWarnings("MustBeClosedChecker")
+      Scope ws = tracer.withSpan(span);
+      OpenCensusUtils.recordSentMessageEvent(span, lowLevelHttpRequest.getContentLength());
       try {
         LowLevelHttpResponse lowLevelHttpResponse = lowLevelHttpRequest.execute();
+        if (lowLevelHttpResponse != null) {
+          OpenCensusUtils.recordReceivedMessageEvent(span, lowLevelHttpResponse.getContentLength());
+        }
         // Flag used to indicate if an exception is thrown before the response is constructed.
         boolean responseConstructed = false;
         try {
@@ -986,6 +1014,8 @@ public final class HttpRequest {
         if (!retryOnExecuteIOException
             && (ioExceptionHandler == null
                 || !ioExceptionHandler.handleIOException(this, retryRequest))) {
+          // static analysis shows response is always null here
+          span.end(OpenCensusUtils.getEndSpanOptions(null));
           throw e;
         }
         // Save the exception in case the retries do not work and we need to re-throw it later.
@@ -993,6 +1023,8 @@ public final class HttpRequest {
         if (loggable) {
           logger.log(Level.WARNING, "exception thrown while executing request", e);
         }
+      } finally {
+        ws.close();
       }
 
       // Flag used to indicate if an exception is thrown before the response has completed
@@ -1049,6 +1081,8 @@ public final class HttpRequest {
         }
       }
     } while (retryRequest);
+    span.end(OpenCensusUtils.getEndSpanOptions(response == null ? null : response.getStatusCode()));
+
     if (response == null) {
       // Retries did not help resolve the execute exception, re-throw it.
       throw executeException;
@@ -1163,6 +1197,12 @@ public final class HttpRequest {
     return this;
   }
 
+  private static void addSpanAttribute(Span span, String key, String value) {
+    if (value != null) {
+      span.putAttribute(key, AttributeValue.stringAttributeValue(value));
+    }
+  }
+  
   private static String getVersion() {
     String version = HttpRequest.class.getPackage().getImplementationVersion();
     // in a non-packaged environment (local), there's no implementation version to read
diff --git a/google-http-client/src/main/java/com/google/api/client/http/OpenCensusUtils.java b/google-http-client/src/main/java/com/google/api/client/http/OpenCensusUtils.java
new file mode 100644
index 00000000..5b2c9d41
--- /dev/null
+++ b/google-http-client/src/main/java/com/google/api/client/http/OpenCensusUtils.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http;
+
+import com.google.api.client.util.Beta;
+import com.google.api.client.util.Preconditions;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import io.opencensus.contrib.http.util.HttpPropagationUtil;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.propagation.TextFormat;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * {@link Beta} <br>
+ * Utilities for Census monitoring and tracing.
+ *
+ * @author Hailong Wen
+ * @since 1.28
+ */
+@Beta
+public class OpenCensusUtils {
+
+  private static final Logger logger = Logger.getLogger(OpenCensusUtils.class.getName());
+
+  /** Span name for tracing {@link HttpRequest#execute()}. */
+  public static final String SPAN_NAME_HTTP_REQUEST_EXECUTE =
+      "Sent." + HttpRequest.class.getName() + ".execute";
+
+  /**
+   * OpenCensus tracing component. When no OpenCensus implementation is provided, it will return a
+   * no-op tracer.
+   */
+  private static final Tracer tracer = Tracing.getTracer();
+
+  /** Sequence id generator for message event. */
+  private static final AtomicLong idGenerator = new AtomicLong();
+
+  /** Whether spans should be recorded locally. Defaults to true. */
+  private static volatile boolean isRecordEvent = true;
+
+  /** {@link TextFormat} used in tracing context propagation. */
+  @Nullable @VisibleForTesting static volatile TextFormat propagationTextFormat = null;
+
+  /** {@link TextFormat.Setter} for {@link #propagationTextFormat}. */
+  @Nullable @VisibleForTesting static volatile TextFormat.Setter propagationTextFormatSetter = null;
+
+  /**
+   * Sets the {@link TextFormat} used in context propagation.
+   *
+   * <p>This API allows users of google-http-client to specify other text format, or disable context
+   * propagation by setting it to {@code null}. It should be used along with {@link
+   * #setPropagationTextFormatSetter} for setting purpose.
+   *
+   * @param textFormat the text format.
+   */
+  public static void setPropagationTextFormat(@Nullable TextFormat textFormat) {
+    propagationTextFormat = textFormat;
+  }
+
+  /**
+   * Sets the {@link io.opencensus.trace.propagation.TextFormat.Setter} used in context propagation.
+   *
+   * <p>This API allows users of google-http-client to specify other text format setter, or disable
+   * context propagation by setting it to {@code null}. It should be used along with {@link
+   * #setPropagationTextFormat} for setting purpose.
+   *
+   * @param textFormatSetter the {@code TextFormat.Setter} for the text format.
+   */
+  public static void setPropagationTextFormatSetter(@Nullable TextFormat.Setter textFormatSetter) {
+    propagationTextFormatSetter = textFormatSetter;
+  }
+
+  /**
+   * Sets whether spans should be recorded locally.
+   *
+   * <p>This API allows users of google-http-client to turn on/off local span collection.
+   *
+   * @param recordEvent record span locally if true.
+   */
+  public static void setIsRecordEvent(boolean recordEvent) {
+    isRecordEvent = recordEvent;
+  }
+
+  /**
+   * Returns the tracing component of OpenCensus.
+   *
+   * @return the tracing component of OpenCensus.
+   */
+  public static Tracer getTracer() {
+    return tracer;
+  }
+
+  /**
+   * Returns whether spans should be recorded locally.
+   *
+   * @return whether spans should be recorded locally.
+   */
+  public static boolean isRecordEvent() {
+    return isRecordEvent;
+  }
+
+  /**
+   * Propagate information of current tracing context. This information will be injected into HTTP
+   * header.
+   *
+   * @param span the span to be propagated.
+   * @param headers the headers used in propagation.
+   */
+  public static void propagateTracingContext(Span span, HttpHeaders headers) {
+    Preconditions.checkArgument(span != null, "span should not be null.");
+    Preconditions.checkArgument(headers != null, "headers should not be null.");
+    if (propagationTextFormat != null && propagationTextFormatSetter != null) {
+      if (!span.equals(BlankSpan.INSTANCE)) {
+        propagationTextFormat.inject(span.getContext(), headers, propagationTextFormatSetter);
+      }
+    }
+  }
+
+  /**
+   * Returns an {@link EndSpanOptions} to end a http span according to the status code.
+   *
+   * @param statusCode the status code, can be null to represent no valid response is returned.
+   * @return an {@code EndSpanOptions} that best suits the status code.
+   */
+  public static EndSpanOptions getEndSpanOptions(@Nullable Integer statusCode) {
+    // Always sample the span, but optionally export it.
+    EndSpanOptions.Builder builder = EndSpanOptions.builder();
+    if (statusCode == null) {
+      builder.setStatus(Status.UNKNOWN);
+    } else if (!HttpStatusCodes.isSuccess(statusCode)) {
+      switch (statusCode) {
+        case HttpStatusCodes.STATUS_CODE_BAD_REQUEST:
+          builder.setStatus(Status.INVALID_ARGUMENT);
+          break;
+        case HttpStatusCodes.STATUS_CODE_UNAUTHORIZED:
+          builder.setStatus(Status.UNAUTHENTICATED);
+          break;
+        case HttpStatusCodes.STATUS_CODE_FORBIDDEN:
+          builder.setStatus(Status.PERMISSION_DENIED);
+          break;
+        case HttpStatusCodes.STATUS_CODE_NOT_FOUND:
+          builder.setStatus(Status.NOT_FOUND);
+          break;
+        case HttpStatusCodes.STATUS_CODE_PRECONDITION_FAILED:
+          builder.setStatus(Status.FAILED_PRECONDITION);
+          break;
+        case HttpStatusCodes.STATUS_CODE_SERVER_ERROR:
+          builder.setStatus(Status.UNAVAILABLE);
+          break;
+        default:
+          builder.setStatus(Status.UNKNOWN);
+      }
+    } else {
+      builder.setStatus(Status.OK);
+    }
+    return builder.build();
+  }
+
+  /**
+   * Records a new message event which contains the size of the request content. Note that the size
+   * represents the message size in application layer, i.e., content-length.
+   *
+   * @param span The {@code span} in which the send event occurs.
+   * @param size Size of the request.
+   */
+  public static void recordSentMessageEvent(Span span, long size) {
+    recordMessageEvent(span, size, Type.SENT);
+  }
+
+  /**
+   * Records a new message event which contains the size of the response content. Note that the size
+   * represents the message size in application layer, i.e., content-length.
+   *
+   * @param span The {@code span} in which the receive event occurs.
+   * @param size Size of the response.
+   */
+  public static void recordReceivedMessageEvent(Span span, long size) {
+    recordMessageEvent(span, size, Type.RECEIVED);
+  }
+
+  /**
+   * Records a message event of a certain {@link MessageEvent.Type}. This method is package
+   * protected since {@link MessageEvent} might be deprecated in future releases.
+   *
+   * @param span The {@code span} in which the event occurs.
+   * @param size Size of the message.
+   * @param eventType The {@code NetworkEvent.Type} of the message event.
+   */
+  @VisibleForTesting
+  static void recordMessageEvent(Span span, long size, Type eventType) {
+    Preconditions.checkArgument(span != null, "span should not be null.");
+    if (size < 0) {
+      size = 0;
+    }
+    MessageEvent event =
+        MessageEvent.builder(eventType, idGenerator.getAndIncrement())
+            .setUncompressedMessageSize(size)
+            .build();
+    span.addMessageEvent(event);
+  }
+
+  static {
+    try {
+      propagationTextFormat = HttpPropagationUtil.getCloudTraceFormat();
+      propagationTextFormatSetter =
+          new TextFormat.Setter<HttpHeaders>() {
+            @Override
+            public void put(HttpHeaders carrier, String key, String value) {
+              carrier.set(key, value);
+            }
+          };
+    } catch (Exception e) {
+      logger.log(
+          Level.WARNING, "Cannot initialize default OpenCensus HTTP propagation text format.", e);
+    }
+
+    try {
+      Tracing.getExportComponent()
+          .getSampledSpanStore()
+          .registerSpanNamesForCollection(ImmutableList.of(SPAN_NAME_HTTP_REQUEST_EXECUTE));
+    } catch (Exception e) {
+      logger.log(Level.WARNING, "Cannot register default OpenCensus span names for collection.", e);
+    }
+  }
+
+  private OpenCensusUtils() {}
+}
diff --git a/pom.xml b/pom.xml
index 41ff1298..6ffe61f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -166,6 +166,11 @@
         <artifactId>guava-testlib</artifactId>
         <version>${project.guava.version}</version>
       </dependency>
+      <dependency>
+        <groupId>com.google.code.findbugs</groupId>
+        <artifactId>jsr305</artifactId>
+        <version>${project.jsr305.version}</version>
+      </dependency>
       <dependency>
         <groupId>com.google.protobuf</groupId>
         <artifactId>protobuf-java</artifactId>
@@ -236,6 +241,26 @@
         <artifactId>j2objc-annotations</artifactId>
         <version>1.3</version>
       </dependency>
+      <dependency>
+        <groupId>io.opencensus</groupId>
+        <artifactId>opencensus-api</artifactId>
+        <version>${project.opencensus.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.opencensus</groupId>
+        <artifactId>opencensus-contrib-http-util</artifactId>
+        <version>${project.opencensus.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.opencensus</groupId>
+        <artifactId>opencensus-impl</artifactId>
+        <version>${project.opencensus.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.opencensus</groupId>
+        <artifactId>opencensus-testing</artifactId>
+        <version>${project.opencensus.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -527,6 +552,7 @@
     <project.http-client.version>1.32.2-SNAPSHOT</project.http-client.version><!-- {x-version-update:google-http-client-parent:current} -->
     <project.appengine.version>1.9.71</project.appengine.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.jsr305.version>3.0.2</project.jsr305.version>
     <project.gson.version>2.8.6</project.gson.version>
     <project.jackson-core2.version>2.10.0</project.jackson-core2.version>
     <project.protobuf-java.version>3.10.0</project.protobuf-java.version>
@@ -535,6 +561,7 @@
     <project.commons-logging.version>1.2</project.commons-logging.version>
     <project.httpclient.version>4.5.10</project.httpclient.version>
     <project.httpcore.version>4.4.12</project.httpcore.version>
+    <project.opencensus.version>0.24.0</project.opencensus.version>
     <project.root-directory>..</project.root-directory>
     <deploy.autorelease>false</deploy.autorelease>
   </properties>


Test patch:
diff --git a/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTracingTest.java b/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTracingTest.java
new file mode 100644
index 00000000..5d89f035
--- /dev/null
+++ b/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTracingTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.api.client.http;
+
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import io.opencensus.common.Functions;
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.google.api.client.http.OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class HttpRequestTracingTest {
+  private static final TestHandler testHandler = new TestHandler();
+
+  @Before
+  public void setupTestTracer() {
+    Tracing.getExportComponent().getSpanExporter().registerHandler("test", testHandler);
+    TraceParams params =
+        Tracing.getTraceConfig()
+            .getActiveTraceParams()
+            .toBuilder()
+            .setSampler(Samplers.alwaysSample())
+            .build();
+    Tracing.getTraceConfig().updateActiveTraceParams(params);
+  }
+
+  @After
+  public void teardownTestTracer() {
+    Tracing.getExportComponent().getSpanExporter().unregisterHandler("test");
+  }
+
+  @Test(timeout = 20_000L)
+  public void executeCreatesSpan() throws IOException {
+    MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse()
+        .setStatusCode(200);
+    HttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(mockResponse)
+        .build();
+    HttpRequest request = new HttpRequestFactory(transport, null)
+        .buildGetRequest(new GenericUrl("https://google.com/"));
+    request.execute();
+
+    // This call blocks - we set a timeout on this test to ensure we don't wait forever
+    List<SpanData> spans = testHandler.waitForExport(1);
+    assertEquals(1, spans.size());
+    SpanData span = spans.get(0);
+
+    // Ensure the span name is set
+    assertEquals(SPAN_NAME_HTTP_REQUEST_EXECUTE, span.getName());
+
+    // Ensure we have basic span attributes
+    assertAttributeEquals(span, "http.path", "/");
+    assertAttributeEquals(span, "http.host", "google.com");
+    assertAttributeEquals(span, "http.url", "https://google.com/");
+    assertAttributeEquals(span, "http.method", "GET");
+
+    // Ensure we have a single annotation for starting the first attempt
+    assertEquals(1, span.getAnnotations().getEvents().size());
+
+    // Ensure we have 2 message events, SENT and RECEIVED
+    assertEquals(2, span.getMessageEvents().getEvents().size());
+    assertEquals(MessageEvent.Type.SENT, span.getMessageEvents().getEvents().get(0).getEvent().getType());
+    assertEquals(MessageEvent.Type.RECEIVED, span.getMessageEvents().getEvents().get(1).getEvent().getType());
+
+    // Ensure we record the span status as OK
+    assertEquals(Status.OK, span.getStatus());
+  }
+
+  @Test(timeout = 20_000L)
+  public void executeExceptionCreatesSpan() throws IOException {
+    HttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpRequest(new MockLowLevelHttpRequest() {
+          @Override
+          public LowLevelHttpResponse execute() throws IOException {
+            throw new IOException("some IOException");
+          }
+        })
+        .build();
+    HttpRequest request = new HttpRequestFactory(transport, null)
+        .buildGetRequest(new GenericUrl("https://google.com/"));
+
+    try {
+      request.execute();
+      fail("expected to throw an IOException");
+    } catch (IOException expected) {
+    }
+
+    // This call blocks - we set a timeout on this test to ensure we don't wait forever
+    List<SpanData> spans = testHandler.waitForExport(1);
+    assertEquals(1, spans.size());
+    SpanData span = spans.get(0);
+
+    // Ensure the span name is set
+    assertEquals(SPAN_NAME_HTTP_REQUEST_EXECUTE, span.getName());
+
+    // Ensure we have basic span attributes
+    assertAttributeEquals(span, "http.path", "/");
+    assertAttributeEquals(span, "http.host", "google.com");
+    assertAttributeEquals(span, "http.url", "https://google.com/");
+    assertAttributeEquals(span, "http.method", "GET");
+
+    // Ensure we have a single annotation for starting the first attempt
+    assertEquals(1, span.getAnnotations().getEvents().size());
+
+    // Ensure we have 2 message events, SENT and RECEIVED
+    assertEquals(1, span.getMessageEvents().getEvents().size());
+    assertEquals(MessageEvent.Type.SENT, span.getMessageEvents().getEvents().get(0).getEvent().getType());
+
+    // Ensure we record the span status as UNKNOWN
+    assertEquals(Status.UNKNOWN, span.getStatus());  }
+
+  void assertAttributeEquals(SpanData span, String attributeName, String expectedValue) {
+    Object attributeValue = span.getAttributes().getAttributeMap().get(attributeName);
+    assertNotNull("expected span to contain attribute: " + attributeName, attributeValue);
+    assertTrue(attributeValue instanceof AttributeValue);
+    String value = ((AttributeValue) attributeValue).match(
+        Functions.returnToString(),
+        Functions.returnToString(),
+        Functions.returnToString(),
+        Functions.returnToString(),
+        Functions.</*@Nullable*/ String>returnNull());
+    assertEquals(expectedValue, value);
+  }
+}
diff --git a/google-http-client/src/test/java/com/google/api/client/http/OpenCensusUtilsTest.java b/google-http-client/src/test/java/com/google/api/client/http/OpenCensusUtilsTest.java
new file mode 100644
index 00000000..8fa8624e
--- /dev/null
+++ b/google-http-client/src/test/java/com/google/api/client/http/OpenCensusUtilsTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2018 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.api.client.http;
+
+
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.propagation.TextFormat;
+import java.util.List;
+import java.util.Map;
+import junit.framework.TestCase;
+
+/**
+ * Tests {@link OpenCensusUtils}.
+ *
+ * @author Hailong Wen
+ */
+public class OpenCensusUtilsTest extends TestCase {
+
+  TextFormat mockTextFormat;
+  TextFormat.Setter mockTextFormatSetter;
+  TextFormat originTextFormat;
+  TextFormat.Setter originTextFormatSetter;
+  Span mockSpan;
+  HttpHeaders headers;
+  Tracer tracer;
+
+  public OpenCensusUtilsTest(String testName) {
+    super(testName);
+  }
+
+  @Override
+  public void setUp() {
+    mockTextFormat =
+        new TextFormat() {
+          @Override
+          public List<String> fields() {
+            throw new UnsupportedOperationException("TextFormat.fields");
+          }
+
+          @Override
+          public <C> void inject(SpanContext spanContext, C carrier, Setter<C> setter) {
+            throw new UnsupportedOperationException("TextFormat.inject");
+          }
+
+          @Override
+          public <C> SpanContext extract(C carrier, Getter<C> getter) {
+            throw new UnsupportedOperationException("TextFormat.extract");
+          }
+        };
+    mockTextFormatSetter =
+        new TextFormat.Setter<HttpHeaders>() {
+          @Override
+          public void put(HttpHeaders carrier, String key, String value) {
+            throw new UnsupportedOperationException("TextFormat.Setter.put");
+          }
+        };
+    headers = new HttpHeaders();
+    tracer = OpenCensusUtils.getTracer();
+    mockSpan =
+        new Span(tracer.getCurrentSpan().getContext(), null) {
+
+          @Override
+          public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+
+          @Override
+          public void addAnnotation(Annotation annotation) {}
+
+          @Override
+          public void addMessageEvent(MessageEvent event) {
+            throw new UnsupportedOperationException("Span.addMessageEvent");
+          }
+
+          @Override
+          public void addLink(Link link) {}
+
+          @Override
+          public void end(EndSpanOptions options) {}
+        };
+    originTextFormat = OpenCensusUtils.propagationTextFormat;
+    originTextFormatSetter = OpenCensusUtils.propagationTextFormatSetter;
+  }
+
+  @Override
+  public void tearDown() {
+    OpenCensusUtils.setPropagationTextFormat(originTextFormat);
+    OpenCensusUtils.setPropagationTextFormatSetter(originTextFormatSetter);
+  }
+
+  public void testInitializatoin() {
+    assertNotNull(OpenCensusUtils.getTracer());
+    assertNotNull(OpenCensusUtils.propagationTextFormat);
+    assertNotNull(OpenCensusUtils.propagationTextFormatSetter);
+  }
+
+  public void testSetPropagationTextFormat() {
+    OpenCensusUtils.setPropagationTextFormat(mockTextFormat);
+    assertEquals(mockTextFormat, OpenCensusUtils.propagationTextFormat);
+  }
+
+  public void testSetPropagationTextFormatSetter() {
+    OpenCensusUtils.setPropagationTextFormatSetter(mockTextFormatSetter);
+    assertEquals(mockTextFormatSetter, OpenCensusUtils.propagationTextFormatSetter);
+  }
+
+  public void testPropagateTracingContextInjection() {
+    OpenCensusUtils.setPropagationTextFormat(mockTextFormat);
+    try {
+      OpenCensusUtils.propagateTracingContext(mockSpan, headers);
+      fail("expected " + UnsupportedOperationException.class);
+    } catch (UnsupportedOperationException e) {
+      assertEquals(e.getMessage(), "TextFormat.inject");
+    }
+  }
+
+  public void testPropagateTracingContextHeader() {
+    OpenCensusUtils.setPropagationTextFormatSetter(mockTextFormatSetter);
+    try {
+      OpenCensusUtils.propagateTracingContext(mockSpan, headers);
+      fail("expected " + UnsupportedOperationException.class);
+    } catch (UnsupportedOperationException e) {
+      assertEquals(e.getMessage(), "TextFormat.Setter.put");
+    }
+  }
+
+  public void testPropagateTracingContextNullSpan() {
+    OpenCensusUtils.setPropagationTextFormat(mockTextFormat);
+    try {
+      OpenCensusUtils.propagateTracingContext(null, headers);
+      fail("expected " + IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertEquals(e.getMessage(), "span should not be null.");
+    }
+  }
+
+  public void testPropagateTracingContextNullHeaders() {
+    OpenCensusUtils.setPropagationTextFormat(mockTextFormat);
+    try {
+      OpenCensusUtils.propagateTracingContext(mockSpan, null);
+      fail("expected " + IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertEquals(e.getMessage(), "headers should not be null.");
+    }
+  }
+
+  public void testPropagateTracingContextInvalidSpan() {
+    OpenCensusUtils.setPropagationTextFormat(mockTextFormat);
+    // No injection. No exceptions should be thrown.
+    OpenCensusUtils.propagateTracingContext(BlankSpan.INSTANCE, headers);
+  }
+
+  public void testGetEndSpanOptionsNoResponse() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.UNKNOWN).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(null));
+  }
+
+  public void testGetEndSpanOptionsSuccess() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.OK).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(200));
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(201));
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(202));
+  }
+
+  public void testGetEndSpanOptionsBadRequest() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.INVALID_ARGUMENT).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(400));
+  }
+
+  public void testGetEndSpanOptionsUnauthorized() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.UNAUTHENTICATED).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(401));
+  }
+
+  public void testGetEndSpanOptionsForbidden() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.PERMISSION_DENIED).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(403));
+  }
+
+  public void testGetEndSpanOptionsNotFound() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.NOT_FOUND).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(404));
+  }
+
+  public void testGetEndSpanOptionsPreconditionFailed() {
+    EndSpanOptions expected =
+        EndSpanOptions.builder().setStatus(Status.FAILED_PRECONDITION).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(412));
+  }
+
+  public void testGetEndSpanOptionsServerError() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.UNAVAILABLE).build();
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(500));
+  }
+
+  public void testGetEndSpanOptionsOther() {
+    EndSpanOptions expected = EndSpanOptions.builder().setStatus(Status.UNKNOWN).build();
+    // test some random unsupported statuses
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(301));
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(402));
+    assertEquals(expected, OpenCensusUtils.getEndSpanOptions(501));
+  }
+
+  public void testRecordMessageEventInNullSpan() {
+    try {
+      OpenCensusUtils.recordMessageEvent(null, 0, MessageEvent.Type.SENT);
+      fail("expected " + IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertEquals(e.getMessage(), "span should not be null.");
+    }
+  }
+
+  public void testRecordMessageEvent() {
+    try {
+      OpenCensusUtils.recordMessageEvent(mockSpan, 0, MessageEvent.Type.SENT);
+      fail("expected " + UnsupportedOperationException.class);
+    } catch (UnsupportedOperationException e) {
+      assertEquals(e.getMessage(), "Span.addMessageEvent");
+    }
+  }
+}