Repository: elastic/apm-agent-python

Issue:
Botomongo - S3 spans throwing an "validation error: span: context: destination: service: 'name' required"
**Flask Python application doesn't record the SPAN with S3 call**:

**To Reproduce**

1. Flask Application doing S3 call
2. In console you will see the exception `Failed to submit message: 'HTTP 400: {"accepted":3,"errors":[{"message":"validation error: span: context: destination: service: \'name\' required",...`

**Environment (please complete the following information)**
- OS: Linux
- Python version: 3.6
- Framework and version: Flask 1.1.2
- APM Server version: v7.12.0
- Agent version: 6.1.0


**From the APM version 7.12 name field is required**

Problem is located here:
elasticapm/instrumentation/packages/botocore.py
`context["destination"]["service"] = {"type": span_type}`
for destination.service there is no destination.service.name element

IMHO: destination.service.name should be set as in the elasticapm/instrumentation/packages/elasticsearch.py


Discussion:

Resolving patch:
diff --git a/elasticapm/instrumentation/packages/botocore.py b/elasticapm/instrumentation/packages/botocore.py
--- a/elasticapm/instrumentation/packages/botocore.py
+++ b/elasticapm/instrumentation/packages/botocore.py
@@ -96,9 +96,7 @@ def handle_s3(operation_name, service, instance, args, kwargs, context):
         bucket = ""
     signature = f"S3 {operation_name} {bucket}"
 
-    context["destination"]["name"] = span_subtype
-    context["destination"]["resource"] = bucket
-    context["destination"]["service"] = {"type": span_type}
+    context["destination"]["service"] = {"name": span_subtype, "resource": bucket, "type": span_type}
 
     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
 
@@ -117,9 +115,7 @@ def handle_dynamodb(operation_name, service, instance, args, kwargs, context):
     if operation_name == "Query" and len(args) > 1 and "KeyConditionExpression" in args[1]:
         context["db"]["statement"] = args[1]["KeyConditionExpression"]
 
-    context["destination"]["name"] = span_subtype
-    context["destination"]["resource"] = table
-    context["destination"]["service"] = {"type": span_type}
+    context["destination"]["service"] = {"name": span_subtype, "resource": table, "type": span_type}
     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
 
 
@@ -137,9 +133,11 @@ def handle_sns(operation_name, service, instance, args, kwargs, context):
         if "TopicArn" in args[1]:
             topic_name = args[1]["TopicArn"].rsplit(":", maxsplit=1)[-1]
     signature = f"SNS {operation_name} {topic_name}".rstrip()
-    context["destination"]["name"] = span_subtype
-    context["destination"]["resource"] = f"{span_subtype}/{topic_name}" if topic_name else span_subtype
-    context["destination"]["type"] = span_type
+    context["destination"]["service"] = {
+        "name": span_subtype,
+        "resource": f"{span_subtype}/{topic_name}" if topic_name else span_subtype,
+        "type": span_type,
+    }
     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
 
 
@@ -152,6 +150,8 @@ def handle_default(operation_name, service, instance, args, kwargs, destination)
     span_subtype = service.lower()
     span_action = operation_name
 
+    destination["service"] = {"name": span_subtype, "resource": span_subtype, "type": span_type}
+
     signature = f"{service}:{operation_name}"
     return HandlerInfo(signature, span_type, span_subtype, span_action, destination)
 


Test patch:
diff --git a/tests/instrumentation/botocore_tests.py b/tests/instrumentation/botocore_tests.py
--- a/tests/instrumentation/botocore_tests.py
+++ b/tests/instrumentation/botocore_tests.py
@@ -104,8 +104,8 @@ def test_s3(instrument, elasticapm_client):
         assert span["context"]["destination"]["address"] == LOCALSTACK_ENDPOINT_URL.hostname
         assert span["context"]["destination"]["port"] == LOCALSTACK_ENDPOINT_URL.port
         assert span["context"]["destination"]["cloud"]["region"] == "us-east-1"
-        assert span["context"]["destination"]["name"] == "s3"
-        assert span["context"]["destination"]["resource"] == "xyz"
+        assert span["context"]["destination"]["service"]["name"] == "s3"
+        assert span["context"]["destination"]["service"]["resource"] == "xyz"
         assert span["context"]["destination"]["service"]["type"] == "storage"
     assert spans[0]["name"] == "S3 CreateBucket xyz"
     assert spans[0]["action"] == "CreateBucket"
@@ -162,9 +162,9 @@ def test_dynamodb(instrument, elasticapm_client, dynamodb):
         assert span["context"]["destination"]["address"] == LOCALSTACK_ENDPOINT_URL.hostname
         assert span["context"]["destination"]["port"] == LOCALSTACK_ENDPOINT_URL.port
         assert span["context"]["destination"]["cloud"]["region"] == "us-east-1"
-        assert span["context"]["destination"]["name"] == "dynamodb"
-        # assert span["context"]["destination"]["resource"] == "xyz"
-        # assert span["context"]["destination"]["service"]["type"] == "storage"
+        assert span["context"]["destination"]["service"]["name"] == "dynamodb"
+        assert span["context"]["destination"]["service"]["resource"] == "Movies"
+        assert span["context"]["destination"]["service"]["type"] == "db"
     assert spans[0]["name"] == "DynamoDB PutItem Movies"
     assert spans[1]["name"] == "DynamoDB Query Movies"
     assert spans[1]["context"]["db"]["statement"] == "title = :v1 and #y = :v2"
@@ -184,3 +184,9 @@ def test_sns(instrument, elasticapm_client):
     assert spans[2]["type"] == "messaging"
     assert spans[2]["subtype"] == "sns"
     assert spans[2]["action"] == "send"
+    assert spans[2]["context"]["destination"]["address"] == LOCALSTACK_ENDPOINT_URL.hostname
+    assert spans[2]["context"]["destination"]["port"] == LOCALSTACK_ENDPOINT_URL.port
+    assert spans[2]["context"]["destination"]["cloud"]["region"] == "us-east-1"
+    assert spans[2]["context"]["destination"]["service"]["name"] == "sns"
+    assert spans[2]["context"]["destination"]["service"]["resource"] == "sns/mytopic"
+    assert spans[2]["context"]["destination"]["service"]["type"] == "messaging"