Skip to content

Commit

Permalink
Add grpc.aio client-side support
Browse files Browse the repository at this point in the history
This adds support for instrumenting grpc.aio channels with spans and
telemetry. The instrumentation needed to work differently that the
standard grpc channel support but is functionally the same.
  • Loading branch information
cookiefission committed Sep 2, 2022
1 parent 03e021b commit 13b87a7
Show file tree
Hide file tree
Showing 6 changed files with 797 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,59 @@ def serve():
server = grpc.server(futures.ThreadPoolExecutor(),
interceptors = [server_interceptor()])
Usage Aio Client
------------
.. code-block:: python
import logging
import asyncio
import grpc
from opentelemetry import trace
from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
try:
from .gen import helloworld_pb2, helloworld_pb2_grpc
except ImportError:
from gen import helloworld_pb2, helloworld_pb2_grpc
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
SimpleSpanProcessor(ConsoleSpanExporter())
)
grpc_client_instrumentor = GrpcAioInstrumentorClient()
grpc_client_instrumentor.instrument()
async def run():
with grpc.aio.insecure_channel("localhost:50051") as channel:
stub = helloworld_pb2_grpc.GreeterStub(channel)
response = await stub.SayHello(helloworld_pb2.HelloRequest(name="YOU"))
print("Greeter client received: " + response.message)
if __name__ == "__main__":
logging.basicConfig()
asyncio.run(run())
You can also add the interceptor manually, rather than using
:py:class:`~opentelemetry.instrumentation.grpc.GrpcAioInstrumentorClient`:
.. code-block:: python
from opentelemetry.instrumentation.grpc import aio_client_interceptors
channel = grpc.aio.insecure_channel("localhost:12345", interceptors=aio_client_interceptors())
Usage Aio Server
------------
.. code-block:: python
Expand Down Expand Up @@ -321,6 +374,58 @@ def wrapper_fn(self, original_func, instance, args, kwargs):
)


class GrpcAioInstrumentorClient(BaseInstrumentor):
"""
Globally instrument the grpc.aio client.
Usage::
grpc_aio_client_instrumentor = GrpcAioInstrumentorClient()
grpc_aio_client_instrumentor.instrument()
"""

# pylint:disable=attribute-defined-outside-init, redefined-outer-name

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _add_interceptors(self, tracer_provider, kwargs):
if "interceptors" in kwargs and kwargs["interceptors"]:
kwargs["interceptors"] = (
aio_client_interceptors(tracer_provider=tracer_provider)
+ kwargs["interceptors"]
)
else:
kwargs["interceptors"] = aio_client_interceptors(
tracer_provider=tracer_provider
)

return kwargs

def _instrument(self, **kwargs):
self._original_insecure = grpc.aio.insecure_channel
self._original_secure = grpc.aio.secure_channel
tracer_provider = kwargs.get("tracer_provider")

def insecure(*args, **kwargs):
kwargs = self._add_interceptors(tracer_provider, kwargs)

return self._original_insecure(*args, **kwargs)

def secure(*args, **kwargs):
kwargs = self._add_interceptors(tracer_provider, kwargs)

return self._original_secure(*args, **kwargs)

grpc.aio.insecure_channel = insecure
grpc.aio.secure_channel = secure

def _uninstrument(self, **kwargs):
grpc.aio.insecure_channel = self._original_insecure
grpc.aio.secure_channel = self._original_secure


def client_interceptor(tracer_provider=None):
"""Create a gRPC client channel interceptor.
Expand Down Expand Up @@ -353,6 +458,27 @@ def server_interceptor(tracer_provider=None):
return _server.OpenTelemetryServerInterceptor(tracer)


def aio_client_interceptors(tracer_provider=None):
"""Create a gRPC client channel interceptor.
Args:
tracer: The tracer to use to create client-side spans.
Returns:
An invocation-side interceptor object.
"""
from . import _aio_client

tracer = trace.get_tracer(__name__, __version__, tracer_provider)

return [
_aio_client.UnaryUnaryAioClientInterceptor(tracer),
_aio_client.UnaryStreamAioClientInterceptor(tracer),
_aio_client.StreamUnaryAioClientInterceptor(tracer),
_aio_client.StreamStreamAioClientInterceptor(tracer),
]


def aio_server_interceptor(tracer_provider=None):
"""Create a gRPC aio server interceptor.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import OrderedDict
import functools

import grpc
from grpc.aio import ClientCallDetails

from opentelemetry import context, trace
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.propagate import inject
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
from opentelemetry.instrumentation.grpc.version import __version__

