google-cloud-spanner 3.55.0__py3-none-any.whl

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 (119) hide show
  1. google/cloud/spanner.py +47 -0
  2. google/cloud/spanner_admin_database_v1/__init__.py +146 -0
  3. google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
  4. google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
  5. google/cloud/spanner_admin_database_v1/py.typed +2 -0
  6. google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
  7. google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
  8. google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
  9. google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
  10. google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
  11. google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
  12. google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
  13. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
  14. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
  15. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
  16. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
  17. google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
  18. google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
  19. google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
  20. google/cloud/spanner_admin_database_v1/types/common.py +180 -0
  21. google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
  22. google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
  23. google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
  24. google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
  25. google/cloud/spanner_admin_instance_v1/py.typed +2 -0
  26. google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
  27. google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
  28. google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
  29. google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
  30. google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
  31. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
  32. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
  33. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
  34. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
  35. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
  36. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
  37. google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
  38. google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
  39. google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
  40. google/cloud/spanner_dbapi/__init__.py +93 -0
  41. google/cloud/spanner_dbapi/_helpers.py +113 -0
  42. google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
  43. google/cloud/spanner_dbapi/checksum.py +80 -0
  44. google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
  45. google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
  46. google/cloud/spanner_dbapi/connection.py +818 -0
  47. google/cloud/spanner_dbapi/cursor.py +609 -0
  48. google/cloud/spanner_dbapi/exceptions.py +172 -0
  49. google/cloud/spanner_dbapi/parse_utils.py +392 -0
  50. google/cloud/spanner_dbapi/parsed_statement.py +63 -0
  51. google/cloud/spanner_dbapi/parser.py +258 -0
  52. google/cloud/spanner_dbapi/partition_helper.py +41 -0
  53. google/cloud/spanner_dbapi/transaction_helper.py +294 -0
  54. google/cloud/spanner_dbapi/types.py +106 -0
  55. google/cloud/spanner_dbapi/utils.py +147 -0
  56. google/cloud/spanner_dbapi/version.py +20 -0
  57. google/cloud/spanner_v1/__init__.py +154 -0
  58. google/cloud/spanner_v1/_helpers.py +751 -0
  59. google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
  60. google/cloud/spanner_v1/backup.py +397 -0
  61. google/cloud/spanner_v1/batch.py +433 -0
  62. google/cloud/spanner_v1/client.py +538 -0
  63. google/cloud/spanner_v1/data_types.py +350 -0
  64. google/cloud/spanner_v1/database.py +1968 -0
  65. google/cloud/spanner_v1/database_sessions_manager.py +249 -0
  66. google/cloud/spanner_v1/gapic_metadata.json +268 -0
  67. google/cloud/spanner_v1/gapic_version.py +16 -0
  68. google/cloud/spanner_v1/instance.py +735 -0
  69. google/cloud/spanner_v1/keyset.py +193 -0
  70. google/cloud/spanner_v1/merged_result_set.py +146 -0
  71. google/cloud/spanner_v1/metrics/constants.py +71 -0
  72. google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
  73. google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
  74. google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
  75. google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
  76. google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
  77. google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
  78. google/cloud/spanner_v1/param_types.py +110 -0
  79. google/cloud/spanner_v1/pool.py +813 -0
  80. google/cloud/spanner_v1/py.typed +2 -0
  81. google/cloud/spanner_v1/request_id_header.py +64 -0
  82. google/cloud/spanner_v1/services/__init__.py +15 -0
  83. google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
  84. google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
  85. google/cloud/spanner_v1/services/spanner/client.py +2624 -0
  86. google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
  87. google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
  88. google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
  89. google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
  90. google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
  91. google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
  92. google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
  93. google/cloud/spanner_v1/session.py +631 -0
  94. google/cloud/spanner_v1/session_options.py +133 -0
  95. google/cloud/spanner_v1/snapshot.py +1057 -0
  96. google/cloud/spanner_v1/streamed.py +402 -0
  97. google/cloud/spanner_v1/table.py +181 -0
  98. google/cloud/spanner_v1/testing/__init__.py +0 -0
  99. google/cloud/spanner_v1/testing/database_test.py +121 -0
  100. google/cloud/spanner_v1/testing/interceptors.py +118 -0
  101. google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
  102. google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
  103. google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
  104. google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
  105. google/cloud/spanner_v1/transaction.py +747 -0
  106. google/cloud/spanner_v1/types/__init__.py +118 -0
  107. google/cloud/spanner_v1/types/commit_response.py +94 -0
  108. google/cloud/spanner_v1/types/keys.py +248 -0
  109. google/cloud/spanner_v1/types/mutation.py +201 -0
  110. google/cloud/spanner_v1/types/query_plan.py +220 -0
  111. google/cloud/spanner_v1/types/result_set.py +379 -0
  112. google/cloud/spanner_v1/types/spanner.py +1815 -0
  113. google/cloud/spanner_v1/types/transaction.py +818 -0
  114. google/cloud/spanner_v1/types/type.py +288 -0
  115. google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
  116. google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
  117. google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
  118. google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
  119. google_cloud_spanner-3.55.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,384 @@
