Repository navigation
feat(storage): add StorageMetricsContext and 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
5
commits into
feat/storage-client-metrics-config
from
feat/storage-metrics-context-error-mapping
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e8ebc18
feat(storage): add StorageMetricsContext and StorageErrorTypeMapper f…
nidhiii-27 1bf9a9d
feat(storage): refine context propagation and HTTP error mappings in …
nidhiii-27 08ad936
chore(storage): merge feat/storage-client-metrics-config into feat/st…
nidhiii-27 823de60
fix(storage): align auth status and unwrap specific causes in error m…
nidhiii-27 6374b3e
fix(storage): address PR #14694 review comments on error mapping
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
384 changes: 384 additions & 0 deletions
384
...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,384 @@ | ||
| /* | ||
| * 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 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.Locale; | ||
| 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 javax.net.ssl.SSLHandshakeException; | ||
| 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_INVALID_ARGUMENT = "INVALID_ARGUMENT"; | ||
| static final String STATUS_DEADLINE_EXCEEDED = "TIMEOUT"; | ||
| static final String STATUS_NOT_FOUND = "NOT_FOUND"; | ||
| static final String STATUS_ALREADY_EXISTS = "ALREADY_EXISTS"; | ||
| static final String STATUS_PERMISSION_DENIED = "PERMISSION_DENIED"; | ||
| static final String STATUS_RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"; | ||
| static final String STATUS_FAILED_PRECONDITION = "FAILED_PRECONDITION"; | ||
| static final String STATUS_ABORTED = "ABORTED"; | ||
| static final String STATUS_OUT_OF_RANGE = "OUT_OF_RANGE"; | ||
| static final String STATUS_UNIMPLEMENTED = "UNIMPLEMENTED"; | ||
| static final String STATUS_INTERNAL = "INTERNAL"; | ||
| static final String STATUS_UNAVAILABLE = "UNAVAILABLE"; | ||
| static final String STATUS_DATA_LOSS = "DATA_LOSS"; | ||
| 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"; | ||
|
|
||
| private StorageErrorTypeMapper() {} | ||
|
|
||
| static String mapGrpcStatus(@Nullable Status status) { | ||
| if (status == null) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| switch (status.getCode()) { | ||
| case OK: | ||
| return STATUS_OK; | ||
| case CANCELLED: | ||
| return STATUS_CANCELLED; | ||
| case DEADLINE_EXCEEDED: | ||
| return STATUS_TIMEOUT; | ||
| case INVALID_ARGUMENT: | ||
| return STATUS_INVALID_ARGUMENT; | ||
| case NOT_FOUND: | ||
| return STATUS_NOT_FOUND; | ||
| case ALREADY_EXISTS: | ||
| return STATUS_ALREADY_EXISTS; | ||
| case PERMISSION_DENIED: | ||
| return STATUS_PERMISSION_DENIED; | ||
| case RESOURCE_EXHAUSTED: | ||
| return STATUS_RESOURCE_EXHAUSTED; | ||
| case FAILED_PRECONDITION: | ||
| return STATUS_FAILED_PRECONDITION; | ||
| case ABORTED: | ||
| return STATUS_ABORTED; | ||
| case OUT_OF_RANGE: | ||
| return STATUS_OUT_OF_RANGE; | ||
| case UNIMPLEMENTED: | ||
| return STATUS_UNIMPLEMENTED; | ||
| case INTERNAL: | ||
| return STATUS_INTERNAL; | ||
| case UNAVAILABLE: | ||
| return STATUS_UNAVAILABLE; | ||
| case DATA_LOSS: | ||
| return STATUS_DATA_LOSS; | ||
| case UNAUTHENTICATED: | ||
| return STATUS_UNAUTHENTICATED; | ||
| case UNKNOWN: | ||
| default: | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| 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) { | ||
| if (t.getCause() != null && t.getCause() != t && hasSpecificClientCause(t.getCause(), true)) { | ||
| return mapThrowable(t.getCause()); | ||
| } | ||
| return mapGrpcStatus(((StatusRuntimeException) t).getStatus()); | ||
| } | ||
|
|
||
| if (t instanceof StatusException) { | ||
| if (t.getCause() != null && t.getCause() != t && hasSpecificClientCause(t.getCause(), true)) { | ||
| return mapThrowable(t.getCause()); | ||
| } | ||
| return mapGrpcStatus(((StatusException) t).getStatus()); | ||
| } | ||
|
|
||
| if (t instanceof WatchdogTimeoutException || t instanceof DeadlineExceededException) { | ||
| return STATUS_TIMEOUT; | ||
| } | ||
|
|
||
| if (t instanceof ApiException) { | ||
| if (t.getCause() != null && t.getCause() != t && hasSpecificClientCause(t.getCause(), true)) { | ||
| return mapThrowable(t.getCause()); | ||
| } | ||
| ApiException apiException = (ApiException) t; | ||
| StatusCode.Code code = apiException.getStatusCode().getCode(); | ||
| String mapped = mapGaxStatusCode(code); | ||
| if (!STATUS_UNKNOWN.equals(mapped)) { | ||
| return mapped; | ||
| } | ||
| } | ||
|
nidhiii-27 marked this conversation as resolved.
|
||
|
|
||
| if (t instanceof HttpResponseException) { | ||
| 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, true))) { | ||
| String mappedCause = mapThrowable(cause); | ||
| if (!STATUS_UNKNOWN.equals(mappedCause)) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| if (se.getCode() > 0) { | ||
| return mapHttpResponseCode(se.getCode()); | ||
| } | ||
| } | ||
|
|
||
| if (isAuthException(t)) { | ||
| if (t.getCause() != null | ||
| && t.getCause() != t | ||
| && hasSpecificClientCause(t.getCause(), false)) { | ||
| String mappedCause = mapThrowable(t.getCause()); | ||
| if (!STATUS_UNKNOWN.equals(mappedCause)) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| return STATUS_AUTHENTICATION_ERROR; | ||
| } | ||
|
|
||
| if (t instanceof SocketTimeoutException || t instanceof TimeoutException) { | ||
| return STATUS_TIMEOUT; | ||
| } | ||
|
|
||
| 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 && message.toLowerCase(Locale.US).contains("reset")) { | ||
| return STATUS_CONNECTION_RESET; | ||
| } | ||
| return STATUS_CONNECTIVITY; | ||
| } | ||
|
|
||
| if (t instanceof SSLHandshakeException || t instanceof SSLException) { | ||
| return STATUS_TLS_FAILURE; | ||
| } | ||
|
|
||
| if (t instanceof IOException) { | ||
| if (t.getCause() != null && t.getCause() != t && hasSpecificClientCause(t.getCause(), true)) { | ||
| String mappedCause = mapThrowable(t.getCause()); | ||
| if (!STATUS_UNKNOWN.equals(mappedCause)) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| return STATUS_CONNECTIVITY; | ||
| } | ||
|
|
||
| if (t.getCause() != null && t.getCause() != t) { | ||
| String causeMapping = mapThrowable(t.getCause()); | ||
| if (!STATUS_UNKNOWN.equals(causeMapping)) { | ||
| return causeMapping; | ||
| } | ||
| } | ||
|
|
||
| return STATUS_UNKNOWN; | ||
| } | ||
|
|
||
| private static boolean hasSpecificClientCause(@Nullable Throwable t) { | ||
| return hasSpecificClientCause(t, true); | ||
| } | ||
|
|
||
| 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 SSLHandshakeException | ||
| || 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 = message.toLowerCase(Locale.US); | ||
| if (lower.contains("mismatch checksum") || lower.contains("checksum mismatch")) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static boolean isAuthException(Throwable t) { | ||
| String className = t.getClass().getName(); | ||
| String simpleName = t.getClass().getSimpleName(); | ||
| return className.startsWith("com.google.auth.") | ||
| || className.startsWith("com.google.api.client.auth.") | ||
| || simpleName.equals("GoogleAuthException") | ||
| || simpleName.equals("OAuth2Exception") | ||
| || simpleName.equals("AuthenticationException") | ||
| || className.contains("GoogleAuthException"); | ||
| } | ||
|
|
||
| private static String mapGaxStatusCode(StatusCode.Code code) { | ||
| switch (code) { | ||
| case OK: | ||
| return STATUS_OK; | ||
| case CANCELLED: | ||
| return STATUS_CANCELLED; | ||
| case DEADLINE_EXCEEDED: | ||
| return STATUS_TIMEOUT; | ||
| case INVALID_ARGUMENT: | ||
| return STATUS_INVALID_ARGUMENT; | ||
| case NOT_FOUND: | ||
| return STATUS_NOT_FOUND; | ||
| case ALREADY_EXISTS: | ||
| return STATUS_ALREADY_EXISTS; | ||
| case PERMISSION_DENIED: | ||
| return STATUS_PERMISSION_DENIED; | ||
| case RESOURCE_EXHAUSTED: | ||
| return STATUS_RESOURCE_EXHAUSTED; | ||
| case FAILED_PRECONDITION: | ||
| return STATUS_FAILED_PRECONDITION; | ||
| case ABORTED: | ||
| return STATUS_ABORTED; | ||
| case OUT_OF_RANGE: | ||
| return STATUS_OUT_OF_RANGE; | ||
| case UNIMPLEMENTED: | ||
| return STATUS_UNIMPLEMENTED; | ||
| case INTERNAL: | ||
| return STATUS_INTERNAL; | ||
| case UNAVAILABLE: | ||
| return STATUS_UNAVAILABLE; | ||
| case DATA_LOSS: | ||
| return STATUS_DATA_LOSS; | ||
| case UNAUTHENTICATED: | ||
| return STATUS_UNAUTHENTICATED; | ||
| case UNKNOWN: | ||
| default: | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| } | ||
| } | ||
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.