Skip to content

Commit b8ea557

Browse files
committed
feat: add requestID info in error exceptions
1 parent 3b1792a commit b8ea557

File tree

10 files changed

+430
-56
lines changed

10 files changed

+430
-56
lines changed

google/cloud/spanner_v1/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from .types.type import TypeCode
6666
from .data_types import JsonObject, Interval
6767
from .transaction import BatchTransactionId, DefaultTransactionOptions
68+
from .exceptions import SpannerError, wrap_with_request_id
6869

6970
from google.cloud.spanner_v1 import param_types
7071
from google.cloud.spanner_v1.client import Client
@@ -88,6 +89,9 @@
8889
# google.cloud.spanner_v1
8990
"__version__",
9091
"param_types",
92+
# google.cloud.spanner_v1.exceptions
93+
"SpannerError",
94+
"wrap_with_request_id",
9195
# google.cloud.spanner_v1.client
9296
"Client",
9397
# google.cloud.spanner_v1.keyset

google/cloud/spanner_v1/_helpers.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import threading
2323
import logging
2424
import uuid
25+
from contextlib import contextmanager
2526

2627
from google.protobuf.struct_pb2 import ListValue
2728
from google.protobuf.struct_pb2 import Value
@@ -34,8 +35,12 @@
3435
from google.cloud.spanner_v1.types import ExecuteSqlRequest
3536
from google.cloud.spanner_v1.types import TransactionOptions
3637
from google.cloud.spanner_v1.data_types import JsonObject, Interval
37-
from google.cloud.spanner_v1.request_id_header import with_request_id
38+
from google.cloud.spanner_v1.request_id_header import (
39+
with_request_id,
40+
with_request_id_metadata_only,
41+
)
3842
from google.cloud.spanner_v1.types import TypeCode
43+
from google.cloud.spanner_v1.exceptions import wrap_with_request_id
3944

4045
from google.rpc.error_details_pb2 import RetryInfo
4146

@@ -767,9 +772,65 @@ def reset(self):
767772

768773

769774
def _metadata_with_request_id(*args, **kwargs):
775+
"""Return metadata with request ID header.
776+
777+
This function returns only the metadata list (not a tuple),
778+
maintaining backward compatibility with existing code.
779+
780+
Args:
781+
*args: Arguments to pass to with_request_id
782+
**kwargs: Keyword arguments to pass to with_request_id
783+
784+
Returns:
785+
list: gRPC metadata with request ID header
786+
"""
787+
return with_request_id_metadata_only(*args, **kwargs)
788+
789+
790+
def _metadata_with_request_id_and_req_id(*args, **kwargs):
791+
"""Return both metadata and request ID string.
792+
793+
This is used when we need to augment errors with the request ID.
794+
795+
Args:
796+
*args: Arguments to pass to with_request_id
797+
**kwargs: Keyword arguments to pass to with_request_id
798+
799+
Returns:
800+
tuple: (metadata, request_id)
801+
"""
770802
return with_request_id(*args, **kwargs)
771803

772804

805+
def _augment_error_with_request_id(error, request_id=None):
806+
"""Augment an error with request ID information.
807+
808+
Args:
809+
error: The error to augment (typically GoogleAPICallError)
810+
request_id (str): The request ID to include
811+
812+
Returns:
813+
The augmented error with request ID information
814+
"""
815+
return wrap_with_request_id(error, request_id)
816+
817+
818+
@contextmanager
819+
def _augment_errors_with_request_id(request_id):
820+
"""Context manager to augment exceptions with request ID.
821+
822+
Args:
823+
request_id (str): The request ID to include in exceptions
824+
825+
Yields:
826+
None
827+
"""
828+
try:
829+
yield
830+
except Exception as exc:
831+
raise _augment_error_with_request_id(exc, request_id)
832+
833+
773834
def _merge_Transaction_Options(
774835
defaultTransactionOptions: TransactionOptions,
775836
mergeTransactionOptions: TransactionOptions,

google/cloud/spanner_v1/database.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
_metadata_with_prefix,
5656
_metadata_with_leader_aware_routing,
5757
_metadata_with_request_id,
58+
_augment_errors_with_request_id,
59+
_metadata_with_request_id_and_req_id,
5860
)
5961
from google.cloud.spanner_v1.batch import Batch
6062
from google.cloud.spanner_v1.batch import MutationGroups
@@ -496,6 +498,66 @@ def metadata_with_request_id(
496498
span,
497499
)
498500