from opentelemetry.trace.status import Status, StatusCode

from opentelemetry.instrumentation.grpc._client import (
OpenTelemetryClientInterceptor,
_carrier_setter,
)


def _unary_done_callback(span, code, details):
def callback(call):
try:
span.set_attribute(
SpanAttributes.RPC_GRPC_STATUS_CODE,
code.value[0],
)
if code != grpc.StatusCode.OK:
span.set_status(
Status(
status_code=StatusCode.ERROR,
description=details,
)
)
finally:
span.end()

return callback


class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor):
@staticmethod
def propagate_trace_in_details(client_call_details):
method = client_call_details.method.decode("utf-8")
metadata = client_call_details.metadata
if not metadata:
mutable_metadata = OrderedDict()
else:
mutable_metadata = OrderedDict(metadata)

inject(mutable_metadata, setter=_carrier_setter)
metadata = tuple(mutable_metadata.items())

return ClientCallDetails(
client_call_details.method,
client_call_details.timeout,
metadata,
client_call_details.credentials,
client_call_details.wait_for_ready,
)

@staticmethod
def add_error_details_to_span(span, exc):
if isinstance(exc, grpc.RpcError):
span.set_attribute(
SpanAttributes.RPC_GRPC_STATUS_CODE,
exc.code().value[0],
)
span.set_status(
Status(
status_code=StatusCode.ERROR,
description=f"{type(exc).__name__}: {exc}",
)
)
span.record_exception(exc)

async def _wrap_unary_response(self, continuation, span):
try:
call = await continuation()

# code and details are both coroutines that need to be await-ed,
# the callbacks added with add_done_callback do not allow async
# code so we need to get the code and details here then pass them
# to the callback.
code = await call.code()
details = await call.details()

call.add_done_callback(_unary_done_callback(span, code, details))

return call
except grpc.aio.AioRpcError as exc:
self.add_error_details_to_span(span, exc)
raise exc

async def _wrap_stream_response(self, span, call):
try:
async for response in call:
yield response
except Exception as exc:
self.add_error_details_to_span(span, exc)
raise exc
finally:
span.end()


class UnaryUnaryAioClientInterceptor(
grpc.aio.UnaryUnaryClientInterceptor,
_BaseAioClientInterceptor,
):
async def intercept_unary_unary(
self, continuation, client_call_details, request
):
if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return await continuation(client_call_details, request)

method = client_call_details.method.decode("utf-8")
with self._start_span(
method,
end_on_exit=False,
record_exception=False,
set_status_on_exception=False,
) as span:
new_details = self.propagate_trace_in_details(client_call_details)

continuation_with_args = functools.partial(
continuation, new_details, request
)
return await self._wrap_unary_response(
continuation_with_args, span
)


class UnaryStreamAioClientInterceptor(
grpc.aio.UnaryStreamClientInterceptor,
_BaseAioClientInterceptor,
):
async def intercept_unary_stream(
self, continuation, client_call_details, request
):
if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return await continuation(client_call_details, request)

method = client_call_details.method.decode("utf-8")
with self._start_span(
method,
end_on_exit=False,
record_exception=False,
set_status_on_exception=False,
) as span:
new_details = self.propagate_trace_in_details(client_call_details)

resp = await continuation(new_details, request)

return self._wrap_stream_response(span, resp)


class StreamUnaryAioClientInterceptor(
grpc.aio.StreamUnaryClientInterceptor,
_BaseAioClientInterceptor,
):
async def intercept_stream_unary(
self, continuation, client_call_details, request_iterator
):
if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return await continuation(client_call_details, request_iterator)

method = client_call_details.method.decode("utf-8")
with self._start_span(
method,
end_on_exit=False,
record_exception=False,
set_status_on_exception=False,
) as span:
new_details = self.propagate_trace_in_details(client_call_details)

continuation_with_args = functools.partial(
continuation, new_details, request_iterator
)
return await self._wrap_unary_response(
continuation_with_args, span
)


class StreamStreamAioClientInterceptor(
grpc.aio.StreamStreamClientInterceptor,
_BaseAioClientInterceptor,
):
async def intercept_stream_stream(
self, continuation, client_call_details, request_iterator
):
if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return await continuation(client_call_details, request_iterator)

method = client_call_details.method.decode("utf-8")
with self._start_span(
method,
end_on_exit=False,
record_exception=False,
set_status_on_exception=False,
) as span:
new_details = self.propagate_trace_in_details(client_call_details)

resp = await continuation(new_details, request_iterator)

return self._wrap_stream_response(span, resp)

0 comments on commit 13b87a7

Please sign in to comment.