Skip to content

Commit 5e6eb84

Browse files
authored
feat: Add OpenTelemetry Traces to GRPC (#2783)
* feat: Add OpenTelemetry to GRPC * adding rpc.system value * refactor test for both transports * lint * add branch protection rules to otel feature branch * pr comment
1 parent f6dd4ab commit 5e6eb84

File tree

5 files changed

+119
-26
lines changed

5 files changed

+119
-26
lines changed

.github/sync-repo-settings.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,22 @@ branchProtectionRules:
162162
- 'Kokoro - Test: Java GraalVM Native Image'
163163
- 'Kokoro - Test: Java 17 GraalVM Native Image'
164164
- javadoc
165+
- pattern: otel-v1-branch
166+
isAdminEnforced: true
167+
requiredApprovingReviewCount: 1
168+
requiresCodeOwnerReviews: true
169+
requiresStrictStatusChecks: false
170+
requiredStatusCheckContexts:
171+
- dependencies (17)
172+
- lint
173+
- clirr
174+
- units (8)
175+
- units (11)
176+
- 'Kokoro - Test: Integration'
177+
- cla/google
178+
- 'Kokoro - Test: Java GraalVM Native Image'
179+
- 'Kokoro - Test: Java 17 GraalVM Native Image'
180+
- javadoc
165181
permissionRules:
166182
- team: yoshi-admins
167183
permission: admin

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import com.google.cloud.storage.UnifiedOpts.Opts;
6969
import com.google.cloud.storage.UnifiedOpts.ProjectId;
7070
import com.google.cloud.storage.UnifiedOpts.UserProject;
71+
import com.google.cloud.storage.otel.OpenTelemetryTraceUtil;
7172
import com.google.common.annotations.VisibleForTesting;
7273
import com.google.common.collect.ImmutableList;
7374
import com.google.common.collect.ImmutableSet;
@@ -168,6 +169,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
168169
// workaround for https://github.com/googleapis/java-storage/issues/1736
169170
private final Opts<UserProject> defaultOpts;
170171
@Deprecated private final ProjectId defaultProjectId;
172+
private final OpenTelemetryTraceUtil openTelemetryTraceUtil;
171173

172174
GrpcStorageImpl(
173175
GrpcStorageOptions options,
@@ -184,6 +186,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
184186
this.retryAlgorithmManager = options.getRetryAlgorithmManager();
185187
this.syntaxDecoders = new SyntaxDecoders();
186188
this.defaultProjectId = UnifiedOpts.projectId(options.getProjectId());
189+
this.openTelemetryTraceUtil = OpenTelemetryTraceUtil.getInstance(options);
187190
}
188191

189192
@Override
@@ -198,6 +201,8 @@ public void close() throws Exception {
198201

199202
@Override
200203
public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
204+
OpenTelemetryTraceUtil.Span otelSpan =
205+
openTelemetryTraceUtil.startSpan("create(Bucket, BucketTargetOption");
201206
Opts<BucketTargetOpt> opts = Opts.unwrap(options).resolveFrom(bucketInfo).prepend(defaultOpts);
202207
GrpcCallContext grpcCallContext =
203208
opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
@@ -212,11 +217,20 @@ public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
212217
.setParent("projects/_");
213218
CreateBucketRequest req = opts.createBucketsRequest().apply(builder).build();
214219
GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
215-
return Retrying.run(
216-
getOptions(),
217-
retryAlgorithmManager.getFor(req),
218-
() -> storageClient.createBucketCallable().call(req, merge),
219-
syntaxDecoders.bucket);
220+
try (OpenTelemetryTraceUtil.Scope unused = otelSpan.makeCurrent()) {
221+
return Retrying.run(
222+
getOptions(),
223+
retryAlgorithmManager.getFor(req),
224+
() -> storageClient.createBucketCallable().call(req, merge),
225+
syntaxDecoders.bucket);
226+
} catch (Exception ex) {
227+
otelSpan.recordException(ex);
228+
otelSpan.setStatus(
229+
io.opentelemetry.api.trace.StatusCode.ERROR, ex.getClass().getSimpleName());
230+
throw StorageException.coalesce(ex);
231+
} finally {
232+
otelSpan.end();
233+
}
220234
}
221235

222236
@Override

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public final class GrpcStorageOptions extends StorageOptions
119119
private final boolean grpcClientMetricsManuallyEnabled;
120120
private final GrpcInterceptorProvider grpcInterceptorProvider;
121121
private final BlobWriteSessionConfig blobWriteSessionConfig;
122+
private OpenTelemetrySdk openTelemetrySdk;
122123

123124
private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) {
124125
super(builder, serviceDefaults);
@@ -134,6 +135,7 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults)
134135
this.grpcClientMetricsManuallyEnabled = builder.grpcMetricsManuallyEnabled;
135136
this.grpcInterceptorProvider = builder.grpcInterceptorProvider;
136137
this.blobWriteSessionConfig = builder.blobWriteSessionConfig;
138+
this.openTelemetrySdk = builder.openTelemetrySdk;
137139
}
138140

