Skip to content

Commit dd8522b

Browse files
committed
feat(metrics): add AFE latency metrics and simplify metadata extraction
1 parent 82f08c2 commit dd8522b

7 files changed

Lines changed: 213 additions & 29 deletions

File tree

packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,17 @@
6060
METRIC_NAME_ATTEMPT_COUNT = "attempt_count"
6161
METRIC_NAME_GFE_LATENCY = "gfe_latency"
6262
METRIC_NAME_GFE_MISSING_HEADER_COUNT = "gfe_missing_header_count"
63+
METRIC_NAME_AFE_LATENCY = "afe_latency"
64+
METRIC_NAME_AFE_MISSING_HEADER_COUNT = "afe_missing_header_count"
6365
METRIC_NAMES = [
6466
METRIC_NAME_OPERATION_LATENCIES,
6567
METRIC_NAME_ATTEMPT_LATENCIES,
6668
METRIC_NAME_OPERATION_COUNT,
6769
METRIC_NAME_ATTEMPT_COUNT,
70+
METRIC_NAME_GFE_LATENCY,
71+
METRIC_NAME_GFE_MISSING_HEADER_COUNT,
72+
METRIC_NAME_AFE_LATENCY,
73+
METRIC_NAME_AFE_MISSING_HEADER_COUNT,
6874
]
6975

7076
METRIC_EXPORT_INTERVAL_MS = 60000 # 1 Minute

packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_interceptor.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,8 @@ def _wrap_response(response: Any, tracer: Any) -> Any:
147147
metadata.extend(response.initial_metadata() or [])
148148
except Exception as e:
149149
logger.warning(f"Failed to retrieve initial metadata: {e}")
150-
if hasattr(response, "trailing_metadata"):
151-
try:
152-
metadata.extend(response.trailing_metadata() or [])
153-
except Exception as e:
154-
logger.warning(f"Failed to retrieve trailing metadata: {e}")
155150
tracer.record_gfe_metrics(metadata)
151+
tracer.record_afe_metrics(metadata)
156152
except Exception as e:
157153
logger.warning(f"Failed to record metrics: {e}")
158154
return response
@@ -260,12 +256,8 @@ def _record_metrics(self):
260256
metadata.extend(self._response.initial_metadata() or [])
261257
except Exception as e:
262258
logger.warning(f"Failed to retrieve initial metadata: {e}")
263-
if hasattr(self._response, "trailing_metadata"):
264-
try:
265-
metadata.extend(self._response.trailing_metadata() or [])
266-
except Exception as e:
267-
logger.warning(f"Failed to retrieve trailing metadata: {e}")
268259
self._tracer.record_gfe_metrics(metadata)
260+
self._tracer.record_afe_metrics(metadata)
269261
except Exception as e:
270262
logger.warning(f"Failed to record metrics: {e}")
271263

@@ -341,15 +333,8 @@ async def _record_metrics(self):
341333
metadata.extend(res or [])
342334
except Exception as e:
343335
logger.warning(f"Failed to retrieve initial metadata: {e}")
344-
if hasattr(self._response, "trailing_metadata"):
345-
try:
346-
res = self._response.trailing_metadata()
347-
if inspect.isawaitable(res):
348-
res = await res
349-
metadata.extend(res or [])
350-
except Exception as e:
351-
logger.warning(f"Failed to retrieve trailing metadata: {e}")
352336
self._tracer.record_gfe_metrics(metadata)
337+
self._tracer.record_afe_metrics(metadata)
353338
except Exception as e:
354339
logger.warning(f"Failed to record metrics: {e}")
355340

