44{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
55
66import inspect
7+ import json
8+ import pickle
9+ import logging as std_logging
710import warnings
811from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union
912
@@ -16,8 +19,11 @@ from google.api_core import operations_v1
1619{% endif %}
1720from google.auth import credentials as ga_credentials # type: ignore
1821from google.auth.transport.grpc import SslCredentials # type: ignore
22+ from google.protobuf.json_format import MessageToJson
23+ import google.protobuf.message
1924
2025import grpc # type: ignore
26+ import proto # type: ignore
2127from grpc.experimental import aio # type: ignore
2228
2329{% filter sort_lines %}
@@ -47,6 +53,81 @@ from google.longrunning import operations_pb2 # type: ignore
4753from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
4854from .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+
80+ request_metadata = {
81+ key: value.decode("utf-8") if isinstance(value, bytes) else value
82+ for key, value in request_metadata
83+ }
84+ grpc_request = {
85+ "payload": request_payload,
86+ "requestMethod": "grpc",
87+ "metadata": dict(request_metadata),
88+ }
89+ _LOGGER.debug(
90+ f"Sending request for {client_call_details.method}",
91+ extra = {
92+ "serviceName": "{{ service.meta.address.proto }}",
93+ "rpcName": str(client_call_details.method),
94+ "request": grpc_request,
95+ {# 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.' #}
96+ "metadata": grpc_request["metadata"],
97+ },
98+ )
99+ response = await continuation(client_call_details, request)
100+ if logging_enabled: # pragma: NO COVER
101+ response_metadata = await response.trailing_metadata()
102+ # Convert gRPC metadata `<class ' grpc.aio._metadata.Metadata' >` to list of tuples
103+ metadata = dict([(k, str(v)) for k, v in response_metadata]) if response_metadata else None
104+ result = await response
105+ if isinstance(result, proto.Message):
106+ {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2293): Investigate if we can improve this logic
107+ or wait for next gen protobuf.
108+ #}
109+ response_payload = type(result).to_json(result)
110+ elif isinstance(result, google.protobuf.message.Message):
111+ response_payload = MessageToJson(result)
112+ else:
113+ response_payload = f"{type(result).__name__}: {pickle.dumps(result)}"
114+ grpc_response = {
115+ "payload": response_payload,
116+ "metadata": metadata,
117+ "status": "OK",
118+ }
119+ _LOGGER.debug(
120+ f"Received response to rpc {client_call_details.method}.",
121+ extra = {
122+ "serviceName": "{{ service.meta.address.proto }}",
123+ "rpcName": str(client_call_details.method),
124+ "response": grpc_response,
125+ {# 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.' #}
126+ "metadata": grpc_response["metadata"],
127+ },
128+ )
129+ return response
130+
50131
51132class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
52133 """gRPC AsyncIO backend transport for {{ service.name }}.
@@ -242,8 +323,11 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
242323 ],
243324 )
244325
245- # Wrap messages. This must be done after self._grpc_channel exists
326+ self._interceptor = _LoggingClientAIOInterceptor()
327+ self._grpc_channel._unary_unary_interceptors.append(self._interceptor)
328+ self._logged_channel = self._grpc_channel
246329 self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters
330+ # Wrap messages. This must be done after self._logged_channel exists
247331 self._prep_wrapped_messages(client_info)
248332
249333 @property
@@ -267,7 +351,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
267351 # Quick check: Only create a new client if we do not already have one.
268352 if self._operations_client is None:
269353 self._operations_client = operations_v1.OperationsAsyncClient(
270- self.grpc_channel
354+ self._logged_channel
271355 )
272356
273357 # Return the client from cache.
@@ -297,7 +381,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
297381 # gRPC handles serialization and deserialization, so we just need
298382 # to pass in the functions for each.
299383 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 }}(
384+ self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel .{{ method.grpc_stub_type }}(
301385 '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}',
302386 request_serializer={{ method.input.ident }}.{% if method .input .ident .python_import .module .endswith ('_pb2' ) %} SerializeToString{% else %} serialize{% endif %} ,
303387 response_deserializer={{ method.output.ident }}.{% if method .output .ident .python_import .module .endswith ('_pb2' ) %} FromString{% else %} deserialize{% endif %} ,
@@ -325,7 +409,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
325409 # gRPC handles serialization and deserialization, so we just need
326410 # to pass in the functions for each.
327411 if "set_iam_policy" not in self._stubs:
328- self._stubs["set_iam_policy"] = self.grpc_channel .unary_unary(
412+ self._stubs["set_iam_policy"] = self._logged_channel .unary_unary(
329413 "/google.iam.v1.IAMPolicy/SetIamPolicy",
330414 request_serializer=iam_policy_pb2.SetIamPolicyRequest.SerializeToString,
331415 response_deserializer=policy_pb2.Policy.FromString,
@@ -351,7 +435,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
351435 # gRPC handles serialization and deserialization, so we just need
352436 # to pass in the functions for each.
353437 if "get_iam_policy" not in self._stubs:
354- self._stubs["get_iam_policy"] = self.grpc_channel .unary_unary(
438+ self._stubs["get_iam_policy"] = self._logged_channel .unary_unary(
355439 "/google.iam.v1.IAMPolicy/GetIamPolicy",
356440 request_serializer=iam_policy_pb2.GetIamPolicyRequest.SerializeToString,
357441 response_deserializer=policy_pb2.Policy.FromString,
@@ -380,7 +464,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
380464 # gRPC handles serialization and deserialization, so we just need
381465 # to pass in the functions for each.
382466 if "test_iam_permissions" not in self._stubs:
383- self._stubs["test_iam_permissions"] = self.grpc_channel .unary_unary(
467+ self._stubs["test_iam_permissions"] = self._logged_channel .unary_unary(
384468 "/google.iam.v1.IAMPolicy/TestIamPermissions",
385469 request_serializer=iam_policy_pb2.TestIamPermissionsRequest.SerializeToString,
386470 response_deserializer=iam_policy_pb2.TestIamPermissionsResponse.FromString,
@@ -393,7 +477,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
393477 {{ shared_macros.wrap_async_method_macro()|indent(4) }}
394478
395479 def close(self):
396- return self.grpc_channel .close()
480+ return self._logged_channel .close()
397481
398482 @property
399483 def kind(self) -> str:
@@ -405,4 +489,4 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
405489__all__ = (
406490 '{{ service.name }}GrpcAsyncIOTransport',
407491)
408- {% endblock %}
492+ {% endblock %}
0 commit comments