139141
@Override
@@ -350,7 +352,7 @@ private Tuple<StorageSettings, Opts<UserProject>> resolveSettingsAndOpts() throw
350352

351353
@Override
352354
public OpenTelemetrySdk getOpenTelemetrySdk() {
353-
return null;
355+
return openTelemetrySdk;
354356
}
355357

356358
/** @since 2.14.0 This new api is in preview and is subject to breaking changes. */
@@ -435,6 +437,8 @@ public static final class Builder extends StorageOptions.Builder {
435437

436438
private boolean grpcMetricsManuallyEnabled = false;
437439

440+
private OpenTelemetrySdk openTelemetrySdk;
441+
438442
Builder() {}
439443

440444
Builder(StorageOptions options) {
@@ -446,6 +450,7 @@ public static final class Builder extends StorageOptions.Builder {
446450
this.enableGrpcClientMetrics = gso.enableGrpcClientMetrics;
447451
this.grpcInterceptorProvider = gso.grpcInterceptorProvider;
448452
this.blobWriteSessionConfig = gso.blobWriteSessionConfig;
453+
this.openTelemetrySdk = gso.openTelemetrySdk;
449454
}
450455

451456
/**
@@ -634,9 +639,15 @@ public GrpcStorageOptions.Builder setBlobWriteSessionConfig(
634639
return this;
635640
}
636641

637-
@Override
638-
public StorageOptions.Builder setOpenTelemetrySdk(@NonNull OpenTelemetrySdk openTelemetrySdk) {
639-
return null;
642+
/**
643+
* Enable OpenTelemetry Tracing and provide an instance for the client to use.
644+
*
645+
* @param openTelemetrySdk User defined instance of OpenTelemetry SDK to be used by the library
646+
*/
647+
public GrpcStorageOptions.Builder setOpenTelemetrySdk(OpenTelemetrySdk openTelemetrySdk) {
648+
requireNonNull(openTelemetrySdk, "openTelemetry must be non null");
649+
this.openTelemetrySdk = openTelemetrySdk;
650+
return this;
640651
}
641652

642653
/** @since 2.14.0 This new api is in preview and is subject to breaking changes. */

google-cloud-storage/src/main/java/com/google/cloud/storage/otel/OpenTelemetryInstance.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import com.google.api.core.ApiFuture;
2020
import com.google.api.gax.core.GaxProperties;
21+
import com.google.cloud.storage.GrpcStorageOptions;
2122
import com.google.cloud.storage.StorageOptions;
2223
import com.google.cloud.storage.otel.OpenTelemetryTraceUtil.Context;
2324
import com.google.cloud.storage.otel.OpenTelemetryTraceUtil.Span;
2425
import io.opentelemetry.api.OpenTelemetry;
26+
import io.opentelemetry.api.common.AttributeKey;
2527
import io.opentelemetry.api.common.Attributes;
2628
import io.opentelemetry.api.common.AttributesBuilder;
2729
import io.opentelemetry.api.trace.SpanBuilder;
@@ -38,11 +40,14 @@ class OpenTelemetryInstance implements OpenTelemetryTraceUtil {
3840

3941
private static final String LIBRARY_NAME = "cloud.google.com/java/storage";
4042

43+
private final String transport;
44+
4145
public OpenTelemetryInstance(StorageOptions storageOptions) {
4246
this.storageOptions = storageOptions;
4347
this.openTelemetry = storageOptions.getOpenTelemetrySdk();
4448
this.tracer =
4549
openTelemetry.getTracer(LIBRARY_NAME, GaxProperties.getLibraryVersion(this.getClass()));
50+
this.transport = storageOptions instanceof GrpcStorageOptions ? "grpc" : "http";
4651
}
4752

4853
static class Span implements OpenTelemetryTraceUtil.Span {
@@ -56,7 +61,12 @@ static class Span implements OpenTelemetryTraceUtil.Span {
5661

5762
@Override
5863
public OpenTelemetryTraceUtil.Span recordException(Throwable error) {
59-
span.recordException(error);
64+
span.recordException(
65+
error,
66+
Attributes.of(
67+
AttributeKey.stringKey("exception.message"), error.getMessage(),
68+
AttributeKey.stringKey("exception.type"), error.getClass().getName(),
69+
AttributeKey.stringKey("exception.stacktrace"), error.getStackTrace().toString()));
6070
return this;
6171
}
6272

@@ -146,6 +156,7 @@ public Scope makeCurrent() {
146156
public OpenTelemetryTraceUtil.Span startSpan(String methodName) {
147157
String formatSpanName = String.format("%s.%s/%s", "storage", "client", methodName);
148158
SpanBuilder spanBuilder = tracer.spanBuilder(formatSpanName).setSpanKind(SpanKind.CLIENT);
159+
spanBuilder.setAttribute("rpc.system", transport);
149160
io.opentelemetry.api.trace.Span span =
150161
addSettingsAttributesToCurrentSpan(spanBuilder).startSpan();
151162
return new Span(span, formatSpanName);

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

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@
1616

1717
package com.google.cloud.storage;
1818

19-
import com.google.cloud.storage.TransportCompatibility.Transport;
20-
import com.google.cloud.storage.it.runner.StorageITRunner;
21-
import com.google.cloud.storage.it.runner.annotations.Backend;
22-
import com.google.cloud.storage.it.runner.annotations.CrossRun;
23-
import com.google.cloud.storage.it.runner.annotations.Inject;
24-
import com.google.cloud.storage.it.runner.registry.Generator;
19+
import com.google.cloud.storage.testing.RemoteStorageHelper;
2520
import io.opentelemetry.api.common.AttributeKey;
2621
import io.opentelemetry.sdk.OpenTelemetrySdk;
2722
import io.opentelemetry.sdk.common.CompletableResultCode;
@@ -33,16 +28,11 @@
3328
import java.util.Collection;
3429
import java.util.Collections;
3530
import java.util.List;
31+
import java.util.UUID;
3632
import org.junit.Assert;
3733
import org.junit.Test;
38-
import org.junit.runner.RunWith;
3934

40-
@RunWith(StorageITRunner.class)
41-
@CrossRun(
42-
backends = {Backend.PROD},
43-
transports = {Transport.HTTP})
4435
public final class ITOpenTelemetryTest {
45-
@Inject public Generator generator;
4636

4737
@Test
4838
public void checkInstrumentation() {
@@ -58,29 +48,80 @@ public void checkInstrumentation() {
5848
StorageOptions storageOptions =
5949
StorageOptions.http().setOpenTelemetrySdk(openTelemetrySdk).build();
6050
Storage storage = storageOptions.getService();
61-
storage.create(BucketInfo.of(generator.randomBucketName()));
51+
String bucket = randomBucketName();
52+
storage.create(BucketInfo.of(bucket));
6253
TestExporter testExported = (TestExporter) exporter;
6354
SpanData spanData = testExported.getExportedSpans().get(0);
6455
Assert.assertEquals("Storage", getAttributeValue(spanData, "gcp.client.service"));
6556
Assert.assertEquals("googleapis/java-storage", getAttributeValue(spanData, "gcp.client.repo"));
6657
Assert.assertEquals(
6758
"com.google.cloud.google-cloud-storage",
6859
getAttributeValue(spanData, "gcp.client.artifact"));
60+
Assert.assertEquals("http", getAttributeValue(spanData, "rpc.system"));
61+
62+
// Cleanup
63+
RemoteStorageHelper.forceDelete(storage, bucket);
6964
}
7065

7166
@Test
72-
public void noOpDoesNothing() {
73-
StorageOptions storageOptions = StorageOptions.http().build();
67+
public void checkInstrumentationGrpc() {
68+
SpanExporter exporter = new TestExporter();
69+
70+
OpenTelemetrySdk openTelemetrySdk =
71+
OpenTelemetrySdk.builder()
72+
.setTracerProvider(
73+
SdkTracerProvider.builder()
74+
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
75+
.build())
76+
.build();
77+
StorageOptions storageOptions =
78+
StorageOptions.grpc().setOpenTelemetrySdk(openTelemetrySdk).build();
7479
Storage storage = storageOptions.getService();
75-
storage.create(BucketInfo.of(generator.randomBucketName()));
80+
String bucket = randomBucketName();
81+
storage.create(BucketInfo.of(bucket));
82+
TestExporter testExported = (TestExporter) exporter;
83+
SpanData spanData = testExported.getExportedSpans().get(0);
84+
Assert.assertEquals("Storage", getAttributeValue(spanData, "gcp.client.service"));
85+
Assert.assertEquals("googleapis/java-storage", getAttributeValue(spanData, "gcp.client.repo"));
86+
Assert.assertEquals(
87+
"com.google.cloud.google-cloud-storage",
88+
getAttributeValue(spanData, "gcp.client.artifact"));
89+
Assert.assertEquals("grpc", getAttributeValue(spanData, "rpc.system"));
90+
91+
// Cleanup
92+
RemoteStorageHelper.forceDelete(storage, bucket);
93+
}
94+
95+
@Test
96+
public void noOpDoesNothing() {
97+
String httpBucket = randomBucketName();
98+
String grpcBucket = randomBucketName();
99+
// NoOp for HTTP
100+
StorageOptions storageOptionsHttp = StorageOptions.http().build();
101+
Storage storageHttp = storageOptionsHttp.getService();
102+
storageHttp.create(BucketInfo.of(httpBucket));
103+
104+
// NoOp for Grpc
105+
StorageOptions storageOptionsGrpc = StorageOptions.grpc().build();
106+
Storage storageGrpc = storageOptionsGrpc.getService();
107+
storageGrpc.create(BucketInfo.of(grpcBucket));
108+
109+
// cleanup
110+
RemoteStorageHelper.forceDelete(storageHttp, httpBucket);
111+
RemoteStorageHelper.forceDelete(storageGrpc, grpcBucket);
76112
}
77113

78114
private String getAttributeValue(SpanData spanData, String key) {
79115
return spanData.getAttributes().get(AttributeKey.stringKey(key)).toString();
80116
}
117+
118+
public String randomBucketName() {
119+
return "java-storage-grpc-rand-" + UUID.randomUUID();
120+
}
81121
}
82122

83123
class TestExporter implements SpanExporter {
124+
84125
public final List<SpanData> exportedSpans = Collections.synchronizedList(new ArrayList<>());
85126

86127
@Override

0 commit comments

Comments
 (0)