Skip to content

feat(storage): add http.client.request.duration metric for HTTP requests - #14744

Open
nidhiii-27 wants to merge 14 commits into
feat/storage-rpc-call-durationfrom
feat/storage-http-request-duration
Open

nidhiii-27 wants to merge 14 commits into
feat/storage-rpc-call-durationfrom
feat/storage-http-request-duration

Conversation

@nidhiii-27

@nidhiii-27 nidhiii-27 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Instruments HTTP/JSON storage requests to record http.client.request.duration OpenTelemetry histogram metrics.

  • Adds StorageHttpMetricsInterceptor to measure per-request duration and attach rpc.system.name, http.request.method, http.response.status_code, network.path, url.template, server.address, and error.type attributes across normal responses and IOException failures.
  • Wires StorageHttpMetricsInterceptor via StorageOptions.decorateWithMetrics and HttpStorageRpc.
  • Adds unit tests in StorageHttpMetricsInterceptorTest.

Stacked on #14743.

@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 the StorageHttpMetricsInterceptor class to record HTTP client metrics, such as request duration, using OpenTelemetry. It integrates this interceptor into StorageOptions and HttpStorageRpc to decorate request initializers, and adds comprehensive unit tests. Feedback on the changes suggests initializing the recorded AtomicBoolean to true instead of false to prevent recording metrics for requests that fail before execution begins.

@nidhiii-27
nidhiii-27 marked this pull request as ready for review November 5, 2026 09:03
@nidhiii-27
nidhiii-27 requested review from a team as code owners November 5, 2026 09:03
Merge updated feat/storage-rpc-call-duration base branch, use OpenTelemetryBootstrappingUtils.extractServerAddress, pass String status codes to KEY_HTTP_RESPONSE_STATUS_CODE, wire batchRequestInitializer on overflow batches, and refine StorageHttpMetricsInterceptor Javadoc.

[Generated-by: AI]
…uestDuration to StorageMetricsContext

[Generated-by: AI]
…Interceptor

- Merge updated base branch feat/storage-rpc-call-duration (including #14694)
- Update HttpIOExceptionHandler error type to StorageErrorTypeMapper.STATUS_CONNECTION_ERROR
- Return "/storage/v1/b" in extractV1UrlTemplate when rest is "b" (CreateBucket / ListBuckets)

[Generated-by: AI]
… mapping

After merging StorageErrorTypeMapper updates from #14694, Status.DEADLINE_EXCEEDED
maps to DEADLINE_EXCEEDED while SocketTimeoutException maps to TIMEOUT. Update
recordAttemptError_specificExceptions_mappedToCanonicalTypes to assert 1 count
each for DEADLINE_EXCEEDED, TIMEOUT, and DNS_FAILURE.

[Generated-by: AI]
@nidhiii-27
nidhiii-27 requested a review from cpriti-os November 9, 2026 10:50

@cpriti-os cpriti-os 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.

Thanks for the fixes, a few follow-ups inline.

@nidhiii-27
nidhiii-27 requested a review from cpriti-os November 12, 2026 08:59
*
* <p>Note: {@code google-http-client} invokes {@link HttpResponseInterceptor#interceptResponse}
* inside {@link HttpRequest#execute()} when the HTTP response headers arrive, before streaming
* download bodies are consumed by the caller (matching the Go SDK's {@code

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.

Go actually records this metric when the body hits EOF or is closed, not at headers, so this parenthetical is misleading. Can we drop the Go reference and just say download durations end at response headers and exclude body transfer, with a follow-up tracked for recording at body close?

(req, response, supportsRetry) -> {
boolean handled =
existingUnsuccessfulHandler.handleResponse(req, response, supportsRetry);
if (handled && recorded.compareAndSet(false, true)) {

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.

Can we take the end time before delegating to the existing handler here? Otherwise the 401 point includes the token refresh round trip.

if ("iam".equals(segments[2]) && segments.length > 3) {
return "/storage/v1/b/{bucket}/iam/" + segments[3];
}
return "/storage/v1/b/{bucket}/" + segments[2];

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.

My earlier comment was imprecise, sorry: only raw ids should not leak; Python keeps {entity} on acl paths and {notification} on notificationConfigs. Fine to restore those so get-one and list don't share a template, otherwise happy to leave as is.

assertThat(
StorageHttpMetricsInterceptor.extractUrlTemplate(
new GenericUrl(
"https://storage.googleapis.com/storage/v1/b/b1/notifications/notif1")))

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.

The real path is notificationConfigs rather than notifications, can we use that so the test covers the actual template?

}

@Test
public void initialize_404NotFound_recordsErrorType() throws IOException {

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.

Worth adding a case where an existing handler returns false on a 404 so we check the point is still recorded exactly once?

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.

2 participants