Skip to content

feat(bigtable): client side metrics handlers - #16760

Open
daniel-sanche wants to merge 51 commits into
mainfrom
bigtable_csm_3_handlers
Open

feat(bigtable): client side metrics handlers#16760
daniel-sanche wants to merge 51 commits into
mainfrom
bigtable_csm_3_handlers

Conversation

@daniel-sanche

@daniel-sanche daniel-sanche commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Migrate googleapis/python-bigtable#1189 to the monorepo

This PR builds off of googleapis/python-bigtable#1187 to add handlers to the client-side metrics system, which can subscribe to the metrics stream, and export the results into different collection systems

We add two handlers to the system:

  • GoogleCloudMetricsHandler: sends metrics to a private OpenTelemetry meter, and then periodically exports them to GCP. Built on top of OpenTelemetryMetricsHandler
  • OpenTelemetryMetricsHandler: sends metrics to the root MeterProvider, so the user can access the exported metrics for their own systems. This will be off by default, but can be added alongside GoogleCloudMetricsHandler if needed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements client-side metrics for the Bigtable library using OpenTelemetry, including a custom exporter for Google Cloud Monitoring. The review feedback focuses on improving resource efficiency by moving the MeterProvider and client_uid generation to the client level to avoid thread leaks and inconsistent identifiers across tables. Additionally, recommendations were made to handle potential KeyError exceptions in the exporter, improve logging for background export failures, and ensure non-negative timeouts during batch writes.

Comment thread packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/client.py Outdated
write_ind = 0
while write_ind < len(series):
# find time left for next batch
timeout = deadline - time.time() if deadline else gapic_v1.method.DEFAULT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to calculate the remaining deadline for each batch? I think each batch should just use the default timeout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention here is to be consistent with the OpenTelemetry exporter spec, which passes in a timeout value for exporters to conform to.

I'm not entirely sure what happens if we ignore it, but I can look into it if needed. Does Java not have anything similar?

Base automatically changed from bigtable_csm_2_instrumentation_advanced to main June 5, 2026 15:54
@daniel-sanche

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces client-side metrics collection and exporting to Google Cloud Monitoring for the Bigtable Python client by adding GoogleCloudMetricsHandler and BigtableMetricsExporter. Feedback on the changes highlights several critical issues, including potential KeyError and TypeError exceptions during metric label processing and protobuf serialization, broad exception handling that silently masks export failures, and side-effects from instantiating OpenTelemetry instruments as default arguments. Additionally, corrections are needed in the system tests to resolve mismatches in metric names and type prefixes.

daniel-sanche and others added 3 commits July 23, 2026 17:13
…rics/handlers/opentelemetry.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…rics/handlers/gcp_exporter.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@daniel-sanche daniel-sanche changed the title [DRAFT] feat: client side metrics handlers feat: client side metrics handlers Jul 24, 2026
@daniel-sanche
daniel-sanche marked this pull request as ready for review July 28, 2026 22:02
@daniel-sanche
daniel-sanche requested a review from a team as a code owner July 28, 2026 22:02
self.meter_provider = MeterProvider(
metric_readers=[gcp_reader], views=VIEW_LIST
)
otel = _OpenTelemetryInstruments(meter_provider=self.meter_provider)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this should support application has it's own otel instrumentation. Is there an example of how that can be done somewhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I designed the architecture to work well with user-provided otel meter providers, but so far I haven't actually exposed it. I thought we should wait for an ask before making it an official feature

If you want me to add this now, I can. But it might be better as a separate PR

super().__init__()
self.client = MetricServiceClient(*client_args, **client_kwargs)
self.prefix = "bigtable.googleapis.com/internal/client"
self.project_id = project_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think CloudMonitoring client needs to be created with a project id. The project id can be extracted from metrics label ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I originally did it this way

  1. because we know the expected project at client init-time
  2. to be more in line with the upstream OTel Exporter

But I don't think there's any reason we need to do it this way. I changed it to extract all project ids from the labels

write_ind += max_batch_size

@staticmethod
def _to_point(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the resouce level attributes (project, instance, table, cluster, zone) removed from the metric data points?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that was happening here. But I updated this code to be a bit cleaner in this area

metric_kind = MetricDescriptor.MetricKind.CUMULATIVE
all_series: list[TimeSeries] = []
# process each metric from OTel format into Cloud Monitoring format
for resource_metric in metrics_data.resource_metrics:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume one of these for loops is filtering out bigtable only metrics?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this handler, we're using an isolated MeterProvider/instruments for bigtable metrics, so we don't actually expect to see any non-bigtable metrics come through.

(we made it so users can also interact with these metrics in their own OTel set-up if we want to expose that, but this pipeline is private)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But also, yes, there is some filtering here, skipping over data that doesn't have the project id set properly

# fixed labels sent with each metric update
self.shared_labels = {
"client_name": f"python-bigtable/{client_version}",
"client_uid": client_uid or self._generate_client_uid(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is client_uid generated otherwise? maybe we should always generate it? This is really to prevent cloud monitoring to having issue merging the streams. maybe we should move it to the exporter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is client_uid generated otherwise? maybe we should always generate it?

Currently, it's always auto-generated, unless explicitly passed in. I don't think we currently need to hard-code it anywhere (outside of possible testing), but if we choose to expose this to end-users, they may want to be able to control this field for their private metric systems

maybe we should move it to the exporter?

This is where we're setting all the other labels, so it feels like a natural place for it. But yeah, if you want to expose these metrics to external OTel set-ups, and don't want them to see this field, we can move it to the GCP-specific subclass instead

@daniel-sanche daniel-sanche changed the title feat: client side metrics handlers feat(bigtable): client side metrics handlers Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants