Skip to content

Commit 30a2939

Browse files
committed
feat: add client logging support for sync gRPC
1 parent b10cc21 commit 30a2939

File tree

8 files changed

+678
-156
lines changed
  • gapic/templates/%namespace/%name_%version/%sub/services/%service/transports
  • tests/integration/goldens
    • asset/google/cloud/asset_v1/services/asset_service/transports
    • credentials/google/iam/credentials_v1/services/iam_credentials/transports
    • eventarc/google/cloud/eventarc_v1/services/eventarc/transports
    • logging/google/cloud/logging_v2/services
      • config_service_v2/transports
      • logging_service_v2/transports
      • metrics_service_v2/transports
    • redis/google/cloud/redis_v1/services/cloud_redis/transports

8 files changed

+678
-156
lines changed

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

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
{% block content %}
44

5+
import logging as std_logging
56
import warnings
67
from typing import Callable, Dict, Optional, Sequence, Tuple, Union
78

@@ -13,6 +14,7 @@ from google.api_core import gapic_v1
1314
import google.auth # type: ignore
1415
from google.auth import credentials as ga_credentials # type: ignore
1516
from google.auth.transport.grpc import SslCredentials # type: ignore
17+
from google.protobuf.json_format import MessageToJson
1618

1719
import grpc # type: ignore
1820

@@ -42,6 +44,66 @@ from google.longrunning import operations_pb2 # type: ignore
4244
{% endfilter %}
4345
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
4446

