Skip to content

Commit 8753a15

Browse files
committed
fix(spanner): align GFE/AFE metric names and instruments with backend specs
1 parent 4b8350d commit 8753a15

7 files changed

Lines changed: 138 additions & 198 deletions

File tree

packages/google-cloud-spanner/google/cloud/spanner_v1/metrics/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Custom Metric Exporter
1+
# Custom Metric Exporter
22
The custom metric exporter, as defined in [metrics_exporter.py](./metrics_exporter.py), is designed to work in conjunction with OpenTelemetry and the Spanner client. It converts data into its protobuf equivalent and sends it to Google Cloud Monitoring.
33

44
## Filtering Criteria
@@ -10,8 +10,10 @@ The exporter filters metrics based on the following conditions, utilizing values
1010
* `attempt_count`
1111
* `operation_latencies`
1212
* `operation_count`
13-
* `gfe_latency`
14-
* `gfe_missing_header_count`
13+
* `gfe_latencies`
14+
* `gfe_connectivity_error_count`
15+
* `afe_latencies`
16+
* `afe_connectivity_error_count`
1517

1618
## Service Endpoint
1719
The exporter sends metrics to the Google Cloud Monitoring [service endpoint](https://cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.metric_service.MetricServiceClient#google_cloud_monitoring_v3_services_metric_service_MetricServiceClient_create_service_time_series), distinct from the regular client endpoint. This service endpoint operates under a different quota limit than the user endpoint and features an additional server-side filter that only permits a predefined set of metrics to pass through.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,18 @@
5959
METRIC_NAME_OPERATION_COUNT = "operation_count"
6060
METRIC_NAME_ATTEMPT_COUNT = "attempt_count"
6161
METRIC_NAME_GFE_LATENCY = "gfe_latencies"
62-
METRIC_NAME_GFE_MISSING_HEADER_COUNT = "gfe_missing_header_count"
62+
METRIC_NAME_GFE_CONNECTIVITY_ERROR_COUNT = "gfe_connectivity_error_count"
6363
METRIC_NAME_AFE_LATENCY = "afe_latencies"
64-
METRIC_NAME_AFE_MISSING_HEADER_COUNT = "afe_missing_header_count"
64+
METRIC_NAME_AFE_CONNECTIVITY_ERROR_COUNT = "afe_connectivity_error_count"
6565
METRIC_NAMES = [
6666
METRIC_NAME_OPERATION_LATENCIES,
6767
METRIC_NAME_ATTEMPT_LATENCIES,
6868
METRIC_NAME_OPERATION_COUNT,
6969
METRIC_NAME_ATTEMPT_COUNT,
7070
METRIC_NAME_GFE_LATENCY,
71-
METRIC_NAME_GFE_MISSING_HEADER_COUNT,
71+
METRIC_NAME_GFE_CONNECTIVITY_ERROR_COUNT,
7272
METRIC_NAME_AFE_LATENCY,
73-
METRIC_NAME_AFE_MISSING_HEADER_COUNT,
73+
METRIC_NAME_AFE_CONNECTIVITY_ERROR_COUNT,
7474
]
7575

7676
METRIC_EXPORT_INTERVAL_MS = 60000 # 1 Minute

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,27 @@ def _parse_resource_path(path: str) -> dict:
5252
return {}
5353

5454
@staticmethod
55-
def _extract_resource_from_path(metadata: Dict[str, str]) -> Dict[str, str]:
55+
def _extract_resource_from_path(metadata: Any) -> Dict[str, str]:
5656
"""
5757
Extracts resource information from the metadata based on the path.
5858
59-
This method iterates through the metadata dictionary to find the first tuple containing the key 'google-cloud-resource-prefix'. It then extracts the path from this tuple and parses it to extract project, instance, and database information using the _parse_resource_path method.
60-
6159
Args:
62-
metadata (Dict[str, str]): A dictionary containing metadata information.
60+
metadata (Any): A sequence or dictionary containing metadata information.
6361
6462
Returns:
6563
Dict[str, str]: A dictionary containing extracted project, instance, and database information.
6664
"""
67-
# Extract resource info from the first metadata tuple containing :path
68-
path = next(
69-
(value for key, value in metadata if key == GOOGLE_CLOUD_RESOURCE_KEY), ""
70-
)
65+
if not metadata:
66+
return {}
67+
68+
items = metadata.items() if isinstance(metadata, dict) else metadata
69+
path = ""
70+
71+
for key, value in items:
72+
key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
73+
if key_str == GOOGLE_CLOUD_RESOURCE_KEY:
74+
path = value.decode("utf-8") if isinstance(value, bytes) else str(value)
75+
break
7176

7277
resources = MetricsInterceptor._parse_resource_path(path)
7378
return resources
@@ -147,8 +152,7 @@ def _wrap_response(response: Any, tracer: Any) -> Any:
147152
metadata.extend(response.initial_metadata() or [])
148153
except Exception as e:
149154
logger.warning(f"Failed to retrieve initial metadata: {e}")
150-
tracer.record_gfe_metrics(metadata)
151-
tracer.record_afe_metrics(metadata)
155+
tracer.record_front_end_metrics(metadata)
152156
except Exception as e:
153157
logger.warning(f"Failed to record metrics: {e}")
154158
return response
@@ -256,8 +260,7 @@ def _record_metrics(self):
256260
metadata.extend(self._response.initial_metadata() or [])
257261
except Exception as e:
258262
logger.warning(f"Failed to retrieve initial metadata: {e}")
259-
self._tracer.record_gfe_metrics(metadata)
260-
self._tracer.record_afe_metrics(metadata)
263+
self._tracer.record_front_end_metrics(metadata)
261264
except Exception as e:
262265
logger.warning(f"Failed to record metrics: {e}")
263266

@@ -333,8 +336,7 @@ async def _record_metrics(self):
333336
metadata.extend(res or [])
334337
except Exception as e:
335338
logger.warning(f"Failed to retrieve initial metadata: {e}")
336-
self._tracer.record_gfe_metrics(metadata)
337-
self._tracer.record_afe_metrics(metadata)
339+
self._tracer.record_front_end_metrics(metadata)
338340
except Exception as e:
339341
logger.warning(f"Failed to record metrics: {e}")
340342

@@ -439,8 +441,7 @@ async def _record_metrics(self):
439441
metadata.extend(res or [])
440442
except Exception as e:
441443
logger.warning(f"Failed to retrieve initial metadata: {e}")
442-
self._tracer.record_gfe_metrics(metadata)
443-
self._tracer.record_afe_metrics(metadata)
444+
self._tracer.record_front_end_metrics(metadata)
444445
except Exception as e:
445446
logger.warning(f"Failed to record metrics: {e}")
446447

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

Lines changed: 64 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,9 @@ class should not have any knowledge about the observability framework used for m
184184
_instrument_operation_counter: "Counter"
185185
_instrument_operation_latency: "Histogram"
186186
_instrument_gfe_latency: "Histogram"
187-
_instrument_gfe_missing_header_count: "Counter"
187+
_instrument_gfe_connectivity_error_count: "Counter"
188188
_instrument_afe_latency: "Histogram"
189-
_instrument_afe_missing_header_count: "Counter"
189+
_instrument_afe_connectivity_error_count: "Counter"
190190
current_op: MetricOpTracer
191191
enabled: bool
192192
gfe_enabled: bool
@@ -202,9 +202,9 @@ def __init__(
202202
client_attributes: Dict[str, str],
203203
gfe_enabled: bool = False,
204204
instrument_gfe_latency: Optional["Histogram"] = None,
205-
instrument_gfe_missing_header_count: Optional["Counter"] = None,
205+
instrument_gfe_connectivity_error_count: Optional["Counter"] = None,
206206
instrument_afe_latency: Optional["Histogram"] = None,
207-
instrument_afe_missing_header_count: Optional["Counter"] = None,
207+
instrument_afe_connectivity_error_count: Optional["Counter"] = None,
208208
):
209209
"""
210210
Initialize a MetricsTracer instance with the given parameters.
@@ -222,9 +222,9 @@ def __init__(
222222
client_attributes (Dict[str, str]): Dictionary of client attributes used for metrics tracing.
223223
gfe_enabled (bool, optional): Indicates if GFE metrics are enabled. Defaults to False.
224224
instrument_gfe_latency (Histogram, optional): Instrument for measuring GFE latency.
225-
instrument_gfe_missing_header_count (Counter, optional): Instrument for counting missing GFE headers.
225+
instrument_gfe_connectivity_error_count (Counter, optional): Instrument for counting GFE connectivity errors.
226226
instrument_afe_latency (Histogram, optional): Instrument for measuring AFE latency.
227-
instrument_afe_missing_header_count (Counter, optional): Instrument for counting missing AFE headers.
227+
instrument_afe_connectivity_error_count (Counter, optional): Instrument for counting AFE connectivity errors.
228228
"""
229229
self.current_op = MetricOpTracer()
230230
self._client_attributes = client_attributes
@@ -233,9 +233,9 @@ def __init__(
233233
self._instrument_operation_latency = instrument_operation_latency
234234
self._instrument_operation_counter = instrument_operation_counter
235235
self._instrument_gfe_latency = instrument_gfe_latency
236-
self._instrument_gfe_missing_header_count = instrument_gfe_missing_header_count
236+
self._instrument_gfe_connectivity_error_count = instrument_gfe_connectivity_error_count
237237
self._instrument_afe_latency = instrument_afe_latency
238-
self._instrument_afe_missing_header_count = instrument_afe_missing_header_count
238+
self._instrument_afe_connectivity_error_count = instrument_afe_connectivity_error_count
239239
self.enabled = enabled
240240
self.gfe_enabled = True
241241

@@ -424,17 +424,17 @@ def record_gfe_latency(self, latency: int) -> None:
424424
amount=latency, attributes=self.client_attributes
425425
)
426426

427-
def record_gfe_missing_header_count(self) -> None:
427+
def record_gfe_connectivity_error_count(self) -> None:
428428
"""
429-
Increments the counter for missing GFE headers.
429+
Increments the counter for GFE connectivity errors.
430430
"""
431431
if (
432432
not self.enabled
433433
or not HAS_OPENTELEMETRY_INSTALLED
434-
or not getattr(self, "_instrument_gfe_missing_header_count", None)
434+
or not getattr(self, "_instrument_gfe_connectivity_error_count", None)
435435
):
436436
return
437-
self._instrument_gfe_missing_header_count.add(
437+
self._instrument_gfe_connectivity_error_count.add(
438438
amount=1, attributes=self.client_attributes
439439
)
440440

@@ -455,101 +455,46 @@ def record_afe_latency(self, latency: int) -> None:
455455
amount=latency, attributes=self.client_attributes
456456
)
457457

458-
def record_afe_missing_header_count(self) -> None:
458+
def record_afe_connectivity_error_count(self) -> None:
459459
"""
460-
Increments the counter for missing AFE headers.
460+
Increments the counter for AFE connectivity errors.
461461
"""
462462
if (
463463
not self.enabled
464464
or not HAS_OPENTELEMETRY_INSTALLED
465-
or not getattr(self, "_instrument_afe_missing_header_count", None)
465+
or not getattr(self, "_instrument_afe_connectivity_error_count", None)
466466
):
467467
return
468-
self._instrument_afe_missing_header_count.add(
468+
self._instrument_afe_connectivity_error_count.add(
469469
amount=1, attributes=self.client_attributes
470470
)
471471

472472
@staticmethod
473-
def extract_gfe_latency(metadata: Any) -> Optional[int]:
473+
def extract_front_end_latencies(metadata: Any) -> tuple[Optional[int], Optional[int]]:
474474
"""
475-
Extracts the GFE latency value (in milliseconds) from response metadata.
475+
Extracts both GFE and AFE latency values (in milliseconds) from response metadata.
476476
"""
477477
if not metadata:
478-
return None
478+
return None, None
479479

480-
header_vals = []
481480
if isinstance(metadata, dict):
482-
for key, val in metadata.items():
483-
if key and str(key).lower() in ("server-timing", "server_timing"):
484-
if isinstance(val, (list, tuple)):
485-
header_vals.extend(val)
486-
else:
487-
header_vals.append(val)
481+
items = metadata.items()
488482
elif isinstance(metadata, (list, tuple)):
489-
for item in metadata:
490-
if isinstance(item, (list, tuple)) and len(item) == 2:
491-
key, val = item
492-
if key and str(key).lower() in ("server-timing", "server_timing"):
493-
if isinstance(val, (list, tuple)):
494-
header_vals.extend(val)
495-
else:
496-
header_vals.append(val)
497-
498-
for header_val in header_vals:
499-
if not header_val:
500-
continue
501-
if isinstance(header_val, bytes):
502-
try:
503-
header_val = header_val.decode("utf-8")
504-
except Exception:
505-
header_val = str(header_val)
506-
elif not isinstance(header_val, str):
507-
header_val = str(header_val)
508-
match = re.search(r"gfet4t7;\s*dur=([0-9.]+)", header_val)
509-
if match:
510-
try:
511-
return int(float(match.group(1)))
512-
except ValueError:
513-
pass
514-
return None
515-
516-
def record_gfe_metrics(self, metadata: Any) -> None:
517-
"""
518-
Extracts and records GFE metrics from the RPC response metadata.
519-
"""
520-
if not self.enabled or not HAS_OPENTELEMETRY_INSTALLED:
521-
return
522-
latency = self.extract_gfe_latency(metadata)
523-
if latency is not None:
524-
self.record_gfe_latency(latency)
483+
items = [item for item in metadata if isinstance(item, (list, tuple)) and len(item) == 2]
525484
else:
526-
self.record_gfe_missing_header_count()
527-
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
485+
items = []
535486

536487
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)
488+
for key, val in items:
489+
key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
490+
if key_str and key_str.lower() in ("server-timing", "server_timing"):
491+
if isinstance(val, (list, tuple)):
492+
header_vals.extend(val)
493+
else:
494+
header_vals.append(val)
495+
496+
gfe_latency = None
497+
afe_latency = None
553498

554499
for header_val in header_vals:
555500
if not header_val:
@@ -561,25 +506,42 @@ def extract_afe_latency(metadata: Any) -> Optional[int]:
561506
header_val = str(header_val)
562507
elif not isinstance(header_val, str):
563508
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.
509+
510+
if gfe_latency is None:
511+
match = re.search(r"gfet4t7;\s*dur=([0-9.]+)", header_val)
512+
if match:
513+
try:
514+
gfe_latency = int(float(match.group(1)))
515+
except ValueError:
516+
pass
517+
518+
if afe_latency is None:
519+
match = re.search(r"afe(?:t4t7)?;\s*dur=([0-9.]+)", header_val)
520+
if match:
521+
try:
522+
afe_latency = int(float(match.group(1)))
523+
except ValueError:
524+
pass
525+
526+
return gfe_latency, afe_latency
527+
528+
def record_front_end_metrics(self, metadata: Any) -> None:
529+
"""
530+
Extracts and records both GFE and AFE metrics from the RPC response metadata.
575531
"""
576532
if not self.enabled or not HAS_OPENTELEMETRY_INSTALLED:
577533
return
578-
latency = self.extract_afe_latency(metadata)
579-
if latency is not None:
580-
self.record_afe_latency(latency)
534+
gfe_latency, afe_latency = self.extract_front_end_latencies(metadata)
535+
536+
if gfe_latency is not None:
537+
self.record_gfe_latency(gfe_latency)
538+
else:
539+
self.record_gfe_connectivity_error_count()
540+
541+
if afe_latency is not None:
542+
self.record_afe_latency(afe_latency)
581543
else:
582-
self.record_afe_missing_header_count()
544+
self.record_afe_connectivity_error_count()
583545

584546
def _create_operation_otel_attributes(self) -> dict:
585547
"""

0 commit comments

Comments
 (0)