mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-30 05:32:30 +08:00
added request and response hooks for grpc client (#1706)
This commit is contained in:
@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- `opentelemetry-instrumentation-system-metrics` Add `process.` prefix to `runtime.memory`, `runtime.cpu.time`, and `runtime.gc_count`. Change `runtime.memory` from count to UpDownCounter. ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735))
|
- `opentelemetry-instrumentation-system-metrics` Add `process.` prefix to `runtime.memory`, `runtime.cpu.time`, and `runtime.gc_count`. Change `runtime.memory` from count to UpDownCounter. ([#1735](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1735))
|
||||||
|
- Add request and response hooks for GRPC instrumentation (client only)
|
||||||
|
([#1706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1706))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -434,6 +434,8 @@ class GrpcInstrumentorClient(BaseInstrumentor):
|
|||||||
else:
|
else:
|
||||||
filter_ = any_of(filter_, excluded_service_filter)
|
filter_ = any_of(filter_, excluded_service_filter)
|
||||||
self._filter = filter_
|
self._filter = filter_
|
||||||
|
self._request_hook = None
|
||||||
|
self._response_hook = None
|
||||||
|
|
||||||
# Figures out which channel type we need to wrap
|
# Figures out which channel type we need to wrap
|
||||||
def _which_channel(self, kwargs):
|
def _which_channel(self, kwargs):
|
||||||
@ -455,6 +457,8 @@ class GrpcInstrumentorClient(BaseInstrumentor):
|
|||||||
return _instruments
|
return _instruments
|
||||||
|
|
||||||
def _instrument(self, **kwargs):
|
def _instrument(self, **kwargs):
|
||||||
|
self._request_hook = kwargs.get("request_hook")
|
||||||
|
self._response_hook = kwargs.get("response_hook")
|
||||||
for ctype in self._which_channel(kwargs):
|
for ctype in self._which_channel(kwargs):
|
||||||
_wrap(
|
_wrap(
|
||||||
"grpc",
|
"grpc",
|
||||||
@ -469,11 +473,15 @@ class GrpcInstrumentorClient(BaseInstrumentor):
|
|||||||
def wrapper_fn(self, original_func, instance, args, kwargs):
|
def wrapper_fn(self, original_func, instance, args, kwargs):
|
||||||
channel = original_func(*args, **kwargs)
|
channel = original_func(*args, **kwargs)
|
||||||
tracer_provider = kwargs.get("tracer_provider")
|
tracer_provider = kwargs.get("tracer_provider")
|
||||||
|
request_hook = self._request_hook
|
||||||
|
response_hook = self._response_hook
|
||||||
return intercept_channel(
|
return intercept_channel(
|
||||||
channel,
|
channel,
|
||||||
client_interceptor(
|
client_interceptor(
|
||||||
tracer_provider=tracer_provider,
|
tracer_provider=tracer_provider,
|
||||||
filter_=self._filter,
|
filter_=self._filter,
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -499,6 +507,8 @@ class GrpcAioInstrumentorClient(BaseInstrumentor):
|
|||||||
else:
|
else:
|
||||||
filter_ = any_of(filter_, excluded_service_filter)
|
filter_ = any_of(filter_, excluded_service_filter)
|
||||||
self._filter = filter_
|
self._filter = filter_
|
||||||
|
self._request_hook = None
|
||||||
|
self._response_hook = None
|
||||||
|
|
||||||
def instrumentation_dependencies(self) -> Collection[str]:
|
def instrumentation_dependencies(self) -> Collection[str]:
|
||||||
return _instruments
|
return _instruments
|
||||||
@ -507,13 +517,19 @@ class GrpcAioInstrumentorClient(BaseInstrumentor):
|
|||||||
if "interceptors" in kwargs and kwargs["interceptors"]:
|
if "interceptors" in kwargs and kwargs["interceptors"]:
|
||||||
kwargs["interceptors"] = (
|
kwargs["interceptors"] = (
|
||||||
aio_client_interceptors(
|
aio_client_interceptors(
|
||||||
tracer_provider=tracer_provider, filter_=self._filter
|
tracer_provider=tracer_provider,
|
||||||
|
filter_=self._filter,
|
||||||
|
request_hook=self._request_hook,
|
||||||
|
response_hook=self._response_hook,
|
||||||
)
|
)
|
||||||
+ kwargs["interceptors"]
|
+ kwargs["interceptors"]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
kwargs["interceptors"] = aio_client_interceptors(
|
kwargs["interceptors"] = aio_client_interceptors(
|
||||||
tracer_provider=tracer_provider, filter_=self._filter
|
tracer_provider=tracer_provider,
|
||||||
|
filter_=self._filter,
|
||||||
|
request_hook=self._request_hook,
|
||||||
|
response_hook=self._response_hook,
|
||||||
)
|
)
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -521,6 +537,8 @@ class GrpcAioInstrumentorClient(BaseInstrumentor):
|
|||||||
def _instrument(self, **kwargs):
|
def _instrument(self, **kwargs):
|
||||||
self._original_insecure = grpc.aio.insecure_channel
|
self._original_insecure = grpc.aio.insecure_channel
|
||||||
self._original_secure = grpc.aio.secure_channel
|
self._original_secure = grpc.aio.secure_channel
|
||||||
|
self._request_hook = kwargs.get("request_hook")
|
||||||
|
self._response_hook = kwargs.get("response_hook")
|
||||||
tracer_provider = kwargs.get("tracer_provider")
|
tracer_provider = kwargs.get("tracer_provider")
|
||||||
|
|
||||||
def insecure(*args, **kwargs):
|
def insecure(*args, **kwargs):
|
||||||
@ -541,7 +559,9 @@ class GrpcAioInstrumentorClient(BaseInstrumentor):
|
|||||||
grpc.aio.secure_channel = self._original_secure
|
grpc.aio.secure_channel = self._original_secure
|
||||||
|
|
||||||
|
|
||||||
def client_interceptor(tracer_provider=None, filter_=None):
|
def client_interceptor(
|
||||||
|
tracer_provider=None, filter_=None, request_hook=None, response_hook=None
|
||||||
|
):
|
||||||
"""Create a gRPC client channel interceptor.
|
"""Create a gRPC client channel interceptor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -558,7 +578,12 @@ def client_interceptor(tracer_provider=None, filter_=None):
|
|||||||
|
|
||||||
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
|
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
|
||||||
|
|
||||||
return _client.OpenTelemetryClientInterceptor(tracer, filter_=filter_)
|
return _client.OpenTelemetryClientInterceptor(
|
||||||
|
tracer,
|
||||||
|
filter_=filter_,
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def server_interceptor(tracer_provider=None, filter_=None):
|
def server_interceptor(tracer_provider=None, filter_=None):
|
||||||
@ -581,7 +606,9 @@ def server_interceptor(tracer_provider=None, filter_=None):
|
|||||||
return _server.OpenTelemetryServerInterceptor(tracer, filter_=filter_)
|
return _server.OpenTelemetryServerInterceptor(tracer, filter_=filter_)
|
||||||
|
|
||||||
|
|
||||||
def aio_client_interceptors(tracer_provider=None, filter_=None):
|
def aio_client_interceptors(
|
||||||
|
tracer_provider=None, filter_=None, request_hook=None, response_hook=None
|
||||||
|
):
|
||||||
"""Create a gRPC client channel interceptor.
|
"""Create a gRPC client channel interceptor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -595,10 +622,30 @@ def aio_client_interceptors(tracer_provider=None, filter_=None):
|
|||||||
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
|
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
_aio_client.UnaryUnaryAioClientInterceptor(tracer, filter_=filter_),
|
_aio_client.UnaryUnaryAioClientInterceptor(
|
||||||
_aio_client.UnaryStreamAioClientInterceptor(tracer, filter_=filter_),
|
tracer,
|
||||||
_aio_client.StreamUnaryAioClientInterceptor(tracer, filter_=filter_),
|
filter_=filter_,
|
||||||
_aio_client.StreamStreamAioClientInterceptor(tracer, filter_=filter_),
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
),
|
||||||
|
_aio_client.UnaryStreamAioClientInterceptor(
|
||||||
|
tracer,
|
||||||
|
filter_=filter_,
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
),
|
||||||
|
_aio_client.StreamUnaryAioClientInterceptor(
|
||||||
|
tracer,
|
||||||
|
filter_=filter_,
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
),
|
||||||
|
_aio_client.StreamStreamAioClientInterceptor(
|
||||||
|
tracer,
|
||||||
|
filter_=filter_,
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
@ -28,8 +29,10 @@ from opentelemetry.propagate import inject
|
|||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.trace.status import Status, StatusCode
|
from opentelemetry.trace.status import Status, StatusCode
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _unary_done_callback(span, code, details):
|
|
||||||
|
def _unary_done_callback(span, code, details, response_hook):
|
||||||
def callback(call):
|
def callback(call):
|
||||||
try:
|
try:
|
||||||
span.set_attribute(
|
span.set_attribute(
|
||||||
@ -43,6 +46,8 @@ def _unary_done_callback(span, code, details):
|
|||||||
description=details,
|
description=details,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
response_hook(span, details)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
span.end()
|
span.end()
|
||||||
|
|
||||||
@ -110,7 +115,11 @@ class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor):
|
|||||||
code = await call.code()
|
code = await call.code()
|
||||||
details = await call.details()
|
details = await call.details()
|
||||||
|
|
||||||
call.add_done_callback(_unary_done_callback(span, code, details))
|
call.add_done_callback(
|
||||||
|
_unary_done_callback(
|
||||||
|
span, code, details, self._call_response_hook
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return call
|
return call
|
||||||
except grpc.aio.AioRpcError as exc:
|
except grpc.aio.AioRpcError as exc:
|
||||||
@ -120,6 +129,8 @@ class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor):
|
|||||||
async def _wrap_stream_response(self, span, call):
|
async def _wrap_stream_response(self, span, call):
|
||||||
try:
|
try:
|
||||||
async for response in call:
|
async for response in call:
|
||||||
|
if self._response_hook:
|
||||||
|
self._call_response_hook(span, response)
|
||||||
yield response
|
yield response
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.add_error_details_to_span(span, exc)
|
self.add_error_details_to_span(span, exc)
|
||||||
@ -151,6 +162,9 @@ class UnaryUnaryAioClientInterceptor(
|
|||||||
) as span:
|
) as span:
|
||||||
new_details = self.propagate_trace_in_details(client_call_details)
|
new_details = self.propagate_trace_in_details(client_call_details)
|
||||||
|
|
||||||
|
if self._request_hook:
|
||||||
|
self._call_request_hook(span, request)
|
||||||
|
|
||||||
continuation_with_args = functools.partial(
|
continuation_with_args = functools.partial(
|
||||||
continuation, new_details, request
|
continuation, new_details, request
|
||||||
)
|
)
|
||||||
@ -175,7 +189,8 @@ class UnaryStreamAioClientInterceptor(
|
|||||||
new_details = self.propagate_trace_in_details(client_call_details)
|
new_details = self.propagate_trace_in_details(client_call_details)
|
||||||
|
|
||||||
resp = await continuation(new_details, request)
|
resp = await continuation(new_details, request)
|
||||||
|
if self._request_hook:
|
||||||
|
self._call_request_hook(span, request)
|
||||||
return self._wrap_stream_response(span, resp)
|
return self._wrap_stream_response(span, resp)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
|
|
||||||
"""Implementation of the invocation-side open-telemetry interceptor."""
|
"""Implementation of the invocation-side open-telemetry interceptor."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import MutableMapping
|
from typing import Callable, MutableMapping
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
@ -33,6 +34,8 @@ from opentelemetry.propagators.textmap import Setter
|
|||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.trace.status import Status, StatusCode
|
from opentelemetry.trace.status import Status, StatusCode
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _CarrierSetter(Setter):
|
class _CarrierSetter(Setter):
|
||||||
"""We use a custom setter in order to be able to lower case
|
"""We use a custom setter in order to be able to lower case
|
||||||
@ -59,12 +62,27 @@ def _make_future_done_callback(span, rpc_info):
|
|||||||
return callback
|
return callback
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_invoke(function: Callable, *args):
|
||||||
|
function_name = "<unknown>"
|
||||||
|
try:
|
||||||
|
function_name = function.__name__
|
||||||
|
function(*args)
|
||||||
|
except Exception as ex: # pylint:disable=broad-except
|
||||||
|
logger.error(
|
||||||
|
"Error when invoking function '%s'", function_name, exc_info=ex
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OpenTelemetryClientInterceptor(
|
class OpenTelemetryClientInterceptor(
|
||||||
grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor
|
grpcext.UnaryClientInterceptor, grpcext.StreamClientInterceptor
|
||||||
):
|
):
|
||||||
def __init__(self, tracer, filter_=None):
|
def __init__(
|
||||||
|
self, tracer, filter_=None, request_hook=None, response_hook=None
|
||||||
|
):
|
||||||
self._tracer = tracer
|
self._tracer = tracer
|
||||||
self._filter = filter_
|
self._filter = filter_
|
||||||
|
self._request_hook = request_hook
|
||||||
|
self._response_hook = response_hook
|
||||||
|
|
||||||
def _start_span(self, method, **kwargs):
|
def _start_span(self, method, **kwargs):
|
||||||
service, meth = method.lstrip("/").split("/", 1)
|
service, meth = method.lstrip("/").split("/", 1)
|
||||||
@ -99,6 +117,8 @@ class OpenTelemetryClientInterceptor(
|
|||||||
if isinstance(result, tuple):
|
if isinstance(result, tuple):
|
||||||
response = result[0]
|
response = result[0]
|
||||||
rpc_info.response = response
|
rpc_info.response = response
|
||||||
|
if self._response_hook:
|
||||||
|
self._call_response_hook(span, response)
|
||||||
span.end()
|
span.end()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -127,7 +147,8 @@ class OpenTelemetryClientInterceptor(
|
|||||||
timeout=client_info.timeout,
|
timeout=client_info.timeout,
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
|
if self._request_hook:
|
||||||
|
self._call_request_hook(span, request)
|
||||||
result = invoker(request, metadata)
|
result = invoker(request, metadata)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if isinstance(exc, grpc.RpcError):
|
if isinstance(exc, grpc.RpcError):
|
||||||
@ -148,6 +169,16 @@ class OpenTelemetryClientInterceptor(
|
|||||||
span.end()
|
span.end()
|
||||||
return self._trace_result(span, rpc_info, result)
|
return self._trace_result(span, rpc_info, result)
|
||||||
|
|
||||||
|
def _call_request_hook(self, span, request):
|
||||||
|
if not callable(self._request_hook):
|
||||||
|
return
|
||||||
|
_safe_invoke(self._request_hook, span, request)
|
||||||
|
|
||||||
|
def _call_response_hook(self, span, response):
|
||||||
|
if not callable(self._response_hook):
|
||||||
|
return
|
||||||
|
_safe_invoke(self._response_hook, span, response)
|
||||||
|
|
||||||
def intercept_unary(self, request, metadata, client_info, invoker):
|
def intercept_unary(self, request, metadata, client_info, invoker):
|
||||||
if self._filter is not None and not self._filter(client_info):
|
if self._filter is not None and not self._filter(client_info):
|
||||||
return invoker(request, metadata)
|
return invoker(request, metadata)
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
# 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.
|
||||||
|
try:
|
||||||
|
from unittest import IsolatedAsyncioTestCase
|
||||||
|
except ImportError:
|
||||||
|
# unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use
|
||||||
|
# simplifies the following tests. Without it, the amount of test code
|
||||||
|
# increases significantly, with most of the additional code handling
|
||||||
|
# the asyncio set up.
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
class IsolatedAsyncioTestCase(TestCase):
|
||||||
|
def run(self, result=None):
|
||||||
|
self.skipTest(
|
||||||
|
"This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient
|
||||||
|
from opentelemetry.test.test_base import TestBase
|
||||||
|
|
||||||
|
from ._aio_client import simple_method
|
||||||
|
from ._server import create_test_server
|
||||||
|
from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module
|
||||||
|
|
||||||
|
|
||||||
|
def request_hook(span, request):
|
||||||
|
span.set_attribute("request_data", request.request_data)
|
||||||
|
|
||||||
|
|
||||||
|
def response_hook(span, response):
|
||||||
|
span.set_attribute("response_data", response)
|
||||||
|
|
||||||
|
|
||||||
|
def request_hook_with_exception(_span, _request):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
|
def response_hook_with_exception(_span, _response):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
class TestAioClientInterceptorWithHooks(TestBase, IsolatedAsyncioTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.server = create_test_server(25565)
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
self.server.stop(None)
|
||||||
|
|
||||||
|
async def test_request_and_response_hooks(self):
|
||||||
|
instrumentor = GrpcAioInstrumentorClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
instrumentor.instrument(
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = grpc.aio.insecure_channel(
|
||||||
|
"localhost:25565",
|
||||||
|
)
|
||||||
|
stub = test_server_pb2_grpc.GRPCTestServerStub(channel)
|
||||||
|
|
||||||
|
response = await simple_method(stub)
|
||||||
|
assert response.response_data == "data"
|
||||||
|
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 1)
|
||||||
|
span = spans[0]
|
||||||
|
|
||||||
|
self.assertIn("request_data", span.attributes)
|
||||||
|
self.assertEqual(span.attributes["request_data"], "data")
|
||||||
|
|
||||||
|
self.assertIn("response_data", span.attributes)
|
||||||
|
self.assertEqual(span.attributes["response_data"], "")
|
||||||
|
finally:
|
||||||
|
instrumentor.uninstrument()
|
||||||
|
|
||||||
|
async def test_hooks_with_exception(self):
|
||||||
|
instrumentor = GrpcAioInstrumentorClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
instrumentor.instrument(
|
||||||
|
request_hook=request_hook_with_exception,
|
||||||
|
response_hook=response_hook_with_exception,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = grpc.aio.insecure_channel(
|
||||||
|
"localhost:25565",
|
||||||
|
)
|
||||||
|
stub = test_server_pb2_grpc.GRPCTestServerStub(channel)
|
||||||
|
|
||||||
|
response = await simple_method(stub)
|
||||||
|
assert response.response_data == "data"
|
||||||
|
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 1)
|
||||||
|
span = spans[0]
|
||||||
|
|
||||||
|
self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod")
|
||||||
|
finally:
|
||||||
|
instrumentor.uninstrument()
|
@ -0,0 +1,149 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
from tests.protobuf import ( # pylint: disable=no-name-in-module
|
||||||
|
test_server_pb2_grpc,
|
||||||
|
)
|
||||||
|
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
|
||||||
|
from opentelemetry.test.test_base import TestBase
|
||||||
|
|
||||||
|
from ._client import simple_method
|
||||||
|
from ._server import create_test_server
|
||||||
|
|
||||||
|
|
||||||
|
# User defined interceptor. Is used in the tests along with the opentelemetry client interceptor.
|
||||||
|
class Interceptor(
|
||||||
|
grpc.UnaryUnaryClientInterceptor,
|
||||||
|
grpc.UnaryStreamClientInterceptor,
|
||||||
|
grpc.StreamUnaryClientInterceptor,
|
||||||
|
grpc.StreamStreamClientInterceptor,
|
||||||
|
):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def intercept_unary_unary(
|
||||||
|
self, continuation, client_call_details, request
|
||||||
|
):
|
||||||
|
return self._intercept_call(continuation, client_call_details, request)
|
||||||
|
|
||||||
|
def intercept_unary_stream(
|
||||||
|
self, continuation, client_call_details, request
|
||||||
|
):
|
||||||
|
return self._intercept_call(continuation, client_call_details, request)
|
||||||
|
|
||||||
|
def intercept_stream_unary(
|
||||||
|
self, continuation, client_call_details, request_iterator
|
||||||
|
):
|
||||||
|
return self._intercept_call(
|
||||||
|
continuation, client_call_details, request_iterator
|
||||||
|
)
|
||||||
|
|
||||||
|
def intercept_stream_stream(
|
||||||
|
self, continuation, client_call_details, request_iterator
|
||||||
|
):
|
||||||
|
return self._intercept_call(
|
||||||
|
continuation, client_call_details, request_iterator
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _intercept_call(
|
||||||
|
continuation, client_call_details, request_or_iterator
|
||||||
|
):
|
||||||
|
return continuation(client_call_details, request_or_iterator)
|
||||||
|
|
||||||
|
|
||||||
|
def request_hook(span, request):
|
||||||
|
span.set_attribute("request_data", request.request_data)
|
||||||
|
|
||||||
|
|
||||||
|
def response_hook(span, response):
|
||||||
|
span.set_attribute("response_data", response.response_data)
|
||||||
|
|
||||||
|
|
||||||
|
def request_hook_with_exception(_span, _request):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
|
def response_hook_with_exception(_span, _response):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHooks(TestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.server = create_test_server(25565)
|
||||||
|
self.server.start()
|
||||||
|
# use a user defined interceptor along with the opentelemetry client interceptor
|
||||||
|
self.interceptors = [Interceptor()]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
self.server.stop(None)
|
||||||
|
|
||||||
|
def test_response_and_request_hooks(self):
|
||||||
|
instrumentor = GrpcInstrumentorClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
instrumentor.instrument(
|
||||||
|
request_hook=request_hook,
|
||||||
|
response_hook=response_hook,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = grpc.insecure_channel("localhost:25565")
|
||||||
|
channel = grpc.intercept_channel(channel, *self.interceptors)
|
||||||
|
|
||||||
|
stub = test_server_pb2_grpc.GRPCTestServerStub(channel)
|
||||||
|
|
||||||
|
simple_method(stub)
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 1)
|
||||||
|
span = spans[0]
|
||||||
|
|
||||||
|
self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod")
|
||||||
|
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||||
|
|
||||||
|
self.assertIn("request_data", span.attributes)
|
||||||
|
self.assertEqual(span.attributes["request_data"], "data")
|
||||||
|
|
||||||
|
self.assertIn("response_data", span.attributes)
|
||||||
|
self.assertEqual(span.attributes["response_data"], "data")
|
||||||
|
finally:
|
||||||
|
instrumentor.uninstrument()
|
||||||
|
|
||||||
|
def test_hooks_with_exception(self):
|
||||||
|
instrumentor = GrpcInstrumentorClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
instrumentor.instrument(
|
||||||
|
request_hook=request_hook_with_exception,
|
||||||
|
response_hook=response_hook_with_exception,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = grpc.insecure_channel("localhost:25565")
|
||||||
|
channel = grpc.intercept_channel(channel, *self.interceptors)
|
||||||
|
|
||||||
|
stub = test_server_pb2_grpc.GRPCTestServerStub(channel)
|
||||||
|
|
||||||
|
simple_method(stub)
|
||||||
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(spans), 1)
|
||||||
|
span = spans[0]
|
||||||
|
|
||||||
|
self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod")
|
||||||
|
self.assertIs(span.kind, trace.SpanKind.CLIENT)
|
||||||
|
finally:
|
||||||
|
instrumentor.uninstrument()
|
Reference in New Issue
Block a user