Added recording of exceptions in Pyramid (#2622)

This commit is contained in:
Rytis Bagdziunas
2024-06-20 19:19:20 +02:00
committed by GitHub
parent b776ac92a8
commit fecb1e2862
5 changed files with 45 additions and 2 deletions

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `opentelemetry-instrumentation-pyramid` Record exceptions raised when serving a request
([#2622](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2622))
- `opentelemetry-sdk-extension-aws` Add AwsXrayLambdaPropagator - `opentelemetry-sdk-extension-aws` Add AwsXrayLambdaPropagator
([#2573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2573)) ([#2573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2573))

View File

@ -31,6 +31,7 @@ from opentelemetry.instrumentation.utils import _start_internal_or_server_span
from opentelemetry.metrics import get_meter from opentelemetry.metrics import get_meter
from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.http import get_excluded_urls from opentelemetry.util.http import get_excluded_urls
TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory"
@ -180,6 +181,7 @@ def trace_tween_factory(handler, registry):
response = None response = None
status = None status = None
recordable_exc = None
try: try:
response = handler(request) response = handler(request)
@ -190,11 +192,14 @@ def trace_tween_factory(handler, registry):
# As described in docs, Pyramid exceptions are all valid # As described in docs, Pyramid exceptions are all valid
# response types # response types
response = exc response = exc
if isinstance(exc, HTTPServerError):
recordable_exc = exc
raise raise
except BaseException: except BaseException as exc:
# In the case that a non-HTTPException is bubbled up we # In the case that a non-HTTPException is bubbled up we
# should infer a internal server error and raise # should infer a internal server error and raise
status = "500 InternalServerError" status = "500 InternalServerError"
recordable_exc = exc
raise raise
finally: finally:
duration = max(round((default_timer() - start) * 1000), 0) duration = max(round((default_timer() - start) * 1000), 0)
@ -222,6 +227,12 @@ def trace_tween_factory(handler, registry):
getattr(response, "headerlist", None), getattr(response, "headerlist", None),
) )
if recordable_exc is not None:
span.set_status(
Status(StatusCode.ERROR, str(recordable_exc))
)
span.record_exception(recordable_exc)
if span.is_recording() and span.kind == trace.SpanKind.SERVER: if span.is_recording() and span.kind == trace.SpanKind.SERVER:
custom_attributes = ( custom_attributes = (
otel_wsgi.collect_custom_response_headers_attributes( otel_wsgi.collect_custom_response_headers_attributes(

View File

@ -35,7 +35,7 @@ class InstrumentationTest:
if helloid == 204: if helloid == 204:
raise exc.HTTPNoContent() raise exc.HTTPNoContent()
if helloid == 900: if helloid == 900:
raise NotImplementedError() raise NotImplementedError("error message")
return Response("Hello: " + str(helloid)) return Response("Hello: " + str(helloid))
@staticmethod @staticmethod

View File

@ -121,6 +121,7 @@ class TestAutomatic(InstrumentationTest, WsgiTestBase):
span_list = self.memory_exporter.get_finished_spans() span_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(span_list), 1) self.assertEqual(len(span_list), 1)
self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET)
self.assertEqual(len(span_list[0].events), 0)
PyramidInstrumentor().uninstrument() PyramidInstrumentor().uninstrument()

View File

@ -23,6 +23,7 @@ from opentelemetry.instrumentation.propagators import (
set_global_response_propagator, set_global_response_propagator,
) )
from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.instrumentation.pyramid import PyramidInstrumentor
from opentelemetry.semconv.attributes import exception_attributes
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.test.wsgitestutil import WsgiTestBase
from opentelemetry.util.http import get_excluded_urls from opentelemetry.util.http import get_excluded_urls
@ -149,6 +150,7 @@ class TestProgrammatic(InstrumentationTest, WsgiTestBase):
self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].name, "POST /bye")
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(span_list[0].attributes, expected_attrs)
self.assertEqual(len(span_list[0].events), 0)
def test_internal_error(self): def test_internal_error(self):
expected_attrs = expected_attributes( expected_attrs = expected_attributes(
@ -166,6 +168,18 @@ class TestProgrammatic(InstrumentationTest, WsgiTestBase):
self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].name, "/hello/{helloid}")
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(span_list[0].attributes, expected_attrs)
self.assertEqual(
span_list[0].status.status_code, trace.StatusCode.ERROR
)
self.assertIn(
"HTTPInternalServerError", span_list[0].status.description
)
self.assertEqual(
span_list[0]
.events[0]
.attributes[exception_attributes.EXCEPTION_TYPE],
"pyramid.httpexceptions.HTTPInternalServerError",
)
def test_internal_exception(self): def test_internal_exception(self):
expected_attrs = expected_attributes( expected_attrs = expected_attributes(
@ -184,6 +198,21 @@ class TestProgrammatic(InstrumentationTest, WsgiTestBase):
self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].name, "/hello/{helloid}")
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
self.assertEqual(span_list[0].attributes, expected_attrs) self.assertEqual(span_list[0].attributes, expected_attrs)
self.assertEqual(
span_list[0].status.status_code, trace.StatusCode.ERROR
)
self.assertEqual(span_list[0].status.description, "error message")
expected_error_event_attrs = {
exception_attributes.EXCEPTION_TYPE: "NotImplementedError",
exception_attributes.EXCEPTION_MESSAGE: "error message",
}
self.assertEqual(span_list[0].events[0].name, "exception")
# Ensure exception event has specific attributes, but allow additional ones
self.assertLess(
expected_error_event_attrs.items(),
dict(span_list[0].events[0].attributes).items(),
)
def test_tween_list(self): def test_tween_list(self):
tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory"