Skip to content

Commit 553a42a

Browse files
committed
Bugfix: Pika basicConsume context propegation
Fixing the context propegation to consumer callback. Bug was fix by attaching and detaching the context before executing the user callback. Hook location was changed to hook the user callback and not the async enqeueuing of messages.
1 parent 3049b4b commit 553a42a

File tree

4 files changed

+52
-74
lines changed

4 files changed

+52
-74
lines changed

Diff for: instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py

+36-44
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import wrapt
1818
from pika.adapters import BlockingConnection
19-
from pika.channel import Channel
19+
from pika.adapters.blocking_connection import BlockingChannel
2020

2121
from opentelemetry import trace
2222
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -35,18 +35,25 @@
3535
class PikaInstrumentor(BaseInstrumentor): # type: ignore
3636
# pylint: disable=attribute-defined-outside-init
3737
@staticmethod
38-
def _instrument_consumers(
39-
consumers_dict: Dict[str, Callable[..., Any]], tracer: Tracer
38+
def _instrument_blocking_channel_consumers(
39+
channel: BlockingChannel, tracer: Tracer
4040
) -> Any:
41-
for key, callback in consumers_dict.items():
41+
for consumer_tag, consumer_info in channel._consumer_infos.items():
4242
decorated_callback = utils._decorate_callback(
43-
callback, tracer, key
43+
consumer_info.on_message_callback, tracer, consumer_tag
4444
)
45-
setattr(decorated_callback, "_original_callback", callback)
46-
consumers_dict[key] = decorated_callback
45+
46+
setattr(
47+
decorated_callback,
48+
"_original_callback",
49+
consumer_info.on_message_callback,
50+
)
51+
consumer_info.on_message_callback = decorated_callback
4752

4853
@staticmethod
49-
def _instrument_basic_publish(channel: Channel, tracer: Tracer) -> None:
54+
def _instrument_basic_publish(
55+
channel: BlockingChannel, tracer: Tracer
56+
) -> None:
5057
original_function = getattr(channel, "basic_publish")
5158
decorated_function = utils._decorate_basic_publish(
5259
original_function, channel, tracer
@@ -57,13 +64,13 @@ def _instrument_basic_publish(channel: Channel, tracer: Tracer) -> None:
5764

5865
@staticmethod
5966
def _instrument_channel_functions(
60-
channel: Channel, tracer: Tracer
67+
channel: BlockingChannel, tracer: Tracer
6168
) -> None:
6269
if hasattr(channel, "basic_publish"):
6370
PikaInstrumentor._instrument_basic_publish(channel, tracer)
6471

6572
@staticmethod
66-
def _uninstrument_channel_functions(channel: Channel) -> None:
73+
def _uninstrument_channel_functions(channel: BlockingChannel) -> None:
6774
for function_name in _FUNCTIONS_TO_UNINSTRUMENT:
6875
if not hasattr(channel, function_name):
6976
continue
@@ -73,8 +80,10 @@ def _uninstrument_channel_functions(channel: Channel) -> None:
7380
unwrap(channel, "basic_consume")
7481

7582
@staticmethod
83+
# Make sure that the spans are created inside hash them set as parent and not as brothers
7684
def instrument_channel(
77-
channel: Channel, tracer_provider: Optional[TracerProvider] = None,
85+
channel: BlockingChannel,
86+
tracer_provider: Optional[TracerProvider] = None,
7887
) -> None:
7988
if not hasattr(channel, "_is_instrumented_by_opentelemetry"):
8089
channel._is_instrumented_by_opentelemetry = False
@@ -84,18 +93,14 @@ def instrument_channel(
8493
)
8594
return
8695
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
87-
if not hasattr(channel, "_impl"):
88-
_LOG.error("Could not find implementation for provided channel!")
89-
return
90-
if channel._impl._consumers:
91-
PikaInstrumentor._instrument_consumers(
92-
channel._impl._consumers, tracer
93-
)
96+
PikaInstrumentor._instrument_blocking_channel_consumers(
97+
channel, tracer
98+
)
9499
PikaInstrumentor._decorate_basic_consume(channel, tracer)
95100
PikaInstrumentor._instrument_channel_functions(channel, tracer)
96101

97102
@staticmethod
98-
def uninstrument_channel(channel: Channel) -> None:
103+
def uninstrument_channel(channel: BlockingChannel) -> None:
99104
if (
100105
not hasattr(channel, "_is_instrumented_by_opentelemetry")
101106
or not channel._is_instrumented_by_opentelemetry
@@ -104,12 +109,12 @@ def uninstrument_channel(channel: Channel) -> None:
104109
"Attempting to uninstrument Pika channel while already uninstrumented!"
105110
)
106111
return
107-
if not hasattr(channel, "_impl"):
108-
_LOG.error("Could not find implementation for provided channel!")
109-
return
110-
for key, callback in channel._impl._consumers.items():
111-
if hasattr(callback, "_original_callback"):
112-
channel._impl._consumers[key] = callback._original_callback
112+
113+
for consumers_tag, client_info in channel._consumer_infos.items():
114+
if hasattr(client_info.on_message_callback, "_original_callback"):
115+
channel._consumer_infos[
116+
consumers_tag
117+
] = client_info.on_message_callback._original_callback
113118
PikaInstrumentor._uninstrument_channel_functions(channel)
114119

115120
def _decorate_channel_function(
@@ -123,28 +128,15 @@ def wrapper(wrapped, instance, args, kwargs):
123128
wrapt.wrap_function_wrapper(BlockingConnection, "channel", wrapper)
124129

125130
@staticmethod
126-
def _decorate_basic_consume(channel, tracer: Optional[Tracer]) -> None:
131+
def _decorate_basic_consume(
132+
channel: BlockingChannel, tracer: Optional[Tracer]
133+
) -> None:
127134
def wrapper(wrapped, instance, args, kwargs):
128-
if not hasattr(channel, "_impl"):
129-
_LOG.error(
130-
"Could not find implementation for provided channel!"
131-
)
132-
return wrapped(*args, **kwargs)
133-
current_keys = set(channel._impl._consumers.keys())
134135
return_value = wrapped(*args, **kwargs)
135-
new_key_list = list(
136-
set(channel._impl._consumers.keys()) - current_keys
137-
)
138-
if not new_key_list:
139-
_LOG.error("Could not find added callback")
140-
return return_value
141-
new_key = new_key_list[0]
142-
callback = channel._impl._consumers[new_key]
143-
decorated_callback = utils._decorate_callback(
144-
callback, tracer, new_key
136+
137+
PikaInstrumentor._instrument_blocking_channel_consumers(
138+
channel, tracer
145139
)
146-
setattr(decorated_callback, "_original_callback", callback)
147-
channel._impl._consumers[new_key] = decorated_callback
148140
return return_value
149141

150142
wrapt.wrap_function_wrapper(channel, "basic_consume", wrapper)

Diff for: instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,18 @@ def decorated_callback(
4646
ctx = propagate.extract(properties.headers, getter=_pika_getter)
4747
if not ctx:
4848
ctx = context.get_current()
49+
token = context.attach(ctx)
4950
span = _get_span(
5051
tracer,
5152
channel,
5253
properties,
5354
span_kind=SpanKind.CONSUMER,
5455
task_name=task_name,
55-
ctx=ctx,
5656
operation=MessagingOperationValues.RECEIVE,
5757
)
5858
with trace.use_span(span, end_on_exit=True):
5959
retval = callback(channel, method, properties, body)
60+
context.detach(token)
6061
return retval
6162

6263
return decorated_callback
@@ -78,14 +79,12 @@ def decorated_function(
7879
properties = BasicProperties(headers={})
7980
if properties.headers is None:
8081
properties.headers = {}
81-
ctx = context.get_current()
8282
span = _get_span(
8383
tracer,
8484
channel,
8585
properties,
8686
span_kind=SpanKind.PRODUCER,
8787
task_name="(temporary)",
88-
ctx=ctx,
8988
operation=None,
9089
)
9190
if not span:
@@ -109,7 +108,6 @@ def _get_span(
109108
properties: BasicProperties,
110109
task_name: str,
111110
span_kind: SpanKind,
112-
ctx: context.Context,
113111
operation: Optional[MessagingOperationValues] = None,
114112
) -> Optional[Span]:
115113
if context.get_value("suppress_instrumentation") or context.get_value(
@@ -118,9 +116,7 @@ def _get_span(
118116
return None
119117
task_name = properties.type if properties.type else task_name
120118
span = tracer.start_span(
121-
context=ctx,
122-
name=_generate_span_name(task_name, operation),
123-
kind=span_kind,
119+
name=_generate_span_name("pika", operation), kind=span_kind,
124120
)
125121
if span.is_recording():
126122
_enrich_span(span, channel, properties, task_name, operation)

Diff for: instrumentation/opentelemetry-instrumentation-pika/tests/test_pika_instrumentation.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
class TestPika(TestCase):
2525
def setUp(self) -> None:
2626
self.channel = mock.MagicMock(spec=Channel)
27-
self.channel._impl = mock.MagicMock(spec=BaseConnection)
27+
consumer_info = mock.MagicMock()
28+
consumer_info.on_message_callback = mock.MagicMock()
29+
self.channel._consumer_infos = {"consumer-tag": consumer_info}
2830
self.mock_callback = mock.MagicMock()
29-
self.channel._impl._consumers = {"mock_key": self.mock_callback}
3031

3132
def test_instrument_api(self) -> None:
3233
instrumentation = PikaInstrumentor()
@@ -49,19 +50,19 @@ def test_instrument_api(self) -> None:
4950
"opentelemetry.instrumentation.pika.PikaInstrumentor._decorate_basic_consume"
5051
)
5152
@mock.patch(
52-
"opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_consumers"
53+
"opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_blocking_channel_consumers"
5354
)
5455
def test_instrument(
5556
self,
56-
instrument_consumers: mock.MagicMock,
57+
instrument_blocking_channel_consumers: mock.MagicMock,
5758
instrument_basic_consume: mock.MagicMock,
5859
instrument_channel_functions: mock.MagicMock,
5960
):
6061
PikaInstrumentor.instrument_channel(channel=self.channel)
6162
assert hasattr(
6263
self.channel, "_is_instrumented_by_opentelemetry"
6364
), "channel is not marked as instrumented!"
64-
instrument_consumers.assert_called_once()
65+
instrument_blocking_channel_consumers.assert_called_once()
6566
instrument_basic_consume.assert_called_once()
6667
instrument_channel_functions.assert_called_once()
6768

@@ -71,18 +72,18 @@ def test_instrument_consumers(
7172
) -> None:
7273
tracer = mock.MagicMock(spec=Tracer)
7374
expected_decoration_calls = [
74-
mock.call(value, tracer, key)
75-
for key, value in self.channel._impl._consumers.items()
75+
mock.call(value.on_message_callback, tracer, key)
76+
for key, value in self.channel._consumer_infos.items()
7677
]
77-
PikaInstrumentor._instrument_consumers(
78-
self.channel._impl._consumers, tracer
78+
PikaInstrumentor._instrument_blocking_channel_consumers(
79+
self.channel, tracer
7980
)
8081
decorate_callback.assert_has_calls(
8182
calls=expected_decoration_calls, any_order=True
8283
)
8384
assert all(
8485
hasattr(callback, "_original_callback")
85-
for callback in self.channel._impl._consumers.values()
86+
for callback in self.channel._consumer_infos.values()
8687
)
8788

8889
@mock.patch(

Diff for: instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py

+2-13
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,10 @@ def test_get_span(
4040
task_name = "test.test"
4141
span_kind = mock.MagicMock(spec=SpanKind)
4242
get_value.return_value = None
43-
ctx = mock.MagicMock()
44-
_ = utils._get_span(
45-
tracer, channel, properties, task_name, span_kind, ctx
46-
)
43+
_ = utils._get_span(tracer, channel, properties, task_name, span_kind)
4744
generate_span_name.assert_called_once()
4845
tracer.start_span.assert_called_once_with(
49-
context=ctx, name=generate_span_name.return_value, kind=span_kind
46+
name=generate_span_name.return_value, kind=span_kind
5047
)
5148
enrich_span.assert_called_once()
5249

@@ -200,7 +197,6 @@ def test_decorate_callback(
200197
properties,
201198
span_kind=SpanKind.CONSUMER,
202199
task_name=mock_task_name,
203-
ctx=extract.return_value,
204200
operation=MessagingOperationValues.RECEIVE,
205201
)
206202
use_span.assert_called_once_with(
@@ -213,12 +209,10 @@ def test_decorate_callback(
213209

214210
@mock.patch("opentelemetry.instrumentation.pika.utils._get_span")
215211
@mock.patch("opentelemetry.propagate.inject")
216-
@mock.patch("opentelemetry.context.get_current")
217212
@mock.patch("opentelemetry.trace.use_span")
218213
def test_decorate_basic_publish(
219214
self,
220215
use_span: mock.MagicMock,
221-
get_current: mock.MagicMock,
222216
inject: mock.MagicMock,
223217
get_span: mock.MagicMock,
224218
) -> None:
@@ -234,14 +228,12 @@ def test_decorate_basic_publish(
234228
retval = decorated_basic_publish(
235229
channel, method, mock_body, properties
236230
)
237-
get_current.assert_called_once()
238231
get_span.assert_called_once_with(
239232
tracer,
240233
channel,
241234
properties,
242235
span_kind=SpanKind.PRODUCER,
243236
task_name="(temporary)",
244-
ctx=get_current.return_value,
245237
operation=None,
246238
)
247239
use_span.assert_called_once_with(
@@ -256,14 +248,12 @@ def test_decorate_basic_publish(
256248

257249
@mock.patch("opentelemetry.instrumentation.pika.utils._get_span")
258250
@mock.patch("opentelemetry.propagate.inject")
259-
@mock.patch("opentelemetry.context.get_current")
260251
@mock.patch("opentelemetry.trace.use_span")
261252
@mock.patch("pika.spec.BasicProperties.__new__")
262253
def test_decorate_basic_publish_no_properties(
263254
self,
264255
basic_properties: mock.MagicMock,
265256
use_span: mock.MagicMock,
266-
get_current: mock.MagicMock,
267257
inject: mock.MagicMock,
268258
get_span: mock.MagicMock,
269259
) -> None:
@@ -277,7 +267,6 @@ def test_decorate_basic_publish_no_properties(
277267
)
278268
retval = decorated_basic_publish(channel, method, body=mock_body)
279269
basic_properties.assert_called_once_with(BasicProperties, headers={})
280-
get_current.assert_called_once()
281270
use_span.assert_called_once_with(
282271
get_span.return_value, end_on_exit=True
283272
)

0 commit comments

Comments
 (0)