Repository navigation
feat(google-api-core): extend ErrorInfo dict accessors, centralize status code extraction, and update StructuredLogFormatter - #18884
Conversation
There was a problem hiding this comment.
Code Review
This pull request enhances client logging and exception handling in google-api-core. It expands recognized logging fields, optimizes field extraction using pre-compiled allowlists, and updates StructuredLogFormatter to unpack record.json_fields and extract allowed fields from vars(record). Additionally, it updates exception properties to support dictionary-based error info and centralizes the _extract_status_code utility. The review feedback suggests reversing the processing order in StructuredLogFormatter so that custom fields in json_fields take precedence over automatically extracted attributes from vars(record).
| # 1. Unpack record.json_fields (keeping api_core and google-cloud-logging formatters consistent) | ||
| json_fields = getattr(record, "json_fields", None) | ||
| if isinstance(json_fields, collections.abc.Mapping): | ||
| for key, value in json_fields.items(): | ||
| if value is not None: | ||
| log_obj[key] = value | ||
|
|
||
| # 2. Extract allowed fields in a single pass over vars(record) | ||
| for key, value in vars(record).items(): | ||
| if value is not None and ( | ||
| key in _EXACT_FIELDS or key.startswith(_PREFIX_FIELDS) | ||
| ): | ||
| log_obj[key] = value |
There was a problem hiding this comment.
Currently, record.json_fields is unpacked first, and then the allowed fields from vars(record) are extracted and added to log_obj. If a key exists in both json_fields and vars(record) (for example, if a user explicitly overrides a standard/allowed field in json_fields with a custom value), the value from vars(record) will overwrite the custom value from json_fields because step 2 runs after step 1.
To ensure that explicitly provided custom fields in json_fields take precedence over automatically extracted attributes from vars(record), we should reverse the order of these two steps.
| # 1. Unpack record.json_fields (keeping api_core and google-cloud-logging formatters consistent) | |
| json_fields = getattr(record, "json_fields", None) | |
| if isinstance(json_fields, collections.abc.Mapping): | |
| for key, value in json_fields.items(): | |
| if value is not None: | |
| log_obj[key] = value | |
| # 2. Extract allowed fields in a single pass over vars(record) | |
| for key, value in vars(record).items(): | |
| if value is not None and ( | |
| key in _EXACT_FIELDS or key.startswith(_PREFIX_FIELDS) | |
| ): | |
| log_obj[key] = value | |
| # 1. Extract allowed fields in a single pass over vars(record) | |
| for key, value in vars(record).items(): | |
| if value is not None and ( | |
| key in _EXACT_FIELDS or key.startswith(_PREFIX_FIELDS) | |
| ): | |
| log_obj[key] = value | |
| # 2. Unpack record.json_fields (keeping api_core and google-cloud-logging formatters consistent) | |
| json_fields = getattr(record, "json_fields", None) | |
| if isinstance(json_fields, collections.abc.Mapping): | |
| for key, value in json_fields.items(): | |
| if value is not None: | |
| log_obj[key] = value |
e4c70cf to
f4d9d6a
Compare
7568958 to
781a900
Compare
f4d9d6a to
3240601
Compare
781a900 to
aeacfb7
Compare
3240601 to
7fad61b
Compare
aeacfb7 to
813aa30
Compare
7fad61b to
858b5dc
Compare
813aa30 to
91cde6b
Compare
858b5dc to
f6ab8ea
Compare
91cde6b to
d6f0e97
Compare
f6ab8ea to
024c8b8
Compare
d6f0e97 to
9bca1d0
Compare
…de extraction, and update StructuredLogFormatter
024c8b8 to
b1c49d0
Compare
9bca1d0 to
07e6fa4
Compare
Problem / Context
To support uniform structured error logging and OpenTelemetry telemetry across both gRPC and REST transports in
google-api-core, several foundational utilities need to handle both transport formats consistently:ErrorInfoAccessors:GoogleAPICallError.reason,.domain, and.metadatapreviously assumedself._error_infowas always a protobufErrorInfomessage (as returned by gRPCgoogle.rpc.status_pb2.Status). For REST JSON error payloads,ErrorInfoentries inerror["details"]are parsed as standard Pythondictobjects, causing attribute access (self._error_info.reason) to fail or returnNone.GoogleAPICallErrorand rawgrpc.RpcErrorinstances (_extract_status_code) lived insidegoogle.api_core.gapic_v1.method, preventing reuse by transport-level observability helpers.StructuredLogFormatteronly recognized legacy camelCase fields and matched fields via exact set membership (in _recognized_logging_fields). It did not unpackrecord.json_fieldsor support OpenTelemetry semantic conventions (such asrpc.*,http.*,url.*,server.*,error.type,exception.*, and dynamicgcp.errors.metadata.*keys).Solution
google/api_core/exceptions.py:GoogleAPICallError.reason,.domain, and.metadatato inspectisinstance(self._error_info, dict)first before falling back to protobuf attribute access._extract_status_code(exc, default=None)togoogle.api_core.exceptionsso bothgapic_v1.methodand transport observability helpers can share a single status code normalization implementation.google/api_core/gapic_v1/method.py:_extract_status_code = exceptions._extract_status_codeto preserve backward compatibility while eliminating duplicate code.google/api_core/client_logging.py:_recognized_logging_fieldswith OpenTelemetry semantic convention attributes, Google Cloud error domain/metadata fields (gcp.errors.domain,gcp.errors.metadata,gcp.errors.metadata.*), and trace correlation keys (logging.googleapis.com/trace,logging.googleapis.com/spanId,logging.googleapis.com/trace_sampled)._EXACT_FIELDS(frozenset) and_PREFIX_FIELDS(tuple) at module import time and updatedStructuredLogFormatter.formatto unpackrecord.json_fieldsand filter recognized fields in a single pass.tests/unit/test_exceptions.pyandtests/unit/test_client_logging.pyachieving 100% line and branch coverage on modified modules.