diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 09a3af01c..b849dfd8d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -19,6 +19,7 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; +import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; @@ -110,10 +111,9 @@ public abstract CompleteMultipartUploadResponse completeMultipartUpload( @BetaApi public static MultipartUploadClient create(MultipartUploadSettings config) { HttpStorageOptions options = config.getOptions(); - return new MultipartUploadClientImpl( - URI.create(options.getHost()), - options.createRetrier(), - MultipartUploadHttpRequestManager.createFrom(options), - options.getRetryAlgorithmManager()); + MultipartUploadClient client = new MultipartUploadClientImpl( + URI.create(options.getHost()), options.createRetrier(), + MultipartUploadHttpRequestManager.createFrom(options), options.getRetryAlgorithmManager()); + return OtelMultipartUploadClientDecorator.decorate(client, options.getOpenTelemetry(), Transport.HTTP); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelMultipartUploadClientDecorator.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelMultipartUploadClientDecorator.java new file mode 100644 index 000000000..5dc033eaf --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelMultipartUploadClientDecorator.java @@ -0,0 +1,161 @@ +/* + * Copyright 2025 Google LLC + * + * 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.cloud.storage; + +import com.google.api.core.BetaApi; +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ListPartsRequest; +import com.google.cloud.storage.multipartupload.model.ListPartsResponse; +import com.google.cloud.storage.multipartupload.model.UploadPartRequest; +import com.google.cloud.storage.multipartupload.model.UploadPartResponse; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.util.Locale; + +@BetaApi +final class OtelMultipartUploadClientDecorator extends MultipartUploadClient { + + private final MultipartUploadClient delegate; + private final Tracer tracer; + + private OtelMultipartUploadClientDecorator( + MultipartUploadClient delegate, OpenTelemetry otel, Attributes baseAttributes) { + this.delegate = delegate; + this.tracer = + OtelStorageDecorator.TracerDecorator.decorate( + null, otel, baseAttributes, MultipartUploadClient.class.getName() + "/"); + } + + @Override + public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) { + Span span = + tracer + .spanBuilder("createMultipartUpload") + .setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key())) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.createMultipartUpload(request); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + + @Override + public ListPartsResponse listParts(ListPartsRequest request) { + Span span = + tracer + .spanBuilder("listParts") + .setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key())) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.listParts(request); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + + @Override + public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request) { + Span span = + tracer + .spanBuilder("abortMultipartUpload") + .setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key())) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.abortMultipartUpload(request); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + + @Override + public CompleteMultipartUploadResponse completeMultipartUpload( + CompleteMultipartUploadRequest request) { + Span span = + tracer + .spanBuilder("completeMultipartUpload") + .setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key())) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.completeMultipartUpload(request); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + + @Override + public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody) { + Span span = + tracer + .spanBuilder("uploadPart") + .setAttribute("gsutil.uri", String.format("gs://%s/%s", request.bucket(), request.key())) + .setAttribute("partNumber", request.partNumber()) + .startSpan(); + try (Scope ignore = span.makeCurrent()) { + return delegate.uploadPart(request, requestBody); + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName()); + throw t; + } finally { + span.end(); + } + } + + static MultipartUploadClient decorate( + MultipartUploadClient delegate, OpenTelemetry otel, Transport transport) { + if (otel == OpenTelemetry.noop()) { + return delegate; + } + Attributes baseAttributes = + Attributes.builder() + .put("gcp.client.service", "Storage") + .put("gcp.client.version", StorageOptions.getDefaultInstance().getLibraryVersion()) + .put("gcp.client.repo", "googleapis/java-storage") + .put("gcp.client.artifact", "com.google.cloud:google-cloud-storage") + .put("rpc.system", transport.toString().toLowerCase(Locale.ROOT)) + .put("service.name", "storage.googleapis.com") + .build(); + return new OtelMultipartUploadClientDecorator(delegate, otel, baseAttributes); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java index e418e5e10..291db00ae 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java @@ -1561,13 +1561,13 @@ static UnaryOperator retryContextDecorator(OpenTelemetry otel) { return String.format(Locale.US, "gs://%s/", bucket); } - private static final class TracerDecorator implements Tracer { + static final class TracerDecorator implements Tracer { @Nullable private final Context parentContextOverride; private final Tracer delegate; private final Attributes baseAttributes; private final String spanNamePrefix; - private TracerDecorator( + TracerDecorator( @Nullable Context parentContextOverride, Tracer delegate, Attributes baseAttributes, @@ -1578,7 +1578,7 @@ private TracerDecorator( this.spanNamePrefix = spanNamePrefix; } - private static TracerDecorator decorate( + static TracerDecorator decorate( @Nullable Context parentContextOverride, OpenTelemetry otel, Attributes baseAttributes, diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITOpenTelemetryMPUTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITOpenTelemetryMPUTest.java new file mode 100644 index 000000000..a3a33bef9 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITOpenTelemetryMPUTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 Google LLC + * + * 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.cloud.storage; + +import static com.google.cloud.storage.TestUtils.assertAll; +import static com.google.common.truth.Truth.assertThat; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import com.google.cloud.storage.TransportCompatibility.Transport; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.CrossRun; +import com.google.cloud.storage.it.runner.annotations.Inject; +import com.google.cloud.storage.it.runner.registry.Generator; +import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CompletedMultipartUpload; +import com.google.cloud.storage.multipartupload.model.CompletedPart; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.UploadPartRequest; +import com.google.cloud.storage.multipartupload.model.UploadPartResponse; +import com.google.cloud.storage.otel.TestExporter; +import com.google.common.collect.ImmutableList; +import java.util.List; + + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@CrossRun( + backends = Backend.PROD, + transports = {Transport.HTTP}) +public final class ITOpenTelemetryMPUTest { + + @Inject public Storage storage; + + @Inject public BucketInfo bucket; + + @Inject public Generator generator; + @Inject public Transport transport; + + @Test + public void checkMPUInstrumentation() throws Exception { + TestExporter exporter = new TestExporter(); + + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build()) + .build(); + + HttpStorageOptions httpStorageOptions = (HttpStorageOptions) storage.getOptions(); + StorageOptions storageOptions = + httpStorageOptions.toBuilder().setOpenTelemetry(openTelemetrySdk).build(); + + String objectName = generator.randomObjectName(); + + try (Storage storage = storageOptions.getService()) { + MultipartUploadClient mpuClient = + MultipartUploadClient.create( + MultipartUploadSettings.of((HttpStorageOptions) storage.getOptions())); + + CreateMultipartUploadResponse create = + mpuClient.createMultipartUpload( + CreateMultipartUploadRequest.builder() + .bucket(bucket.getName()) + .key(objectName) + .build()); + + byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); + RequestBody body = RequestBody.of(ByteBuffer.wrap(data)); + UploadPartResponse upload = + mpuClient.uploadPart( + UploadPartRequest.builder() + .bucket(bucket.getName()) + .key(objectName) + .uploadId(create.uploadId()) + .partNumber(1) + .build(), + body); + + mpuClient.completeMultipartUpload( + CompleteMultipartUploadRequest.builder() + .bucket(bucket.getName()) + .key(objectName) + .uploadId(create.uploadId()) + .multipartUpload( + CompletedMultipartUpload.builder() + .parts( + ImmutableList.of( + CompletedPart.builder().partNumber(1).eTag(upload.eTag()).build())) + .build()) + .build()); + } + + List spans = exporter.getExportedSpans(); + assertThat(spans).hasSize(3); + + SpanData createSpan = spans.get(0); + assertThat(createSpan.getName()) + .isEqualTo("com.google.cloud.storage.MultipartUploadClient/createMultipartUpload"); + assertThat(createSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri"))) + .isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName)); + + SpanData uploadSpan = spans.get(1); + assertThat(uploadSpan.getName()) + .isEqualTo("com.google.cloud.storage.MultipartUploadClient/uploadPart"); + assertThat(uploadSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri"))) + .isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName)); + assertThat(uploadSpan.getAttributes().get(AttributeKey.longKey("partNumber"))).isEqualTo(1); + + SpanData completeSpan = spans.get(2); + assertThat(completeSpan.getName()) + .isEqualTo("com.google.cloud.storage.MultipartUploadClient/completeMultipartUpload"); + assertThat(completeSpan.getAttributes().get(AttributeKey.stringKey("gsutil.uri"))) + .isEqualTo(String.format("gs://%s/%s", bucket.getName(), objectName)); + } +} \ No newline at end of file