Skip to content

Commit c2db445

Browse files
Merge branch 'main' into feature/http-route-in-metric
2 parents 36f2078 + b16394b commit c2db445

File tree

10 files changed

+137
-10
lines changed

10 files changed

+137
-10
lines changed

CHANGELOG.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9+
- `opentelemetry-instrumentation-django` Handle exceptions from request/response hooks
10+
([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153))
11+
- `opentelemetry-instrumentation-asyncio` instrumented `asyncio.wait_for` properly raises `asyncio.TimeoutError` as expected
12+
([#2637](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2637))
913
- `opentelemetry-instrumentation-aws-lambda` Bugfix: AWS Lambda event source key incorrect for SNS in instrumentation library.
1014
([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612))
1115
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
@@ -38,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3842
([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590))
3943
- Reference symbols from generated semantic conventions
4044
([#2611](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2611))
45+
- `opentelemetry-instrumentation-psycopg` Bugfix: Handle empty statement.
46+
([#2644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2644))
47+
- `opentelemetry-instrumentation-confluent-kafka` Confluent Kafka: Ensure consume span is ended when consumer is closed
48+
([#2640](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2640))
4149

4250
## Version 1.25.0/0.46b0 (2024-05-31)
4351

@@ -145,7 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
145153
([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136))
146154
- `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call
147155
([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178))
148-
- AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245))
156+
- AwsLambdaInstrumentor handles and re-raises function exception
157+
([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245))
149158

150159
### Added
151160

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

+3
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,11 @@ async def trace_coroutine(self, coro):
280280
# CancelledError is raised when a coroutine is cancelled
281281
# before it has a chance to run. We don't want to record
282282
# this as an error.
283+
# Still it needs to be raised in order for `asyncio.wait_for`
284+
# to properly work with timeout and raise accordingly `asyncio.TimeoutError`
283285
except asyncio.CancelledError:
284286
attr["state"] = "cancelled"
287+
raise
285288
except Exception as exc:
286289
exception = exc
287290
state = determine_state(exception)

instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py

+13
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ async def main():
6868
spans = self.memory_exporter.get_finished_spans()
6969
self.assertEqual(len(spans), 2)
7070

71+
def test_asyncio_wait_for_with_timeout(self):
72+
expected_timeout_error = None
73+
74+
async def main():
75+
nonlocal expected_timeout_error
76+
try:
77+
await asyncio.wait_for(async_func(), 0.01)
78+
except asyncio.TimeoutError as timeout_error:
79+
expected_timeout_error = timeout_error
80+
81+
asyncio.run(main())
82+
self.assertNotEqual(expected_timeout_error, None)
83+
7184
def test_asyncio_as_completed(self):
7285
async def main():
7386
if sys.version_info >= (3, 11):

instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def consume(
144144
): # pylint: disable=useless-super-delegation
145145
return super().consume(*args, **kwargs)
146146

147+
# This method is deliberately implemented in order to allow wrapt to wrap this function
148+
def close(self): # pylint: disable=useless-super-delegation
149+
return super().close()
150+
147151

148152
class ProxiedProducer(Producer):
149153
def __init__(self, producer: Producer, tracer: Tracer):
@@ -181,6 +185,11 @@ def __init__(self, consumer: Consumer, tracer: Tracer):
181185
self._current_consume_span = None
182186
self._current_context_token = None
183187

188+
def close(self):
189+
return ConfluentKafkaInstrumentor.wrap_close(
190+
self._consumer.close, self
191+
)
192+
184193
def committed(self, partitions, timeout=-1):
185194
return self._consumer.committed(partitions, timeout)
186195

@@ -303,6 +312,9 @@ def _inner_wrap_consume(func, instance, args, kwargs):
303312
func, instance, self._tracer, args, kwargs
304313
)
305314

315+
def _inner_wrap_close(func, instance):
316+
return ConfluentKafkaInstrumentor.wrap_close(func, instance)
317+
306318
wrapt.wrap_function_wrapper(
307319
AutoInstrumentedProducer,
308320
"produce",
@@ -321,6 +333,12 @@ def _inner_wrap_consume(func, instance, args, kwargs):
321333
_inner_wrap_consume,
322334
)
323335

336+
wrapt.wrap_function_wrapper(
337+
AutoInstrumentedConsumer,
338+
"close",
339+
_inner_wrap_close,
340+
)
341+
324342
def _uninstrument(self, **kwargs):
325343
confluent_kafka.Producer = self._original_kafka_producer
326344
confluent_kafka.Consumer = self._original_kafka_consumer
@@ -403,3 +421,9 @@ def wrap_consume(func, instance, tracer, args, kwargs):
403421
)
404422

405423
return records
424+
425+
@staticmethod
426+
def wrap_close(func, instance):
427+
if instance._current_consume_span:
428+
_end_current_consume_span(instance)
429+
func()

instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py

+37
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,44 @@ def test_consume(self) -> None:
237237
span_list = self.memory_exporter.get_finished_spans()
238238
self._compare_spans(span_list, expected_spans)
239239

240+
def test_close(self) -> None:
241+
instrumentation = ConfluentKafkaInstrumentor()
242+
mocked_messages = [
243+
MockedMessage("topic-a", 0, 0, []),
244+
]
245+
expected_spans = [
246+
{"name": "recv", "attributes": {}},
247+
{
248+
"name": "topic-a process",
249+
"attributes": {
250+
SpanAttributes.MESSAGING_OPERATION: "process",
251+
SpanAttributes.MESSAGING_KAFKA_PARTITION: 0,
252+
SpanAttributes.MESSAGING_SYSTEM: "kafka",
253+
SpanAttributes.MESSAGING_DESTINATION: "topic-a",
254+
SpanAttributes.MESSAGING_DESTINATION_KIND: MessagingDestinationKindValues.QUEUE.value,
255+
SpanAttributes.MESSAGING_MESSAGE_ID: "topic-a.0.0",
256+
},
257+
},
258+
]
259+
260+
consumer = MockConsumer(
261+
mocked_messages,
262+
{
263+
"bootstrap.servers": "localhost:29092",
264+
"group.id": "mygroup",
265+
"auto.offset.reset": "earliest",
266+
},
267+
)
268+
self.memory_exporter.clear()
269+
consumer = instrumentation.instrument_consumer(consumer)
270+
consumer.poll()
271+
consumer.close()
272+
273+
span_list = self.memory_exporter.get_finished_spans()
274+
self._compare_spans(span_list, expected_spans)
275+
240276
def _compare_spans(self, spans, expected_spans):
277+
self.assertEqual(len(spans), len(expected_spans))
241278
for span, expected_span in zip(spans, expected_spans):
242279
self.assertEqual(expected_span["name"], span.name)
243280
for attribute_key, expected_attribute_value in expected_span[

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def _get_span_name(request):
187187
return request.method
188188

189189
# pylint: disable=too-many-locals
190+
# pylint: disable=too-many-branches
190191
def process_request(self, request):
191192
# request.META is a dictionary containing all available HTTP headers
192193
# Read more about request.META here:
@@ -286,9 +287,14 @@ def process_request(self, request):
286287
request.META[self._environ_token] = token
287288

288289
if _DjangoMiddleware._otel_request_hook:
289-
_DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable
290-
span, request
291-
)
290+
try:
291+
_DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable
292+
span, request
293+
)
294+
except Exception: # pylint: disable=broad-exception-caught
295+
# Raising an exception here would leak the request span since process_response
296+
# would not be called. Log the exception instead.
297+
_logger.exception("Exception raised by request_hook")
292298

293299
# pylint: disable=unused-argument
294300
def process_view(self, request, view_func, *args, **kwargs):
@@ -385,10 +391,14 @@ def process_response(self, request, response):
385391

386392
# record any exceptions raised while processing the request
387393
exception = request.META.pop(self._environ_exception_key, None)
394+
388395
if _DjangoMiddleware._otel_response_hook:
389-
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable
390-
span, request, response
391-
)
396+
try:
397+
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable
398+
span, request, response
399+
)
400+
except Exception: # pylint: disable=broad-exception-caught
401+
_logger.exception("Exception raised by response_hook")
392402

393403
if exception:
394404
activation.__exit__(

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

+26
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,32 @@ def response_hook(span, request, response):
392392
self.assertIsInstance(response_hook_args[2], HttpResponse)
393393
self.assertEqual(response_hook_args[2], response)
394394

395+
def test_request_hook_exception(self):
396+
def request_hook(span, request):
397+
# pylint: disable=broad-exception-raised
398+
raise Exception("request hook exception")
399+
400+
_DjangoMiddleware._otel_request_hook = request_hook
401+
Client().get("/span_name/1234/")
402+
_DjangoMiddleware._otel_request_hook = None
403+
404+
# ensure that span ended
405+
finished_spans = self.memory_exporter.get_finished_spans()
406+
self.assertEqual(len(finished_spans), 1)
407+
408+
def test_response_hook_exception(self):
409+
def response_hook(span, request, response):
410+
# pylint: disable=broad-exception-raised
411+
raise Exception("response hook exception")
412+
413+
_DjangoMiddleware._otel_response_hook = response_hook
414+
Client().get("/span_name/1234/")
415+
_DjangoMiddleware._otel_response_hook = None
416+
417+
# ensure that span ended
418+
finished_spans = self.memory_exporter.get_finished_spans()
419+
self.assertEqual(len(finished_spans), 1)
420+
395421
def test_trace_parent(self):
396422
id_generator = RandomIdGenerator()
397423
trace_id = format_trace_id(id_generator.generate_trace_id())

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ def get_operation_name(self, cursor, args):
269269
if isinstance(statement, Composed):
270270
statement = statement.as_string(cursor)
271271

272-
if isinstance(statement, str):
272+
# `statement` can be empty string. See #2643
273+
if statement and isinstance(statement, str):
273274
# Strip leading comments so we get the operation name.
274275
return self._leading_comment_remover.sub("", statement).split()[0]
275276

instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,18 @@ def test_span_name(self):
245245
cursor.execute("/* leading comment */ query")
246246
cursor.execute("/* leading comment */ query /* trailing comment */")
247247
cursor.execute("query /* trailing comment */")
248+
cursor.execute("")
249+
cursor.execute("--")
248250
spans_list = self.memory_exporter.get_finished_spans()
249-
self.assertEqual(len(spans_list), 6)
251+
self.assertEqual(len(spans_list), 8)
250252
self.assertEqual(spans_list[0].name, "Test")
251253
self.assertEqual(spans_list[1].name, "multi")
252254
self.assertEqual(spans_list[2].name, "tab")
253255
self.assertEqual(spans_list[3].name, "query")
254256
self.assertEqual(spans_list[4].name, "query")
255257
self.assertEqual(spans_list[5].name, "query")
258+
self.assertEqual(spans_list[6].name, "postgresql")
259+
self.assertEqual(spans_list[7].name, "--")
256260

257261
# pylint: disable=unused-argument
258262
def test_not_recording(self):

processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
BaggageKeyPredicateT = Callable[[str], bool]
2424

2525
# A BaggageKeyPredicate that always returns True, allowing all baggage keys to be added to spans
26-
ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True
26+
ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True # noqa: E731
2727

2828

2929
class BaggageSpanProcessor(SpanProcessor):

0 commit comments

Comments
 (0)