24
24
25
25
from opentelemetry import propagators , trace
26
26
from opentelemetry .ext .wsgi .version import __version__ # noqa
27
+ from opentelemetry .trace .status import Status , StatusCanonicalCode
28
+
29
+ _HTTP_VERSION_PREFIX = "HTTP/"
27
30
28
31
29
32
def get_header_from_environ (
@@ -41,44 +44,80 @@ def get_header_from_environ(
41
44
return []
42
45
43
46
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
78
101
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
80
112
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
82
121
83
122
84
123
def add_response_attributes (
@@ -93,9 +132,15 @@ def add_response_attributes(
93
132
try :
94
133
status_code = int (status_code )
95
134
except ValueError :
96
- pass
135
+ span .set_status (
136
+ Status (
137
+ StatusCanonicalCode .UNKNOWN ,
138
+ "Non-integer HTTP status: " + repr (status_code ),
139
+ )
140
+ )
97
141
else :
98
142
span .set_attribute ("http.status_code" , status_code )
143
+ span .set_status (Status (http_status_to_canonical_code (status_code )))
99
144
100
145
101
146
def get_default_span_name (environ ):
@@ -142,19 +187,22 @@ def __call__(self, environ, start_response):
142
187
span_name = get_default_span_name (environ )
143
188
144
189
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 ),
146
194
)
147
195
148
196
try :
149
197
with tracer .use_span (span ):
150
- add_request_attributes (span , environ )
151
198
start_response = self ._create_start_response (
152
199
span , start_response
153
200
)
154
201
155
202
iterable = self .wsgi (environ , start_response )
156
203
return _end_span_after_iterating (iterable , span , tracer )
157
204
except : # noqa
205
+ # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292)
158
206
span .end ()
159
207
raise
160
208
0 commit comments