@@ -454,15 +439,8 @@ async def _record_metrics(self):
454439
metadata.extend(res or [])
455440
except Exception as e:
456441
logger.warning(f"Failed to retrieve initial metadata: {e}")
457-
if hasattr(self._response, "trailing_metadata"):
458-
try:
459-
res = self._response.trailing_metadata()
460-
if inspect.isawaitable(res):
461-
res = await res
462-
metadata.extend(res or [])
463-
except Exception as e:
464-
logger.warning(f"Failed to retrieve trailing metadata: {e}")
465442
self._tracer.record_gfe_metrics(metadata)
443+
self._tracer.record_afe_metrics(metadata)
466444
except Exception as e:
467445
logger.warning(f"Failed to record metrics: {e}")
468446

packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ class should not have any knowledge about the observability framework used for m
185185
_instrument_operation_latency: "Histogram"
186186
_instrument_gfe_latency: "Histogram"
187187
_instrument_gfe_missing_header_count: "Counter"
188+
_instrument_afe_latency: "Histogram"
189+
_instrument_afe_missing_header_count: "Counter"
188190
current_op: MetricOpTracer
189191
enabled: bool
190192
gfe_enabled: bool
@@ -201,6 +203,8 @@ def __init__(
201203
gfe_enabled: bool = False,
202204
instrument_gfe_latency: Optional["Histogram"] = None,
203205
instrument_gfe_missing_header_count: Optional["Counter"] = None,
206+
instrument_afe_latency: Optional["Histogram"] = None,
207+
instrument_afe_missing_header_count: Optional["Counter"] = None,
204208
):
205209
"""
206210
Initialize a MetricsTracer instance with the given parameters.
@@ -219,6 +223,8 @@ def __init__(
219223
gfe_enabled (bool, optional): Indicates if GFE metrics are enabled. Defaults to False.
220224
instrument_gfe_latency (Histogram, optional): Instrument for measuring GFE latency.
221225
instrument_gfe_missing_header_count (Counter, optional): Instrument for counting missing GFE headers.
226+
instrument_afe_latency (Histogram, optional): Instrument for measuring AFE latency.
227+
instrument_afe_missing_header_count (Counter, optional): Instrument for counting missing AFE headers.
222228
"""
223229
self.current_op = MetricOpTracer()
224230
self._client_attributes = client_attributes
@@ -228,6 +234,8 @@ def __init__(
228234
self._instrument_operation_counter = instrument_operation_counter
229235
self._instrument_gfe_latency = instrument_gfe_latency
230236
self._instrument_gfe_missing_header_count = instrument_gfe_missing_header_count
237+
self._instrument_afe_latency = instrument_afe_latency
238+
self._instrument_afe_missing_header_count = instrument_afe_missing_header_count
231239
self.enabled = enabled
232240
self.gfe_enabled = True
233241

@@ -430,6 +438,37 @@ def record_gfe_missing_header_count(self) -> None:
430438
amount=1, attributes=self.client_attributes
431439
)
432440

