Repository: open-telemetry/opentelemetry-python-contrib

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.


Discussion:

Resolving patch:
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 @@ async def wrapped_send(message):
                         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)


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
--- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
+++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
@@ -146,6 +146,7 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
                     SpanAttributes.HTTP_URL: "http://127.0.0.1/",
                     SpanAttributes.NET_PEER_IP: "127.0.0.1",
                     SpanAttributes.NET_PEER_PORT: 32767,
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
                 },
             },
         ]
@@ -303,15 +304,57 @@ def test_websocket(self):
         span_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(span_list), 6)
         expected = [
-            "/ asgi.websocket.receive",
-            "/ asgi.websocket.send",
-            "/ asgi.websocket.receive",
-            "/ asgi.websocket.send",
-            "/ asgi.websocket.receive",
-            "/ asgi",
+            {
+                "name": "/ asgi.websocket.receive",
+                "kind": trace_api.SpanKind.INTERNAL,
+                "attributes": {"type": "websocket.connect"},
+            },
+            {
+                "name": "/ asgi.websocket.send",
+                "kind": trace_api.SpanKind.INTERNAL,
+                "attributes": {"type": "websocket.accept"},
+            },
+            {
+                "name": "/ asgi.websocket.receive",
+                "kind": trace_api.SpanKind.INTERNAL,
+                "attributes": {
+                    "type": "websocket.receive",
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                },
+            },
+            {
+                "name": "/ asgi.websocket.send",
+                "kind": trace_api.SpanKind.INTERNAL,
+                "attributes": {
+                    "type": "websocket.send",
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                },
+            },
+            {
+                "name": "/ asgi.websocket.receive",
+                "kind": trace_api.SpanKind.INTERNAL,
+                "attributes": {"type": "websocket.disconnect"},
+            },
+            {
+                "name": "/ asgi",
+                "kind": trace_api.SpanKind.SERVER,
+                "attributes": {
+                    SpanAttributes.HTTP_SCHEME: self.scope["scheme"],
+                    SpanAttributes.NET_HOST_PORT: self.scope["server"][1],
+                    SpanAttributes.HTTP_HOST: self.scope["server"][0],
+                    SpanAttributes.HTTP_FLAVOR: self.scope["http_version"],
+                    SpanAttributes.HTTP_TARGET: self.scope["path"],
+                    SpanAttributes.HTTP_URL: f'{self.scope["scheme"]}://{self.scope["server"][0]}{self.scope["path"]}',
+                    SpanAttributes.NET_PEER_IP: self.scope["client"][0],
+                    SpanAttributes.NET_PEER_PORT: self.scope["client"][1],
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                },
+            },
         ]
-        actual = [span.name for span in span_list]
-        self.assertListEqual(actual, expected)
+        for span, expected in zip(span_list, expected):
+            self.assertEqual(span.name, expected["name"])
+            self.assertEqual(span.kind, expected["kind"])
+            self.assertDictEqual(dict(span.attributes), expected["attributes"])
 
     def test_lifespan(self):
         self.scope["type"] = "lifespan"