Skip to content

Commit 40c19b1

Browse files
ldetmerblakeli0
andauthored
feat(gax): add protobuf version tracking to headers (#3199)
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 <[email protected]>
1 parent 259e9f7 commit 40c19b1

File tree

7 files changed

+168
-24
lines changed

7 files changed

+168
-24
lines changed

gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/GaxHttpJsonProperties.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
@InternalApi
3737
public class GaxHttpJsonProperties {
3838
private static final Pattern DEFAULT_API_CLIENT_HEADER_PATTERN =
39-
Pattern.compile("gl-java/.+ gapic/.* gax/.+ rest/.*");
39+
Pattern.compile("gl-java/.+ gapic/.*?--protobuf-.+ gax/.+ rest/.*");
4040

4141
/** Returns default api client header pattern (to facilitate testing) */
4242
public static Pattern getDefaultApiClientHeaderPattern() {

gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/GaxHttpJsonPropertiesTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class GaxHttpJsonPropertiesTest {
4141
void testDefaultHeaderPattern() {
4242
assertTrue(
4343
GaxHttpJsonProperties.getDefaultApiClientHeaderPattern()
44-
.matcher("gl-java/1.8_00 gapic/1.2.3-alpha gax/1.5.0 rest/1.7.0")
44+
.matcher("gl-java/1.8_00 gapic/1.2.3-alpha--protobuf-1.5.0 gax/1.5.0 rest/1.7.0")
4545
.matches());
4646
}
4747
}

gax-java/gax/src/main/java/com/google/api/gax/core/GaxProperties.java

+36
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@
3232
import com.google.api.core.InternalApi;
3333
import com.google.common.annotations.VisibleForTesting;
3434
import com.google.common.base.Strings;
35+
import com.google.protobuf.Any;
36+
import java.io.File;
3537
import java.io.IOException;
3638
import java.io.InputStream;
39+
import java.net.URISyntaxException;
40+
import java.util.Optional;
3741
import java.util.Properties;
42+
import java.util.jar.Attributes;
43+
import java.util.jar.JarFile;
3844

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

4755
private GaxProperties() {}
4856

@@ -91,6 +99,11 @@ public static String getGaxVersion() {
9199
return GAX_VERSION;
92100
}
93101

102+
/** Returns the current version of protobuf runtime library. */
103+
public static String getProtobufVersion() {
104+
return PROTOBUF_VERSION;
105+
}
106+
94107
/**
95108
* Returns the current runtime version. For GraalVM the values in this method will be fetched at
96109
* build time and the values should not differ from the runtime (executable)
@@ -113,4 +126,27 @@ static String getRuntimeVersion() {
113126
// with hyphens.
114127
return javaRuntimeInformation.replaceAll("[^0-9a-zA-Z_\\\\.]", "-");
115128
}
129+
130+
/**
131+
* Returns the current library version as reported by Bundle-Version attribute in library's
132+
* META-INF/MANIFEST for libraries using OSGi bundle manifest specification
133+
* https://www.ibm.com/docs/en/wasdtfe?topic=overview-osgi-bundles. This should only be used if
134+
* MANIFEST file does not contain a widely recognized version declaration such as Specific-Version
135+
* OR Implementation-Version declared in Manifest Specification
136+
* https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Manifest_Specification,
137+
* otherwise please use #getLibraryVersion
138+
*/
139+
@VisibleForTesting
140+
static Optional<String> getBundleVersion(Class<?> clazz) {
141+
try {
142+
File file = new File(clazz.getProtectionDomain().getCodeSource().getLocation().toURI());
143+
try (JarFile jar = new JarFile(file.getPath())) {
144+
Attributes attributes = jar.getManifest().getMainAttributes();
145+
return Optional.ofNullable(attributes.getValue("Bundle-Version"));
146+
}
147+
} catch (URISyntaxException | IOException e) {
148+
// Unable to read Bundle-Version from manifest. Recover gracefully.
149+
return Optional.empty();
150+
}
151+
}
116152
}

gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java

+27-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import com.google.common.collect.ImmutableMap;
3434
import java.io.Serializable;
3535
import java.util.Map;
36+
import java.util.regex.Matcher;
37+
import java.util.regex.Pattern;
3638

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

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

@@ -57,8 +60,12 @@ protected ApiClientHeaderProvider(Builder builder) {
5760
appendToken(apiClientHeaderValue, builder.getGeneratedLibToken());
5861
appendToken(apiClientHeaderValue, builder.getGeneratedRuntimeToken());
5962
appendToken(apiClientHeaderValue, builder.getTransportToken());
63+
appendToken(apiClientHeaderValue, builder.protobufRuntimeToken);
64+
6065
if (apiClientHeaderValue.length() > 0) {
61-
headersBuilder.put(builder.getApiClientHeaderKey(), apiClientHeaderValue.toString());
66+
headersBuilder.put(
67+
builder.getApiClientHeaderKey(),
68+
checkAndAppendProtobufVersionIfNecessary(apiClientHeaderValue));
6269
}
6370
}
6471

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

86+
private static String checkAndAppendProtobufVersionIfNecessary(
87+
StringBuilder apiClientHeaderValue) {
88+
// TODO(b/366417603): appending protobuf version to existing client library token until resolved
89+
Pattern pattern = Pattern.compile("(gccl|gapic)\\S*");
90+
Matcher matcher = pattern.matcher(apiClientHeaderValue);
91+
if (matcher.find()) {
92+
return apiClientHeaderValue.substring(0, matcher.end())
93+
+ "--"
94+
+ PROTOBUF_HEADER_VERSION_KEY
95+
+ "-"
96+
+ GaxProperties.getProtobufVersion()
97+
+ apiClientHeaderValue.substring(matcher.end());
98+
}
99+
return apiClientHeaderValue.toString();
100+
}
101+
79102
@Override
80103
public Map<String, String> getHeaders() {
81104
return headers;
@@ -110,6 +133,7 @@ public static class Builder {
110133
private String generatedRuntimeToken;
111134
private String transportToken;
112135
private String quotaProjectIdToken;
136+
private final String protobufRuntimeToken;
113137

114138
private String resourceHeaderKey;
115139
private String resourceToken;
@@ -125,11 +149,11 @@ protected Builder() {
125149
setClientRuntimeToken(GaxProperties.getGaxVersion());
126150
transportToken = null;
127151
quotaProjectIdToken = null;
128-
129152
resourceHeaderKey = getDefaultResourceHeaderKey();
130153
resourceToken = null;
131-
132154
apiVersionToken = null;
155+
protobufRuntimeToken =
156+
constructToken(PROTOBUF_HEADER_VERSION_KEY, GaxProperties.getProtobufVersion());
133157
}
134158

135159
public String getApiClientHeaderKey() {

gax-java/gax/src/test/java/com/google/api/gax/core/GaxPropertiesTest.java

+45-10
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@
2929
*/
3030
package com.google.api.gax.core;
3131

32+
import static com.google.api.gax.core.GaxProperties.getBundleVersion;
3233
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.assertFalse;
3335
import static org.junit.jupiter.api.Assertions.assertTrue;
3436

3537
import com.google.common.base.Strings;
38+
import java.io.IOException;
39+
import java.util.Optional;
3640
import java.util.regex.Pattern;
3741
import org.junit.jupiter.api.AfterEach;
3842
import org.junit.jupiter.api.Test;
@@ -41,17 +45,11 @@ class GaxPropertiesTest {
4145

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

52-
assertTrue(major >= 1);
53-
if (major == 1) {
54-
assertTrue(minor >= 56);
50+
assertTrue(version.major >= 1);
51+
if (version.major == 1) {
52+
assertTrue(version.minor >= 56);
5553
}
5654
}
5755

@@ -159,4 +157,41 @@ void testGetJavaRuntimeInfo_nullJavaVersion() {
159157
String runtimeInfo = GaxProperties.getRuntimeVersion();
160158
assertEquals("null__oracle__20.0.1", runtimeInfo);
161159
}
160+
161+
@Test
162+
public void testGetProtobufVersion() throws IOException {
163+
Version version = readVersion(GaxProperties.getProtobufVersion());
164+
165+
assertTrue(version.major >= 3);
166+
if (version.major == 3) {
167+
assertTrue(version.minor >= 25);
168+
}
169+
}
170+
171+
@Test
172+
public void testGetBundleVersion_noManifestFile() throws IOException {
173+
Optional<String> version = getBundleVersion(GaxProperties.class);
174+
175+
assertFalse(version.isPresent());
176+
}
177+
178+
private Version readVersion(String version) {
179+
assertTrue(Pattern.compile("^\\d+\\.\\d+\\.\\d+").matcher(version).find());
180+
String[] versionComponents = version.split("\\.");
181+
// This test was added in version 1.56.0, so check that the major and minor numbers are greater
182+
// than that.
183+
int major = Integer.parseInt(versionComponents[0]);
184+
int minor = Integer.parseInt(versionComponents[1]);
185+
return new Version(major, minor);
186+
}
187+
188+
private static class Version {
189+
public int major;
190+
public int minor;
191+
192+
public Version(int major, int minor) {
193+
this.major = major;
194+
this.minor = minor;
195+
}
196+
}
162197
}

gax-java/gax/src/test/java/com/google/api/gax/rpc/ApiClientHeaderProviderTest.java

+28-8
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class ApiClientHeaderProviderTest {
4242
void testServiceHeaderDefault() {
4343
ApiClientHeaderProvider provider = ApiClientHeaderProvider.newBuilder().build();
4444
assertThat(provider.getHeaders().size()).isEqualTo(1);
45-
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT)).matches("^gl-java/.* gax/.*$");
45+
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
46+
.matches("^gl-java/.* gax/.* protobuf/.*");
4647
}
4748

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

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

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

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

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

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

@@ -131,7 +133,7 @@ void testQuotaProjectHeader() {
131133
.build();
132134
assertThat(provider.getHeaders().size()).isEqualTo(2);
133135
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
134-
.matches("^gl-java/.* gccl/1\\.2\\.3 gax/.*$");
136+
.matches("^gl-java/.* gccl/1\\.2\\.3--protobuf-.* gax/.* protobuf/.*");
135137
assertThat(provider.getHeaders().get(ApiClientHeaderProvider.QUOTA_PROJECT_ID_HEADER_KEY))
136138
.matches(quotaProjectHeaderValue);
137139
}
@@ -149,4 +151,22 @@ void testApiVersionHeader() {
149151
assertThat(
150152
emptyProvider.getHeaders().get(ApiClientHeaderProvider.API_VERSION_HEADER_KEY).isEmpty());
151153
}
154+
155+
@Test
156+
void testNonGapicGeneratedLibToken_doesNotAppendProtobufVersion() {
157+
ApiClientHeaderProvider provider =
158+
ApiClientHeaderProvider.newBuilder().setGeneratedLibToken("other-token", "1.2.3").build();
159+
160+
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
161+
.matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*");
162+
}
163+
164+
@Test
165+
void testNonGcclGeneratedLibToken_doesNotAppendProtobufVersion() {
166+
ApiClientHeaderProvider provider =
167+
ApiClientHeaderProvider.newBuilder().setClientLibToken("other-token", "1.2.3").build();
168+
169+
assertThat(provider.getHeaders().get(X_GOOG_API_CLIENT))
170+
.matches("^gl-java/.* other-token/1.2.3 gax/.* protobuf/.*");
171+
}
152172
}

showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITVersionHeaders.java

+30-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static com.google.common.truth.Truth.assertThat;
1919
import static org.junit.jupiter.api.Assertions.assertThrows;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
2021

2122
import com.google.api.gax.httpjson.*;
2223
import com.google.api.gax.rpc.ApiClientHeaderProvider;
@@ -33,6 +34,7 @@
3334
import java.io.IOException;
3435
import java.util.ArrayList;
3536
import java.util.concurrent.TimeUnit;
37+
import java.util.regex.Pattern;
3638
import org.junit.jupiter.api.AfterAll;
3739
import org.junit.jupiter.api.BeforeAll;
3840
import org.junit.jupiter.api.Test;
@@ -41,13 +43,19 @@
4143
// https://github.com/googleapis/gapic-showcase/pull/1456
4244
// TODO: watch for showcase gRPC trailer changes suggested in
4345
// https://github.com/googleapis/gapic-showcase/pull/1509#issuecomment-2089147103
44-
class ITApiVersionHeaders {
46+
class ITVersionHeaders {
4547
private static final String HTTP_RESPONSE_HEADER_STRING =
4648
"x-showcase-request-" + ApiClientHeaderProvider.API_VERSION_HEADER_KEY;
49+
private static final String HTTP_CLIENT_API_HEADER_KEY =
50+
"x-showcase-request-" + ApiClientHeaderProvider.getDefaultApiClientHeaderKey();
4751
private static final Metadata.Key<String> API_VERSION_HEADER_KEY =
4852
Metadata.Key.of(
4953
ApiClientHeaderProvider.API_VERSION_HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER);
5054

55+
private static final Metadata.Key<String> API_CLIENT_HEADER_KEY =
56+
Metadata.Key.of(
57+
ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), Metadata.ASCII_STRING_MARSHALLER);
58+
5159
private static final String EXPECTED_ECHO_API_VERSION = "v1_20240408";
5260
private static final String CUSTOM_API_VERSION = "user-supplied-version";
5361
private static final String EXPECTED_EXCEPTION_MESSAGE =
@@ -229,4 +237,25 @@ void testHttpJsonCompliance_userApiVersionSetSuccess() throws IOException {
229237
assertThat(headerValue).isEqualTo(CUSTOM_API_VERSION);
230238
}
231239
}
240+
241+
@Test
242+
void testGrpcCall_sendsCorrectApiClientHeader() {
243+
Pattern defautlGrpcHeaderPattern =
244+
Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* grpc/.* protobuf/.*");
245+
grpcClient.echo(EchoRequest.newBuilder().build());
246+
String headerValue = grpcInterceptor.metadata.get(API_CLIENT_HEADER_KEY);
247+
assertTrue(defautlGrpcHeaderPattern.matcher(headerValue).matches());
248+
}
249+
250+
@Test
251+
void testHttpJson_sendsCorrectApiClientHeader() {
252+
Pattern defautlHttpHeaderPattern =
253+
Pattern.compile("gl-java/.* gapic/.*?--protobuf-.* gax/.* rest/ protobuf/.*");
254+
httpJsonClient.echo(EchoRequest.newBuilder().build());
255+
ArrayList<String> headerValues =
256+
(ArrayList<String>)
257+
httpJsonInterceptor.metadata.getHeaders().get(HTTP_CLIENT_API_HEADER_KEY);
258+
String headerValue = headerValues.get(0);
259+
assertTrue(defautlHttpHeaderPattern.matcher(headerValue).matches());
260+
}
232261
}

0 commit comments

Comments
 (0)