Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Micrometer Observation support #1879

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
114 changes: 114 additions & 0 deletions google-http-client-micrometer/pom.xml
@@ -0,0 +1,114 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-parent</artifactId>
<version>1.43.4-SNAPSHOT</version><!-- {x-version-update:google-http-client-parent:current} -->
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>google-http-client-micrometer</artifactId>
<version>1.43.4-SNAPSHOT</version><!-- {x-version-update:google-http-client-xml:current} -->
<name>Micrometer Support</name>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<links>
<link>http://download.oracle.com/javase/8/docs/api/</link>
</links>
<doctitle>${project.name} ${project.version}</doctitle>
<windowtitle>${project.artifactId} ${project.version}</windowtitle>
<doclint>none</doclint>
<source>8</source>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>source-jar</id>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-test-sources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<argLine>-Xmx1024m</argLine>
<reportNameSuffix>sponge_log</reportNameSuffix>
</configuration>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.google.api.client.http.micrometer</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-integration-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -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
*
* <p>Implementations should normally be thread-safe.
*
* @since 1.43
* @author Marcin Grzejszczak
*/
public class GoogleClientContext extends RequestReplySenderContext<HttpRequest, HttpResponse> {

public GoogleClientContext(HttpRequest httpRequest) {
super((req, key, value) -> Objects.requireNonNull(req).getHeaders().put(key, value));
setCarrier(httpRequest);
}
}
@@ -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
*
* <p>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<Object, Object> 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> T getRequired(Object key, Map<Object, Object> 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<Object, Object> context, HttpRequest httpRequest, int numRetries, int retriesRemaining) {
Observation observation = getObservation(context);
observation.highCardinalityKeyValue("retry #", String.valueOf(numRetries - retriesRemaining));
}

@Override
public void beforeSingleExecutionRequestBuilding(Map<Object, Object> 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<Object, Object> context) {
return getRequired(Observation.class, context);
}

private Observation.Scope getScope(Map<Object, Object> context) {
return getRequired(Observation.Scope.class, context);
}

@Override
public void beforeSingleExecutionHeadersSerialization(Map<Object, Object> 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<Object, Object> 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<Object, Object> 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<Object, Object> context, Throwable throwable) {
Observation observation = getObservation(context);
observation.error(throwable);
}

@Override
public void afterSingleExecutionOnFinally(Map<Object, Object> context, HttpRequest httpRequest, LowLevelHttpResponse lowLevelHttpResponse) {
getScope(context).close();
}

@Override
public void afterAllExecutions(Map<Object, Object> 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);
}
}
}
@@ -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
*
* <p>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<FinishedSpan> finishedSpans = buildingBlocks.getFinishedSpans();

Assertions.assertThat(finishedSpans).isNotEmpty();
});
};
}
}