Skip to content

feat(storage): add OpenTelemetry standard metrics for HTTP media, fileio, and transfer_manager - #18899

Draft
shradhakatyal wants to merge 8 commits into
googleapis:mainfrom
shradhakatyal:feat/gcs-otel-http-media-metrics
Draft

shradhakatyal wants to merge 8 commits into
googleapis:mainfrom
shradhakatyal:feat/gcs-otel-http-media-metrics

Conversation

@shradhakatyal

Copy link
Copy Markdown
Contributor

Summary

Adds experimental OpenTelemetry standard metrics support across HTTP media downloads/uploads (_media/requests/), fileio (BlobReader, BlobWriter, Blob.open), and transfer_manager in google-cloud-storage.

Note: Stacked on #18753 (feat/gcs-otel-standard-metrics) and #18898 (feat/gcs-otel-http-json-metrics). Commit f0c1cb2ccf3 contains the isolated changes for this PR.

Key Changes

  • HTTP Media Transport (_media/requests/_request_helpers.py, download.py, upload.py, blob.py): Instruments Download, RawDownload, ChunkedDownload, RawChunkedDownload, SimpleUpload, MultipartUpload, ResumableUpload, XMLMPUContainer, and XMLMPUPart for per-attempt HTTP duration, streaming first-byte gcp.storage.client.ttfb, retries, errors, and logical payload body sizes.
  • Stream File I/O (fileio.py, blob.py): Records one logical ReadObject or WriteObject operation across the full stream lifetime of BlobReader, BlobWriter, and Blob.open() (binary and text modes), while attributing per-chunk HTTP attempts to the parent operation.
  • Transfer Manager (transfer_manager.py): Instruments upload_many, download_many, upload_many_from_filenames, download_many_to_path, download_chunks_concurrently, and upload_chunks_concurrently across both THREAD and PROCESS worker pools, including _LazyClient metrics propagation and shutdown flushing.
  • Primary Exception Attribution (_helpers.py, blob.py, transfer_manager.py): Preserves primary upload/corruption failure attribution on logical metrics when cleanup calls fail without mutating caller exception chains.

…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]
- Add gcp.storage.client.retries counter and rename TTFB metric to gcp.storage.client.ttfb
- Remove gcp.client.repo from _COMMON_ATTRIBUTES
- Update _build_size_buckets to generate 48 buckets matching Go and Java SDKs
- Align per-metric attributes across logical operation, RPC/HTTP attempt, error, retry, and TTFB recorders

[Generated-by: AI]
…fy CONNECTION_ERROR

- Scope histogram View registrations to meter_name="google.cloud.storage"
- Guard normalize_url_template and _extract_server_address against malformed URIs and gRPC authority schemes
- Consolidate CONNECTION_REFUSED, CONNECTION_RESET, and CONNECTIVITY error labels into CONNECTION_ERROR

[Generated-by: AI]
…eneer APIs

- Wire _MetricsRecorder lifecycle and meter_provider configuration into Client
- Instrument _StorageConnection.api_request for per-attempt HTTP duration, TTFB, retries, and errors
- Add logical operation instrumentation across Client, Bucket, Blob, ACL, Batch, HMACKeyMetadata, and BucketNotification
- Defer Batch sub-request operations until Batch.finish() and track lazy HTTPIterator page metrics
- Suppress internal background bucket metadata cache lookups from emitting user-facing metrics
…eio, and transfer_manager

- Instrument _media/requests downloads, simple/multipart/resumable uploads, and XML MPU operations
- Record streaming download TTFB on first body chunk and logical payload body sizes once per operation
- Add stream-lifetime metrics for BlobReader, BlobWriter, and Blob.open binary and text streams
- Instrument transfer_manager batch and concurrent chunk helpers across THREAD and PROCESS workers
- Preserve primary failure attribution when cleanup calls fail without mutating exception chains

@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 implements OpenTelemetry standard metrics instrumentation across the Google Cloud Storage Python client library, adding comprehensive tracking for operations, attempts, retries, latencies, and payload sizes. It introduces a dedicated metrics management module, instruments core components like the HTTP connection, media download/upload helpers, file I/O streams, and the concurrent transfer manager, and adds extensive unit tests. The review feedback highlights potential ValueError: detached buffer issues in _MetricsTextIOWrapper within fileio.py when accessing self.buffer after a stream has been detached, providing actionable code suggestions to handle these cases gracefully in _capture_text_op, __exit__, and __del__.

Comment on lines +701 to +710
def _capture_text_op(self, fn, *args, **kwargs):
try:
result = fn(*args, **kwargs)
except BaseException as exc:
raw = self.buffer
if raw is not None:
raw._stream_error = exc
raise
self.buffer._stream_error = None
return result

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 _capture_text_op, accessing self.buffer can raise a ValueError: detached buffer if the underlying stream has been detached. This would override the original exception exc with a ValueError or raise an unexpected ValueError on success.

We can make this robust by retrieving the buffer reference once at the start of the method and handling the potential ValueError gracefully.

Suggested change
def _capture_text_op(self, fn, *args, **kwargs):
try:
result = fn(*args, **kwargs)
except BaseException as exc:
raw = self.buffer
if raw is not None:
raw._stream_error = exc
raise
self.buffer._stream_error = None
return result
def _capture_text_op(self, fn, *args, **kwargs):
try:
raw = self.buffer
except ValueError:
raw = None
try:
result = fn(*args, **kwargs)
except BaseException as exc:
if raw is not None:
raw._stream_error = exc
raise
if raw is not None:
raw._stream_error = None
return result

Comment on lines +744 to +755
def __exit__(self, exc_type, exc_val, exc_tb):
raw = self.buffer
if exc_type is not None and isinstance(raw, BlobWriter) and not raw.closed:
raw.terminate()
return
super().__exit__(exc_type, exc_val, exc_tb)

def __del__(self):
raw = self.buffer
if raw is not None and not raw.closed:
raw._metrics_finalized = True
raw._buffer.close()

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 __exit__ and __del__, accessing self.buffer can raise a ValueError: detached buffer if the stream has been detached. In __del__, this will print an ignored exception warning to sys.stderr, and in __exit__ it will prevent super().__exit__ from running.

We should wrap the self.buffer access in a try...except ValueError: block to handle detached buffers gracefully.

Suggested change
def __exit__(self, exc_type, exc_val, exc_tb):
raw = self.buffer
if exc_type is not None and isinstance(raw, BlobWriter) and not raw.closed:
raw.terminate()
return
super().__exit__(exc_type, exc_val, exc_tb)
def __del__(self):
raw = self.buffer
if raw is not None and not raw.closed:
raw._metrics_finalized = True
raw._buffer.close()
def __exit__(self, exc_type, exc_val, exc_tb):
try:
raw = self.buffer
except ValueError:
raw = None
if exc_type is not None and isinstance(raw, BlobWriter) and not raw.closed:
raw.terminate()
return
super().__exit__(exc_type, exc_val, exc_tb)
def __del__(self):
try:
raw = self.buffer
except ValueError:
raw = None
if raw is not None and not raw.closed:
raw._metrics_finalized = True
raw._buffer.close()

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