Replaced WSGI name callback with request/response hooks (#424)

This commit is contained in:
Owais Lone
2021-04-14 20:48:47 +05:30
committed by GitHub
parent e7d26a4c2d
commit 96b0f592b7
3 changed files with 61 additions and 27 deletions

View File

@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/387)) ([#387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/387))
- Update redis instrumentation to follow semantic conventions - Update redis instrumentation to follow semantic conventions
([#403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/403)) ([#403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/403))
- `opentelemetry-instrumentation-wsgi` Replaced `name_callback` with `request_hook`
and `response_hook` callbacks.
([#424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/424))
- Update gRPC instrumentation to better wrap server context - Update gRPC instrumentation to better wrap server context
([#420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/420)) ([#420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/420))
@ -38,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove `http.status_text` from span attributes - Remove `http.status_text` from span attributes
([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406)) ([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406))
## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26 ## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26
- Implement context methods for `_InterceptorChannel` - Implement context methods for `_InterceptorChannel`

View File

@ -186,21 +186,26 @@ class OpenTelemetryMiddleware:
Args: Args:
wsgi: The WSGI application callable to forward requests to. wsgi: The WSGI application callable to forward requests to.
name_callback: Callback which calculates a generic span name for an request_hook: Optional callback which is called with the server span and WSGI
incoming HTTP request based on the PEP3333 WSGI environ. environ object for every incoming request.
Optional: Defaults to get_default_span_name. response_hook: Optional callback which is called with the server span,
WSGI environ, status_code and response_headers for every
incoming request.
""" """
def __init__(self, wsgi, name_callback=get_default_span_name): def __init__(self, wsgi, request_hook=None, response_hook=None):
self.wsgi = wsgi self.wsgi = wsgi
self.tracer = trace.get_tracer(__name__, __version__) self.tracer = trace.get_tracer(__name__, __version__)
self.name_callback = name_callback self.request_hook = request_hook
self.response_hook = response_hook
@staticmethod @staticmethod
def _create_start_response(span, start_response): def _create_start_response(span, start_response, response_hook):
@functools.wraps(start_response) @functools.wraps(start_response)
def _start_response(status, response_headers, *args, **kwargs): def _start_response(status, response_headers, *args, **kwargs):
add_response_attributes(span, status, response_headers) add_response_attributes(span, status, response_headers)
if response_hook:
response_hook(status, response_headers)
return start_response(status, response_headers, *args, **kwargs) return start_response(status, response_headers, *args, **kwargs)
return _start_response return _start_response
@ -214,18 +219,24 @@ class OpenTelemetryMiddleware:
""" """
token = context.attach(extract(environ, getter=wsgi_getter)) token = context.attach(extract(environ, getter=wsgi_getter))
span_name = self.name_callback(environ)
span = self.tracer.start_span( span = self.tracer.start_span(
span_name, get_default_span_name(environ),
kind=trace.SpanKind.SERVER, kind=trace.SpanKind.SERVER,
attributes=collect_request_attributes(environ), attributes=collect_request_attributes(environ),
) )
if self.request_hook:
self.request_hook(span, environ)
response_hook = self.response_hook
if response_hook:
response_hook = functools.partial(response_hook, span, environ)
try: try:
with trace.use_span(span): with trace.use_span(span):
start_response = self._create_start_response( start_response = self._create_start_response(
span, start_response span, start_response, response_hook
) )
iterable = self.wsgi(environ, start_response) iterable = self.wsgi(environ, start_response)
return _end_span_after_iterating( return _end_span_after_iterating(

View File

@ -81,7 +81,13 @@ def error_wsgi_unhandled(environ, start_response):
class TestWsgiApplication(WsgiTestBase): class TestWsgiApplication(WsgiTestBase):
def validate_response( def validate_response(
self, response, error=None, span_name="HTTP GET", http_method="GET" self,
response,
error=None,
span_name="HTTP GET",
http_method="GET",
span_attributes=None,
response_headers=None,
): ):
while True: while True:
try: try:
@ -90,10 +96,12 @@ class TestWsgiApplication(WsgiTestBase):
except StopIteration: except StopIteration:
break break
expected_headers = [("Content-Type", "text/plain")]
if response_headers:
expected_headers.extend(response_headers)
self.assertEqual(self.status, "200 OK") self.assertEqual(self.status, "200 OK")
self.assertEqual( self.assertEqual(self.response_headers, expected_headers)
self.response_headers, [("Content-Type", "text/plain")]
)
if error: if error:
self.assertIs(self.exc_info[0], error) self.assertIs(self.exc_info[0], error)
self.assertIsInstance(self.exc_info[1], error) self.assertIsInstance(self.exc_info[1], error)
@ -114,6 +122,7 @@ class TestWsgiApplication(WsgiTestBase):
"http.url": "http://127.0.0.1/", "http.url": "http://127.0.0.1/",
"http.status_code": 200, "http.status_code": 200,
} }
expected_attributes.update(span_attributes or {})
if http_method is not None: if http_method is not None:
expected_attributes["http.method"] = http_method expected_attributes["http.method"] = http_method
self.assertEqual(span_list[0].attributes, expected_attributes) self.assertEqual(span_list[0].attributes, expected_attributes)
@ -123,6 +132,30 @@ class TestWsgiApplication(WsgiTestBase):
response = app(self.environ, self.start_response) response = app(self.environ, self.start_response)
self.validate_response(response) self.validate_response(response)
def test_hooks(self):
hook_headers = (
"hook_attr",
"hello otel",
)
def request_hook(span, environ):
span.update_name("name from hook")
def response_hook(span, environ, status_code, response_headers):
span.set_attribute("hook_attr", "hello world")
response_headers.append(hook_headers)
app = otel_wsgi.OpenTelemetryMiddleware(
simple_wsgi, request_hook, response_hook
)
response = app(self.environ, self.start_response)
self.validate_response(
response,
span_name="name from hook",
span_attributes={"hook_attr": "hello world"},
response_headers=(hook_headers,),
)
def test_wsgi_not_recording(self): def test_wsgi_not_recording(self):
mock_tracer = mock.Mock() mock_tracer = mock.Mock()
mock_span = mock.Mock() mock_span = mock.Mock()
@ -176,20 +209,6 @@ class TestWsgiApplication(WsgiTestBase):
span_list[0].status.status_code, StatusCode.ERROR, span_list[0].status.status_code, StatusCode.ERROR,
) )
def test_override_span_name(self):
"""Test that span_names can be overwritten by our callback function."""
span_name = "Dymaxion"
def get_predefined_span_name(scope):
# pylint: disable=unused-argument
return span_name
app = otel_wsgi.OpenTelemetryMiddleware(
simple_wsgi, name_callback=get_predefined_span_name
)
response = app(self.environ, self.start_response)
self.validate_response(response, span_name=span_name)
def test_default_span_name_missing_request_method(self): def test_default_span_name_missing_request_method(self):
"""Test that default span_names with missing request method.""" """Test that default span_names with missing request method."""
self.environ.pop("REQUEST_METHOD") self.environ.pop("REQUEST_METHOD")