Skip to content

chore(spanner): support built-in metrics for ResultSet streaming RPCs - #6302

Open
olavloite wants to merge 2 commits into
googleapis:mainfrom
olavloite:spanner-streaming-resultset-metrics
Open

chore(spanner): support built-in metrics for ResultSet streaming RPCs#6302
olavloite wants to merge 2 commits into
googleapis:mainfrom
olavloite:spanner-streaming-resultset-metrics

Conversation

@olavloite

Copy link
Copy Markdown
Contributor

Adds OpenTelemetry built-in metrics instrumentation for streaming operations (ExecuteStreamingSql and StreamingRead) in Cloud Spanner ResultSet.

  • Records attempt_count, attempt_latencies, gfe_latencies, and afe_latencies across all initial attempts and stream restarts.
  • Records end-to-end operation_count and operation_latencies spanning the entire query lifecycle until stream completion or permanent failure.
  • Preserves attempt response headers across background stream trailer drains (seen_last) and early drops.
  • Standardizes metric method attribute formatting to "Spanner.<MethodName>" (Spanner.ExecuteStreamingSql, Spanner.StreamingRead) matching Java and Go reference clients.
  • Parses single and multiple server-timing response headers for GFE and AFE timings.

Adds OpenTelemetry built-in metrics instrumentation for streaming operations (`ExecuteStreamingSql` and `StreamingRead`) in Cloud Spanner `ResultSet`.
- Records `attempt_count`, `attempt_latencies`, `gfe_latencies`, and `afe_latencies` across all initial attempts and stream restarts.
- Records end-to-end `operation_count` and `operation_latencies` spanning the entire query lifecycle until stream completion or permanent failure.
- Preserves attempt response headers across background stream trailer drains (`seen_last`) and early drops.
- Standardizes metric `method` attribute formatting to `"Spanner.<MethodName>"` (`Spanner.ExecuteStreamingSql`, `Spanner.StreamingRead`) matching Java and Go reference clients.
- Parses single and multiple `server-timing` response headers for GFE and AFE timings.
@olavloite
olavloite requested review from a team as code owners August 7, 2026 12:01
@product-auto-label product-auto-label Bot added the api: spanner Issues related to the Spanner API. label Aug 7, 2026

@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 attempt and operation metrics tracking for Spanner streaming queries and reads, including parsing server-timing headers for GFE/AFE latencies and normalising method names. The review feedback highlights several critical gaps in error handling and metrics accuracy: stream initialization failures are currently not recorded (and incorrectly reported as successful upon drop), stale headers from previous attempts are not cleared during retries, and complete connection failures prior to ResultSet creation are missed entirely by the metrics recorder.

Comment thread src/spanner/src/result_set.rs
Comment thread src/spanner/src/result_set.rs
Comment thread src/spanner/src/read_only_transaction.rs
Comment thread src/spanner/src/batch_read_only_transaction.rs
Comment thread src/spanner/src/batch_read_only_transaction.rs
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.45054% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.29%. Comparing base (3fb8e6d) to head (499440c).

Files with missing lines Patch % Lines
src/spanner/src/result_set.rs 98.51% 10 Missing ⚠️
src/spanner/src/server_streaming/stream.rs 83.33% 2 Missing ⚠️
src/spanner/src/observability/metrics.rs 99.26% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6302      +/-   ##
==========================================
+ Coverage   96.26%   96.29%   +0.03%     
==========================================
  Files         282      282              
  Lines       72762    73529     +767     
==========================================
+ Hits        70044    70806     +762     
- Misses       2718     2723       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@olavloite
olavloite force-pushed the spanner-streaming-resultset-metrics branch from 7f24278 to 3e80e52 Compare August 7, 2026 12:53
@olavloite

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 implements built-in metrics collection for Spanner streaming operations, tracking attempt and operation counts, latencies, and GFE/AFE latencies parsed from response headers. It updates ResultSet to record these metrics during stream initialization, execution, retries, and upon being dropped, and adds comprehensive unit and integration tests to verify the metrics recording behavior. The review feedback suggests extending the stream auto-trait assertions to explicitly verify the Sync trait, ensuring compatibility with thread-safe types.

Comment thread src/spanner/src/server_streaming/stream.rs Outdated
@olavloite
olavloite force-pushed the spanner-streaming-resultset-metrics branch from 3e80e52 to 499440c Compare August 7, 2026 13:51
@olavloite

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 implements attempt and operation metrics tracking for Spanner streaming RPCs (ExecuteStreamingSql and StreamingRead), including parsing GFE/AFE latencies from response headers and recording metrics on completion or failure. While the implementation is well-tested, the review feedback highlights a critical blind spot: initial stream creation failures (when the initial send().await fails before a ResultSet is instantiated) are currently not recorded in the metrics. To ensure complete observability, these initial failure paths in both batch_read_only_transaction.rs and the retry macro in read_only_transaction.rs should be updated to record attempt and operation metrics.

