Skip to content

Commit 5ff9600

Browse files
authored
Adding gRPC instrumentor (#788)
1 parent e9527da commit 5ff9600

File tree

4 files changed

+203
-3
lines changed

4 files changed

+203
-3
lines changed

ext/opentelemetry-ext-grpc/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Add status code to gRPC client spans
66
([896](https://github.com/open-telemetry/opentelemetry-python/pull/896))
7+
- Add gRPC client and server instrumentors
8+
([788](https://github.com/open-telemetry/opentelemetry-python/pull/788))
79

810
## 0.8b0
911

ext/opentelemetry-ext-grpc/setup.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,8 @@ test =
5151

5252
[options.packages.find]
5353
where = src
54+
55+
[options.entry_points]
56+
opentelemetry_instrumentor =
57+
grpc_client = opentelemetry.ext.grpc:GrpcInstrumentorClient
58+
grpc_server = opentelemetry.ext.grpc:GrpcInstrumentorServer

ext/opentelemetry-ext-grpc/src/opentelemetry/ext/grpc/__init__.py

+139-2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,150 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# pylint:disable=import-outside-toplevel
16-
# pylint:disable=import-self
1715
# pylint:disable=no-name-in-module
1816
# pylint:disable=relative-beyond-top-level
17+
# pylint:disable=import-error
18+
# pylint:disable=no-self-use
19+
"""
20+
Usage Client
21+
------------
22+
.. code-block:: python
23+
24+
import logging
25+
26+
import grpc
27+
28+
from opentelemetry import trace
29+
from opentelemetry.ext.grpc import GrpcInstrumentorClient, client_interceptor
30+
from opentelemetry.sdk.trace import TracerProvider
31+
from opentelemetry.sdk.trace.export import (
32+
ConsoleSpanExporter,
33+
SimpleExportSpanProcessor,
34+
)
35+
36+
try:
37+
from .gen import helloworld_pb2, helloworld_pb2_grpc
38+
except ImportError:
39+
from gen import helloworld_pb2, helloworld_pb2_grpc
40+
41+
trace.set_tracer_provider(TracerProvider())
42+
trace.get_tracer_provider().add_span_processor(
43+
SimpleExportSpanProcessor(ConsoleSpanExporter())
44+
)
45+
instrumentor = GrpcInstrumentorClient()
46+
instrumentor.instrument()
47+
48+
def run():
49+
with grpc.insecure_channel("localhost:50051") as channel:
50+
51+
stub = helloworld_pb2_grpc.GreeterStub(channel)
52+
response = stub.SayHello(helloworld_pb2.HelloRequest(name="YOU"))
53+
54+
print("Greeter client received: " + response.message)
55+
56+
57+
if __name__ == "__main__":
58+
logging.basicConfig()
59+
run()
60+
61+
Usage Server
62+
------------
63+
.. code-block:: python
64+
65+
import logging
66+
from concurrent import futures
67+
68+
import grpc
69+
70+
from opentelemetry import trace
71+
from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor
72+
from opentelemetry.ext.grpc.grpcext import intercept_server
73+
from opentelemetry.sdk.trace import TracerProvider
74+
from opentelemetry.sdk.trace.export import (
75+
ConsoleSpanExporter,
76+
SimpleExportSpanProcessor,
77+
)
78+
79+
try:
80+
from .gen import helloworld_pb2, helloworld_pb2_grpc
81+
except ImportError:
82+
from gen import helloworld_pb2, helloworld_pb2_grpc
83+
84+
trace.set_tracer_provider(TracerProvider())
85+
trace.get_tracer_provider().add_span_processor(
86+
SimpleExportSpanProcessor(ConsoleSpanExporter())
87+
)
88+
grpc_server_instrumentor = GrpcInstrumentorServer()
89+
grpc_server_instrumentor.instrument()
90+
91+
92+
class Greeter(helloworld_pb2_grpc.GreeterServicer):
93+
def SayHello(self, request, context):
94+
return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name)
95+
96+
97+
def serve():
98+
99+
server = grpc.server(futures.ThreadPoolExecutor())
100+
101+
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
102+
server.add_insecure_port("[::]:50051")
103+
server.start()
104+
server.wait_for_termination()
105+
106+
107+
if __name__ == "__main__":
108+
logging.basicConfig()
109+
serve()
110+
"""
111+
from contextlib import contextmanager
112+
113+
import grpc
114+
from wrapt import wrap_function_wrapper as _wrap
19115

20116
from opentelemetry import trace
117+
from opentelemetry.ext.grpc.grpcext import intercept_channel, intercept_server
21118
from opentelemetry.ext.grpc.version import __version__
119+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
120+
from opentelemetry.instrumentation.utils import unwrap
121+
122+
# pylint:disable=import-outside-toplevel
123+
# pylint:disable=import-self
124+
# pylint:disable=unused-argument
125+
# isort:skip
126+
127+
128+
class GrpcInstrumentorServer(BaseInstrumentor):
129+
def _instrument(self, **kwargs):
130+
_wrap("grpc", "server", self.wrapper_fn)
131+
132+
def _uninstrument(self, **kwargs):
133+
unwrap(grpc, "server")
134+
135+
def wrapper_fn(self, original_func, instance, args, kwargs):
136+
server = original_func(*args, **kwargs)
137+
return intercept_server(server, server_interceptor())
138+
139+
140+
class GrpcInstrumentorClient(BaseInstrumentor):
141+
def _instrument(self, **kwargs):
142+
if kwargs.get("channel_type") == "secure":
143+
_wrap("grpc", "secure_channel", self.wrapper_fn)
144+
145+
else:
146+
_wrap("grpc", "insecure_channel", self.wrapper_fn)
147+
148+
def _uninstrument(self, **kwargs):
149+
if kwargs.get("channel_type") == "secure":
150+
unwrap(grpc, "secure_channel")
151+
152+
else:
153+
unwrap(grpc, "insecure_channel")
154+
155+
@contextmanager
156+
def wrapper_fn(self, original_func, instance, args, kwargs):
157+
with original_func(*args, **kwargs) as channel:
158+
yield intercept_channel(channel, client_interceptor())
22159

23160

24161
def client_interceptor(tracer_provider=None):

ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py

+57-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import opentelemetry.ext.grpc
2424
from opentelemetry import trace
25-
from opentelemetry.ext.grpc import server_interceptor
25+
from opentelemetry.ext.grpc import GrpcInstrumentorServer, server_interceptor
2626
from opentelemetry.ext.grpc.grpcext import intercept_server
2727
from opentelemetry.sdk import trace as trace_sdk
2828
from opentelemetry.test.test_base import TestBase
@@ -49,6 +49,62 @@ def service(self, handler_call_details):
4949

5050

5151
class TestOpenTelemetryServerInterceptor(TestBase):
52+
def test_instrumentor(self):
53+
def handler(request, context):
54+
return b""
55+
56+
grpc_server_instrumentor = GrpcInstrumentorServer()
57+
grpc_server_instrumentor.instrument()
58+
server = grpc.server(
59+
futures.ThreadPoolExecutor(max_workers=1),
60+
options=(("grpc.so_reuseport", 0),),
61+
)
62+
63+
server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),))
64+
65+
port = server.add_insecure_port("[::]:0")
66+
channel = grpc.insecure_channel("localhost:{:d}".format(port))
67+
68+
try:
69+
server.start()
70+
channel.unary_unary("test")(b"test")
71+
finally:
72+
server.stop(None)
73+
74+
spans_list = self.memory_exporter.get_finished_spans()
75+
self.assertEqual(len(spans_list), 1)
76+
span = spans_list[0]
77+
self.assertEqual(span.name, "test")
78+
self.assertIs(span.kind, trace.SpanKind.SERVER)
79+
self.check_span_instrumentation_info(span, opentelemetry.ext.grpc)
80+
grpc_server_instrumentor.uninstrument()
81+
82+
def test_uninstrument(self):
83+
def handler(request, context):
84+
return b""
85+
86+
grpc_server_instrumentor = GrpcInstrumentorServer()
87+
grpc_server_instrumentor.instrument()
88+
grpc_server_instrumentor.uninstrument()
89+
server = grpc.server(
90+
futures.ThreadPoolExecutor(max_workers=1),
91+
options=(("grpc.so_reuseport", 0),),
92+
)
93+
94+
server.add_generic_rpc_handlers((UnaryUnaryRpcHandler(handler),))
95+
96+
port = server.add_insecure_port("[::]:0")
97+
channel = grpc.insecure_channel("localhost:{:d}".format(port))
98+
99+
try:
100+
server.start()
101+
channel.unary_unary("test")(b"test")
102+
finally:
103+
server.stop(None)
104+
105+
spans_list = self.memory_exporter.get_finished_spans()
106+
self.assertEqual(len(spans_list), 0)
107+
52108
def test_create_span(self):
53109
"""Check that the interceptor wraps calls with spans server-side."""
54110

0 commit comments

Comments
 (0)