From 36f6a672cbecb7e8ab4d149facac67ded102288b Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 15 Sep 2023 14:42:46 +0200 Subject: [PATCH] Initial commit --- google-http-client-micrometer/pom.xml | 114 ++++++++++++++ .../http/micrometer/GoogleClientContext.java | 46 ++++++ .../MicrometerObservationHttpInterceptor.java | 143 ++++++++++++++++++ .../GoogleClientObservationTests.java | 57 +++++++ .../api/client/http/HttpInterceptor.java | 47 ++++++ .../google/api/client/http/HttpRequest.java | 109 ++++++++----- .../http/OpenCensusHttpInterceptor.java | 141 +++++++++++++++++ pom.xml | 13 ++ 8 files changed, 634 insertions(+), 36 deletions(-) create mode 100644 google-http-client-micrometer/pom.xml create mode 100644 google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/GoogleClientContext.java create mode 100644 google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/MicrometerObservationHttpInterceptor.java create mode 100644 google-http-client-micrometer/src/test/java/com/google/api/client/http/micrometer/GoogleClientObservationTests.java create mode 100644 google-http-client/src/main/java/com/google/api/client/http/HttpInterceptor.java create mode 100644 google-http-client/src/main/java/com/google/api/client/http/OpenCensusHttpInterceptor.java diff --git a/google-http-client-micrometer/pom.xml b/google-http-client-micrometer/pom.xml new file mode 100644 index 000000000..5695ac711 --- /dev/null +++ b/google-http-client-micrometer/pom.xml @@ -0,0 +1,114 @@ + + 4.0.0 + + com.google.http-client + google-http-client-parent + 1.43.4-SNAPSHOT + ../pom.xml + + google-http-client-micrometer + 1.43.4-SNAPSHOT + Micrometer Support + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + true + + + + maven-compiler-plugin + + 8 + 8 + + + + maven-javadoc-plugin + + + http://download.oracle.com/javase/8/docs/api/ + + ${project.name} ${project.version} + ${project.artifactId} ${project.version} + none + 8 + + + + maven-source-plugin + + + source-jar + compile + + jar + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-test-source + generate-test-sources + + add-test-source + + + + target/generated-test-sources + + + + + + + maven-surefire-plugin + 3.1.2 + + -Xmx1024m + sponge_log + + + + maven-jar-plugin + + + + com.google.api.client.http.micrometer + + + + + + + + + com.google.http-client + google-http-client + + + io.micrometer + micrometer-observation + + + io.micrometer + micrometer-tracing-integration-test + test + + + org.junit.vintage + junit-vintage-engine + 5.10.0 + test + + + diff --git a/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/GoogleClientContext.java b/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/GoogleClientContext.java new file mode 100644 index 000000000..474d09a72 --- /dev/null +++ b/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/GoogleClientContext.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.micrometer; + +import com.google.api.client.http.*; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.transport.Kind; +import io.micrometer.observation.transport.Propagator; +import io.micrometer.observation.transport.RequestReplySenderContext; +import io.opencensus.common.Scope; +import io.opencensus.contrib.http.util.HttpTraceAttributeConstants; +import io.opencensus.trace.AttributeValue; +import io.opencensus.trace.Span; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +/** + * TODO: Write some docs + * + *

