Skip to content

Commit

Permalink
ext-{wsgi,flask}: Update for open-telemetry/opentelemetry-specificati…
Browse files Browse the repository at this point in the history
  • Loading branch information
Oberon00 committed Nov 25, 2019
1 parent 693391c commit 9c61977
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 109 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ignore =
E501 # line too long, defer to black
F401 # unused import, defer to pylint
W503 # allow line breaks after binary ops, not after
E203 # allow whitespace before ':' (https://github.com/psf/black#slices)
exclude =
.bzr
.git
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,21 @@ def _before_flask_request():

tracer = trace.tracer()

attributes = otel_wsgi.collect_request_attributes(environ)
if flask_request.url_rule:
# For 404 that result from no route found, etc, we don't have a url_rule.
attributes["http.route"] = flask_request.url_rule.rule
span = tracer.create_span(
span_name, parent_span, kind=trace.SpanKind.SERVER
span_name,
parent_span,
kind=trace.SpanKind.SERVER,
attributes=attributes,
)
span.start(environ.get(_ENVIRON_STARTTIME_KEY))
activation = tracer.use_span(span, end_on_exit=True)
activation.__enter__()
environ[_ENVIRON_ACTIVATION_KEY] = activation
environ[_ENVIRON_SPAN_KEY] = span
otel_wsgi.add_request_attributes(span, environ)
if flask_request.url_rule:
# For 404 that result from no route found, etc, we don't have a url_rule.
span.set_attribute("http.route", flask_request.url_rule.rule)


def _teardown_flask_request(exc):
Expand Down
56 changes: 34 additions & 22 deletions ext/opentelemetry-ext-flask/tests/test_flask_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,25 @@ def test_simple(self):
"hello_endpoint",
trace_api.INVALID_SPAN_CONTEXT,
kind=trace_api.SpanKind.SERVER,
attributes={
"component": "http",
"http.method": "GET",
"http.server_name": "localhost",
"http.scheme": "http",
"host.port": 80,
"http.host": "localhost",
"http.target": "/hello/123",
"http.flavor": "1.1",
"http.route": "/hello/<int:helloid>",
},
)
self.assertEqual(1, self.span.start.call_count)

# TODO: Change this test to use the SDK, as mocking becomes painful

self.assertEqual(
self.span_attrs,
{
"component": "http",
"http.method": "GET",
"http.host": "localhost",
"http.url": "http://localhost/hello/123",
"http.route": "/hello/<int:helloid>",
"http.status_code": 200,
"http.status_text": "OK",
},
{"http.status_code": 200, "http.status_text": "OK"},
)

def test_404(self):
Expand All @@ -83,6 +86,16 @@ def test_404(self):
"/bye",
trace_api.INVALID_SPAN_CONTEXT,
kind=trace_api.SpanKind.SERVER,
attributes={
"component": "http",
"http.method": "POST",
"http.server_name": "localhost",
"http.scheme": "http",
"host.port": 80,
"http.host": "localhost",
"http.target": "/bye",
"http.flavor": "1.1",
},
)
self.assertEqual(1, self.span.start.call_count)

Expand All @@ -92,14 +105,7 @@ def test_404(self):

self.assertEqual(
self.span_attrs,
{
"component": "http",
"http.method": "POST",
"http.host": "localhost",
"http.url": "http://localhost/bye",
"http.status_code": 404,
"http.status_text": "NOT FOUND",
},
{"http.status_code": 404, "http.status_text": "NOT FOUND"},
)

def test_internal_error(self):
Expand All @@ -111,6 +117,17 @@ def test_internal_error(self):
"hello_endpoint",
trace_api.INVALID_SPAN_CONTEXT,
kind=trace_api.SpanKind.SERVER,
attributes={
"component": "http",
"http.method": "GET",
"http.server_name": "localhost",
"http.scheme": "http",
"host.port": 80,
"http.host": "localhost",
"http.target": "/hello/500",
"http.flavor": "1.1",
"http.route": "/hello/<int:helloid>",
},
)
self.assertEqual(1, self.span.start.call_count)

