Skip to content

feat(storage): add rpc.client.call.duration metric for gRPC calls - #14743

Draft
nidhiii-27 wants to merge 1 commit into
feat/storage-grpc-veneer-operation-metricsfrom
feat/storage-rpc-call-duration
Draft

nidhiii-27 wants to merge 1 commit into
feat/storage-grpc-veneer-operation-metricsfrom
feat/storage-rpc-call-duration

Conversation

@nidhiii-27

Copy link
Copy Markdown
Contributor

Instruments gRPC storage calls to record rpc.client.call.duration OpenTelemetry histogram metrics.

  • Adds StorageGrpcMetricsInterceptor to measure per-RPC duration and attach rpc.system.name, rpc.method, rpc.response.status_code, network.path, server.address, and error.type attributes.
  • Wires StorageGrpcMetricsInterceptor into GrpcStorageOptions and initializes rpc.client.call.duration in StorageClientMetrics.
  • Adds unit tests in StorageGrpcMetricsInterceptorTest and StorageClientMetricsTest.

Stacked on #14729.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces StorageGrpcMetricsInterceptor, a new gRPC client interceptor designed to record call metrics such as RPC duration, and integrates it into GrpcStorageOptions. It also adds comprehensive unit tests to verify its behavior. The review feedback is highly constructive, focusing on preventing a potential NullPointerException when combining interceptors, simplifying redundant server address resolution, and optimizing performance on the hot path by precomputing the DirectPath endpoint status in the constructor rather than performing string operations on every RPC completion.

Comment on lines +338 to +345
GrpcInterceptorProvider combinedProvider =
NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If grpcInterceptorProvider is null, calling grpcInterceptorProvider.getInterceptors() will throw a NullPointerException when the interceptor provider is invoked. We should add a null check to safely handle this case.

Suggested change
GrpcInterceptorProvider combinedProvider =
NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();
GrpcInterceptorProvider combinedProvider =
grpcInterceptorProvider == null
|| NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();

Comment on lines +61 to +67
String initialServerAddress =
StorageOptions.extractServerAddress(
defaultEndpoint != null ? defaultEndpoint : next.authority());
final String serverAddress =
initialServerAddress != null
? initialServerAddress
: StorageOptions.extractServerAddress(next.authority());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When defaultEndpoint is null, StorageOptions.extractServerAddress(next.authority()) is called twice if it returns null. We can simplify this logic to avoid redundant calls and improve readability.

    String targetEndpoint = defaultEndpoint != null ? defaultEndpoint : next.authority();
    String resolvedAddress = StorageOptions.extractServerAddress(targetEndpoint);
    final String serverAddress =
        resolvedAddress != null
            ? resolvedAddress
            : (defaultEndpoint != null ? StorageOptions.extractServerAddress(next.authority()) : null);

Comment on lines +44 to +51
@Nullable private final String defaultEndpoint;

