Skip to content

Commit 460041f

Browse files
Corvin LasoggaCoLa5
Corvin Lasogga
authored andcommitted
added event metric recorder
1 parent dbeed1b commit 460041f

File tree

1 file changed

+375
-16
lines changed
  • instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc

1 file changed

+375
-16
lines changed

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

Lines changed: 375 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,379 @@
1414

1515
"""Internal utilities."""
1616

17+
from collections import namedtuple
18+
from contextlib import contextmanager
19+
from enum import Enum
20+
from timeit import default_timer
21+
from typing import Generator, Iterator
1722

18-
class RpcInfo:
19-
def __init__(
20-
self,
21-
full_method=None,
22-
metadata=None,
23-
timeout=None,
24-
request=None,
25-
response=None,
26-
error=None,
27-
):
28-
self.full_method = full_method
29-
self.metadata = metadata
30-
self.timeout = timeout
31-
self.request = request
32-
self.response = response
33-
self.error = error
23+
import grpc
24+
from opentelemetry.instrumentation.grpc._types import ProtoMessage
25+
from opentelemetry.metrics import Meter
26+
from opentelemetry.trace import Span
27+
from opentelemetry.semconv.trace import MessageTypeValues, SpanAttributes
28+
from opentelemetry.util.types import Attributes
29+
30+
31+
_MESSAGE: str = "message"
32+
"""Event name of a message."""
33+
34+
35+
def _add_message_event(
36+
active_span: Span,
37+
message_type: str,
38+
message_size_by: int,
39+
message_id: int = 1
40+
) -> None:
41+
"""Adds a message event of an RPC to an active span.
42+
43+
Args:
44+
active_span (Span): The active span in which to record the message
45+
as event.
46+
message_type (str): The message type value as str, either "SENT" or
47+
"RECEIVED".
48+
message_size_by (int): The (uncompressed) message size in bytes as int.
49+
message_id (int, optional): The message ID. Defaults to 1.
50+
"""
51+
52+
active_span.add_event(
53+
_MESSAGE,
54+
{
55+
SpanAttributes.MESSAGE_TYPE: message_type,
56+
SpanAttributes.MESSAGE_ID: message_id,
57+
SpanAttributes.MESSAGE_UNCOMPRESSED_SIZE: message_size_by,
58+
}
59+
)
60+
61+
62+
class _ClientCallDetails(
63+
namedtuple(
64+
"_ClientCallDetails",
65+
("method", "timeout", "metadata", "credentials",
66+
"wait_for_ready", "compression")
67+
),
68+
grpc.ClientCallDetails
69+
):
70+
pass
71+
72+
73+
class _MetricKind(Enum):
74+
"""Specifies the kind of the metric.
75+
"""
76+
77+
#: Indicates that the metric is of a server.
78+
CLIENT = "client"
79+
80+
#: Indicates that the metric is of a server.
81+
SERVER = "server"
82+
83+
84+
class _EventMetricRecorder:
85+
"""Internal class for recording messages as event and in the histograms
86+
and for recording the duration of a RPC.
87+
"""
88+
89+
def __init__(self, meter: Meter, kind: _MetricKind) -> None:
90+
"""Initializes the _EventMetricRecorder.
91+
92+
Args:
93+
meter (Meter): The meter to create the metrics.
94+
kind (str): The kind of the metric recorder, either for a "client"
95+
or "server".
96+
"""
97+
98+
self._meter = meter
99+
100+
metric_kind = _MetricKind(kind)
101+
self._duration_histogram = self._meter.create_histogram(
102+
name=f"rpc.{metric_kind.value}.duration",
103+
unit="ms",
104+
description="Measures duration of RPC",
105+
)
106+
self._request_size_histogram = self._meter.create_histogram(
107+
name=f"rpc.{metric_kind.value}.request.size",
108+
unit="By",
109+
description="Measures size of RPC request messages (uncompressed)",
110+
)
111+
self._response_size_histogram = self._meter.create_histogram(
112+
name=f"rpc.{metric_kind.value}.response.size",
113+
unit="By",
114+
description="Measures size of RPC response messages "
115+
"(uncompressed)",
116+
)
117+
self._requests_per_rpc_histogram = self._meter.create_histogram(
118+
name=f"rpc.{metric_kind.value}.requests_per_rpc",
119+
unit="1",
120+
description="Measures the number of messages received per RPC. "
121+
"Should be 1 for all non-streaming RPCs",
122+
)
123+
self._responses_per_rpc_histogram = self._meter.create_histogram(
124+
name=f"rpc.{metric_kind.value}.responses_per_rpc",
125+
unit="1",
126+
description="Measures the number of messages sent per RPC. "
127+
"Should be 1 for all non-streaming RPCs",
128+
)
129+
130+
def _record_unary_request(
131+
self,
132+
active_span: Span,
133+
request: ProtoMessage,
134+
message_type: MessageTypeValues,
135+
metric_attributes: Attributes
136+
) -> None:
137+
"""Records a unary request.
138+
139+
The request is recorded as event, its size in the request-size-
140+
histogram and a one for a unary request in the requests-per-RPC-
141+
histogram.
142+
143+
Args:
144+
active_span (Span): The active span in which to record the request
145+
as event.
146+
request (ProtoMessage): The request message.
147+
message_type (MessageTypeValues): The message type value.
148+
metric_attributes (Attributes): The attributes to record in the
149+
metrics.
150+
"""
151+
152+
message_size_by = request.ByteSize()
153+
_add_message_event(active_span, message_type.value, message_size_by)
154+
self._request_size_histogram.record(message_size_by, metric_attributes)
155+
self._requests_per_rpc_histogram.record(1, metric_attributes)
156+
157+
def _record_response(
158+
self,
159+
active_span: Span,
160+
response: ProtoMessage,
161+
message_type: MessageTypeValues,
162+
metric_attributes: Attributes,
163+
response_id: int = 1
164+
) -> None:
165+
"""Records a unary OR streaming response.
166+
167+
The response is recorded as event and its size in the response-size-
168+
histogram.
169+
170+
Args:
171+
active_span (Span): The active span in which to record the response
172+
as event.
173+
response (ProtoMessage): The response message.
174+
message_type (MessageTypeValues): The message type value.
175+
metric_attributes (Attributes): The attributes to record in the
176+
metrics.
177+
response_id (int, optional): The response ID. Defaults to 1.
178+
"""
179+
180+
message_size_by = response.ByteSize()
181+
_add_message_event(
182+
active_span,
183+
message_type.value,
184+
message_size_by,
185+
message_id=response_id
186+
)
187+
self._response_size_histogram.record(
188+
message_size_by, metric_attributes
189+
)
190+
191+
def _record_responses_per_rpc(
192+
self,
193+
responses_per_rpc: int,
194+
metric_attributes: Attributes
195+
) -> None:
196+
"""Records the number of responses in the responses-per-RPC-histogram
197+
for a streaming response.
198+
199+
200+
Args:
201+
responses_per_rpc (int): The number of responses.
202+
metric_attributes (Attributes): The attributes to record in the
203+
metric.
204+
"""
205+
206+
self._responses_per_rpc_histogram.record(
207+
responses_per_rpc, metric_attributes
208+
)
209+
210+
def _record_unary_response(
211+
self,
212+
active_span: Span,
213+
response: ProtoMessage,
214+
message_type: MessageTypeValues,
215+
metric_attributes: Attributes
216+
) -> None:
217+
"""Records a unary response.
218+
219+
The response is recorded as event, its size in the response-size-
220+
histogram and a one for a unary response in the responses-per-RPC-
221+
histogram.
222+
223+
Args:
224+
active_span (Span): The active span in which to record the response
225+
as event.
226+
response (ProtoMessage): The response message.
227+
message_type (MessageTypeValues): The message type value.
228+
metric_attributes (Attributes): The attributes to record in the
229+
metrics.
230+
"""
231+
232+
self._record_response(
233+
active_span, response, message_type, metric_attributes
234+
)
235+
self._record_responses_per_rpc(1, metric_attributes)
236+
237+
def _record_streaming_request(
238+
self,
239+
active_span: Span,
240+
request_iterator: Iterator[ProtoMessage],
241+
message_type: MessageTypeValues,
242+
metric_attributes: Attributes
243+
) -> Iterator[ProtoMessage]:
244+
"""Records a streaming request.
245+
246+
The requests are recorded as events, their size in the request-size-
247+
histogram and the total number of requests in the requests-per-RPC-
248+
histogram.
249+
250+
Args:
251+
active_span (Span): The active span in which to record the request
252+
as event.
253+
request_iterator (Iterator[ProtoMessage]): The iterator over the
254+
request messages.
255+
message_type (MessageTypeValues): The message type value.
256+
metric_attributes (Attributes): The attributes to record in the
257+
metrics.
258+
259+
Yields:
260+
Iterator[ProtoMessage]: The iterator over the recorded request
261+
messages.
262+
"""
263+
264+
try:
265+
req_id = 0
266+
for req_id, request in enumerate(request_iterator, start=1):
267+
message_size_by = request.ByteSize()
268+
_add_message_event(
269+
active_span,
270+
message_type.value,
271+
message_size_by,
272+
message_id=req_id
273+
)
274+
self._request_size_histogram.record(
275+
message_size_by, metric_attributes
276+
)
277+
yield request
278+
finally:
279+
self._requests_per_rpc_histogram.record(req_id, metric_attributes)
280+
281+
def _record_streaming_response(
282+
self,
283+
active_span: Span,
284+
response_iterator: Iterator[ProtoMessage],
285+
message_type: MessageTypeValues,
286+
metric_attributes: Attributes
287+
) -> Iterator[ProtoMessage]:
288+
"""Records a streaming response.
289+
290+
The responses are recorded as events, their size in the response-size-
291+
histogram and the total number of responses in the responses-per-RPC-
292+
histogram.
293+
294+
Args:
295+
active_span (Span): The active span in which to record the response
296+
as event.
297+
response_iterator (Iterator[ProtoMessage]): The iterator over the
298+
response messages.
299+
message_type (MessageTypeValues): The message type value.
300+
metric_attributes (Attributes): The attributes to record in the
301+
metrics.
302+
303+
Yields:
304+
Iterator[ProtoMessage]: The iterator over the recorded response
305+
messages.
306+
"""
307+
308+
try:
309+
res_id = 0
310+
for res_id, response in enumerate(response_iterator, start=1):
311+
self._record_response(
312+
active_span,
313+
response,
314+
message_type,
315+
metric_attributes,
316+
response_id=res_id
317+
)
318+
yield response
319+
finally:
320+
self._record_responses_per_rpc(res_id, metric_attributes)
321+
322+
def _start_duration_measurement(self) -> float:
323+
"""Starts a duration measurement and returns the start time.
324+
325+
Returns:
326+
float: The start time.
327+
"""
328+
329+
return default_timer()
330+
331+
def _record_duration(
332+
self,
333+
start_time: float,
334+
metric_attributes: Attributes,
335+
context: grpc.RpcContext
336+
) -> None:
337+
"""Records a duration of an RPC in the duration histogram. The duration
338+
is calculated as difference between the call of this method and the
339+
start time which has been returned by _start_duration_measurement.d
340+
341+
Args:
342+
start_time (float): The start time.
343+
metric_attributes (Attributes): The attributes to record in the
344+
metric.
345+
context (grpc.RpcContext): The RPC context to update the status
346+
code of the RPC.
347+
"""
348+
349+
duration = max(round((default_timer() - start_time) * 1000), 0)
350+
if context.code() in (None, grpc.StatusCode.OK):
351+
metric_attributes[SpanAttributes.RPC_GRPC_STATUS_CODE] = (
352+
grpc.StatusCode.OK.value[0]
353+
)
354+
else:
355+
metric_attributes[SpanAttributes.RPC_GRPC_STATUS_CODE] = (
356+
context.code().value[0]
357+
)
358+
self._duration_histogram.record(duration, metric_attributes)
359+
360+
@contextmanager
361+
def _record_duration_manager(
362+
self,
363+
metric_attributes: Attributes,
364+
context: grpc.RpcContext
365+
) -> Generator[None, None, None]:
366+
"""Returns a context manager to measure the duration of an RPC in the
367+
duration histogram.
368+
369+
Args:
370+
metric_attributes (Attributes): The attributes to record in the
371+
metric.
372+
context (grpc.RpcContext): The RPC context to update the status
373+
code of the RPC.
374+
375+
Yields:
376+
Generator[None, None, None]: The context manager.
377+
"""
378+
379+
start_time = default_timer()
380+
try:
381+
yield
382+
finally:
383+
duration = max(round((default_timer() - start_time) * 1000), 0)
384+
if context.code() in (None, grpc.StatusCode.OK):
385+
metric_attributes[SpanAttributes.RPC_GRPC_STATUS_CODE] = (
386+
grpc.StatusCode.OK.value[0]
387+
)
388+
else:
389+
metric_attributes[SpanAttributes.RPC_GRPC_STATUS_CODE] = (
390+
context.code().value[0]
391+
)
392+
self._duration_histogram.record(duration, metric_attributes)

0 commit comments

Comments
 (0)