/// Helper macro to execute a streaming SQL or streaming read RPC with retry logic.
macro_rules! execute_stream_with_retry {
($self:expr, $request:ident, $gax_options:ident, $rpc_method:ident, $operation_variant:path, $method_name:expr) => {{
let attempt_start_time = Instant::now();

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.

high

Missing Metrics for Initial Stream Creation Failures

If the initial send().await call fails, the macro returns an error immediately (or after a failed explicit begin fallback) without ever creating a ResultSet. Consequently, no attempt or operation metrics are recorded for these initial failures. This creates a significant blind spot in monitoring, as initial connection/handshake/validation failures will not be reflected in the metrics.

Additionally, if the first attempt fails but the retry succeeds, the first failed attempt is not recorded, and the second attempt's start time is incorrectly measured from the beginning of the first attempt.

To fix this, we should:

  1. Record attempt and operation metrics on all initial failure paths.
  2. Record the first failed attempt and reset attempt_start_time before retrying.

Since the middle of the macro is not part of the diff hunks, a direct code suggestion cannot be applied here. However, you can refactor the macro as follows:

macro_rules! execute_stream_with_retry {
    ($self:expr, $request:ident, $gax_options:ident, $rpc_method:ident, $operation_variant:path, $method_name:expr) => {{
        let mut attempt_start_time = Instant::now();
        let stream = match $self
            .client
            .spanner
            .$rpc_method($request.clone(), $gax_options.clone(), $self.channel_hint)
            .send()
            .await
        {
            Ok(s) => s,
            Err(e) => {
                let elapsed = attempt_start_time.elapsed();
                let is_starting = matches!(
                    $request
                        .transaction
                        .as_ref()
                        .and_then(|t| t.selector.as_ref()),
                    Some(crate::model::transaction_selector::Selector::Begin(_))
                );
                if is_starting {
                    if $self.transaction_selector.is_read_write() {
                        $self.client.o11y.record_attempt_with_status($method_name, elapsed, Some(&e), None, None);
                        $self.client.o11y.record_operation_with_status($method_name, elapsed, Some(&e));
                        $self.transaction_selector.set_failed(&e);
                        return Err(e);
                    } else {
                        if is_aborted(&e) {
                            $self.client.o11y.record_attempt_with_status($method_name, elapsed, Some(&e), None, None);
                            $self.client.o11y.record_operation_with_status($method_name, elapsed, Some(&e));
                            return Err(e);
                        }
                        $self.client.o11y.record_attempt_with_status($method_name, elapsed, Some(&e), None, None);
                        if $self
                            .begin_explicitly_if_not_started($gax_options.clone(), true, None)
                            .await?
                        {
                            $request.transaction =
                                Some($self.transaction_selector.selector().await?);
                            attempt_start_time = Instant::now();
                            match $self
                                .client
                                .spanner
                                .$rpc_method(
                                    $request.clone(),
                                    $gax_options.clone(),
                                    $self.channel_hint,
                                )
                                .send()
                                .await
                            {
                                Ok(s) => s,
                                Err(retry_err) => {
                                    let retry_elapsed = attempt_start_time.elapsed();
                                    $self.client.o11y.record_attempt_with_status($method_name, retry_elapsed, Some(&retry_err), None, None);
                                    $self.client.o11y.record_operation_with_status($method_name, retry_elapsed, Some(&retry_err));
                                    return Err(retry_err);
                                }
                            }
                        } else {
                            $self.client.o11y.record_operation_with_status($method_name, elapsed, Some(&e));
                            return Err(e);
                        }
                    }
                } else {
                    $self.client.o11y.record_attempt_with_status($method_name, elapsed, Some(&e), None, None);
                    $self.client.o11y.record_operation_with_status($method_name, elapsed, Some(&e));
                    return Err(e);
                }
            }
        };
        // ... rest of the macro ...
References
  1. Scrutinize Edge Cases: Always consider failure modes (network failures, unexpected inputs). Do not assume a perfect world. (link)

) -> crate::Result<ResultSet> {
let channel_hint = client.spanner.next_channel_hint();
let gax_options = client.spanner.attach_request_id(gax_options, channel_hint);
let attempt_start_time = Instant::now();

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.

high

Missing Metrics for Initial Stream Creation Failures

If the initial send().await call fails, the error is returned immediately via ? without ever creating a ResultSet. Consequently, no attempt or operation metrics are recorded for these initial failures. This creates a significant blind spot in monitoring, as initial connection/handshake/validation failures will not be reflected in the metrics.

We should match on the result of send().await and record the metrics on error:

        let attempt_start_time = Instant::now();
        let stream = match client
            .spanner
            .execute_streaming_sql(req.clone(), gax_options.clone(), channel_hint)
            .send()
            .await
        {
            Ok(s) => s,
            Err(e) => {
                let elapsed = attempt_start_time.elapsed();
                client.o11y.record_attempt_with_status("ExecuteStreamingSql", elapsed, Some(&e), None, None);
                client.o11y.record_operation_with_status("ExecuteStreamingSql", elapsed, Some(&e));
                return Err(e);
            }
        };
References
  1. Scrutinize Edge Cases: Always consider failure modes (network failures, unexpected inputs). Do not assume a perfect world. (link)

) -> crate::Result<ResultSet> {
let channel_hint = client.spanner.next_channel_hint();
let gax_options = client.spanner.attach_request_id(gax_options, channel_hint);
let attempt_start_time = Instant::now();

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.

high

Missing Metrics for Initial Stream Creation Failures

If the initial send().await call fails, the error is returned immediately via ? without ever creating a ResultSet. Consequently, no attempt or operation metrics are recorded for these initial failures. This creates a significant blind spot in monitoring, as initial connection/handshake/validation failures will not be reflected in the metrics.

We should match on the result of send().await and record the metrics on error:

        let attempt_start_time = Instant::now();
        let stream = match client
            .spanner
            .streaming_read(req.clone(), gax_options.clone(), channel_hint)
            .send()
            .await
        {
            Ok(s) => s,
            Err(e) => {
                let elapsed = attempt_start_time.elapsed();
                client.o11y.record_attempt_with_status("StreamingRead", elapsed, Some(&e), None, None);
                client.o11y.record_operation_with_status("StreamingRead", elapsed, Some(&e));
                return Err(e);
            }
        };
References
  1. Scrutinize Edge Cases: Always consider failure modes (network failures, unexpected inputs). Do not assume a perfect world. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the Spanner API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant