Skip to content

Commit df66376

Browse files
author
Dhriti Chopra
committed
Adding initial otel for MPU
1 parent feb1912 commit df66376

File tree

4 files changed

+312
-8
lines changed

4 files changed

+312
-8
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.api.core.BetaApi;
2020
import com.google.api.core.InternalExtensionOnly;
2121
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest;
22+
import com.google.cloud.storage.TransportCompatibility.Transport;
2223
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse;
2324
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest;
2425
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
@@ -110,10 +111,9 @@ public abstract CompleteMultipartUploadResponse completeMultipartUpload(
110111
@BetaApi
111112
public static MultipartUploadClient create(MultipartUploadSettings config) {
112113
HttpStorageOptions options = config.getOptions();
113-
return new MultipartUploadClientImpl(
114-
URI.create(options.getHost()),
115-
options.createRetrier(),
116-
MultipartUploadHttpRequestManager.createFrom(options),
117-
options.getRetryAlgorithmManager());
114+
MultipartUploadClient client = new MultipartUploadClientImpl(
115+
URI.create(options.getHost()), options.createRetrier(),
116+
MultipartUploadHttpRequestManager.createFrom(options), options.getRetryAlgorithmManager());
117+
return OtelMultipartUploadClientDecorator.decorate(client, options.getOpenTelemetry(), Transport.HTTP);
118118
}
119119
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2025 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;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.cloud.storage.TransportCompatibility.Transport;
21+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest;
22+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse;
23+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest;
24+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
25+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
26+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
27+
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
28+
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
29+
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
30+
import com.google.cloud.storage.multipartupload.model.UploadPartResponse;
31+
import io.opentelemetry.api.OpenTelemetry;
32+
import io.opentelemetry.api.common.Attributes;
33+
import io.opentelemetry.api.trace.Span;
34+
import io.opentelemetry.api.trace.StatusCode;
35+
import io.opentelemetry.api.trace.Tracer;
36+
import io.opentelemetry.context.Scope;
37+
import java.util.Locale;
38+
39+
@BetaApi
40+
final class OtelMultipartUploadClientDecorator extends MultipartUploadClient {
41+
42+
private final MultipartUploadClient delegate;
43+
private final Tracer tracer;
44+
45+
private OtelMultipartUploadClientDecorator(
46+
MultipartUploadClient delegate, OpenTelemetry otel, Attributes baseAttributes) {
47+
this.delegate = delegate;
48+
this.tracer =
49+
OtelStorageDecorator.TracerDecorator.decorate(
50+
null, otel, baseAttributes, MultipartUploadClient.class.getName() + "/");
51+
}
52+
53+
@Override
54+
public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) {
55+
Span span =
56+
tracer
57+
.spanBuilder("createMultipartUpload")
58+
.setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key()))
59+
.startSpan();
60+
try (Scope ignore = span.makeCurrent()) {
61+
return delegate.createMultipartUpload(request);
62+
} catch (Throwable t) {
63+
span.recordException(t);
64+
span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName());
65+
throw t;
66+
} finally {
67+
span.end();
68+
}
69+
}
70+
71+
@Override
72+
public ListPartsResponse listParts(ListPartsRequest request) {
73+
Span span =
74+
tracer
75+
.spanBuilder("listParts")
76+
.setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key()))
77+
.startSpan();
78+
try (Scope ignore = span.makeCurrent()) {
79+
return delegate.listParts(request);
80+
} catch (Throwable t) {
81+
span.recordException(t);
82+
span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName());
83+
throw t;
84+
} finally {
85+
span.end();
86+
}
87+
}
88+
89+
@Override
90+
public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request) {
91+
Span span =
92+
tracer
93+
.spanBuilder("abortMultipartUpload")
94+
.setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key()))
95+
.startSpan();
96+
try (Scope ignore = span.makeCurrent()) {
97+
return delegate.abortMultipartUpload(request);
98+
} catch (Throwable t) {
99+
span.recordException(t);
100+
span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName());
101+
throw t;
102+
} finally {
103+
span.end();
104+
}
105+
}
106+
107+
@Override
108+
public CompleteMultipartUploadResponse completeMultipartUpload(
109+
CompleteMultipartUploadRequest request) {
110+
Span span =
111+
tracer
112+
.spanBuilder("completeMultipartUpload")
113+
.setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key()))
114+
.startSpan();
115+
try (Scope ignore = span.makeCurrent()) {
116+
return delegate.completeMultipartUpload(request);
117+
} catch (Throwable t) {
118+
span.recordException(t);
119+
span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName());
120+
throw t;
121+
} finally {
122+
span.end();
123+
}
124+
}
125+
126+
@Override
127+
public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody) {
128+
Span span =
129+
tracer
130+
.spanBuilder("uploadPart")
131+
.setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key()))
132+
.setAttribute("partNumber", request.partNumber())
133+
.startSpan();
134+
try (Scope ignore = span.makeCurrent()) {
135+
return delegate.uploadPart(request, requestBody);
136+
} catch (Throwable t) {
137+
span.recordException(t);
138+
span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName());
139+
throw t;
140+
} finally {
141+
span.end();
142+
}
143+
}
144+
145+
static MultipartUploadClient decorate(
146+
MultipartUploadClient delegate, OpenTelemetry otel, Transport transport) {
147+
if (otel == OpenTelemetry.noop()) {
148+
return delegate;
149+
}
150+
Attributes baseAttributes =
151+
Attributes.builder()
152+
.put("gcp.client.service", "Storage")
153+
.put("gcp.client.version", StorageOptions.getDefaultInstance().getLibraryVersion())
154+
.put("gcp.client.repo", "googleapis/java-storage")
155+
.put("gcp.client.artifact", "com.google.cloud:google-cloud-storage")
156+
.put("rpc.system", transport.toString().toLowerCase(Locale.ROOT))
157+
.put("service.name", "storage.googleapis.com")
158+
.build();
159+
return new OtelMultipartUploadClientDecorator(delegate, otel, baseAttributes);
160+
}
161+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,13 +1561,13 @@ static UnaryOperator<RetryContext> retryContextDecorator(OpenTelemetry otel) {
15611561
return String.format(Locale.US, "gs://%s/", bucket);
15621562
}
15631563