501+
def metadata_and_request_id(
502+
self, nth_request, nth_attempt, prior_metadata=[], span=None
503+
):
504+
"""Return metadata and request ID string.
505+
506+
This method returns both the gRPC metadata with request ID header
507+
and the request ID string itself, which can be used to augment errors.
508+
509+
Args:
510+
nth_request: The request sequence number
511+
nth_attempt: The attempt number (for retries)
512+
prior_metadata: Prior metadata to include
513+
span: Optional span for tracing
514+
515+
Returns:
516+
tuple: (metadata_list, request_id_string)
517+
"""
518+
if span is None:
519+
span = get_current_span()
520+
521+
return _metadata_with_request_id_and_req_id(
522+
self._nth_client_id,
523+
self._channel_id,
524+
nth_request,
525+
nth_attempt,
526+
prior_metadata,
527+
span,
528+
)
529+
530+
def with_error_augmentation(
531+
self, nth_request, nth_attempt, prior_metadata=[], span=None
532+
):
533+
"""Context manager for gRPC calls with error augmentation.
534+
535+
This context manager provides both metadata with request ID and
536+
automatically augments any exceptions with the request ID.
537+
538+
Args:
539+
nth_request: The request sequence number
540+
nth_attempt: The attempt number (for retries)
541+
prior_metadata: Prior metadata to include
542+
span: Optional span for tracing
543+
544+
Yields:
545+
tuple: (metadata_list, context_manager)
546+
"""
547+
if span is None:
548+
span = get_current_span()
549+
550+
metadata, request_id = _metadata_with_request_id_and_req_id(
551+
self._nth_client_id,
552+
self._channel_id,
553+
nth_request,
554+
nth_attempt,
555+
prior_metadata,
556+
span,
557+
)
558+
559+
return metadata, _augment_errors_with_request_id(request_id)
560+
499561
def __eq__(self, other):
500562
if not isinstance(other, self.__class__):
501563
return NotImplemented
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2026 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Cloud Spanner exception classes with request ID support."""
16+
17+
from google.api_core.exceptions import GoogleAPICallError
18+
19+
20+
class SpannerError(Exception):
21+
"""Base Spanner exception that includes request ID.
22+
23+
This class wraps an error (typically GoogleAPICallError) to add request ID information
24+
for better debugging and error correlation.
25+
26+
Args:
27+
error: The original error to wrap (typically GoogleAPICallError)
28+
request_id (str): The request ID associated with this error
29+
"""
30+
31+
def __init__(self, error, request_id=None):
32+
self._error = error
33+
self._request_id = request_id or ""
34+
35+
@property
36+
def request_id(self):
37+
return self._request_id
38+
39+
def __str__(self):
40+
s = str(self._error)
41+
if self._request_id:
42+
s = f"{s}, request_id = {self._request_id!r}"
43+
return s
44+
45+
def __repr__(self):
46+
return f"SpannerError(error={self._error!r}, request_id={self._request_id!r})"
47+
48+
def __getattr__(self, name):
49+
if name == "request_id":
50+
return self._request_id
51+
return getattr(self._error, name)
52+
53+
54+
def wrap_with_request_id(error, request_id=None):
55+
"""Wrap a GoogleAPICallError with request ID information.
56+
57+
Args:
58+
error: The error to wrap. If not a GoogleAPICallError, returns as-is
59+
request_id (str): The request ID to include
60+
61+
Returns:
62+
SpannerError if error is GoogleAPICallError and request_id is provided,
63+
otherwise returns the original error
64+
"""
65+
if isinstance(error, GoogleAPICallError) and request_id:
66+
return SpannerError(error, request_id)
67+
return error

google/cloud/spanner_v1/request_id_header.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ def with_request_id(
4343
all_metadata = (other_metadata or []).copy()
4444
all_metadata.append((REQ_ID_HEADER_KEY, req_id))
4545

46+
if span:
47+
span.set_attribute(X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR, req_id)
48+
49+
return all_metadata, req_id
50+
51+
52+
def with_request_id_metadata_only(
53+
client_id, channel_id, nth_request, attempt, other_metadata=[], span=None
54+
):
55+
req_id = build_request_id(client_id, channel_id, nth_request, attempt)
56+
all_metadata = (other_metadata or []).copy()
57+
all_metadata.append((REQ_ID_HEADER_KEY, req_id))
58+
4659
if span:
4760
span.set_attribute(X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR, req_id)
4861

0 commit comments

Comments
 (0)