We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `elasticapm/instrumentation/packages/botocore.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2019, Elasticsearch BV
4 #  All rights reserved.
5 #
6 #  Redistribution and use in source and binary forms, with or without
7 #  modification, are permitted provided that the following conditions are met:
8 #
9 #  * Redistributions of source code must retain the above copyright notice, this
10 #    list of conditions and the following disclaimer.
11 #
12 #  * Redistributions in binary form must reproduce the above copyright notice,
13 #    this list of conditions and the following disclaimer in the documentation
14 #    and/or other materials provided with the distribution.
15 #
16 #  * Neither the name of the copyright holder nor the names of its
17 #    contributors may be used to endorse or promote products derived from
18 #    this software without specific prior written permission.
19 #
20 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 
31 from collections import namedtuple
32 
33 from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
34 from elasticapm.traces import capture_span
35 from elasticapm.utils.compat import urlparse
36 
37 HandlerInfo = namedtuple("HandlerInfo", ("signature", "span_type", "span_subtype", "span_action", "context"))
38 
39 # Used for boto3 < 1.7
40 endpoint_to_service_id = {"SNS": "SNS", "S3": "S3", "DYNAMODB": "DynamoDB", "SQS": "SQS"}
41 
42 
43 class BotocoreInstrumentation(AbstractInstrumentedModule):
44     name = "botocore"
45 
46     instrument_list = [("botocore.client", "BaseClient._make_api_call")]
47 
48     def call(self, module, method, wrapped, instance, args, kwargs):
49         if "operation_name" in kwargs:
50             operation_name = kwargs["operation_name"]
51         else:
52             operation_name = args[0]
53 
54         service_model = instance.meta.service_model
55         if hasattr(service_model, "service_id"):  # added in boto3 1.7
56             service = service_model.service_id
57         else:
58             service = service_model.service_name.upper()
59             service = endpoint_to_service_id.get(service, service)
60 
61         parsed_url = urlparse.urlparse(instance.meta.endpoint_url)
62         context = {
63             "destination": {
64                 "address": parsed_url.hostname,
65                 "port": parsed_url.port,
66                 "cloud": {"region": instance.meta.region_name},
67             }
68         }
69 
70         handler_info = None
71         handler = handlers.get(service, False)
72         if handler:
73             handler_info = handler(operation_name, service, instance, args, kwargs, context)
74         if not handler_info:
75             handler_info = handle_default(operation_name, service, instance, args, kwargs, context)
76 
77         with capture_span(
78             handler_info.signature,
79             span_type=handler_info.span_type,
80             leaf=True,
81             span_subtype=handler_info.span_subtype,
82             span_action=handler_info.span_action,
83             extra=handler_info.context,
84         ):
85             return wrapped(*args, **kwargs)
86 
87 
88 def handle_s3(operation_name, service, instance, args, kwargs, context):
89     span_type = "storage"
90     span_subtype = "s3"
91     span_action = operation_name
92     if len(args) > 1 and "Bucket" in args[1]:
93         bucket = args[1]["Bucket"]
94     else:
95         # TODO handle Access Points
96         bucket = ""
97     signature = f"S3 {operation_name} {bucket}"
98 
99     context["destination"]["name"] = span_subtype
100     context["destination"]["resource"] = bucket
101     context["destination"]["service"] = {"type": span_type}
102 
103     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
104 
105 
106 def handle_dynamodb(operation_name, service, instance, args, kwargs, context):
107     span_type = "db"
108     span_subtype = "dynamodb"
109     span_action = "query"
110     if len(args) > 1 and "TableName" in args[1]:
111         table = args[1]["TableName"]
112     else:
113         table = ""
114     signature = f"DynamoDB {operation_name} {table}".rstrip()
115 
116     context["db"] = {"type": "dynamodb", "instance": instance.meta.region_name}
117     if operation_name == "Query" and len(args) > 1 and "KeyConditionExpression" in args[1]:
118         context["db"]["statement"] = args[1]["KeyConditionExpression"]
119 
120     context["destination"]["name"] = span_subtype
121     context["destination"]["resource"] = table
122     context["destination"]["service"] = {"type": span_type}
123     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
124 
125 
126 def handle_sns(operation_name, service, instance, args, kwargs, context):
127     if operation_name != "Publish":
128         # only "publish" is handled specifically, other endpoints get the default treatment
129         return False
130     span_type = "messaging"
131     span_subtype = "sns"
132     span_action = "send"
133     topic_name = ""
134     if len(args) > 1:
135         if "Name" in args[1]:
136             topic_name = args[1]["Name"]
137         if "TopicArn" in args[1]:
138             topic_name = args[1]["TopicArn"].rsplit(":", maxsplit=1)[-1]
139     signature = f"SNS {operation_name} {topic_name}".rstrip()
140     context["destination"]["name"] = span_subtype
141     context["destination"]["resource"] = f"{span_subtype}/{topic_name}" if topic_name else span_subtype
142     context["destination"]["type"] = span_type
143     return HandlerInfo(signature, span_type, span_subtype, span_action, context)
144 
145 
146 def handle_sqs(operation_name, service, instance, args, kwargs, destination):
147     pass
148 
149 
150 def handle_default(operation_name, service, instance, args, kwargs, destination):
151     span_type = "aws"
152     span_subtype = service.lower()
153     span_action = operation_name
154 
155     signature = f"{service}:{operation_name}"
156     return HandlerInfo(signature, span_type, span_subtype, span_action, destination)
157 
158 
159 handlers = {
160     "S3": handle_s3,
161     "DynamoDB": handle_dynamodb,
162     "SNS": handle_sns,
163     "default": handle_default,
164 }
165
```
--- 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/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 @@
         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 @@
     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 @@
         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 @@
     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)

{"golden_diff": "diff --git a/elasticapm/instrumentation/packages/botocore.py b/elasticapm/instrumentation/packages/botocore.py\n--- a/elasticapm/instrumentation/packages/botocore.py\n+++ b/elasticapm/instrumentation/packages/botocore.py\n@@ -96,9 +96,7 @@\n         bucket = \"\"\n     signature = f\"S3 {operation_name} {bucket}\"\n \n-    context[\"destination\"][\"name\"] = span_subtype\n-    context[\"destination\"][\"resource\"] = bucket\n-    context[\"destination\"][\"service\"] = {\"type\": span_type}\n+    context[\"destination\"][\"service\"] = {\"name\": span_subtype, \"resource\": bucket, \"type\": span_type}\n \n     return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n \n@@ -117,9 +115,7 @@\n     if operation_name == \"Query\" and len(args) > 1 and \"KeyConditionExpression\" in args[1]:\n         context[\"db\"][\"statement\"] = args[1][\"KeyConditionExpression\"]\n \n-    context[\"destination\"][\"name\"] = span_subtype\n-    context[\"destination\"][\"resource\"] = table\n-    context[\"destination\"][\"service\"] = {\"type\": span_type}\n+    context[\"destination\"][\"service\"] = {\"name\": span_subtype, \"resource\": table, \"type\": span_type}\n     return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n \n \n@@ -137,9 +133,11 @@\n         if \"TopicArn\" in args[1]:\n             topic_name = args[1][\"TopicArn\"].rsplit(\":\", maxsplit=1)[-1]\n     signature = f\"SNS {operation_name} {topic_name}\".rstrip()\n-    context[\"destination\"][\"name\"] = span_subtype\n-    context[\"destination\"][\"resource\"] = f\"{span_subtype}/{topic_name}\" if topic_name else span_subtype\n-    context[\"destination\"][\"type\"] = span_type\n+    context[\"destination\"][\"service\"] = {\n+        \"name\": span_subtype,\n+        \"resource\": f\"{span_subtype}/{topic_name}\" if topic_name else span_subtype,\n+        \"type\": span_type,\n+    }\n     return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n \n \n@@ -152,6 +150,8 @@\n     span_subtype = service.lower()\n     span_action = operation_name\n \n+    destination[\"service\"] = {\"name\": span_subtype, \"resource\": span_subtype, \"type\": span_type}\n+\n     signature = f\"{service}:{operation_name}\"\n     return HandlerInfo(signature, span_type, span_subtype, span_action, destination)\n", "issue": "Botomongo - S3 spans throwing an \"validation error: span: context: destination: service: 'name' required\"\n**Flask Python application doesn't record the SPAN with S3 call**:\r\n\r\n**To Reproduce**\r\n\r\n1. Flask Application doing S3 call\r\n2. 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\",...`\r\n\r\n**Environment (please complete the following information)**\r\n- OS: Linux\r\n- Python version: 3.6\r\n- Framework and version: Flask 1.1.2\r\n- APM Server version: v7.12.0\r\n- Agent version: 6.1.0\r\n\r\n\r\n**From the APM version 7.12 name field is required**\r\n\r\nProblem is located here:\r\nelasticapm/instrumentation/packages/botocore.py\r\n`context[\"destination\"][\"service\"] = {\"type\": span_type}`\r\nfor destination.service there is no destination.service.name element\r\n\r\nIMHO: destination.service.name should be set as in the elasticapm/instrumentation/packages/elasticsearch.py\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfrom collections import namedtuple\n\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import capture_span\nfrom elasticapm.utils.compat import urlparse\n\nHandlerInfo = namedtuple(\"HandlerInfo\", (\"signature\", \"span_type\", \"span_subtype\", \"span_action\", \"context\"))\n\n# Used for boto3 < 1.7\nendpoint_to_service_id = {\"SNS\": \"SNS\", \"S3\": \"S3\", \"DYNAMODB\": \"DynamoDB\", \"SQS\": \"SQS\"}\n\n\nclass BotocoreInstrumentation(AbstractInstrumentedModule):\n    name = \"botocore\"\n\n    instrument_list = [(\"botocore.client\", \"BaseClient._make_api_call\")]\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        if \"operation_name\" in kwargs:\n            operation_name = kwargs[\"operation_name\"]\n        else:\n            operation_name = args[0]\n\n        service_model = instance.meta.service_model\n        if hasattr(service_model, \"service_id\"):  # added in boto3 1.7\n            service = service_model.service_id\n        else:\n            service = service_model.service_name.upper()\n            service = endpoint_to_service_id.get(service, service)\n\n        parsed_url = urlparse.urlparse(instance.meta.endpoint_url)\n        context = {\n            \"destination\": {\n                \"address\": parsed_url.hostname,\n                \"port\": parsed_url.port,\n                \"cloud\": {\"region\": instance.meta.region_name},\n            }\n        }\n\n        handler_info = None\n        handler = handlers.get(service, False)\n        if handler:\n            handler_info = handler(operation_name, service, instance, args, kwargs, context)\n        if not handler_info:\n            handler_info = handle_default(operation_name, service, instance, args, kwargs, context)\n\n        with capture_span(\n            handler_info.signature,\n            span_type=handler_info.span_type,\n            leaf=True,\n            span_subtype=handler_info.span_subtype,\n            span_action=handler_info.span_action,\n            extra=handler_info.context,\n        ):\n            return wrapped(*args, **kwargs)\n\n\ndef handle_s3(operation_name, service, instance, args, kwargs, context):\n    span_type = \"storage\"\n    span_subtype = \"s3\"\n    span_action = operation_name\n    if len(args) > 1 and \"Bucket\" in args[1]:\n        bucket = args[1][\"Bucket\"]\n    else:\n        # TODO handle Access Points\n        bucket = \"\"\n    signature = f\"S3 {operation_name} {bucket}\"\n\n    context[\"destination\"][\"name\"] = span_subtype\n    context[\"destination\"][\"resource\"] = bucket\n    context[\"destination\"][\"service\"] = {\"type\": span_type}\n\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_dynamodb(operation_name, service, instance, args, kwargs, context):\n    span_type = \"db\"\n    span_subtype = \"dynamodb\"\n    span_action = \"query\"\n    if len(args) > 1 and \"TableName\" in args[1]:\n        table = args[1][\"TableName\"]\n    else:\n        table = \"\"\n    signature = f\"DynamoDB {operation_name} {table}\".rstrip()\n\n    context[\"db\"] = {\"type\": \"dynamodb\", \"instance\": instance.meta.region_name}\n    if operation_name == \"Query\" and len(args) > 1 and \"KeyConditionExpression\" in args[1]:\n        context[\"db\"][\"statement\"] = args[1][\"KeyConditionExpression\"]\n\n    context[\"destination\"][\"name\"] = span_subtype\n    context[\"destination\"][\"resource\"] = table\n    context[\"destination\"][\"service\"] = {\"type\": span_type}\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_sns(operation_name, service, instance, args, kwargs, context):\n    if operation_name != \"Publish\":\n        # only \"publish\" is handled specifically, other endpoints get the default treatment\n        return False\n    span_type = \"messaging\"\n    span_subtype = \"sns\"\n    span_action = \"send\"\n    topic_name = \"\"\n    if len(args) > 1:\n        if \"Name\" in args[1]:\n            topic_name = args[1][\"Name\"]\n        if \"TopicArn\" in args[1]:\n            topic_name = args[1][\"TopicArn\"].rsplit(\":\", maxsplit=1)[-1]\n    signature = f\"SNS {operation_name} {topic_name}\".rstrip()\n    context[\"destination\"][\"name\"] = span_subtype\n    context[\"destination\"][\"resource\"] = f\"{span_subtype}/{topic_name}\" if topic_name else span_subtype\n    context[\"destination\"][\"type\"] = span_type\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_sqs(operation_name, service, instance, args, kwargs, destination):\n    pass\n\n\ndef handle_default(operation_name, service, instance, args, kwargs, destination):\n    span_type = \"aws\"\n    span_subtype = service.lower()\n    span_action = operation_name\n\n    signature = f\"{service}:{operation_name}\"\n    return HandlerInfo(signature, span_type, span_subtype, span_action, destination)\n\n\nhandlers = {\n    \"S3\": handle_s3,\n    \"DynamoDB\": handle_dynamodb,\n    \"SNS\": handle_sns,\n    \"default\": handle_default,\n}\n", "path": "elasticapm/instrumentation/packages/botocore.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfrom collections import namedtuple\n\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import capture_span\nfrom elasticapm.utils.compat import urlparse\n\nHandlerInfo = namedtuple(\"HandlerInfo\", (\"signature\", \"span_type\", \"span_subtype\", \"span_action\", \"context\"))\n\n# Used for boto3 < 1.7\nendpoint_to_service_id = {\"SNS\": \"SNS\", \"S3\": \"S3\", \"DYNAMODB\": \"DynamoDB\", \"SQS\": \"SQS\"}\n\n\nclass BotocoreInstrumentation(AbstractInstrumentedModule):\n    name = \"botocore\"\n\n    instrument_list = [(\"botocore.client\", \"BaseClient._make_api_call\")]\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        if \"operation_name\" in kwargs:\n            operation_name = kwargs[\"operation_name\"]\n        else:\n            operation_name = args[0]\n\n        service_model = instance.meta.service_model\n        if hasattr(service_model, \"service_id\"):  # added in boto3 1.7\n            service = service_model.service_id\n        else:\n            service = service_model.service_name.upper()\n            service = endpoint_to_service_id.get(service, service)\n\n        parsed_url = urlparse.urlparse(instance.meta.endpoint_url)\n        context = {\n            \"destination\": {\n                \"address\": parsed_url.hostname,\n                \"port\": parsed_url.port,\n                \"cloud\": {\"region\": instance.meta.region_name},\n            }\n        }\n\n        handler_info = None\n        handler = handlers.get(service, False)\n        if handler:\n            handler_info = handler(operation_name, service, instance, args, kwargs, context)\n        if not handler_info:\n            handler_info = handle_default(operation_name, service, instance, args, kwargs, context)\n\n        with capture_span(\n            handler_info.signature,\n            span_type=handler_info.span_type,\n            leaf=True,\n            span_subtype=handler_info.span_subtype,\n            span_action=handler_info.span_action,\n            extra=handler_info.context,\n        ):\n            return wrapped(*args, **kwargs)\n\n\ndef handle_s3(operation_name, service, instance, args, kwargs, context):\n    span_type = \"storage\"\n    span_subtype = \"s3\"\n    span_action = operation_name\n    if len(args) > 1 and \"Bucket\" in args[1]:\n        bucket = args[1][\"Bucket\"]\n    else:\n        # TODO handle Access Points\n        bucket = \"\"\n    signature = f\"S3 {operation_name} {bucket}\"\n\n    context[\"destination\"][\"service\"] = {\"name\": span_subtype, \"resource\": bucket, \"type\": span_type}\n\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_dynamodb(operation_name, service, instance, args, kwargs, context):\n    span_type = \"db\"\n    span_subtype = \"dynamodb\"\n    span_action = \"query\"\n    if len(args) > 1 and \"TableName\" in args[1]:\n        table = args[1][\"TableName\"]\n    else:\n        table = \"\"\n    signature = f\"DynamoDB {operation_name} {table}\".rstrip()\n\n    context[\"db\"] = {\"type\": \"dynamodb\", \"instance\": instance.meta.region_name}\n    if operation_name == \"Query\" and len(args) > 1 and \"KeyConditionExpression\" in args[1]:\n        context[\"db\"][\"statement\"] = args[1][\"KeyConditionExpression\"]\n\n    context[\"destination\"][\"service\"] = {\"name\": span_subtype, \"resource\": table, \"type\": span_type}\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_sns(operation_name, service, instance, args, kwargs, context):\n    if operation_name != \"Publish\":\n        # only \"publish\" is handled specifically, other endpoints get the default treatment\n        return False\n    span_type = \"messaging\"\n    span_subtype = \"sns\"\n    span_action = \"send\"\n    topic_name = \"\"\n    if len(args) > 1:\n        if \"Name\" in args[1]:\n            topic_name = args[1][\"Name\"]\n        if \"TopicArn\" in args[1]:\n            topic_name = args[1][\"TopicArn\"].rsplit(\":\", maxsplit=1)[-1]\n    signature = f\"SNS {operation_name} {topic_name}\".rstrip()\n    context[\"destination\"][\"service\"] = {\n        \"name\": span_subtype,\n        \"resource\": f\"{span_subtype}/{topic_name}\" if topic_name else span_subtype,\n        \"type\": span_type,\n    }\n    return HandlerInfo(signature, span_type, span_subtype, span_action, context)\n\n\ndef handle_sqs(operation_name, service, instance, args, kwargs, destination):\n    pass\n\n\ndef handle_default(operation_name, service, instance, args, kwargs, destination):\n    span_type = \"aws\"\n    span_subtype = service.lower()\n    span_action = operation_name\n\n    destination[\"service\"] = {\"name\": span_subtype, \"resource\": span_subtype, \"type\": span_type}\n\n    signature = f\"{service}:{operation_name}\"\n    return HandlerInfo(signature, span_type, span_subtype, span_action, destination)\n\n\nhandlers = {\n    \"S3\": handle_s3,\n    \"DynamoDB\": handle_dynamodb,\n    \"SNS\": handle_sns,\n    \"default\": handle_default,\n}\n", "path": "elasticapm/instrumentation/packages/botocore.py"}]}