Skip to content

Commit 4de385e

Browse files
committed
Merge branch 'master' into named-tracers-merge
2 parents 725bb16 + b25af7f commit 4de385e

File tree

15 files changed

+786
-187
lines changed

15 files changed

+786
-187
lines changed

.flake8

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ 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
910
.hg
1011
.svn
1112
.tox
1213
CVS
14+
.venv*/
15+
venv*/
1316
target
1417
__pycache__
1518
ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# This file controls who is tagged for review for any given pull request.
33

44
# For anything not explicitly taken by someone else:
5-
* @a-feld @c24t @carlosalberto @lzchen @Oberon00 @reyang @toumorokoshi
5+
* @open-telemetry/python-approvers

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ lib
2020
lib64
2121
__pycache__
2222
venv*/
23+
.venv*/
2324

2425
# Installer logs
2526
pip-log.txt

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ line_length=79
1313
; docs: https://github.com/timothycrosley/isort#multi-line-output-modes
1414
multi_line_output=3
1515
skip=target
16-
skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/*
16+
skip_glob=ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/*,.venv*/*,venv*/*
1717
known_first_party=opentelemetry

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ during their normal contribution hours.
2828

2929
## Development
3030

31-
This project uses [`tox`](https://tox.readthedocs.io) to automate some aspects
31+
To quickly get up and running, you can use the `scripts/eachdist.py` tool that
32+
ships with this project. First create a virtualenv and activate it.
33+
Then run `python scripts/eachdist.py develop` to install all required packages
34+
as well as the project's packages themselves (in `--editable` mode).
35+
You can then run `scripts/eachdist.py test` to test everything or
36+
`scripts/eachdist.py lint` to lint everything (fixing anything that is auto-fixable).
37+
38+
Additionally, this project uses [`tox`](https://tox.readthedocs.io) to automate some aspects
3239
of development, including testing against multiple Python versions.
3340

3441
You can run:

dev-requirements.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pylint~=2.3
2+
flake8~=3.7
3+
isort~=4.3
4+
black>=19.3b0,==19.*
5+
mypy==0.740
6+
sphinx~=2.1
7+
sphinx-rtd-theme~=0.4
8+
sphinx-autodoc-typehints~=1.10.2
9+
pytest!=5.2.3
10+
pytest-cov>=2.8

eachdist.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# These will be sorted first in that order.
2+
# All packages that are depended upon by others should be listed here.
3+
[DEFAULT]
4+
sortfirst=
5+
opentelemetry-api
6+
opentelemetry-sdk
7+
ext/opentelemetry-ext-wsgi
8+
ext/*
9+
10+
[lintroots]
11+
extraroots=examples/*,scripts/
12+
subglob=*.py,tests/,test/,src/*,examples/*
13+
14+
[testroots]
15+
extraroots=examples/*,tests/
16+
subglob=tests/,test/

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,21 @@ def _before_flask_request():
6565
"opentelemetry-ext-flask", __version__
6666
)
6767

68+
attributes = otel_wsgi.collect_request_attributes(environ)
69+
if flask_request.url_rule:
70+
# For 404 that result from no route found, etc, we don't have a url_rule.
71+
attributes["http.route"] = flask_request.url_rule.rule
6872
span = tracer.start_span(
6973
span_name,
7074
parent_span,
7175
kind=trace.SpanKind.SERVER,
76+
attributes=attributes,
7277
start_time=environ.get(_ENVIRON_STARTTIME_KEY),
7378
)
7479
activation = tracer.use_span(span, end_on_exit=True)
7580
activation.__enter__()
7681
environ[_ENVIRON_ACTIVATION_KEY] = activation
7782
environ[_ENVIRON_SPAN_KEY] = span
78-
otel_wsgi.add_request_attributes(span, environ)
79-
if flask_request.url_rule:
80-
# For 404 that result from no route found, etc, we don't have a url_rule.
81-
span.set_attribute("http.route", flask_request.url_rule.rule)
8283

8384

8485
def _teardown_flask_request(exc):

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

Lines changed: 34 additions & 22 deletions
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-testutil/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ package_dir=
3838
=src
3939
packages=find_namespace:
4040
install_requires =
41-
opentelemetry-ext-wsgi
41+
opentelemetry-api
4242

4343
[options.extras_require]
4444
test = flask~=1.0

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

Lines changed: 87 additions & 39 deletions
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__
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):
@@ -144,18 +189,21 @@ def __call__(self, environ, start_response):
144189
span_name = get_default_span_name(environ)
145190

146191
span = self.tracer.start_span(
147-
span_name, parent_span, kind=trace.SpanKind.SERVER
192+
span_name,
193+
parent_span,
194+
kind=trace.SpanKind.SERVER,
195+
attributes=collect_request_attributes(environ),
148196
)
149197

150198
try:
151199
with self.tracer.use_span(span):
152-
add_request_attributes(span, environ)
153200
start_response = self._create_start_response(
154201
span, start_response
155202
)
156203
iterable = self.wsgi(environ, start_response)
157204
return _end_span_after_iterating(iterable, span, self.tracer)
158205
except: # noqa
206+
# TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292)
159207
span.end()
160208
raise
161209

0 commit comments

Comments
 (0)