fix asynchonous unary call traces (#536)
This commit is contained in:
parent
753e22896a
commit
2ee2cf3cb5
|
|
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
([#545](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/545))
|
([#545](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/545))
|
||||||
- `openelemetry-sdk-extension-aws` Take a dependency on `opentelemetry-sdk`
|
- `openelemetry-sdk-extension-aws` Take a dependency on `opentelemetry-sdk`
|
||||||
([#558](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/558))
|
([#558](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/558))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `opentelemetry-instrumentation-tornado` properly instrument work done in tornado on_finish method.
|
- `opentelemetry-instrumentation-tornado` properly instrument work done in tornado on_finish method.
|
||||||
([#499](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/499))
|
([#499](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/499))
|
||||||
|
|
@ -33,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Updating dependency for opentelemetry api/sdk packages to support major version instead
|
- Updating dependency for opentelemetry api/sdk packages to support major version instead
|
||||||
of pinning to specific versions.
|
of pinning to specific versions.
|
||||||
([#567](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/567))
|
([#567](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/567))
|
||||||
|
- `opentelemetry-instrumentation-grpc` Fixed asynchonous unary call traces
|
||||||
|
([#536](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/536))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation
|
- `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation
|
||||||
|
|
|
||||||
|
|
@ -33,27 +33,6 @@ from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.trace.status import Status, StatusCode
|
from opentelemetry.trace.status import Status, StatusCode
|
||||||
|
|
||||||
|
|
||||||
class _GuardedSpan:
|
|
||||||
def __init__(self, span):
|
|
||||||
self.span = span
|
|
||||||
self.generated_span = None
|
|
||||||
self._engaged = True
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.generated_span = self.span.__enter__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *args, **kwargs):
|
|
||||||
if self._engaged:
|
|
||||||
self.generated_span = None
|
|
||||||
return self.span.__exit__(*args, **kwargs)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def release(self):
|
|
||||||
self._engaged = False
|
|
||||||
return self.span
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
keys as is required by grpc.
|
keys as is required by grpc.
|
||||||
|
|
@ -68,7 +47,7 @@ _carrier_setter = _CarrierSetter()
|
||||||
|
|
||||||
def _make_future_done_callback(span, rpc_info):
|
def _make_future_done_callback(span, rpc_info):
|
||||||
def callback(response_future):
|
def callback(response_future):
|
||||||
with span:
|
with trace.use_span(span, end_on_exit=True):
|
||||||
code = response_future.code()
|
code = response_future.code()
|
||||||
if code != grpc.StatusCode.OK:
|
if code != grpc.StatusCode.OK:
|
||||||
rpc_info.error = code
|
rpc_info.error = code
|
||||||
|
|
@ -85,7 +64,7 @@ class OpenTelemetryClientInterceptor(
|
||||||
def __init__(self, tracer):
|
def __init__(self, tracer):
|
||||||
self._tracer = tracer
|
self._tracer = tracer
|
||||||
|
|
||||||
def _start_span(self, method):
|
def _start_span(self, method, **kwargs):
|
||||||
service, meth = method.lstrip("/").split("/", 1)
|
service, meth = method.lstrip("/").split("/", 1)
|
||||||
attributes = {
|
attributes = {
|
||||||
SpanAttributes.RPC_SYSTEM: "grpc",
|
SpanAttributes.RPC_SYSTEM: "grpc",
|
||||||
|
|
@ -95,16 +74,19 @@ class OpenTelemetryClientInterceptor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return self._tracer.start_as_current_span(
|
return self._tracer.start_as_current_span(
|
||||||
name=method, kind=trace.SpanKind.CLIENT, attributes=attributes
|
name=method,
|
||||||
|
kind=trace.SpanKind.CLIENT,
|
||||||
|
attributes=attributes,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint:disable=no-self-use
|
# pylint:disable=no-self-use
|
||||||
def _trace_result(self, guarded_span, rpc_info, result):
|
def _trace_result(self, span, rpc_info, result):
|
||||||
# If the RPC is called asynchronously, release the guard and add a
|
# If the RPC is called asynchronously, add a callback to end the span
|
||||||
# callback so that the span can be finished once the future is done.
|
# when the future is done, else end the span immediately
|
||||||
if isinstance(result, grpc.Future):
|
if isinstance(result, grpc.Future):
|
||||||
result.add_done_callback(
|
result.add_done_callback(
|
||||||
_make_future_done_callback(guarded_span.release(), rpc_info)
|
_make_future_done_callback(span, rpc_info)
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
response = result
|
response = result
|
||||||
|
|
@ -115,19 +97,22 @@ class OpenTelemetryClientInterceptor(
|
||||||
if isinstance(result, tuple):
|
if isinstance(result, tuple):
|
||||||
response = result[0]
|
response = result[0]
|
||||||
rpc_info.response = response
|
rpc_info.response = response
|
||||||
|
span.end()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _start_guarded_span(self, *args, **kwargs):
|
def _intercept(self, request, metadata, client_info, invoker):
|
||||||
return _GuardedSpan(self._start_span(*args, **kwargs))
|
|
||||||
|
|
||||||
def intercept_unary(self, request, metadata, client_info, invoker):
|
|
||||||
if not metadata:
|
if not metadata:
|
||||||
mutable_metadata = OrderedDict()
|
mutable_metadata = OrderedDict()
|
||||||
else:
|
else:
|
||||||
mutable_metadata = OrderedDict(metadata)
|
mutable_metadata = OrderedDict(metadata)
|
||||||
|
with self._start_span(
|
||||||
with self._start_guarded_span(client_info.full_method) as guarded_span:
|
client_info.full_method,
|
||||||
|
end_on_exit=False,
|
||||||
|
record_exception=False,
|
||||||
|
set_status_on_exception=False,
|
||||||
|
) as span:
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
inject(mutable_metadata, setter=_carrier_setter)
|
inject(mutable_metadata, setter=_carrier_setter)
|
||||||
metadata = tuple(mutable_metadata.items())
|
metadata = tuple(mutable_metadata.items())
|
||||||
|
|
||||||
|
|
@ -138,18 +123,28 @@ class OpenTelemetryClientInterceptor(
|
||||||
request=request,
|
request=request,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
result = invoker(request, metadata)
|
result = invoker(request, metadata)
|
||||||
except grpc.RpcError as err:
|
except Exception as exc:
|
||||||
guarded_span.generated_span.set_status(
|
if isinstance(exc, grpc.RpcError):
|
||||||
Status(StatusCode.ERROR)
|
span.set_attribute(
|
||||||
|
SpanAttributes.RPC_GRPC_STATUS_CODE,
|
||||||
|
exc.code().value[0],
|
||||||
)
|
)
|
||||||
guarded_span.generated_span.set_attribute(
|
span.set_status(
|
||||||
SpanAttributes.RPC_GRPC_STATUS_CODE, err.code().value[0]
|
Status(
|
||||||
|
status_code=StatusCode.ERROR,
|
||||||
|
description="{}: {}".format(type(exc).__name__, exc),
|
||||||
)
|
)
|
||||||
raise err
|
)
|
||||||
|
span.record_exception(exc)
|
||||||
|
raise exc
|
||||||
|
finally:
|
||||||
|
if not result:
|
||||||
|
span.end()
|
||||||
|
return self._trace_result(span, rpc_info, result)
|
||||||
|
|
||||||
return self._trace_result(guarded_span, rpc_info, result)
|
def intercept_unary(self, request, metadata, client_info, invoker):
|
||||||
|
return self._intercept(request, metadata, client_info, invoker)
|
||||||
|
|
||||||
# For RPCs that stream responses, the result can be a generator. To record
|
# For RPCs that stream responses, the result can be a generator. To record
|
||||||
# the span across the generated responses and detect any errors, we wrap
|
# the span across the generated responses and detect any errors, we wrap
|
||||||
|
|
@ -194,32 +189,6 @@ class OpenTelemetryClientInterceptor(
|
||||||
request_or_iterator, metadata, client_info, invoker
|
request_or_iterator, metadata, client_info, invoker
|
||||||
)
|
)
|
||||||
|
|
||||||
if not metadata:
|
return self._intercept(
|
||||||
mutable_metadata = OrderedDict()
|
request_or_iterator, metadata, client_info, invoker
|
||||||
else:
|
|
||||||
mutable_metadata = OrderedDict(metadata)
|
|
||||||
|
|
||||||
with self._start_guarded_span(client_info.full_method) as guarded_span:
|
|
||||||
inject(mutable_metadata, setter=_carrier_setter)
|
|
||||||
metadata = tuple(mutable_metadata.items())
|
|
||||||
rpc_info = RpcInfo(
|
|
||||||
full_method=client_info.full_method,
|
|
||||||
metadata=metadata,
|
|
||||||
timeout=client_info.timeout,
|
|
||||||
request=request_or_iterator,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rpc_info.request = request_or_iterator
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = invoker(request_or_iterator, metadata)
|
|
||||||
except grpc.RpcError as err:
|
|
||||||
guarded_span.generated_span.set_status(
|
|
||||||
Status(StatusCode.ERROR)
|
|
||||||
)
|
|
||||||
guarded_span.generated_span.set_attribute(
|
|
||||||
SpanAttributes.RPC_GRPC_STATUS_CODE, err.code().value[0],
|
|
||||||
)
|
|
||||||
raise err
|
|
||||||
|
|
||||||
return self._trace_result(guarded_span, rpc_info, result)
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ def simple_method(stub, error=False):
|
||||||
stub.SimpleMethod(request)
|
stub.SimpleMethod(request)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_method_future(stub, error=False):
|
||||||
|
request = Request(
|
||||||
|
client_id=CLIENT_ID, request_data="error" if error else "data"
|
||||||
|
)
|
||||||
|
return stub.SimpleMethod.future(request)
|
||||||
|
|
||||||
|
|
||||||
def client_streaming_method(stub, error=False):
|
def client_streaming_method(stub, error=False):
|
||||||
# create a generator
|
# create a generator
|
||||||
def request_messages():
|
def request_messages():
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from ._client import (
|
||||||
client_streaming_method,
|
client_streaming_method,
|
||||||
server_streaming_method,
|
server_streaming_method,
|
||||||
simple_method,
|
simple_method,
|
||||||
|
simple_method_future,
|
||||||
)
|
)
|
||||||
from ._server import create_test_server
|
from ._server import create_test_server
|
||||||
from .protobuf.test_server_pb2 import Request
|
from .protobuf.test_server_pb2 import Request
|
||||||
|
|
@ -100,6 +101,20 @@ class TestClientProto(TestBase):
|
||||||
self.server.stop(None)
|
self.server.stop(None)
|
||||||
self.channel.close()
|
self.channel.close()
|
||||||
|
|
||||||
|
def test_unary_unary_future(self):
|
||||||
|
simple_method_future(self._stub).result()
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Check version and name in span's instrumentation info
|
||||||
|
self.check_span_instrumentation_info(
|
||||||
|
span, opentelemetry.instrumentation.grpc
|
||||||
|
)
|
||||||
|
|
||||||
def test_unary_unary(self):
|
def test_unary_unary(self):
|
||||||
simple_method(self._stub)
|
simple_method(self._stub)
|
||||||
spans = self.memory_exporter.get_finished_spans()
|
spans = self.memory_exporter.get_finished_spans()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue