google-api-core 2.40.0__tar.gz → 2.42.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {google_api_core-2.40.0/google_api_core.egg-info → google_api_core-2.42.0}/PKG-INFO +1 -1
  2. google_api_core-2.42.0/google/api_core/_observability.py +594 -0
  3. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/method.py +23 -7
  4. google_api_core-2.42.0/google/api_core/gapic_v1/method_async.py +245 -0
  5. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/grpc_helpers_async.py +54 -1
  6. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/resumable_transfer/upload.py +15 -2
  7. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/resumable_transfer/upload_async.py +182 -80
  8. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/version.py +1 -1
  9. {google_api_core-2.40.0 → google_api_core-2.42.0/google_api_core.egg-info}/PKG-INFO +1 -1
  10. google_api_core-2.42.0/tests/asyncio/gapic/test_method_async.py +597 -0
  11. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_grpc_helpers_async.py +90 -1
  12. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_resumable_transfer_async.py +302 -3
  13. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/gapic/test_method.py +64 -19
  14. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/operations_v1/test_operations_rest_client.py +12 -16
  15. google_api_core-2.42.0/tests/unit/test_observability.py +1304 -0
  16. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_resumable_transfer.py +109 -0
  17. google_api_core-2.40.0/google/api_core/_observability.py +0 -281
  18. google_api_core-2.40.0/google/api_core/gapic_v1/method_async.py +0 -61
  19. google_api_core-2.40.0/tests/asyncio/gapic/test_method_async.py +0 -276
  20. google_api_core-2.40.0/tests/unit/test_observability.py +0 -544
  21. {google_api_core-2.40.0 → google_api_core-2.42.0}/LICENSE +0 -0
  22. {google_api_core-2.40.0 → google_api_core-2.42.0}/MANIFEST.in +0 -0
  23. {google_api_core-2.40.0 → google_api_core-2.42.0}/README.rst +0 -0
  24. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/__init__.py +0 -0
  25. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/_feature_gating_helpers.py +0 -0
  26. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/_python_package_support.py +0 -0
  27. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/_python_version_support.py +0 -0
  28. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/_rest_streaming_base.py +0 -0
  29. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/bidi.py +0 -0
  30. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/bidi_async.py +0 -0
  31. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/bidi_base.py +0 -0
  32. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/client_info.py +0 -0
  33. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/client_logging.py +0 -0
  34. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/client_options.py +0 -0
  35. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/datetime_helpers.py +0 -0
  36. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/exceptions.py +0 -0
  37. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/extended_operation.py +0 -0
  38. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/future/__init__.py +0 -0
  39. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/future/_helpers.py +0 -0
  40. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/future/async_future.py +0 -0
  41. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/future/base.py +0 -0
  42. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/future/polling.py +0 -0
  43. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/__init__.py +0 -0
  44. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/client_info.py +0 -0
  45. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/config.py +0 -0
  46. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/config_async.py +0 -0
  47. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/requests.py +0 -0
  48. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  49. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/general_helpers.py +0 -0
  50. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/grpc_helpers.py +0 -0
  51. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/iam.py +0 -0
  52. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operation.py +0 -0
  53. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operation_async.py +0 -0
  54. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/__init__.py +0 -0
  55. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  56. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  57. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  58. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/operations_client.py +0 -0
  59. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  60. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  61. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/pagers.py +0 -0
  62. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  63. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  64. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  65. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/transports/base.py +0 -0
  66. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  67. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  68. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/page_iterator.py +0 -0
  69. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/page_iterator_async.py +0 -0
  70. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/path_template.py +0 -0
  71. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/protobuf_helpers.py +0 -0
  72. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/py.typed +0 -0
  73. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/rest_helpers.py +0 -0
  74. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/rest_streaming.py +0 -0
  75. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/rest_streaming_async.py +0 -0
  76. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/resumable_transfer/__init__.py +0 -0
  77. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/resumable_transfer/common.py +0 -0
  78. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/resumable_transfer/upload_state.py +0 -0
  79. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/__init__.py +0 -0
  80. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/retry_base.py +0 -0
  81. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/retry_streaming.py +0 -0
  82. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  83. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/retry_unary.py +0 -0
  84. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry/retry_unary_async.py +0 -0
  85. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/retry_async.py +0 -0
  86. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/timeout.py +0 -0
  87. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/universe.py +0 -0
  88. {google_api_core-2.40.0 → google_api_core-2.42.0}/google/api_core/version_header.py +0 -0
  89. {google_api_core-2.40.0 → google_api_core-2.42.0}/google_api_core.egg-info/SOURCES.txt +0 -0
  90. {google_api_core-2.40.0 → google_api_core-2.42.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  91. {google_api_core-2.40.0 → google_api_core-2.42.0}/google_api_core.egg-info/requires.txt +0 -0
  92. {google_api_core-2.40.0 → google_api_core-2.42.0}/google_api_core.egg-info/top_level.txt +0 -0
  93. {google_api_core-2.40.0 → google_api_core-2.42.0}/pyproject.toml +0 -0
  94. {google_api_core-2.40.0 → google_api_core-2.42.0}/setup.cfg +0 -0
  95. {google_api_core-2.40.0 → google_api_core-2.42.0}/setup.py +0 -0
  96. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/__init__.py +0 -0
  97. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/__init__.py +0 -0
  98. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/future/__init__.py +0 -0
  99. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/future/test_async_future.py +0 -0
  100. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  101. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  102. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  103. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/retry/__init__.py +0 -0
  104. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  105. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  106. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_bidi_async.py +0 -0
  107. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_operation_async.py +0 -0
  108. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  109. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  110. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/conftest.py +0 -0
  111. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/helpers.py +0 -0
  112. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/__init__.py +0 -0
  113. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/future/__init__.py +0 -0
  114. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/future/test__helpers.py +0 -0
  115. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/future/test_polling.py +0 -0
  116. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/gapic/test_client_info.py +0 -0
  117. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/gapic/test_config.py +0 -0
  118. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/gapic/test_requests.py +0 -0
  119. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/gapic/test_routing_header.py +0 -0
  120. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/operations_v1/__init__.py +0 -0
  121. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  122. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/retry/__init__.py +0 -0
  123. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/retry/test_retry_base.py +0 -0
  124. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/retry/test_retry_imports.py +0 -0
  125. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  126. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/retry/test_retry_unary.py +0 -0
  127. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_bidi.py +0 -0
  128. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_client_info.py +0 -0
  129. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_client_logging.py +0 -0
  130. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_client_options.py +0 -0
  131. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_datetime_helpers.py +0 -0
  132. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_exceptions.py +0 -0
  133. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_extended_operation.py +0 -0
  134. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_feature_gating_helpers.py +0 -0
  135. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_grpc_helpers.py +0 -0
  136. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_iam.py +0 -0
  137. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_operation.py +0 -0
  138. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_packaging.py +0 -0
  139. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_page_iterator.py +0 -0
  140. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_path_template.py +0 -0
  141. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_protobuf_helpers.py +0 -0
  142. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_python_package_support.py +0 -0
  143. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_python_version_support.py +0 -0
  144. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_rest_helpers.py +0 -0
  145. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_rest_streaming.py +0 -0
  146. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_resumable_transfer_scenarios.py +0 -0
  147. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_timeout.py +0 -0
  148. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_universe.py +0 -0
  149. {google_api_core-2.40.0 → google_api_core-2.42.0}/tests/unit/test_version_header.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.40.0
3
+ Version: 2.42.0
4
4
  Summary: Google API client core library
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache 2.0
@@ -0,0 +1,594 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """OpenTelemetry helpers for resolving and instantiating interceptors."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import urllib.parse
22
+ from typing import TYPE_CHECKING, Any, Callable, Sequence
23
+
24
+ from google.api_core import _feature_gating_helpers
25
+ from google.api_core.client_options import ClientOptions
26
+
27
+ if TYPE_CHECKING:
28
+ # flake8: grpc, trace, and ClientInterceptor are imported only for static analysis and type annotations
29
+ # The 'noqa: F401' comment avoids flake8 "imported but not used" errors.
30
+ import grpc # noqa: F401
31
+ import opentelemetry.trace # noqa: F401
32
+
33
+ from google.api_core.grpc_helpers import ClientInterceptor # noqa: F401
34
+
35
+ _TRACER_PROVIDER = "tracer_provider"
36
+
37
+
38
+ def is_otel_capabilities_enabled(
39
+ client_options: ClientOptions | dict[str, Any] | None = None,
40
+ env_var: str = "GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED",
41
+ ) -> bool:
42
+ """Checks if OTel capabilities are enabled and installed.
43
+
44
+ Args:
45
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options
46
+ object or dictionary.
47
+ env_var (str): The environment variable to check for enablement.
48
+
49
+ Returns:
50
+ bool: True if enabled and installed, False otherwise.
51
+ """
52
+ is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags(
53
+ env_var=env_var,
54
+ feature_key=_TRACER_PROVIDER,
55
+ configuration=client_options,
56
+ )
57
+
58
+ if is_tracing_enabled:
59
+ try:
60
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] # noqa: F401
61
+
62
+ return True
63
+ except ImportError:
64
+ pass
65
+
66
+ return False
67
+
68
+
69
+ def _extract_endpoint_attributes(
70
+ client_options: ClientOptions | dict[str, Any] | None = None,
71
+ ) -> dict[str, Any]:
72
+ """Extracts server.address, server.port (if non-default), and url.domain from client options if present.
73
+
74
+ Args:
75
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options
76
+ object or dictionary.
77
+
78
+ Returns:
79
+ dict[str, Any]: A dictionary containing url.domain and, if an api_endpoint is configured,
80
+ server.address and non-default server.port.
81
+ """
82
+ attrs: dict[str, Any] = {}
83
+ endpoint = None
84
+ universe_domain = None
85
+
86
+ if isinstance(client_options, dict):
87
+ endpoint = client_options.get("api_endpoint")
88
+ universe_domain = client_options.get("universe_domain")
89
+ elif client_options is not None:
90
+ endpoint = getattr(client_options, "api_endpoint", None)
91
+ universe_domain = getattr(client_options, "universe_domain", None)
92
+
93
+ attrs["url.domain"] = universe_domain or "googleapis.com"
94
+
95
+ if endpoint and isinstance(endpoint, str):
96
+ target = endpoint if "//" in endpoint else f"//{endpoint}"
97
+ parsed = None
98
+ hostname = None
99
+ port = None
100
+ try:
101
+ parsed = urllib.parse.urlsplit(target)
102
+ hostname = parsed.hostname
103
+ port = parsed.port
104
+ except ValueError:
105
+ pass
106
+
107
+ if hostname:
108
+ attrs["server.address"] = hostname
109
+ if port and parsed:
110
+ scheme = parsed.scheme.lower()
111
+ is_default_port = (port == 443 and scheme in ("https", "")) or (
112
+ port == 80 and scheme == "http"
113
+ )
114
+ if not is_default_port:
115
+ attrs["server.port"] = port
116
+ return attrs
117
+
118
+
119
+ def _make_grpc_client_request_hook(
120
+ endpoint_attrs: dict[str, Any] | None = None,
121
+ ) -> Callable[[Any, Any], None]:
122
+ """Creates an OpenTelemetry gRPC client request hook with optional endpoint attributes.
123
+
124
+ Args:
125
+ endpoint_attrs (Optional[dict[str, Any]]): Optional static endpoint attributes to attach
126
+ to every span.
127
+
128
+ Returns:
129
+ Callable[[Any, Any], None]: The request hook callback.
130
+ """
131
+ static_attrs = dict(endpoint_attrs) if endpoint_attrs else {}
132
+
133
+ def client_request_hook(span: Any, request: Any) -> None:
134
+ if span is None or not getattr(span, "is_recording", lambda: True)():
135
+ return
136
+
137
+ # Upstream opentelemetry-instrumentation-grpc may format span names with a
138
+ # leading slash (e.g. "/package.Service/Method"). Normalize the span name
139
+ # and ensure rpc.method is always captured as the clean, fully-qualified name.
140
+ span_name = getattr(span, "name", None)
141
+ if isinstance(span_name, str) and span_name:
142
+ clean_method_name = span_name.lstrip("/")
143
+ if span_name.startswith("/") and hasattr(span, "update_name"):
144
+ span.update_name(clean_method_name)
145
+ span.set_attribute("rpc.method", clean_method_name)
146
+
147
+ attrs: dict[str, Any] = {
148
+ "rpc.system.name": "grpc",
149
+ }
150
+ if static_attrs:
151
+ attrs.update(static_attrs)
152
+ for key, value in attrs.items():
153
+ span.set_attribute(key, value)
154
+
155
+ return client_request_hook
156
+
157
+
158
+ _grpc_client_request_hook = _make_grpc_client_request_hook()
159
+
160
+
161
+ def _grpc_client_response_hook(span: Any, response: Any) -> None:
162
+ """OpenTelemetry gRPC client response hook to record successful response status.
163
+
164
+ Upstream ``opentelemetry-instrumentation-grpc`` sets the integer status code
165
+ ``rpc.grpc.status_code`` (e.g. 0), but does not record the modern string status
166
+ ``rpc.response.status_code`` (e.g. "OK") required by Cloud Trace and current
167
+ OpenTelemetry semantic conventions (v1.27.0+).
168
+
169
+ This hook enriches successful RPC attempt spans with ``rpc.response.status_code = "OK"``.
170
+ Errors and non-OK statuses are handled at the Tier 3 method span layer or upstream.
171
+
172
+ Upstream handles synchronous and asynchronous invocations differently:
173
+ - **Synchronous gRPC**: Upstream only invokes the response hook when an RPC call
174
+ succeeds. On failure, the hook is bypassed entirely.
175
+ - **Asynchronous gRPC**: Upstream invokes the response hook unconditionally for
176
+ both successes and failures (passing exception details on error). However, it
177
+ always marks ``span.status`` with an error status before calling the hook.
178
+
179
+ Because of this disparity, this hook checks ``span.status`` to guard against
180
+ async failure callbacks while allowing synchronous and successful asynchronous
181
+ calls to be marked "OK".
182
+
183
+ Note:
184
+ If upstream ``opentelemetry-instrumentation-grpc`` adds native support for
185
+ modern ``rpc.response.status_code`` in future releases, this hook can be retired.
186
+
187
+ Args:
188
+ span (Optional[Any]): The OpenTelemetry span.
189
+ response (Any): The gRPC response object or details.
190
+ """
191
+ if span is None or not getattr(span, "is_recording", lambda: False)():
192
+ return
193
+
194
+ # Guard against upstream async calls that invoke this hook on failures.
195
+ status = getattr(span, "status", None)
196
+ status_code = getattr(status, "status_code", None)
197
+ if (
198
+ getattr(status_code, "name", None) == "ERROR"
199
+ or getattr(status_code, "value", None) == 2
200
+ ):
201
+ return
202
+
203
+ span.set_attribute("rpc.response.status_code", "OK")
204
+
205
+
206
+ def _get_tracer_provider(
207
+ client_options: ClientOptions | dict[str, Any] | None = None,
208
+ ) -> opentelemetry.trace.TracerProvider | None:
209
+ """Extracts the OpenTelemetry tracer provider from client options if present.
210
+
211
+ Args:
212
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options
213
+ object or dictionary.
214
+
215
+ Returns:
216
+ opentelemetry.trace.TracerProvider | None: The tracer provider if present,
217
+ None otherwise.
218
+ """
219
+ if isinstance(client_options, dict):
220
+ return client_options.get(_TRACER_PROVIDER)
221
+ elif client_options is not None:
222
+ return getattr(client_options, _TRACER_PROVIDER, None)
223
+ return None
224
+
225
+
226
+ def get_otel_interceptor(
227
+ client_options: ClientOptions | dict[str, Any] | None = None,
228
+ ) -> Callable[[grpc.Channel], grpc.Channel] | None:
229
+ """Returns an interceptor callable that wraps a sync gRPC channel with OpenTelemetry tracing.
230
+
231
+ Args:
232
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options
233
+ object or dictionary used for feature gating and extracting the tracer provider.
234
+
235
+ Returns:
236
+ Callable[[grpc.Channel], grpc.Channel] | None: An interceptor callable if OpenTelemetry
237
+ tracing is enabled and installed, None otherwise.
238
+ """
239
+ if not is_otel_capabilities_enabled(client_options):
240
+ return None
241
+
242
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
243
+
244
+ endpoint_attrs = _extract_endpoint_attributes(client_options)
245
+ request_hook = _make_grpc_client_request_hook(endpoint_attrs)
246
+
247
+ interceptor: ClientInterceptor = otel_grpc.client_interceptor(
248
+ tracer_provider=_get_tracer_provider(client_options),
249
+ request_hook=request_hook,
250
+ response_hook=_grpc_client_response_hook,
251
+ )
252
+
253
+ def otel_interceptor(channel: grpc.Channel) -> grpc.Channel:
254
+ return otel_grpc.intercept_channel(channel, interceptor)
255
+
256
+ otel_interceptor._is_otel_interceptor = True # type: ignore[attr-defined]
257
+ return otel_interceptor
258
+
259
+
260
+ def get_otel_async_interceptor(
261
+ client_options: ClientOptions | dict[str, Any] | None = None,
262
+ ) -> Sequence[grpc.aio.ClientInterceptor] | None:
263
+ """Returns async gRPC client interceptors for OpenTelemetry tracing.
264
+
265
+ Args:
266
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options
267
+ object or dictionary used for feature gating and extracting the tracer provider.
268
+
269
+ Returns:
270
+ Sequence[grpc.aio.ClientInterceptor] | None: Instantiated OpenTelemetry async
271
+ client interceptors if tracing is enabled and installed, None otherwise.
272
+ """
273
+ if not is_otel_capabilities_enabled(client_options):
274
+ return None
275
+
276
+ # Ignored by mypy: Optional dependency only loaded if early-return is skipped
277
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
278
+
279
+ endpoint_attrs = _extract_endpoint_attributes(client_options)
280
+ request_hook = _make_grpc_client_request_hook(endpoint_attrs)
281
+
282
+ return otel_grpc.aio_client_interceptors(
283
+ tracer_provider=_get_tracer_provider(client_options),
284
+ request_hook=request_hook,
285
+ response_hook=_grpc_client_response_hook,
286
+ )
287
+
288
+
289
+ _TRACE_CONTEXT_PROPAGATOR: Any = None
290
+
291
+
292
+ class _TraceContext:
293
+ """Context manager for tracing an HTTP wire request with OpenTelemetry.
294
+
295
+ Manages span creation, semantic attribute attachment, W3C traceparent injection,
296
+ and automatic error recording on failure without suppressing caller exceptions.
297
+
298
+ Supports two calling conventions:
299
+ - Pass a single `request` object (such as `requests.PreparedRequest`).
300
+ - Pass explicit keyword arguments (`method`, `url`, `headers`, `body`, `client_options`).
301
+ """
302
+
303
+ @staticmethod
304
+ def _get_trace_context_propagator() -> Any:
305
+ global _TRACE_CONTEXT_PROPAGATOR
306
+ if _TRACE_CONTEXT_PROPAGATOR is None:
307
+ from opentelemetry.trace.propagation.tracecontext import ( # type: ignore[import-not-found]
308
+ TraceContextTextMapPropagator,
309
+ )
310
+
311
+ _TRACE_CONTEXT_PROPAGATOR = TraceContextTextMapPropagator()
312
+ return _TRACE_CONTEXT_PROPAGATOR
313
+
314
+ # The HTTP tracing context manager deliberately supports two distinct invocation styles:
315
+ # 1. Bundled Request Object: `trace_http_request(request, ...)`
316
+ # Used when callers already possess an HTTP request instance (such as
317
+ # requests.PreparedRequest or urllib.request.Request) with `.method`, `.url`, etc.
318
+ # 2. Unpacked Keyword Arguments: `trace_http_request(method=..., url=..., headers=..., body=...)`
319
+ # Used by generated GAPIC REST transports (_shared_macros.j2).
320
+ # In GAPIC templates, requests are assembled from local strings and dictionaries before
321
+ # hitting the session. Supporting keyword arguments avoids the CPU and memory overhead
322
+ # of instantiating a throwaway dummy request object on every single RPC execution.
323
+ @staticmethod
324
+ def _build_http_span_attributes(
325
+ request: Any = None,
326
+ *,
327
+ method: str | None = None,
328
+ url: str | None = None,
329
+ url_template: str | None = None,
330
+ headers: dict[str, Any] | None = None,
331
+ body: Any = None,
332
+ client_options: ClientOptions | dict[str, Any] | None = None,
333
+ ) -> tuple[str, dict[str, Any], Any]:
334
+ """Extract span name, semantic attributes dictionary, and resolved headers.
335
+
336
+ Supports two calling conventions:
337
+ - Pass a single `request` object (such as `requests.PreparedRequest`).
338
+ - Pass explicit keyword arguments (`method`, `url`, `headers`, `body`, `client_options`).
339
+
340
+ Returns:
341
+ tuple[str, dict[str, Any], Any]: A tuple of (span_name, attributes, resolved_headers).
342
+ """
343
+ if request is not None:
344
+ resolved_method = getattr(request, "method", "HTTP") or "HTTP"
345
+ resolved_url = getattr(request, "url", "") or ""
346
+ resolved_headers = getattr(request, "headers", None)
347
+ resolved_body = getattr(request, "body", None)
348
+ else:
349
+ resolved_method = method or "HTTP"
350
+ resolved_url = url or ""
351
+ resolved_headers = headers
352
+ resolved_body = body
353
+
354
+ resolved_method = resolved_method.upper()
355
+ endpoint_attrs = _extract_endpoint_attributes(client_options)
356
+
357
+ server_address = endpoint_attrs.get("server.address")
358
+ server_port = endpoint_attrs.get("server.port")
359
+ if not server_address and resolved_url:
360
+ try:
361
+ parsed = urllib.parse.urlsplit(resolved_url)
362
+ server_address = parsed.hostname
363
+ if not server_port and parsed.port:
364
+ server_port = parsed.port
365
+ except Exception: # Fail-open on malformed URL parsing
366
+ pass
367
+
368
+ span_name = resolved_method
369
+ span_attributes: dict[str, Any] = {
370
+ "http.request.method": resolved_method,
371
+ "server.address": server_address or "",
372
+ "server.port": server_port or 443,
373
+ "url.domain": endpoint_attrs.get("url.domain", "googleapis.com"),
374
+ }
375
+ if url_template:
376
+ span_attributes["url.template"] = url_template
377
+ if resolved_url:
378
+ span_attributes["url.full"] = resolved_url
379
+
380
+ if resolved_body is not None and isinstance(resolved_body, (bytes, str)):
381
+ span_attributes["http.request.body.size"] = len(resolved_body)
382
+
383
+ return span_name, span_attributes, resolved_headers
384
+
385
+ def __init__(
386
+ self,
387
+ request: Any = None,
388
+ *,
389
+ method: str | None = None,
390
+ url: str | None = None,
391
+ url_template: str | None = None,
392
+ headers: dict[str, Any] | None = None,
393
+ body: Any = None,
394
+ client_options: ClientOptions | dict[str, Any] | None = None,
395
+ ):
396
+ self._request = request
397
+ self._method = method
398
+ self._url = url
399
+ self._url_template = url_template
400
+ self._headers = headers
401
+ self._body = body
402
+ self._client_options = client_options
403
+ self._span: Any = None
404
+ self._cm: Any = None
405
+
406
+ def record_response(self, response: Any) -> None:
407
+ """Record HTTP response attributes on the active span.
408
+
409
+ Args:
410
+ response (Any): The HTTP response object (e.g. requests.Response).
411
+ """
412
+ span = self._span
413
+ if span is None or not hasattr(span, "set_attribute"):
414
+ return
415
+
416
+ try:
417
+ from opentelemetry.trace.status import ( # type: ignore[import-not-found]
418
+ Status,
419
+ StatusCode,
420
+ )
421
+
422
+ status_code = getattr(
423
+ response, "status_code", getattr(response, "status", None)
424
+ )
425
+ if status_code is not None:
426
+ span.set_attribute("http.response.status_code", int(status_code))
427
+ if int(status_code) >= 400:
428
+ span.set_status(Status(StatusCode.ERROR))
429
+ else:
430
+ span.set_status(Status(StatusCode.OK))
431
+
432
+ headers = getattr(response, "headers", None)
433
+ if headers and "Content-Length" in headers:
434
+ try:
435
+ span.set_attribute(
436
+ "http.response.body.size", int(headers["Content-Length"])
437
+ )
438
+ except (ValueError, TypeError):
439
+ pass
440
+ elif hasattr(response, "_content") and response._content is not None:
441
+ try:
442
+ span.set_attribute(
443
+ "http.response.body.size", len(response._content)
444
+ )
445
+ except Exception:
446
+ pass
447
+ except Exception:
448
+ pass
449
+
450
+ # Alias for method name parity with Daniel's suggested convention
451
+ record_http_response = record_response
452
+
453
+ def record_error(self, exc: BaseException | None) -> None:
454
+ """Record supplemental Cloud Observability error attributes on the active span.
455
+
456
+ Upstream OpenTelemetry's ``start_as_current_span`` context manager automatically
457
+ records the exception event and marks the span status as ERROR when an unhandled
458
+ exception escapes (via its default ``record_exception=True`` and
459
+ ``set_status_on_exception=True`` settings).
460
+
461
+ This method supplements that base OpenTelemetry behavior by enriching the span
462
+ with semantic attributes required by Cloud Trace and the Google Cloud Observability
463
+ specification:
464
+ - ``error.type``: Low-cardinality status code string (e.g. "404") or exception class name.
465
+ - ``status.message``: Error description message.
466
+
467
+ Args:
468
+ exc (Optional[BaseException]): The exception raised during dispatch.
469
+ """
470
+ span = self._span
471
+ if span is None or exc is None:
472
+ return
473
+
474
+ try:
475
+ if hasattr(span, "set_attribute"):
476
+ status_code = getattr(exc, "code", None) or getattr(
477
+ exc, "status_code", None
478
+ )
479
+ if status_code:
480
+ span.set_attribute("error.type", str(status_code))
481
+ else:
482
+ span.set_attribute("error.type", exc.__class__.__name__)
483
+ msg = str(exc)
484
+ if msg:
485
+ span.set_attribute("status.message", msg)
486
+ except Exception: # Fail-open on error attribute extraction failure
487
+ pass
488
+
489
+ # Alias for method name parity with Daniel's suggested convention
490
+ record_http_error = record_error
491
+
492
+ def __enter__(self) -> "_TraceContext":
493
+ if not is_otel_capabilities_enabled(self._client_options):
494
+ return self
495
+
496
+ try:
497
+ from opentelemetry import trace
498
+
499
+ tracer_provider = _get_tracer_provider(self._client_options)
500
+ if tracer_provider is not None:
501
+ tracer = tracer_provider.get_tracer("google.api_core")
502
+ else:
503
+ tracer = trace.get_tracer("google.api_core")
504
+
505
+ span_name, span_attributes, resolved_headers = (
506
+ self._build_http_span_attributes(
507
+ self._request,
508
+ method=self._method,
509
+ url=self._url,
510
+ url_template=self._url_template,
511
+ headers=self._headers,
512
+ body=self._body,
513
+ client_options=self._client_options,
514
+ )
515
+ )
516
+
517
+ self._cm = tracer.start_as_current_span(
518
+ span_name,
519
+ kind=trace.SpanKind.CLIENT,
520
+ attributes=span_attributes,
521
+ )
522
+ self._span = self._cm.__enter__()
523
+
524
+ if resolved_headers is not None and hasattr(
525
+ resolved_headers, "__setitem__"
526
+ ):
527
+ try:
528
+ self._get_trace_context_propagator().inject(resolved_headers)
529
+ except Exception: # Fail-open on header injection failure
530
+ pass
531
+
532
+ return self
533
+ except Exception:
534
+ # Fail-open: telemetry failures must never disrupt core RPC execution
535
+ return self
536
+
537
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
538
+ if self._cm is not None:
539
+ try:
540
+ if exc_val is not None and self._span is not None:
541
+ self.record_error(exc_val)
542
+ finally:
543
+ self._cm.__exit__(exc_type, exc_val, exc_tb)
544
+ # Always return None so caller exceptions are never suppressed
545
+ return None
546
+
547
+
548
+ def trace_http_request(
549
+ request: Any = None,
550
+ *,
551
+ method: str | None = None,
552
+ url: str | None = None,
553
+ url_template: str | None = None,
554
+ headers: dict[str, Any] | None = None,
555
+ body: Any = None,
556
+ client_options: ClientOptions | dict[str, Any] | None = None,
557
+ ) -> _TraceContext:
558
+ """Context manager factory for tracing an HTTP wire request with OpenTelemetry.
559
+
560
+ Manages span creation, semantic attribute attachment, W3C traceparent injection,
561
+ and automatic error recording on failure without suppressing caller exceptions.
562
+
563
+ Supports two calling conventions:
564
+ 1. Bundled Request Object: `trace_http_request(request, ...)`
565
+ Used when callers already possess an HTTP request instance (such as
566
+ `requests.PreparedRequest` or `urllib.request.Request`) with `.method`, `.url`, etc.
567
+ 2. Unpacked Keyword Arguments: `trace_http_request(method=..., url=..., headers=..., body=...)`
568
+ Used by generated GAPIC REST transports (_shared_macros.j2).
569
+ In GAPIC templates, requests are assembled from local strings and dictionaries before
570
+ hitting the session. Supporting keyword arguments avoids the CPU and memory overhead
571
+ of instantiating a throwaway dummy request object on every single RPC execution.
572
+
573
+ Args:
574
+ request (Optional[Any]): Bundled HTTP request object with .method, .url, .headers.
575
+ method (Optional[str]): Explicit HTTP method (e.g. "GET", "POST").
576
+ url (Optional[str]): Fully qualified request URL.
577
+ url_template (Optional[str]): Parameterized path template for url.template.
578
+ headers (Optional[dict[str, Any]]): Mutable dictionary of request headers.
579
+ body (Optional[Any]): Request payload (used for body size calculation).
580
+ client_options (Optional[Union[ClientOptions, dict[str, Any]]]): Client options
581
+ containing custom endpoint and tracer provider configurations.
582
+
583
+ Returns:
584
+ _TraceContext: An active context manager managing the HTTP client span.
585
+ """
586
+ return _TraceContext(
587
+ request=request,
588
+ method=method,
589
+ url=url,
590
+ url_template=url_template,
591
+ headers=headers,
592
+ body=body,
593
+ client_options=client_options,
594
+ )
@@ -216,11 +216,25 @@ def _extract_error_attributes(exc: Optional[Exception]) -> dict[str, Any]:
216
216
  reason = getattr(source, "reason", None)
217
217
  if reason:
218
218
  attrs["error.type"] = reason
219
+ else:
220
+ # Fallback per OpenTelemetry Semantic Conventions: every failed span should record
221
+ # a low-cardinality error.type. Use canonical status code name or exception class name.
222
+ status_code = _extract_status_code(target_exc)
223
+ attrs["error.type"] = status_code or target_exc.__class__.__name__
219
224
  metadata = getattr(source, "metadata", None)
220
225
  if metadata:
221
226
  for k, v in metadata.items():
222
227
  attrs[f"gcp.errors.metadata.{k}"] = str(v)
223
228
 
229
+ # 5. Extract human-readable error description for cross-language PRD parity
230
+ message = getattr(target_exc, "message", None)
231
+ if not message and hasattr(target_exc, "details") and callable(target_exc.details):
232
+ message = target_exc.details()
233
+ if not message and isinstance(target_exc, Exception):
234
+ message = str(target_exc)
235
+ if message:
236
+ attrs["status.message"] = str(message)
237
+
224
238
  return attrs
225
239
 
226
240
 
@@ -254,7 +268,7 @@ class _GapicCallable(object):
254
268
  client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
255
269
  Client information used for metadata headers. Defaults to None.
256
270
  kind (str): The transport kind for the RPC method. Defaults to "grpc".
257
- Allowed values for OpenTelemetry method tracing are "grpc" and "grpc_asyncio".
271
+ Allowed values for OpenTelemetry method tracing are "grpc" and "rest".
258
272
  """
259
273
 
260
274
  def __init__(
@@ -291,7 +305,7 @@ class _GapicCallable(object):
291
305
  self._start_span_fn = None
292
306
  if (
293
307
  not is_streaming
294
- and kind == "grpc"
308
+ and kind in ("grpc", "rest")
295
309
  and method_name is not None
296
310
  and _observability.is_otel_capabilities_enabled(client_options)
297
311
  ):
@@ -309,8 +323,9 @@ class _GapicCallable(object):
309
323
  tracer = trace.get_tracer("google.api_core")
310
324
 
311
325
  span_name, _, _ = _extract_rpc_identity(method_name)
326
+ is_rest = kind in ("rest", "rest_asyncio")
312
327
  span_attributes = {
313
- "rpc.system.name": "grpc",
328
+ "rpc.system.name": "http" if is_rest else "grpc",
314
329
  "rpc.method": span_name,
315
330
  }
316
331
  self._start_span_fn = functools.partial(
@@ -358,14 +373,16 @@ class _GapicCallable(object):
358
373
  elif self._default_metadata:
359
374
  kwargs["metadata"] = self._default_metadata
360
375
 
361
- if self._compression is not None:
376
+ if compression is not None:
362
377
  kwargs["compression"] = compression
363
378
 
364
379
  span_cm = contextlib.nullcontext()
365
380
  if self._start_span_fn is not None:
366
381
  try:
367
382
  span_cm = self._start_span_fn()
368
- except Exception:
383
+ except (
384
+ Exception
385
+ ): # Fail-open: proceed without span if tracing initialization fails
369
386
  span_cm = contextlib.nullcontext()
370
387
 
371
388
  with span_cm as span:
@@ -489,8 +506,7 @@ def wrap_method(
489
506
  is_streaming (bool): Whether the RPC method is streaming. Defaults to False.
490
507
  Streaming methods are currently gated and do not generate Tier 3 spans.
491
508
  kind (str): The transport kind for the RPC method. Defaults to "grpc".
492
- Non-gRPC transports (e.g. "rest") are currently gated and do not generate
493
- Tier 3 method spans.
509
+ Allowed values for OpenTelemetry method tracing are "grpc" and "rest".
494
510
 
495
511
  Returns:
496
512
  Callable: A new callable that takes optional ``retry``, ``timeout``,