1
+ # Copyright 2025 Google LLC
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
+
16
+ from .constants import (
17
+ BUILT_IN_METRICS_METER_NAME,
18
+ NATIVE_METRICS_PREFIX,
19
+ SPANNER_RESOURCE_TYPE,
20
+ MONITORED_RESOURCE_LABELS,
21
+ METRIC_LABELS,
22
+ METRIC_NAMES,
23
+ )
24
+
25
+ import logging
26
+ from typing import Optional, List, Union, NoReturn, Tuple, Dict
27
+
28
+ import google.auth
29
+ from google.auth import credentials as ga_credentials
30
+ from google.api.distribution_pb2 import ( # pylint: disable=no-name-in-module
31
+ Distribution,
32
+ )
33
+
34
+ # pylint: disable=no-name-in-module
35
+ from google.api.metric_pb2 import ( # pylint: disable=no-name-in-module
36
+ Metric as GMetric,
37
+ MetricDescriptor,
38
+ )
39
+ from google.api.monitored_resource_pb2 import ( # pylint: disable=no-name-in-module
40
+ MonitoredResource,
41
+ )
42
+
43
+ # pylint: disable=no-name-in-module
44
+ from google.protobuf.timestamp_pb2 import Timestamp
45
+ from google.cloud.spanner_v1.gapic_version import __version__
46
+
47
+ try:
48
+ from opentelemetry.sdk.metrics.export import (
49
+ Gauge,
50
+ Histogram,
51
+ HistogramDataPoint,
52
+ Metric,
53
+ MetricExporter,
54
+ MetricExportResult,
55
+ MetricsData,
56
+ NumberDataPoint,
57
+ Sum,
58
+ )
59
+ from opentelemetry.sdk.resources import Resource
60
+ from google.cloud.monitoring_v3.services.metric_service.transports.grpc import (
61
+ MetricServiceGrpcTransport,
62
+ )
63
+ from google.cloud.monitoring_v3 import (
64
+ CreateTimeSeriesRequest,
65
+ MetricServiceClient,
66
+ Point,
67
+ TimeInterval,
68
+ TimeSeries,
69
+ TypedValue,
70
+ )
71
+
72
+ HAS_OPENTELEMETRY_INSTALLED = True
73
+ except ImportError: # pragma: NO COVER
74
+ HAS_OPENTELEMETRY_INSTALLED = False
75
+ MetricExporter = object
76
+
77
+ logger = logging.getLogger(__name__)
78
+ MAX_BATCH_WRITE = 200
79
+ MILLIS_PER_SECOND = 1000
80
+
81
+ _USER_AGENT = f"python-spanner; google-cloud-service-metric-exporter {__version__}"
82
+
83
+ # Set user-agent metadata, see https://github.com/grpc/grpc/issues/23644 and default options
84
+ # from
85
+ # https://github.com/googleapis/python-monitoring/blob/v2.11.3/google/cloud/monitoring_v3/services/metric_service/transports/grpc.py#L175-L178
86
+ _OPTIONS = [
87
+ ("grpc.max_send_message_length", -1),
88
+ ("grpc.max_receive_message_length", -1),
89
+ ("grpc.primary_user_agent", _USER_AGENT),
90
+ ]
91
+
92
+
93
+ # pylint is unable to resolve members of protobuf objects
94
+ # pylint: disable=no-member
95
+ # pylint: disable=too-many-branches
96
+ # pylint: disable=too-many-locals
97
+ class CloudMonitoringMetricsExporter(MetricExporter):
98
+ """Implementation of Metrics Exporter to Google Cloud Monitoring.
99
+
100
+ You can manually pass in project_id and client, or else the
101
+ Exporter will take that information from Application Default
102
+ Credentials.
103
+
104
+ Args:
105
+ project_id: project id of your Google Cloud project.
106
+ client: Client to upload metrics to Google Cloud Monitoring.
107
+ """
108
+
109
+ # Based on the cloud_monitoring exporter found here: https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py
110
+
111
+ def __init__(
112
+ self,
113
+ project_id: Optional[str] = None,
114
+ client: Optional["MetricServiceClient"] = None,
115
+ credentials: Optional[ga_credentials.Credentials] = None,
116
+ ):
117
+ """Initialize a custom exporter to send metrics for the Spanner Service Metrics."""
118
+ # Default preferred_temporality is all CUMULATIVE so need to customize
119
+ super().__init__()
120
+
121
+ # Create a new GRPC Client for Google Cloud Monitoring if not provided
122
+ self.client = client or MetricServiceClient(
123
+ transport=MetricServiceGrpcTransport(
124
+ channel=MetricServiceGrpcTransport.create_channel(
125
+ options=_OPTIONS,
126
+ credentials=credentials,
127
+ )
128
+ )
129
+ )
130
+
131
+ # Set project information
132
+ self.project_id: str
133
+ if not project_id:
134
+ _, default_project_id = google.auth.default()
135
+ self.project_id = str(default_project_id)
136
+ else:
137
+ self.project_id = project_id
138
+ self.project_name = self.client.common_project_path(self.project_id)
139
+
140
+ def _batch_write(self, series: List["TimeSeries"], timeout_millis: float) -> None:
141
+ """Cloud Monitoring allows writing up to 200 time series at once.
142
+
143
+ :param series: ProtoBuf TimeSeries
144
+ :return:
145
+ """
146
+ write_ind = 0
147
+ timeout = timeout_millis / MILLIS_PER_SECOND
148
+ while write_ind < len(series):
149
+ request = CreateTimeSeriesRequest(
150
+ name=self.project_name,
151
+ time_series=series[write_ind : write_ind + MAX_BATCH_WRITE],
152
+ )
153
+
154
+ self.client.create_service_time_series(
155
+ request=request,
156
+ timeout=timeout,
157
+ )
158
+ write_ind += MAX_BATCH_WRITE
159
+
160
+ @staticmethod
161
+ def _resource_to_monitored_resource_pb(
162
+ resource: "Resource", labels: Dict[str, str]
163
+ ) -> "MonitoredResource":
164
+ """
165
+ Convert the resource to a Google Cloud Monitoring monitored resource.
166
+
167
+ :param resource: OpenTelemetry resource
168
+ :param labels: labels to add to the monitored resource
169
+ :return: Google Cloud Monitoring monitored resource
170
+ """
171
+ monitored_resource = MonitoredResource(
172
+ type=SPANNER_RESOURCE_TYPE,
173
+ labels=labels,
174
+ )
175
+ return monitored_resource
176
+
177
+ @staticmethod
178
+ def _to_metric_kind(metric: "Metric") -> MetricDescriptor.MetricKind:
179
+ """
180
+ Convert the metric to a Google Cloud Monitoring metric kind.
181
+
182
+ :param metric: OpenTelemetry metric
183
+ :return: Google Cloud Monitoring metric kind
184
+ """
185
+ data = metric.data
186
+ if isinstance(data, Sum):
187
+ if data.is_monotonic:
188
+ return MetricDescriptor.MetricKind.CUMULATIVE
189
+ else:
190
+ return MetricDescriptor.MetricKind.GAUGE
191
+ elif isinstance(data, Gauge):
192
+ return MetricDescriptor.MetricKind.GAUGE
193
+ elif isinstance(data, Histogram):
194
+ return MetricDescriptor.MetricKind.CUMULATIVE
195
+ else:
196
+ # Exhaustive check
197
+ _: NoReturn = data
198
+ logger.warning(
199
+ "Unsupported metric data type %s, ignoring it",
200
+ type(data).__name__,
201
+ )
202
+ return None
203
+
204
+ @staticmethod
205
+ def _extract_metric_labels(
206
+ data_point: Union["NumberDataPoint", "HistogramDataPoint"]
207
+ ) -> Tuple[dict, dict]:
208
+ """
209
+ Extract the metric labels from the data point.
210
+
211
+ :param data_point: OpenTelemetry data point
212
+ :return: tuple of metric labels and monitored resource labels
213
+ """
214
+ metric_labels = {}
215
+ monitored_resource_labels = {}
216
+ for key, value in (data_point.attributes or {}).items():
217
+ normalized_key = _normalize_label_key(key)
218
+ val = str(value)
219
+ if key in METRIC_LABELS:
220
+ metric_labels[normalized_key] = val
221
+ if key in MONITORED_RESOURCE_LABELS:
222
+ monitored_resource_labels[normalized_key] = val
223
+ return metric_labels, monitored_resource_labels
224
+
225
+ # Unchanged from https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/main/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py
226
+ @staticmethod
227
+ def _to_point(
228
+ kind: "MetricDescriptor.MetricKind.V",
229
+ data_point: Union["NumberDataPoint", "HistogramDataPoint"],
230
+ ) -> "Point":
231
+ # Create a Google Cloud Monitoring data point value based on the OpenTelemetry metric data point type
232
+ ## For histograms, we need to calculate the mean and bucket counts
233
+ if isinstance(data_point, HistogramDataPoint):
234
+ mean = data_point.sum / data_point.count if data_point.count else 0.0
235
+ point_value = TypedValue(
236
+ distribution_value=Distribution(
237
+ count=data_point.count,
238
+ mean=mean,
239
+ bucket_counts=data_point.bucket_counts,
240
+ bucket_options=Distribution.BucketOptions(
241
+ explicit_buckets=Distribution.BucketOptions.Explicit(
242
+ bounds=data_point.explicit_bounds,
243
+ )
244
+ ),
245
+ )
246
+ )
247
+ else:
248
+ # For other metric types, we can use the data point value directly
249
+ if isinstance(data_point.value, int):
250
+ point_value = TypedValue(int64_value=data_point.value)
251
+ else:
252
+ point_value = TypedValue(double_value=data_point.value)
253
+
254
+ # DELTA case should never happen but adding it to be future proof
255
+ if (
256
+ kind is MetricDescriptor.MetricKind.CUMULATIVE
257
+ or kind is MetricDescriptor.MetricKind.DELTA
258
+ ):
259
+ # Create a Google Cloud Monitoring time interval from the OpenTelemetry data point timestamps
260
+ interval = TimeInterval(
261
+ start_time=_timestamp_from_nanos(data_point.start_time_unix_nano),
262
+ end_time=_timestamp_from_nanos(data_point.time_unix_nano),
263
+ )
264
+ else:
265
+ # For non time ranged metrics, we only need the end time
266
+ interval = TimeInterval(
267
+ end_time=_timestamp_from_nanos(data_point.time_unix_nano),
268
+ )
269
+ return Point(interval=interval, value=point_value)
270
+
271
+ @staticmethod
272
+ def _data_point_to_timeseries_pb(
273
+ data_point,
274
+ metric,
275
+ monitored_resource,
276
+ labels,
277
+ ) -> "TimeSeries":
278
+ """
279
+ Convert the data point to a Google Cloud Monitoring time series.
280
+
281
+ :param data_point: OpenTelemetry data point
282
+ :param metric: OpenTelemetry metric
283
+ :param monitored_resource: Google Cloud Monitoring monitored resource
284
+ :param labels: metric labels
285
+ :return: Google Cloud Monitoring time series
286
+ """
287
+ if metric.name not in METRIC_NAMES:
288
+ return None
289
+
290
+ kind = CloudMonitoringMetricsExporter._to_metric_kind(metric)
291
+ point = CloudMonitoringMetricsExporter._to_point(kind, data_point)
292
+ type = f"{NATIVE_METRICS_PREFIX}/{metric.name}"
293
+ series = TimeSeries(
294
+ resource=monitored_resource,
295
+ metric_kind=kind,
296
+ points=[point],
297
+ metric=GMetric(type=type, labels=labels),
298
+ unit=metric.unit or "",
299
+ )
300
+ return series
301
+
302
+ @staticmethod
303
+ def _resource_metrics_to_timeseries_pb(
304
+ metrics_data: "MetricsData",
305
+ ) -> List["TimeSeries"]:
306
+ """
307
+ Convert the metrics data to a list of Google Cloud Monitoring time series.
308
+
309
+ :param metrics_data: OpenTelemetry metrics data
310
+ :return: list of Google Cloud Monitoring time series
311
+ """
312
+ timeseries_list = []
313
+ for resource_metric in metrics_data.resource_metrics:
314
+ for scope_metric in resource_metric.scope_metrics:
315
+ # Filter for spanner builtin metrics
316
+ if scope_metric.scope.name != BUILT_IN_METRICS_METER_NAME:
317
+ continue
318
+
319
+ for metric in scope_metric.metrics:
320
+ for data_point in metric.data.data_points:
321
+ (
322
+ metric_labels,
323
+ monitored_resource_labels,
324
+ ) = CloudMonitoringMetricsExporter._extract_metric_labels(
325
+ data_point
326
+ )
327
+ monitored_resource = CloudMonitoringMetricsExporter._resource_to_monitored_resource_pb(
328
+ resource_metric.resource, monitored_resource_labels
329
+ )
330
+ timeseries = (
331
+ CloudMonitoringMetricsExporter._data_point_to_timeseries_pb(
332
+ data_point, metric, monitored_resource, metric_labels
333
+ )
334
+ )
335
+ if timeseries is not None:
336
+ timeseries_list.append(timeseries)
337
+
338
+ return timeseries_list
339
+
340
+ def export(
341
+ self,
342
+ metrics_data: "MetricsData",
343
+ timeout_millis: float = 10_000,
344
+ **kwargs,
345
+ ) -> "MetricExportResult":
346
+ """
347
+ Export the metrics data to Google Cloud Monitoring.
348
+
349
+ :param metrics_data: OpenTelemetry metrics data
350
+ :param timeout_millis: timeout in milliseconds
351
+ :return: MetricExportResult
352
+ """
353
+ if not HAS_OPENTELEMETRY_INSTALLED:
354
+ logger.warning("Metric exporter called without dependencies installed.")
355
+ return False
356
+ time_series_list = self._resource_metrics_to_timeseries_pb(metrics_data)
357
+ self._batch_write(time_series_list, timeout_millis)
358
+ return True
359
+
360
+ def force_flush(self, timeout_millis: float = 10_000) -> bool:
361
+ """Not implemented."""
362
+ return True
363
+
364
+ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
365
+ """Safely shuts down the exporter and closes all opened GRPC channels."""
366
+ self.client.transport.close()
367
+
368
+
369
+ def _timestamp_from_nanos(nanos: int) -> Timestamp:
370
+ ts = Timestamp()
371
+ ts.FromNanoseconds(nanos)
372
+ return ts
373
+
374
+
375
+ def _normalize_label_key(key: str) -> str:
376
+ """Make the key into a valid Google Cloud Monitoring label key.
377
+
378
+ See reference impl
379
+ https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/e955c204f4f2bfdc92ff0ad52786232b975efcc2/exporter/metric/metric.go#L595-L604
380
+ """
381
+ sanitized = "".join(c if c.isalpha() or c.isnumeric() else "_" for c in key)
382
+ if sanitized[0].isdigit():
383
+ sanitized = "key_" + sanitized
384
+ return sanitized
@@ -0,0 +1,156 @@
1
+ # Copyright 2025 Google LLC
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
+ """Interceptor for collecting Cloud Spanner metrics."""
16
+
17
+ from grpc_interceptor import ClientInterceptor
18
+ from .constants import (
19
+ GOOGLE_CLOUD_RESOURCE_KEY,
20
+ SPANNER_METHOD_PREFIX,
21
+ )
22
+
23
+ from typing import Dict
24
+ from .spanner_metrics_tracer_factory import SpannerMetricsTracerFactory
25
+ import re
26
+
27
+
28
+ class MetricsInterceptor(ClientInterceptor):
29
+ """Interceptor that collects metrics for Cloud Spanner operations."""
30
+
31
+ @staticmethod
32
+ def _parse_resource_path(path: str) -> dict:
33
+ """Parse the resource path to extract project, instance and database.
34
+
35
+ Args:
36
+ path (str): The resource path from the request
37
+
38
+ Returns:
39
+ dict: Extracted resource components
40
+ """
41
+ # Match paths like:
42
+ # projects/{project}/instances/{instance}/databases/{database}/sessions/{session}
43
+ # projects/{project}/instances/{instance}/databases/{database}
44
+ # projects/{project}/instances/{instance}
45
+ pattern = r"^projects/(?P<project>[^/]+)(/instances/(?P<instance>[^/]+))?(/databases/(?P<database>[^/]+))?(/sessions/(?P<session>[^/]+))?.*$"
46
+ match = re.match(pattern, path)
47
+ if match:
48
+ return {k: v for k, v in match.groupdict().items() if v is not None}
49
+ return {}
50
+
51
+ @staticmethod
52
+ def _extract_resource_from_path(metadata: Dict[str, str]) -> Dict[str, str]:
53
+ """
54
+ Extracts resource information from the metadata based on the path.
55
+
56
+ This method iterates through the metadata dictionary to find the first tuple containing the key 'google-cloud-resource-prefix'. It then extracts the path from this tuple and parses it to extract project, instance, and database information using the _parse_resource_path method.
57
+
58
+ Args:
59
+ metadata (Dict[str, str]): A dictionary containing metadata information.
60
+
61
+ Returns:
62
+ Dict[str, str]: A dictionary containing extracted project, instance, and database information.
63
+ """
64
+ # Extract resource info from the first metadata tuple containing :path
65
+ path = next(
66
+ (value for key, value in metadata if key == GOOGLE_CLOUD_RESOURCE_KEY), ""
67
+ )
68
+
69
+ resources = MetricsInterceptor._parse_resource_path(path)
70
+ return resources
71
+
72
+ @staticmethod
73
+ def _remove_prefix(s: str, prefix: str) -> str:
74
+ """
75
+ This function removes the prefix from the given string.
76
+
77
+ Args:
78
+ s (str): The string from which the prefix is to be removed.
79
+ prefix (str): The prefix to be removed from the string.
80
+
81
+ Returns:
82
+ str: The string with the prefix removed.
83
+
84
+ Note:
85
+ This function is used because the `removeprefix` method does not exist in Python 3.8.
86
+ """
87
+ if s.startswith(prefix):
88
+ return s[len(prefix) :]
89
+ return s
90
+
91
+ def _set_metrics_tracer_attributes(self, resources: Dict[str, str]) -> None:
92
+ """
93
+ Sets the metric tracer attributes based on the provided resources.
94
+
95
+ This method updates the current metric tracer's attributes with the project, instance, and database information extracted from the resources dictionary. If the current metric tracer is not set, the method does nothing.
96
+
97
+ Args:
98
+ resources (Dict[str, str]): A dictionary containing project, instance, and database information.
99
+ """
100
+ if SpannerMetricsTracerFactory.current_metrics_tracer is None:
101
+ return
102
+
103
+ if resources:
104
+ if "project" in resources:
105
+ SpannerMetricsTracerFactory.current_metrics_tracer.set_project(
106
+ resources["project"]
107
+ )
108
+ if "instance" in resources:
109
+ SpannerMetricsTracerFactory.current_metrics_tracer.set_instance(
110
+ resources["instance"]
111
+ )
112
+ if "database" in resources:
113
+ SpannerMetricsTracerFactory.current_metrics_tracer.set_database(
114
+ resources["database"]
115
+ )
116
+
117
+ def intercept(self, invoked_method, request_or_iterator, call_details):
118
+ """Intercept gRPC calls to collect metrics.
119
+
120
+ Args:
121
+ invoked_method: The RPC method
122
+ request_or_iterator: The RPC request
123
+ call_details: Details about the RPC call
124
+
125
+ Returns:
126
+ The RPC response
127
+ """
128
+ factory = SpannerMetricsTracerFactory()
129
+ if (
130
+ SpannerMetricsTracerFactory.current_metrics_tracer is None
131
+ or not factory.enabled
132
+ ):
133
+ return invoked_method(request_or_iterator, call_details)
134
+
135
+ # Setup Metric Tracer attributes from call details
136
+ ## Extract Project / Instance / Databse from header information
137
+ resources = self._extract_resource_from_path(call_details.metadata)
138
+ self._set_metrics_tracer_attributes(resources)
139
+
140
+ ## Format method to be be spanner.<method name>
141
+ method_name = self._remove_prefix(
142
+ call_details.method, SPANNER_METHOD_PREFIX
143
+ ).replace("/", ".")
144
+
145
+ SpannerMetricsTracerFactory.current_metrics_tracer.set_method(method_name)
146
+ SpannerMetricsTracerFactory.current_metrics_tracer.record_attempt_start()
147
+ response = invoked_method(request_or_iterator, call_details)
148
+ SpannerMetricsTracerFactory.current_metrics_tracer.record_attempt_completion()
149
+
150
+ # Process and send GFE metrics if enabled
151
+ if SpannerMetricsTracerFactory.current_metrics_tracer.gfe_enabled:
152
+ metadata = response.initial_metadata()
153
+ SpannerMetricsTracerFactory.current_metrics_trace.record_gfe_metrics(
154
+ metadata
155
+ )
156
+ return response