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
37
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.
+1,078
−0
Open
Changes from all commits
Commits
Show all changes
37 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 1ee3ce0
fix(storage): resolve PR comments for StorageErrorTypeMapper
nidhiii-27 fd0a88f
fix(storage): address final PR review comments in StorageErrorTypeMapper
nidhiii-27 ce389ea
chore(storage): merge main to resolve spanner test compilation in gap…
nidhiii-27 dfdd5c4
refactor(storage): move mapGaxStatusCode helper below package-private…
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
362 changes: 362 additions & 0 deletions
362
...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,362 @@ | ||
| /* | ||
| * 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.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.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"; | ||
| static final String STATUS_DEADLINE_EXCEEDED = "DEADLINE_EXCEEDED"; | ||
|
|
||
| private static final String STATUS_INVALID_ARGUMENT = "INVALID_ARGUMENT"; | ||
| private static final String STATUS_ABORTED = "ABORTED"; | ||
| 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_TLS_FAILURE = "TLS_FAILURE"; | ||
| static final String STATUS_CONNECTION_ERROR = "CONNECTION_ERROR"; | ||
| 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) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| Throwable cause = status.getCause(); | ||
| if (cause != null && hasSpecificClientCause(cause, /* includeAuth= */ true)) { | ||
| String mappedCause = mapKnownCause(null, cause); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| } | ||
| if (isServerChecksumGrpcStatus(status)) { | ||
| return STATUS_CHECKSUM_MISMATCH; | ||
| } | ||
| if (status.getCode() == Status.Code.UNKNOWN) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| return status.getCode().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_DEADLINE_EXCEEDED; | ||
| case 409: | ||
| return STATUS_ABORTED; | ||
| 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.
Resolved
|
||
| case 504: | ||
| return STATUS_DEADLINE_EXCEEDED; | ||
| 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 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.
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; | ||
| String mappedCause = mapKnownCause(se, se.getCause()); | ||
| 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 SSLException) { | ||
| return STATUS_TLS_FAILURE; | ||
| } | ||
|
|
||
| if (t instanceof IOException) { | ||
| String mappedCause = mapSpecificClientCause(t, /* includeAuth= */ true); | ||
| if (mappedCause != null) { | ||
| return mappedCause; | ||
| } | ||
| return STATUS_CONNECTION_ERROR; | ||
| } | ||
|
|
||
| String causeMapping = mapKnownCause(t, t.getCause()); | ||
| if (causeMapping != null) { | ||
| return causeMapping; | ||
| } | ||
|
|
||
| return STATUS_UNKNOWN; | ||
| } | ||
|
|
||
| private static String mapGaxStatusCode(StatusCode.@Nullable Code code) { | ||
| if (code == null || code == StatusCode.Code.UNKNOWN) { | ||
| return STATUS_UNKNOWN; | ||
| } | ||
| return code.name(); | ||
| } | ||
|
|
||
| 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 TimeoutException | ||
| || current instanceof CancellationException | ||
| || isChecksumMismatch(current) | ||
| || (includeAuth && isAuthException(current)) | ||
| || (current instanceof IOException | ||
| && !(current instanceof HttpResponseException) | ||
| && (includeAuth || !isAuthException(current)))) { | ||
| return true; | ||
|
nidhiii-27 marked this conversation as resolved.
Resolved
|
||
| } | ||
| 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 isServerChecksumError(t); | ||
| } | ||
|
|
||
| private static boolean isServerChecksumError(Throwable t) { | ||
| if (t instanceof StatusRuntimeException) { | ||
| return isServerChecksumGrpcStatus(((StatusRuntimeException) t).getStatus()); | ||
| } | ||
| if (t instanceof StatusException) { | ||
| return isServerChecksumGrpcStatus(((StatusException) t).getStatus()); | ||
| } | ||
| if (t instanceof ApiException) { | ||
| StatusCode statusCode = ((ApiException) t).getStatusCode(); | ||
| StatusCode.Code code = statusCode != null ? statusCode.getCode() : null; | ||
| return (code == StatusCode.Code.INVALID_ARGUMENT || code == StatusCode.Code.DATA_LOSS) | ||
| && hasServerChecksumMismatchMessage(t.getMessage()); | ||
| } | ||
| if (t instanceof HttpResponseException) { | ||
| return ((HttpResponseException) t).getStatusCode() == 400 | ||
| && hasServerChecksumMismatchMessage(t.getMessage()); | ||
| } | ||
| if (t instanceof StorageException) { | ||
| StorageException se = (StorageException) t; | ||
| Throwable cause = se.getCause(); | ||
| if (cause instanceof ApiException | ||
| || cause instanceof StatusRuntimeException | ||
| || cause instanceof StatusException) { | ||
| return isServerChecksumError(cause); | ||
| } | ||
| return se.getCode() == 400 && hasServerChecksumMismatchMessage(se.getMessage()); | ||
| } | ||
| return false; | ||
| } | ||
|
nidhiii-27 marked this conversation as resolved.
Resolved
|
||
|
|
||
| private static boolean isServerChecksumGrpcStatus(@Nullable Status status) { | ||
| if (status == null) { | ||
| return false; | ||
| } | ||
| Status.Code code = status.getCode(); | ||
| return (code == Status.Code.INVALID_ARGUMENT || code == Status.Code.DATA_LOSS) | ||
| && hasServerChecksumMismatchMessage(status.getDescription()); | ||
| } | ||
|
|
||
| private static boolean hasServerChecksumMismatchMessage(@Nullable String message) { | ||
| if (message == null) { | ||
| return false; | ||
| } | ||
| String lower = Ascii.toLowerCase(message); | ||
| return (lower.contains("crc32c") || lower.contains("md5")) && lower.contains("match"); | ||
| } | ||
|
|
||
| private static boolean isAuthException(Throwable t) { | ||
| for (Class<?> cls = t.getClass(); | ||
| cls != null && cls != Throwable.class; | ||
| cls = cls.getSuperclass()) { | ||
| String className = cls.getName(); | ||
|
nidhiii-27 marked this conversation as resolved.
Resolved
|
||
| 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.