Expand All @@ -121,11 +138,6 @@ def test_internal_error(self):
self.assertEqual(
self.span_attrs,
{
"component": "http",
"http.method": "GET",
"http.host": "localhost",
"http.url": "http://localhost/hello/500",
"http.route": "/hello/<int:helloid>",
"http.status_code": 500,
"http.status_text": "INTERNAL SERVER ERROR",
},
Expand Down
127 changes: 88 additions & 39 deletions ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@
import functools
import typing
import wsgiref.util as wsgiref_util
from http import HTTPStatus

from opentelemetry import propagators, trace
from opentelemetry.ext.wsgi.version import __version__ # noqa
from opentelemetry.trace.status import Status, StatusCanonicalCode
from opentelemetry.util import time_ns

_HTTP_VERSION_PREFIX = "HTTP/"


def get_header_from_environ(
environ: dict, header_name: str
Expand All @@ -42,44 +46,80 @@ def get_header_from_environ(
return []


def add_request_attributes(span, environ):
"""Adds HTTP request attributes from the PEP3333-conforming WSGI environ to span."""

span.set_attribute("component", "http")
span.set_attribute("http.method", environ["REQUEST_METHOD"])

host = environ.get("HTTP_HOST")
if not host:
host = environ["SERVER_NAME"]
port = environ["SERVER_PORT"]
scheme = environ["wsgi.url_scheme"]
if (
scheme == "http"
and port != "80"
or scheme == "https"
and port != "443"
):
host += ":" + port

# NOTE: Nonstandard (but see
# https://github.com/open-telemetry/opentelemetry-specification/pull/263)
span.set_attribute("http.host", host)

url = environ.get("REQUEST_URI") or environ.get("RAW_URI")

if url:
if url[0] == "/":
# We assume that no scheme-relative URLs will be in url here.
# After all, if a request is made to http://myserver//foo, we may get
# //foo which looks like scheme-relative but isn't.
url = environ["wsgi.url_scheme"] + "://" + host + url
elif not url.startswith(environ["wsgi.url_scheme"] + ":"):
# Something fishy is in RAW_URL. Let's fall back to request_uri()
url = wsgiref_util.request_uri(environ)
def setifnotnone(dic, key, value):
if value is not None:
dic[key] = value


def http_status_to_canonical_code(code: int, allow_redirect: bool = True):
# pylint:disable=too-many-branches,too-many-return-statements
if code < 100:
return StatusCanonicalCode.UNKNOWN
if code <= 299:
return StatusCanonicalCode.OK
if code <= 399:
if allow_redirect:
return StatusCanonicalCode.OK
return StatusCanonicalCode.DEADLINE_EXCEEDED
if code <= 499:
if code == HTTPStatus.UNAUTHORIZED:
return StatusCanonicalCode.UNAUTHENTICATED
if code == HTTPStatus.FORBIDDEN:
return StatusCanonicalCode.PERMISSION_DENIED
if code == HTTPStatus.NOT_FOUND:
return StatusCanonicalCode.NOT_FOUND
if code == HTTPStatus.TOO_MANY_REQUESTS:
return StatusCanonicalCode.RESOURCE_EXHAUSTED
return StatusCanonicalCode.INVALID_ARGUMENT
if code <= 599:
if code == HTTPStatus.NOT_IMPLEMENTED:
return StatusCanonicalCode.UNIMPLEMENTED
if code == HTTPStatus.SERVICE_UNAVAILABLE:
return StatusCanonicalCode.UNAVAILABLE
if code == HTTPStatus.GATEWAY_TIMEOUT:
return StatusCanonicalCode.DEADLINE_EXCEEDED
return StatusCanonicalCode.INTERNAL
return StatusCanonicalCode.UNKNOWN


def collect_request_attributes(environ):
"""Collects HTTP request attributes from the PEP3333-conforming
WSGI environ and returns a dictionary to be used as span creation attributes."""

result = {
"component": "http",
"http.method": environ["REQUEST_METHOD"],
"http.server_name": environ["SERVER_NAME"],
"http.scheme": environ["wsgi.url_scheme"],
"host.port": int(environ["SERVER_PORT"]),
}

setifnotnone(result, "http.host", environ.get("HTTP_HOST"))
target = environ.get("RAW_URI")
if target is None: # Note: `"" or None is None`
target = environ.get("REQUEST_URI")
if target is not None:
result["http.target"] = target
else:
url = wsgiref_util.request_uri(environ)
result["http.url"] = wsgiref_util.request_uri(environ)

remote_addr = environ.get("REMOTE_ADDR")
if remote_addr:
result[
"peer.ipv6" if ":" in remote_addr else "peer.ipv4"
] = remote_addr
remote_host = environ.get("REMOTE_HOST")
if remote_host and remote_host != remote_addr:
result["peer.hostname"] = remote_host

span.set_attribute("http.url", url)
setifnotnone(result, "peer.port", environ.get("REMOTE_PORT"))
flavor = environ.get("SERVER_PROTOCOL", "")
if flavor.upper().startswith(_HTTP_VERSION_PREFIX):
flavor = flavor[len(_HTTP_VERSION_PREFIX) :]
if flavor:
result["http.flavor"] = flavor

return result


def add_response_attributes(
Expand All @@ -94,9 +134,15 @@ def add_response_attributes(
try:
status_code = int(status_code)
except ValueError:
pass
span.set_status(
Status(
StatusCanonicalCode.UNKNOWN,
"Non-integer HTTP status: " + repr(status_code),
)
)
else:
span.set_attribute("http.status_code", status_code)
span.set_status(Status(http_status_to_canonical_code(status_code)))


def get_default_span_name(environ):
Expand Down Expand Up @@ -145,20 +191,23 @@ def __call__(self, environ, start_response):
span_name = get_default_span_name(environ)

span = tracer.create_span(
span_name, parent_span, kind=trace.SpanKind.SERVER
span_name,
parent_span,
kind=trace.SpanKind.SERVER,
attributes=collect_request_attributes(environ),
)
span.start(start_timestamp)

try:
with tracer.use_span(span):
add_request_attributes(span, environ)
start_response = self._create_start_response(
span, start_response
)

iterable = self.wsgi(environ, start_response)
return _end_span_after_iterating(iterable, span, tracer)
except: # noqa
# TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292)
span.end()
raise

Expand Down
Loading

0 comments on commit 9c61977

Please sign in to comment.