Skip to content

fix(metrics): correct GFE metrics extraction and enable by default - #17561

Open
sinhasubham wants to merge 6 commits into
mainfrom
gfe-metrics-fix
Open

fix(metrics): correct GFE metrics extraction and enable by default#17561
sinhasubham wants to merge 6 commits into
mainfrom
gfe-metrics-fix

Conversation

@sinhasubham

@sinhasubham sinhasubham commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

This PR enables and properly extracts Spanner Google Front End (GFE) and Application Front End (AFE) latency metrics.

Key Changes:

  • Fix Metadata Extraction Logic: Modified MetricsInterceptor and AsyncMetricsInterceptor to strictly extract server-timing data from initial_metadata().
  • Add AFE Metrics Publishing: Extended MetricsTracer to parse AFE latency (afe;\s*dur=) from the server-timing header alongside GFE metrics. Two new OpenTelemetry instruments (afe_latency and afe_missing_header_count) have been introduced to publish these.
  • Support for Streaming RPCs: Removed the manual toggle in SpannerMetricsTracerFactory. GFE and AFE metrics capture is now always-on whenever OpenTelemetry tracing is enabled.
  • Enable Frontend Metrics by Default: Removed the gfe_enabled toggle in SpannerMetricsTracerFactory. GFE metrics capture is now always-on whenever OpenTelemetry tracing is enabled.
  • Testing: Added unit tests and a new mockserver test (test_frontend_metrics.py) to ensure both GFE and AFE metrics are correctly published end-to-end.
  • Local Testing Screenshot: https://screenshot.googleplex.com/image/4tmjwuPjzdttcjW.png

@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 deferred metrics recording for streaming and async streaming RPC responses in Google Cloud Spanner by wrapping responses in specialized wrappers. It also implements GFE latency extraction from response metadata and adds corresponding tests. The review feedback highlights several critical improvements for robustness: refining streaming response detection to check for iterator methods (next and anext) rather than iterable methods to prevent incorrect wrapping of standard iterables; wrapping telemetry and metrics recording blocks in try-except blocks to ensure telemetry failures do not disrupt the main application flow; defensively validating metadata elements before unpacking to avoid unpacking errors; and properly decoding bytes metadata values before regex matching.

Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer.py Outdated
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer.py Outdated
@parthea

parthea commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@sinhasubham, Please merge main into this branch. The presubmit failures were addressed in #17578

@sinhasubham
sinhasubham force-pushed the gfe-metrics-fix branch 6 times, most recently from a495bbb to 82f08c2 Compare July 1, 2026 09:38
@sinhasubham
sinhasubham marked this pull request as ready for review July 6, 2026 06:11
@sinhasubham
sinhasubham requested a review from a team as a code owner July 6, 2026 06:11
@sinhasubham
sinhasubham requested a review from surbhigarg92 July 6, 2026 06:11
Comment on lines +204 to +207
instrument_gfe_latency: Optional["Histogram"] = None,
instrument_gfe_missing_header_count: Optional["Counter"] = None,
instrument_afe_latency: Optional["Histogram"] = None,
instrument_afe_missing_header_count: Optional["Counter"] = None,

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 Optional ?

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.

Addressed

Comment on lines +442 to +443
self._tracer.record_gfe_metrics(metadata)
self._tracer.record_afe_metrics(metadata)

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.

Both methods are extracting latency values from metadata seprarately, this can be optimized to fetch both afe and gfe latency values from the metadata once, and pass it in these recorder methods

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.

Addressed

Comment on lines +138 to +139
if hasattr(response, "__next__"):
return _StreamingResponseWrapper(response, tracer)

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.

Instead of checking this, cant we have interceptor listening to different events.

For eg: refer Node code, similar thing is used in Java and Go as well https://github.com/googleapis/google-cloud-node/blob/main/handwritten/spanner/src/metrics/interceptor.ts#L65

We have events like onReceiveStatus onReceiveMetadata ,

onReceiveStatus will only be called after the requests has finished execution which should trigger record_attempt_completion

onReceiveMetadata should be the place where we capture the metadata and use the value while calling record_gfe_metrics from onReceiveStatus

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.

