Skip to content

feat(storage): add OpenTelemetry standard metrics recorder, views, and helpers - #18753

Draft
shradhakatyal wants to merge 1 commit into
googleapis:mainfrom
shradhakatyal:feat/gcs-otel-standard-metrics
Draft

shradhakatyal wants to merge 1 commit into
googleapis:mainfrom
shradhakatyal:feat/gcs-otel-standard-metrics

Conversation

@shradhakatyal

@shradhakatyal shradhakatyal commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the core OpenTelemetry standard metrics infrastructure for Google Cloud Storage (_opentelemetry_metrics.py), laying the foundation for gRPC and HTTP standard metrics instrumentation:

  • _MetricsRecorder: Manages the 9 standard metric instruments (rpc.client.call.duration, http.client.request.duration, gcp.client.request.duration, gcp.storage.client.operations, gcp.storage.client.attempts, gcp.storage.client.request.body.size, gcp.storage.client.response.body.size, gcp.storage.client.operation.ttfb, and gcp.storage.client.errors), per-instrument attribute scoping, and custom MeterProvider flushing on shutdown().
  • Custom Histogram Views (get_histogram_views): Defines custom bucket boundaries for latency (165 buckets from 0.0s to <300.0s) and byte size (45 buckets from 0 B to 16 GiB).
  • Canonical Error Classification (classify_error_type): Maps transport, authentication, checksum, timeout, SSL/TLS, DNS, socket, gRPC, and HTTP errors to low-cardinality error.type values.
  • Bounded Route Normalization (normalize_url_template): Normalizes GCS JSON, Media, Batch, and XML Multipart Upload URLs into low-cardinality url.template strings, falling back to "UNKNOWN".
  • Logical Operation Context (start_operation): Tracks the outermost user-invoked Veneer method via ContextVar to prevent duplicate logical operation metrics on nested internal calls and supports suppress=True for internal chunk/prefetch sub-operations.
  • Unit Tests: Adds 106 unit tests in tests/unit/test__opentelemetry_metrics.py with 100% statement coverage.

[Generated-by: AI]

…d helpers

Implement core OpenTelemetry standard metrics infrastructure for GCS:
- Add _MetricsRecorder with the 9 standard metric instruments
- Add custom latency (165 buckets) and byte-size (45 buckets) histogram views
- Add classify_error_type for low-cardinality error.type mapping
- Add normalize_url_template for bounded HTTP url.template routing
- Add start_operation ContextVar for outermost logical operation tracking and nested call suppression

[Generated-by: AI]

@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 comprehensive OpenTelemetry metrics instrumentation for the Google Cloud Storage client, including custom histogram buckets, canonical error classification, URL template normalization, logical operation context tracking, and a metrics recorder class, along with extensive unit tests. The review feedback suggests several robustness improvements to the error classification logic, such as validating that exception args are iterable, handling mock objects gracefully during MRO and gRPC status name resolution, performing more precise module prefix matching, and classifying concurrent.futures.CancelledError as a cancellation.

Comment on lines +361 to +364
for arg in getattr(current, "args", ()):
if isinstance(arg, BaseException) and id(arg) not in visited:
visited.add(id(arg))
queue.append(arg)

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 prevent potential TypeError exceptions when traversing the exception chain (for example, if a mock exception or a custom exception with a non-tuple args attribute is passed), it is safer to verify that args is a tuple or list before iterating over it.

Suggested change
for arg in getattr(current, "args", ()):
if isinstance(arg, BaseException) and id(arg) not in visited:
visited.add(id(arg))
queue.append(arg)
args = getattr(current, "args", ())
if isinstance(args, (tuple, list)):
for arg in args:
if isinstance(arg, BaseException) and id(arg) not in visited:
visited.add(id(arg))
queue.append(arg)

Comment on lines +371 to +376
for base in getattr(cls, "__mro__", ()):
mod = getattr(base, "__module__", "") or ""
if mod.startswith(module_prefix):
if class_names is None or base.__name__ in class_names:
return True
return False

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 checking class inheritance by module, if cls is a mock object (commonly used in tests), getattr(cls, "__mro__", ()) can return a mock object instead of a tuple, which will raise a TypeError when iterated. Additionally, using mod.startswith(module_prefix) can incorrectly match sub-modules or modules with a common prefix (e.g., matching google.cloud.storage_transfer when prefix is google.cloud.storage). We should check isinstance(mro, tuple) and perform a more precise module prefix check.

Suggested change
for base in getattr(cls, "__mro__", ()):
mod = getattr(base, "__module__", "") or ""
if mod.startswith(module_prefix):
if class_names is None or base.__name__ in class_names:
return True
return False
mro = getattr(cls, "__mro__", None)
if isinstance(mro, tuple):
for base in mro:
mod = getattr(base, "__module__", "") or ""
if mod == module_prefix or mod.startswith(module_prefix + "."):
if class_names is None or getattr(base, "__name__", None) in class_names:
return True
return False

Comment on lines +424 to +426
for item in chain:
if isinstance(item, asyncio.CancelledError):
return "CANCELLED"

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

In addition to asyncio.CancelledError, we should also check for concurrent.futures.CancelledError to ensure that cancellations occurring in thread pools or background workers (such as those in transfer_manager) are correctly classified as "CANCELLED".

Suggested change
for item in chain:
if isinstance(item, asyncio.CancelledError):
return "CANCELLED"
for item in chain:
if isinstance(item, (asyncio.CancelledError, concurrent.futures.CancelledError)):
return "CANCELLED"

Comment on lines +455 to +458
if rpc_status is not None:
name = getattr(rpc_status, "name", None)
if name:
return str(name)

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 resolving the gRPC status code name, if rpc_status is a mock object, getattr(rpc_status, "name", None) will return a mock object which is truthy, leading to returning a string representation of the mock as the error type. We should explicitly verify that name is a string to prevent this.

Suggested change
if rpc_status is not None:
name = getattr(rpc_status, "name", None)
if name:
return str(name)
if rpc_status is not None:
name = getattr(rpc_status, "name", None)
if isinstance(name, str):
return name

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