Skip to content

Commit

Permalink
feat(gax): add protobuf version tracking to headers (#3199)
Browse files Browse the repository at this point in the history
Update the Java client libraries to report the runtime version of
Protobuf as part of the existing x-goog-api-client request header.

Tested: java-cloud-library api (billing) and hand written api (storage)

---------

Co-authored-by: Blake Li <blakeli@google.com>
  • Loading branch information
ldetmer and blakeli0 authored Oct 15, 2024
1 parent 259e9f7 commit 40c19b1
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
@InternalApi
public class GaxHttpJsonProperties {
private static final Pattern DEFAULT_API_CLIENT_HEADER_PATTERN =
Pattern.compile("gl-java/.+ gapic/.* gax/.+ rest/.*");
Pattern.compile("gl-java/.+ gapic/.*?--protobuf-.+ gax/.+ rest/.*");

/** Returns default api client header pattern (to facilitate testing) */
public static Pattern getDefaultApiClientHeaderPattern() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class GaxHttpJsonPropertiesTest {
void testDefaultHeaderPattern() {
assertTrue(
GaxHttpJsonProperties.getDefaultApiClientHeaderPattern()
.matcher("gl-java/1.8_00 gapic/1.2.3-alpha gax/1.5.0 rest/1.7.0")
.matcher("gl-java/1.8_00 gapic/1.2.3-alpha--protobuf-1.5.0 gax/1.5.0 rest/1.7.0")
.matches());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@
import com.google.api.core.InternalApi;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.protobuf.Any;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.Optional;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;

/** Provides properties of the GAX library. */
@InternalApi
Expand All @@ -43,6 +49,8 @@ public class GaxProperties {
private static final String DEFAULT_VERSION = "";
private static final String GAX_VERSION = getLibraryVersion(GaxProperties.class, "version.gax");
private static final String JAVA_VERSION = getRuntimeVersion();
private static final String PROTOBUF_VERSION =
getBundleVersion(Any.class).orElse(DEFAULT_VERSION);

private GaxProperties() {}

Expand Down Expand Up @@ -91,6 +99,11 @@ public static String getGaxVersion() {
return GAX_VERSION;
}

/** Returns the current version of protobuf runtime library. */
public static String getProtobufVersion() {
return PROTOBUF_VERSION;
}

/**
* Returns the current runtime version. For GraalVM the values in this method will be fetched at
* build time and the values should not differ from the runtime (executable)
Expand All @@ -113,4 +126,27 @@ static String getRuntimeVersion() {
// with hyphens.
return javaRuntimeInformation.replaceAll("[^0-9a-zA-Z_\\\\.]", "-");
}

/**
* Returns the current library version as reported by Bundle-Version attribute in library's
* META-INF/MANIFEST for libraries using OSGi bundle manifest specification
* https://www.ibm.com/docs/en/wasdtfe?topic=overview-osgi-bundles. This should only be used if
* MANIFEST file does not contain a widely recognized version declaration such as Specific-Version
* OR Implementation-Version declared in Manifest Specification
* https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Manifest_Specification,
* otherwise please use #getLibraryVersion
*/
@VisibleForTesting
static Optional<String> getBundleVersion(Class<?> clazz) {
try {
File file = new File(clazz.getProtectionDomain().getCodeSource().getLocation().toURI());
try (JarFile jar = new JarFile(file.getPath())) {
Attributes attributes = jar.getManifest().getMainAttributes();
return Optional.ofNullable(attributes.getValue("Bundle-Version"));
}
} catch (URISyntaxException | IOException e) {
// Unable to read Bundle-Version from manifest. Recover gracefully.
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Implementation of HeaderProvider that provides headers describing the API client library making
Expand All @@ -41,6 +43,7 @@
public class ApiClientHeaderProvider implements HeaderProvider, Serializable {
private static final long serialVersionUID = -8876627296793342119L;
static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
static final String PROTOBUF_HEADER_VERSION_KEY = "protobuf";

public static final String API_VERSION_HEADER_KEY = "x-goog-api-version";

Expand All @@ -57,8 +60,12 @@ protected ApiClientHeaderProvider(Builder builder) {
appendToken(apiClientHeaderValue, builder.getGeneratedLibToken());
appendToken(apiClientHeaderValue, builder.getGeneratedRuntimeToken());
appendToken(apiClientHeaderValue, builder.getTransportToken());
appendToken(apiClientHeaderValue, builder.protobufRuntimeToken);

if (apiClientHeaderValue.length() > 0) {
headersBuilder.put(builder.getApiClientHeaderKey(), apiClientHeaderValue.toString());
headersBuilder.put(
builder.getApiClientHeaderKey(),
checkAndAppendProtobufVersionIfNecessary(apiClientHeaderValue));
}
}

Expand All @@ -76,6 +83,22 @@ protected ApiClientHeaderProvider(Builder builder) {
this.headers = headersBuilder.build();
}

private static String checkAndAppendProtobufVersionIfNecessary(
StringBuilder apiClientHeaderValue) {
// TODO(b/366417603): appending protobuf version to existing client library token until resolved
Pattern pattern = Pattern.compile("(gccl|gapic)\\S*");
Matcher matcher = pattern.matcher(apiClientHeaderValue);
if (matcher.find()) {
return apiClientHeaderValue.substring(0, matcher.end())
+ "--"
+ PROTOBUF_HEADER_VERSION_KEY
+ "-"
+ GaxProperties.getProtobufVersion()
+ apiClientHeaderValue.substring(matcher.end());
}
return apiClientHeaderValue.toString();
}

@Override
public Map<String, String> getHeaders() {
return headers;
Expand Down Expand Up @@ -110,6 +133,7 @@ public static class Builder {
private String generatedRuntimeToken;
private String transportToken;
private String quotaProjectIdToken;
private final String protobufRuntimeToken;

private String resourceHeaderKey;
private String resourceToken;
Expand All @@ -125,11 +149,11 @@ protected Builder() {
setClientRuntimeToken(GaxProperties.getGaxVersion());
transportToken = null;
quotaProjectIdToken = null;

resourceHeaderKey = getDefaultResourceHeaderKey();
resourceToken = null;

apiVersionToken = null;
protobufRuntimeToken =
constructToken(PROTOBUF_HEADER_VERSION_KEY, GaxProperties.getProtobufVersion());
}

public String getApiClientHeaderKey() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@
*/
package com.google.api.gax.core;

import static com.google.api.gax.core.GaxProperties.getBundleVersion;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.common.base.Strings;
import java.io.IOException;
import java.util.Optional;
import java.util.regex.Pattern;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
Expand All @@ -41,17 +45,11 @@ class GaxPropertiesTest {

@Test
void testGaxVersion() {
String gaxVersion = GaxProperties.getGaxVersion();
assertTrue(Pattern.compile("^\\d+\\.\\d+\\.\\d+").matcher(gaxVersion).find());
String[] versionComponents = gaxVersion.split("\\.");
// This test was added in version 1.56.0, so check that the major and minor numbers are greater
// than that.
int major = Integer.parseInt(versionComponents[0]);
int minor = Integer.parseInt(versionComponents[1]);
Version version = readVersion(GaxProperties.getGaxVersion());

assertTrue(major >= 1);
if (major == 1) {
assertTrue(minor >= 56);
assertTrue(version.major >= 1);
if (version.major == 1) {
assertTrue(version.minor >= 56);
}
}

Expand Down Expand Up @@ -159,4 +157,41 @@ void testGetJavaRuntimeInfo_nullJavaVersion() {
String runtimeInfo = GaxProperties.getRuntimeVersion();
assertEquals("null__oracle__20.0.1", runtimeInfo);
}

@Test
public void testGetProtobufVersion() throws IOException {
Version version = readVersion(GaxProperties.getProtobufVersion());

assertTrue(version.major >= 3);
if (version.major == 3) {
assertTrue(version.minor >= 25);
}
}

@Test
public void testGetBundleVersion_noManifestFile() throws IOException {
Optional<String> version = getBundleVersion(GaxProperties.class);

assertFalse(version.isPresent());
}

private Version readVersion(String version) {
assertTrue(Pattern.compile("^\\d+\\.\\d+\\.\\d+").matcher(version).find());
String[] versionComponents = version.split("\\.");
// This test was added in version 1.56.0, so check that the major and minor numbers are greater
// than that.
int major = Integer.parseInt(versionComponents[0]);
int minor = Integer.parseInt(versionComponents[1]);
return new Version(major, minor);
}

private static class Version {
public int major;
public int minor;

public Version(int major, int minor) {
this.major = major;
this.minor = minor;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class ApiClientHeaderProviderTest {
void testServiceHeaderDefault() {
ApiClientHeaderProvider provider = ApiClientHeaderProvider.newBuilder().build();
assertThat(provider.getHeaders().size()).isEqualTo(1);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)).matches("^gl-java/.* gax/.*$");
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gax/.* protobuf/.*");
}

@Test
Expand All @@ -51,7 +52,7 @@ void testServiceHeaderManual() {
ApiClientHeaderProvider.newBuilder().setClientLibToken("gccl", "1.2.3").build();
assertThat(provider.getHeaders().size()).isEqualTo(1);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gccl/1\\.2\\.3 gax/.*$");
.matches("^gl-java/.* gccl/1\\.2\\.3--protobuf-.* gax/.* protobuf/.*");
}

@Test
Expand All @@ -64,7 +65,8 @@ void testServiceHeaderManualGapic() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(1);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gccl/4\\.5\\.6 gapic/7\\.8\\.9 gax/.* grpc/1\\.2\\.3$");
.matches(
"^gl-java/.* gccl/4\\.5\\.6--protobuf-.* gapic/7\\.8\\.9 gax/.* grpc/1\\.2\\.3 protobuf/.*");
}

@Test
Expand All @@ -76,7 +78,7 @@ void testServiceHeaderManualGrpc() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(1);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gccl/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$");
.matches("^gl-java/.* gccl/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*");
}

@Test
Expand All @@ -88,7 +90,7 @@ void testServiceHeaderGapic() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(1);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$");
.matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*");
}

@Test
Expand All @@ -101,7 +103,7 @@ void testCloudResourcePrefixHeader() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(2);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$");
.matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*");
assertThat(provider.getHeaders().get(CLOUD_RESOURCE_PREFIX)).isEqualTo("test-prefix");
}

Expand All @@ -117,7 +119,7 @@ void testCustomHeaderKeys() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(2);
assertThat(provider.getHeaders().get("custom-header1"))
.matches("^gl-java/.* gapic/4\\.5\\.6 gax/.* grpc/1\\.2\\.3$");
.matches("^gl-java/.* gapic/4\\.5\\.6--protobuf-.* gax/.* grpc/1\\.2\\.3 protobuf/.*");
assertThat(provider.getHeaders().get("custom-header2")).isEqualTo("test-prefix");
}

Expand All @@ -131,7 +133,7 @@ void testQuotaProjectHeader() {
.build();
assertThat(provider.getHeaders().size()).isEqualTo(2);
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* gccl/1\\.2\\.3 gax/.*$");
.matches("^gl-java/.* gccl/1\\.2\\.3--protobuf-.* gax/.* protobuf/.*");
assertThat(provider.getHeaders().get(ApiClientHeaderProvider.QUOTA_PROJECT_ID_HEADER_KEY))
.matches(quotaProjectHeaderValue);
}
Expand All @@ -149,4 +151,22 @@ void testApiVersionHeader() {
assertThat(
emptyProvider.getHeaders().get(ApiClientHeaderProvider.API_VERSION_HEADER_KEY).isEmpty());
}

@Test
void testNonGapicGeneratedLibToken_doesNotAppendProtobufVersion() {
ApiClientHeaderProvider provider =
ApiClientHeaderProvider.newBuilder().setGeneratedLibToken("other-token", "1.2.3").build();

assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*");
}

@Test
void testNonGcclGeneratedLibToken_doesNotAppendProtobufVersion() {
ApiClientHeaderProvider provider =
ApiClientHeaderProvider.newBuilder().setClientLibToken("other-token", "1.2.3").build();

assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
.matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.api.gax.httpjson.*;
import com.google.api.gax.rpc.ApiClientHeaderProvider;
Expand All @@ -33,6 +34,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand All @@ -41,13 +43,19 @@
// https://github.com/googleapis/gapic-showcase/pull/1456
// TODO: watch for showcase gRPC trailer changes suggested in
// https://github.com/googleapis/gapic-showcase/pull/1509#issuecomment-2089147103
class ITApiVersionHeaders {
class ITVersionHeaders {
private static final String HTTP_RESPONSE_HEADER_STRING =
"x-showcase-request-" + ApiClientHeaderProvider.API_VERSION_HEADER_KEY;
private static final String HTTP_CLIENT_API_HEADER_KEY =
"x-showcase-request-" + ApiClientHeaderProvider.getDefaultApiClientHeaderKey();
private static final Metadata.Key<String> API_VERSION_HEADER_KEY =
Metadata.Key.of(
ApiClientHeaderProvider.API_VERSION_HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER);

private static final Metadata.Key<String> API_CLIENT_HEADER_KEY =
Metadata.Key.of(
ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), Metadata.ASCII_STRING_MARSHALLER);

private static final String EXPECTED_ECHO_API_VERSION = "v1_20240408";
private static final String CUSTOM_API_VERSION = "user-supplied-version";
private static final String EXPECTED_EXCEPTION_MESSAGE =
Expand Down Expand Up @@ -229,4 +237,25 @@ void testHttpJsonCompliance_userApiVersionSetSuccess() throws IOException {
assertThat(headerValue).isEqualTo(CUSTOM_API_VERSION);
}
}

@Test
void testGrpcCall_sendsCorrectApiClientHeader() {
Pattern defautlGrpcHeaderPattern =
Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* grpc/.* protobuf/.*");
grpcClient.echo(EchoRequest.newBuilder().build());
String headerValue = grpcInterceptor.metadata.get(API_CLIENT_HEADER_KEY);
assertTrue(defautlGrpcHeaderPattern.matcher(headerValue).matches());
}

@Test
void testHttpJson_sendsCorrectApiClientHeader() {
Pattern defautlHttpHeaderPattern =
Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* rest/ protobuf/.*");
httpJsonClient.echo(EchoRequest.newBuilder().build());
ArrayList<String> headerValues =
(ArrayList<String>)
httpJsonInterceptor.metadata.getHeaders().get(HTTP_CLIENT_API_HEADER_KEY);
String headerValue = headerValues.get(0);
assertTrue(defautlHttpHeaderPattern.matcher(headerValue).matches());
}
}

0 comments on commit 40c19b1

Please sign in to comment.