Repository: open-telemetry/opentelemetry-python

Issue:
ext/boto and ext/botocore: fails to export spans via jaeger
**Describe your environment**
```
opentelemetry-api = "=0.9b0"
opentelemetry-ext-boto = "=0.9b0"
opentelemetry-ext-botocore = "=0.9b0"
opentelemetry-ext-dbapi = "=0.9b0"
opentelemetry-ext-jaeger = "=0.9b0"
opentelemetry-ext-psycopg2= "=0.9b0"
opentelemetry-ext-requests = "=0.9b0"
opentelemetry-instrumentation = "=0.9b0"
opentelemetry-sdk = "=0.9b0"
python = "=3.7.4"
```

**Steps to reproduce**
Install and enable ext/boto with a jaeger exporter configured

**What is the expected behavior?**
Spans are exported via jaeger

**What is the actual behavior?**
```
Traceback (most recent call last):
  File "/opt/cadre/web/.venv/lib/python3.7/site-packages/opentelemetry/sdk/trace/export/__init__.py", line 80, in on_end
    self.span_exporter.export((span,))
  File "/opt/cadre/web/.venv/lib/python3.7/site-packages/opentelemetry/ext/jaeger/__init__.py", line 156, in export
    jaeger_spans = _translate_to_jaeger(spans)
  File "/opt/cadre/web/.venv/lib/python3.7/site-packages/opentelemetry/ext/jaeger/__init__.py", line 200, in _translate_to_jaeger
    tags.extend(_extract_tags(span.resource.labels))
AttributeError: 'str' object has no attribute 'labels'
```

**Additional context**
Span creation code stomps the resource field with a string, which is not the expected type of `Resource`:
- https://github.com/open-telemetry/opentelemetry-python/blob/0ce8cb69261adcbfb7e6ac1758248947ac2c8a68/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py#L126
- https://github.com/open-telemetry/opentelemetry-python/blob/0ce8cb69261adcbfb7e6ac1758248947ac2c8a68/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py#L101

Not sure what the intent is here, but maybe resource should be an attribute instead?



Discussion:

Resolving patch:
diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py
--- a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py
+++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py
@@ -52,6 +52,7 @@
 
 from opentelemetry.ext.boto.version import __version__
 from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.sdk.trace import Resource
 from opentelemetry.trace import SpanKind, get_tracer
 
 logger = logging.getLogger(__name__)
@@ -123,9 +124,14 @@ def _common_request(  # pylint: disable=too-many-locals
         ) as span:
             if args:
                 http_method = args[0]
-                span.resource = "%s.%s" % (endpoint_name, http_method.lower())
+                span.resource = Resource(
+                    labels={
+                        "endpoint": endpoint_name,
+                        "http_method": http_method.lower(),
+                    }
+                )
             else:
-                span.resource = endpoint_name
+                span.resource = Resource(labels={"endpoint": endpoint_name})
 
             add_span_arg_tags(
                 span, endpoint_name, args, args_name, traced_args,
diff --git a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py
--- a/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py
+++ b/ext/opentelemetry-ext-botocore/src/opentelemetry/ext/botocore/__init__.py
@@ -58,6 +58,7 @@
 
 from opentelemetry.ext.botocore.version import __version__
 from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.sdk.trace import Resource
 from opentelemetry.trace import SpanKind, get_tracer
 
 logger = logging.getLogger(__name__)
@@ -98,10 +99,15 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
             operation = None
             if args:
                 operation = args[0]
-                span.resource = "%s.%s" % (endpoint_name, operation.lower())
+                span.resource = Resource(
+                    labels={
+                        "endpoint": endpoint_name,
+                        "operation": operation.lower(),
+                    }
+                )
 
             else:
-                span.resource = endpoint_name
+                span.resource = Resource(labels={"endpoint": endpoint_name})
 
             add_span_arg_tags(
                 span,


Test patch:
diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py
--- a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py
+++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py
@@ -27,6 +27,7 @@
 )
 
 from opentelemetry.ext.boto import BotoInstrumentor
+from opentelemetry.sdk.resources import Resource
 from opentelemetry.test.test_base import TestBase
 
 
@@ -69,7 +70,12 @@ def test_ec2_client(self):
         span = spans[1]
         self.assertEqual(span.attributes["aws.operation"], "RunInstances")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "ec2.runinstances")