47+
try:
48+
from google.api_core import client_logging # type: ignore
49+
CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER
50+
except ImportError: # pragma: NO COVER
51+
CLIENT_LOGGING_SUPPORTED = False
52+
53+
_LOGGER = std_logging.getLogger(__name__)
54+
55+
56+
class _LoggingClientInterceptor(grpc.UnaryUnaryClientInterceptor): # pragma: NO COVER
57+
def intercept_unary_unary(self, continuation, client_call_details, request):
58+
request_metadata = client_call_details.metadata
59+
if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG):
60+
try:
61+
request_payload = type(request).to_json(request)
62+
except:
63+
request_payload = MessageToJson(request)
64+
grpc_request = {
65+
"payload": request_payload,
66+
"requestMethod": "grpc",
67+
"metadata": dict(request_metadata),
68+
}
69+
_LOGGER.debug(
70+
f"Sending request for {client_call_details.method}",
71+
extra = {
72+
"serviceName": "{{ service.meta.address.proto }}",
73+
"rpcName": client_call_details.method,
74+
"request": grpc_request,
75+
{# 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.' #}
76+
"metadata": grpc_request["metadata"],
77+
},
78+
)
79+
80+
response = continuation(client_call_details, request)
81+
response_metadata = response.trailing_metadata()
82+
# Convert gRPC metadata `<class 'grpc.aio._metadata.Metadata'>` to list of tuples
83+
metadata = dict([(k, v) for k, v in response_metadata]) if response_metadata else None
84+
result = response.result()
85+
if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER
86+
try:
87+
response_payload = type(result).to_json(result)
88+
except:
89+
response_payload = MessageToJson(result)
90+
grpc_response = {
91+
"payload": response_payload,
92+
"metadata": metadata,
93+
"status": "OK",
94+
}
95+
_LOGGER.debug(
96+
f"Received response for {client_call_details.method}.",
97+
extra = {
98+
"serviceName": "{{ service.meta.address.proto }}",
99+
"rpcName": client_call_details.method,
100+
"response": grpc_response,
101+
{# 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.' #}
102+
"metadata": grpc_response["metadata"],
103+
},
104+
)
105+
return response
106+
45107

46108
class {{ service.name }}GrpcTransport({{ service.name }}Transport):
47109
"""gRPC backend transport for {{ service.name }}.
@@ -123,7 +185,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
123185
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
124186
and ``credentials_file`` are passed.
125187
"""
126-
self._grpc_channel = None
188+
self._base_channel = None
127189
self._ssl_channel_credentials = ssl_channel_credentials
128190
self._stubs: Dict[str, Callable] = {}
129191
{% if service.has_lro %}
@@ -140,7 +202,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
140202
credentials = None
141203
self._ignore_credentials = True
142204
# If a channel was explicitly provided, set it.
143-
self._grpc_channel = channel
205+
self._base_channel = channel
144206
self._ssl_channel_credentials = None
145207

146208
else:
@@ -176,10 +238,10 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
176238
api_audience=api_audience,
177239
)
178240

179-
if not self._grpc_channel:
241+
if not self._base_channel:
180242
# initialize with the provided callable or the default channel
181243
channel_init = channel or type(self).create_channel
182-
self._grpc_channel = channel_init(
244+
self._base_channel = channel_init(
183245
self._host,
184246
# use the credentials which are saved
185247
credentials=self._credentials,
@@ -195,6 +257,11 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
195257
],
196258
)
197259

260+
self._grpc_channel = self._base_channel
261+
if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER
262+
self._interceptor = _LoggingClientInterceptor()
263+
self._grpc_channel = grpc.intercept_channel(self._base_channel, self._interceptor)
264+
198265
# Wrap messages. This must be done after self._grpc_channel exists
199266
self._prep_wrapped_messages(client_info)
200267

@@ -248,7 +315,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
248315
def grpc_channel(self) -> grpc.Channel:
249316
"""Return the channel designed to connect to this service.
250317
"""
251-
return self._grpc_channel
318+
return self._base_channel
252319

253320
{% if service.has_lro %}
254321

@@ -262,7 +329,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
262329
# Quick check: Only create a new client if we do not already have one.
263330
if self._operations_client is None:
264331
self._operations_client = operations_v1.OperationsClient(
265-
self.grpc_channel
332+
self._grpc_channel
266333
)
267334

268335
# Return the client from cache.
@@ -292,7 +359,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
292359
# gRPC handles serialization and deserialization, so we just need
293360
# to pass in the functions for each.
294361
if '{{ method.transport_safe_name|snake_case }}' not in self._stubs:
295-
self._stubs['{{ method.transport_safe_name|snake_case }}'] = self.grpc_channel.{{ method.grpc_stub_type }}(
362+
self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._grpc_channel.{{ method.grpc_stub_type }}(
296363
'/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}',
297364
request_serializer={{ method.input.ident }}.{% if method.input.ident.python_import.module.endswith('_pb2') %}SerializeToString{% else %}serialize{% endif %},
298365
response_deserializer={{ method.output.ident }}.{% if method.output.ident.python_import.module.endswith('_pb2') %}FromString{% else %}deserialize{% endif %},
@@ -320,7 +387,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
320387
# gRPC handles serialization and deserialization, so we just need
321388
# to pass in the functions for each.
322389
if "set_iam_policy" not in self._stubs:
323-
self._stubs["set_iam_policy"] = self.grpc_channel.unary_unary(
390+
self._stubs["set_iam_policy"] = self._grpc_channel.unary_unary(
324391
"/google.iam.v1.IAMPolicy/SetIamPolicy",
325392
request_serializer=iam_policy_pb2.SetIamPolicyRequest.SerializeToString,
326393
response_deserializer=policy_pb2.Policy.FromString,
@@ -346,7 +413,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
346413
# gRPC handles serialization and deserialization, so we just need
347414
# to pass in the functions for each.
348415
if "get_iam_policy" not in self._stubs:
349-
self._stubs["get_iam_policy"] = self.grpc_channel.unary_unary(
416+
self._stubs["get_iam_policy"] = self._grpc_channel.unary_unary(
350417
"/google.iam.v1.IAMPolicy/GetIamPolicy",
351418
request_serializer=iam_policy_pb2.GetIamPolicyRequest.SerializeToString,
352419
response_deserializer=policy_pb2.Policy.FromString,
@@ -374,7 +441,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
374441
# gRPC handles serialization and deserialization, so we just need
375442
# to pass in the functions for each.
376443
if "test_iam_permissions" not in self._stubs:
377-
self._stubs["test_iam_permissions"] = self.grpc_channel.unary_unary(
444+
self._stubs["test_iam_permissions"] = self._grpc_channel.unary_unary(
378445
"/google.iam.v1.IAMPolicy/TestIamPermissions",
379446
request_serializer=iam_policy_pb2.TestIamPermissionsRequest.SerializeToString,
380447
response_deserializer=iam_policy_pb2.TestIamPermissionsResponse.FromString,
@@ -383,7 +450,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
383450
{% endif %}
384451

385452
def close(self):
386-
self.grpc_channel.close()
453+
self._grpc_channel.close()
387454

388455
{% include '%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2' %}
389456

0 commit comments

Comments
 (0)