StorageGrpcMetricsInterceptor(
StorageClientMetrics metrics, ApiClock clock, @Nullable String defaultEndpoint) {
this.metrics = metrics;
this.clock = clock;
this.defaultEndpoint = defaultEndpoint;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid the overhead of string trimming and prefix matching on every single RPC completion, we can precompute whether the defaultEndpoint is a DirectPath endpoint once in the constructor.

  @Nullable private final String defaultEndpoint;
  private final boolean isDirectPathEndpoint;

  StorageGrpcMetricsInterceptor(
      StorageClientMetrics metrics, ApiClock clock, @Nullable String defaultEndpoint) {
    this.metrics = metrics;
    this.clock = clock;
    this.defaultEndpoint = defaultEndpoint;
    this.isDirectPathEndpoint =
        defaultEndpoint != null && defaultEndpoint.trim().startsWith("google-c2p:///");
  }

Comment on lines +85 to +87
double durationSeconds =
Math.max(0.0, (clock.nanoTime() - startTimeNanos) / 1_000_000_000.0);
String networkPath = detectNetworkPath(getAttributes(), defaultEndpoint);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the call to detectNetworkPath to pass the precomputed isDirectPathEndpoint boolean instead of the raw defaultEndpoint string.

Suggested change
double durationSeconds =
Math.max(0.0, (clock.nanoTime() - startTimeNanos) / 1_000_000_000.0);
String networkPath = detectNetworkPath(getAttributes(), defaultEndpoint);
double durationSeconds =
Math.max(0.0, (clock.nanoTime() - startTimeNanos) / 1_000_000_000.0);
String networkPath = detectNetworkPath(getAttributes(), isDirectPathEndpoint);

Comment on lines +130 to +146
static String detectNetworkPath(
@Nullable Attributes callAttributes, @Nullable String defaultEndpoint) {
SocketAddress remoteAddr =
callAttributes != null ? callAttributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) : null;
if (remoteAddr instanceof InetSocketAddress) {
InetAddress inetAddr = ((InetSocketAddress) remoteAddr).getAddress();
if (inetAddr != null) {
return isDirectPathIp(inetAddr)
? StorageClientMetrics.NETWORK_PATH_DIRECTPATH
: StorageClientMetrics.NETWORK_PATH_CLOUDPATH;
}
}
if (defaultEndpoint != null && defaultEndpoint.trim().startsWith("google-c2p:///")) {
return StorageClientMetrics.NETWORK_PATH_DIRECTPATH;
}
return StorageClientMetrics.NETWORK_PATH_UNKNOWN;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can update detectNetworkPath to accept the precomputed isDirectPathEndpoint boolean instead of the raw defaultEndpoint string, avoiding string operations on the hot path.

Suggested change
static String detectNetworkPath(
@Nullable Attributes callAttributes, @Nullable String defaultEndpoint) {
SocketAddress remoteAddr =
callAttributes != null ? callAttributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) : null;
if (remoteAddr instanceof InetSocketAddress) {
InetAddress inetAddr = ((InetSocketAddress) remoteAddr).getAddress();
if (inetAddr != null) {
return isDirectPathIp(inetAddr)
? StorageClientMetrics.NETWORK_PATH_DIRECTPATH
: StorageClientMetrics.NETWORK_PATH_CLOUDPATH;
}
}
if (defaultEndpoint != null && defaultEndpoint.trim().startsWith("google-c2p:///")) {
return StorageClientMetrics.NETWORK_PATH_DIRECTPATH;
}
return StorageClientMetrics.NETWORK_PATH_UNKNOWN;
}
static String detectNetworkPath(
@Nullable Attributes callAttributes, boolean isDirectPathEndpoint) {
SocketAddress remoteAddr =
callAttributes != null ? callAttributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR) : null;
if (remoteAddr instanceof InetSocketAddress) {
InetAddress inetAddr = ((InetSocketAddress) remoteAddr).getAddress();
if (inetAddr != null) {
return isDirectPathIp(inetAddr)
? StorageClientMetrics.NETWORK_PATH_DIRECTPATH
: StorageClientMetrics.NETWORK_PATH_CLOUDPATH;
}
}
if (isDirectPathEndpoint) {
return StorageClientMetrics.NETWORK_PATH_DIRECTPATH;
}
return StorageClientMetrics.NETWORK_PATH_UNKNOWN;
}

Comment on lines +115 to +135
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(directPathAttrs, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);

io.grpc.Attributes cloudPathAttrs =
io.grpc.Attributes.newBuilder()
.set(
Grpc.TRANSPORT_ATTR_REMOTE_ADDR,
new InetSocketAddress(InetAddress.getByName("142.250.1.1"), 443))
.build();
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(cloudPathAttrs, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_CLOUDPATH);

