We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
FastAPI Instrumentor not populating attributes required for Azure exporter


**Describe your environment** 

**Package Version - azure-monitor-opentelemetry-exporter = {version = "^1.0.0-beta.3", allow-prereleases = true}
opentelemetry-instrumentation-fastapi = "^0.19b0"
Operating System - macOS Mojave v10.14.6**
**Python Version - v3.9.0**

**Steps to reproduce**
When using the azure-monitor-opentelemetry-exporter in conjunction with opentelemetry-instrumentation-fastapi 
the response code is not populated by the exporter, raised this issue with Azure monitor team and was redirected here;

> Looking at the output, it seems like the spans are missing some attributes. This is probably an issue in the opentelemetry-instrumentation-asgi, could you file an issue here for this problem specifically? Basically, the exporter sees if http.status_code is populated in the span attributes to populate response_Code in app insights. Since the attribute is not there, this is why you are seeing 0s. Normally, it is up to the instrumentation to populate this.

[Azure monitor issue#17839](https://github.com/Azure/azure-sdk-for-python/issues/17839)

Spans obtained from console exporter: 

- one for successful - 201 request
- one for failed - 422 request
- one on exit

[json.zip](https://github.com/open-telemetry/opentelemetry-python-contrib/files/6281056/json.zip)

Screen grab from Azure App Insights indication reponse code as 0:

![image](https://user-images.githubusercontent.com/43657587/114087357-6bae9380-9879-11eb-98db-3d3b1a0c1055.png)

**What is the expected behavior?**
Compatibility with Azure monitor exporter

**Additional context**
Detailed context for this issue

[Azure monitor issue#17839](https://github.com/Azure/azure-sdk-for-python/issues/17839)

Please let me know in case of additional info required.

Thanks.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py`
Content:
```
1 # Copyright The OpenTelemetry Authors
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 """
16 The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used
17 on any ASGI framework (such as Django-channels / Quart) to track requests
18 timing through OpenTelemetry.
19 """
20 
21 import typing
22 import urllib
23 from functools import wraps
24 from typing import Tuple
25 
26 from asgiref.compatibility import guarantee_single_callable
27 
28 from opentelemetry import context, trace
29 from opentelemetry.instrumentation.asgi.version import __version__  # noqa
30 from opentelemetry.instrumentation.utils import http_status_to_status_code
31 from opentelemetry.propagate import extract
32 from opentelemetry.propagators.textmap import Getter
33 from opentelemetry.semconv.trace import SpanAttributes
34 from opentelemetry.trace.status import Status, StatusCode
35 
36 
37 class ASGIGetter(Getter):
38     def get(
39         self, carrier: dict, key: str
40     ) -> typing.Optional[typing.List[str]]:
41         """Getter implementation to retrieve a HTTP header value from the ASGI
42         scope.
43 
44         Args:
45             carrier: ASGI scope object
46             key: header name in scope
47         Returns:
48             A list with a single string with the header value if it exists,
49                 else None.
50         """
51         headers = carrier.get("headers")
52         if not headers:
53             return None
54 
55         # asgi header keys are in lower case
56         key = key.lower()
57         decoded = [
58             _value.decode("utf8")
59             for (_key, _value) in headers
60             if _key.decode("utf8") == key
61         ]
62         if not decoded:
63             return None
64         return decoded
65 
66     def keys(self, carrier: dict) -> typing.List[str]:
67         return list(carrier.keys())
68 
69 
70 asgi_getter = ASGIGetter()
71 
72 
73 def collect_request_attributes(scope):
74     """Collects HTTP request attributes from the ASGI scope and returns a
75     dictionary to be used as span creation attributes."""
76     server_host, port, http_url = get_host_port_url_tuple(scope)
77     query_string = scope.get("query_string")
78     if query_string and http_url:
79         if isinstance(query_string, bytes):
80             query_string = query_string.decode("utf8")
81         http_url = http_url + ("?" + urllib.parse.unquote(query_string))
82 
83     result = {
84         SpanAttributes.HTTP_SCHEME: scope.get("scheme"),
85         SpanAttributes.HTTP_HOST: server_host,
86         SpanAttributes.NET_HOST_PORT: port,
87         SpanAttributes.HTTP_FLAVOR: scope.get("http_version"),
88         SpanAttributes.HTTP_TARGET: scope.get("path"),
89         SpanAttributes.HTTP_URL: http_url,
90     }
91     http_method = scope.get("method")
92     if http_method:
93         result[SpanAttributes.HTTP_METHOD] = http_method
94 
95     http_host_value_list = asgi_getter.get(scope, "host")
96     if http_host_value_list:
97         result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
98             http_host_value_list
99         )
100     http_user_agent = asgi_getter.get(scope, "user-agent")
101     if http_user_agent:
102         result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0]
103 
104     if "client" in scope and scope["client"] is not None:
105         result[SpanAttributes.NET_PEER_IP] = scope.get("client")[0]
106         result[SpanAttributes.NET_PEER_PORT] = scope.get("client")[1]
107 
108     # remove None values
109     result = {k: v for k, v in result.items() if v is not None}
110 
111     return result
112 
113 
114 def get_host_port_url_tuple(scope):
115     """Returns (host, port, full_url) tuple.
116     """
117     server = scope.get("server") or ["0.0.0.0", 80]
118     port = server[1]
119     server_host = server[0] + (":" + str(port) if port != 80 else "")
120     full_path = scope.get("root_path", "") + scope.get("path", "")
121     http_url = scope.get("scheme", "http") + "://" + server_host + full_path
122     return server_host, port, http_url
123 
124 
125 def set_status_code(span, status_code):
126     """Adds HTTP response attributes to span using the status_code argument."""
127     if not span.is_recording():
128         return
129     try:
130         status_code = int(status_code)
131     except ValueError:
132         span.set_status(
133             Status(
134                 StatusCode.ERROR,
135                 "Non-integer HTTP status: " + repr(status_code),
136             )
137         )
138     else:
139         span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
140         span.set_status(Status(http_status_to_status_code(status_code)))
141 
142 
143 def get_default_span_details(scope: dict) -> Tuple[str, dict]:
144     """Default implementation for span_details_callback
145 
146     Args:
147         scope: the asgi scope dictionary
148 
149     Returns:
150         a tuple of the span, and any attributes to attach to the
151         span.
152     """
153     method_or_path = scope.get("method") or scope.get("path")
154 
155     return method_or_path, {}
156 
157 
158 class OpenTelemetryMiddleware:
159     """The ASGI application middleware.
160 
161     This class is an ASGI middleware that starts and annotates spans for any
162     requests it is invoked with.
163 
164     Args:
165         app: The ASGI application callable to forward requests to.
166         span_details_callback: Callback which should return a string
167             and a tuple, representing the desired span name and a
168             dictionary with any additional span attributes to set.
169             Optional: Defaults to get_default_span_details.
170         tracer_provider: The optional tracer provider to use. If omitted
171             the current globally configured one is used.
172     """
173 
174     def __init__(
175         self,
176         app,
177         excluded_urls=None,
178         span_details_callback=None,
179         tracer_provider=None,
180     ):
181         self.app = guarantee_single_callable(app)
182         self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)
183         self.span_details_callback = (
184             span_details_callback or get_default_span_details
185         )
186         self.excluded_urls = excluded_urls
187 
188     async def __call__(self, scope, receive, send):
189         """The ASGI application
190 
191         Args:
192             scope: A ASGI environment.
193             receive: An awaitable callable yielding dictionaries
194             send: An awaitable callable taking a single dictionary as argument.
195         """
196         if scope["type"] not in ("http", "websocket"):
197             return await self.app(scope, receive, send)
198 
199         _, _, url = get_host_port_url_tuple(scope)
200         if self.excluded_urls and self.excluded_urls.url_disabled(url):
201             return await self.app(scope, receive, send)
202 
203         token = context.attach(extract(scope, getter=asgi_getter))
204         span_name, additional_attributes = self.span_details_callback(scope)
205 
206         try:
207             with self.tracer.start_as_current_span(
208                 span_name + " asgi", kind=trace.SpanKind.SERVER,
209             ) as span:
210                 if span.is_recording():
211                     attributes = collect_request_attributes(scope)
212                     attributes.update(additional_attributes)
213                     for key, value in attributes.items():
214                         span.set_attribute(key, value)
215 
216                 @wraps(receive)
217                 async def wrapped_receive():
218                     with self.tracer.start_as_current_span(
219                         span_name + " asgi." + scope["type"] + ".receive"
220                     ) as receive_span:
221                         message = await receive()
222                         if receive_span.is_recording():
223                             if message["type"] == "websocket.receive":
224                                 set_status_code(receive_span, 200)
225                             receive_span.set_attribute("type", message["type"])
226                     return message
227 
228                 @wraps(send)
229                 async def wrapped_send(message):
230                     with self.tracer.start_as_current_span(
231                         span_name + " asgi." + scope["type"] + ".send"
232                     ) as send_span:
233                         if send_span.is_recording():
234                             if message["type"] == "http.response.start":
235                                 status_code = message["status"]
236                                 set_status_code(send_span, status_code)
237                             elif message["type"] == "websocket.send":
238                                 set_status_code(send_span, 200)
239                             send_span.set_attribute("type", message["type"])
240                         await send(message)
241 
242                 await self.app(scope, wrapped_receive, wrapped_send)
243         finally:
244             context.detach(token)
245
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
@@ -233,8 +233,10 @@
                         if send_span.is_recording():
                             if message["type"] == "http.response.start":
                                 status_code = message["status"]
+                                set_status_code(span, status_code)
                                 set_status_code(send_span, status_code)
                             elif message["type"] == "websocket.send":
+                                set_status_code(span, 200)
                                 set_status_code(send_span, 200)
                             send_span.set_attribute("type", message["type"])
                         await send(message)

{"golden_diff": "diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py\n--- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py\n+++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py\n@@ -233,8 +233,10 @@\n                         if send_span.is_recording():\n                             if message[\"type\"] == \"http.response.start\":\n                                 status_code = message[\"status\"]\n+                                set_status_code(span, status_code)\n                                 set_status_code(send_span, status_code)\n                             elif message[\"type\"] == \"websocket.send\":\n+                                set_status_code(span, 200)\n                                 set_status_code(send_span, 200)\n                             send_span.set_attribute(\"type\", message[\"type\"])\n                         await send(message)\n", "issue": "FastAPI Instrumentor not populating attributes required for Azure exporter\n\r\n\r\n**Describe your environment** \r\n\r\n**Package Version - azure-monitor-opentelemetry-exporter = {version = \"^1.0.0-beta.3\", allow-prereleases = true}\r\nopentelemetry-instrumentation-fastapi = \"^0.19b0\"\r\nOperating System - macOS Mojave v10.14.6**\r\n**Python Version - v3.9.0**\r\n\r\n**Steps to reproduce**\r\nWhen using the azure-monitor-opentelemetry-exporter in conjunction with opentelemetry-instrumentation-fastapi \r\nthe response code is not populated by the exporter, raised this issue with Azure monitor team and was redirected here;\r\n\r\n> Looking at the output, it seems like the spans are missing some attributes. This is probably an issue in the opentelemetry-instrumentation-asgi, could you file an issue here for this problem specifically? Basically, the exporter sees if http.status_code is populated in the span attributes to populate response_Code in app insights. Since the attribute is not there, this is why you are seeing 0s. Normally, it is up to the instrumentation to populate this.\r\n\r\n[Azure monitor issue#17839](https://github.com/Azure/azure-sdk-for-python/issues/17839)\r\n\r\nSpans obtained from console exporter: \r\n\r\n- one for successful - 201 request\r\n- one for failed - 422 request\r\n- one on exit\r\n\r\n[json.zip](https://github.com/open-telemetry/opentelemetry-python-contrib/files/6281056/json.zip)\r\n\r\nScreen grab from Azure App Insights indication reponse code as 0:\r\n\r\n![image](https://user-images.githubusercontent.com/43657587/114087357-6bae9380-9879-11eb-98db-3d3b1a0c1055.png)\r\n\r\n**What is the expected behavior?**\r\nCompatibility with Azure monitor exporter\r\n\r\n**Additional context**\r\nDetailed context for this issue\r\n\r\n[Azure monitor issue#17839](https://github.com/Azure/azure-sdk-for-python/issues/17839)\r\n\r\nPlease let me know in case of additional info required.\r\n\r\nThanks.\n", "before_files": [{"content": "# Copyright The OpenTelemetry Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThe opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used\non any ASGI framework (such as Django-channels / Quart) to track requests\ntiming through OpenTelemetry.\n\"\"\"\n\nimport typing\nimport urllib\nfrom functools import wraps\nfrom typing import Tuple\n\nfrom asgiref.compatibility import guarantee_single_callable\n\nfrom opentelemetry import context, trace\nfrom opentelemetry.instrumentation.asgi.version import __version__  # noqa\nfrom opentelemetry.instrumentation.utils import http_status_to_status_code\nfrom opentelemetry.propagate import extract\nfrom opentelemetry.propagators.textmap import Getter\nfrom opentelemetry.semconv.trace import SpanAttributes\nfrom opentelemetry.trace.status import Status, StatusCode\n\n\nclass ASGIGetter(Getter):\n    def get(\n        self, carrier: dict, key: str\n    ) -> typing.Optional[typing.List[str]]:\n        \"\"\"Getter implementation to retrieve a HTTP header value from the ASGI\n        scope.\n\n        Args:\n            carrier: ASGI scope object\n            key: header name in scope\n        Returns:\n            A list with a single string with the header value if it exists,\n                else None.\n        \"\"\"\n        headers = carrier.get(\"headers\")\n        if not headers:\n            return None\n\n        # asgi header keys are in lower case\n        key = key.lower()\n        decoded = [\n            _value.decode(\"utf8\")\n            for (_key, _value) in headers\n            if _key.decode(\"utf8\") == key\n        ]\n        if not decoded:\n            return None\n        return decoded\n\n    def keys(self, carrier: dict) -> typing.List[str]:\n        return list(carrier.keys())\n\n\nasgi_getter = ASGIGetter()\n\n\ndef collect_request_attributes(scope):\n    \"\"\"Collects HTTP request attributes from the ASGI scope and returns a\n    dictionary to be used as span creation attributes.\"\"\"\n    server_host, port, http_url = get_host_port_url_tuple(scope)\n    query_string = scope.get(\"query_string\")\n    if query_string and http_url:\n        if isinstance(query_string, bytes):\n            query_string = query_string.decode(\"utf8\")\n        http_url = http_url + (\"?\" + urllib.parse.unquote(query_string))\n\n    result = {\n        SpanAttributes.HTTP_SCHEME: scope.get(\"scheme\"),\n        SpanAttributes.HTTP_HOST: server_host,\n        SpanAttributes.NET_HOST_PORT: port,\n        SpanAttributes.HTTP_FLAVOR: scope.get(\"http_version\"),\n        SpanAttributes.HTTP_TARGET: scope.get(\"path\"),\n        SpanAttributes.HTTP_URL: http_url,\n    }\n    http_method = scope.get(\"method\")\n    if http_method:\n        result[SpanAttributes.HTTP_METHOD] = http_method\n\n    http_host_value_list = asgi_getter.get(scope, \"host\")\n    if http_host_value_list:\n        result[SpanAttributes.HTTP_SERVER_NAME] = \",\".join(\n            http_host_value_list\n        )\n    http_user_agent = asgi_getter.get(scope, \"user-agent\")\n    if http_user_agent:\n        result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0]\n\n    if \"client\" in scope and scope[\"client\"] is not None:\n        result[SpanAttributes.NET_PEER_IP] = scope.get(\"client\")[0]\n        result[SpanAttributes.NET_PEER_PORT] = scope.get(\"client\")[1]\n\n    # remove None values\n    result = {k: v for k, v in result.items() if v is not None}\n\n    return result\n\n\ndef get_host_port_url_tuple(scope):\n    \"\"\"Returns (host, port, full_url) tuple.\n    \"\"\"\n    server = scope.get(\"server\") or [\"0.0.0.0\", 80]\n    port = server[1]\n    server_host = server[0] + (\":\" + str(port) if port != 80 else \"\")\n    full_path = scope.get(\"root_path\", \"\") + scope.get(\"path\", \"\")\n    http_url = scope.get(\"scheme\", \"http\") + \"://\" + server_host + full_path\n    return server_host, port, http_url\n\n\ndef set_status_code(span, status_code):\n    \"\"\"Adds HTTP response attributes to span using the status_code argument.\"\"\"\n    if not span.is_recording():\n        return\n    try:\n        status_code = int(status_code)\n    except ValueError:\n        span.set_status(\n            Status(\n                StatusCode.ERROR,\n                \"Non-integer HTTP status: \" + repr(status_code),\n            )\n        )\n    else:\n        span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)\n        span.set_status(Status(http_status_to_status_code(status_code)))\n\n\ndef get_default_span_details(scope: dict) -> Tuple[str, dict]:\n    \"\"\"Default implementation for span_details_callback\n\n    Args:\n        scope: the asgi scope dictionary\n\n    Returns:\n        a tuple of the span, and any attributes to attach to the\n        span.\n    \"\"\"\n    method_or_path = scope.get(\"method\") or scope.get(\"path\")\n\n    return method_or_path, {}\n\n\nclass OpenTelemetryMiddleware:\n    \"\"\"The ASGI application middleware.\n\n    This class is an ASGI middleware that starts and annotates spans for any\n    requests it is invoked with.\n\n    Args:\n        app: The ASGI application callable to forward requests to.\n        span_details_callback: Callback which should return a string\n            and a tuple, representing the desired span name and a\n            dictionary with any additional span attributes to set.\n            Optional: Defaults to get_default_span_details.\n        tracer_provider: The optional tracer provider to use. If omitted\n            the current globally configured one is used.\n    \"\"\"\n\n    def __init__(\n        self,\n        app,\n        excluded_urls=None,\n        span_details_callback=None,\n        tracer_provider=None,\n    ):\n        self.app = guarantee_single_callable(app)\n        self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)\n        self.span_details_callback = (\n            span_details_callback or get_default_span_details\n        )\n        self.excluded_urls = excluded_urls\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"The ASGI application\n\n        Args:\n            scope: A ASGI environment.\n            receive: An awaitable callable yielding dictionaries\n            send: An awaitable callable taking a single dictionary as argument.\n        \"\"\"\n        if scope[\"type\"] not in (\"http\", \"websocket\"):\n            return await self.app(scope, receive, send)\n\n        _, _, url = get_host_port_url_tuple(scope)\n        if self.excluded_urls and self.excluded_urls.url_disabled(url):\n            return await self.app(scope, receive, send)\n\n        token = context.attach(extract(scope, getter=asgi_getter))\n        span_name, additional_attributes = self.span_details_callback(scope)\n\n        try:\n            with self.tracer.start_as_current_span(\n                span_name + \" asgi\", kind=trace.SpanKind.SERVER,\n            ) as span:\n                if span.is_recording():\n                    attributes = collect_request_attributes(scope)\n                    attributes.update(additional_attributes)\n                    for key, value in attributes.items():\n                        span.set_attribute(key, value)\n\n                @wraps(receive)\n                async def wrapped_receive():\n                    with self.tracer.start_as_current_span(\n                        span_name + \" asgi.\" + scope[\"type\"] + \".receive\"\n                    ) as receive_span:\n                        message = await receive()\n                        if receive_span.is_recording():\n                            if message[\"type\"] == \"websocket.receive\":\n                                set_status_code(receive_span, 200)\n                            receive_span.set_attribute(\"type\", message[\"type\"])\n                    return message\n\n                @wraps(send)\n                async def wrapped_send(message):\n                    with self.tracer.start_as_current_span(\n                        span_name + \" asgi.\" + scope[\"type\"] + \".send\"\n                    ) as send_span:\n                        if send_span.is_recording():\n                            if message[\"type\"] == \"http.response.start\":\n                                status_code = message[\"status\"]\n                                set_status_code(send_span, status_code)\n                            elif message[\"type\"] == \"websocket.send\":\n                                set_status_code(send_span, 200)\n                            send_span.set_attribute(\"type\", message[\"type\"])\n                        await send(message)\n\n                await self.app(scope, wrapped_receive, wrapped_send)\n        finally:\n            context.detach(token)\n", "path": "instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py"}], "after_files": [{"content": "# Copyright The OpenTelemetry Authors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nThe opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used\non any ASGI framework (such as Django-channels / Quart) to track requests\ntiming through OpenTelemetry.\n\"\"\"\n\nimport typing\nimport urllib\nfrom functools import wraps\nfrom typing import Tuple\n\nfrom asgiref.compatibility import guarantee_single_callable\n\nfrom opentelemetry import context, trace\nfrom opentelemetry.instrumentation.asgi.version import __version__  # noqa\nfrom opentelemetry.instrumentation.utils import http_status_to_status_code\nfrom opentelemetry.propagate import extract\nfrom opentelemetry.propagators.textmap import Getter\nfrom opentelemetry.semconv.trace import SpanAttributes\nfrom opentelemetry.trace.status import Status, StatusCode\n\n\nclass ASGIGetter(Getter):\n    def get(\n        self, carrier: dict, key: str\n    ) -> typing.Optional[typing.List[str]]:\n        \"\"\"Getter implementation to retrieve a HTTP header value from the ASGI\n        scope.\n\n        Args:\n            carrier: ASGI scope object\n            key: header name in scope\n        Returns:\n            A list with a single string with the header value if it exists,\n                else None.\n        \"\"\"\n        headers = carrier.get(\"headers\")\n        if not headers:\n            return None\n\n        # asgi header keys are in lower case\n        key = key.lower()\n        decoded = [\n            _value.decode(\"utf8\")\n            for (_key, _value) in headers\n            if _key.decode(\"utf8\") == key\n        ]\n        if not decoded:\n            return None\n        return decoded\n\n    def keys(self, carrier: dict) -> typing.List[str]:\n        return list(carrier.keys())\n\n\nasgi_getter = ASGIGetter()\n\n\ndef collect_request_attributes(scope):\n    \"\"\"Collects HTTP request attributes from the ASGI scope and returns a\n    dictionary to be used as span creation attributes.\"\"\"\n    server_host, port, http_url = get_host_port_url_tuple(scope)\n    query_string = scope.get(\"query_string\")\n    if query_string and http_url:\n        if isinstance(query_string, bytes):\n            query_string = query_string.decode(\"utf8\")\n        http_url = http_url + (\"?\" + urllib.parse.unquote(query_string))\n\n    result = {\n        SpanAttributes.HTTP_SCHEME: scope.get(\"scheme\"),\n        SpanAttributes.HTTP_HOST: server_host,\n        SpanAttributes.NET_HOST_PORT: port,\n        SpanAttributes.HTTP_FLAVOR: scope.get(\"http_version\"),\n        SpanAttributes.HTTP_TARGET: scope.get(\"path\"),\n        SpanAttributes.HTTP_URL: http_url,\n    }\n    http_method = scope.get(\"method\")\n    if http_method:\n        result[SpanAttributes.HTTP_METHOD] = http_method\n\n    http_host_value_list = asgi_getter.get(scope, \"host\")\n    if http_host_value_list:\n        result[SpanAttributes.HTTP_SERVER_NAME] = \",\".join(\n            http_host_value_list\n        )\n    http_user_agent = asgi_getter.get(scope, \"user-agent\")\n    if http_user_agent:\n        result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0]\n\n    if \"client\" in scope and scope[\"client\"] is not None:\n        result[SpanAttributes.NET_PEER_IP] = scope.get(\"client\")[0]\n        result[SpanAttributes.NET_PEER_PORT] = scope.get(\"client\")[1]\n\n    # remove None values\n    result = {k: v for k, v in result.items() if v is not None}\n\n    return result\n\n\ndef get_host_port_url_tuple(scope):\n    \"\"\"Returns (host, port, full_url) tuple.\n    \"\"\"\n    server = scope.get(\"server\") or [\"0.0.0.0\", 80]\n    port = server[1]\n    server_host = server[0] + (\":\" + str(port) if port != 80 else \"\")\n    full_path = scope.get(\"root_path\", \"\") + scope.get(\"path\", \"\")\n    http_url = scope.get(\"scheme\", \"http\") + \"://\" + server_host + full_path\n    return server_host, port, http_url\n\n\ndef set_status_code(span, status_code):\n    \"\"\"Adds HTTP response attributes to span using the status_code argument.\"\"\"\n    if not span.is_recording():\n        return\n    try:\n        status_code = int(status_code)\n    except ValueError:\n        span.set_status(\n            Status(\n                StatusCode.ERROR,\n                \"Non-integer HTTP status: \" + repr(status_code),\n            )\n        )\n    else:\n        span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)\n        span.set_status(Status(http_status_to_status_code(status_code)))\n\n\ndef get_default_span_details(scope: dict) -> Tuple[str, dict]:\n    \"\"\"Default implementation for span_details_callback\n\n    Args:\n        scope: the asgi scope dictionary\n\n    Returns:\n        a tuple of the span, and any attributes to attach to the\n        span.\n    \"\"\"\n    method_or_path = scope.get(\"method\") or scope.get(\"path\")\n\n    return method_or_path, {}\n\n\nclass OpenTelemetryMiddleware:\n    \"\"\"The ASGI application middleware.\n\n    This class is an ASGI middleware that starts and annotates spans for any\n    requests it is invoked with.\n\n    Args:\n        app: The ASGI application callable to forward requests to.\n        span_details_callback: Callback which should return a string\n            and a tuple, representing the desired span name and a\n            dictionary with any additional span attributes to set.\n            Optional: Defaults to get_default_span_details.\n        tracer_provider: The optional tracer provider to use. If omitted\n            the current globally configured one is used.\n    \"\"\"\n\n    def __init__(\n        self,\n        app,\n        excluded_urls=None,\n        span_details_callback=None,\n        tracer_provider=None,\n    ):\n        self.app = guarantee_single_callable(app)\n        self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)\n        self.span_details_callback = (\n            span_details_callback or get_default_span_details\n        )\n        self.excluded_urls = excluded_urls\n\n    async def __call__(self, scope, receive, send):\n        \"\"\"The ASGI application\n\n        Args:\n            scope: A ASGI environment.\n            receive: An awaitable callable yielding dictionaries\n            send: An awaitable callable taking a single dictionary as argument.\n        \"\"\"\n        if scope[\"type\"] not in (\"http\", \"websocket\"):\n            return await self.app(scope, receive, send)\n\n        _, _, url = get_host_port_url_tuple(scope)\n        if self.excluded_urls and self.excluded_urls.url_disabled(url):\n            return await self.app(scope, receive, send)\n\n        token = context.attach(extract(scope, getter=asgi_getter))\n        span_name, additional_attributes = self.span_details_callback(scope)\n\n        try:\n            with self.tracer.start_as_current_span(\n                span_name + \" asgi\", kind=trace.SpanKind.SERVER,\n            ) as span:\n                if span.is_recording():\n                    attributes = collect_request_attributes(scope)\n                    attributes.update(additional_attributes)\n                    for key, value in attributes.items():\n                        span.set_attribute(key, value)\n\n                @wraps(receive)\n                async def wrapped_receive():\n                    with self.tracer.start_as_current_span(\n                        span_name + \" asgi.\" + scope[\"type\"] + \".receive\"\n                    ) as receive_span:\n                        message = await receive()\n                        if receive_span.is_recording():\n                            if message[\"type\"] == \"websocket.receive\":\n                                set_status_code(receive_span, 200)\n                            receive_span.set_attribute(\"type\", message[\"type\"])\n                    return message\n\n                @wraps(send)\n                async def wrapped_send(message):\n                    with self.tracer.start_as_current_span(\n                        span_name + \" asgi.\" + scope[\"type\"] + \".send\"\n                    ) as send_span:\n                        if send_span.is_recording():\n                            if message[\"type\"] == \"http.response.start\":\n                                status_code = message[\"status\"]\n                                set_status_code(span, status_code)\n                                set_status_code(send_span, status_code)\n                            elif message[\"type\"] == \"websocket.send\":\n                                set_status_code(span, 200)\n                                set_status_code(send_span, 200)\n                            send_span.set_attribute(\"type\", message[\"type\"])\n                        await send(message)\n\n                await self.app(scope, wrapped_receive, wrapped_send)\n        finally:\n            context.detach(token)\n", "path": "instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py"}]}