Repository navigation
feat(storage): add StorageErrorTypeMapper for standard metrics #14694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nidhiii-27
wants to merge
33
commits into
main
Choose a base branch
from
feat/storage-metrics-context-error-mapping
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
07f72be
feat(storage): add OpenTelemetry client metrics infrastructure and co…
nidhiii-27 235a230
fix(storage): resolve code formatting violations for client metrics
nidhiii-27 590ad9e
fix(storage): resolve PR review comments on OpenTelemetry metrics
nidhiii-27 02a7740
fix(storage): resolve code formatting violation in OpenTelemetryBoots…
nidhiii-27 f686723
feat(storage): add OpenTelemetry client metrics instrument registry a…
nidhiii-27 babb702
fix(storage): refactor histogram views and remove deferred connection…
nidhiii-27 22a233f
fix(storage): support transport-derived api label in createClientMete…
nidhiii-27 1f2c146
fix(storage): address review comments for OpenTelemetry client metrics
nidhiii-27 7828fd2
feat(storage): address review feedback on client metrics infra
nidhiii-27 983f248
style(storage): format code with fmt-maven-plugin
nidhiii-27 52cd16a
refactor(storage): address PR review comments for OpenTelemetry clien…
nidhiii-27 18b1e73
fix(storage): resolve review comments for client metrics infra
nidhiii-27 6e7a5c6
refactor(storage): preserve detectedAttributes in buildMeterProvider …
nidhiii-27 57fa64a
fix(metrics): update metric counter units to match Go SDK and design doc
nidhiii-27 f39f525
test(storage): deduplicate instrument nullability assertions in Stora…
nidhiii-27 a1f434d
test(storage): initialize InMemoryMetricReader in @Before and clean u…
nidhiii-27 4607b23
refactor(storage): remove misleading @VisibleForTesting from histogra…
nidhiii-27 97ed050
refactor(storage): remove gcp.storage.client.retries counter [Generat…
nidhiii-27 dc20d62
feat(storage): add OpenTelemetry client metrics configuration and opt…
nidhiii-27 07dcbb1
fix(storage): preserve meterProvider equality across toBuilder and se…
nidhiii-27 434948f
fix(storage): add deserialization guard for metricInterval and update…
nidhiii-27 327b558
fix(storage): address PR review comments for otel metrics config
nidhiii-27 517e61f
style(storage): format StorageOptions classes with fmt-maven-plugin
nidhiii-27 7f624a2
fix(storage): address review comments on otel metrics config
nidhiii-27 4f4f726
fix(storage): clamp metricInterval on deserialization and resolve rev…
nidhiii-27 9519e62
refactor(storage): address PR review comments for client metrics conf…
nidhiii-27 3d889a7
fix(storage): address review comments on PR #14484 for metrics option…
nidhiii-27 e7a3588
test(storage): deduplicate HTTP and gRPC test cases in StorageOptions…
nidhiii-27 ed62afa
feat(storage): add StorageErrorTypeMapper for standard metrics [Gener…
nidhiii-27 e5d17f3
chore(storage): remove unused helper and duplicate constant in Storag…
nidhiii-27 88ef6d0
Merge branch 'main' into feat/storage-metrics-context-error-mapping
nidhiii-27 8a64086
fix(storage): address PR #14694 review comments in StorageErrorTypeMa…
nidhiii-27 c998ce6
refactor(storage): polish StorageErrorTypeMapper cause unwrapping and…
nidhiii-27 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
336 changes: 336 additions & 0 deletions
336
...e/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageErrorTypeMapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,336 @@ | ||
| /* | ||
| * Copyright 2026 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.client.http.HttpResponseException; | ||
| import com.google.api.gax.rpc.ApiException; | ||
| import com.google.api.gax.rpc.DeadlineExceededException; | ||
| import com.google.api.gax.rpc.StatusCode; | ||
| import com.google.api.gax.rpc.WatchdogTimeoutException; | ||
| import com.google.common.base.Ascii; | ||
| import io.grpc.Status; | ||
| import io.grpc.StatusException; | ||
| import io.grpc.StatusRuntimeException; | ||
| import java.io.IOException; | ||
| import java.net.ConnectException; | ||
| import java.net.SocketException; | ||
| import java.net.SocketTimeoutException; | ||
| import java.net.UnknownHostException; | ||
| import java.util.concurrent.CancellationException; | ||
| import java.util.concurrent.CompletionException; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.TimeoutException; | ||
| import javax.net.ssl.SSLException; | ||
| import org.checkerframework.checker.nullness.qual.Nullable; | ||
|
|
||
| /** | ||
| * Centralized package-private utility mapping exceptions and status codes to the standard HLD | ||
| * uppercase canonical error strings. | ||
| */ | ||
| final class StorageErrorTypeMapper { | ||
|
|
||
| static final String STATUS_OK = "OK"; | ||
| static final String STATUS_CANCELLED = "CANCELLED"; | ||
| static final String STATUS_UNKNOWN = "UNKNOWN"; | ||
| static final String STATUS_NOT_FOUND = "NOT_FOUND"; | ||
| static final String STATUS_UNAVAILABLE = "UNAVAILABLE"; | ||
|
|
||
| private static final String STATUS_INVALID_ARGUMENT = "INVALID_ARGUMENT"; | ||
| private static final String STATUS_ALREADY_EXISTS = "ALREADY_EXISTS"; | ||
| private static final String STATUS_PERMISSION_DENIED = "PERMISSION_DENIED"; | ||
| private static final String STATUS_RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"; | ||
| private static final String STATUS_FAILED_PRECONDITION = "FAILED_PRECONDITION"; | ||
| private static final String STATUS_OUT_OF_RANGE = "OUT_OF_RANGE"; | ||
| private static final String STATUS_UNIMPLEMENTED = "UNIMPLEMENTED"; | ||
| private static final String STATUS_INTERNAL = "INTERNAL"; | ||
| private static final String STATUS_UNAUTHENTICATED = "UNAUTHENTICATED"; | ||
|
|
||
| static final String STATUS_TIMEOUT = "TIMEOUT"; | ||
| static final String STATUS_DNS_FAILURE = "DNS_FAILURE"; | ||
| static final String STATUS_CONNECTION_REFUSED = "CONNECTION_REFUSED"; | ||
| static final String STATUS_CONNECTION_RESET = "CONNECTION_RESET"; | ||
| static final String STATUS_TLS_FAILURE = "TLS_FAILURE"; | ||
| static final String STATUS_CONNECTIVITY = "CONNECTIVITY"; | ||
| static final String STATUS_AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"; | ||
| static final String STATUS_CHECKSUM_MISMATCH = "CHECKSUM_MISMATCH"; | ||
|
|
||
| static String mapGrpcStatus(@Nullable Status status) { | ||
| if (status == null || status.getCode() == Status.Code.UNKNOWN) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| if (status.getCode() == Status.Code.DEADLINE_EXCEEDED) { | ||
| return STATUS_TIMEOUT; | ||
| } | ||
| return status.getCode().name(); | ||
| } | ||
|
|
||
| private static String mapGaxStatusCode(StatusCode.@Nullable Code code) { | ||
| if (code == null || code == StatusCode.Code.UNKNOWN) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| if (code == StatusCode.Code.DEADLINE_EXCEEDED) { | ||
| return STATUS_TIMEOUT; | ||
| } | ||
| return code.name(); | ||
| } | ||
|
|
||
| static String mapHttpResponseCode(int statusCode) { | ||
| if (statusCode >= 200 && statusCode < 300) { | ||
| return STATUS_OK; | ||
| } | ||
| switch (statusCode) { | ||
| case 400: | ||
| return STATUS_INVALID_ARGUMENT; | ||
| case 401: | ||
| return STATUS_UNAUTHENTICATED; | ||
| case 403: | ||
| return STATUS_PERMISSION_DENIED; | ||
| case 404: | ||
| return STATUS_NOT_FOUND; | ||
| case 408: | ||
| return STATUS_TIMEOUT; | ||
| case 409: | ||
| return STATUS_ALREADY_EXISTS; | ||
| case 412: | ||
| return STATUS_FAILED_PRECONDITION; | ||
| case 416: | ||
| return STATUS_OUT_OF_RANGE; | ||
| case 429: | ||
| return STATUS_RESOURCE_EXHAUSTED; | ||
| case 499: | ||
| return STATUS_CANCELLED; | ||
| case 500: | ||
| return STATUS_INTERNAL; | ||
| case 501: | ||
| return STATUS_UNIMPLEMENTED; | ||
| case 502: | ||
| case 503: | ||
| return STATUS_UNAVAILABLE; | ||
|
nidhiii-27 marked this conversation as resolved.
|
||
| case 504: | ||
| return STATUS_TIMEOUT; | ||
| default: | ||
| if (statusCode >= 400 && statusCode < 600) { | ||
| return String.valueOf(statusCode); | ||
| } | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| } | ||
|
|
||
| static String mapThrowable(@Nullable Throwable t) { | ||
| if (t == null) { | ||
| return STATUS_OK; | ||
| } | ||
|
|
||
| while ((t instanceof ExecutionException || t instanceof CompletionException) | ||
| && t.getCause() != null | ||
| && t.getCause() != t) { | ||
| t = t.getCause(); | ||
| } | ||
|
|
||
| if (isChecksumMismatch(t)) { | ||
| return STATUS_CHECKSUM_MISMATCH; | ||
| } | ||
|
|
||
| if (t instanceof CancellationException) { | ||
| return STATUS_CANCELLED; | ||
| } | ||
|
|
||
| if (t instanceof StatusRuntimeException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return mapGrpcStatus(((StatusRuntimeException) t).getStatus()); | ||
| } | ||
|
|
||
| if (t instanceof StatusException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return mapGrpcStatus(((StatusException) t).getStatus()); | ||
| } | ||
|
|
||
| if (t instanceof WatchdogTimeoutException | ||
| || t instanceof DeadlineExceededException | ||
| || t instanceof SocketTimeoutException | ||
| || t instanceof TimeoutException) { | ||
| return STATUS_TIMEOUT; | ||
| } | ||
|
|
||
| if (t instanceof ApiException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| ApiException apiException = (ApiException) t; | ||
| StatusCode statusCode = apiException.getStatusCode(); | ||
| String mapped = mapGaxStatusCode(statusCode != null ? statusCode.getCode() : null); | ||
| if (!STATUS_UNKNOWN.equals(mapped)) { | ||
| return mapped; | ||
| } | ||
| } | ||
|
nidhiii-27 marked this conversation as resolved.
|
||
|
|
||
| if (t instanceof HttpResponseException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return mapHttpResponseCode(((HttpResponseException) t).getStatusCode()); | ||
| } | ||
|
|
||
| if (t instanceof StorageException) { | ||
| StorageException se = (StorageException) t; | ||
| Throwable cause = se.getCause(); | ||
| if (cause != null | ||
| && cause != se | ||
| && (cause instanceof ApiException | ||
| || cause instanceof StatusRuntimeException | ||
| || cause instanceof StatusException | ||
| || hasSpecificClientCause(cause, /* includeAuth= */ true))) { | ||
| String mappedCause = mapKnownCause(se, cause); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| if (se.getCode() > 0) { | ||
| return mapHttpResponseCode(se.getCode()); | ||
| } | ||
| } | ||
|
|
||
| if (isAuthException(t)) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ false); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return STATUS_AUTHENTICATION_ERROR; | ||
| } | ||
|
|
||
| if (t instanceof UnknownHostException) { | ||
| return STATUS_DNS_FAILURE; | ||
| } | ||
|
|
||
| if (t instanceof ConnectException) { | ||
| return STATUS_CONNECTION_REFUSED; | ||
| } | ||
|
|
||
| if (t instanceof SocketException) { | ||
| String message = t.getMessage(); | ||
| if (message != null && Ascii.toLowerCase(message).contains("reset")) { | ||
| return STATUS_CONNECTION_RESET; | ||
| } | ||
| return STATUS_CONNECTIVITY; | ||
| } | ||
|
|
||
| if (t instanceof SSLException) { | ||
| return STATUS_TLS_FAILURE; | ||
| } | ||
|
|
||
| if (t instanceof IOException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return STATUS_CONNECTIVITY; | ||
| } | ||
|
|
||
| String causeMapping = mapKnownCause(t, t.getCause()); | ||
| if (causeMapping != null) { | ||
| return causeMapping; | ||
| } | ||
|
|
||
| return STATUS_UNKNOWN; | ||
| } | ||
|
|
||
| private static @Nullable String mapSpecificClientCause(Throwable t, boolean includeAuth) { | ||
| Throwable cause = t.getCause(); | ||
| if (cause != null && cause != t && hasSpecificClientCause(cause, includeAuth)) { | ||
| return mapKnownCause(t, cause); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static @Nullable String mapKnownCause( | ||
| @Nullable Throwable parent, @Nullable Throwable cause) { | ||
| if (cause != null && cause != parent) { | ||
| String mappedCause = mapThrowable(cause); | ||
| if (!STATUS_UNKNOWN.equals(mappedCause)) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static boolean hasSpecificClientCause(@Nullable Throwable t, boolean includeAuth) { | ||
| Throwable current = t; | ||
| while (current != null) { | ||
| if (current instanceof WatchdogTimeoutException | ||
| || current instanceof DeadlineExceededException | ||
| || current instanceof SocketTimeoutException | ||
| || current instanceof TimeoutException | ||
| || current instanceof UnknownHostException | ||
| || current instanceof ConnectException | ||
| || current instanceof SocketException | ||
| || current instanceof SSLException | ||
| || current instanceof CancellationException | ||
| || isChecksumMismatch(current) | ||
| || (includeAuth && isAuthException(current))) { | ||
| return true; | ||
| } | ||
| Throwable next = current.getCause(); | ||
| if (next == current) { | ||
| break; | ||
| } | ||
| current = next; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static boolean isChecksumMismatch(Throwable t) { | ||
| if (t instanceof Hasher.ChecksumMismatchException | ||
| || t instanceof Hasher.UncheckedChecksumMismatchException) { | ||
| return true; | ||
| } | ||
| String message = t.getMessage(); | ||
| if (message != null) { | ||
| String lower = Ascii.toLowerCase(message); | ||
| if (lower.contains("mismatch checksum") || lower.contains("checksum mismatch")) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static boolean isAuthException(Throwable t) { | ||
| for (Class<?> cls = t.getClass(); | ||
| cls != null && cls != Throwable.class; | ||
| cls = cls.getSuperclass()) { | ||
| String className = cls.getName(); | ||
| String simpleName = cls.getSimpleName(); | ||
| if (className.startsWith("com.google.auth.") | ||
| || className.startsWith("com.google.api.client.auth.") | ||
| || className.contains("GoogleAuthException") | ||
| || simpleName.equals("OAuth2Exception") | ||
| || simpleName.equals("AuthenticationException")) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private StorageErrorTypeMapper() {} | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.