+        self.assertEqual(
+            span.resource,
+            Resource(
+                labels={"endpoint": "ec2", "http_method": "runinstances"}
+            ),
+        )
         self.assertEqual(span.attributes["http.method"], "POST")
         self.assertEqual(span.attributes["aws.region"], "us-west-2")
         self.assertEqual(span.name, "ec2.command")
@@ -123,7 +129,10 @@ def test_s3_client(self):
         self.assertEqual(len(spans), 3)
         span = spans[2]
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "s3.head")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "s3", "http_method": "head"}),
+        )
         self.assertEqual(span.attributes["http.method"], "HEAD")
         self.assertEqual(span.attributes["aws.operation"], "head_bucket")
         self.assertEqual(span.name, "s3.command")
@@ -135,7 +144,10 @@ def test_s3_client(self):
             spans = self.memory_exporter.get_finished_spans()
             assert spans
             span = spans[2]
-            self.assertEqual(span.resource, "s3.head")
+            self.assertEqual(
+                span.resource,
+                Resource(labels={"endpoint": "s3", "http_method": "head"}),
+            )
 
     @mock_s3_deprecated
     def test_s3_put(self):
@@ -152,15 +164,24 @@ def test_s3_put(self):
         self.assertEqual(len(spans), 3)
         self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket")
         assert_span_http_status_code(spans[0], 200)
-        self.assertEqual(spans[0].resource, "s3.put")
+        self.assertEqual(
+            spans[0].resource,
+            Resource(labels={"endpoint": "s3", "http_method": "put"}),
+        )
         # get bucket
         self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket")
-        self.assertEqual(spans[1].resource, "s3.head")
+        self.assertEqual(
+            spans[1].resource,
+            Resource(labels={"endpoint": "s3", "http_method": "head"}),
+        )
         # put object
         self.assertEqual(
             spans[2].attributes["aws.operation"], "_send_file_internal"
         )
-        self.assertEqual(spans[2].resource, "s3.put")
+        self.assertEqual(
+            spans[2].resource,
+            Resource(labels={"endpoint": "s3", "http_method": "put"}),
+        )
 
     @mock_lambda_deprecated
     def test_unpatch(self):
@@ -200,7 +221,10 @@ def test_lambda_client(self):
         self.assertEqual(len(spans), 2)
         span = spans[0]
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "lambda.get")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "lambda", "http_method": "get"}),
+        )
         self.assertEqual(span.attributes["http.method"], "GET")
         self.assertEqual(span.attributes["aws.region"], "us-east-2")
         self.assertEqual(span.attributes["aws.operation"], "list_functions")
@@ -214,7 +238,12 @@ def test_sts_client(self):
         spans = self.memory_exporter.get_finished_spans()
         assert spans
         span = spans[0]
-        self.assertEqual(span.resource, "sts.getfederationtoken")
+        self.assertEqual(
+            span.resource,
+            Resource(
+                labels={"endpoint": "sts", "http_method": "getfederationtoken"}
+            ),
+        )
         self.assertEqual(span.attributes["aws.region"], "us-west-2")
         self.assertEqual(
             span.attributes["aws.operation"], "GetFederationToken"
@@ -238,5 +267,7 @@ def test_elasticache_client(self):
         spans = self.memory_exporter.get_finished_spans()
         assert spans
         span = spans[0]
-        self.assertEqual(span.resource, "elasticache")
+        self.assertEqual(
+            span.resource, Resource(labels={"endpoint": "elasticcache"})
+        )
         self.assertEqual(span.attributes["aws.region"], "us-west-2")
diff --git a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py
--- a/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py
+++ b/ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py
@@ -10,6 +10,7 @@
 )
 
 from opentelemetry.ext.botocore import BotocoreInstrumentor