Implementations should normally be thread-safe. + * + * @since 1.43 + * @author Marcin Grzejszczak + */ +public class GoogleClientContext extends RequestReplySenderContext { + + public GoogleClientContext(HttpRequest httpRequest) { + super((req, key, value) -> Objects.requireNonNull(req).getHeaders().put(key, value)); + setCarrier(httpRequest); + } +} \ No newline at end of file diff --git a/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/MicrometerObservationHttpInterceptor.java b/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/MicrometerObservationHttpInterceptor.java new file mode 100644 index 000000000..9a66e180d --- /dev/null +++ b/google-http-client-micrometer/src/main/java/com/google/api/client/http/micrometer/MicrometerObservationHttpInterceptor.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2023 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.micrometer; + +import com.google.api.client.http.*; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.opencensus.contrib.http.util.HttpTraceAttributeConstants; +import io.opencensus.trace.AttributeValue; +import io.opencensus.trace.MessageEvent; +import io.opencensus.trace.Span; + +import java.io.IOException; +import java.util.Map; + +/** + * TODO: Write some docs + * + *

Implementations should normally be thread-safe. + * + * @since 1.43 + * @author Marcin Grzejszczak + */ +public class MicrometerObservationHttpInterceptor implements HttpInterceptor { + + private final ObservationRegistry observationRegistry; + + public MicrometerObservationHttpInterceptor(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public void beforeAllExecutions(Map context, HttpRequest httpRequest) { + // TODO: Add conventions etc for customization + GoogleClientContext clientContext = new GoogleClientContext(httpRequest); + Observation observation = Observation.createNotStarted("http.client.duration", () -> clientContext, observationRegistry); + context.put(Observation.class, observation); + } + + @SuppressWarnings("unchecked") + private T getRequired(Object key, Map context) { + if (!context.containsKey(key)) { + throw new IllegalStateException("Object with key <" + key + "> was not found in <" + context + + ">"); + } + return (T) context.get(key); + } + + @Override + public void beforeSingleExecutionStart(Map context, HttpRequest httpRequest, int numRetries, int retriesRemaining) { + Observation observation = getObservation(context); + observation.highCardinalityKeyValue("retry #", String.valueOf(numRetries - retriesRemaining)); + } + + @Override + public void beforeSingleExecutionRequestBuilding(Map context, HttpRequest httpRequest, String urlString) { + Observation observation = getObservation(context); + String requestMethod = httpRequest.getRequestMethod(); + GenericUrl url = httpRequest.getUrl(); + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_METHOD, requestMethod); + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_HOST, url.getHost()); + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_PATH, url.getRawPath()); + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_URL, urlString); + } + + private Observation getObservation(Map context) { + return getRequired(Observation.class, context); + } + + private Observation.Scope getScope(Map context) { + return getRequired(Observation.Scope.class, context); + } + + @Override + public void beforeSingleExecutionHeadersSerialization(Map context, HttpRequest httpRequest, String originalUserAgent) { + HttpHeaders headers = httpRequest.getHeaders(); + Observation observation = getObservation(context); + if (!httpRequest.getSuppressUserAgentSuffix()) { + if (originalUserAgent == null) { + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_USER_AGENT, HttpRequest.USER_AGENT_SUFFIX); + } else { + addHighCardinalityKey(observation, HttpTraceAttributeConstants.HTTP_USER_AGENT, headers.getUserAgent()); + } + } + observation.contextualName(httpRequest.getRequestMethod()); + observation.start(); //propagation happens here + } + + @Override + public void beforeSingleExecutionBytesSending(Map context, HttpRequest httpRequest, LowLevelHttpRequest lowLevelHttpRequest) { + // switch tracing scope to current span + Observation observation = getObservation(context); + @SuppressWarnings("MustBeClosedChecker") + Observation.Scope ws = observation.openScope(); + context.put(Observation.Scope.class, ws); + } + + @Override + public void afterSingleExecutionResponseReceived(Map context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse) throws IOException { + Observation observation = getObservation(context); + if (lowLevelHttpResponse != null) { + observation.lowCardinalityKeyValue( + HttpTraceAttributeConstants.HTTP_STATUS_CODE, + String.valueOf(lowLevelHttpResponse.getStatusCode())); + } + } + + @Override + public void afterSingleExecutionExceptionHappened(Map context, Throwable throwable) { + Observation observation = getObservation(context); + observation.error(throwable); + } + + @Override + public void afterSingleExecutionOnFinally(Map context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse) { + getScope(context).close(); + } + + @Override + public void afterAllExecutions(Map context, HttpRequest httpRequest, HttpResponse response) { + Observation observation = getObservation(context); + ((GoogleClientContext) observation.getContext()).setResponse(response); + observation.stop(); + } + + private static void addHighCardinalityKey(Observation observation, String key, String value) { + if (value != null) { + observation.highCardinalityKeyValue(key, value); + } + } +} diff --git a/google-http-client-micrometer/src/test/java/com/google/api/client/http/micrometer/GoogleClientObservationTests.java b/google-http-client-micrometer/src/test/java/com/google/api/client/http/micrometer/GoogleClientObservationTests.java new file mode 100644 index 000000000..61ceb8ca4 --- /dev/null +++ b/google-http-client-micrometer/src/test/java/com/google/api/client/http/micrometer/GoogleClientObservationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.micrometer; + +import com.google.api.client.http.*; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import io.micrometer.observation.transport.RequestReplySenderContext; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import org.assertj.core.api.Assertions; +import org.awaitility.Awaitility; + +import java.util.List; +import java.util.Objects; + +/** + * TODO: Write some docs + * + *

Implementations should normally be thread-safe. + * + * @since 1.43 + * @author Marcin Grzejszczak + */ +public class GoogleClientObservationTests extends SampleTestRunner { + + @Override + public SampleTestRunnerConsumer yourCode() throws Exception { + return (buildingBlocks, meterRegistry) -> { + MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse().setStatusCode(200); + HttpTransport transport = + new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); + HttpRequest request = transport.createRequestFactory() + .buildGetRequest(new GenericUrl("https://google.com/")); + request.addHttpInterceptor(new MicrometerObservationHttpInterceptor(getObservationRegistry())); + request.execute(); + + Awaitility.await().untilAsserted(() -> { + List finishedSpans = buildingBlocks.getFinishedSpans(); + + Assertions.assertThat(finishedSpans).isNotEmpty(); + }); + }; + } +} \ No newline at end of file diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpInterceptor.java b/google-http-client/src/main/java/com/google/api/client/http/HttpInterceptor.java new file mode 100644 index 000000000..800641828 --- /dev/null +++ b/google-http-client/src/main/java/com/google/api/client/http/HttpInterceptor.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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 java.io.IOException; +import java.util.Map; + +/** + * TODO: Write some docs + * + *

Implementations should normally be thread-safe. + * + * @since 1.43 + * @author Marcin Grzejszczak + */ +public interface HttpInterceptor { + + void beforeAllExecutions(Map context, HttpRequest httpRequest); + + void beforeSingleExecutionStart(Map context, HttpRequest httpRequest, int numRetries, int retriesRemaining); + + void beforeSingleExecutionRequestBuilding(Map context, HttpRequest httpRequest, String urlString); + + void beforeSingleExecutionHeadersSerialization(Map context, HttpRequest httpRequest, String originalUserAgent); + + void beforeSingleExecutionBytesSending(Map context, HttpRequest httpRequest, LowLevelHttpRequest lowLevelHttpRequest); + + void afterSingleExecutionResponseReceived(Map context, HttpRequest httpRequest, LowLevelHttpResponse httpResponse) throws IOException; + + void afterSingleExecutionExceptionHappened(Map context, Throwable throwable); + + void afterSingleExecutionOnFinally(Map context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse); + + void afterAllExecutions(Map context, HttpRequest httpRequest, HttpResponse httpResponse); +} 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 78f15d868..ca5f556ed 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 @@ -29,6 +29,10 @@ import io.opencensus.trace.Tracer; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.Executor; @@ -81,6 +85,11 @@ public final class HttpRequest { */ private HttpExecuteInterceptor executeInterceptor; + /** + * TODO: Write docs + */ + private List httpInterceptors = new ArrayList<>(); + /** HTTP request headers. */ private HttpHeaders headers = new HttpHeaders(); @@ -199,9 +208,6 @@ 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. @@ -217,6 +223,7 @@ public final class HttpRequest { HttpRequest(HttpTransport transport, String requestMethod) { this.transport = transport; setRequestMethod(requestMethod); + this.httpInterceptors.add(new OpenCensusHttpInterceptor()); } /** @@ -564,6 +571,30 @@ public HttpRequest setInterceptor(HttpExecuteInterceptor interceptor) { return this; } + /** + * TODO: Docs + * @return + */ + public List getHttpInterceptors() { + return httpInterceptors; + } + + /** + * TODO: Docs + * @param httpInterceptors + */ + public void setHttpInterceptors(List httpInterceptors) { + this.httpInterceptors = httpInterceptors; + } + + /** + * TODO: Docs + * @param httpInterceptor + */ + public void addHttpInterceptor(HttpInterceptor httpInterceptor) { + this.httpInterceptors.add(httpInterceptor); + } + /** * Returns the HTTP unsuccessful (non-2XX) response handler or {@code null} for none. * @@ -860,13 +891,15 @@ public HttpResponse execute() throws IOException { Preconditions.checkNotNull(requestMethod); Preconditions.checkNotNull(url); - Span span = - tracer - .spanBuilder(OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE) - .setRecordEvents(OpenCensusUtils.isRecordEvent()) - .startSpan(); + Map context = new HashMap<>(); + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.beforeAllExecutions(context, this); + } + do { - span.addAnnotation("retry #" + (numRetries - retriesRemaining)); + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.beforeSingleExecutionStart(context, this, numRetries, retriesRemaining); + } // Cleanup any unneeded response from a previous iteration if (response != null) { response.ignore(); @@ -881,10 +914,10 @@ public HttpResponse execute() throws IOException { } // 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); + + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.beforeSingleExecutionRequestBuilding(context, this, urlString); + } LowLevelHttpRequest lowLevelHttpRequest = transport.buildRequest(requestMethod, urlString); Logger logger = HttpTransport.LOGGER; @@ -911,17 +944,20 @@ public HttpResponse execute() throws IOException { } // add to user agent String originalUserAgent = headers.getUserAgent(); + // Before single execution - handle user agent + 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); + + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.beforeSingleExecutionHeadersSerialization(context, this, originalUserAgent); + } // headers HttpHeaders.serializeHeaders(headers, logbuf, curlbuf, logger, lowLevelHttpRequest); @@ -1004,18 +1040,19 @@ public HttpResponse execute() throws IOException { 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()); + + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.beforeSingleExecutionBytesSending(context, this, lowLevelHttpRequest); + } + + LowLevelHttpResponse lowLevelHttpResponse = null; try { - LowLevelHttpResponse lowLevelHttpResponse = lowLevelHttpRequest.execute(); - if (lowLevelHttpResponse != null) { - OpenCensusUtils.recordReceivedMessageEvent(span, lowLevelHttpResponse.getContentLength()); - span.putAttribute( - HttpTraceAttributeConstants.HTTP_STATUS_CODE, - AttributeValue.longAttributeValue(lowLevelHttpResponse.getStatusCode())); + lowLevelHttpResponse = lowLevelHttpRequest.execute(); + + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.afterSingleExecutionResponseReceived(context, this, lowLevelHttpResponse); } + // Flag used to indicate if an exception is thrown before the response is constructed. boolean responseConstructed = false; try { @@ -1033,8 +1070,9 @@ public HttpResponse execute() throws IOException { if (!retryOnExecuteIOException && (ioExceptionHandler == null || !ioExceptionHandler.handleIOException(this, retryRequest))) { - // static analysis shows response is always null here - span.end(OpenCensusUtils.getEndSpanOptions(null)); + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.afterSingleExecutionExceptionHappened(context, e); + } throw e; } // Save the exception in case the retries do not work and we need to re-throw it later. @@ -1043,7 +1081,9 @@ public HttpResponse execute() throws IOException { logger.log(Level.WARNING, "exception thrown while executing request", e); } } finally { - ws.close(); + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.afterSingleExecutionOnFinally(context, this, lowLevelHttpResponse); + } } // Flag used to indicate if an exception is thrown before the response has completed @@ -1100,7 +1140,10 @@ public HttpResponse execute() throws IOException { } } } while (retryRequest); - span.end(OpenCensusUtils.getEndSpanOptions(response == null ? null : response.getStatusCode())); + + for (HttpInterceptor httpInterceptor : httpInterceptors) { + httpInterceptor.afterAllExecutions(context, this, response); + } if (response == null) { // Retries did not help resolve the execute exception, re-throw it. @@ -1218,12 +1261,6 @@ public HttpRequest setSleeper(Sleeper sleeper) { 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() { // attempt to read the library's version from a properties file generated during the build // this value should be read and cached for later use diff --git a/google-http-client/src/main/java/com/google/api/client/http/OpenCensusHttpInterceptor.java b/google-http-client/src/main/java/com/google/api/client/http/OpenCensusHttpInterceptor.java new file mode 100644 index 000000000..3be2bff5d --- /dev/null +++ b/google-http-client/src/main/java/com/google/api/client/http/OpenCensusHttpInterceptor.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 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.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.util.Map; + +/** + * TODO: Write some docs + * + *

Implementations should normally be thread-safe. + * + * @since 1.43 + * @author Marcin Grzejszczak + */ +public class OpenCensusHttpInterceptor implements HttpInterceptor { + + /** OpenCensus tracing component. */ + private final Tracer tracer = OpenCensusUtils.getTracer(); + + @Override + public void beforeAllExecutions(Map context, HttpRequest httpRequest) { + Span span = + tracer + .spanBuilder(OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE) + .setRecordEvents(OpenCensusUtils.isRecordEvent()) + .startSpan(); + context.put(Span.class, span); + } + + @SuppressWarnings("unchecked") + private T getRequired(Object key, Map context) { + if (!context.containsKey(key)) { + throw new IllegalStateException("Object with key <" + key + "> was not found in <" + context + + ">"); + } + return (T) context.get(key); + } + + @Override + public void beforeSingleExecutionStart(Map context, HttpRequest httpRequest, int numRetries, int retriesRemaining) { + Span span = getSpan(context); + span.addAnnotation("retry #" + (numRetries - retriesRemaining)); + } + + @Override + public void beforeSingleExecutionRequestBuilding(Map context, HttpRequest httpRequest, String urlString) { + Span span = getSpan(context); + String requestMethod = httpRequest.getRequestMethod(); + GenericUrl url = httpRequest.getUrl(); + 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); + } + + private Span getSpan(Map context) { + return getRequired(Span.class, context); + } + + private Scope getScope(Map context) { + return getRequired(Scope.class, context); + } + + @Override + public void beforeSingleExecutionHeadersSerialization(Map context, HttpRequest httpRequest, String originalUserAgent) { + HttpHeaders headers = httpRequest.getHeaders(); + Span span = getSpan(context); + if (!httpRequest.getSuppressUserAgentSuffix()) { + if (originalUserAgent == null) { + addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, HttpRequest.USER_AGENT_SUFFIX); + } else { + addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_USER_AGENT, headers.getUserAgent()); + } + } + OpenCensusUtils.propagateTracingContext(span, headers); + } + + @Override + public void beforeSingleExecutionBytesSending(Map context, HttpRequest httpRequest, LowLevelHttpRequest lowLevelHttpRequest) { + // switch tracing scope to current span + Span span = getSpan(context); + @SuppressWarnings("MustBeClosedChecker") + Scope ws = tracer.withSpan(span); + OpenCensusUtils.recordSentMessageEvent(span, lowLevelHttpRequest.getContentLength()); + context.put(Scope.class, ws); + } + + @Override + public void afterSingleExecutionResponseReceived(Map context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse) throws IOException { + Span span = getSpan(context); + if (lowLevelHttpResponse != null) { + OpenCensusUtils.recordReceivedMessageEvent(span, lowLevelHttpResponse.getContentLength()); + span.putAttribute( + HttpTraceAttributeConstants.HTTP_STATUS_CODE, + AttributeValue.longAttributeValue(lowLevelHttpResponse.getStatusCode())); + } + } + + @Override + public void afterSingleExecutionExceptionHappened(Map context, Throwable throwable) { + Span span = getSpan(context); + // static analysis shows response is always null here + span.end(OpenCensusUtils.getEndSpanOptions(null)); + } + + @Override + public void afterSingleExecutionOnFinally(Map context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse) { + getScope(context).close(); + } + + @Override + public void afterAllExecutions(Map context, HttpRequest httpRequest, HttpResponse response) { + Span span = getSpan(context); + span.end(OpenCensusUtils.getEndSpanOptions(response == null ? null : response.getStatusCode())); + } + + private static void addSpanAttribute(Span span, String key, String value) { + if (value != null) { + span.putAttribute(key, AttributeValue.stringAttributeValue(value)); + } + } +} diff --git a/pom.xml b/pom.xml index 4069a98f4..ba8632c9a 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ google-http-client-gson google-http-client-jackson2 google-http-client-xml + google-http-client-micrometer google-http-client-findbugs google-http-client-test @@ -252,6 +253,16 @@ opencensus-testing ${project.opencensus.version} + + io.micrometer + micrometer-observation + ${project.micrometer.version} + + + io.micrometer + micrometer-tracing-integration-test + ${project.micrometer-tracing.version} + @@ -580,6 +591,8 @@ 4.5.14 4.4.16 0.31.1 + 1.11.4 + 1.1.5 .. 3.0.0-M7 false