Skip to content

Commit 889baba

Browse files
Oberon00c24t
authored andcommitted
Update WSGI & Flask integrations to follow new semantic conventions (#299)
Updates flask & WSGI integrations to follow new semantic conventions for HTTP as of #263.
1 parent 1c8b9a2 commit 889baba

File tree

5 files changed

+216
-108
lines changed

5 files changed

+216
-108
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

+5-4
Original file line numberDiff line numberDiff line change
@@ -62,20 +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.start_span(
6670
span_name,
6771
parent_span,
6872
kind=trace.SpanKind.SERVER,
73+
attributes=attributes,
6974
start_time=environ.get(_ENVIRON_STARTTIME_KEY),
7075
)
7176
activation = tracer.use_span(span, end_on_exit=True)
7277
activation.__enter__()
7378
environ[_ENVIRON_ACTIVATION_KEY] = activation
7479
environ[_ENVIRON_SPAN_KEY] = span
75-
otel_wsgi.add_request_attributes(span, environ)
76-
if flask_request.url_rule:
77-
# For 404 that result from no route found, etc, we don't have a url_rule.
78-
span.set_attribute("http.route", flask_request.url_rule.rule)
7980

8081

8182
def _teardown_flask_request(exc):

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

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

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

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

7881
def test_404(self):
@@ -84,6 +87,16 @@ def test_404(self):
8487
"/bye",
8588
trace_api.INVALID_SPAN_CONTEXT,
8689
kind=trace_api.SpanKind.SERVER,
90+
attributes={
91+
"component": "http",
92+
"http.method": "POST",
93+
"http.server_name": "localhost",
94+
"http.scheme": "http",
95+
"host.port": 80,
96+
"http.host": "localhost",
97+
"http.target": "/bye",
98+
"http.flavor": "1.1",
99+
},
87100
start_time=mock.ANY,
88101
)
89102

@@ -93,14 +106,7 @@ def test_404(self):
93106

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

106112
def test_internal_error(self):
@@ -112,6 +118,17 @@ def test_internal_error(self):
112118
"hello_endpoint",
113119
trace_api.INVALID_SPAN_CONTEXT,
114120
kind=trace_api.SpanKind.SERVER,
121+
attributes={
122+
"component": "http",
123+
"http.method": "GET",
124+
"http.server_name": "localhost",
125+
"http.scheme": "http",
126+
"host.port": 80,
127+
"http.host": "localhost",
128+
"http.target": "/hello/500",
129+
"http.flavor": "1.1",
130+
"http.route": "/hello/<int:helloid>",
131+
},
115132
start_time=mock.ANY,
116133
)
117134

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

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

+87-39
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
from opentelemetry import propagators, trace
2626
from opentelemetry.ext.wsgi.version import __version__ # noqa
27+
from opentelemetry.trace.status import Status, StatusCanonicalCode
28+
29+
_HTTP_VERSION_PREFIX = "HTTP/"
2730

2831

2932
def get_header_from_environ(
@@ -41,44 +44,80 @@ def get_header_from_environ(
4144
return []
4245

4346

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

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

83122

84123
def add_response_attributes(
@@ -93,9 +132,15 @@ def add_response_attributes(
93132
try:
94133
status_code = int(status_code)
95134
except ValueError:
96-
pass
135+
span.set_status(
136+
Status(
137+
StatusCanonicalCode.UNKNOWN,
138+
"Non-integer HTTP status: " + repr(status_code),
139+
)
140+
)
97141
else:
98142
span.set_attribute("http.status_code", status_code)
143+
span.set_status(Status(http_status_to_canonical_code(status_code)))
99144

100145

101146
def get_default_span_name(environ):
@@ -142,19 +187,22 @@ def __call__(self, environ, start_response):
142187
span_name = get_default_span_name(environ)
143188

144189
span = tracer.start_span(
145-
span_name, parent_span, kind=trace.SpanKind.SERVER
190+
span_name,
191+
parent_span,
192+
kind=trace.SpanKind.SERVER,
193+
attributes=collect_request_attributes(environ),
146194
)
147195

148196
try:
149197
with tracer.use_span(span):
150-
add_request_attributes(span, environ)
151198
start_response = self._create_start_response(
152199
span, start_response
153200
)
154201

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

0 commit comments

Comments
 (0)