Skip to content

Commit ade29f6

Browse files
authored
gRPC instrumentation: client additions (#269)
The docs on metric labels suggests that they should probably be strings, and all others I can find are strings, and so these ought to be also. Otherwise, some of the exporters/processors have to handle things specifically, and not all of these come out as nice as could be when you `str()` them. I've also made sure to use the `StatusCode` name, as that's the interesting thing. Finally, there's no need to report specifically that `error=false`, so I've removed that tag.
1 parent 55efeb6 commit ade29f6

File tree

5 files changed

+126
-51
lines changed

5 files changed

+126
-51
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7676
([#246](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/246))
7777
- Update TraceState to adhere to specs
7878
([#276](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/276))
79+
- `opentelemetry-instrumentation-grpc` Updated client attributes, added tests, fixed examples, docs
80+
([#269](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/269))
7981

8082
### Removed
8183
- Remove Configuration

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

+20-16
Original file line numberDiff line numberDiff line change
@@ -185,29 +185,33 @@ class GrpcInstrumentorClient(BaseInstrumentor):
185185
186186
"""
187187

188+
# Figures out which channel type we need to wrap
189+
def _which_channel(self, kwargs):
190+
# handle legacy argument
191+
if "channel_type" in kwargs:
192+
if kwargs.get("channel_type") == "secure":
193+
return ("secure_channel",)
194+
return ("insecure_channel",)
195+
196+
# handle modern arguments
197+
types = []
198+
for ctype in ("secure_channel", "insecure_channel"):
199+
if kwargs.get(ctype, True):
200+
types.append(ctype)
201+
202+
return tuple(types)
203+
188204
def _instrument(self, **kwargs):
189205
exporter = kwargs.get("exporter", None)
190206
interval = kwargs.get("interval", 30)
191-
if kwargs.get("channel_type") == "secure":
207+
for ctype in self._which_channel(kwargs):
192208
_wrap(
193-
"grpc",
194-
"secure_channel",
195-
partial(self.wrapper_fn, exporter, interval),
196-
)
197-
198-
else:
199-
_wrap(
200-
"grpc",
201-
"insecure_channel",
202-
partial(self.wrapper_fn, exporter, interval),
209+
"grpc", ctype, partial(self.wrapper_fn, exporter, interval),
203210
)
204211

205212
def _uninstrument(self, **kwargs):
206-
if kwargs.get("channel_type") == "secure":
207-
unwrap(grpc, "secure_channel")
208-
209-
else:
210-
unwrap(grpc, "insecure_channel")
213+
for ctype in self._which_channel(kwargs):
214+
unwrap(grpc, ctype)
211215

212216
def wrapper_fn(
213217
self, exporter, interval, original_func, instance, args, kwargs

instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,16 @@ def __init__(self, tracer, exporter, interval):
102102
)
103103

104104
def _start_span(self, method):
105+
service, meth = method.lstrip("/").split("/", 1)
106+
attributes = {
107+
"rpc.system": "grpc",
108+
"rpc.grpc.status_code": grpc.StatusCode.OK.value[0],
109+
"rpc.method": meth,
110+
"rpc.service": service,
111+
}
112+
105113
return self._tracer.start_as_current_span(
106-
name=method, kind=trace.SpanKind.CLIENT
114+
name=method, kind=trace.SpanKind.CLIENT, attributes=attributes
107115
)
108116

109117
# pylint:disable=no-self-use
@@ -133,6 +141,7 @@ def _trace_result(self, guarded_span, rpc_info, result, client_info):
133141
self._metrics_recorder.record_bytes_in(
134142
response.ByteSize(), client_info.full_method
135143
)
144+
136145
return result
137146

138147
def _start_guarded_span(self, *args, **kwargs):
@@ -175,11 +184,14 @@ def intercept_unary(self, request, metadata, client_info, invoker):
175184

176185
try:
177186
result = invoker(request, metadata)
178-
except grpc.RpcError:
187+
except grpc.RpcError as err:
179188
guarded_span.generated_span.set_status(
180189
Status(StatusCode.ERROR)
181190
)
182-
raise
191+
guarded_span.generated_span.set_attribute(
192+
"rpc.grpc.status_code", err.code().value[0]
193+
)
194+
raise err
183195

184196
return self._trace_result(
185197
guarded_span, rpc_info, result, client_info
@@ -230,9 +242,12 @@ def _intercept_server_stream(
230242
response.ByteSize(), client_info.full_method
231243
)
232244
yield response
233-
except grpc.RpcError:
245+
except grpc.RpcError as err:
234246
span.set_status(Status(StatusCode.ERROR))
235-
raise
247+
span.set_attribute(
248+
"rpc.grpc.status_code", err.code().value[0]
249+
)
250+
raise err
236251

237252
def intercept_stream(
238253
self, request_or_iterator, metadata, client_info, invoker
@@ -268,11 +283,14 @@ def intercept_stream(
268283

269284
try:
270285
result = invoker(request_or_iterator, metadata)
271-
except grpc.RpcError:
286+
except grpc.RpcError as err:
272287
guarded_span.generated_span.set_status(
273288
Status(StatusCode.ERROR)
274289
)
275-
raise
290+
guarded_span.generated_span.set_attribute(
291+
"rpc.grpc.status_code", err.code().value[0],
292+
)
293+
raise err
276294

277295
return self._trace_result(
278296
guarded_span, rpc_info, result, client_info

instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -72,33 +72,32 @@ def __init__(self, meter, span_kind):
7272

7373
def record_bytes_in(self, bytes_in, method):
7474
if self._meter:
75-
labels = {"method": method}
75+
labels = {"rpc.method": method}
7676
self._bytes_in.add(bytes_in, labels)
7777

7878
def record_bytes_out(self, bytes_out, method):
7979
if self._meter:
80-
labels = {"method": method}
80+
labels = {"rpc.method": method}
8181
self._bytes_out.add(bytes_out, labels)
8282

8383
@contextmanager
8484
def record_latency(self, method):
8585
start_time = time()
8686
labels = {
87-
"method": method,
88-
"status_code": grpc.StatusCode.OK, # pylint:disable=no-member
87+
"rpc.method": method,
88+
"rpc.system": "grpc",
89+
"rpc.grpc.status_code": grpc.StatusCode.OK.name,
8990
}
9091
try:
9192
yield labels
9293
except grpc.RpcError as exc: # pylint:disable=no-member
9394
if self._meter:
9495
# pylint: disable=no-member
95-
labels["status_code"] = exc.code()
96+
labels["rpc.grpc.status_code"] = exc.code().name
9697
self._error_count.add(1, labels)
97-
labels["error"] = True
98+
labels["error"] = "true"
9899
raise
99100
finally:
100101
if self._meter:
101-
if "error" not in labels:
102-
labels["error"] = False
103102
elapsed_time = (time() - start_time) * 1000
104103
self._duration.record(elapsed_time, labels)

instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py

+72-20
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,21 @@ def _verify_success_records(self, num_bytes_out, num_bytes_in, method):
7373

7474
self.assertIsNotNone(bytes_out)
7575
self.assertEqual(bytes_out.instrument.name, "grpcio/client/bytes_out")
76-
self.assertEqual(bytes_out.labels, (("method", method),))
76+
self.assertEqual(bytes_out.labels, (("rpc.method", method),))
7777

7878
self.assertIsNotNone(bytes_in)
7979
self.assertEqual(bytes_in.instrument.name, "grpcio/client/bytes_in")
80-
self.assertEqual(bytes_in.labels, (("method", method),))
80+
self.assertEqual(bytes_in.labels, (("rpc.method", method),))
8181

8282
self.assertIsNotNone(duration)
8383
self.assertEqual(duration.instrument.name, "grpcio/client/duration")
84-
self.assertEqual(
85-
duration.labels,
86-
(
87-
("error", False),
88-
("method", method),
89-
("status_code", grpc.StatusCode.OK),
90-
),
84+
self.assertSequenceEqual(
85+
sorted(duration.labels),
86+
[
87+
("rpc.grpc.status_code", grpc.StatusCode.OK.name),
88+
("rpc.method", method),
89+
("rpc.system", "grpc"),
90+
],
9191
)
9292

9393
self.assertEqual(type(bytes_out.aggregator), SumAggregator)
@@ -116,6 +116,16 @@ def test_unary_unary(self):
116116

117117
self._verify_success_records(8, 8, "/GRPCTestServer/SimpleMethod")
118118

119+
self.assert_span_has_attributes(
120+
span,
121+
{
122+
"rpc.method": "SimpleMethod",
123+
"rpc.service": "GRPCTestServer",
124+
"rpc.system": "grpc",
125+
"rpc.grpc.status_code": grpc.StatusCode.OK.value[0],
126+
},
127+
)
128+
119129
def test_unary_stream(self):
120130
server_streaming_method(self._stub)
121131
spans = self.memory_exporter.get_finished_spans()
@@ -134,6 +144,16 @@ def test_unary_stream(self):
134144
8, 40, "/GRPCTestServer/ServerStreamingMethod"
135145
)
136146

147+
self.assert_span_has_attributes(
148+
span,
149+
{
150+
"rpc.method": "ServerStreamingMethod",
151+
"rpc.service": "GRPCTestServer",
152+
"rpc.system": "grpc",
153+
"rpc.grpc.status_code": grpc.StatusCode.OK.value[0],
154+
},
155+
)
156+
137157
def test_stream_unary(self):
138158
client_streaming_method(self._stub)
139159
spans = self.memory_exporter.get_finished_spans()
@@ -152,6 +172,16 @@ def test_stream_unary(self):
152172
40, 8, "/GRPCTestServer/ClientStreamingMethod"
153173
)
154174

175+
self.assert_span_has_attributes(
176+
span,
177+
{
178+
"rpc.method": "ClientStreamingMethod",
179+
"rpc.service": "GRPCTestServer",
180+
"rpc.system": "grpc",
181+
"rpc.grpc.status_code": grpc.StatusCode.OK.value[0],
182+
},
183+
)
184+
155185
def test_stream_stream(self):
156186
bidirectional_streaming_method(self._stub)
157187
spans = self.memory_exporter.get_finished_spans()
@@ -172,6 +202,16 @@ def test_stream_stream(self):
172202
40, 40, "/GRPCTestServer/BidirectionalStreamingMethod"
173203
)
174204

205+
self.assert_span_has_attributes(
206+
span,
207+
{
208+
"rpc.method": "BidirectionalStreamingMethod",
209+
"rpc.service": "GRPCTestServer",
210+
"rpc.system": "grpc",
211+
"rpc.grpc.status_code": grpc.StatusCode.OK.value[0],
212+
},
213+
)
214+
175215
def _verify_error_records(self, method):
176216
# pylint: disable=protected-access,no-member
177217
self.channel._interceptor.controller.tick()
@@ -195,21 +235,33 @@ def _verify_error_records(self, method):
195235
self.assertIsNotNone(duration)
196236

197237
self.assertEqual(errors.instrument.name, "grpcio/client/errors")
198-
self.assertEqual(
199-
errors.labels,
200-
(
201-
("method", method),
202-
("status_code", grpc.StatusCode.INVALID_ARGUMENT),
238+
self.assertSequenceEqual(
239+
sorted(errors.labels),
240+
sorted(
241+
(
242+
(
243+
"rpc.grpc.status_code",
244+
grpc.StatusCode.INVALID_ARGUMENT.name,
245+
),
246+
("rpc.method", method),
247+
("rpc.system", "grpc"),
248+
)
203249
),
204250
)
205251
self.assertEqual(errors.aggregator.checkpoint, 1)
206252

207-
self.assertEqual(
208-
duration.labels,
209-
(
210-
("error", True),
211-
("method", method),
212-
("status_code", grpc.StatusCode.INVALID_ARGUMENT),
253+
self.assertSequenceEqual(
254+
sorted(duration.labels),
255+
sorted(
256+
(
257+
("error", "true"),
258+
("rpc.method", method),
259+
("rpc.system", "grpc"),
260+
(
261+
"rpc.grpc.status_code",
262+
grpc.StatusCode.INVALID_ARGUMENT.name,
263+
),
264+
)
213265
),
214266
)
215267

0 commit comments

Comments
 (0)