+from opentelemetry.sdk.resources import Resource
 from opentelemetry.test.test_base import TestBase
 
 
@@ -49,7 +50,12 @@ def test_traced_client(self):
         self.assertEqual(span.attributes["aws.region"], "us-west-2")
         self.assertEqual(span.attributes["aws.operation"], "DescribeInstances")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "ec2.describeinstances")
+        self.assertEqual(
+            span.resource,
+            Resource(
+                labels={"endpoint": "ec2", "operation": "describeinstances"}
+            ),
+        )
         self.assertEqual(span.name, "ec2.command")
 
     @mock_ec2
@@ -73,7 +79,10 @@ def test_s3_client(self):
         self.assertEqual(len(spans), 2)
         self.assertEqual(span.attributes["aws.operation"], "ListBuckets")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "s3.listbuckets")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "s3", "operation": "listbuckets"}),
+        )
 
         # testing for span error
         self.memory_exporter.get_finished_spans()
@@ -82,7 +91,10 @@ def test_s3_client(self):
         spans = self.memory_exporter.get_finished_spans()
         assert spans
         span = spans[2]
-        self.assertEqual(span.resource, "s3.listobjects")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "s3", "operation": "listobjects"}),
+        )
 
     @mock_s3
     def test_s3_put(self):
@@ -97,9 +109,15 @@ def test_s3_put(self):
         self.assertEqual(len(spans), 2)
         self.assertEqual(span.attributes["aws.operation"], "CreateBucket")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "s3.createbucket")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "s3", "operation": "createbucket"}),
+        )
         self.assertEqual(spans[1].attributes["aws.operation"], "PutObject")
-        self.assertEqual(spans[1].resource, "s3.putobject")
+        self.assertEqual(
+            spans[1].resource,
+            Resource(labels={"endpoint": "s3", "operation": "putobject"}),
+        )
         self.assertEqual(spans[1].attributes["params.Key"], str(params["Key"]))
         self.assertEqual(
             spans[1].attributes["params.Bucket"], str(params["Bucket"])
@@ -119,7 +137,10 @@ def test_sqs_client(self):
         self.assertEqual(span.attributes["aws.region"], "us-east-1")
         self.assertEqual(span.attributes["aws.operation"], "ListQueues")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "sqs.listqueues")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "sqs", "operation": "listqueues"}),
+        )
 
     @mock_kinesis
     def test_kinesis_client(self):
@@ -136,7 +157,12 @@ def test_kinesis_client(self):
         self.assertEqual(span.attributes["aws.region"], "us-east-1")
         self.assertEqual(span.attributes["aws.operation"], "ListStreams")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "kinesis.liststreams")
+        self.assertEqual(
+            span.resource,
+            Resource(
+                labels={"endpoint": "kinesis", "operation": "liststreams"}
+            ),
+        )
 
     @mock_kinesis
     def test_unpatch(self):
@@ -176,7 +202,12 @@ def test_lambda_client(self):
         self.assertEqual(span.attributes["aws.region"], "us-east-1")
         self.assertEqual(span.attributes["aws.operation"], "ListFunctions")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "lambda.listfunctions")
+        self.assertEqual(
+            span.resource,
+            Resource(
+                labels={"endpoint": "lambda", "operation": "listfunctions"}
+            ),
+        )
 
     @mock_kms
     def test_kms_client(self):
@@ -191,7 +222,10 @@ def test_kms_client(self):
         self.assertEqual(span.attributes["aws.region"], "us-east-1")
         self.assertEqual(span.attributes["aws.operation"], "ListKeys")
         assert_span_http_status_code(span, 200)
-        self.assertEqual(span.resource, "kms.listkeys")
+        self.assertEqual(
+            span.resource,
+            Resource(labels={"endpoint": "kms", "operation": "listkeys"}),
+        )
 
         # checking for protection on sts against security leak
         self.assertTrue("params" not in span.attributes.keys())