Skip to content

Commit 335c267

Browse files
fix: add gccl-invocation-id interceptor (#1309)
add gccl-invocation-id to all HTTP requests to allow identifying operation attempts across multiple rpcs. Excludes signed urls. Co-authored-by: BenWhitehead <[email protected]>
1 parent 9d8c520 commit 335c267

File tree

7 files changed

+375
-2
lines changed

7 files changed

+375
-2
lines changed

google-cloud-storage/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
<groupId>com.fasterxml.jackson.core</groupId>
9393
<artifactId>jackson-core</artifactId>
9494
</dependency>
95+
<dependency>
96+
<groupId>com.google.code.findbugs</groupId>
97+
<artifactId>jsr305</artifactId>
98+
</dependency>
9599

96100
<!-- Test dependencies -->
97101
<dependency>

google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.gax.retrying.ResultRetryAlgorithm;
2323
import com.google.api.gax.retrying.RetrySettings;
2424
import com.google.cloud.RetryHelper.RetryHelperException;
25+
import com.google.cloud.storage.spi.v1.HttpRpcContext;
2526
import java.util.concurrent.Callable;
2627
import java.util.function.Function;
2728

@@ -47,11 +48,15 @@ final class Retrying {
4748
*/
4849
static <T, U> U run(
4950
StorageOptions options, ResultRetryAlgorithm<?> algorithm, Callable<T> c, Function<T, U> f) {
51+
HttpRpcContext httpRpcContext = HttpRpcContext.getInstance();
5052
try {
53+
httpRpcContext.newInvocationId();
5154
T result = runWithRetries(c, options.getRetrySettings(), algorithm, options.getClock());
5255
return result == null ? null : f.apply(result);
5356
} catch (RetryHelperException e) {
5457
throw StorageException.coalesce(e);
58+
} finally {
59+
httpRpcContext.clearInvocationId();
5560
}
5661
}
5762
}

google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ public class StorageOptions extends ServiceOptions<Storage, StorageOptions> {
3838
private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control";
3939
private static final Set<String> SCOPES = ImmutableSet.of(GCS_SCOPE);
4040
private static final String DEFAULT_HOST = "https://storage.googleapis.com";
41-
41+
private static final boolean DEFAULT_INCLUDE_INVOCATION_ID = true;
4242
private final RetryAlgorithmManager retryAlgorithmManager;
43+
private final boolean includeInvocationId;
4344

4445
public static class DefaultStorageFactory implements StorageFactory {
4546

@@ -64,11 +65,13 @@ public ServiceRpc create(StorageOptions options) {
6465
public static class Builder extends ServiceOptions.Builder<Storage, StorageOptions, Builder> {
6566

6667
private StorageRetryStrategy storageRetryStrategy;
68+
private boolean includeInvocationId;
6769

6870
private Builder() {}
6971

7072
private Builder(StorageOptions options) {
7173
super(options);
74+
this.includeInvocationId = options.includeInvocationId;
7275
}
7376

7477
@Override
@@ -93,6 +96,17 @@ public Builder setStorageRetryStrategy(StorageRetryStrategy storageRetryStrategy
9396
return this;
9497
}
9598

99+
/**
100+
* Override default enablement of invocation id added to x-goog-api-client header.
101+
*
102+
* @param includeInvocationId a boolean to change enablement of invocation id
103+
* @return the builder
104+
*/
105+
public Builder setIncludeInvocationId(boolean includeInvocationId) {
106+
this.includeInvocationId = includeInvocationId;
107+
return this;
108+
}
109+
96110
@Override
97111
public StorageOptions build() {
98112
return new StorageOptions(this, new StorageDefaults());
@@ -105,6 +119,7 @@ private StorageOptions(Builder builder, StorageDefaults serviceDefaults) {
105119
new RetryAlgorithmManager(
106120
MoreObjects.firstNonNull(
107121
builder.storageRetryStrategy, serviceDefaults.getStorageRetryStrategy()));
122+
this.includeInvocationId = builder.includeInvocationId;
108123
}
109124

110125
private static class StorageDefaults implements ServiceDefaults<Storage, StorageOptions> {
@@ -127,6 +142,10 @@ public TransportOptions getDefaultTransportOptions() {
127142
public StorageRetryStrategy getStorageRetryStrategy() {
128143
return StorageRetryStrategy.getDefaultStorageRetryStrategy();
129144
}
145+
146+
public boolean isIncludeInvocationId() {
147+
return DEFAULT_INCLUDE_INVOCATION_ID;
148+
}
130149
}
131150

132151
public static HttpTransportOptions getDefaultHttpTransportOptions() {
@@ -153,6 +172,11 @@ RetryAlgorithmManager getRetryAlgorithmManager() {
153172
return retryAlgorithmManager;
154173
}
155174

175+
/** Returns if Invocation ID is enabled and transmitted through x-goog-api-client header. */
176+
public boolean isIncludeInvocationId() {
177+
return includeInvocationId;
178+
}
179+
156180
/** Returns a default {@code StorageOptions} instance. */
157181
public static StorageOptions getDefaultInstance() {
158182
return newBuilder().build();
@@ -180,6 +204,8 @@ public boolean equals(Object obj) {
180204
}
181205

182206
public static Builder newBuilder() {
183-
return new Builder().setHost(DEFAULT_HOST);
207+
return new Builder()
208+
.setHost(DEFAULT_HOST)
209+
.setIncludeInvocationId(DEFAULT_INCLUDE_INVOCATION_ID);
184210
}
185211
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage.spi.v1;
18+
19+
import com.google.api.core.InternalApi;
20+
import java.util.UUID;
21+
import java.util.function.Supplier;
22+
import javax.annotation.Nullable;
23+
24+
@InternalApi
25+
public final class HttpRpcContext {
26+
27+
private static final Object GET_INSTANCE_LOCK = new Object();
28+
29+
private static volatile HttpRpcContext instance;
30+
31+
private final ThreadLocal<UUID> invocationId;
32+
private final Supplier<UUID> supplier;
33+
34+
HttpRpcContext(Supplier<UUID> randomUUID) {
35+
this.invocationId = new InheritableThreadLocal<>();
36+
this.supplier = randomUUID;
37+
}
38+
39+
@InternalApi
40+
@Nullable
41+
public UUID getInvocationId() {
42+
return invocationId.get();
43+
}
44+
45+
@InternalApi
46+
public UUID newInvocationId() {
47+
invocationId.set(supplier.get());
48+
return getInvocationId();
49+
}
50+
51+
@InternalApi
52+
public void clearInvocationId() {
53+
invocationId.remove();
54+
}
55+
56+
@InternalApi
57+
public static HttpRpcContext init() {
58+
return new HttpRpcContext(UUID::randomUUID);
59+
}
60+
61+
@InternalApi
62+
public static HttpRpcContext getInstance() {
63+
if (instance == null) {
64+
synchronized (GET_INSTANCE_LOCK) {
65+
if (instance == null) {
66+
instance = init();
67+
}
68+
}
69+
}
70+
return instance;
71+
}
72+
}

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.base.MoreObjects.firstNonNull;
2020
import static com.google.common.base.Preconditions.checkArgument;
21+
import static com.google.common.base.Preconditions.checkNotNull;
2122
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
2223

2324
import com.google.api.client.googleapis.batch.BatchRequest;
@@ -26,6 +27,7 @@
2627
import com.google.api.client.http.ByteArrayContent;
2728
import com.google.api.client.http.EmptyContent;
2829
import com.google.api.client.http.GenericUrl;
30+
import com.google.api.client.http.HttpExecuteInterceptor;
2931
import com.google.api.client.http.HttpHeaders;
3032
import com.google.api.client.http.HttpRequest;
3133
import com.google.api.client.http.HttpRequestFactory;
@@ -86,6 +88,8 @@
8688
import java.util.List;
8789
import java.util.Locale;
8890
import java.util.Map;
91+
import java.util.UUID;
92+
import javax.annotation.Nullable;
8993

9094
public class HttpStorageRpc implements StorageRpc {
9195
public static final String DEFAULT_PROJECTION = "full";
@@ -114,6 +118,9 @@ public HttpStorageRpc(StorageOptions options) {
114118
// Open Census initialization
115119
censusHttpModule = new CensusHttpModule(tracer, true);
116120
initializer = censusHttpModule.getHttpRequestInitializer(initializer);
121+
if (options.isIncludeInvocationId()) {
122+
initializer = new InvocationIdInitializer(initializer);
123+
}
117124
batchRequestInitializer = censusHttpModule.getHttpRequestInitializer(null);
118125
storage =
119126
new Storage.Builder(transport, new JacksonFactory(), initializer)
@@ -122,6 +129,54 @@ public HttpStorageRpc(StorageOptions options) {
122129
.build();
123130
}
124131

132+
private static final class InvocationIdInitializer implements HttpRequestInitializer {
133+
@Nullable HttpRequestInitializer initializer;
134+
135+
private InvocationIdInitializer(@Nullable HttpRequestInitializer initializer) {
136+
this.initializer = initializer;
137+
}
138+
139+
@Override
140+
public void initialize(HttpRequest request) throws IOException {
141+
checkNotNull(request);
142+
if (this.initializer != null) {
143+
this.initializer.initialize(request);
144+
}
145+
request.setInterceptor(new InvocationIdInterceptor(request.getInterceptor()));
146+
}
147+
}
148+
149+
private static final class InvocationIdInterceptor implements HttpExecuteInterceptor {
150+
@Nullable HttpExecuteInterceptor interceptor;
151+
152+
private InvocationIdInterceptor(@Nullable HttpExecuteInterceptor interceptor) {
153+
this.interceptor = interceptor;
154+
}
155+
156+
@Override
157+
public void intercept(HttpRequest request) throws IOException {
158+
checkNotNull(request);
159+
if (this.interceptor != null) {
160+
this.interceptor.intercept(request);
161+
}
162+
UUID invocationId = HttpRpcContext.getInstance().getInvocationId();
163+
final String signatureKey = "Signature="; // For V2 and V4 signedURLs
164+
final String builtURL = request.getUrl().build();
165+
if (invocationId != null && !builtURL.contains(signatureKey)) {
166+
HttpHeaders headers = request.getHeaders();
167+
String existing = (String) headers.get("x-goog-api-client");
168+
String invocationEntry = "gccl-invocation-id/" + invocationId;
169+
final String newValue;
170+
if (existing != null && !existing.isEmpty()) {
171+
newValue = existing + " " + invocationEntry;
172+
} else {
173+
newValue = invocationEntry;
174+
}
175+
headers.set("x-goog-api-client", newValue);
176+
}
177+
}
178+
}
179+
125180
private class DefaultRpcBatch implements RpcBatch {
126181

127182
// Batch size is limited as, due to some current service implementation details, the service

google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.google.cloud.storage;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertTrue;
2022

2123
import com.google.cloud.TransportOptions;
2224
import org.easymock.EasyMock;
@@ -65,4 +67,18 @@ public void testDefaultInstanceSpecifiesCorrectHost() {
6567

6668
assertThat(opts1.getHost()).isEqualTo("https://storage.googleapis.com");
6769
}
70+
71+
@Test
72+
public void testDefaultInvocationId() {
73+
StorageOptions opts1 = StorageOptions.getDefaultInstance();
74+
75+
assertTrue(opts1.isIncludeInvocationId());
76+
}
77+
78+
@Test
79+
public void testDisableInvocationId() {
80+
StorageOptions opts1 = StorageOptions.newBuilder().setIncludeInvocationId(false).build();
81+
82+
assertFalse(opts1.isIncludeInvocationId());
83+
}
6884
}

0 commit comments

Comments
 (0)