Skip to content

Commit 9c61977

Browse files
committed
1 parent 693391c commit 9c61977

File tree

5 files changed

+220
-109
lines changed

5 files changed

+220
-109
lines changed

.flake8

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ignore =
33
E501 # line too long, defer to black
44
F401 # unused import, defer to pylint
55
W503 # allow line breaks after binary ops, not after
6+
E203 # allow whitespace before ':' (https://github.com/psf/black#slices)
67
exclude =
78
.bzr
89
.git

ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,21 @@ def _before_flask_request():
6262

6363
tracer = trace.tracer()
6464

65+
attributes = otel_wsgi.collect_request_attributes(environ)
66+
if flask_request.url_rule:
67+
# For 404 that result from no route found, etc, we don't have a url_rule.
68+
attributes["http.route"] = flask_request.url_rule.rule
6569
span = tracer.create_span(
66-
span_name, parent_span, kind=trace.SpanKind.SERVER
70+
span_name,
71+
parent_span,
72+
kind=trace.SpanKind.SERVER,
73+
attributes=attributes,
6774
)
6875
span.start(environ.get(_ENVIRON_STARTTIME_KEY))
6976
activation = tracer.use_span(span, end_on_exit=True)
7077
activation.__enter__()
7178
environ[_ENVIRON_ACTIVATION_KEY] = activation
7279
environ[_ENVIRON_SPAN_KEY] = span
73-
otel_wsgi.add_request_attributes(span, environ)
74-
if flask_request.url_rule:
75-
# For 404 that result from no route found, etc, we don't have a url_rule.
76-
span.set_attribute("http.route", flask_request.url_rule.rule)
7780

7881

7982
def _teardown_flask_request(exc):

ext/opentelemetry-ext-flask/tests/test_flask_integration.py

+34-22
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,25 @@ def test_simple(self):
5656
"hello_endpoint",
5757
trace_api.INVALID_SPAN_CONTEXT,
5858
kind=trace_api.SpanKind.SERVER,
59+
attributes={
60+
"component": "http",
61+
"http.method": "GET",
62+
"http.server_name": "localhost",
63+
"http.scheme": "http",
64+
"host.port": 80,
65+
"http.host": "localhost",
66+
"http.target": "/hello/123",
67+
"http.flavor": "1.1",
68+
"http.route": "/hello/<int:helloid>",
69+
},
5970
)
6071
self.assertEqual(1, self.span.start.call_count)
6172

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

6475
self.assertEqual(
6576
self.span_attrs,
66-
{
67-
"component": "http",
68-
"http.method": "GET",
69-
"http.host": "localhost",
70-
"http.url": "http://localhost/hello/123",
71-
"http.route": "/hello/<int:helloid>",
72-
"http.status_code": 200,
73-
"http.status_text": "OK",
74-
},
77+
{"http.status_code": 200, "http.status_text": "OK"},
7578
)
7679

7780
def test_404(self):
@@ -83,6 +86,16 @@ def test_404(self):
8386
"/bye",
8487
trace_api.INVALID_SPAN_CONTEXT,
8588
kind=trace_api.SpanKind.SERVER,
89+
attributes={
90+
"component": "http",
91+
"http.method": "POST",
92+
"http.server_name": "localhost",
93+
"http.scheme": "http",
94+
"host.port": 80,
95+
"http.host": "localhost",
96+
"http.target": "/bye",
97+
"http.flavor": "1.1",
98+
},
8699
)
87100
self.assertEqual(1, self.span.start.call_count)
88101

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

93106
self.assertEqual(
94107
self.span_attrs,
95-
{
96-
"component": "http",
97-
"http.method": "POST",
98-
"http.host": "localhost",
99-
"http.url": "http://localhost/bye",
100-
"http.status_code": 404,
101-
"http.status_text": "NOT FOUND",
102-
},
108+
{"http.status_code": 404, "http.status_text": "NOT FOUND"},
103109
)
104110