assertThat(
StorageGrpcMetricsInterceptor.detectNetworkPath(
null, "google-c2p:///storage.googleapis.com"))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);
assertThat(
StorageGrpcMetricsInterceptor.detectNetworkPath(null, "https://storage.googleapis.com"))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_UNKNOWN);
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(null, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_UNKNOWN);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the tests to match the new signature of detectNetworkPath which now accepts a boolean instead of a string.

Suggested change
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(directPathAttrs, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);
io.grpc.Attributes cloudPathAttrs =
io.grpc.Attributes.newBuilder()
.set(
Grpc.TRANSPORT_ATTR_REMOTE_ADDR,
new InetSocketAddress(InetAddress.getByName("142.250.1.1"), 443))
.build();
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(cloudPathAttrs, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_CLOUDPATH);
assertThat(
StorageGrpcMetricsInterceptor.detectNetworkPath(
null, "google-c2p:///storage.googleapis.com"))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);
assertThat(
StorageGrpcMetricsInterceptor.detectNetworkPath(null, "https://storage.googleapis.com"))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_UNKNOWN);
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(null, null))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_UNKNOWN);
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(directPathAttrs, false))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);
io.grpc.Attributes cloudPathAttrs =
io.grpc.Attributes.newBuilder()
.set(
Grpc.TRANSPORT_ATTR_REMOTE_ADDR,
new InetSocketAddress(InetAddress.getByName("142.250.1.1"), 443))
.build();
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(cloudPathAttrs, false))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_CLOUDPATH);
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(null, true))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_DIRECTPATH);
assertThat(StorageGrpcMetricsInterceptor.detectNetworkPath(null, false))
.isEqualTo(StorageClientMetrics.NETWORK_PATH_UNKNOWN);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces StorageGrpcMetricsInterceptor to record gRPC client call metrics (such as duration, status codes, and network paths) and integrates it into GrpcStorageOptions. It also adds comprehensive unit tests to verify the interceptor's behavior. The review feedback highlights two key improvement opportunities: adding a null check for grpcInterceptorProvider to prevent a potential NullPointerException when combining providers, and simplifying the redundant logic used to resolve the serverAddress.

Comment on lines +338 to +345
GrpcInterceptorProvider combinedProvider =
NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If grpcInterceptorProvider is null, NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider) evaluates to false. This leads to executing the else branch of the ternary operator, where calling grpcInterceptorProvider.getInterceptors() will throw a NullPointerException. Adding a null check for grpcInterceptorProvider prevents this potential runtime crash.

Suggested change
GrpcInterceptorProvider combinedProvider =
NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();
GrpcInterceptorProvider combinedProvider =
grpcInterceptorProvider == null
|| NoopGrpcInterceptorProvider.INSTANCE.equals(grpcInterceptorProvider)
? () -> ImmutableList.of(metricsInterceptor)
: () ->
ImmutableList.<ClientInterceptor>builder()
.addAll(grpcInterceptorProvider.getInterceptors())
.add(metricsInterceptor)
.build();

Comment on lines +61 to +68
String initialServerAddress =
StorageOptions.extractServerAddress(
defaultEndpoint != null ? defaultEndpoint : next.authority());
final String serverAddress =
initialServerAddress != null
? initialServerAddress
: StorageOptions.extractServerAddress(next.authority());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current logic for resolving serverAddress is redundant and hard to read. If defaultEndpoint is null, StorageOptions.extractServerAddress(next.authority()) is evaluated twice. Simplifying this logic avoids redundant method calls and improves readability.

Suggested change
String initialServerAddress =
StorageOptions.extractServerAddress(
defaultEndpoint != null ? defaultEndpoint : next.authority());
final String serverAddress =
initialServerAddress != null
? initialServerAddress
: StorageOptions.extractServerAddress(next.authority());
String shortMethod = extractShortMethodName(method.getFullMethodName());
String serverAddress = null;
if (defaultEndpoint != null) {
serverAddress = StorageOptions.extractServerAddress(defaultEndpoint);
}
if (serverAddress == null) {
serverAddress = StorageOptions.extractServerAddress(next.authority());
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant