Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 7fd21ca

Browse files
committed
copied files from previous branch
1 parent 4a4f80a commit 7fd21ca

4 files changed

Lines changed: 560 additions & 0 deletions

File tree

google/cloud/bigtable/data/_metrics/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import (
15+
OpenTelemetryMetricsHandler,
16+
)
17+
from google.cloud.bigtable.data._metrics.handlers.gcp_exporter import (
18+
GoogleCloudMetricsHandler,
19+
)
20+
from google.cloud.bigtable.data._metrics.handlers._stdout import _StdoutMetricsHandler
1421
from google.cloud.bigtable.data._metrics.metrics_controller import (
1522
BigtableClientSideMetricsController,
1623
)
@@ -23,6 +30,9 @@
2330

2431
__all__ = (
2532
"BigtableClientSideMetricsController",
33+
"OpenTelemetryMetricsHandler",
34+
"GoogleCloudMetricsHandler",
35+
"_StdoutMetricsHandler",
2636
"OperationType",
2737
"ActiveOperationMetric",
2838
"ActiveAttemptMetric",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler
15+
from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric
16+
17+
18+
class _StdoutMetricsHandler(MetricsHandler):
19+
"""
20+
Prints a table of metric data after each operation, for debugging purposes.
21+
"""
22+
23+
def __init__(self, **kwargs):
24+
self._completed_ops = {}
25+
26+
def on_operation_complete(self, op: CompletedOperationMetric) -> None:
27+
"""
28+
After each operation, update the state and print the metrics table.
29+
"""
30+
current_list = self._completed_ops.setdefault(op.op_type, [])
31+
current_list.append(op)
32+
self.print()
33+
34+
def print(self):
35+
"""
36+
Print the current state of the metrics table.
37+
"""
38+
print("Bigtable Metrics:")
39+
for ops_type, ops_list in self._completed_ops.items():
40+
count = len(ops_list)
41+
total_latency = sum([op.duration_ns // 1_000_000 for op in ops_list])
42+
total_attempts = sum([len(op.completed_attempts) for op in ops_list])
43+
avg_latency = total_latency / count
44+
avg_attempts = total_attempts / count
45+
print(
46+
f"{ops_type}: count: {count}, avg latency: {avg_latency:.2f} ms, avg attempts: {avg_attempts:.1f}"
47+
)
48+
print()
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
from __future__ import annotations
16+
17+
import time
18+
19+
from opentelemetry.sdk.metrics import MeterProvider
20+
from opentelemetry.sdk.metrics import view
21+
from opentelemetry.sdk.metrics.export import (
22+
HistogramDataPoint,
23+
MetricExporter,
24+
MetricExportResult,
25+
MetricsData,
26+
NumberDataPoint,
27+
PeriodicExportingMetricReader,
28+
)
29+
from google.protobuf.timestamp_pb2 import Timestamp
30+
from google.api.distribution_pb2 import Distribution
31+
from google.api.metric_pb2 import Metric as GMetric
32+
from google.api.monitored_resource_pb2 import MonitoredResource
33+
from google.api.metric_pb2 import MetricDescriptor
34+
from google.api_core import gapic_v1
35+
from google.cloud.monitoring_v3 import (
36+
CreateTimeSeriesRequest,
37+
MetricServiceClient,
38+
Point,
39+
TimeInterval,
40+
TimeSeries,
41+
TypedValue,
42+
)
43+
44+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import (
45+
OpenTelemetryMetricsHandler,
46+
)
47+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import (
48+
_OpenTelemetryInstruments,
49+
)
50+
51+
52+
# create OpenTelemetry views for Bigtable metrics
53+
# avoid reformatting into individual lines
54+
# fmt: off
55+
MILLIS_AGGREGATION = view.ExplicitBucketHistogramAggregation(
56+
[
57+
0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1, 2, 3, 4, 5, 6, 8, 10, 13, 16,
58+
20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400,
59+
500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000,
60+
]
61+
)
62+
# fmt: on
63+
COUNT_AGGREGATION = view.SumAggregation()
64+
INSTRUMENT_NAMES = (
65+
"operation_latencies",
66+
"first_response_latencies",
67+
"attempt_latencies",
68+
"retry_count",
69+
"server_latencies",
70+
"connectivity_error_count",
71+
"application_latencies",
72+
"throttling_latencies",
73+
)
74+
VIEW_LIST = [
75+
view.View(
76+
instrument_name=n,
77+
name=n,
78+
aggregation=MILLIS_AGGREGATION
79+
if n.endswith("latencies")
80+
else COUNT_AGGREGATION,
81+
)
82+
for n in INSTRUMENT_NAMES
83+
]
84+
85+
86+
class GoogleCloudMetricsHandler(OpenTelemetryMetricsHandler):
87+
"""
88+
Maintains an internal set of OpenTelemetry metrics for the Bigtable client library,
89+
and periodically exports them to Google Cloud Monitoring.
90+
91+
The OpenTelemetry metrics that are tracked are as follows:
92+
- operation_latencies: latency of each client method call, over all of it's attempts.
93+
- first_response_latencies: latency of receiving the first row in a ReadRows operation.
94+
- attempt_latencies: latency of each client attempt RPC.
95+
- retry_count: Number of additional RPCs sent after the initial attempt.
96+
- server_latencies: latency recorded on the server side for each attempt.
97+
- connectivity_error_count: number of attempts that failed to reach Google's network.
98+
- application_latencies: the time spent waiting for the application to process the next response.
99+
- throttling_latencies: latency introduced by waiting when there are too many outstanding requests in a bulk operation.
100+
101+
Args:
102+
- project_id: The Google Cloud project ID for the associated Bigtable Table
103+
- export_interval: The interval (in seconds) at which to export metrics to Cloud Monitoring.
104+
"""
105+
106+
def __init__(self, *args, project_id: str, export_interval=60, **kwargs):
107+
# internal exporter to write metrics to Cloud Monitoring
108+
exporter = _BigtableMetricsExporter(project_id=project_id)
109+
# periodically executes exporter
110+
gcp_reader = PeriodicExportingMetricReader(
111+
exporter, export_interval_millis=export_interval * 1000
112+
)
113+
# use private meter provider to store instruments and views
114+
meter_provider = MeterProvider(metric_readers=[gcp_reader], views=VIEW_LIST)
115+
otel = _OpenTelemetryInstruments(meter_provider=meter_provider)
116+
super().__init__(*args, instruments=otel, project_id=project_id, **kwargs)
117+
118+
119+
class _BigtableMetricsExporter(MetricExporter):
120+
"""
121+
OpenTelemetry Exporter implementation for sending metrics to Google Cloud Monitoring.
122+
123+
We must use a custom exporter because the public one doesn't support writing to internal
124+
metrics like `bigtable.googleapis.com/internal/client/`
125+
126+
Each GoogleCloudMetricsHandler will maintain its own exporter instance associated with the
127+
project_id it is configured with.
128+
129+
Args:
130+
- project_id: GCP project id to associate metrics with
131+
"""
132+
133+
def __init__(self, project_id: str):
134+
super().__init__()
135+
self.client = MetricServiceClient()
136+
self.prefix = "bigtable.googleapis.com/internal/client"
137+
self.project_name = self.client.common_project_path(project_id)
138+
139+
def export(
140+
self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs
141+
) -> MetricExportResult:
142+
"""
143+
Write a set of metrics to Cloud Monitoring.
144+
This method is called by the OpenTelemetry SDK
145+
"""
146+
deadline = time.time() + (timeout_millis / 1000)
147+
metric_kind = MetricDescriptor.MetricKind.CUMULATIVE
148+
all_series: list[TimeSeries] = []
149+
# process each metric from OTel format into Cloud Monitoring format
150+
for resource_metric in metrics_data.resource_metrics:
151+
for scope_metric in resource_metric.scope_metrics:
152+
for metric in scope_metric.metrics:
153+
for data_point in [
154+
pt for pt in metric.data.data_points if pt.attributes
155+
]:
156+
if data_point.attributes:
157+
project_id = data_point.attributes.get("resource_project")
158+
if not isinstance(project_id, str):
159+
# we expect string for project_id field
160+
continue
161+
monitored_resource = MonitoredResource(
162+
type="bigtable_client_raw",
163+
labels={
164+
"project_id": project_id,
165+
"instance": data_point.attributes[
166+
"resource_instance"
167+
],
168+
"cluster": data_point.attributes[
169+
"resource_cluster"
170+
],
171+
"table": data_point.attributes["resource_table"],
172+
"zone": data_point.attributes["resource_zone"],
173+
},
174+
)
175+
point = self._to_point(data_point)
176+
series = TimeSeries(
177+
resource=monitored_resource,
178+
metric_kind=metric_kind,
179+
points=[point],
180+
metric=GMetric(
181+
type=f"{self.prefix}/{metric.name}",
182+
labels={
183+
k: v
184+
for k, v in data_point.attributes.items()
185+
if not k.startswith("resource_")
186+
},
187+
),
188+
unit=metric.unit,
189+
)
190+
all_series.append(series)
191+
# send all metrics to Cloud Monitoring
192+
try:
193+
self._batch_write(all_series, deadline)
194+
return MetricExportResult.SUCCESS
195+
except Exception:
196+
return MetricExportResult.FAILURE
197+
198+
def _batch_write(
199+
self, series: list[TimeSeries], deadline=None, max_batch_size=200
200+
) -> None:
201+
"""
202+
Adapted from CloudMonitoringMetricsExporter
203+
https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/3668dfe7ce3b80dd01f42af72428de957b58b316/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L82
204+
205+
Args:
206+
- series: list of TimeSeries to write. Will be split into batches if necessary
207+
- deadline: designates the time.time() at which to stop writing. If None, uses API default
208+
- max_batch_size: maximum number of time series to write at once.
209+
Cloud Monitoring allows up to 200 per request
210+
"""
211+
write_ind = 0
212+
while write_ind < len(series):
213+
# find time left for next batch
214+
timeout = deadline - time.time() if deadline else gapic_v1.method.DEFAULT
215+
# write next batch
216+
self.client.create_service_time_series(
217+
CreateTimeSeriesRequest(
218+
name=self.project_name,
219+
time_series=series[write_ind : write_ind + max_batch_size],
220+
),
221+
timeout=timeout,
222+
)
223+
write_ind += max_batch_size
224+
225+
@staticmethod
226+
def _to_point(data_point: NumberDataPoint | HistogramDataPoint) -> Point:
227+
"""
228+
Adapted from CloudMonitoringMetricsExporter
229+
https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/3668dfe7ce3b80dd01f42af72428de957b58b316/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L82
230+
"""
231+
if isinstance(data_point, HistogramDataPoint):
232+
mean = data_point.sum / data_point.count if data_point.count else 0.0
233+
point_value = TypedValue(
234+
distribution_value=Distribution(
235+
count=data_point.count,
236+
mean=mean,
237+
bucket_counts=data_point.bucket_counts,
238+
bucket_options=Distribution.BucketOptions(
239+
explicit_buckets=Distribution.BucketOptions.Explicit(
240+
bounds=data_point.explicit_bounds,
241+
)
242+
),
243+
)
244+
)
245+
else:
246+
if isinstance(data_point.value, int):
247+
point_value = TypedValue(int64_value=data_point.value)
248+
else:
249+
point_value = TypedValue(double_value=data_point.value)
250+
start_time = Timestamp()
251+
start_time.FromNanoseconds(data_point.start_time_unix_nano)
252+
end_time = Timestamp()
253+
end_time.FromNanoseconds(data_point.time_unix_nano)
254+
interval = TimeInterval(start_time=start_time, end_time=end_time)
255+
return Point(interval=interval, value=point_value)
256+
257+
def shutdown(self, timeout_millis: float = 30_000, **kwargs):
258+
"""
259+
Adapted from CloudMonitoringMetricsExporter
260+
https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/3668dfe7ce3b80dd01f42af72428de957b58b316/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L82
261+
"""
262+
pass
263+
264+
def force_flush(self, timeout_millis: float = 10_000):
265+
"""
266+
Adapted from CloudMonitoringMetricsExporter
267+
https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/blob/3668dfe7ce3b80dd01f42af72428de957b58b316/opentelemetry-exporter-gcp-monitoring/src/opentelemetry/exporter/cloud_monitoring/__init__.py#L82
268+
"""
269+
return True

0 commit comments

Comments
 (0)