105111
def test_internal_error(self):
@@ -111,6 +117,17 @@ def test_internal_error(self):
111117
"hello_endpoint",
112118
trace_api.INVALID_SPAN_CONTEXT,
113119
kind=trace_api.SpanKind.SERVER,
120+
attributes={
121+
"component": "http",
122+
"http.method": "GET",
123+
"http.server_name": "localhost",
124+
"http.scheme": "http",
125+
"host.port": 80,
126+
"http.host": "localhost",
127+
"http.target": "/hello/500",
128+
"http.flavor": "1.1",
129+
"http.route": "/hello/<int:helloid>",
130+
},
114131
)
115132
self.assertEqual(1, self.span.start.call_count)
116133

@@ -121,11 +138,6 @@ def test_internal_error(self):
121138
self.assertEqual(
122139
self.span_attrs,
123140
{
124-
"component": "http",
125-
"http.method": "GET",
126-
"http.host": "localhost",
127-
"http.url": "http://localhost/hello/500",
128-
"http.route": "/hello/<int:helloid>",
129141
"http.status_code": 500,
130142
"http.status_text": "INTERNAL SERVER ERROR",
131143
},

ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py

+88-39
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@
2121
import functools
2222
import typing
2323
import wsgiref.util as wsgiref_util
24+
from http import HTTPStatus
2425

2526
from opentelemetry import propagators, trace
2627
from opentelemetry.ext.wsgi.version import __version__ # noqa
28+
from opentelemetry.trace.status import Status, StatusCanonicalCode
2729
from opentelemetry.util import time_ns
2830

31+
_HTTP_VERSION_PREFIX = "HTTP/"
32+
2933

3034
def get_header_from_environ(
3135
environ: dict, header_name: str
@@ -42,44 +46,80 @@ def get_header_from_environ(
4246
return []
4347

4448

45-
def add_request_attributes(span, environ):
46-
"""Adds HTTP request attributes from the PEP3333-conforming WSGI environ to span."""
47-
48-
span.set_attribute("component", "http")
49-
span.set_attribute("http.method", environ["REQUEST_METHOD"])
50-
51-
host = environ.get("HTTP_HOST")
52-
if not host:
53-
host = environ["SERVER_NAME"]
54-
port = environ["SERVER_PORT"]
55-
scheme = environ["wsgi.url_scheme"]
56-
if (
57-
scheme == "http"
58-
and port != "80"
59-
or scheme == "https"
60-
and port != "443"
61-
):
62-
host += ":" + port
63-
64-
# NOTE: Nonstandard (but see
65-
# https://github.com/open-telemetry/opentelemetry-specification/pull/263)
66-
span.set_attribute("http.host", host)
67-
68-
url = environ.get("REQUEST_URI") or environ.get("RAW_URI")
69-
70-
if url:
71-
if url[0] == "/":
72-
# We assume that no scheme-relative URLs will be in url here.
73-
# After all, if a request is made to http://myserver//foo, we may get
74-
# //foo which looks like scheme-relative but isn't.
75-
url = environ["wsgi.url_scheme"] + "://" + host + url
76-
elif not url.startswith(environ["wsgi.url_scheme"] + ":"):
77-
# Something fishy is in RAW_URL. Let's fall back to request_uri()
78-
url = wsgiref_util.request_uri(environ)
49+
def setifnotnone(dic, key, value):
50+
if value is not None:
51+
dic[key] = value
52+
53+
54+
def http_status_to_canonical_code(code: int, allow_redirect: bool = True):
55+
# pylint:disable=too-many-branches,too-many-return-statements
56+
if code < 100:
57+
return StatusCanonicalCode.UNKNOWN
58+
if code <= 299:
59+
return StatusCanonicalCode.OK
60+
if code <= 399:
61+
if allow_redirect:
62+
return StatusCanonicalCode.OK
63+
return StatusCanonicalCode.DEADLINE_EXCEEDED
64+
if code <= 499:
65+
if code == HTTPStatus.UNAUTHORIZED:
66+
return StatusCanonicalCode.UNAUTHENTICATED
67+
if code == HTTPStatus.FORBIDDEN:
68+
return StatusCanonicalCode.PERMISSION_DENIED
69+
if code == HTTPStatus.NOT_FOUND:
70+
return StatusCanonicalCode.NOT_FOUND
71+
if code == HTTPStatus.TOO_MANY_REQUESTS:
72+
return StatusCanonicalCode.RESOURCE_EXHAUSTED
73+
return StatusCanonicalCode.INVALID_ARGUMENT
74+
if code <= 599:
75+
if code == HTTPStatus.NOT_IMPLEMENTED:
76+
return StatusCanonicalCode.UNIMPLEMENTED
77+
if code == HTTPStatus.SERVICE_UNAVAILABLE:
78+
return StatusCanonicalCode.UNAVAILABLE
79+
if code == HTTPStatus.GATEWAY_TIMEOUT:
80+
return StatusCanonicalCode.DEADLINE_EXCEEDED
81+
return StatusCanonicalCode.INTERNAL
82+
return StatusCanonicalCode.UNKNOWN
83+
84+
85+
def collect_request_attributes(environ):
86+
"""Collects HTTP request attributes from the PEP3333-conforming
87+
WSGI environ and returns a dictionary to be used as span creation attributes."""
88+
89+
result = {
90+
"component": "http",
91+
"http.method": environ["REQUEST_METHOD"],
92+
"http.server_name": environ["SERVER_NAME"],
93+
"http.scheme": environ["wsgi.url_scheme"],
94+
"host.port": int(environ["SERVER_PORT"]),
95+
}
96+
97+
setifnotnone(result, "http.host", environ.get("HTTP_HOST"))
98+
target = environ.get("RAW_URI")
99+
if target is None: # Note: `"" or None is None`
100+
target = environ.get("REQUEST_URI")
101+
if target is not None:
102+
result["http.target"] = target
79103
else:
80-
url = wsgiref_util.request_uri(environ)
104+
result["http.url"] = wsgiref_util.request_uri(environ)
105+
106+
remote_addr = environ.get("REMOTE_ADDR")
107+
if remote_addr:
108+
result[
109+
"peer.ipv6" if ":" in remote_addr else "peer.ipv4"
110+
] = remote_addr
111+
remote_host = environ.get("REMOTE_HOST")
112+
if remote_host and remote_host != remote_addr:
113+
result["peer.hostname"] = remote_host
81114

82-
span.set_attribute("http.url", url)
115+
setifnotnone(result, "peer.port", environ.get("REMOTE_PORT"))
116+
flavor = environ.get("SERVER_PROTOCOL", "")
117+
if flavor.upper().startswith(_HTTP_VERSION_PREFIX):
118+
flavor = flavor[len(_HTTP_VERSION_PREFIX) :]
119+
if flavor:
120+
result["http.flavor"] = flavor
121+
122+
return result
83123

84124

85125
def add_response_attributes(
@@ -94,9 +134,15 @@ def add_response_attributes(
94134
try:
95135
status_code = int(status_code)
96136
except ValueError:
97-
pass
137+
span.set_status(
138+
Status(
139+
StatusCanonicalCode.UNKNOWN,
140+
"Non-integer HTTP status: " + repr(status_code),
141+
)
142+
)
98143
else:
99144
span.set_attribute("http.status_code", status_code)
145+
span.set_status(Status(http_status_to_canonical_code(status_code)))
100146

101147

102148
def get_default_span_name(environ):
@@ -145,20 +191,23 @@ def __call__(self, environ, start_response):
145191
span_name = get_default_span_name(environ)
146192

147193
span = tracer.create_span(
148-
span_name, parent_span, kind=trace.SpanKind.SERVER
194+
span_name,
195+
parent_span,
196+
kind=trace.SpanKind.SERVER,
197+
attributes=collect_request_attributes(environ),
149198
)
150199
span.start(start_timestamp)
151200

152201
try:
153202
with tracer.use_span(span):
154-
add_request_attributes(span, environ)
155203
start_response = self._create_start_response(
156204
span, start_response
157205
)
158206

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

0 commit comments

Comments
 (0)