1564-
private static final class TracerDecorator implements Tracer {
1564+
static final class TracerDecorator implements Tracer {
15651565
@Nullable private final Context parentContextOverride;
15661566
private final Tracer delegate;
15671567
private final Attributes baseAttributes;
15681568
private final String spanNamePrefix;
15691569

1570-
private TracerDecorator(
1570+
TracerDecorator(
15711571
@Nullable Context parentContextOverride,
15721572
Tracer delegate,
15731573
Attributes baseAttributes,
@@ -1578,7 +1578,7 @@ private TracerDecorator(
15781578
this.spanNamePrefix = spanNamePrefix;
15791579
}
15801580

1581-
private static TracerDecorator decorate(
1581+
static TracerDecorator decorate(
15821582
@Nullable Context parentContextOverride,
15831583
OpenTelemetry otel,
15841584
Attributes baseAttributes,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2024 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;
18+
19+
import static com.google.cloud.storage.TestUtils.assertAll;
20+
import static com.google.common.truth.Truth.assertThat;
21+
22+
import java.nio.ByteBuffer;
23+
import java.nio.charset.StandardCharsets;
24+
25+
import com.google.cloud.storage.TransportCompatibility.Transport;
26+
import com.google.cloud.storage.it.runner.StorageITRunner;
27+
import com.google.cloud.storage.it.runner.annotations.Backend;
28+
import com.google.cloud.storage.it.runner.annotations.CrossRun;
29+
import com.google.cloud.storage.it.runner.annotations.Inject;
30+
import com.google.cloud.storage.it.runner.registry.Generator;
31+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest;
32+
import com.google.cloud.storage.multipartupload.model.CompletedMultipartUpload;
33+
import com.google.cloud.storage.multipartupload.model.CompletedPart;
34+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
35+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
36+
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
37+
import com.google.cloud.storage.multipartupload.model.UploadPartResponse;
38+
import com.google.cloud.storage.otel.TestExporter;
39+
import com.google.common.collect.ImmutableList;
40+
import java.util.List;
41+
42+
43+
import io.opentelemetry.api.OpenTelemetry;
44+
import io.opentelemetry.api.common.AttributeKey;
45+
import io.opentelemetry.sdk.OpenTelemetrySdk;
46+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
47+
import io.opentelemetry.sdk.trace.data.SpanData;
48+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
49+
import org.junit.Test;
50+
import org.junit.runner.RunWith;
51+
52+
@RunWith(StorageITRunner.class)
53+
@CrossRun(
54+
backends = Backend.PROD,
55+
transports = {Transport.HTTP})
56+
public final class ITOpenTelemetryMPUTest {
57+
58+
@Inject public Storage storage;
59+
60+
@Inject public BucketInfo bucket;
61+
62+
@Inject public Generator generator;
63+
@Inject public Transport transport;
64+
65+
@Test
66+
public void checkMPUInstrumentation() throws Exception {
67+
TestExporter exporter = new TestExporter();
68+
69+
OpenTelemetrySdk openTelemetrySdk =
70+
OpenTelemetrySdk.builder()
71+
.setTracerProvider(
72+
SdkTracerProvider.builder()
73+
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
74+
.build())
75+
.build();
76+
77+
HttpStorageOptions httpStorageOptions = (HttpStorageOptions) storage.getOptions();
78+
StorageOptions storageOptions =
79+
httpStorageOptions.toBuilder().setOpenTelemetry(openTelemetrySdk).build();
80+
81+
String objectName = generator.randomObjectName();
82+
83+
try (Storage storage = storageOptions.getService()) {
84+
MultipartUploadClient mpuClient =
85+
MultipartUploadClient.create(
86+
MultipartUploadSettings.of((HttpStorageOptions) storage.getOptions()));
87+
88+
CreateMultipartUploadResponse create =
89+
mpuClient.createMultipartUpload(
90+
CreateMultipartUploadRequest.builder()
91+
.bucket(bucket.getName())
92+
.key(objectName)
93+
.build());
94+
95+
byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8);
96+
RequestBody body = RequestBody.of(ByteBuffer.wrap(data));
97+
UploadPartResponse upload =
98+
mpuClient.uploadPart(
99+
UploadPartRequest.builder()
100+
.bucket(bucket.getName())
101+
.key(objectName)
102+
.uploadId(create.uploadId())
103+
.partNumber(1)
104+
.build(),
105+
body);
106+
107+
mpuClient.completeMultipartUpload(
108+
CompleteMultipartUploadRequest.builder()
109+
.bucket(bucket.getName())
110+
.key(objectName)
111+
.uploadId(create.uploadId())
112+
.multipartUpload(
113+
CompletedMultipartUpload.builder()
114+
.parts(
115+
ImmutableList.of(
116+
CompletedPart.builder().partNumber(1).eTag(upload.eTag()).build()))
117+
.build())
118+
.build());
119+
}
120+
121+
List<SpanData> spans = exporter.getExportedSpans();
122+
assertThat(spans).hasSize(3);
123+
124+
SpanData createSpan = spans.get(0);
125+
assertThat(createSpan.getName())
126+
.isEqualTo("com.google.cloud.storage.MultipartUploadClient/createMultipartUpload");
127+
assertThat(createSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri")))
128+
.isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName));
129+
130+
SpanData uploadSpan = spans.get(1);
131+
assertThat(uploadSpan.getName())
132+
.isEqualTo("com.google.cloud.storage.MultipartUploadClient/uploadPart");
133+
assertThat(uploadSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri")))
134+
.isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName));
135+
assertThat(uploadSpan.getAttributes().get(AttributeKey.longKey("partNumber"))).isEqualTo(1);
136+
137+
SpanData completeSpan = spans.get(2);
138+
assertThat(completeSpan.getName())
139+
.isEqualTo("com.google.cloud.storage.MultipartUploadClient/completeMultipartUpload");
140+
assertThat(completeSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri")))
141+
.isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName));
142+
}
143+
}

0 commit comments

Comments
 (0)