Unlike Node.js, Java or Go: Python grpc and grpc.aio client interceptor APIs do not natively expose event-based lifecycle hooks (like onReceiveMetadata or onReceiveStatus).

@surbhigarg92

Copy link
Copy Markdown
Contributor

/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 adds support for collecting and exporting Application Front End (AFE) metrics alongside Google Front End (GFE) metrics, and introduces an AsyncMetricsInterceptor to handle asynchronous gRPC calls. The review feedback highlights critical issues: first, passing call_details.metadata directly to _extract_resource_from_path in the async interceptor will raise an AttributeError because it is a grpc.aio.Metadata object rather than a dictionary; second, metadata keys can be bytes in some gRPC environments, meaning str(key) will produce "b'server-timing'" and fail string comparisons in both extract_gfe_latency and extract_afe_latency.

Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer.py Outdated
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer.py Outdated
Comment on lines +63 to +64
METRIC_NAME_AFE_LATENCY = "afe_latency"
METRIC_NAME_AFE_MISSING_HEADER_COUNT = "afe_missing_header_count"

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.

Metric names are "gfe_latencies" and "afe_latencies" .

Not sure how were you able to export metrics to Cloud Monarch and Monitoring ? The screenshot attached shows metrics in "Generic Task" , it should be under Spanner -> Client with full name as spanner.googleapis.com/client/afe_latencies

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.

Addressed. updated test screenshot

@sinhasubham
sinhasubham force-pushed the gfe-metrics-fix branch 4 times, most recently from ceaa773 to 644fe1c Compare July 26, 2026 05:14

@surbhigarg92 surbhigarg92 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.

)
method_str = call_details.method
if isinstance(method_str, bytes):
method_str = method_str.decode("utf-8")

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 this ? Why would method name have the encoding ? It was not there earlier, which case was failing because of it ?

tracer.set_method(method_name)
tracer.record_attempt_start()

if os.environ.get("SPANNER_DISABLE_AFE_SERVER_TIMING", "").lower() != "true":

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.

Instead of getting this from ENV for every call, can we fetch it once during client init and pass it to the interceptor ?

Comment on lines +138 to +142
if os.environ.get("SPANNER_DISABLE_AFE_SERVER_TIMING", "").lower() != "true":
metadata = list(call_details.metadata or [])
metadata.append(("x-goog-spanner-enable-afe-server-timing", "true"))
call_details = call_details._replace(metadata=metadata)

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.

nit: Good to create a method in helpers file for appending metadata like we have for all other headers

https://github.com/googleapis/google-cloud-python/blob/main/packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py#L803

if os.environ.get("SPANNER_DISABLE_AFE_SERVER_TIMING", "").lower() != "true":
metadata = list(call_details.metadata or [])
metadata.append(("x-goog-spanner-enable-afe-server-timing", "true"))
call_details = call_details._replace(metadata=metadata)

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.

Instead of doing call_details._replace which might be a little expensive operation , this can be done with all other metadata.

Eg: We could do this where we are appending prefix metadata which is already called for every operation

https://github.com/googleapis/google-cloud-python/blob/main/packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py#L710C5-L710C26

)
self.enabled = enabled
self.gfe_enabled = gfe_enabled
self.gfe_enabled = True

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.

nit: We could remove this

not self.enabled
or not HAS_OPENTELEMETRY_INSTALLED
or not getattr(self, "_instrument_afe_latency", None)
or os.environ.get("SPANNER_DISABLE_AFE_SERVER_TIMING", "").lower() == "true"

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.

nit: Same here, dont fetch from ENV for every call, have a static property and use it.

header_vals = []
for key, val in items:
key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
if key_str and key_str.lower() in ("server-timing", "server_timing"):

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.

We are only capturing "server-timing" in all other clients not "server_timing"

pass

if afe_latency is None:
match = re.search(r"afe(?:t4t7)?;\s*dur=([0-9.]+)", header_val)

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.

nit: Optional "t4t7" is not required, it will always be like "/afe; dur=([0-9]+).*/"

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.

4 participants