Skip to content

Commit 30e3122

Browse files
committed
fix(spanner): align GFE/AFE metric names and instruments with backend specs
1 parent ee696db commit 30e3122

7 files changed

Lines changed: 168 additions & 205 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: 76 additions & 104 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
@@ -200,11 +200,11 @@ def __init__(
200200
instrument_operation_latency: "Histogram",
201201
instrument_operation_counter: "Counter",
202202
client_attributes: Dict[str, str],
203+
instrument_gfe_latency: "Histogram",
204+
instrument_gfe_connectivity_error_count: "Counter",
205+
instrument_afe_latency: "Histogram",
206+
instrument_afe_connectivity_error_count: "Counter",
203207
gfe_enabled: bool = False,
204-
instrument_gfe_latency: Optional["Histogram"] = None,
205-
instrument_gfe_missing_header_count: Optional["Counter"] = None,
206-
instrument_afe_latency: Optional["Histogram"] = None,
207-
instrument_afe_missing_header_count: Optional["Counter"] = None,
208208
):
209209
"""
210210
Initialize a MetricsTracer instance with the given parameters.
@@ -221,10 +221,10 @@ def __init__(
221221
instrument_operation_counter (Counter): Instrument for counting operations.
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.
224-
instrument_gfe_latency (Histogram, optional): Instrument for measuring GFE latency.
225-
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.
224+
instrument_gfe_latency (Histogram): Instrument for measuring GFE latency.
225+
instrument_gfe_connectivity_error_count (Counter): Instrument for counting GFE connectivity errors.
226+
instrument_afe_latency (Histogram): Instrument for measuring AFE latency.
227+
instrument_afe_connectivity_error_count (Counter): Instrument for counting AFE connectivity errors.
228228
"""
229229
self.current_op = MetricOpTracer()
230230
self._client_attributes = client_attributes
@@ -233,9 +233,13 @@ 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 = (
237+
instrument_gfe_connectivity_error_count
238+
)
237239
self._instrument_afe_latency = instrument_afe_latency
238-
self._instrument_afe_missing_header_count = instrument_afe_missing_header_count
240+
self._instrument_afe_connectivity_error_count = (
241+
instrument_afe_connectivity_error_count
242+
)
239243
self.enabled = enabled
240244
self.gfe_enabled = True
241245

@@ -424,17 +428,17 @@ def record_gfe_latency(self, latency: int) -> None:
424428
amount=latency, attributes=self.client_attributes
425429
)
426430

427-
def record_gfe_missing_header_count(self) -> None:
431+
def record_gfe_connectivity_error_count(self) -> None:
428432
"""
429-
Increments the counter for missing GFE headers.
433+
Increments the counter for GFE connectivity errors.
430434
"""
431435
if (
432436
not self.enabled
433437
or not HAS_OPENTELEMETRY_INSTALLED
434-
or not getattr(self, "_instrument_gfe_missing_header_count", None)
438+
or not getattr(self, "_instrument_gfe_connectivity_error_count", None)
435439
):
436440
return
437-
self._instrument_gfe_missing_header_count.add(
441+
self._instrument_gfe_connectivity_error_count.add(
438442
amount=1, attributes=self.client_attributes
439443
)
440444

@@ -455,101 +459,52 @@ def record_afe_latency(self, latency: int) -> None:
455459
amount=latency, attributes=self.client_attributes
456460
)
457461

458-
def record_afe_missing_header_count(self) -> None:
462+
def record_afe_connectivity_error_count(self) -> None:
459463
"""
460-
Increments the counter for missing AFE headers.
464+
Increments the counter for AFE connectivity errors.
461465
"""
462466
if (
463467
not self.enabled
464468
or not HAS_OPENTELEMETRY_INSTALLED
465-
or not getattr(self, "_instrument_afe_missing_header_count", None)
469+
or not getattr(self, "_instrument_afe_connectivity_error_count", None)
466470
):
467471
return
468-
self._instrument_afe_missing_header_count.add(
472+
self._instrument_afe_connectivity_error_count.add(
469473
amount=1, attributes=self.client_attributes
470474
)
471475

472476
@staticmethod
473-
def extract_gfe_latency(metadata: Any) -> Optional[int]:
477+
def extract_front_end_latencies(
478+
metadata: Any,
479+
) -> tuple[Optional[int], Optional[int]]:
474480
"""
475-
Extracts the GFE latency value (in milliseconds) from response metadata.
481+
Extracts both GFE and AFE latency values (in milliseconds) from response metadata.
476482
"""
477483
if not metadata:
478-
return None
484+
return None, None
479485

480-
header_vals = []
481486
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)
487+
items = metadata.items()
488488
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)
489+
items = [
490+
item
491+
for item in metadata
492+
if isinstance(item, (list, tuple)) and len(item) == 2
493+
]
525494
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
495+
items = []
535496

536497
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)
498+
for key, val in items:
499+
key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
500+
if key_str and key_str.lower() in ("server-timing", "server_timing"):
501+
if isinstance(val, (list, tuple)):
502+
header_vals.extend(val)
503+
else:
504+
header_vals.append(val)
505+
506+
gfe_latency = None
507+
afe_latency = None
553508

554509
for header_val in header_vals:
555510
if not header_val:
@@ -561,25 +516,42 @@ def extract_afe_latency(metadata: Any) -> Optional[int]:
561516
header_val = str(header_val)
562517
elif not isinstance(header_val, str):
563518
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
571519

572-
def record_afe_metrics(self, metadata: Any) -> None:
520+
if gfe_latency is None:
521+
match = re.search(r"gfet4t7;\s*dur=([0-9.]+)", header_val)
522+
if match:
523+
try:
524+
gfe_latency = int(float(match.group(1)))
525+
except ValueError:
526+
pass
527+
528+
if afe_latency is None:
529+
match = re.search(r"afe(?:t4t7)?;\s*dur=([0-9.]+)", header_val)
530+
if match:
531+
try:
532+
afe_latency = int(float(match.group(1)))
533+
except ValueError:
534+
pass
535+
536+
return gfe_latency, afe_latency
537+
538+
def record_front_end_metrics(self, metadata: Any) -> None:
573539
"""
574-
Extracts and records AFE metrics from the RPC response metadata.
540+
Extracts and records both GFE and AFE metrics from the RPC response metadata.
575541
"""
576542
if not self.enabled or not HAS_OPENTELEMETRY_INSTALLED:
577543
return
578-
latency = self.extract_afe_latency(metadata)
579-
if latency is not None:
580-
self.record_afe_latency(latency)
544+
gfe_latency, afe_latency = self.extract_front_end_latencies(metadata)
545+
546+
if gfe_latency is not None:
547+
self.record_gfe_latency(gfe_latency)
548+
else:
549+
self.record_gfe_connectivity_error_count()
550+
551+
if afe_latency is not None:
552+
self.record_afe_latency(afe_latency)
581553
else:
582-
self.record_afe_missing_header_count()
554+
self.record_afe_connectivity_error_count()
583555

584556
def _create_operation_otel_attributes(self) -> dict:
585557
"""

0 commit comments

Comments
 (0)