@@ -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.
0 commit comments