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