21
21
import functools
22
22
import typing
23
23
import wsgiref .util as wsgiref_util
24
+ from http import HTTPStatus
24
25
25
26
from opentelemetry import propagators , trace
26
27
from opentelemetry .ext .wsgi .version import __version__ # noqa
28
+ from opentelemetry .trace .status import Status , StatusCanonicalCode
27
29
from opentelemetry .util import time_ns
28
30
31
+ _HTTP_VERSION_PREFIX = "HTTP/"
32
+
29
33
30
34
def get_header_from_environ (
31
35
environ : dict , header_name : str
@@ -42,44 +46,80 @@ def get_header_from_environ(
42
46
return []
43
47
44
48
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
79
103
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
81
114
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
83
123
84
124
85
125
def add_response_attributes (
@@ -94,9 +134,15 @@ def add_response_attributes(
94
134
try :
95
135
status_code = int (status_code )
96
136
except ValueError :
97
- pass
137
+ span .set_status (
138
+ Status (
139
+ StatusCanonicalCode .UNKNOWN ,
140
+ "Non-integer HTTP status: " + repr (status_code ),
141
+ )
142
+ )
98
143
else :
99
144
span .set_attribute ("http.status_code" , status_code )
145
+ span .set_status (Status (http_status_to_canonical_code (status_code )))
100
146
101
147
102
148
def get_default_span_name (environ ):
@@ -145,20 +191,23 @@ def __call__(self, environ, start_response):
145
191
span_name = get_default_span_name (environ )
146
192
147
193
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 ),
149
198
)
150
199
span .start (start_timestamp )
151
200
152
201
try :
153
202
with tracer .use_span (span ):
154
- add_request_attributes (span , environ )
155
203
start_response = self ._create_start_response (
156
204
span , start_response
157
205
)
158
206
159
207
iterable = self .wsgi (environ , start_response )
160
208
return _end_span_after_iterating (iterable , span , tracer )
161
209
except : # noqa
210
+ # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292)
162
211
span .end ()
163
212
raise
164
213
0 commit comments