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.
- google/cloud/spanner.py +47 -0
- google/cloud/spanner_admin_database_v1/__init__.py +146 -0
- google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
- google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_database_v1/py.typed +2 -0
- google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
- google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
- google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
- google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
- google/cloud/spanner_admin_database_v1/types/common.py +180 -0
- google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
- google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
- google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
- google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_instance_v1/py.typed +2 -0
- google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
- google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
- google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
- google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
- google/cloud/spanner_dbapi/__init__.py +93 -0
- google/cloud/spanner_dbapi/_helpers.py +113 -0
- google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
- google/cloud/spanner_dbapi/checksum.py +80 -0
- google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
- google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
- google/cloud/spanner_dbapi/connection.py +818 -0
- google/cloud/spanner_dbapi/cursor.py +609 -0
- google/cloud/spanner_dbapi/exceptions.py +172 -0
- google/cloud/spanner_dbapi/parse_utils.py +392 -0
- google/cloud/spanner_dbapi/parsed_statement.py +63 -0
- google/cloud/spanner_dbapi/parser.py +258 -0
- google/cloud/spanner_dbapi/partition_helper.py +41 -0
- google/cloud/spanner_dbapi/transaction_helper.py +294 -0
- google/cloud/spanner_dbapi/types.py +106 -0
- google/cloud/spanner_dbapi/utils.py +147 -0
- google/cloud/spanner_dbapi/version.py +20 -0
- google/cloud/spanner_v1/__init__.py +154 -0
- google/cloud/spanner_v1/_helpers.py +751 -0
- google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
- google/cloud/spanner_v1/backup.py +397 -0
- google/cloud/spanner_v1/batch.py +433 -0
- google/cloud/spanner_v1/client.py +538 -0
- google/cloud/spanner_v1/data_types.py +350 -0
- google/cloud/spanner_v1/database.py +1968 -0
- google/cloud/spanner_v1/database_sessions_manager.py +249 -0
- google/cloud/spanner_v1/gapic_metadata.json +268 -0
- google/cloud/spanner_v1/gapic_version.py +16 -0
- google/cloud/spanner_v1/instance.py +735 -0
- google/cloud/spanner_v1/keyset.py +193 -0
- google/cloud/spanner_v1/merged_result_set.py +146 -0
- google/cloud/spanner_v1/metrics/constants.py +71 -0
- google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
- google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
- google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
- google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
- google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
- google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
- google/cloud/spanner_v1/param_types.py +110 -0
- google/cloud/spanner_v1/pool.py +813 -0
- google/cloud/spanner_v1/py.typed +2 -0
- google/cloud/spanner_v1/request_id_header.py +64 -0
- google/cloud/spanner_v1/services/__init__.py +15 -0
- google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
- google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
- google/cloud/spanner_v1/services/spanner/client.py +2624 -0
- google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
- google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
- google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
- google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
- google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
- google/cloud/spanner_v1/session.py +631 -0
- google/cloud/spanner_v1/session_options.py +133 -0
- google/cloud/spanner_v1/snapshot.py +1057 -0
- google/cloud/spanner_v1/streamed.py +402 -0
- google/cloud/spanner_v1/table.py +181 -0
- google/cloud/spanner_v1/testing/__init__.py +0 -0
- google/cloud/spanner_v1/testing/database_test.py +121 -0
- google/cloud/spanner_v1/testing/interceptors.py +118 -0
- google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
- google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
- google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
- google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
- google/cloud/spanner_v1/transaction.py +747 -0
- google/cloud/spanner_v1/types/__init__.py +118 -0
- google/cloud/spanner_v1/types/commit_response.py +94 -0
- google/cloud/spanner_v1/types/keys.py +248 -0
- google/cloud/spanner_v1/types/mutation.py +201 -0
- google/cloud/spanner_v1/types/query_plan.py +220 -0
- google/cloud/spanner_v1/types/result_set.py +379 -0
- google/cloud/spanner_v1/types/spanner.py +1815 -0
- google/cloud/spanner_v1/types/transaction.py +818 -0
- google/cloud/spanner_v1/types/type.py +288 -0
- google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
- google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
- google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
- google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
- 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
|