441+
def record_afe_latency(self, latency: int) -> None:
442+
"""
443+
Records the AFE latency using the Histogram instrument.
444+
445+
Args:
446+
latency (int): The latency duration to be recorded.
447+
"""
448+
if (
449+
not self.enabled
450+
or not HAS_OPENTELEMETRY_INSTALLED
451+
or not getattr(self, "_instrument_afe_latency", None)
452+
):
453+
return
454+
self._instrument_afe_latency.record(
455+
amount=latency, attributes=self.client_attributes
456+
)
457+
458+
def record_afe_missing_header_count(self) -> None:
459+
"""
460+
Increments the counter for missing AFE headers.
461+
"""
462+
if (
463+
not self.enabled
464+
or not HAS_OPENTELEMETRY_INSTALLED
465+
or not getattr(self, "_instrument_afe_missing_header_count", None)
466+
):
467+
return
468+
self._instrument_afe_missing_header_count.add(
469+
amount=1, attributes=self.client_attributes
470+
)
471+
433472
@staticmethod
434473
def extract_gfe_latency(metadata: Any) -> Optional[int]:
435474
"""
@@ -486,6 +525,62 @@ def record_gfe_metrics(self, metadata: Any) -> None:
486525
else:
487526
self.record_gfe_missing_header_count()
488527

528+
@staticmethod
529+
def extract_afe_latency(metadata: Any) -> Optional[int]:
530+
"""
531+
Extracts the AFE latency value (in milliseconds) from response metadata.
532+
"""
533+
if not metadata:
534+
return None
535+
536+
header_vals = []
537+
if isinstance(metadata, dict):
538+
for key, val in metadata.items():
539+
if key and str(key).lower() in ("server-timing", "server_timing"):
540+
if isinstance(val, (list, tuple)):
541+
header_vals.extend(val)
542+
else:
543+
header_vals.append(val)
544+
elif isinstance(metadata, (list, tuple)):
545+
for item in metadata:
546+
if isinstance(item, (list, tuple)) and len(item) == 2:
547+
key, val = item
548+
if key and str(key).lower() in ("server-timing", "server_timing"):
549+
if isinstance(val, (list, tuple)):
550+
header_vals.extend(val)
551+
else:
552+
header_vals.append(val)
553+
554+
for header_val in header_vals:
555+
if not header_val:
556+
continue
557+
if isinstance(header_val, bytes):
558+
try:
559+
header_val = header_val.decode("utf-8")
560+
except Exception:
561+
header_val = str(header_val)
562+
elif not isinstance(header_val, str):
563+
header_val = str(header_val)
564+
match = re.search(r"afe(?:t4t7)?;\s*dur=([0-9.]+)", header_val)
565+
if match:
566+
try:
567+
return int(float(match.group(1)))
568+
except ValueError:
569+
pass
570+
return None
571+
572+
def record_afe_metrics(self, metadata: Any) -> None:
573+
"""
574+
Extracts and records AFE metrics from the RPC response metadata.
575+
"""
576+
if not self.enabled or not HAS_OPENTELEMETRY_INSTALLED:
577+
return
578+
latency = self.extract_afe_latency(metadata)
579+
if latency is not None:
580+
self.record_afe_latency(latency)
581+
else:
582+
self.record_afe_missing_header_count()
583+
489584
def _create_operation_otel_attributes(self) -> dict:
490585
"""
491586
Create additional attributes for operation metrics tracing.

packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/metrics_tracer_factory.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
METRIC_LABEL_KEY_CLIENT_UID,
2323
METRIC_LABEL_KEY_DATABASE,
2424
METRIC_LABEL_KEY_DIRECT_PATH_ENABLED,
25+
METRIC_NAME_AFE_LATENCY,
26+
METRIC_NAME_AFE_MISSING_HEADER_COUNT,
2527
METRIC_NAME_ATTEMPT_COUNT,
2628
METRIC_NAME_ATTEMPT_LATENCIES,
2729
METRIC_NAME_GFE_LATENCY,
@@ -57,6 +59,8 @@ class MetricsTracerFactory:
5759
_instrument_operation_counter: "Counter"
5860
_instrument_gfe_latency: "Histogram"
5961
_instrument_gfe_missing_header_count: "Counter"
62+
_instrument_afe_latency: "Histogram"
63+
_instrument_afe_missing_header_count: "Counter"
6064
_client_attributes: Dict[str, str]
6165

6266
@property
@@ -274,6 +278,10 @@ def create_metrics_tracer(self) -> MetricsTracer:
274278
instrument_gfe_missing_header_count=getattr(
275279
self, "_instrument_gfe_missing_header_count", None
276280
),
281+
instrument_afe_latency=getattr(self, "_instrument_afe_latency", None),
282+
instrument_afe_missing_header_count=getattr(
283+
self, "_instrument_afe_missing_header_count", None
284+
),
277285
)
278286
return metrics_tracer
279287

