Skip to content

Commit 99f2dbf

Browse files
committed
feat: add client debug logging support for async gRPC
1 parent dddf797 commit 99f2dbf

File tree

16 files changed

+721
-186
lines changed

16 files changed

+721
-186
lines changed

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
# gRPC handles serialization and deserialization, so we just need
3333
# to pass in the functions for each.
3434
if "delete_operation" not in self._stubs:
35-
self._stubs["delete_operation"] = self.grpc_channel.unary_unary(
35+
self._stubs["delete_operation"] = self._logged_channel.unary_unary(
3636
"/google.longrunning.Operations/DeleteOperation",
3737
request_serializer=operations_pb2.DeleteOperationRequest.SerializeToString,
3838
response_deserializer=None,
@@ -52,7 +52,7 @@
5252
# gRPC handles serialization and deserialization, so we just need
5353
# to pass in the functions for each.
5454
if "cancel_operation" not in self._stubs:
55-
self._stubs["cancel_operation"] = self.grpc_channel.unary_unary(
55+
self._stubs["cancel_operation"] = self._logged_channel.unary_unary(
5656
"/google.longrunning.Operations/CancelOperation",
5757
request_serializer=operations_pb2.CancelOperationRequest.SerializeToString,
5858
response_deserializer=None,
@@ -72,7 +72,7 @@
7272
# gRPC handles serialization and deserialization, so we just need
7373
# to pass in the functions for each.
7474
if "wait_operation" not in self._stubs:
75-
self._stubs["wait_operation"] = self.grpc_channel.unary_unary(
75+
self._stubs["wait_operation"] = self._logged_channel.unary_unary(
7676
"/google.longrunning.Operations/WaitOperation",
7777
request_serializer=operations_pb2.WaitOperationRequest.SerializeToString,
7878
response_deserializer=None,
@@ -92,7 +92,7 @@
9292
# gRPC handles serialization and deserialization, so we just need
9393
# to pass in the functions for each.
9494
if "get_operation" not in self._stubs:
95-
self._stubs["get_operation"] = self.grpc_channel.unary_unary(
95+
self._stubs["get_operation"] = self._logged_channel.unary_unary(
9696
"/google.longrunning.Operations/GetOperation",
9797
request_serializer=operations_pb2.GetOperationRequest.SerializeToString,
9898
response_deserializer=operations_pb2.Operation.FromString,
@@ -112,7 +112,7 @@
112112
# gRPC handles serialization and deserialization, so we just need
113113
# to pass in the functions for each.
114114
if "list_operations" not in self._stubs:
115-
self._stubs["list_operations"] = self.grpc_channel.unary_unary(
115+
self._stubs["list_operations"] = self._logged_channel.unary_unary(
116116
"/google.longrunning.Operations/ListOperations",
117117
request_serializer=operations_pb2.ListOperationsRequest.SerializeToString,
118118
response_deserializer=operations_pb2.ListOperationsResponse.FromString,
@@ -136,7 +136,7 @@
136136
# gRPC handles serialization and deserialization, so we just need
137137
# to pass in the functions for each.
138138
if "list_locations" not in self._stubs:
139-
self._stubs["list_locations"] = self.grpc_channel.unary_unary(
139+
self._stubs["list_locations"] = self._logged_channel.unary_unary(
140140
"/google.cloud.location.Locations/ListLocations",
141141
request_serializer=locations_pb2.ListLocationsRequest.SerializeToString,
142142
response_deserializer=locations_pb2.ListLocationsResponse.FromString,
@@ -156,7 +156,7 @@
156156
# gRPC handles serialization and deserialization, so we just need
157157
# to pass in the functions for each.
158158
if "get_location" not in self._stubs:
159-
self._stubs["get_location"] = self.grpc_channel.unary_unary(
159+
self._stubs["get_location"] = self._logged_channel.unary_unary(
160160
"/google.cloud.location.Locations/GetLocation",
161161
request_serializer=locations_pb2.GetLocationRequest.SerializeToString,
162162
response_deserializer=locations_pb2.Location.FromString,
@@ -188,7 +188,7 @@
188188
# gRPC handles serialization and deserialization, so we just need
189189
# to pass in the functions for each.
190190
if "set_iam_policy" not in self._stubs:
191-
self._stubs["set_iam_policy"] = self.grpc_channel.unary_unary(
191+
self._stubs["set_iam_policy"] = self._logged_channel.unary_unary(
192192
"/google.iam.v1.IAMPolicy/SetIamPolicy",
193193
request_serializer=iam_policy_pb2.SetIamPolicyRequest.SerializeToString,
194194
response_deserializer=policy_pb2.Policy.FromString,
@@ -216,7 +216,7 @@
216216
# gRPC handles serialization and deserialization, so we just need
217217
# to pass in the functions for each.
218218
if "get_iam_policy" not in self._stubs:
219-
self._stubs["get_iam_policy"] = self.grpc_channel.unary_unary(
219+
self._stubs["get_iam_policy"] = self._logged_channel.unary_unary(
220220
"/google.iam.v1.IAMPolicy/GetIamPolicy",
221221
request_serializer=iam_policy_pb2.GetIamPolicyRequest.SerializeToString,
222222
response_deserializer=policy_pb2.Policy.FromString,
@@ -246,7 +246,7 @@
246246
# gRPC handles serialization and deserialization, so we just need
247247
# to pass in the functions for each.
248248
if "test_iam_permissions" not in self._stubs:
249-
self._stubs["test_iam_permissions"] = self.grpc_channel.unary_unary(
249+
self._stubs["test_iam_permissions"] = self._logged_channel.unary_unary(
250250
"/google.iam.v1.IAMPolicy/TestIamPermissions",
251251
request_serializer=iam_policy_pb2.TestIamPermissionsRequest.SerializeToString,
252252
response_deserializer=iam_policy_pb2.TestIamPermissionsResponse.FromString,

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class _LoggingClientInterceptor(grpc.UnaryUnaryClientInterceptor): # pragma: NO
6969
elif isinstance(request, google.protobuf.message.Message):
7070
request_payload = MessageToJson(request)
7171
else:
72-
request_payload = f"{type(result).__name__}: {pickle.dumps(request)}"
72+
request_payload = f"{type(request).__name__}: {pickle.dumps(request)}"
7373
grpc_request = {
7474
"payload": request_payload,
7575
"requestMethod": "grpc",

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
55

66
import inspect
7+
import json
8+
import pickle
9+
import logging as std_logging
710
import warnings
811
from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union
912

@@ -16,8 +19,11 @@ from google.api_core import operations_v1
1619
{% endif %}
1720
from google.auth import credentials as ga_credentials # type: ignore
1821
from google.auth.transport.grpc import SslCredentials # type: ignore
22+
from google.protobuf.json_format import MessageToJson
23+
import google.protobuf.message
1924

2025
import grpc # type: ignore
26+
import proto # type: ignores
2127
from grpc.experimental import aio # type: ignore
2228

2329
{% filter sort_lines %}
@@ -47,6 +53,76 @@ from google.longrunning import operations_pb2 # type: ignore
4753
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
4854
from .grpc import {{ service.name }}GrpcTransport
4955

56+
try:
57+
from google.api_core import client_logging # type: ignore
58+
CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER
59+
except ImportError: # pragma: NO COVER
60+
CLIENT_LOGGING_SUPPORTED = False
61+
62+
_LOGGER = std_logging.getLogger(__name__)
63+
64+
65+
class _LoggingClientAIOInterceptor(grpc.aio.UnaryUnaryClientInterceptor): # pragma: NO COVER
66+
async def intercept_unary_unary(self, continuation, client_call_details, request):
67+
logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG)
68+
if logging_enabled: # pragma: NO COVER
69+
request_metadata = client_call_details.metadata
70+
if isinstance(request, proto.Message):
71+
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2293): Investigate if we can improve this logic
72+
or wait for next gen protobuf.
73+
#}
74+
request_payload = type(request).to_json(request)
75+
elif isinstance(request, google.protobuf.message.Message):
76+
request_payload = MessageToJson(request)
77+
else:
78+
request_payload = f"{type(request).__name__}: {pickle.dumps(request)}"
79+
grpc_request = {
80+
"payload": request_payload,
81+
"requestMethod": "grpc",
82+
"metadata": dict(request_metadata),
83+
}
84+
_LOGGER.debug(
85+
f"Sending request for {client_call_details.method}",
86+
extra = {
87+
"serviceName": "{{ service.meta.address.proto }}",
88+
"rpcName": str(client_call_details.method),
89+
"request": grpc_request,
90+
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2275): logging `metadata` seems repetitive and may need to be cleaned up. We're including it within "request" for consistency with REST transport.' #}
91+
"metadata": grpc_request["metadata"],
92+
},
93+
)
94+
response = await continuation(client_call_details, request)
95+
if logging_enabled: # pragma: NO COVER
96+
response_metadata = await response.trailing_metadata()
97+
# Convert gRPC metadata `<class 'grpc.aio._metadata.Metadata'>` to list of tuples
98+
metadata = dict([(k, v) for k, v in response_metadata]) if response_metadata else None
99+
result = await response
100+
if isinstance(result, proto.Message):
101+
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2293): Investigate if we can improve this logic
102+
or wait for next gen protobuf.
103+
#}
104+
response_payload = type(result).to_json(result)
105+
elif isinstance(result, google.protobuf.message.Message):
106+
response_payload = MessageToJson(result)
107+
else:
108+
response_payload = f"{type(result).__name__}: {pickle.dumps(result)}"
109+
grpc_response = {
110+
"payload": response_payload,
111+
"metadata": metadata,
112+
"status": "OK",
113+
}
114+
_LOGGER.debug(
115+
f"Received response to rpc {client_call_details.method}.",
116+
extra = {
117+
"serviceName": "{{ service.meta.address.proto }}",
118+
"rpcName": str(client_call_details.method),
119+
"response": grpc_response,
120+
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2275): logging `metadata` seems repetitive and may need to be cleaned up. We're including it within "request" for consistency with REST transport.' #}
121+
"metadata": grpc_response["metadata"],
122+
},
123+
)
124+
return response
125+
50126

51127
class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
52128
"""gRPC AsyncIO backend transport for {{ service.name }}.
@@ -171,7 +247,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
171247
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
172248
and ``credentials_file`` are passed.
173249
"""
174-
self._grpc_channel = None
250+
self._logged_channel = None
175251
self._ssl_channel_credentials = ssl_channel_credentials
176252
self._stubs: Dict[str, Callable] = {}
177253
{% if service.has_lro %}
@@ -188,7 +264,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
188264
credentials = None
189265
self._ignore_credentials = True
190266
# If a channel was explicitly provided, set it.
191-
self._grpc_channel = channel
267+
self._logged_channel = channel
192268
self._ssl_channel_credentials = None
193269
else:
194270
if api_mtls_endpoint:
@@ -223,10 +299,10 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
223299
api_audience=api_audience,
224300
)
225301

226-
if not self._grpc_channel:
302+
if not self._logged_channel:
227303
# initialize with the provided callable or the default channel
228304
channel_init = channel or type(self).create_channel
229-
self._grpc_channel = channel_init(
305+
self._logged_channel = channel_init(
230306
self._host,
231307
# use the credentials which are saved
232308
credentials=self._credentials,
@@ -242,8 +318,10 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
242318
],
243319
)
244320

245-
# Wrap messages. This must be done after self._grpc_channel exists
321+
self._interceptor = _LoggingClientAIOInterceptor()
322+
self._logged_channel._unary_unary_interceptors.append(self._interceptor)
246323
self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters
324+
# Wrap messages. This must be done after self._logged_channel exists
247325
self._prep_wrapped_messages(client_info)
248326

249327
@property
@@ -254,7 +332,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
254332
the same channel.
255333
"""
256334
# Return the channel from cache.
257-
return self._grpc_channel
335+
return self._logged_channel
258336
{% if service.has_lro %}
259337

260338
@property
@@ -267,7 +345,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
267345
# Quick check: Only create a new client if we do not already have one.
268346
if self._operations_client is None:
269347
self._operations_client = operations_v1.OperationsAsyncClient(
270-
self.grpc_channel
348+
self._logged_channel
271349
)
272350

273351
# Return the client from cache.
@@ -297,7 +375,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
297375
# gRPC handles serialization and deserialization, so we just need
298376
# to pass in the functions for each.
299377
if '{{ method.transport_safe_name|snake_case }}' not in self._stubs:
300-
self._stubs['{{ method.transport_safe_name|snake_case }}'] = self.grpc_channel.{{ method.grpc_stub_type }}(
378+
self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}(
301379
'/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}',
302380
request_serializer={{ method.input.ident }}.{% if method.input.ident.python_import.module.endswith('_pb2') %}SerializeToString{% else %}serialize{% endif %},
303381
response_deserializer={{ method.output.ident }}.{% if method.output.ident.python_import.module.endswith('_pb2') %}FromString{% else %}deserialize{% endif %},
@@ -325,7 +403,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
325403
# gRPC handles serialization and deserialization, so we just need
326404
# to pass in the functions for each.
327405
if "set_iam_policy" not in self._stubs:
328-
self._stubs["set_iam_policy"] = self.grpc_channel.unary_unary(
406+
self._stubs["set_iam_policy"] = self._logged_channel.unary_unary(
329407
"/google.iam.v1.IAMPolicy/SetIamPolicy",
330408
request_serializer=iam_policy_pb2.SetIamPolicyRequest.SerializeToString,
331409
response_deserializer=policy_pb2.Policy.FromString,
@@ -351,7 +429,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
351429
# gRPC handles serialization and deserialization, so we just need
352430
# to pass in the functions for each.
353431
if "get_iam_policy" not in self._stubs:
354-
self._stubs["get_iam_policy"] = self.grpc_channel.unary_unary(
432+
self._stubs["get_iam_policy"] = self._logged_channel.unary_unary(
355433
"/google.iam.v1.IAMPolicy/GetIamPolicy",
356434
request_serializer=iam_policy_pb2.GetIamPolicyRequest.SerializeToString,
357435
response_deserializer=policy_pb2.Policy.FromString,
@@ -380,7 +458,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
380458
# gRPC handles serialization and deserialization, so we just need
381459
# to pass in the functions for each.
382460
if "test_iam_permissions" not in self._stubs:
383-
self._stubs["test_iam_permissions"] = self.grpc_channel.unary_unary(
461+
self._stubs["test_iam_permissions"] = self._logged_channel.unary_unary(
384462
"/google.iam.v1.IAMPolicy/TestIamPermissions",
385463
request_serializer=iam_policy_pb2.TestIamPermissionsRequest.SerializeToString,
386464
response_deserializer=iam_policy_pb2.TestIamPermissionsResponse.FromString,
@@ -393,7 +471,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
393471
{{ shared_macros.wrap_async_method_macro()|indent(4) }}
394472

395473
def close(self):
396-
return self.grpc_channel.close()
474+
return self._logged_channel.close()
397475

398476
@property
399477
def kind(self) -> str:
@@ -405,4 +483,4 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
405483
__all__ = (
406484
'{{ service.name }}GrpcAsyncIOTransport',
407485
)
408-
{% endblock %}
486+
{% endblock %}

tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ def get_operation(
10191019
# gRPC handles serialization and deserialization, so we just need
10201020
# to pass in the functions for each.
10211021
if "get_operation" not in self._stubs:
1022-
self._stubs["get_operation"] = self.grpc_channel.unary_unary(
1022+
self._stubs["get_operation"] = self._grpc_channel.unary_unary(
10231023
"/google.longrunning.Operations/GetOperation",
10241024
request_serializer=operations_pb2.GetOperationRequest.SerializeToString,
10251025
response_deserializer=operations_pb2.Operation.FromString,

0 commit comments

Comments
 (0)