24
24
25
25
import grpc
26
26
27
- from opentelemetry import propagators , trace
27
+ from opentelemetry import metrics , propagators , trace
28
+ from opentelemetry .sdk .metrics .export .controller import PushController
28
29
from opentelemetry .trace .status import Status , StatusCanonicalCode
29
30
30
31
from . import grpcext
31
- from ._utilities import RpcInfo
32
+ from ._utilities import RpcInfo , TimedMetricRecorder
32
33
33
34
34
35
class _GuardedSpan :
@@ -63,7 +64,7 @@ def append_metadata(
63
64
propagators .inject (append_metadata , metadata )
64
65
65
66
66
- def _make_future_done_callback (span , rpc_info ):
67
+ def _make_future_done_callback (span , rpc_info , client_info , metrics_recorder ):
67
68
def callback (response_future ):
68
69
with span :
69
70
code = response_future .code ()
@@ -72,28 +73,45 @@ def callback(response_future):
72
73
return
73
74
response = response_future .result ()
74
75
rpc_info .response = response
76
+ if "ByteSize" in dir (response ):
77
+ metrics_recorder .record_bytes_in (
78
+ response .ByteSize (), client_info .full_method
79
+ )
75
80
76
81
return callback
77
82
78
83
79
84
class OpenTelemetryClientInterceptor (
80
85
grpcext .UnaryClientInterceptor , grpcext .StreamClientInterceptor
81
86
):
82
- def __init__ (self , tracer ):
87
+ def __init__ (self , tracer , exporter , interval ):
83
88
self ._tracer = tracer
84
89
90
+ self ._meter = None
91
+ if exporter and interval :
92
+ self ._meter = metrics .get_meter (__name__ )
93
+ self .controller = PushController (
94
+ meter = self ._meter , exporter = exporter , interval = interval
95
+ )
96
+ self ._metrics_recorder = TimedMetricRecorder (self ._meter , "client" )
97
+
85
98
def _start_span (self , method ):
86
99
return self ._tracer .start_as_current_span (
87
100
name = method , kind = trace .SpanKind .CLIENT
88
101
)
89
102
90
103
# pylint:disable=no-self-use
91
- def _trace_result (self , guarded_span , rpc_info , result ):
104
+ def _trace_result (self , guarded_span , rpc_info , result , client_info ):
92
105
# If the RPC is called asynchronously, release the guard and add a
93
106
# callback so that the span can be finished once the future is done.
94
107
if isinstance (result , grpc .Future ):
95
108
result .add_done_callback (
96
- _make_future_done_callback (guarded_span .release (), rpc_info )
109
+ _make_future_done_callback (
110
+ guarded_span .release (),
111
+ rpc_info ,
112
+ client_info ,
113
+ self ._metrics_recorder ,
114
+ )
97
115
)
98
116
return result
99
117
response = result
@@ -104,37 +122,62 @@ def _trace_result(self, guarded_span, rpc_info, result):
104
122
if isinstance (result , tuple ):
105
123
response = result [0 ]
106
124
rpc_info .response = response
125
+
126
+ if "ByteSize" in dir (response ):
127
+ self ._metrics_recorder .record_bytes_in (
128
+ response .ByteSize (), client_info .full_method
129
+ )
107
130
return result
108
131
109
132
def _start_guarded_span (self , * args , ** kwargs ):
110
133
return _GuardedSpan (self ._start_span (* args , ** kwargs ))
111
134
135
+ def _bytes_out_iterator_wrapper (self , iterator , client_info ):
136
+ for request in iterator :
137
+ if "ByteSize" in dir (request ):
138
+ self ._metrics_recorder .record_bytes_out (
139
+ request .ByteSize (), client_info .full_method
140
+ )
141
+ yield request
142
+
112
143
def intercept_unary (self , request , metadata , client_info , invoker ):
113
144
if not metadata :
114
145
mutable_metadata = OrderedDict ()
115
146
else :
116
147
mutable_metadata = OrderedDict (metadata )
117
148
118
149
with self ._start_guarded_span (client_info .full_method ) as guarded_span :
119
- _inject_span_context (mutable_metadata )
120
- metadata = tuple (mutable_metadata .items ())
121
-
122
- rpc_info = RpcInfo (
123
- full_method = client_info .full_method ,
124
- metadata = metadata ,
125
- timeout = client_info .timeout ,
126
- request = request ,
127
- )
128
-
129
- try :
130
- result = invoker (request , metadata )
131
- except grpc .RpcError as exc :
132
- guarded_span .generated_span .set_status (
133
- Status (StatusCanonicalCode (exc .code ().value [0 ]))
150
+ with self ._metrics_recorder .record_latency (
151
+ client_info .full_method
152
+ ):
153
+ _inject_span_context (mutable_metadata )
154
+ metadata = tuple (mutable_metadata .items ())
155
+
156
+ # If protobuf is used, we can record the bytes in/out. Otherwise, we have no way
157
+ # to get the size of the request/response properly, so don't record anything
158
+ if "ByteSize" in dir (request ):
159
+ self ._metrics_recorder .record_bytes_out (
160
+ request .ByteSize (), client_info .full_method
161
+ )
162
+
163
+ rpc_info = RpcInfo (
164
+ full_method = client_info .full_method ,
165
+ metadata = metadata ,
166
+ timeout = client_info .timeout ,
167
+ request = request ,
134
168
)
135
- raise
136
169
137
- return self ._trace_result (guarded_span , rpc_info , result )
170
+ try :
171
+ result = invoker (request , metadata )
172
+ except grpc .RpcError as exc :
173
+ guarded_span .generated_span .set_status (
174
+ Status (StatusCanonicalCode (exc .code ().value [0 ]))
175
+ )
176
+ raise
177
+
178
+ return self ._trace_result (
179
+ guarded_span , rpc_info , result , client_info
180
+ )
138
181
139
182
# For RPCs that stream responses, the result can be a generator. To record
140
183
# the span across the generated responses and detect any errors, we wrap
@@ -148,25 +191,44 @@ def _intercept_server_stream(
148
191
mutable_metadata = OrderedDict (metadata )
149
192
150
193
with self ._start_span (client_info .full_method ) as span :
151
- _inject_span_context (mutable_metadata )
152
- metadata = tuple (mutable_metadata .items ())
153
- rpc_info = RpcInfo (
154
- full_method = client_info .full_method ,
155
- metadata = metadata ,
156
- timeout = client_info .timeout ,
157
- )
158
- if client_info .is_client_stream :
159
- rpc_info .request = request_or_iterator
160
-
161
- try :
162
- result = invoker (request_or_iterator , metadata )
163
- for response in result :
164
- yield response
165
- except grpc .RpcError as exc :
166
- span .set_status (
167
- Status (StatusCanonicalCode (exc .code ().value [0 ]))
194
+ with self ._metrics_recorder .record_latency (
195
+ client_info .full_method
196
+ ):
197
+ _inject_span_context (mutable_metadata )
198
+ metadata = tuple (mutable_metadata .items ())
199
+ rpc_info = RpcInfo (
200
+ full_method = client_info .full_method ,
201
+ metadata = metadata ,
202
+ timeout = client_info .timeout ,
168
203
)
169
- raise
204
+
205
+ if client_info .is_client_stream :
206
+ rpc_info .request = request_or_iterator
207
+ request_or_iterator = self ._bytes_out_iterator_wrapper (
208
+ request_or_iterator , client_info
209
+ )
210
+ else :
211
+ if "ByteSize" in dir (request_or_iterator ):
212
+ self ._metrics_recorder .record_bytes_out (
213
+ request_or_iterator .ByteSize (),
214
+ client_info .full_method ,
215
+ )
216
+
217
+ try :
218
+ result = invoker (request_or_iterator , metadata )
219
+
220
+ # Rewrap the result stream into a generator, and record the bytes received
221
+ for response in result :
222
+ if "ByteSize" in dir (response ):
223
+ self ._metrics_recorder .record_bytes_in (
224
+ response .ByteSize (), client_info .full_method
225
+ )
226
+ yield response
227
+ except grpc .RpcError as exc :
228
+ span .set_status (
229
+ Status (StatusCanonicalCode (exc .code ().value [0 ]))
230
+ )
231
+ raise
170
232
171
233
def intercept_stream (
172
234
self , request_or_iterator , metadata , client_info , invoker
@@ -182,21 +244,32 @@ def intercept_stream(
182
244
mutable_metadata = OrderedDict (metadata )
183
245
184
246
with self ._start_guarded_span (client_info .full_method ) as guarded_span :
185
- _inject_span_context (mutable_metadata )
186
- metadata = tuple (mutable_metadata .items ())
187
- rpc_info = RpcInfo (
188
- full_method = client_info .full_method ,
189
- metadata = metadata ,
190
- timeout = client_info .timeout ,
191
- request = request_or_iterator ,
192
- )
247
+ with self ._metrics_recorder .record_latency (
248
+ client_info .full_method
249
+ ):
250
+ _inject_span_context (mutable_metadata )
251
+ metadata = tuple (mutable_metadata .items ())
252
+ rpc_info = RpcInfo (
253
+ full_method = client_info .full_method ,
254
+ metadata = metadata ,
255
+ timeout = client_info .timeout ,
256
+ request = request_or_iterator ,
257
+ )
258
+
259
+ rpc_info .request = request_or_iterator
193
260
194
- try :
195
- result = invoker (request_or_iterator , metadata )
196
- except grpc .RpcError as exc :
197
- guarded_span .generated_span .set_status (
198
- Status (StatusCanonicalCode (exc .code ().value [0 ]))
261
+ request_or_iterator = self ._bytes_out_iterator_wrapper (
262
+ request_or_iterator , client_info
199
263
)
200
- raise
201
264
202
- return self ._trace_result (guarded_span , rpc_info , result )
265
+ try :
266
+ result = invoker (request_or_iterator , metadata )
267
+ except grpc .RpcError as exc :
268
+ guarded_span .generated_span .set_status (
269
+ Status (StatusCanonicalCode (exc .code ().value [0 ]))
270
+ )
271
+ raise
272
+
273
+ return self ._trace_result (
274
+ guarded_span , rpc_info , result , client_info
275
+ )
0 commit comments