@@ -331,3 +339,15 @@ def _create_metric_instruments(self, service_name: str) -> None:
331339
unit="1",
332340
description="GFE missing header count.",
333341
)
342+
343+
self._instrument_afe_latency = meter.create_histogram(
344+
name=METRIC_NAME_AFE_LATENCY,
345+
unit="ms",
346+
description="AFE Latency.",
347+
)
348+
349+
self._instrument_afe_missing_header_count = meter.create_counter(
350+
name=METRIC_NAME_AFE_MISSING_HEADER_COUNT,
351+
unit="1",
352+
description="AFE missing header count.",
353+
)

packages/google-cloud-spanner/tests/mockserver_tests/test_gfe_metrics.py renamed to packages/google-cloud-spanner/tests/mockserver_tests/test_frontend_metrics.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
)
3535

3636

37-
class TestGFEMetricsIntegration(MockServerTestBase):
37+
class TestFrontendMetricsIntegration(MockServerTestBase):
3838
def setUp(self):
3939
super().setUp()
4040
os.environ["SPANNER_DISABLE_BUILTIN_METRICS"] = "false"
@@ -61,13 +61,13 @@ def test_gfe_metrics_exported(self):
6161
def custom_initial_metadata(self):
6262
mocked = getattr(self, "_is_execute_streaming_sql_mock", False)
6363
if mocked:
64-
return (("server-timing", "gfet4t7; dur=55"),)
64+
return (("server-timing", "gfet4t7; dur=55, afe; dur=23"),)
6565
return orig_initial_metadata(self)
6666

6767
def custom_trailing_metadata(self):
6868
mocked = getattr(self, "_is_execute_streaming_sql_mock", False)
6969
if mocked:
70-
return (("server-timing", "gfet4t7; dur=55"),)
70+
return (("server-timing", "gfet4t7; dur=55, afe; dur=23"),)
7171
return orig_trailing_metadata(self)
7272

7373
def custom_call(self_callable, request, *args, **kwargs):
@@ -142,6 +142,11 @@ def custom_call(self_callable, request, *args, **kwargs):
142142
point = next(iter(gfe_metric.data.data_points))
143143
self.assertEqual(point.sum, 55)
144144

145+
self.assertIn("afe_latency", metrics, f"Metrics: {list(metrics.keys())}")
146+
afe_metric = metrics["afe_latency"]
147+
point = next(iter(afe_metric.data.data_points))
148+
self.assertEqual(point.sum, 23)
149+
145150
finally:
146151
pass
147152

@@ -202,5 +207,12 @@ def test_gfe_missing_header_count_exported(self):
202207
missing_metric = metrics["gfe_missing_header_count"]
203208
point = next(iter(missing_metric.data.data_points))
204209
self.assertGreaterEqual(point.value, 1)
210+
211+
self.assertIn(
212+
"afe_missing_header_count", metrics, f"Metrics: {list(metrics.keys())}"
213+
)
214+
afe_missing_metric = metrics["afe_missing_header_count"]
215+
afe_point = next(iter(afe_missing_metric.data.data_points))
216+
self.assertGreaterEqual(afe_point.value, 1)
205217
finally:
206218
pass

packages/google-cloud-spanner/tests/unit/test_metrics_interceptor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(self):
4646
self.record_attempt_completion = MagicMock()
4747
self.set_method = MagicMock()
4848
self.record_gfe_metrics = MagicMock()
49+
self.record_afe_metrics = MagicMock()
4950
self.set_project = MagicMock()
5051
self.set_instance = MagicMock()
5152
self.set_database = MagicMock()
@@ -118,4 +119,5 @@ def test_intercept_with_tracer(interceptor, mock_tracer_ctx):
118119
mock_tracer_ctx.record_attempt_start.assert_called()
119120
mock_tracer_ctx.record_attempt_completion.assert_called_once()
120121
mock_tracer_ctx.record_gfe_metrics.assert_called_once()
122+
mock_tracer_ctx.record_afe_metrics.assert_called_once()
121123
mock_invoked_method.assert_called_once_with("request", call_details)

0 commit comments

Comments
 (0)