From 2f6720576b64c344912ec79b75a879900ad424fd Mon Sep 17 00:00:00 2001 From: Hailong Wen Date: Sat, 27 Jan 2018 22:40:09 -0800 Subject: [PATCH] Add OpenCensus tracing instrument. --- google-http-client/pom.xml | 10 + .../google/api/client/http/HttpRequest.java | 20 ++ .../api/client/util/OpenCensusUtils.java | 157 ++++++++++++++ .../api/client/util/OpenCensusUtilsTest.java | 199 ++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 google-http-client/src/main/java/com/google/api/client/util/OpenCensusUtils.java create mode 100644 google-http-client/src/test/java/com/google/api/client/util/OpenCensusUtilsTest.java diff --git a/google-http-client/pom.xml b/google-http-client/pom.xml index c07390c46..7731b6546 100644 --- a/google-http-client/pom.xml +++ b/google-http-client/pom.xml @@ -164,5 +164,15 @@ commons-codec provided + + io.opencensus + opencensus-api + 0.11.1 + + + io.opencensus + opencensus-contrib-http-util + 0.11.1 + 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 c95fe25c5..0641f60c7 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 @@ -18,11 +18,16 @@ import com.google.api.client.util.IOUtils; import com.google.api.client.util.LoggingStreamingContent; import com.google.api.client.util.ObjectParser; +import com.google.api.client.util.OpenCensusUtils; import com.google.api.client.util.Preconditions; import com.google.api.client.util.Sleeper; import com.google.api.client.util.StreamingContent; import com.google.api.client.util.StringUtils; +import io.opencensus.common.Scope; +import io.opencensus.trace.Span; +import io.opencensus.trace.Tracer; + import java.io.IOException; import java.io.InputStream; import java.util.concurrent.Callable; @@ -210,6 +215,12 @@ static String executeAndGetValueOfSomeCustomHeader(HttpRequest request) { /** Sleeper. */ private Sleeper sleeper = Sleeper.DEFAULT; + /** OpenCensus tracing component. */ + private Tracer tracer = OpenCensusUtils.getTracer(); + + /** Prefix for tracing span name. */ + private static final String traceSpanNamePrefix = "Sent." + HttpRequest.class.getName() + "."; + /** * @param transport HTTP transport * @param requestMethod HTTP request method or {@code null} for none @@ -854,7 +865,9 @@ public HttpResponse execute() throws IOException { Preconditions.checkNotNull(requestMethod); Preconditions.checkNotNull(url); + Span span = tracer.spanBuilder(traceSpanNamePrefix + "execute").startSpan(); do { + span.addAnnotation("retry #" + numRetries); // Cleanup any unneeded response from a previous iteration if (response != null) { response.ignore(); @@ -898,6 +911,8 @@ public HttpResponse execute() throws IOException { headers.setUserAgent(originalUserAgent + " " + USER_AGENT_SUFFIX); } } + OpenCensusUtils.propagateTracingContext(headers); + // headers HttpHeaders.serializeHeaders(headers, logbuf, curlbuf, logger, lowLevelHttpRequest); if (!suppressUserAgentSuffix) { @@ -977,6 +992,8 @@ public HttpResponse execute() throws IOException { // execute lowLevelHttpRequest.setTimeout(connectTimeout, readTimeout); + // switch tracing scope to current span + Scope ws = tracer.withSpan(span); try { LowLevelHttpResponse lowLevelHttpResponse = lowLevelHttpRequest.execute(); // Flag used to indicate if an exception is thrown before the response is constructed. @@ -1002,6 +1019,8 @@ public HttpResponse execute() throws IOException { 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 @@ -1057,6 +1076,7 @@ public HttpResponse execute() throws IOException { } } } 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. diff --git a/google-http-client/src/main/java/com/google/api/client/util/OpenCensusUtils.java b/google-http-client/src/main/java/com/google/api/client/util/OpenCensusUtils.java new file mode 100644 index 000000000..836706021 --- /dev/null +++ b/google-http-client/src/main/java/com/google/api/client/util/OpenCensusUtils.java @@ -0,0 +1,157 @@ +/* + * 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.util; + +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpStatusCodes; + +import io.opencensus.contrib.http.util.HttpPropagationUtil; +import io.opencensus.trace.BlankSpan; +import io.opencensus.trace.EndSpanOptions; +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.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Utilities for Census monitoring and tracing. + * + * @since 1.24 + * @author Hailong Wen + */ +public class OpenCensusUtils { + + private static final Logger LOGGER = Logger.getLogger(OpenCensusUtils.class.getName()); + + /** + * OpenCensus tracing component. + * When no OpenCensus implementation is provided, it will return a no-op tracer. + */ + static Tracer tracer = Tracing.getTracer(); + + /** + * {@link TextFormat} used in tracing context propagation. + */ + @Nullable + static TextFormat propagationTextFormat = null; + + /** + * {@link TextFormat.Setter} for {@link activeTextFormat}. + */ + @Nullable + static TextFormat.Setter propagationTextFormatSetter = null; + + /** + * Sets the {@link TextFormat} used in context propagation. + * @param textFormat the text format. + */ + public static void setPropagationTextFormat(@Nullable TextFormat textFormat) { + propagationTextFormat = textFormat; + } + + /** + * Sets the {@link TextFormat.Setter} used in context propagation. + * @param textFormatSetter the {@code TextFormat.Setter} for the text format. + */ + public static void setPropagationTextFormatSetter(@Nullable TextFormat.Setter textFormatSetter) { + propagationTextFormatSetter = textFormatSetter; + } + + /** + * Returns the tracing component of OpenCensus. + * + * @return the tracing component of OpenCensus. + */ + public static Tracer getTracer() { + return tracer; + } + + /** + * Propagate information of current tracing context. This information will be injected into HTTP + * header. + */ + public static void propagateTracingContext(HttpHeaders headers) { + Preconditions.checkNotNull(headers); + if (propagationTextFormat != null && propagationTextFormatSetter != null) { + Span span = tracer.getCurrentSpan(); + if (span != null && !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().setSampleToLocalSpanStore(true); + 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(); + } + + static { + try { + propagationTextFormat = HttpPropagationUtil.getCloudTraceFormat(); + propagationTextFormatSetter = new TextFormat.Setter() { + @Override + public void put(HttpHeaders carrier, String key, String value) { + carrier.set(key, value); + } + }; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Cannot initiate OpenCensus modules, tracing disabled", e); + } + } + + private OpenCensusUtils() {} +} diff --git a/google-http-client/src/test/java/com/google/api/client/util/OpenCensusUtilsTest.java b/google-http-client/src/test/java/com/google/api/client/util/OpenCensusUtilsTest.java new file mode 100644 index 000000000..7b75c0f01 --- /dev/null +++ b/google-http-client/src/test/java/com/google/api/client/util/OpenCensusUtilsTest.java @@ -0,0 +1,199 @@ +/* + * 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.util; + +import com.google.api.client.http.HttpHeaders; + +import io.opencensus.common.Scope; +import io.opencensus.trace.EndSpanOptions; +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 junit.framework.TestCase; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +/** + * Tests {@link OpenCensusUtils}. + * + * @author Hailong Wen + */ +public class OpenCensusUtilsTest extends TestCase { + + @Rule public ExpectedException thrown = ExpectedException.none(); + TextFormat mockTextFormat; + TextFormat.Setter mockTextFormatSetter; + HttpHeaders headers; + Tracer tracer; + + public OpenCensusUtilsTest(String testName) { + super(testName); + } + + @Override + public void setUp() { + TextFormat mockTextFormat = new TextFormat() { + @Override + public List fields() { + throw new UnsupportedOperationException("TextFormat.fields"); + } + + @Override + public void inject(SpanContext spanContext, C carrier, Setter setter) { + throw new UnsupportedOperationException("TextFormat.inject"); + } + + @Override + public SpanContext extract(C carrier, Getter getter) { + throw new UnsupportedOperationException("TextFormat.extract"); + } + }; + TextFormat.Setter mockTextFormatSetter = new TextFormat.Setter() { + @Override + public void put(HttpHeaders carrier, String key, String value) { + throw new UnsupportedOperationException("TextFormat.Setter.put"); + } + }; + headers = new HttpHeaders(); + tracer = OpenCensusUtils.getTracer(); + } + + 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); + Span span = OpenCensusUtils.getTracer().spanBuilder("test").startSpan(); + Scope scope = OpenCensusUtils.getTracer().withSpan(span); + thrown.expect(UnsupportedOperationException.class); + thrown.expectMessage("TextFormat.inject"); + OpenCensusUtils.propagateTracingContext(headers); + scope.close(); + span.end(); + } + + public void testPropagateTracingContextHeader() { + OpenCensusUtils.setPropagationTextFormatSetter(mockTextFormatSetter); + Span span = OpenCensusUtils.getTracer().spanBuilder("test").startSpan(); + Scope scope = OpenCensusUtils.getTracer().withSpan(span); + thrown.expect(UnsupportedOperationException.class); + thrown.expectMessage("TextFormat.Setter.put"); + OpenCensusUtils.propagateTracingContext(headers); + scope.close(); + span.end(); + } + + public void testPropagateTracingContextInvalidSpan() { + OpenCensusUtils.setPropagationTextFormat(mockTextFormat); + // No injection. No exceptions should be thrown. + OpenCensusUtils.propagateTracingContext(headers); + } + + public void testGetEndSpanOptionsNoResponse() { + EndSpanOptions expected = + EndSpanOptions.builder().setSampleToLocalSpanStore(true).setStatus(Status.UNKNOWN).build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(null)); + } + + public void testGetEndSpanOptionsSuccess() { + EndSpanOptions expected = + EndSpanOptions.builder().setSampleToLocalSpanStore(true).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() + .setSampleToLocalSpanStore(true) + .setStatus(Status.INVALID_ARGUMENT) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(400)); + } + + public void testGetEndSpanOptionsUnauthorized() { + EndSpanOptions expected = EndSpanOptions + .builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.UNAUTHENTICATED) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(401)); + } + + public void testGetEndSpanOptionsForbidden() { + EndSpanOptions expected = EndSpanOptions + .builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.PERMISSION_DENIED) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(403)); + } + + public void testGetEndSpanOptionsNotFound() { + EndSpanOptions expected = EndSpanOptions + .builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.NOT_FOUND) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(404)); + } + + public void testGetEndSpanOptionsPreconditionFailed() { + EndSpanOptions expected = EndSpanOptions + .builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.FAILED_PRECONDITION) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(412)); + } + + public void testGetEndSpanOptionsServerError() { + EndSpanOptions expected = EndSpanOptions + .builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.UNAVAILABLE) + .build(); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(500)); + } + + public void testGetEndSpanOptionsOther() { + EndSpanOptions expected = EndSpanOptions.builder() + .setSampleToLocalSpanStore(true) + .setStatus(Status.UNKNOWN) + .build(); + // test some random unsupported statuses + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(301)); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(402)); + assertEquals(expected, OpenCensusUtils.getEndSpanOptions(501)); + } +}